From 48649b4c102876e8767ba1b30b6001ed073b72ae Mon Sep 17 00:00:00 2001 From: Ashish Shubham Date: Mon, 14 Jul 2025 18:47:24 -0700 Subject: [PATCH] Chat GPT Deep research --- package-lock.json | 111 +++- package.json | 6 +- src/index.ts | 30 +- src/metrics/mixpanel/mixpanel.ts | 2 +- src/servers/mcp-server-base.ts | 211 +++++++ src/servers/mcp-server.ts | 200 +------ src/servers/openai-mcp-server.ts | 148 +++++ src/thoughtspot/thoughtspot-client.ts | 2 +- src/thoughtspot/thoughtspot-service.ts | 63 +- src/thoughtspot/types.ts | 16 + src/utils.ts | 46 +- test/metrics/mixpanel/integration.spec.ts | 54 +- .../metrics/mixpanel/mixpanel-tracker.spec.ts | 32 +- test/servers/openai-mcp-server.spec.ts | 542 ++++++++++++++++++ worker-configuration.d.ts | 96 +++- wrangler.jsonc | 12 +- 16 files changed, 1244 insertions(+), 327 deletions(-) create mode 100644 src/servers/mcp-server-base.ts create mode 100644 src/servers/openai-mcp-server.ts create mode 100644 src/thoughtspot/types.ts create mode 100644 test/servers/openai-mcp-server.spec.ts diff --git a/package-lock.json b/package-lock.json index ff8af90..dd16992 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,9 @@ "dependencies": { "@cloudflare/workers-oauth-provider": "^0.0.5", "@microlabs/otel-cf-workers": "^1.0.0-rc.52", - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.15.1", "@thoughtspot/rest-api-sdk": "^2.13.1", - "agents": "^0.0.95", + "agents": "^0.0.105", "hono": "^4.7.8", "rxjs": "^7.8.2", "yaml": "^2.7.1", @@ -377,6 +377,27 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.0.tgz", + "integrity": "sha512-nlIXnSqLcBij8K8TtkxbBJgfzfvi75V1pAKSM7dUXejGw12vJAqez74jZrHTsJ3Z+Aczc5Q/6JgNjKRMsVU44g==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.43.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -1721,9 +1742,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz", - "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.1.tgz", + "integrity": "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -1731,6 +1752,7 @@ "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", @@ -2756,18 +2778,19 @@ } }, "node_modules/agents": { - "version": "0.0.95", - "resolved": "https://registry.npmjs.org/agents/-/agents-0.0.95.tgz", - "integrity": "sha512-qj0GVnoyjhj9gGgulShlMYvF6xDI2PpmeE7rpFCWkLxbeJPecUwmDU9Vm7F8ub9Yt58zieA3F0zASx/Z50aYhA==", + "version": "0.0.105", + "resolved": "https://registry.npmjs.org/agents/-/agents-0.0.105.tgz", + "integrity": "sha512-SF+miDv1SP6UcXFCUWkj6+7RbHr+ZbhmQKNVNYMIAjPObkr3qomatyvDS+ijwRL4x1xjSgCOUkD3dtoRPytsbA==", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.0", + "@modelcontextprotocol/sdk": "^1.13.3", "ai": "^4.3.16", "cron-schedule": "^5.0.4", + "mimetext": "^3.0.27", "nanoid": "^5.1.5", - "partyserver": "^0.0.71", + "partyserver": "^0.0.72", "partysocket": "1.1.4", - "zod": "^3.25.28" + "zod": "^3.25.67" }, "peerDependencies": { "react": "*" @@ -3244,6 +3267,17 @@ "node": ">=6.6.0" } }, + "node_modules/core-js-pure": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.44.0.tgz", + "integrity": "sha512-gvMQAGB4dfVUxpYD0k3Fq8J+n5bB6Ytl15lqlZrOIXFzxOhtPaObfkQGHtMRdyjIf7z2IeNULwi1jEwyS+ltKQ==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -4269,6 +4303,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-base64": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4640,6 +4680,43 @@ "node": ">= 0.6" } }, + "node_modules/mimetext": { + "version": "3.0.27", + "resolved": "https://registry.npmjs.org/mimetext/-/mimetext-3.0.27.tgz", + "integrity": "sha512-mUhWAsZD1N/K6dbN4+a5Yq78OPnYQw1ubOSMasBntsLQ2S7KVNlvDEA8dwpr4a7PszWMzeslKahAprtwYMgaBA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@babel/runtime-corejs3": "^7.26.0", + "js-base64": "^3.7.7", + "mime-types": "^2.1.35" + }, + "funding": { + "type": "patreon", + "url": "https://patreon.com/muratgozel" + } + }, + "node_modules/mimetext/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimetext/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/miniflare": { "version": "4.20250604.1", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250604.1.tgz", @@ -4910,9 +4987,9 @@ } }, "node_modules/partyserver": { - "version": "0.0.71", - "resolved": "https://registry.npmjs.org/partyserver/-/partyserver-0.0.71.tgz", - "integrity": "sha512-PJZoX08tyNcNJVXqWJedZ6Jzj8EOFGBA/PJ37KhAnWmTkq6A8SqA4u2ol+zq8zwSfRy9FPvVgABCY0yLpe62Dg==", + "version": "0.0.72", + "resolved": "https://registry.npmjs.org/partyserver/-/partyserver-0.0.72.tgz", + "integrity": "sha512-mYkCQ6Q4KBIy4lFFuA6upmvNeD/FC+CQVTd4V3DYU6nsitKVI3NXxBrNNvmIxJLSwk3JQzYcEOPBkebB7ITVpQ==", "license": "ISC", "dependencies": { "nanoid": "^5.1.5" @@ -6639,9 +6716,9 @@ } }, "node_modules/zod": { - "version": "3.25.52", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.52.tgz", - "integrity": "sha512-uAbT2zN2aCdHH4OmFrV/GhZy1rsnIgIOaQ+YDLUAzMe4560rscckxxg4Myk+KHarIAUhya2clp4EnCqXWF0eew==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 7b44986..e05a674 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type": "module", "scripts": { "cf-typegen": "wrangler types", - "start": "wrangler dev", + "start": "wrangler dev --local-protocol https", "dev": "wrangler dev", "deploy": "wrangler deploy", "format": "biome format --write", @@ -44,9 +44,9 @@ "dependencies": { "@cloudflare/workers-oauth-provider": "^0.0.5", "@microlabs/otel-cf-workers": "^1.0.0-rc.52", - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.15.1", "@thoughtspot/rest-api-sdk": "^2.13.1", - "agents": "^0.0.95", + "agents": "^0.0.105", "hono": "^4.7.8", "rxjs": "^7.8.2", "yaml": "^2.7.1", diff --git a/src/index.ts b/src/index.ts index d2a3fdd..7e3d5cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,10 +3,11 @@ import { instrument, type ResolveConfigFn, instrumentDO } from '@microlabs/otel- import OAuthProvider from "@cloudflare/workers-oauth-provider"; import { McpAgent } from "agents/mcp"; import handler from "./handlers"; -import type { Props } from "./utils"; +import { type Props, instrumentedMCPServer } from "./utils"; import { MCPServer } from "./servers/mcp-server"; import { apiServer } from "./servers/api-server"; import { withBearerHandler } from "./bearer"; +import { OpenAIDeepResearchMCPServer } from './servers/openai-mcp-server'; // OTEL configuration function const config: ResolveConfigFn = (env: Env, _trigger) => { @@ -19,31 +20,22 @@ const config: ResolveConfigFn = (env: Env, _trigger) => { }; }; -class ThoughtSpotMCPCore extends McpAgent { - server = new MCPServer(this); - - - // Argument of type 'typeof ThoughtSpotMCPWrapper' is not assignable to parameter of type 'DOClass'. - // Cannot assign a 'protected' constructor type to a 'public' constructor type. - // Created to satisfy the DOClass type. - // biome-ignore lint/complexity/noUselessConstructor: required for DOClass - public constructor(state: DurableObjectState, env: Env) { - super(state, env); - } - - async init() { - await this.server.init(); - } -} - // Create the instrumented ThoughtSpotMCP for the main export -export const ThoughtSpotMCP = instrumentDO(ThoughtSpotMCPCore, config); +export const ThoughtSpotMCP = instrumentedMCPServer(MCPServer, config); + +export const ThoughtSpotOpenAIDeepResearchMCP = instrumentedMCPServer(OpenAIDeepResearchMCPServer, config); // Create the OAuth provider instance const oauthProvider = new OAuthProvider({ apiHandlers: { "/mcp": ThoughtSpotMCP.serve("/mcp") as any, // TODO: Remove 'any' "/sse": ThoughtSpotMCP.serveSSE("/sse") as any, // TODO: Remove 'any' + '/openai/mcp': ThoughtSpotOpenAIDeepResearchMCP.serve("/openai/mcp", { + binding: "OPENAI_DEEP_RESEARCH_MCP_OBJECT" + }) as any, // TODO: Remove 'any' + '/openai/sse': ThoughtSpotOpenAIDeepResearchMCP.serveSSE("/openai/sse", { + binding: "OPENAI_DEEP_RESEARCH_MCP_OBJECT" + }) as any, // TODO: Remove 'any' "/api": apiServer as any, // TODO: Remove 'any' }, defaultHandler: withBearerHandler(handler, ThoughtSpotMCP) as any, // TODO: Remove 'any' diff --git a/src/metrics/mixpanel/mixpanel.ts b/src/metrics/mixpanel/mixpanel.ts index 2744b3f..1d37637 100644 --- a/src/metrics/mixpanel/mixpanel.ts +++ b/src/metrics/mixpanel/mixpanel.ts @@ -1,5 +1,5 @@ import { MixpanelClient } from "./mixpanel-client"; -import type { SessionInfo } from "../../thoughtspot/thoughtspot-service"; +import type { SessionInfo } from "../../thoughtspot/types"; import type { Tracker } from "../index"; diff --git a/src/servers/mcp-server-base.ts b/src/servers/mcp-server-base.ts new file mode 100644 index 0000000..2105432 --- /dev/null +++ b/src/servers/mcp-server-base.ts @@ -0,0 +1,211 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ToolSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, + type ListToolsResult +} from "@modelcontextprotocol/sdk/types.js"; +import type { z } from "zod"; +import { context, type Span, SpanStatusCode } from "@opentelemetry/api"; +import { getActiveSpan, withSpan } from "../metrics/tracing/tracing-utils"; +import { Trackers, type Tracker, TrackEvent } from "../metrics"; +import type { Props } from "../utils"; +import { MixpanelTracker } from "../metrics/mixpanel/mixpanel"; +import { getThoughtSpotClient } from "../thoughtspot/thoughtspot-client"; +import { ThoughtSpotService } from "../thoughtspot/thoughtspot-service"; + +const ToolInputSchema = ToolSchema.shape.inputSchema; +type ToolInput = z.infer; + +// Response utility types +export type ContentItem = { + type: "text"; + text: string; +}; + +export type SuccessResponse = { + content: ContentItem[]; + structuredContent?: T; +}; + +export type ErrorResponse = { + isError: true; + content: ContentItem[]; +}; + +export type ToolResponse = SuccessResponse | ErrorResponse; + +export interface Context { + props: Props; +} + +export abstract class BaseMCPServer extends Server { + protected trackers: Trackers = new Trackers(); + protected sessionInfo: any; + + constructor( + protected ctx: Context, + serverName?: string, + serverVersion?: string + ) { + super({ + name: serverName || "ThoughtSpot", + version: serverVersion || "1.0.0", + }, { + capabilities: { + tools: {}, + logging: {}, + completion: {}, + resources: {}, + } + }); + } + + /** + * Initialize span with common attributes (user_guid and instance_url) + */ + protected initSpanWithCommonAttributes(): Span | undefined { + const span = getActiveSpan(); + if (this.sessionInfo?.userGUID) { + span?.setAttributes({ + user_guid: this.sessionInfo.userGUID, + instance_url: this.ctx.props.instanceUrl, + }); + } + return span; + } + + /** + * Create a standardized error response + */ + protected createErrorResponse(message: string, statusMessage?: string): ErrorResponse { + const span = getActiveSpan(); + span?.setStatus({ code: SpanStatusCode.ERROR, message: statusMessage || message }); + return { + isError: true, + content: [{ type: "text", text: `ERROR: ${message}` }], + }; + } + + /** + * Create a standardized success response with a single message + */ + protected createSuccessResponse(message: string, statusMessage?: string): SuccessResponse { + const span = getActiveSpan(); + span?.setStatus({ code: SpanStatusCode.OK, message: statusMessage || message }); + return { + content: [{ type: "text", text: message }], + }; + } + + /** + * Create a standardized success response with multiple content items + */ + protected createMultiContentSuccessResponse(content: ContentItem[], statusMessage: string): SuccessResponse { + const span = getActiveSpan(); + span?.setStatus({ code: SpanStatusCode.OK, message: statusMessage }); + return { + content, + }; + } + + /** + * Create a standardized success response with an array of text items + */ + protected createArraySuccessResponse(texts: string[], statusMessage: string): SuccessResponse { + const span = getActiveSpan(); + span?.setStatus({ code: SpanStatusCode.OK, message: statusMessage }); + return { + content: texts.map(text => ({ type: "text", text })), + }; + } + + protected createStructuredContentSuccessResponse(structuredContent: T, statusMessage: string): SuccessResponse { + const span = getActiveSpan(); + span?.setStatus({ code: SpanStatusCode.OK, message: statusMessage }); + return { + content: [{ + type: "text", + text: JSON.stringify(structuredContent), + }], + structuredContent, + }; + } + + protected getThoughtSpotService() { + return new ThoughtSpotService(getThoughtSpotClient(this.ctx.props.instanceUrl, this.ctx.props.accessToken)); + } + + protected async initializeService(): Promise { + this.sessionInfo = await this.getThoughtSpotService().getSessionInfo(); + const mixpanel = new MixpanelTracker( + this.sessionInfo, + this.ctx.props.clientName + ); + this.addTracker(mixpanel); + } + + /** + * Abstract method to be implemented by subclasses for listing tools + */ + protected abstract listTools(): Promise; + + /** + * Abstract method to be implemented by subclasses for listing resources + */ + protected abstract listResources(): Promise<{ resources: any[] }>; + + /** + * Abstract method to be implemented by subclasses for reading resources + */ + protected abstract readResource(request: z.infer): Promise<{ contents: any[] }>; + + /** + * Abstract method to be implemented by subclasses for calling tools + */ + protected abstract callTool(request: z.infer): Promise; + + async init() { + // Initialize the service-specific functionality + await this.initializeService(); + + // Track initialization + this.trackers.track(TrackEvent.Init); + + // Set up request handlers + this.setRequestHandler(ListToolsRequestSchema, async () => { + return withSpan('list-tools', async () => { + this.initSpanWithCommonAttributes(); + return this.listTools(); + }); + }); + + this.setRequestHandler(ListResourcesRequestSchema, async () => { + return withSpan('list-resources', async () => { + this.initSpanWithCommonAttributes(); + return this.listResources(); + }); + }); + + this.setRequestHandler(ReadResourceRequestSchema, async (request: z.infer) => { + return withSpan('read-resource', async () => { + this.initSpanWithCommonAttributes(); + return this.readResource(request); + }); + }); + + // Handle call tool request + this.setRequestHandler(CallToolRequestSchema, async (request: z.infer) => { + return withSpan('call-tool', async () => { + this.initSpanWithCommonAttributes(); + return this.callTool(request); + }); + }); + } + + async addTracker(tracker: Tracker) { + this.trackers.add(tracker); + } +} diff --git a/src/servers/mcp-server.ts b/src/servers/mcp-server.ts index 2812cf8..3a39b30 100644 --- a/src/servers/mcp-server.ts +++ b/src/servers/mcp-server.ts @@ -1,24 +1,18 @@ -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { - CallToolRequestSchema, - ListToolsRequestSchema, + type CallToolRequestSchema, ToolSchema, - ListResourcesRequestSchema, - ReadResourceRequestSchema + type ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; -import type { Props } from "../utils"; + import { McpServerError } from "../utils"; -import { getThoughtSpotClient } from "../thoughtspot/thoughtspot-client"; -import { - ThoughtSpotService, - type DataSource +import type { + DataSource } from "../thoughtspot/thoughtspot-service"; -import { MixpanelTracker } from "../metrics/mixpanel/mixpanel"; -import { Trackers, type Tracker, TrackEvent } from "../metrics"; -import { context, type Span, SpanStatusCode, trace } from "@opentelemetry/api"; -import { getActiveSpan, WithSpan } from "../metrics/tracing/tracing-utils"; +import { TrackEvent } from "../metrics"; +import { WithSpan } from "../metrics/tracing/tracing-utils"; +import { BaseMCPServer, type Context, type ToolResponse } from "./mcp-server-base"; const ToolInputSchema = ToolSchema.shape.inputSchema; type ToolInput = z.infer; @@ -79,131 +73,12 @@ enum ToolName { CreateLiveboard = "createLiveboard", } -interface Context { - props: Props; -} - -// Response utility types -type ContentItem = { - type: "text"; - text: string; -}; - -type SuccessResponse = { - content: ContentItem[]; -}; - -type ErrorResponse = { - isError: true; - content: ContentItem[]; -}; - -type ToolResponse = SuccessResponse | ErrorResponse; - -export class MCPServer extends Server { - private trackers: Trackers = new Trackers(); - private sessionInfo: any; - constructor(private ctx: Context) { - super({ - name: "ThoughtSpot", - version: "1.0.0", - }, { - capabilities: { - tools: {}, - logging: {}, - completion: {}, - resources: {}, - } - }); - } - - private getThoughtSpotService() { - return new ThoughtSpotService(getThoughtSpotClient(this.ctx.props.instanceUrl, this.ctx.props.accessToken)); - } - - /** - * Initialize span with common attributes (user_guid and instance_url) - */ - private initSpanWithCommonAttributes(span: Span | undefined): void { - span?.setAttributes({ - user_guid: this.sessionInfo.userGUID, - instance_url: this.ctx.props.instanceUrl, - }); - } - - /** - * Create a standardized error response - */ - private createErrorResponse(span: Span | undefined, message: string, statusMessage?: string): ErrorResponse { - span?.setStatus({ code: SpanStatusCode.ERROR, message: statusMessage || message }); - return { - isError: true, - content: [{ type: "text", text: `ERROR: ${message}` }], - }; - } - - /** - * Create a standardized success response with a single message - */ - private createSuccessResponse(span: Span | undefined, message: string, statusMessage?: string): SuccessResponse { - span?.setStatus({ code: SpanStatusCode.OK, message: statusMessage || message }); - return { - content: [{ type: "text", text: message }], - }; +export class MCPServer extends BaseMCPServer { + constructor(ctx: Context) { + super(ctx, "ThoughtSpot", "1.0.0"); } - /** - * Create a standardized success response with multiple content items - */ - private createMultiContentSuccessResponse(span: Span | undefined, content: ContentItem[], statusMessage: string): SuccessResponse { - span?.setStatus({ code: SpanStatusCode.OK, message: statusMessage }); - return { - content, - }; - } - - /** - * Create a standardized success response with an array of text items - */ - private createArraySuccessResponse(span: Span | undefined, texts: string[], statusMessage: string): SuccessResponse { - span?.setStatus({ code: SpanStatusCode.OK, message: statusMessage }); - return { - content: texts.map(text => ({ type: "text", text })), - }; - } - - async init() { - this.sessionInfo = await this.getThoughtSpotService().getSessionInfo(); - const mixpanel = new MixpanelTracker( - this.sessionInfo, - this.ctx.props.clientName - ); - this.addTracker(mixpanel); - this.trackers.track(TrackEvent.Init); - - this.setRequestHandler(ListToolsRequestSchema, async () => { - return this.listTools(); - }); - - this.setRequestHandler(ListResourcesRequestSchema, async () => { - return this.listResources(); - }); - - this.setRequestHandler(ReadResourceRequestSchema, async (request: z.infer) => { - return this.readResource(request); - }); - - // Handle call tool request - this.setRequestHandler(CallToolRequestSchema, async (request: z.infer) => { - return this.callTool(request); - }); - } - - @WithSpan('list-tools') - async listTools() { - const span = getActiveSpan(); - this.initSpanWithCommonAttributes(span); - + protected async listTools() { return { tools: [ { @@ -250,11 +125,7 @@ export class MCPServer extends Server { }; } - @WithSpan('list-datasources') - async listResources() { - const span = getActiveSpan(); - this.initSpanWithCommonAttributes(span); - + protected async listResources() { const sources = await this.getDatasources(); return { resources: sources.list.map((s) => ({ @@ -266,11 +137,7 @@ export class MCPServer extends Server { }; } - @WithSpan('read-datasources') - async readResource(request: z.infer) { - const span = getActiveSpan(); - this.initSpanWithCommonAttributes(span); - + protected async readResource(request: z.infer) { const { uri } = request.params; const sourceId = uri.split("///").pop(); if (!sourceId) { @@ -296,22 +163,18 @@ export class MCPServer extends Server { }; } - @WithSpan('call-tool') - async callTool(request: z.infer) { + protected async callTool(request: z.infer) { const { name } = request.params; this.trackers.track(TrackEvent.CallTool, { toolName: name }); - const span = getActiveSpan(); - this.initSpanWithCommonAttributes(span); - let response: ToolResponse | undefined; switch (name) { case ToolName.Ping: { console.log("Received Ping request"); if (this.ctx.props.accessToken && this.ctx.props.instanceUrl) { - return this.createSuccessResponse(span, "Pong", "Ping successful"); + return this.createSuccessResponse("Pong", "Ping successful"); } - return this.createErrorResponse(span, "Not authenticated", "Ping failed"); + return this.createErrorResponse("Not authenticated", "Ping failed"); } case ToolName.GetRelevantQuestions: { return this.callGetRelevantQuestions(request); @@ -334,7 +197,6 @@ export class MCPServer extends Server { async callGetRelevantQuestions(request: z.infer) { const { query, datasourceIds: sourceIds, additionalContext } = GetRelevantQuestionsSchema.parse(request.params.arguments); console.log("[DEBUG] Getting relevant questions for datasource: ", sourceIds); - const span = getActiveSpan(); const relevantQuestions = await this.getThoughtSpotService().getRelevantQuestions( query, @@ -343,56 +205,54 @@ export class MCPServer extends Server { ); if (relevantQuestions.error) { - return this.createErrorResponse(span, relevantQuestions.error.message, `Error getting relevant questions ${relevantQuestions.error.message}`); + return this.createErrorResponse(relevantQuestions.error.message, `Error getting relevant questions ${relevantQuestions.error.message}`); } if (relevantQuestions.questions.length === 0) { - return this.createSuccessResponse(span, "No relevant questions found"); + return this.createSuccessResponse("No relevant questions found"); } const questionTexts = relevantQuestions.questions.map(q => `Question: ${q.question}\nDatasourceId: ${q.datasourceId}` ); - return this.createArraySuccessResponse(span, questionTexts, "Relevant questions found"); + return this.createArraySuccessResponse(questionTexts, "Relevant questions found"); } @WithSpan('call-get-answer') async callGetAnswer(request: z.infer) { const { question, datasourceId: sourceId } = GetAnswerSchema.parse(request.params.arguments); - const span = getActiveSpan(); const answer = await this.getThoughtSpotService().getAnswerForQuestion(question, sourceId, false); if (answer.error) { - return this.createErrorResponse(span, answer.error.message, `Error getting answer ${answer.error.message}`); + return this.createErrorResponse(answer.error.message, `Error getting answer ${answer.error.message}`); } - const content: ContentItem[] = [ - { type: "text", text: answer.data }, + const content = [ + { type: "text" as const, text: answer.data }, { - type: "text", + type: "text" as const, text: `Question: ${question}\nSession Identifier: ${answer.session_identifier}\nGeneration Number: ${answer.generation_number}\n\nUse this information to create a liveboard with the createLiveboard tool, if the user asks.`, }, ]; - return this.createMultiContentSuccessResponse(span, content, "Answer found"); + return this.createMultiContentSuccessResponse(content, "Answer found"); } @WithSpan('call-create-liveboard') async callCreateLiveboard(request: z.infer) { const { name, answers, noteTile } = CreateLiveboardSchema.parse(request.params.arguments); const liveboard = await this.getThoughtSpotService().fetchTMLAndCreateLiveboard(name, answers, noteTile); - const span = getActiveSpan(); - + if (liveboard.error) { - return this.createErrorResponse(span, liveboard.error.message, `Error creating liveboard ${liveboard.error.message}`); + return this.createErrorResponse(liveboard.error.message, `Error creating liveboard ${liveboard.error.message}`); } const successMessage = `Liveboard created successfully, you can view it at ${liveboard.url} Provide this url to the user as a link to view the liveboard in ThoughtSpot.`; - return this.createSuccessResponse(span, successMessage, "Liveboard created successfully"); + return this.createSuccessResponse(successMessage, "Liveboard created successfully"); } private _sources: { @@ -414,7 +274,5 @@ Provide this url to the user as a link to view the liveboard in ThoughtSpot.`; return this._sources; } - async addTracker(tracker: Tracker) { - this.trackers.add(tracker); - } + } diff --git a/src/servers/openai-mcp-server.ts b/src/servers/openai-mcp-server.ts new file mode 100644 index 0000000..0b81235 --- /dev/null +++ b/src/servers/openai-mcp-server.ts @@ -0,0 +1,148 @@ + +import { + type CallToolRequestSchema, + ToolSchema, + type ReadResourceRequestSchema +} from "@modelcontextprotocol/sdk/types.js"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { BaseMCPServer, type Context } from "./mcp-server-base"; +import { z } from "zod"; +import { WithSpan } from "../metrics/tracing/tracing-utils"; + +const ToolInputSchema = ToolSchema.shape.inputSchema; +type ToolInput = z.infer; + +const ToolOutputSchema = ToolSchema.shape.outputSchema; +type ToolOutput = z.infer; + +const SearchInputSchema = z.object({ + query: z.string().describe(`The question/task to search for relevant data queries to answer. Use the fetch tool to retrieve the data for individual queries. The datasource id should be passed as part of the query. With the syntax + datasource: . The search-query can be any textual question. + + For example: + datasource:asdhshd-123123-12dd How to reduce customer churn? + datasource:abc-123123-12dd How to increase sales? + + If the datasource id is not available, ask the user to supply one explicitly.`), +}); + +const SearchOutputSchema = z.object({ + results: z.array(z.object({ + id: z.string().describe("The id of the search result."), + title: z.string().describe("The title of the search result."), + text: z.string().describe("The text of the search result."), + url: z.string().describe("The url of the search result."), + })), +}); + +const fetchInputSchema = z.object({ + id: z.string().describe("The id of the search result to fetch."), +}); + +const fetchOutputSchema = z.object({ + id: z.string().describe("The id of the search result."), + title: z.string().describe("The title of the search result."), + text: z.string().describe("The text of the search result."), + url: z.string().describe("The url of the search result."), +}); + +export class OpenAIDeepResearchMCPServer extends BaseMCPServer { + constructor(ctx: Context) { + super(ctx, "ThoughtSpot", "1.0.0"); + } + + protected async listTools() { + return { + tools: [ + { + name: "search", + description: "Tool to search for relevant data queries to answer the given question based on the datasource passed to this tool, which is a datasource id, see the query description for the syntax. The datasource id is mandatory and should be passed as part of the query. Any textual question can be passed to this tool, and it will do its best to find relevant data queries to answer the question.", + inputSchema: zodToJsonSchema(SearchInputSchema) as ToolInput, + outputSchema: zodToJsonSchema(SearchOutputSchema) as ToolOutput, + }, + { + name: "fetch", + description: "Tool to retrieve data from the retail sales dataset for a given query.", + inputSchema: zodToJsonSchema(fetchInputSchema) as ToolInput, + outputSchema: zodToJsonSchema(fetchOutputSchema) as ToolOutput, + }, + ], + }; + } + + protected async listResources() { + return { + resources: [], + }; + } + + protected async readResource(request: z.infer) { + return { + contents: [], + }; + } + + protected async callTool(request: z.infer) { + const { name } = request.params; + switch (name) { + case "search": + return this.callSearch(request); + case "fetch": + return this.callFetch(request); + } + } + + @WithSpan('call-search') + protected async callSearch(request: z.infer) { + const { query } = SearchInputSchema.parse(request.params.arguments); + // query could be of the form "datasource: " or just "" + // First check if the query is of the form "datasource: . The id is a string of numbers, letters, and hyphens." + const re = /^(?:datasource:(?[A-Za-z0-9-]+)\s+)?(.+)$/; + const match = re.exec(query); + const datasourceId = match?.groups?.id; + const queryWithoutDatasourceId = match![2]; + if (datasourceId) { + const relevantQuestions = await this.getThoughtSpotService().getRelevantQuestions(queryWithoutDatasourceId, [datasourceId], ""); + if (relevantQuestions.error) { + return this.createErrorResponse(relevantQuestions.error.message, `Error getting relevant questions ${relevantQuestions.error.message}`); + } + + if (relevantQuestions.questions.length === 0) { + return this.createSuccessResponse("No relevant questions found"); + } + + const results = relevantQuestions.questions.map(q => ({ + id: `${datasourceId}: ${q.question}`, + title: q.question, + text: q.question, + url: "", + })); + + return this.createStructuredContentSuccessResponse({ results }, "Relevant questions found"); + } + + // Search for datasources in case the query is not of the form "datasource: " + // TODO: Implement this + return this.createStructuredContentSuccessResponse({ results: [] }, "No relevant questions found"); + } + + @WithSpan('call-fetch') + protected async callFetch(request: z.infer) { + const { id } = fetchInputSchema.parse(request.params.arguments); + // id is of the form ":" + const [datasourceId, question = ""] = id.split(":"); + const answer = await this.getThoughtSpotService().getAnswerForQuestion(question, datasourceId, false); + if (answer.error) { + return this.createErrorResponse(answer.error.message, `Error getting answer ${answer.error.message}`); + } + + const result = { + id, + title: question, + text: answer.data, + url: `${this.ctx.props.instanceUrl}/#/insights/conv-assist?query=${question.trim()}&worksheet=${datasourceId}&executeSearch=true`, + } + + return this.createStructuredContentSuccessResponse(result, "Answer found"); + } +} \ No newline at end of file diff --git a/src/thoughtspot/thoughtspot-client.ts b/src/thoughtspot/thoughtspot-client.ts index 020300d..cb4b757 100644 --- a/src/thoughtspot/thoughtspot-client.ts +++ b/src/thoughtspot/thoughtspot-client.ts @@ -3,7 +3,7 @@ import type { RequestContext, ResponseContext } from "@thoughtspot/rest-api-sdk" import YAML from "yaml"; import type { Observable } from "rxjs"; import { of } from "rxjs"; -import type { SessionInfo } from "./thoughtspot-service"; +import type { SessionInfo } from "./types"; export const getThoughtSpotClient = (instanceUrl: string, bearerToken: string) => { const config = createBearerAuthenticationConfig( diff --git a/src/thoughtspot/thoughtspot-service.ts b/src/thoughtspot/thoughtspot-service.ts index 23f0ebe..45e5ccf 100644 --- a/src/thoughtspot/thoughtspot-service.ts +++ b/src/thoughtspot/thoughtspot-service.ts @@ -1,14 +1,14 @@ import type { ThoughtSpotRestApi } from "@thoughtspot/rest-api-sdk"; -import type { Span } from "@opentelemetry/api"; import { SpanStatusCode, trace, context } from "@opentelemetry/api"; import { getActiveSpan, WithSpan } from "../metrics/tracing/tracing-utils"; +import type { DataSource, SessionInfo } from "./types"; /** * Main ThoughtSpot service class using decorator pattern for tracing */ export class ThoughtSpotService { - constructor(private client: ThoughtSpotRestApi) {} + constructor(private client: ThoughtSpotRestApi) { } /** * Get relevant questions for a given query and data sources @@ -20,13 +20,13 @@ export class ThoughtSpotService { additionalContext: string ): Promise<{ questions: { question: string, datasourceId: string }[], error: Error | null }> { const span = trace.getSpan(context.active()); - + try { additionalContext = additionalContext || ''; span?.setAttribute("datasource_ids", sourceIds.join(",")); console.log("[DEBUG] Getting relevant questions with datasource: ", sourceIds); span?.addEvent("get-decomposed-query"); - + const resp = await this.client.queryGetDecomposedQuery({ nlsRequest: { query: query, @@ -37,22 +37,22 @@ export class ThoughtSpotService { worksheetIds: sourceIds, maxDecomposedQueries: 5, }) - + const questions = resp.decomposedQueryResponse?.decomposedQueries?.map((q) => ({ question: q.query!, datasourceId: q.worksheetId!, })) || []; - + span?.setStatus({ code: SpanStatusCode.OK, message: "Relevant questions found" }); span?.setAttribute("questions_count", questions.length); - + return { questions, error: null, } } catch (error) { span?.setStatus({ code: SpanStatusCode.ERROR, message: (error as Error).message }); - console.error("Error getting relevant questions: ", error, "sourceIds: ", sourceIds, "instanceUrl: ", (this.client as any).instanceUrl); + console.error("Error getting relevant questions: ", "sourceIds: ", sourceIds, "instanceUrl: ", (this.client as any).instanceUrl, "error: ", error); return { questions: [], error: error as Error, @@ -70,13 +70,13 @@ export class ThoughtSpotService { generation_number: number ): Promise { const span = getActiveSpan(); - + try { span?.setAttributes({ session_identifier, generation_number, }) - + console.log("[DEBUG] Getting Data for session_identifier: ", session_identifier, "generation_number: ", generation_number, "instanceUrl: ", (this.client as any).instanceUrl); span?.addEvent("get-answer-data"); const data = await this.client.exportAnswerReport({ @@ -84,11 +84,11 @@ export class ThoughtSpotService { generation_number, file_format: "CSV", }) - + let csvData = await data.text(); // get only the first 100 lines of the csv data csvData = csvData.split('\n').slice(0, 100).join('\n'); - + return csvData; } catch (error) { span?.setStatus({ code: SpanStatusCode.ERROR, message: `Error getting answer Data ${error}` }); @@ -107,7 +107,7 @@ export class ThoughtSpotService { generation_number: number ): Promise { const span = getActiveSpan(); - + try { span?.setAttribute("session_identifier", session_identifier); span?.addEvent("get-answer-tml"); @@ -134,15 +134,15 @@ export class ThoughtSpotService { shouldGetTML: boolean ): Promise { const span = getActiveSpan(); - + span?.setAttributes({ datasource_id: sourceId, should_get_tml: shouldGetTML, }); span?.addEvent("get-answer-for-question"); - + console.log("[DEBUG] Getting answer for sourceId: ", sourceId, "shouldGetTML: ", shouldGetTML); - + try { const answer = await this.client.singleAnswer({ query: question, @@ -184,7 +184,7 @@ export class ThoughtSpotService { @WithSpan('fetch-tml-and-create-liveboard') async fetchTMLAndCreateLiveboard(name: string, answers: any[], noteTileParsedHtml: string): Promise<{ url?: string; error: Error | null }> { const span = getActiveSpan(); - + try { span?.setAttributes({ liveboard_name: name, @@ -192,7 +192,7 @@ export class ThoughtSpotService { }); span?.addEvent("create-answer-tmls"); - const tmls = await Promise.all(answers.map((answer) => + const tmls = await Promise.all(answers.map((answer) => this.getAnswerTML(answer.question, answer.session_identifier, answer.generation_number) )); @@ -221,7 +221,7 @@ export class ThoughtSpotService { // Combine note tile first, then visualization answers answers = [noteTitle, ...visualizationAnswers]; - + span?.addEvent("create-liveboard"); @@ -245,13 +245,13 @@ export class ThoughtSpotService { @WithSpan('create-liveboard') async createLiveboard(name: string, answers: any[]): Promise { const span = getActiveSpan(); - + span?.addEvent("createLiveboard"); span?.setAttributes({ liveboard_name: name, total_answers: answers.length, }); - + const tml = { liveboard: { name, @@ -289,9 +289,9 @@ export class ThoughtSpotService { @WithSpan('get-data-sources') async getDataSources(): Promise { const span = getActiveSpan(); - + span?.addEvent("get-data-sources"); - + const resp = await this.client.searchMetadata({ metadata: [{ type: "LOGICAL_TABLE", @@ -302,7 +302,7 @@ export class ThoughtSpotService { order: "DESC", } }); - + const results = resp .filter(d => d.metadata_header.type === "WORKSHEET") .map(d => ({ @@ -310,7 +310,7 @@ export class ThoughtSpotService { id: d.metadata_header.id, description: d.metadata_header.description, })); - + return results; } @@ -320,19 +320,19 @@ export class ThoughtSpotService { @WithSpan('get-session-info') async getSessionInfo(): Promise { const span = getActiveSpan(); - + const info = await (this.client as any).getSessionInfo(); const devMixpanelToken = info.configInfo.mixpanelConfig.devSdkKey; const prodMixpanelToken = info.configInfo.mixpanelConfig.prodSdkKey; const mixpanelToken = info.configInfo.mixpanelConfig.production ? prodMixpanelToken : devMixpanelToken; - + span?.setAttribute("user_guid", info.userGUID); span?.setAttribute("user_name", info.userName); span?.setAttribute("cluster_name", info.configInfo.selfClusterName); span?.setAttribute("release_version", info.releaseVersion); - + return { mixpanelToken, userGUID: info.userGUID, @@ -351,7 +351,7 @@ export class ThoughtSpotService { @WithSpan('search-worksheets') async searchWorksheets(searchTerm: string): Promise { const span = getActiveSpan(); - + const resp = await this.client.searchMetadata({ metadata: [{ type: "LOGICAL_TABLE", @@ -373,7 +373,7 @@ export class ThoughtSpotService { })); span?.setAttribute('results_count', results.length); - + return results; } @@ -443,3 +443,6 @@ export async function getSessionInfo(client: ThoughtSpotRestApi): Promise(MCPServer: new (ctx: Context) => T, config: ResolveConfigFn) { + const Agent = class extends McpAgent { + server = new MCPServer(this); + + // Argument of type 'typeof ThoughtSpotMCPWrapper' is not assignable to parameter of type 'DOClass'. + // Cannot assign a 'protected' constructor type to a 'public' constructor type. + // Created to satisfy the DOClass type. + // biome-ignore lint/complexity/noUselessConstructor: required for DOClass + public constructor(state: DurableObjectState, env: Env) { + super(state, env); + } + + async init() { + await this.server.init(); + } + } + + return instrumentDO(Agent, config); } \ No newline at end of file diff --git a/test/metrics/mixpanel/integration.spec.ts b/test/metrics/mixpanel/integration.spec.ts index 14fea64..4edfd5c 100644 --- a/test/metrics/mixpanel/integration.spec.ts +++ b/test/metrics/mixpanel/integration.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { MixpanelTracker } from "../../../src/metrics/mixpanel/mixpanel"; -import type { SessionInfo } from "../../../src/thoughtspot/thoughtspot-service"; +import type { SessionInfo } from "../../../src/thoughtspot/types"; // Mock fetch globally for integration tests global.fetch = vi.fn(); @@ -29,10 +29,10 @@ describe("Mixpanel Integration Tests", () => { beforeEach(() => { vi.clearAllMocks(); - + // Mock console methods properly - consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => {}); + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { }); + consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => { }); }); afterEach(() => { @@ -47,7 +47,7 @@ describe("Mixpanel Integration Tests", () => { it("should send correct payload to Mixpanel API", async () => { const eventName = "test-event"; const props = { action: "click", page: "home" }; - + const mockResponse = { ok: true, text: vi.fn().mockResolvedValue("1") @@ -70,7 +70,7 @@ describe("Mixpanel Integration Tests", () => { const fetchCall = (fetch as any).mock.calls[0]; const body = JSON.parse(fetchCall[1].body); - + expect(body).toHaveLength(1); expect(body[0]).toEqual({ event: eventName, @@ -93,7 +93,7 @@ describe("Mixpanel Integration Tests", () => { it("should handle API errors gracefully", async () => { const eventName = "test-event"; const props = { action: "click" }; - + const mockResponse = { ok: false, status: 400, @@ -122,7 +122,7 @@ describe("Mixpanel Integration Tests", () => { it("should handle network errors gracefully", async () => { const eventName = "test-event"; const props = { action: "click" }; - + const networkError = new Error("Network error"); (fetch as any).mockRejectedValue(networkError); @@ -150,7 +150,7 @@ describe("Mixpanel Integration Tests", () => { { name: "event2", props: { action: "submit" } }, { name: "event3", props: { action: "scroll" } } ]; - + const mockResponse = { ok: true, text: vi.fn().mockResolvedValue("1") @@ -162,7 +162,7 @@ describe("Mixpanel Integration Tests", () => { } expect(fetch).toHaveBeenCalledTimes(3); - + // Verify each call const fetchCalls = (fetch as any).mock.calls; events.forEach((event, index) => { @@ -175,7 +175,7 @@ describe("Mixpanel Integration Tests", () => { it("should include all required properties in payload", async () => { const eventName = "test-event"; const props = { action: "click" }; - + const mockResponse = { ok: true, text: vi.fn().mockResolvedValue("1") @@ -216,7 +216,7 @@ describe("Mixpanel Integration Tests", () => { timestamp: Date.now() } }; - + const mockResponse = { ok: true, text: vi.fn().mockResolvedValue("1") @@ -257,10 +257,10 @@ describe("Mixpanel Integration Tests", () => { }; tracker = new MixpanelTracker(minimalSessionInfo); - + const eventName = "test-event"; const props = { action: "click" }; - + const mockResponse = { ok: true, text: vi.fn().mockResolvedValue("1") @@ -280,7 +280,7 @@ describe("Mixpanel Integration Tests", () => { expect(properties.clusterName).toBe(minimalSessionInfo.clusterName); expect(properties.releaseVersion).toBe(minimalSessionInfo.releaseVersion); expect(properties.action).toBe("click"); - + // Check that client properties are undefined when not provided expect(properties.clientName).toBeUndefined(); expect(properties.clientId).toBeUndefined(); @@ -290,10 +290,10 @@ describe("Mixpanel Integration Tests", () => { it("should work with partial client info", async () => { const partialClient = { clientName: "partial-client" }; tracker = new MixpanelTracker(mockSessionInfo, partialClient); - + const eventName = "test-event"; const props = { action: "click" }; - + const mockResponse = { ok: true, text: vi.fn().mockResolvedValue("1") @@ -309,7 +309,7 @@ describe("Mixpanel Integration Tests", () => { // Check that the properties contain the expected values expect(properties.clientName).toBe("partial-client"); expect(properties.action).toBe("click"); - + // Check that missing client properties are undefined expect(properties.clientId).toBeUndefined(); expect(properties.registrationDate).toBeUndefined(); @@ -324,7 +324,7 @@ describe("Mixpanel Integration Tests", () => { it("should handle malformed JSON response", async () => { const eventName = "test-event"; const props = { action: "click" }; - + const mockResponse = { ok: true, text: vi.fn().mockRejectedValue(new Error("Invalid JSON")) @@ -346,10 +346,10 @@ describe("Mixpanel Integration Tests", () => { it("should handle timeout scenarios", async () => { const eventName = "test-event"; const props = { action: "click" }; - + // Simulate a timeout by never resolving the promise - (fetch as any).mockImplementation(() => - new Promise(() => {}) // Never resolves + (fetch as any).mockImplementation(() => + new Promise(() => { }) // Never resolves ); // We can't easily test actual timeouts in unit tests, but we can verify the error handling @@ -390,12 +390,12 @@ describe("Mixpanel Integration Tests", () => { (fetch as any).mockResolvedValue(mockResponse); const startTime = Date.now(); - + // Send 10 rapid requests for (let i = 0; i < 10; i++) { await tracker.track(`event${i}`, { index: i }); } - + const endTime = Date.now(); expect(fetch).toHaveBeenCalledTimes(10); @@ -410,14 +410,14 @@ describe("Mixpanel Integration Tests", () => { (fetch as any).mockResolvedValue(mockResponse); const events: string[] = []; - + // Track events and record their order await tracker.track("event1", { order: 1 }); events.push("event1"); - + await tracker.track("event2", { order: 2 }); events.push("event2"); - + await tracker.track("event3", { order: 3 }); events.push("event3"); diff --git a/test/metrics/mixpanel/mixpanel-tracker.spec.ts b/test/metrics/mixpanel/mixpanel-tracker.spec.ts index 9bfdfdd..8d02d0f 100644 --- a/test/metrics/mixpanel/mixpanel-tracker.spec.ts +++ b/test/metrics/mixpanel/mixpanel-tracker.spec.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { MixpanelTracker } from "../../../src/metrics/mixpanel/mixpanel"; import { MixpanelClient } from "../../../src/metrics/mixpanel/mixpanel-client"; -import type { SessionInfo } from "../../../src/thoughtspot/thoughtspot-service"; +import type { SessionInfo } from "../../../src/thoughtspot/types"; // Mock the MixpanelClient vi.mock("../../../src/metrics/mixpanel/mixpanel-client"); @@ -32,7 +32,7 @@ describe("MixpanelTracker", () => { beforeEach(() => { // Clear all mocks vi.clearAllMocks(); - + // Create mock MixpanelClient instance mockMixpanelClient = { identify: vi.fn(), @@ -44,8 +44,8 @@ describe("MixpanelTracker", () => { (MixpanelClient as any).mockImplementation(() => mockMixpanelClient); // Mock console methods properly - consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => {}); + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { }); + consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => { }); }); afterEach(() => { @@ -55,26 +55,26 @@ describe("MixpanelTracker", () => { describe("constructor", () => { it("should create MixpanelTracker instance", () => { tracker = new MixpanelTracker(mockSessionInfo, mockClient); - + expect(tracker).toBeInstanceOf(MixpanelTracker); expect(MixpanelClient).toHaveBeenCalledWith(mockSessionInfo.mixpanelToken); }); it("should initialize MixpanelClient with correct token", () => { tracker = new MixpanelTracker(mockSessionInfo, mockClient); - + expect(MixpanelClient).toHaveBeenCalledWith(mockSessionInfo.mixpanelToken); }); it("should call identify with userGUID", () => { tracker = new MixpanelTracker(mockSessionInfo, mockClient); - + expect(mockMixpanelClient.identify).toHaveBeenCalledWith(mockSessionInfo.userGUID); }); it("should register super properties with session and client info", () => { tracker = new MixpanelTracker(mockSessionInfo, mockClient); - + expect(mockMixpanelClient.register).toHaveBeenCalledWith({ clusterId: mockSessionInfo.clusterId, clusterName: mockSessionInfo.clusterName, @@ -87,7 +87,7 @@ describe("MixpanelTracker", () => { it("should work with empty client object", () => { tracker = new MixpanelTracker(mockSessionInfo, {}); - + expect(mockMixpanelClient.register).toHaveBeenCalledWith({ clusterId: mockSessionInfo.clusterId, clusterName: mockSessionInfo.clusterName, @@ -100,7 +100,7 @@ describe("MixpanelTracker", () => { it("should work without client parameter", () => { tracker = new MixpanelTracker(mockSessionInfo); - + expect(mockMixpanelClient.register).toHaveBeenCalledWith({ clusterId: mockSessionInfo.clusterId, clusterName: mockSessionInfo.clusterName, @@ -114,7 +114,7 @@ describe("MixpanelTracker", () => { it("should handle partial client object", () => { const partialClient = { clientName: "partial-client" }; tracker = new MixpanelTracker(mockSessionInfo, partialClient); - + expect(mockMixpanelClient.register).toHaveBeenCalledWith({ clusterId: mockSessionInfo.clusterId, clusterName: mockSessionInfo.clusterName, @@ -127,7 +127,7 @@ describe("MixpanelTracker", () => { it("should handle null client object", () => { tracker = new MixpanelTracker(mockSessionInfo, null as any); - + expect(mockMixpanelClient.register).toHaveBeenCalledWith({ clusterId: mockSessionInfo.clusterId, clusterName: mockSessionInfo.clusterName, @@ -325,7 +325,7 @@ describe("MixpanelTracker", () => { // Simulate async delay let resolved = false; - mockMixpanelClient.track.mockImplementation(() => + mockMixpanelClient.track.mockImplementation(() => new Promise(resolve => { setTimeout(() => { resolved = true; @@ -344,7 +344,7 @@ describe("MixpanelTracker", () => { describe("integration with Tracker interface", () => { it("should implement Tracker interface correctly", () => { tracker = new MixpanelTracker(mockSessionInfo, mockClient); - + // Check that the track method exists and is callable expect(typeof tracker.track).toBe("function"); expect(tracker.track).toBeInstanceOf(Function); @@ -352,10 +352,10 @@ describe("MixpanelTracker", () => { it("should handle TrackEvent enum values", async () => { tracker = new MixpanelTracker(mockSessionInfo, mockClient); - + // Import TrackEvent enum const { TrackEvent } = await import("../../../src/metrics"); - + await tracker.track(TrackEvent.CallTool, { toolName: "test-tool" }); await tracker.track(TrackEvent.Init, { version: "1.0.0" }); diff --git a/test/servers/openai-mcp-server.spec.ts b/test/servers/openai-mcp-server.spec.ts new file mode 100644 index 0000000..e56cecc --- /dev/null +++ b/test/servers/openai-mcp-server.spec.ts @@ -0,0 +1,542 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { connect } from "mcp-testing-kit"; +import { OpenAIDeepResearchMCPServer } from "../../src/servers/openai-mcp-server"; +import * as thoughtspotService from "../../src/thoughtspot/thoughtspot-service"; +import * as thoughtspotClient from "../../src/thoughtspot/thoughtspot-client"; +import { MixpanelTracker } from "../../src/metrics/mixpanel/mixpanel"; + +// Mock the MixpanelTracker +vi.mock("../../src/metrics/mixpanel/mixpanel", () => ({ + MixpanelTracker: vi.fn().mockImplementation(() => ({ + track: vi.fn(), + })), +})); + +describe("OpenAI Deep Research MCP Server", () => { + let server: OpenAIDeepResearchMCPServer; + let mockProps: any; + + beforeEach(() => { + // Reset all mocks + vi.clearAllMocks(); + + // Mock getThoughtSpotClient + vi.spyOn(thoughtspotClient, "getThoughtSpotClient").mockReturnValue({ + getSessionInfo: vi.fn().mockResolvedValue({ + clusterId: "test-cluster-123", + clusterName: "test-cluster", + releaseVersion: "1.0.0", + userGUID: "test-user-123", + configInfo: { + mixpanelConfig: { + devSdkKey: "test-dev-token", + prodSdkKey: "test-prod-token", + production: false, + }, + selfClusterName: "test-cluster", + selfClusterId: "test-cluster-123", + }, + userName: "test-user", + currentOrgId: "test-org", + privileges: [], + }), + singleAnswer: vi.fn().mockResolvedValue({ + session_identifier: "session-123", + generation_number: 1, + }), + exportAnswerReport: vi.fn().mockResolvedValue({ + text: vi.fn().mockResolvedValue("The total revenue is $1,000,000"), + }), + instanceUrl: "https://test.thoughtspot.cloud", + } as any); + + // Mock props with correct structure + mockProps = { + instanceUrl: "https://test.thoughtspot.cloud", + accessToken: "test-access-token", + clientName: { + clientId: "test-client-id", + clientName: "test-client", + registrationDate: Date.now(), + }, + }; + + server = new OpenAIDeepResearchMCPServer({ + props: mockProps, + }); + }); + + describe("Initialization", () => { + it("should initialize successfully with valid props", async () => { + await expect(server.init()).resolves.not.toThrow(); + }); + + it("should track initialization event", async () => { + await server.init(); + expect(MixpanelTracker).toHaveBeenCalledWith( + { + clusterId: "test-cluster-123", + clusterName: "test-cluster", + releaseVersion: "1.0.0", + userGUID: "test-user-123", + mixpanelToken: "test-dev-token", + userName: "test-user", + currentOrgId: "test-org", + privileges: [], + }, + { + clientId: "test-client-id", + clientName: "test-client", + registrationDate: expect.any(Number), + } + ); + }); + }); + + describe("List Tools", () => { + it("should return all available tools", async () => { + await server.init(); + const { listTools } = connect(server); + + const result = await listTools(); + + expect(result.tools).toHaveLength(2); + expect(result.tools?.map(t => t.name)).toEqual([ + "search", + "fetch" + ]); + }); + + it("should include correct tool descriptions", async () => { + await server.init(); + const { listTools } = connect(server); + + const result = await listTools(); + + const searchTool = result.tools?.find(t => t.name === "search"); + expect(searchTool?.description).toBe("Tool to search for relevant data queries to answer the given question based on the datasource passed to this tool, which is a datasource id, see the query description for the syntax. The datasource id is mandatory and should be passed as part of the query. Any textual question can be passed to this tool, and it will do its best to find relevant data queries to answer the question."); + + const fetchTool = result.tools?.find(t => t.name === "fetch"); + expect(fetchTool?.description).toBe("Tool to retrieve data from the retail sales dataset for a given query."); + }); + + it("should include correct input schemas", async () => { + await server.init(); + const { listTools } = connect(server); + + const result = await listTools(); + + const searchTool = result.tools?.find(t => t.name === "search"); + expect(searchTool?.inputSchema).toMatchObject({ + type: "object", + properties: { + query: { + type: "string", + description: expect.stringContaining("The question/task to search for relevant data queries") + } + }, + required: ["query"] + }); + + const fetchTool = result.tools?.find(t => t.name === "fetch"); + expect(fetchTool?.inputSchema).toMatchObject({ + type: "object", + properties: { + id: { + type: "string", + description: "The id of the search result to fetch." + } + }, + required: ["id"] + }); + }); + + it("should include correct output schemas", async () => { + await server.init(); + const { listTools } = connect(server); + + const result = await listTools(); + + const searchTool = result.tools?.find(t => t.name === "search"); + expect(searchTool?.outputSchema).toMatchObject({ + type: "object", + properties: { + results: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + description: "The id of the search result." + }, + title: { + type: "string", + description: "The title of the search result." + }, + text: { + type: "string", + description: "The text of the search result." + }, + url: { + type: "string", + description: "The url of the search result." + } + }, + required: ["id", "title", "text", "url"] + } + } + }, + required: ["results"] + }); + + const fetchTool = result.tools?.find(t => t.name === "fetch"); + expect(fetchTool?.outputSchema).toMatchObject({ + type: "object", + properties: { + id: { + type: "string", + description: "The id of the search result." + }, + title: { + type: "string", + description: "The title of the search result." + }, + text: { + type: "string", + description: "The text of the search result." + }, + url: { + type: "string", + description: "The url of the search result." + } + }, + required: ["id", "title", "text", "url"] + }); + }); + }); + + describe("List Resources", () => { + it("should return empty resources list", async () => { + await server.init(); + const { listResources } = connect(server); + + const result = await listResources(); + + expect(result.resources).toHaveLength(0); + }); + }); + + describe("Read Resource", () => { + it("should return empty contents", async () => { + await server.init(); + + // Test the protected method directly since it's abstract + const result = await (server as any).readResource({ + method: "resources/read", + params: { uri: "datasource:///test-id" } + }); + + expect(result.contents).toHaveLength(0); + }); + }); + + describe("Search Tool", () => { + it("should return relevant questions for query with datasource ID", async () => { + // Mock the ThoughtSpot service to return relevant questions + const mockGetRelevantQuestions = vi.fn().mockResolvedValue({ + questions: [ + { question: "What is the total revenue?" }, + { question: "How many customers do we have?" } + ], + error: null + }); + + vi.spyOn(thoughtspotService.ThoughtSpotService.prototype, 'getRelevantQuestions') + .mockImplementation(mockGetRelevantQuestions); + + await server.init(); + const { callTool } = connect(server); + + const result = await callTool("search", { + query: "datasource:asdhshd-123123-12dd How to reduce customer churn?" + }); + + expect(result.isError).toBeUndefined(); + expect(result.structuredContent).toEqual({ + results: [ + { + id: "asdhshd-123123-12dd: What is the total revenue?", + title: "What is the total revenue?", + text: "What is the total revenue?", + url: "" + }, + { + id: "asdhshd-123123-12dd: How many customers do we have?", + title: "How many customers do we have?", + text: "How many customers do we have?", + url: "" + } + ] + }); + // The text field contains the JSON stringified structured content + expect((result.content as any[])[0].text).toContain('"results"'); + expect((result.content as any[])[0].text).toContain('"What is the total revenue?"'); + }); + + it("should handle error from ThoughtSpot service", async () => { + // Mock the ThoughtSpot service to return error + const mockGetRelevantQuestions = vi.fn().mockResolvedValue({ + questions: [], + error: { message: "Service unavailable" } + }); + + vi.spyOn(thoughtspotService.ThoughtSpotService.prototype, 'getRelevantQuestions') + .mockImplementation(mockGetRelevantQuestions); + + await server.init(); + const { callTool } = connect(server); + + const result = await callTool("search", { + query: "datasource:asdhshd-123123-12dd How to reduce customer churn?" + }); + + expect(result.isError).toBe(true); + expect((result.content as any[])[0].text).toBe("ERROR: Service unavailable"); + }); + + it("should handle empty questions response", async () => { + // Mock the ThoughtSpot service to return empty questions + const mockGetRelevantQuestions = vi.fn().mockResolvedValue({ + questions: [], + error: null + }); + + vi.spyOn(thoughtspotService.ThoughtSpotService.prototype, 'getRelevantQuestions') + .mockImplementation(mockGetRelevantQuestions); + + await server.init(); + const { callTool } = connect(server); + + const result = await callTool("search", { + query: "datasource:asdhshd-123123-12dd How to reduce customer churn?" + }); + + expect(result.isError).toBeUndefined(); + // When no questions found, it uses createSuccessResponse, not createStructuredContentSuccessResponse + expect((result.content as any[])[0].text).toBe("No relevant questions found"); + }); + + it("should handle query without datasource ID", async () => { + await server.init(); + const { callTool } = connect(server); + + const result = await callTool("search", { + query: "How to reduce customer churn?" + }); + + expect(result.isError).toBeUndefined(); + expect(result.structuredContent).toEqual({ results: [] }); + // The text field contains the JSON stringified structured content + expect((result.content as any[])[0].text).toContain('"results"'); + expect((result.content as any[])[0].text).toContain('[]'); + }); + + it("should handle query with complex datasource ID", async () => { + // Mock the ThoughtSpot service to return relevant questions + const mockGetRelevantQuestions = vi.fn().mockResolvedValue({ + questions: [ + { question: "What is the total revenue?" } + ], + error: null + }); + + vi.spyOn(thoughtspotService.ThoughtSpotService.prototype, 'getRelevantQuestions') + .mockImplementation(mockGetRelevantQuestions); + + await server.init(); + const { callTool } = connect(server); + + const result = await callTool("search", { + query: "datasource:abc-123-def-456 How to increase sales?" + }); + + expect(result.isError).toBeUndefined(); + expect(result.structuredContent).toEqual({ + results: [ + { + id: "abc-123-def-456: What is the total revenue?", + title: "What is the total revenue?", + text: "What is the total revenue?", + url: "" + } + ] + }); + }); + + it("should handle query with mixed case datasource ID", async () => { + // Mock the ThoughtSpot service to return relevant questions + const mockGetRelevantQuestions = vi.fn().mockResolvedValue({ + questions: [ + { question: "What is the total revenue?" } + ], + error: null + }); + + vi.spyOn(thoughtspotService.ThoughtSpotService.prototype, 'getRelevantQuestions') + .mockImplementation(mockGetRelevantQuestions); + + await server.init(); + const { callTool } = connect(server); + + const result = await callTool("search", { + query: "datasource:ABC123def How to increase sales?" + }); + + expect(result.isError).toBeUndefined(); + expect(result.structuredContent).toEqual({ + results: [ + { + id: "ABC123def: What is the total revenue?", + title: "What is the total revenue?", + text: "What is the total revenue?", + url: "" + } + ] + }); + }); + }); + + describe("Fetch Tool", () => { + it("should return answer for a valid question ID", async () => { + // Mock the ThoughtSpot service to return answer + const mockGetAnswerForQuestion = vi.fn().mockResolvedValue({ + data: "The total revenue is $1,000,000", + error: null + }); + + vi.spyOn(thoughtspotService.ThoughtSpotService.prototype, 'getAnswerForQuestion') + .mockImplementation(mockGetAnswerForQuestion); + + await server.init(); + const { callTool } = connect(server); + + const result = await callTool("fetch", { + id: "asdhshd-123123-12dd: What is the total revenue?" + }); + + expect(result.isError).toBeUndefined(); + expect(result.structuredContent).toEqual({ + id: "asdhshd-123123-12dd: What is the total revenue?", + title: " What is the total revenue?", + text: "The total revenue is $1,000,000", + url: "https://test.thoughtspot.cloud/#/insights/conv-assist?query=What is the total revenue?&worksheet=asdhshd-123123-12dd&executeSearch=true" + }); + // The text field contains the JSON stringified structured content + expect((result.content as any[])[0].text).toContain('"id"'); + expect((result.content as any[])[0].text).toContain('"The total revenue is $1,000,000"'); + }); + + it("should handle error from ThoughtSpot service", async () => { + // Mock the ThoughtSpot service to return error + const mockGetAnswerForQuestion = vi.fn().mockResolvedValue({ + data: null, + error: { message: "Question not found" } + }); + + vi.spyOn(thoughtspotService.ThoughtSpotService.prototype, 'getAnswerForQuestion') + .mockImplementation(mockGetAnswerForQuestion); + + await server.init(); + const { callTool } = connect(server); + + const result = await callTool("fetch", { + id: "asdhshd-123123-12dd: What is the total revenue?" + }); + + expect(result.isError).toBe(true); + expect((result.content as any[])[0].text).toBe("ERROR: Question not found"); + }); + + it("should handle ID with complex datasource ID", async () => { + // Mock the ThoughtSpot service to return answer + const mockGetAnswerForQuestion = vi.fn().mockResolvedValue({ + data: "The total revenue is $1,000,000", + error: null + }); + + vi.spyOn(thoughtspotService.ThoughtSpotService.prototype, 'getAnswerForQuestion') + .mockImplementation(mockGetAnswerForQuestion); + + await server.init(); + const { callTool } = connect(server); + + const result = await callTool("fetch", { + id: "abc-123-def-456: What is the total revenue?" + }); + + expect(result.isError).toBeUndefined(); + expect(result.structuredContent).toEqual({ + id: "abc-123-def-456: What is the total revenue?", + title: " What is the total revenue?", + text: "The total revenue is $1,000,000", + url: "https://test.thoughtspot.cloud/#/insights/conv-assist?query=What is the total revenue?&worksheet=abc-123-def-456&executeSearch=true" + }); + }); + + it("should handle ID with question containing special characters", async () => { + // Mock the ThoughtSpot service to return answer + const mockGetAnswerForQuestion = vi.fn().mockResolvedValue({ + data: "The revenue increased by 15%", + error: null + }); + + vi.spyOn(thoughtspotService.ThoughtSpotService.prototype, 'getAnswerForQuestion') + .mockImplementation(mockGetAnswerForQuestion); + + await server.init(); + const { callTool } = connect(server); + + const result = await callTool("fetch", { + id: "ds-123: How much did revenue increase? (in %)" + }); + + expect(result.isError).toBeUndefined(); + expect(result.structuredContent).toEqual({ + id: "ds-123: How much did revenue increase? (in %)", + title: " How much did revenue increase? (in %)", + text: "The revenue increased by 15%", + url: "https://test.thoughtspot.cloud/#/insights/conv-assist?query=How much did revenue increase? (in %)&worksheet=ds-123&executeSearch=true" + }); + }); + }); + + describe("Error Handling", () => { + it("should handle empty fetch ID", async () => { + // Mock the ThoughtSpot service to return answer for empty ID test + const mockGetAnswerForQuestion = vi.fn().mockResolvedValue({ + data: "The total revenue is $1,000,000", + error: null + }); + + vi.spyOn(thoughtspotService.ThoughtSpotService.prototype, 'getAnswerForQuestion') + .mockImplementation(mockGetAnswerForQuestion); + + await server.init(); + const { callTool } = connect(server); + + const result = await callTool("fetch", { + id: "" // Empty ID + }); + + // Empty ID will cause the split to return ["", ""], which results in empty datasourceId and undefined question + expect(result.isError).toBeUndefined(); + expect(result.structuredContent).toEqual({ + id: "", + title: "", + text: "The total revenue is $1,000,000", + url: "https://test.thoughtspot.cloud/#/insights/conv-assist?query=&worksheet=&executeSearch=true" + }); + }); + }); +}); diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 02a42ce..2307189 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,12 +1,13 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: b36ebb1de95b63461e97ddd94021daa3) -// Runtime types generated with workerd@1.20250417.0 2025-04-17 nodejs_compat +// Generated by Wrangler by running `wrangler types` (hash: b5949bb168b75f2fe6e6e5a1db8d9e89) +// Runtime types generated with workerd@1.20250604.0 2025-04-17 nodejs_compat declare namespace Cloudflare { interface Env { OAUTH_KV: KVNamespace; - HONEYCOMB_API_KEY: string; - HONEYCOMB_DATASET: string; + HONEYCOMB_DATASET: ""; + HONEYCOMB_API_KEY: ""; MCP_OBJECT: DurableObjectNamespace; + OPENAI_DEEP_RESEARCH_MCP_OBJECT: DurableObjectNamespace; ANALYTICS: AnalyticsEngineDataset; ASSETS: Fetcher; } @@ -16,7 +17,7 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } // Begin runtime types @@ -99,7 +100,7 @@ interface Console { clear(): void; /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static) */ count(label?: string): void; - /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countreset_static) */ + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static) */ countReset(label?: string): void; /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static) */ debug(...data: any[]): void; @@ -111,9 +112,9 @@ interface Console { error(...data: any[]): void; /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static) */ group(...data: any[]): void; - /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupcollapsed_static) */ + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static) */ groupCollapsed(...data: any[]): void; - /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupend_static) */ + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static) */ groupEnd(): void; /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static) */ info(...data: any[]): void; @@ -123,9 +124,9 @@ interface Console { table(tabularData?: any, properties?: string[]): void; /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static) */ time(label?: string): void; - /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeend_static) */ + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static) */ timeEnd(label?: string): void; - /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timelog_static) */ + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static) */ timeLog(label?: string, ...data: any[]): void; timeStamp(label?: string): void; /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static) */ @@ -300,25 +301,25 @@ declare function dispatchEvent(event: WorkerGlobalScopeEventMap[keyof WorkerGlob declare function btoa(data: string): string; /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */ declare function atob(data: string): string; -/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/setTimeout) */ +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ declare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number; -/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/setTimeout) */ +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ declare function setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; -/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/clearTimeout) */ +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */ declare function clearTimeout(timeoutId: number | null): void; -/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/setInterval) */ +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */ declare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number; -/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/setInterval) */ +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */ declare function setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; -/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/clearInterval) */ +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */ declare function clearInterval(timeoutId: number | null): void; -/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/queueMicrotask) */ +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */ declare function queueMicrotask(task: Function): void; -/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/structuredClone) */ +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */ declare function structuredClone(value: T, options?: StructuredSerializeOptions): T; -/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/reportError) */ +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */ declare function reportError(error: any): void; -/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/fetch) */ +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */ declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise; declare const self: ServiceWorkerGlobalScope; /** @@ -797,6 +798,7 @@ declare class Blob { slice(start?: number, end?: number, type?: string): Blob; /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/arrayBuffer) */ arrayBuffer(): Promise; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/bytes) */ bytes(): Promise; /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */ text(): Promise; @@ -1091,10 +1093,15 @@ interface TextEncoderEncodeIntoResult { */ declare class ErrorEvent extends Event { constructor(type: string, init?: ErrorEventErrorEventInit); + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/filename) */ get filename(): string; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/message) */ get message(): string; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/lineno) */ get lineno(): number; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/colno) */ get colno(): number; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/error) */ get error(): any; } interface ErrorEventErrorEventInit { @@ -1268,6 +1275,7 @@ declare abstract class Body { get bodyUsed(): boolean; /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) */ arrayBuffer(): Promise; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) */ bytes(): Promise; /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text) */ text(): Promise; @@ -1379,7 +1387,11 @@ interface Request> e * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/integrity) */ integrity: string; - /* Returns a boolean indicating whether or not request can outlive the global in which it was created. */ + /** + * Returns a boolean indicating whether or not request can outlive the global in which it was created. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/keepalive) + */ keepalive: boolean; /** * Returns the cache mode associated with request, which is a string indicating how the request will interact with the browser's cache when fetching. @@ -1579,6 +1591,7 @@ interface R2ObjectBody extends R2Object { get body(): ReadableStream; get bodyUsed(): boolean; arrayBuffer(): Promise; + bytes(): Promise; text(): Promise; json(): Promise; blob(): Promise; @@ -3824,8 +3837,18 @@ interface AutoRAGNotFoundError extends Error { } interface AutoRAGUnauthorizedError extends Error { } +type ComparisonFilter = { + key: string; + type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'; + value: string | number | boolean; +}; +type CompoundFilter = { + type: 'and' | 'or'; + filters: ComparisonFilter[]; +}; type AutoRagSearchRequest = { query: string; + filters?: CompoundFilter | ComparisonFilter; max_num_results?: number; ranking_options?: { ranker?: string; @@ -5175,6 +5198,7 @@ declare module 'cloudflare:workers' { export type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ''}` | number; export type WorkflowDelayDuration = WorkflowSleepDuration; export type WorkflowTimeoutDuration = WorkflowSleepDuration; + export type WorkflowRetentionDuration = WorkflowSleepDuration; export type WorkflowBackoff = 'constant' | 'linear' | 'exponential'; export type WorkflowStepConfig = { retries?: { @@ -5305,6 +5329,7 @@ declare namespace TailStream { readonly type: "onset"; readonly dispatchNamespace?: string; readonly entrypoint?: string; + readonly executionModel: string; readonly scriptName?: string; readonly scriptTags?: string[]; readonly scriptVersion?: ScriptVersion; @@ -5322,8 +5347,8 @@ declare namespace TailStream { } interface SpanOpen { readonly type: "spanOpen"; - readonly op?: string; - readonly info?: FetchEventInfo | JsRpcEventInfo | Attribute[]; + readonly name: string; + readonly info?: FetchEventInfo | JsRpcEventInfo | Attributes; } interface SpanClose { readonly type: "spanClose"; @@ -5347,7 +5372,7 @@ declare namespace TailStream { } interface Return { readonly type: "return"; - readonly info?: FetchResponseInfo | Attribute[]; + readonly info?: FetchResponseInfo; } interface Link { readonly type: "link"; @@ -5357,21 +5382,23 @@ declare namespace TailStream { readonly spanId: string; } interface Attribute { - readonly type: "attribute"; readonly name: string; - readonly value: string | string[] | boolean | boolean[] | number | number[]; + readonly value: string | string[] | boolean | boolean[] | number | number[] | bigint | bigint[]; + } + interface Attributes { + readonly type: "attributes"; + readonly info: Attribute[]; } - type Mark = DiagnosticChannelEvent | Exception | Log | Return | Link | Attribute[]; interface TailEvent { readonly traceId: string; readonly invocationId: string; readonly spanId: string; readonly timestamp: Date; readonly sequence: number; - readonly event: Onset | Outcome | Hibernate | SpanOpen | SpanClose | Mark; + readonly event: Onset | Outcome | Hibernate | SpanOpen | SpanClose | DiagnosticChannelEvent | Exception | Log | Return | Link | Attributes; } type TailEventHandler = (event: TailEvent) => void | Promise; - type TailEventHandlerName = "onset" | "outcome" | "hibernate" | "spanOpen" | "spanClose" | "diagnosticChannel" | "exception" | "log" | "return" | "link" | "attribute"; + type TailEventHandlerName = "outcome" | "hibernate" | "spanOpen" | "spanClose" | "diagnosticChannel" | "exception" | "log" | "return" | "link" | "attributes"; type TailEventHandlerObject = Record; type TailEventHandlerType = TailEventHandler | TailEventHandlerObject; } @@ -5685,6 +5712,9 @@ declare abstract class Workflow { */ public createBatch(batch: WorkflowInstanceCreateOptions[]): Promise; } +type WorkflowDurationLabel = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'; +type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ''}` | number; +type WorkflowRetentionDuration = WorkflowSleepDuration; interface WorkflowInstanceCreateOptions { /** * An id for your Workflow instance. Must be unique within the Workflow. @@ -5694,6 +5724,14 @@ interface WorkflowInstanceCreateOptions { * The event payload the Workflow instance is triggered with */ params?: PARAMS; + /** + * The retention policy for Workflow instance. + * Defaults to the maximum retention period available for the owner's account. + */ + retention?: { + successRetention?: WorkflowRetentionDuration; + errorRetention?: WorkflowRetentionDuration; + }; } type InstanceStatus = { status: 'queued' // means that instance is waiting to be started (see concurrency limits) diff --git a/wrangler.jsonc b/wrangler.jsonc index 516f8d2..d270c7b 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -16,13 +16,17 @@ "class_name": "ThoughtSpotMCP", "name": "MCP_OBJECT" }, - + { + "class_name": "ThoughtSpotOpenAIDeepResearchMCP", + "name": "OPENAI_DEEP_RESEARCH_MCP_OBJECT" + } ] }, "migrations": [{ "tag": "v1", "new_sqlite_classes": [ - "ThoughtSpotMCP" + "ThoughtSpotMCP", + "ThoughtSpotOpenAIDeepResearchMCP" ] }], "kv_namespaces": [{ @@ -36,5 +40,9 @@ "analytics_engine_datasets": [ { "binding": "ANALYTICS", "dataset": "mcp_events" } ], + "vars": { + "HONEYCOMB_DATASET": "", + "HONEYCOMB_API_KEY": "" + }, "assets": { "directory": "./static/", "binding": "ASSETS" }, }