diff --git a/mcp-worker/src/apiClient.ts b/mcp-worker/src/apiClient.ts index acdfa09d8..dbf35b392 100644 --- a/mcp-worker/src/apiClient.ts +++ b/mcp-worker/src/apiClient.ts @@ -1,7 +1,7 @@ import type { UserProps, DevCycleJWTClaims } from './types' import { IDevCycleApiClient } from '../../src/mcp/api/interface' import { getErrorMessage, ensureError } from '../../src/mcp/utils/api' -import { setDVCReferrer } from '../../src/api/apiClient' +import { setMCPHeaders, setMCPToolCommand } from '../../src/mcp/utils/headers' /** * Interface for state management - allows McpAgent or other state managers @@ -20,9 +20,15 @@ export class WorkerApiClient implements IDevCycleApiClient { private props: UserProps, private env: Env, private stateManager?: IStateManager, - private version: string = 'unknown', ) {} + /** + * Initialize MCP headers with version - should be called once at server startup + */ + static initializeMCPHeaders(version: string): void { + setMCPHeaders(version) + } + /** * Execute an API operation with OAuth token authentication and consistent logging */ @@ -45,8 +51,7 @@ export class WorkerApiClient implements IDevCycleApiClient { } // Set MCP analytics headers for this specific tool operation - // Caller is 'mcp' to distinguish from CLI and other callers; command is the tool name - setDVCReferrer(operationName, this.version, 'mcp') + setMCPToolCommand(operationName) console.log(`Worker MCP ${operationName}:`, { args, diff --git a/mcp-worker/src/index.ts b/mcp-worker/src/index.ts index 5a640d986..4ff80cfd6 100644 --- a/mcp-worker/src/index.ts +++ b/mcp-worker/src/index.ts @@ -1,5 +1,7 @@ import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js' +import type { ZodRawShape } from 'zod' import workerVersion from './version' import { McpAgent } from 'agents/mcp' import { createAuthApp, createTokenExchangeCallback } from './auth' @@ -9,7 +11,7 @@ import { WorkerApiClient } from './apiClient' import { registerAllToolsWithServer } from '../../src/mcp/tools/index' // Import types -import { DevCycleMCPServerInstance } from '../../src/mcp/server' +import type { DevCycleMCPServerInstance } from '../../src/mcp/server' import { handleToolError } from '../../src/mcp/utils/errorHandling' import { registerProjectSelectionTools } from './projectSelectionTools' @@ -48,12 +50,14 @@ export class DevCycleMCP extends McpAgent { * Initialize the MCP server with tools and handlers */ async init() { + // Initialize MCP headers with version once at startup + WorkerApiClient.initializeMCPHeaders(this.version) + // Initialize the Worker-specific API client with OAuth tokens and state management this.apiClient = new WorkerApiClient( this.props, this.env, this, // Pass the McpAgent instance for state management - this.version, ) console.log('Initializing DevCycle MCP Worker', { @@ -62,26 +66,50 @@ export class DevCycleMCP extends McpAgent { hasProject: await this.apiClient.hasProjectKey(), }) + // Check environment variable for output schema support + const enableOutputSchemas = + (this.env.ENABLE_OUTPUT_SCHEMAS as string) === 'true' + if (enableOutputSchemas) { + console.log('DevCycle MCP Worker - Output Schemas: ENABLED') + } + // Create an adapter to make the worker's McpServer compatible with the CLI registration pattern const serverAdapter: DevCycleMCPServerInstance = { registerToolWithErrorHandling: ( name: string, config: { description: string - annotations?: any - inputSchema?: any - outputSchema?: any + annotations?: ToolAnnotations + inputSchema?: ZodRawShape + outputSchema?: ZodRawShape }, - handler: (args: any) => Promise, + handler: (args: unknown) => Promise, ) => { + // Conditionally include output schema based on environment variable + const toolConfig: { + description: string + annotations?: ToolAnnotations + inputSchema?: ZodRawShape + outputSchema?: ZodRawShape + } = { + description: config.description, + annotations: config.annotations, + inputSchema: config.inputSchema, + } + + if (enableOutputSchemas && config.outputSchema) { + toolConfig.outputSchema = config.outputSchema + } + this.server.registerTool( name, - { - description: config.description, - annotations: config.annotations, - inputSchema: config.inputSchema || {}, - }, - async (args: any) => { + // TypeScript workaround: The MCP SDK's registerTool has complex generic constraints + // that cause "Type instantiation is excessively deep" errors when used with our + // adapter pattern. The types are correct at runtime, but TS can't verify them. + toolConfig as Parameters< + typeof this.server.registerTool + >[1], + async (args: unknown) => { try { const result = await handler(args) return { diff --git a/mcp-worker/tsconfig.json b/mcp-worker/tsconfig.json index 9cb8d0a4b..50fe540d6 100644 --- a/mcp-worker/tsconfig.json +++ b/mcp-worker/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "./dist", "rootDir": "../", + "noEmit": true, "types": [ "node" ], diff --git a/mcp-worker/worker-configuration.d.ts b/mcp-worker/worker-configuration.d.ts index 7b20a66ec..a8a23addd 100644 --- a/mcp-worker/worker-configuration.d.ts +++ b/mcp-worker/worker-configuration.d.ts @@ -1,6 +1,6 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: be45b0f19d86c17963edd55010e3b8be) -// Runtime types generated with workerd@1.20250712.0 2025-06-28 nodejs_compat +// Generated by Wrangler by running `wrangler types` (hash: 1d664210c769c6aac43d5af03f7d45db) +// Runtime types generated with workerd@1.20250803.0 2025-06-28 nodejs_compat declare namespace Cloudflare { interface Env { OAUTH_KV: KVNamespace; @@ -9,6 +9,7 @@ declare namespace Cloudflare { AUTH0_DOMAIN: "auth.devcycle.com"; AUTH0_AUDIENCE: "https://api.devcycle.com/"; AUTH0_SCOPE: "openid profile email offline_access"; + ENABLE_OUTPUT_SCHEMAS: "false"; AUTH0_CLIENT_ID: string; AUTH0_CLIENT_SECRET: string; MCP_OBJECT: DurableObjectNamespace; @@ -19,7 +20,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 @@ -1115,6 +1116,47 @@ interface ErrorEventErrorEventInit { colno?: number; error?: any; } +/** + * A message received by a target object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent) + */ +declare class MessageEvent extends Event { + constructor(type: string, initializer: MessageEventInit); + /** + * Returns the data of the message. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/data) + */ + readonly data: any; + /** + * Returns the origin of the message, for server-sent events and cross-document messaging. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/origin) + */ + readonly origin: string | null; + /** + * Returns the last event ID string, for server-sent events. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/lastEventId) + */ + readonly lastEventId: string; + /** + * Returns the WindowProxy of the source window, for cross-document messaging, and the MessagePort being attached, in the connect event fired at SharedWorkerGlobalScope objects. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/source) + */ + readonly source: MessagePort | null; + /** + * Returns the MessagePort array sent with the message, for cross-document messaging and channel messaging. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/ports) + */ + readonly ports: MessagePort[]; +} +interface MessageEventInit { + data: ArrayBuffer | string; +} /** * Provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using the XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to "multipart/form-data". * @@ -1423,7 +1465,7 @@ interface RequestInit { signal?: (AbortSignal | null); encodeResponseBody?: "automatic" | "manual"; } -type Service = Fetcher; +type Service Rpc.WorkerEntrypointBranded) | Rpc.WorkerEntrypointBranded | ExportedHandler | undefined = undefined> = T extends new (...args: any[]) => Rpc.WorkerEntrypointBranded ? Fetcher> : T extends Rpc.WorkerEntrypointBranded ? Fetcher : T extends Exclude ? never : Fetcher; type Fetcher = (T extends Rpc.EntrypointBranded ? Rpc.Provider : unknown) & { fetch(input: RequestInfo | URL, init?: RequestInit): Promise; connect(address: SocketAddress | string, options?: SocketOptions): Socket; @@ -2297,23 +2339,6 @@ interface CloseEventInit { reason?: string; wasClean?: boolean; } -/** - * A message received by a target object. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent) - */ -declare class MessageEvent extends Event { - constructor(type: string, initializer: MessageEventInit); - /** - * Returns the data of the message. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/data) - */ - readonly data: ArrayBuffer | string; -} -interface MessageEventInit { - data: ArrayBuffer | string; -} type WebSocketEventMap = { close: CloseEvent; message: MessageEvent; @@ -2501,6 +2526,38 @@ interface ContainerStartupOptions { enableInternet: boolean; env?: Record; } +/** + * This Channel Messaging API interface represents one of the two ports of a MessageChannel, allowing messages to be sent from one port and listening out for them arriving at the other. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort) + */ +interface MessagePort extends EventTarget { + /** + * Posts a message through the channel. Objects listed in transfer are transferred, not just cloned, meaning that they are no longer usable on the sending side. + * + * Throws a "DataCloneError" DOMException if transfer contains duplicate objects or port, or if message could not be cloned. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/postMessage) + */ + postMessage(data?: any, options?: (any[] | MessagePortPostMessageOptions)): void; + /** + * Disconnects the port, so that it is no longer active. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/close) + */ + close(): void; + /** + * Begins dispatching messages received on the port. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/start) + */ + start(): void; + get onmessage(): any | null; + set onmessage(value: any | null); +} +interface MessagePortPostMessageOptions { + transfer?: any[]; +} type AiImageClassificationInput = { image: number[]; }; @@ -5790,13 +5847,13 @@ interface IncomingRequestCfPropertiesBase extends Record { * * @example 395747 */ - asn: number; + asn?: number; /** * The organization which owns the ASN of the incoming request. * * @example "Google Cloud" */ - asOrganization: string; + asOrganization?: string; /** * The original value of the `Accept-Encoding` header if Cloudflare modified it. * @@ -5920,7 +5977,7 @@ interface IncomingRequestCfPropertiesCloudflareForSaaSEnterprise { * This field is only present if you have Cloudflare for SaaS enabled on your account * and you have followed the [required steps to enable it]((https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/custom-metadata/)). */ - hostMetadata: HostMetadata; + hostMetadata?: HostMetadata; } interface IncomingRequestCfPropertiesCloudflareAccessOrApiShield { /** @@ -6447,7 +6504,7 @@ type ImageTransform = { rotate?: 0 | 90 | 180 | 270; saturation?: number; sharpen?: number; - trim?: "border" | { + trim?: 'border' | { top?: number; bottom?: number; left?: number; @@ -6469,6 +6526,9 @@ type ImageDrawOptions = { bottom?: number; right?: number; }; +type ImageInputOptions = { + encoding?: 'base64'; +}; type ImageOutputOptions = { format: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' | 'image/avif' | 'rgb' | 'rgba'; quality?: number; @@ -6480,13 +6540,13 @@ interface ImagesBinding { * @throws {@link ImagesError} with code 9412 if input is not an image * @param stream The image bytes */ - info(stream: ReadableStream): Promise; + info(stream: ReadableStream, options?: ImageInputOptions): Promise; /** * Begin applying a series of transformations to an image * @param stream The image bytes * @returns A transform handle */ - input(stream: ReadableStream): ImageTransformer; + input(stream: ReadableStream, options?: ImageInputOptions): ImageTransformer; } interface ImageTransformer { /** @@ -6509,6 +6569,9 @@ interface ImageTransformer { */ output(options: ImageOutputOptions): Promise; } +type ImageTransformationOutputOptions = { + encoding?: 'base64'; +}; interface ImageTransformationResult { /** * The image as a response, ready to store in cache or return to users @@ -6521,7 +6584,7 @@ interface ImageTransformationResult { /** * The bytes of the response */ - image(): ReadableStream; + image(options?: ImageTransformationOutputOptions): ReadableStream; } interface ImagesError extends Error { readonly code: number; @@ -6746,6 +6809,19 @@ declare namespace Cloudflare { interface Env { } } +declare module 'cloudflare:node' { + export interface DefaultHandler { + fetch?(request: Request): Response | Promise; + tail?(events: TraceItem[]): void | Promise; + trace?(traces: TraceItem[]): void | Promise; + scheduled?(controller: ScheduledController): void | Promise; + queue?(batch: MessageBatch): void | Promise; + test?(controller: TestController): void | Promise; + } + export function httpServerHandler(options: { + port: number; + }, handlers?: Omit): DefaultHandler; +} declare module 'cloudflare:workers' { export type RpcStub = Rpc.Stub; export const RpcStub: { @@ -6819,6 +6895,7 @@ declare module 'cloudflare:workers' { constructor(ctx: ExecutionContext, env: Env); run(event: Readonly>, step: WorkflowStep): Promise; } + export function waitUntil(promise: Promise): void; export const env: Cloudflare.Env; } interface SecretsStoreSecret { @@ -6841,7 +6918,7 @@ declare namespace TailStream { readonly type: "fetch"; readonly method: string; readonly url: string; - readonly cfJson: string; + readonly cfJson?: object; readonly headers: Header[]; } interface JsRpcEventInfo { @@ -6952,7 +7029,7 @@ declare namespace TailStream { interface Log { readonly type: "log"; readonly level: "debug" | "error" | "info" | "log" | "warn"; - readonly message: string; + readonly message: object; } interface Return { readonly type: "return"; diff --git a/mcp-worker/wrangler.toml b/mcp-worker/wrangler.toml index 9c41ee956..e246c8d24 100644 --- a/mcp-worker/wrangler.toml +++ b/mcp-worker/wrangler.toml @@ -30,7 +30,7 @@ API_BASE_URL = "https://api.devcycle.com" AUTH0_DOMAIN = "auth.devcycle.com" AUTH0_AUDIENCE = "https://api.devcycle.com/" AUTH0_SCOPE = "openid profile email offline_access" - +ENABLE_OUTPUT_SCHEMAS = "false" # Secrets (set via: wrangler secret put ) # AUTH0_CLIENT_ID diff --git a/src/mcp/index.ts b/src/mcp/index.ts index 0a486af78..539e619b4 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -13,7 +13,7 @@ function getVersion(): string { const packagePath = join(__dirname, '..', '..', 'package.json') const packageJson = JSON.parse(readFileSync(packagePath, 'utf8')) return packageJson.version - } catch (error) { + } catch { return 'unknown version' } } diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 5756fd5dc..04dfd52da 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -1,5 +1,6 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' -import { Tool, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js' +import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js' +import type { ZodRawShape } from 'zod' import { DevCycleAuth } from './utils/auth' import { DevCycleApiClient } from './utils/api' import { IDevCycleApiClient } from './api/interface' @@ -8,7 +9,8 @@ import { handleToolError } from './utils/errorHandling' import { registerAllToolsWithServer } from './tools' // Environment variable to control output schema inclusion -const ENABLE_OUTPUT_SCHEMAS = process.env.ENABLE_OUTPUT_SCHEMAS === 'true' +const ENABLE_OUTPUT_SCHEMAS = + (process.env.ENABLE_OUTPUT_SCHEMAS as string) === 'true' if (ENABLE_OUTPUT_SCHEMAS) { console.error('DevCycle MCP Server - Output Schemas: ENABLED') } @@ -17,7 +19,7 @@ if (ENABLE_OUTPUT_SCHEMAS) { export type ToolHandler = ( args: unknown, apiClient: IDevCycleApiClient, -) => Promise +) => Promise // Type for the server instance with our helper method export type DevCycleMCPServerInstance = { @@ -25,27 +27,14 @@ export type DevCycleMCPServerInstance = { name: string, config: { description: string - annotations?: any - inputSchema?: any - outputSchema?: any + annotations?: ToolAnnotations + inputSchema?: ZodRawShape + outputSchema?: ZodRawShape }, - handler: (args: any) => Promise, + handler: (args: unknown) => Promise, ) => void } -// Function to conditionally remove outputSchema from tool definitions -const processToolDefinitions = (tools: Tool[]): Tool[] => { - if (ENABLE_OUTPUT_SCHEMAS) { - return tools - } - - // Remove outputSchema from all tools when disabled - return tools.map((tool) => { - const { outputSchema, ...toolWithoutSchema } = tool - return toolWithoutSchema - }) -} - export class DevCycleMCPServer { private auth: DevCycleAuth private apiClient: DevCycleApiClient @@ -82,13 +71,29 @@ export class DevCycleMCPServer { name: string, config: { description: string - inputSchema?: any - outputSchema?: any + inputSchema?: ZodRawShape + outputSchema?: ZodRawShape annotations?: ToolAnnotations }, - handler: (args: any) => Promise, + handler: (args: unknown) => Promise, ) { - this.server.registerTool(name, config, async (args: any) => { + // Conditionally include output schema based on environment variable + const toolConfig: { + description: string + annotations?: ToolAnnotations + inputSchema?: ZodRawShape + outputSchema?: ZodRawShape + } = { + description: config.description, + annotations: config.annotations, + inputSchema: config.inputSchema, + } + + if (ENABLE_OUTPUT_SCHEMAS && config.outputSchema) { + toolConfig.outputSchema = config.outputSchema + } + + this.server.registerTool(name, toolConfig, async (args: unknown) => { try { const result = await handler(args) @@ -99,7 +104,7 @@ export class DevCycleMCPServer { text: JSON.stringify(result, null, 2), }, ], - } as any + } } catch (error) { return handleToolError(error, name) } diff --git a/src/mcp/tools/selfTargetingTools.ts b/src/mcp/tools/selfTargetingTools.ts index 88af7c51a..054c4465f 100644 --- a/src/mcp/tools/selfTargetingTools.ts +++ b/src/mcp/tools/selfTargetingTools.ts @@ -234,6 +234,7 @@ export function registerSelfTargetingTools( 'Set a self-targeting override for a feature variation. ⚠️ IMPORTANT: Always confirm with the user before setting overrides for production environments (environments where type = "production"). Include dashboard link in the response.', annotations: { title: 'Set Self-Targeting Override For Feature/Environment', + destructiveHint: true, }, inputSchema: SetSelfTargetingOverrideArgsSchema.shape, }, @@ -253,6 +254,7 @@ export function registerSelfTargetingTools( 'Clear self-targeting overrides for a specific feature/environment. ⚠️ IMPORTANT: Always confirm with the user before clearing overrides for production environments (environments where type = "production"). Include dashboard link in the response.', annotations: { title: 'Clear Self-Targeting Override For Feature/Environment', + destructiveHint: true, }, inputSchema: ClearSelfTargetingOverridesArgsSchema.shape, }, diff --git a/src/mcp/utils/headers.ts b/src/mcp/utils/headers.ts index 2ec4aa63e..0eb299f72 100644 --- a/src/mcp/utils/headers.ts +++ b/src/mcp/utils/headers.ts @@ -1,7 +1,7 @@ import { setDVCReferrer } from '../../api/apiClient' // Store the version for reuse in tool commands -let mcpVersion: string = 'unknown' +let mcpVersion: string = '' /** * Sets up MCP-specific headers for all API requests