diff --git a/.changesets/1791371504-7b97c87e.yaml b/.changesets/1791371504-7b97c87e.yaml new file mode 100644 index 000000000..1e64e14c7 --- /dev/null +++ b/.changesets/1791371504-7b97c87e.yaml @@ -0,0 +1,10 @@ +id: 1791371504-7b97c87e +features: + - mcpServer +targets: + - typescriptv2 +type: feat +bump: major +description: migrate MCP servers to the v2 SDK and require Node.js 20 +author: ThomasRooney +date: "2026-10-07" diff --git a/.changesets/1791385688-ba585974.yaml b/.changesets/1791385688-ba585974.yaml new file mode 100644 index 000000000..513782174 --- /dev/null +++ b/.changesets/1791385688-ba585974.yaml @@ -0,0 +1,10 @@ +id: 1791385688-ba585974 +features: + - core +targets: + - mcp-typescript +type: feat +bump: major +description: migrate MCP servers to the v2 SDK, require Node.js 20, and preserve legacy SSE clients +author: ThomasRooney +date: "2026-10-07" diff --git a/templates/perms.go b/templates/perms.go index a68f0cd1f..0eefc687d 100644 --- a/templates/perms.go +++ b/templates/perms.go @@ -1205,6 +1205,7 @@ var fileMode = map[string]fs.FileMode{ "templates/mcp-typescript/tests/primary/authnew_additional.test.ts.stmpl": 0o644, "templates/mcp-typescript/tests/primary/bigint.test.ts": 0o644, "templates/mcp-typescript/tests/primary/collections_additional.test.ts.stmpl": 0o644, + "templates/mcp-typescript/tests/primary/compatibility_additional.test.ts": 0o644, "templates/mcp-typescript/tests/primary/decimal.test.ts": 0o644, "templates/mcp-typescript/tests/primary/dynamic_tools_additional.test.ts": 0o644, "templates/mcp-typescript/tests/primary/encodings_additional.test.ts": 0o644, @@ -2459,6 +2460,7 @@ var fileMode = map[string]fs.FileMode{ "templates/typescriptv2/tests/tertiary": 0o20000000755, "templates/typescriptv2/tests/tertiary/auth_additional.test.ts": 0o644, "templates/typescriptv2/tests/tertiary/mcp_additional.test.ts": 0o644, + "templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts": 0o644, "templates/typescriptv2/tests/tertiary/servers.test.ts": 0o644, "templates/typescriptv2/tests/tertiary/smart_union.test.ts": 0o644, "templates/typescriptv2/tests/tertiary/webhooks.test.ts": 0o644, diff --git a/templates/templates/mcp-typescript/auxiliary/src/cloudflare-worker/cloudflare-worker.ts.stmpl b/templates/templates/mcp-typescript/auxiliary/src/cloudflare-worker/cloudflare-worker.ts.stmpl index 9289ca215..318fbe88c 100644 --- a/templates/templates/mcp-typescript/auxiliary/src/cloudflare-worker/cloudflare-worker.ts.stmpl +++ b/templates/templates/mcp-typescript/auxiliary/src/cloudflare-worker/cloudflare-worker.ts.stmpl @@ -3,75 +3,386 @@ {{- $globalSDK := .Global.AST.MainSDK -}} {{- $coreName := sanitizeCoreSDKName }} {{- $mcpServerName := print (sanitizeClassName .Global.Config.PackageName) "MCP" }} -import { McpAgent } from "agents/mcp"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +{{- $sessionBinding := print (sanitizeEnvVarName .Global.Config.PackageName) "_MCP" }} +import { + createMcpHandler, + isInitializeRequest, + isLegacyRequest, + legacyStatelessFallback, + WebStandardStreamableHTTPServerTransport, +} from "@modelcontextprotocol/server"; +import { SSEServerTransport } from "@modelcontextprotocol/server-legacy/sse"; +{{- if $globalSDK.Security }} +import { AsyncLocalStorage } from "node:async_hooks"; +{{- end }} import { {{ $coreName }} } from "../core.js"; import { createConsoleLogger } from "../mcp-server/console-logger.js"; import { createMCPServer } from "../mcp-server/server.js"; -import type { Env } from "../../worker-configuration.js"; import { landingPage } from "../landing-page.js" -interface State { } +function getSDK({{ if $globalSDK.Security }}headers: Headers | undefined{{ end }}) { +{{- if $globalSDK.Security }} + const getHeader = (name: string) => headers?.get(name) ?? '' +{{- end }} -type Props = Record; + return new {{ $coreName }}({ + debugLogger: { + log: (...args) => console.log(...args), + group: (...args) => console.group(...args), + groupEnd: (...args) => console.groupEnd(...args), + }, +{{- if $globalSDK.Security }} + security: async () => ({{templateRemoteServerHeaders $globalSDK.Security.Type}}) +{{- end }} + }); +} + +function createServer({{ if $globalSDK.Security }}getHeaders: () => Headers | undefined{{ end }}) { + const { server } = createMCPServer({ + logger: createConsoleLogger('debug'), + getSDK: () => getSDK({{ if $globalSDK.Security }}getHeaders(){{ end }}), + }); + return server; +} -export class {{ $mcpServerName }} extends McpAgent { - server!: McpServer; +const PROPS_HEADER = "x-mcp-worker-props"; +{{- if $globalSDK.Security }} - async init() { - const { server } = createMCPServer({ - logger: createConsoleLogger('debug'), - getSDK: () => this.getSDK(), - }); +type Credentials = { headers: Headers; authoritative: boolean }; - this.server = server +function credentials(request: Request): Credentials { + const props = request.headers.get(PROPS_HEADER); + if (props == null) return { headers: request.headers, authoritative: false }; + try { + return { headers: new Headers(JSON.parse(props) as Record), authoritative: true }; + } catch { + return { headers: new Headers(), authoritative: true }; } +} + +function credentialHeaders(request: Request | undefined): Headers | undefined { + return request ? credentials(request).headers : undefined; +} +{{- end }} - getSDK() { - {{- if $globalSDK.Security }} - const getHeader = (name: string) => this.props?.[name] ?? '' - {{- end }} +const modernHandler = createMcpHandler( + ({{ if $globalSDK.Security }}{ requestInfo }{{ end }}) => createServer({{ if $globalSDK.Security }}() => credentialHeaders(requestInfo){{ end }}), + { legacy: "reject" }, +); - const sdk = new {{ $coreName }}({ - debugLogger: { - log: (...args) => console.log(...args), - group: (...args) => console.group(...args), - groupEnd: (...args) => console.groupEnd(...args), - }, - {{- if $globalSDK.Security }} - security: async () => ({{templateRemoteServerHeaders $globalSDK.Security.Type}}) - {{- end }} - }); - return sdk; +const statelessLegacyHandler = legacyStatelessFallback( + ({{ if $globalSDK.Security }}{ requestInfo }{{ end }}) => createServer({{ if $globalSDK.Security }}() => credentialHeaders(requestInfo){{ end }}), +); + +type CorsOptions = { + origin?: string; + methods?: string; + headers?: string; + exposeHeaders?: string; + maxAge?: number; +}; + +type ServeOptions = { + binding?: string; + corsOptions?: CorsOptions; + transport?: "streamable-http" | "sse"; + jurisdiction?: DurableObjectJurisdiction; +}; + +type WorkerProps = Record | undefined; + +function corsHeaders(request: Request, cors: CorsOptions = {}): Headers { + return new Headers({ + "Access-Control-Allow-Origin": cors.origin ?? "*", + "Access-Control-Allow-Methods": cors.methods ?? "GET, POST, DELETE, OPTIONS", + "Access-Control-Allow-Headers": + cors.headers ?? + request.headers.get("Access-Control-Request-Headers") ?? + "Content-Type, Accept, mcp-session-id, mcp-protocol-version", + "Access-Control-Expose-Headers": cors.exposeHeaders ?? "mcp-session-id, mcp-protocol-version", + "Access-Control-Max-Age": String(cors.maxAge ?? 86400), + }); +} + +function withCors(response: Response, request: Request, cors?: CorsOptions): Response { + const headers = new Headers(response.headers); + corsHeaders(request, cors).forEach((value, name) => headers.set(name, value)); + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); +} + +function sessionNotFound(): Response { + return Response.json( + { jsonrpc: "2.0", error: { code: -32001, message: "Session not found" }, id: null }, + { status: 404 }, + ); +} + +function sessionObject(namespace: DurableObjectNamespace, sessionId: string | null) { + if (!sessionId) return undefined; + try { + return namespace.get(namespace.idFromString(sessionId)); + } catch { + return undefined; } } +function forwardRequest(request: Request, pathname: string, props: WorkerProps, search: Record = {}): Request { + const url = new URL(request.url); + url.pathname = pathname; + for (const [name, value] of Object.entries(search)) url.searchParams.set(name, value); + const forwarded = new Request(url.toString(), request as RequestInit); + forwarded.headers.delete(PROPS_HEADER); + if (props) { + const values = Object.fromEntries(Object.entries(props).filter(([, value]) => typeof value === "string")); + forwarded.headers.set(PROPS_HEADER, JSON.stringify(values)); + } + return forwarded; +} -export default { - async fetch(request: Request, env: Env, ctx: any) { - const url = new URL(request.url); +function matchesPath(path: string, pathname: string): boolean { + return path === "/" || pathname === path; +} + +function messagePath(basePath: string): string { + return `${basePath.replace(/\/+$/, "")}/message`; +} + +async function startsSession(request: Request): Promise { + if (request.method !== "POST") return false; + const body: unknown = await request.clone().json().catch(() => undefined); + return Array.isArray(body) ? body.some((message) => isInitializeRequest(message)) : isInitializeRequest(body); +} + +async function legacyStreamableHttp(request: Request, namespace: DurableObjectNamespace): Promise { + const sessionId = request.headers.get("mcp-session-id"); + if (sessionId) { + return sessionObject(namespace, sessionId)?.fetch(request) ?? sessionNotFound(); + } + if (await startsSession(request)) { + return namespace.get(namespace.newUniqueId()).fetch(request); + } + return statelessLegacyHandler(request); +} + +async function handleStreamableHttp(request: Request, namespace: DurableObjectNamespace, props: WorkerProps, cors?: CorsOptions): Promise { + if (request.method === "OPTIONS") { + return new Response(null, { status: 204, headers: corsHeaders(request, cors) }); + } + const forwarded = forwardRequest(request, "/mcp", props); + const response = (await isLegacyRequest(forwarded)) + ? await legacyStreamableHttp(forwarded, namespace) + : await modernHandler.fetch(forwarded); + return withCors(response, request, cors); +} + +async function handleSse(request: Request, namespace: DurableObjectNamespace, basePath: string, props: WorkerProps, cors?: CorsOptions): Promise { + if (request.method === "OPTIONS") { + return new Response(null, { status: 204, headers: corsHeaders(request, cors) }); + } + const url = new URL(request.url); + let response: Response; + if (url.pathname === messagePath(basePath)) { + const session = sessionObject(namespace, url.searchParams.get("session")); + response = session + ? await session.fetch(forwardRequest(request, "/sse/message", props)) + : new Response("Session not found", { status: 404 }); + } else if (matchesPath(basePath, url.pathname) && request.method === "GET") { + response = await namespace.get(namespace.newUniqueId()).fetch( + forwardRequest(request, "/sse", props, { endpoint: messagePath(basePath) }), + ); + } else { + response = new Response("Not Found", { status: 404 }); + } + return withCors(response, request, cors); +} - // Pass auth headers through to the MCP agent - const headers: Record = {}; - for (const [key, value] of request.headers.entries()) { - headers[key.toLowerCase()] = value; +function bindingNamespace(env: Env, options: ServeOptions): DurableObjectNamespace { + const bindings = env as unknown as Record; + const name = options.binding ?? ("MCP_OBJECT" in bindings ? "MCP_OBJECT" : "{{ $sessionBinding }}"); + const namespace = bindings[name] as DurableObjectNamespace | undefined; + if (!namespace || typeof namespace.newUniqueId !== "function") { + throw new Error(`Durable Object binding ${name} was not found. Check the wrangler configuration.`); + } + return options.jurisdiction ? namespace.jurisdiction(options.jurisdiction) : namespace; +} + +type SseResponse = ConstructorParameters[1]; + +function sseStream(request: Request): { response: SseResponse; body: ReadableStream; headers: () => HeadersInit } { + const { readable, writable } = new TransformStream(); + const writer = writable.getWriter(); + const encoder = new TextEncoder(); + const closeListeners: Array<() => void> = []; + let headers: HeadersInit = {}; + let closed = false; + const notifyClosed = () => { + if (closed) return; + closed = true; + for (const listener of closeListeners) listener(); + }; + writer.closed.then(notifyClosed, notifyClosed); + request.signal.addEventListener("abort", () => { + void writer.abort().catch(() => {}); + notifyClosed(); + }); + const response = { + writeHead(_status: number, head?: HeadersInit) { + headers = head ?? {}; + return response; + }, + write(chunk: string) { + writer.write(encoder.encode(chunk)).catch(notifyClosed); + return true; + }, + end() { + writer.close().catch(() => {}); + return response; + }, + on(event: string, listener: () => void) { + if (event === "close") closeListeners.push(listener); + return response; + }, + }; + return { response: response as unknown as SseResponse, body: readable, headers: () => headers }; +} + +{{- if $globalSDK.Security }} + +const requestCredentials = new AsyncLocalStorage(); +{{- end }} + +export class {{ $mcpServerName }} { + readonly #sessionId: string; +{{- if $globalSDK.Security }} + #sessionCredentials: Credentials | undefined; +{{- end }} + #streamableHttp: WebStandardStreamableHTTPServerTransport | undefined; + #sse: SSEServerTransport | undefined; + + constructor(state: DurableObjectState) { + this.#sessionId = state.id.toString(); + } + + static serve(path: string, options: ServeOptions = {}) { + return { + fetch: async (request: Request, env: Env, ctx?: { props?: Record }): Promise => { + const namespace = bindingNamespace(env, options); + if (options.transport === "sse") { + return handleSse(request, namespace, path, ctx?.props, options.corsOptions); + } + if (!matchesPath(path, new URL(request.url).pathname)) { + return new Response("Not Found", { status: 404 }); + } + return handleStreamableHttp(request, namespace, ctx?.props, options.corsOptions); + }, + }; + } + + static serveSSE(path: string, options: Omit = {}) { + return this.serve(path, { ...options, transport: "sse" }); + } + + static mount(path: string, options: Omit = {}) { + return this.serveSSE(path, options); + } + + async fetch(request: Request): Promise { +{{- if $globalSDK.Security }} + return requestCredentials.run(credentials(request), () => this.#route(request)); + } + + #requestHeaders(): Headers | undefined { + const current = requestCredentials.getStore(); + const session = this.#sessionCredentials; + if (!current) return session?.headers; + if (current.authoritative || !session || session.authoritative) return current.headers; + const merged = new Headers(session.headers); + current.headers.forEach((value, name) => merged.set(name, value)); + return merged; + } + + async #route(request: Request): Promise { +{{- end }} + const { pathname } = new URL(request.url); + if (pathname === "/mcp") return this.#handleStreamableHttp(request); + if (pathname === "/sse" && request.method === "GET") return this.#openSse(request); + if (pathname === "/sse/message" && request.method === "POST") return this.#handleSseMessage(request); + return new Response("Not Found", { status: 404 }); + } + + async #handleStreamableHttp(request: Request): Promise { + if (!this.#streamableHttp) { + if (request.headers.has("mcp-session-id")) return sessionNotFound(); + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => this.#sessionId, + onsessionclosed: () => { + this.#streamableHttp = undefined; + }, + }); +{{- if $globalSDK.Security }} + this.#sessionCredentials = credentials(request); +{{- end }} + await createServer({{ if $globalSDK.Security }}() => this.#requestHeaders(){{ end }}).connect(transport); + this.#streamableHttp = transport; } - ctx.props = headers; - - if (url.pathname === "/sse" || url.pathname.startsWith("/sse/")) { - return {{ $mcpServerName }}.serveSSE("/sse", { binding: "{{sanitizeEnvVarName .Global.Config.PackageName}}_MCP" }).fetch( - request, - env, - ctx - ); + return this.#streamableHttp.handleRequest(request); + } + + async #openSse(request: Request): Promise { + if (this.#sse) return new Response("Session already open", { status: 409 }); + const stream = sseStream(request); + const requested = new URL(request.url).searchParams.get("endpoint"); + const endpoint = requested?.startsWith("/") && !requested.startsWith("//") ? requested : "/sse/message"; + const transport = new SSEServerTransport( + `${endpoint}?session=${encodeURIComponent(this.#sessionId)}`, + stream.response, + ); +{{- if $globalSDK.Security }} + this.#sessionCredentials = credentials(request); +{{- end }} + const server = createServer({{ if $globalSDK.Security }}() => this.#requestHeaders(){{ end }}); + transport.onclose = () => { + this.#sse = undefined; + }; + await server.connect(transport); + this.#sse = transport; + return new Response(stream.body, { status: 200, headers: stream.headers() }); + } + + async #handleSseMessage(request: Request): Promise { + const sessionId = new URL(request.url).searchParams.get("sessionId"); + if (!this.#sse || sessionId !== this.#sse.sessionId) { + return new Response("Session not found", { status: 404 }); } + let message: unknown; + try { + message = await request.json(); + } catch { + return new Response("Invalid JSON", { status: 400 }); + } + try { + await this.#sse.handleMessage(message, { request }); + } catch { + return new Response("Invalid message", { status: 400 }); + } + return new Response("Accepted", { status: 202 }); + } +} + +export default { + async fetch(request: Request, env: Env) { + const url = new URL(request.url); if (url.pathname === "/mcp") { - return {{ $mcpServerName }}.serve("/mcp", { binding: "{{sanitizeEnvVarName .Global.Config.PackageName}}_MCP" }).fetch( - request, - env, - ctx - ); + return handleStreamableHttp(request, env.{{ $sessionBinding }}, undefined); + } + + if (url.pathname === "/sse" || url.pathname === "/sse/message") { + return handleSse(request, env.{{ $sessionBinding }}, "/sse", undefined); } // Landing page diff --git a/templates/templates/mcp-typescript/auxiliary/src/landing-page.ts.stmpl b/templates/templates/mcp-typescript/auxiliary/src/landing-page.ts.stmpl index 9efcb6e87..2044ba007 100644 --- a/templates/templates/mcp-typescript/auxiliary/src/landing-page.ts.stmpl +++ b/templates/templates/mcp-typescript/auxiliary/src/landing-page.ts.stmpl @@ -6,26 +6,53 @@ import { Request as ExpressRequest, Response as ExpressResponse } from "express" // Cloudflare-specific wrapper export function landingPage(req: Request): Response { const origin = new URL(req.url).origin; - return new Response(landingPageHTML(origin), { + return new Response(landingPageHTML(origin, "http", "cloudflare"), { headers: { "Content-Type": "text/html" }, }); } // express wrapper export function landingPageExpress(req: ExpressRequest, res: ExpressResponse) { + sendLandingPage(req, res, "sse"); +} + +export function landingPageExpressHTTP(req: ExpressRequest, res: ExpressResponse) { + sendLandingPage(req, res, "http"); +} + +function sendLandingPage(req: ExpressRequest, res: ExpressResponse, transport: "sse" | "http") { const proto = req.get("x-forwarded-proto")?.split(",")[0]?.trim() || req.protocol; const host = req.get("host"); if (!host) { res.status(400).send("Missing Host header"); return; } - res.type("html").send(landingPageHTML(`${proto}://${host}`)); + res.type("html").send(landingPageHTML(`${proto}://${host}`, transport)); } -export function landingPageHTML(origin: string): string { +export function landingPageHTML(origin: string, transport: "sse" | "http" = "sse", runtime: "node" | "cloudflare" = "node"): string { const o = origin - const mcpConfig = {{ templateMcpIDEConfig (dict "location" "landingpage" "format" "js") }} - const codexConfig = `{{ templateMcpIDEConfig (dict "location" "landingpage" "format" "toml") }}` + const endpoint = transport === "sse" ? "sse" : "mcp"; + const headers: Array<{ name: string; env: string }> = runtime === "cloudflare" + ? {{ templateMcpLandingPageHeaders "cloudflare" }} + : transport === "sse" + ? {{ templateMcpLandingPageHeaders "node" "sse" }} + : {{ templateMcpLandingPageHeaders "node" }}; + const mcpConfig = { + command: "npx", + args: ["-y", "mcp-remote@0.1.25", `${o}/${endpoint}`, ...headers.flatMap(({ name, env }) => ["--header", `${name}:\${${env}}`])], + env: Object.fromEntries(headers.map(({ env }) => [env, "YOUR_VALUE_HERE"])), + }; + const cliHeaders = headers.map(({ name }) => `--header "${name}: ..."`).join(" "); + const codexConfig = transport === "sse" + ? `[mcp_servers.{{ sanitizeClassName $globalSDK.Type.Name }}] +command = ${JSON.stringify(mcpConfig.command)} +args = ${JSON.stringify(mcpConfig.args)} +env = { ${Object.entries(mcpConfig.env).map(([name, value]) => `${JSON.stringify(name)} = ${JSON.stringify(value)}`).join(", ")} }` + : `[mcp_servers.{{ sanitizeClassName $globalSDK.Type.Name }}] +url = ${JSON.stringify(`${o}/${endpoint}`)} +http_headers = { ${headers.map(({ name, env }) => `${JSON.stringify(name)} = ${JSON.stringify(`YOUR_${env}`)}`).join(", ")} }` + const encodedConfig = encodeURIComponent(btoa(Array.from(new TextEncoder().encode(JSON.stringify(mcpConfig)), byte => String.fromCharCode(byte)).join(""))); return ` @@ -663,7 +690,7 @@ export function landingPageHTML(origin: string): string { - +
@@ -833,7 +860,7 @@ export function landingPageHTML(origin: string): string { - ${o}/mcp + ${o}/${endpoint}
@@ -888,7 +915,7 @@ export function landingPageHTML(origin: string): string { - {{ templateMcpCLICommand (dict "client" "claude" "location" "landingpage") }} + {{ templateMcpCLICommand (dict "client" "claude" "location" "landingpage" "format" "html") }} @@ -939,7 +966,7 @@ export function landingPageHTML(origin: string): string { - {{ templateMcpCLICommand (dict "client" "gemini" "location" "landingpage") }} + {{ templateMcpCLICommand (dict "client" "gemini" "location" "landingpage" "format" "html") }} diff --git a/templates/templates/mcp-typescript/auxiliary/src/mcp-server/extensions.ts.stmpl b/templates/templates/mcp-typescript/auxiliary/src/mcp-server/extensions.ts.stmpl index 11d5673e5..547f8d449 100644 --- a/templates/templates/mcp-typescript/auxiliary/src/mcp-server/extensions.ts.stmpl +++ b/templates/templates/mcp-typescript/auxiliary/src/mcp-server/extensions.ts.stmpl @@ -1,14 +1,14 @@ {{- recurse 1 -}} {{ genImports }} +{{- addImport "zod" "z" "aliasImport" }} {{- addImport "./tools.js" "ToolDefinition" }} {{- addImport "./prompts.js" "PromptDefinition" }} {{- addImport "./resources.js" "ResourceDefinition" }} {{- addImport "./resources.js" "ResourceTemplateDefinition" }} -{{- addImport "@modelcontextprotocol/sdk/server/zod-compat.js" "ZodRawShapeCompat" }} export type Register = { - tool:
(def: ToolDefinition) => void; + tool: (def: ToolDefinition) => void; resource: (def: ResourceDefinition) => void; resourceTemplate: (def: ResourceTemplateDefinition) => void; - prompt: (prompt: PromptDefinition) => void; + prompt: (prompt: PromptDefinition) => void; } diff --git a/templates/templates/mcp-typescript/auxiliary/src/mcp-server/prompts.ts.stmpl b/templates/templates/mcp-typescript/auxiliary/src/mcp-server/prompts.ts.stmpl index 20f391d84..4ad9b32c1 100644 --- a/templates/templates/mcp-typescript/auxiliary/src/mcp-server/prompts.ts.stmpl +++ b/templates/templates/mcp-typescript/auxiliary/src/mcp-server/prompts.ts.stmpl @@ -1,27 +1,26 @@ {{- recurse 1 -}} {{- $coreName := sanitizeCoreSDKName }} {{ genImports }} +{{- addImport "./shared.js" "mcpServerContext" }} +{{- addImport "./shared.js" "mcpInputSchema" }} +{{- addImport "zod" "z" "aliasImport" }} {{- addImport "./console-logger.js" "ConsoleLogger" }} {{- addImport "../core.js" $coreName }} {{- addImport "./scopes.js" "MCPScope" }} -{{- addImport "@modelcontextprotocol/sdk/server/mcp.js" "McpServer" }} -{{- addImport "@modelcontextprotocol/sdk/shared/protocol.js" "RequestHandlerExtra" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "GetPromptResult" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ServerNotification" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ServerRequest" }} -{{- addImport "@modelcontextprotocol/sdk/server/zod-compat.js" "ShapeOutput" }} -{{- addImport "@modelcontextprotocol/sdk/server/zod-compat.js" "ZodRawShapeCompat" }} +{{- addImport "@modelcontextprotocol/server" "McpServer" }} +{{- addImport "./shared.js" "MCPServerContext" }} +{{- addImport "@modelcontextprotocol/server" "GetPromptResult" }} -export type PromptDefinition = - Args extends ZodRawShapeCompat ? { +export type PromptDefinition = + Args extends z.ZodRawShape ? { name: string; description?: string; scopes?: MCPScope[]; args: Args; prompt: ( client: {{ $coreName }}, - args: ShapeOutput, - extra: RequestHandlerExtra, + args: z.output>, + extra: MCPServerContext, ) => GetPromptResult | Promise; } : { @@ -31,7 +30,7 @@ export type PromptDefinition, + extra: MCPServerContext, ) => GetPromptResult | Promise; }; @@ -55,8 +54,8 @@ export function createRegisterPrompt( server: McpServer, getSDK: () => {{ $coreName }}, allowedScopes: Set, -): (prompt: PromptDefinition) => void { - return (prompt: PromptDefinition): void => { +): (prompt: PromptDefinition) => void { + return (prompt: PromptDefinition): void => { const scopes = prompt.scopes ?? []; if (allowedScopes.size > 0 && scopes.length === 0) { return; @@ -67,25 +66,14 @@ export function createRegisterPrompt( } if (prompt.args) { - if (prompt.description) { - server.prompt(prompt.name, prompt.description, prompt.args, async (args, ctx) => - prompt.prompt(getSDK(), args, ctx) - ); - } else { - server.prompt(prompt.name, prompt.args, async (args, ctx) => - prompt.prompt(getSDK(), args, ctx) - ); - } + server.registerPrompt(prompt.name, { + ...(prompt.description === undefined ? {} : { description: prompt.description }), + argsSchema: { ...mcpInputSchema(prompt.args), shape: prompt.args }, + }, async (args, ctx) => prompt.prompt(getSDK(), args, mcpServerContext(ctx))); } else { - if (prompt.description) { - server.prompt(prompt.name, prompt.description, async (ctx) => - prompt.prompt(getSDK(), ctx) - ); - } else { - server.prompt(prompt.name, async (ctx) => - prompt.prompt(getSDK(), ctx) - ); - } + server.registerPrompt(prompt.name, { + ...(prompt.description === undefined ? {} : { description: prompt.description }), + }, async (ctx) => prompt.prompt(getSDK(), mcpServerContext(ctx))); } logger.debug("Registered prompt", { name: prompt.name }); diff --git a/templates/templates/mcp-typescript/auxiliary/src/mcp-server/resources.ts.stmpl b/templates/templates/mcp-typescript/auxiliary/src/mcp-server/resources.ts.stmpl index f3d27d12d..9154c94c9 100644 --- a/templates/templates/mcp-typescript/auxiliary/src/mcp-server/resources.ts.stmpl +++ b/templates/templates/mcp-typescript/auxiliary/src/mcp-server/resources.ts.stmpl @@ -1,23 +1,23 @@ {{- recurse 1 -}} {{- $coreName := sanitizeCoreSDKName }} +import type { ResourceTemplate as LegacyResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; {{ genImports }} +{{- addImport "./shared.js" "mcpServerContext" }} {{- addImport "./shared.js" "valueToBase64" }} {{- addImport "./scopes.js" "MCPScope" }} {{- addImport "./console-logger.js" "ConsoleLogger" }} {{- addImport "../core.js" $coreName }} -{{- addImport "@modelcontextprotocol/sdk/server/mcp.js" "McpServer" }} -{{- addImport "@modelcontextprotocol/sdk/server/mcp.js" "ResourceMetadata" }} -{{- addImport "@modelcontextprotocol/sdk/server/mcp.js" "ResourceTemplate" }} -{{- addImport "@modelcontextprotocol/sdk/shared/uriTemplate.js" "Variables" }} -{{- addImport "@modelcontextprotocol/sdk/shared/protocol.js" "RequestHandlerExtra" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ReadResourceResult" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ServerNotification" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ServerRequest" }} +{{- addImport "@modelcontextprotocol/server" "McpServer" }} +{{- addImport "@modelcontextprotocol/server" "ResourceMetadata" }} +{{- addImport "@modelcontextprotocol/server" "ResourceTemplate" }} +{{- addImport "@modelcontextprotocol/server" "Variables" }} +{{- addImport "./shared.js" "MCPServerContext" }} +{{- addImport "@modelcontextprotocol/server" "ReadResourceResult" }} export type ReadResourceCallback = ( client: {{ $coreName }}, uri: URL, - extra: RequestHandlerExtra, + extra: MCPServerContext, ) => ReadResourceResult | Promise; export type ResourceDefinition = { @@ -33,7 +33,7 @@ export type ReadResourceTemplateCallback = ( client: {{ $coreName }}, uri: URL, vars: Variables, - extra: RequestHandlerExtra, + extra: MCPServerContext, ) => ReadResourceResult | Promise; export type ResourceTemplateDefinition = { @@ -41,7 +41,7 @@ export type ResourceTemplateDefinition = { description: string; metadata?: ResourceMetadata; scopes?: MCPScope[]; - resource: ResourceTemplate; + resource: ResourceTemplate | LegacyResourceTemplate; read: ReadResourceTemplateCallback; }; @@ -141,11 +141,11 @@ export function createRegisterResource( description: resource.description, }; - server.resource( + server.registerResource( resource.name, resource.resource, metadata, - async (uri, ctx) => resource.read(getSDK(), uri, ctx), + async (uri, ctx) => resource.read(getSDK(), uri, mcpServerContext(ctx)), ); logger.debug("Registered resource", { name: resource.name }); @@ -173,13 +173,27 @@ export function createRegisterResourceTemplate( description: resource.description, }; - server.resource( + server.registerResource( resource.name, - resource.resource, + normalizeResourceTemplate(resource.resource), metadata, - async (uri, vars, ctx) => resource.read(getSDK(), uri, vars, ctx), + async (uri, vars, ctx) => resource.read(getSDK(), uri, vars, mcpServerContext(ctx)), ); logger.debug("Registered resource template", { name: resource.name }); }; } + +function normalizeResourceTemplate(resource: ResourceTemplate | LegacyResourceTemplate): ResourceTemplate { + if (resource instanceof ResourceTemplate) return resource; + const complete: NonNullable[1]["complete"]> = {}; + for (const variable of resource.uriTemplate.variableNames) { + const callback = resource.completeCallback(variable); + if (callback) complete[variable] = callback; + } + const list = resource.listCallback; + return new ResourceTemplate(resource.uriTemplate.toString(), { + list: list ? (ctx) => list(mcpServerContext(ctx)) : undefined, + complete, + }); +} diff --git a/templates/templates/mcp-typescript/auxiliary/src/mcp-server/shared.ts.stmpl b/templates/templates/mcp-typescript/auxiliary/src/mcp-server/shared.ts.stmpl index 7724fd07a..7a422d99f 100644 --- a/templates/templates/mcp-typescript/auxiliary/src/mcp-server/shared.ts.stmpl +++ b/templates/templates/mcp-typescript/auxiliary/src/mcp-server/shared.ts.stmpl @@ -1,4 +1,149 @@ import * as z from "zod"; +import type { ServerContext, StandardSchemaWithJSON } from "@modelcontextprotocol/server"; +import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; +import type { ServerRequest, ServerNotification } from "@modelcontextprotocol/sdk/types.js"; +import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; + +export type MCPServerContext = ServerContext & RequestHandlerExtra; + +export function mcpServerContext(ctx: ServerContext): MCPServerContext { + return { + ...ctx, + signal: ctx.mcpReq.signal, + requestId: ctx.mcpReq.id, + ...(ctx.mcpReq._meta ? { _meta: ctx.mcpReq._meta } : {}), + ...(ctx.http?.authInfo ? { authInfo: ctx.http.authInfo } : {}), + ...(ctx.http?.req ? { requestInfo: { headers: Object.fromEntries(ctx.http.req.headers), url: new URL(ctx.http.req.url) } } : {}), + ...(ctx.http?.closeSSE ? { closeSSEStream: ctx.http.closeSSE } : {}), + ...(ctx.http?.closeStandaloneSSE ? { closeStandaloneSSEStream: ctx.http.closeStandaloneSSE } : {}), + sendNotification: async (notification) => { + if (!ctx.mcpReq.signal.aborted) await ctx.mcpReq.notify(notification); + }, + sendRequest: async (request, resultSchema, options) => { + if (ctx.mcpReq.signal.aborted) throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled"); + return (ctx.mcpReq.send as MCPServerContext["sendRequest"])(request, resultSchema, options); + }, + }; +} + +export function mcpInputSchema(shape: Shape): StandardSchemaWithJSON>, z.output>> { + const schema = z.object(shape); + const standard = schema["~standard"]; + return { + "~standard": { + ...standard, + jsonSchema: { + input: (options) => { + const source = jsonSchemaSource(schema); + return source.schema["~standard"].jsonSchema.input({ + ...options, + libraryOptions: { ...options.libraryOptions, metadata: source.metadata, override: source.override, unrepresentable: "any" }, + }); + }, + output: (options) => { + const source = jsonSchemaSource(schema); + return source.schema["~standard"].jsonSchema.output({ + ...options, + libraryOptions: { ...options.libraryOptions, metadata: source.metadata, override: source.override, unrepresentable: "any" }, + }); + }, + }, + }, + }; +} + +// Zod serializes defaults before its override runs; clone only the conversion schema to preserve runtime defaults. +function jsonSchemaSource(schema: z.ZodType) { + const registry = z.registry(); + const copies = new WeakMap(); + const defaults = new WeakMap(); + const jsonValue = (value: unknown, onNormalized?: () => void): unknown => value === undefined ? undefined : JSON.parse( + JSON.stringify(value, function (this: Record, key, item: unknown) { + if (this[key] instanceof Date) onNormalized?.(); + if (typeof item !== "bigint") return item; + onNormalized?.(); + return item.toString(); + }), + ); + const copy = (value: unknown): unknown => { + if (typeof value !== "object" || value === null) return value; + if (copies.has(value)) return copies.get(value); + if (value instanceof z.ZodType) { + const def = copy(value.def) as z.ZodType["def"]; + const cloned = value.clone(def); + copies.set(value, cloned); + const metadata = value.meta(); + if (metadata) registry.add(cloned, jsonValue(metadata) as typeof metadata); + if (value instanceof z.ZodDefault || value instanceof z.ZodPrefault) { + let hasNormalizedDefault = false; + const normalized = jsonValue(value.def.defaultValue, () => { hasNormalizedDefault = true; }); + if (hasNormalizedDefault) { + defaults.set(cloned, undefined); + try { + if (z.safeParse(value.def.innerType, normalized).success) defaults.set(cloned, normalized); + } catch { + return cloned; + } + } + } + if (value instanceof z.ZodCatch) { + (def as z.ZodCatch["def"]).catchValue = (ctx) => { + let fallback: unknown; + try { + fallback = value.def.catchValue(ctx); + } catch (error) { + if (ctx !== undefined) throw error; + defaults.set(cloned, undefined); + return undefined; + } + let hasNormalizedDefault = false; + const normalized = jsonValue(fallback, () => { hasNormalizedDefault = true; }); + if (hasNormalizedDefault) { + defaults.set(cloned, undefined); + try { + if (z.safeParse(value.def.innerType, normalized).success) defaults.set(cloned, normalized); + } catch { + return normalized; + } + } + return normalized; + }; + } + return cloned; + } + if (Array.isArray(value)) { + const cloned: unknown[] = []; + copies.set(value, cloned); + for (const item of value) cloned.push(copy(item)); + return cloned; + } + if (Object.getPrototypeOf(value) !== Object.prototype) return value; + const cloned: Record = {}; + copies.set(value, cloned); + for (const key of Object.keys(value)) { + const descriptor = Object.getOwnPropertyDescriptor(value, key)!; + const read = () => { + const item: unknown = Reflect.get(value, key); + if (key === "defaultValue") return jsonValue(item); + if (key === "getter" && typeof item === "function") return () => copy(item()); + return copy(item); + }; + if (descriptor.get) { + Object.defineProperty(cloned, key, { enumerable: true, get: read }); + } else { + cloned[key] = read(); + } + } + return cloned; + }; + const override: NonNullable = ({ zodSchema, jsonSchema }) => { + if (!defaults.has(zodSchema)) return; + const value = defaults.get(zodSchema); + if (value === undefined) delete jsonSchema.default; + else jsonSchema.default = value; + }; + return { schema: copy(schema) as z.ZodType, metadata: registry, override }; +} type BinaryData = | Uint8Array diff --git a/templates/templates/mcp-typescript/auxiliary/src/mcp-server/tools.ts.stmpl b/templates/templates/mcp-typescript/auxiliary/src/mcp-server/tools.ts.stmpl index a86d90cc8..ff133ccd5 100644 --- a/templates/templates/mcp-typescript/auxiliary/src/mcp-server/tools.ts.stmpl +++ b/templates/templates/mcp-typescript/auxiliary/src/mcp-server/tools.ts.stmpl @@ -6,16 +6,14 @@ {{- addImport "./scopes.js" "mcpScopes" -}} {{- addImport "./console-logger.js" "ConsoleLogger" -}} {{- addImport "../core.js" $coreName -}} -{{- addImport "@modelcontextprotocol/sdk/server/mcp.js" "McpServer" -}} -{{- addImport "@modelcontextprotocol/sdk/shared/protocol.js" "RequestHandlerExtra" -}} -{{- addImport "@modelcontextprotocol/sdk/types.js" "CallToolResult" -}} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ServerNotification" -}} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ServerRequest" -}} -{{- addImport "@modelcontextprotocol/sdk/server/zod-compat.js" "ShapeOutput" -}} -{{- addImport "@modelcontextprotocol/sdk/server/zod-compat.js" "ZodRawShapeCompat" -}} +{{- addImport "@modelcontextprotocol/server" "McpServer" -}} +{{- addImport "./shared.js" "MCPServerContext" -}} +{{- addImport "@modelcontextprotocol/server" "CallToolResult" -}} {{- addImport "zod" "z" "aliasImport" -}} {{ genImports }} +{{- addImport "./shared.js" "mcpServerContext" }} +{{- addImport "./shared.js" "mcpInputSchema" }} export type MCPToolAnnotationFilter = { readOnlyHint?: boolean; @@ -50,6 +48,9 @@ export function buildAnnotationFilter(annotations: string[] | undefined): MCPToo return filter; } +type ZodRawShapeCompat = z.ZodRawShape; +type ShapeOutput = z.output>; + export type ToolDefinition = Args extends ZodRawShapeCompat ? { @@ -67,7 +68,7 @@ export type ToolDefinition, - extra: RequestHandlerExtra + extra: MCPServerContext ) => CallToolResult | Promise; } : { @@ -84,7 +85,7 @@ export type ToolDefinition + extra: MCPServerContext ) => CallToolResult | Promise; }; @@ -183,13 +184,13 @@ export function createRegisterTool( dynamic?: boolean, annotationFilter?: MCPToolAnnotationFilter, ): [ - (tool: ToolDefinition) => void, + (tool: ToolDefinition) => void, Array<{ name: string; description: string }>, - Map>, + Map>, ] { const tools: Array<{ name: string; description: string }> = []; - const toolMap = new Map>(); - const registerTool = (tool: ToolDefinition): void => { + const toolMap = new Map>(); + const registerTool = (tool: ToolDefinition): void => { if (allowedTools && !allowedTools.has(tool.name)) { return; } @@ -219,7 +220,7 @@ export function createRegisterTool( } } - toolMap.set(tool.name, tool as ToolDefinition); + toolMap.set(tool.name, tool as ToolDefinition); if (dynamic) { logger.debug("Collected tool for dynamic mode", { name: tool.name }); @@ -232,11 +233,11 @@ export function createRegisterTool( tool.name, { description: tool.description, - inputSchema: tool.args, + inputSchema: mcpInputSchema(tool.args), annotations: tool.annotations, }, async (args, ctx) => { - return tool.tool(getSDK(), args, ctx); + return tool.tool(getSDK(), args, mcpServerContext(ctx)); }, ); } else { @@ -247,7 +248,7 @@ export function createRegisterTool( annotations: tool.annotations, }, async (ctx) => { - return tool.tool(getSDK(), ctx); + return tool.tool(getSDK(), mcpServerContext(ctx)); }, ); } @@ -262,7 +263,7 @@ export function createRegisterTool( function matchesSearchTerms( terms: string[], name: string, - def: ToolDefinition, + def: ToolDefinition, ): boolean { if (terms.length === 0) { return true; @@ -285,14 +286,14 @@ export function registerDynamicTools( logger: ConsoleLogger, server: McpServer, getSDK: () => {{ $coreName }}, - toolMap: Map>, + toolMap: Map>, allowedScopes: Set, ): void { // 1. list_tools server.registerTool("list_tools", { description: "List available tools. Optionally filter by search terms that match against tool name, description, and scopes.", - inputSchema: { + inputSchema: z.object({ search_terms: z.array(z.string()).optional().describe( [ "Filter the list of tools.", @@ -301,7 +302,7 @@ export function registerDynamicTools( "If not provided, all tools are returned.", ].join("\n"), ), - }, + }), annotations: { title: "List Tools", readOnlyHint: true, @@ -331,9 +332,9 @@ export function registerDynamicTools( // 2. describe_tool server.registerTool("describe_tool_input", { description: "Get the input schema for one or more tools. It is a good idea to call this tool first to understand how to successfully call execute_tool.", - inputSchema: { + inputSchema: z.object({ tool_names: z.array(z.string()).describe("The names of the tools to describe"), - }, + }), annotations: { title: "Describe Tool", readOnlyHint: true, @@ -358,7 +359,7 @@ export function registerDynamicTools( let schemaText = `\n\n`; if (def.args) { - const jsonSchema = z.toJSONSchema(z.object(def.args), { target: "draft-2020-12", io: "input", unrepresentable: "any" }); + const jsonSchema = mcpInputSchema(def.args)["~standard"].jsonSchema.input({ target: "draft-2020-12" }); schemaText += JSON.stringify(jsonSchema, null, 2) } else { schemaText += "This tool takes no input parameters."; @@ -378,12 +379,12 @@ export function registerDynamicTools( // 3. execute_tool server.registerTool("execute_tool", { description: "Execute a tool by name with its arguments. If executing a given tool for the first time, it is recommended to call describe_tool_input first to understand the expected `arguments` shape.", - inputSchema: { + inputSchema: z.object({ name: z.string().describe("The name of the tool to execute"), arguments: z.looseObject({}).optional().describe( "Arguments for the target tool as a JSON object, matching the schema returned by describe_tool_input.", ), - }, + }), annotations: { title: "Execute Tool", readOnlyHint: false, @@ -402,7 +403,7 @@ export function registerDynamicTools( let validatedInput: Record = {}; if (def.args) { - const vres = z.object(def.args).safeParse(args.arguments ?? {}); + const vres = await z.object(def.args).safeParseAsync(args.arguments ?? {}); if (vres.success) { validatedInput = vres.data; } else { @@ -421,9 +422,9 @@ export function registerDynamicTools( try { if (def.args) { - return await def.tool(getSDK(), validatedInput, ctx); + return await def.tool(getSDK(), validatedInput, mcpServerContext(ctx)); } else { - return await def.tool(getSDK(), ctx); + return await def.tool(getSDK(), mcpServerContext(ctx)); } } catch (error) { const message = error instanceof Error ? error.message : String(error); @@ -485,7 +486,11 @@ function resolveHeader( if (val != null) { return schema.parse(val); } - return disableStaticAuth ? undefined : schema.parse(cliFlagValue); + if (disableStaticAuth || cliFlagValue === undefined) { + const parsed = schema.safeParse(undefined); + return parsed.success ? parsed.data : undefined; + } + return schema.parse(cliFlagValue); } export function buildSDK( diff --git a/templates/templates/mcp-typescript/auxiliary/worker-configuration.d.ts.stmpl b/templates/templates/mcp-typescript/auxiliary/worker-configuration.d.ts.stmpl index 10489a2a1..62f28fba6 100644 --- a/templates/templates/mcp-typescript/auxiliary/worker-configuration.d.ts.stmpl +++ b/templates/templates/mcp-typescript/auxiliary/worker-configuration.d.ts.stmpl @@ -4,7 +4,8 @@ // Runtime types generated with workerd@1.20250709.0 2025-07-15 nodejs_compat declare namespace Cloudflare { interface Env { - {{sanitizeEnvVarName .Global.Config.PackageName}}_MCP: DurableObjectNamespace; + {{sanitizeEnvVarName .Global.Config.PackageName}}_MCP: DurableObjectNamespace; + ASSETS: Fetcher; } } interface Env extends Cloudflare.Env {} diff --git a/templates/templates/mcp-typescript/auxiliary/wrangler.toml.stmpl b/templates/templates/mcp-typescript/auxiliary/wrangler.toml.stmpl index 31617c05f..cb1fd50ed 100644 --- a/templates/templates/mcp-typescript/auxiliary/wrangler.toml.stmpl +++ b/templates/templates/mcp-typescript/auxiliary/wrangler.toml.stmpl @@ -6,14 +6,13 @@ main = "src/cloudflare-worker/cloudflare-worker.ts" compatibility_date = "2025-07-15" # Enable node compatibility for MCP SDK -compatibility_flags = ["nodejs_compat"] +compatibility_flags = ["nodejs_compat", "enable_request_signal"] -# Durable Object binding for MCP Agent +# Durable Object hosting legacy SSE and Streamable HTTP sessions; existing namespaces and stored state are kept [[durable_objects.bindings]] name = "{{ sanitizeEnvVarName .Global.Config.PackageName }}_MCP" class_name = "{{ sanitizeClassName .Global.Config.PackageName }}MCP" -# Durable Object migration [[migrations]] tag = "v1" new_classes = ["{{ sanitizeClassName .Global.Config.PackageName }}MCP"] diff --git a/templates/templates/mcp-typescript/config.ts b/templates/templates/mcp-typescript/config.ts index 5b8fee09c..0e3a33d70 100644 --- a/templates/templates/mcp-typescript/config.ts +++ b/templates/templates/mcp-typescript/config.ts @@ -1,6 +1,6 @@ // @ts-ignore function getCompileDependencies( - _config: GeneratorInitialConfiguration, + config: GeneratorInitialConfiguration, ): RunnerCommandDependencies { return [ { @@ -8,7 +8,7 @@ function getCompileDependencies( version: { args: ["--version"], regex: `(?m).*?(\\d+\\.\\d+\\.\\d+).*?`, - minVersion: "18.0.0", + minVersion: config.LangCfg.gramEnabled ? "22.18.0" : "20.0.0", }, installDocumentation: `Install Node.js by following the instructions at https://nodejs.org/en/download/.`, }, @@ -72,7 +72,36 @@ function getTemplateDependencies() { }, "@modelcontextprotocol/sdk": { name: "@modelcontextprotocol/sdk", - version: "1.26.0", + version: "^1.32.1", + cpe: "cpe:2.3:a:anthropic:model_context_protocol_sdk:*:*:*:*:*:node.js:*:*", + ecosystem: "npm", + category: "runtime", + }, + "@modelcontextprotocol/client": { + name: "@modelcontextprotocol/client", + version: "2.3.1", + cpe: "cpe:2.3:a:anthropic:model_context_protocol_sdk:*:*:*:*:*:node.js:*:*", + ecosystem: "npm", + category: "runtime", + condition: "gram", + }, + "@modelcontextprotocol/server": { + name: "@modelcontextprotocol/server", + version: "2.3.1", + cpe: "cpe:2.3:a:anthropic:model_context_protocol_sdk:*:*:*:*:*:node.js:*:*", + ecosystem: "npm", + category: "runtime", + }, + "@modelcontextprotocol/server-legacy": { + name: "@modelcontextprotocol/server-legacy", + version: "2.3.1", + cpe: "cpe:2.3:a:anthropic:model_context_protocol_sdk:*:*:*:*:*:node.js:*:*", + ecosystem: "npm", + category: "runtime", + }, + "@modelcontextprotocol/node": { + name: "@modelcontextprotocol/node", + version: "2.1.1", cpe: "cpe:2.3:a:anthropic:model_context_protocol_sdk:*:*:*:*:*:node.js:*:*", ecosystem: "npm", category: "runtime", @@ -86,7 +115,7 @@ function getTemplateDependencies() { }, zod: { name: "zod", - version: "^4.0.0", + version: "^4.2.0", cpe: "cpe:2.3:a:colinhacks:zod:*:*:*:*:*:node.js:*:*", ecosystem: "npm", category: "runtime", @@ -119,14 +148,6 @@ function getTemplateDependencies() { ecosystem: "npm", category: "runtime", }, - agents: { - name: "agents", - version: "0.3.10", - cpe: "cpe:2.3:a:cloudflare:agents:*:*:*:*:*:node.js:*:*", - ecosystem: "npm", - category: "runtime", - condition: "cloudflare", - }, "@gram-ai/functions": { name: "@gram-ai/functions", version: "^0.12.1", @@ -216,7 +237,7 @@ function getTemplateDependencies() { }, "@cloudflare/workers-types": { name: "@cloudflare/workers-types", - version: "^4.20250712.0", + version: "^4.20250712.0 || ^5.20261006.1", cpe: "cpe:2.3:a:cloudflare:workers-types:*:*:*:*:*:node.js:*:*", ecosystem: "npm", category: "dev", diff --git a/templates/templates/mcp-typescript/includes/dependencies.ts b/templates/templates/mcp-typescript/includes/dependencies.ts index 1fd784f01..9aa030254 100644 --- a/templates/templates/mcp-typescript/includes/dependencies.ts +++ b/templates/templates/mcp-typescript/includes/dependencies.ts @@ -9,6 +9,11 @@ function templateDependencies(): string { const defaultDependencies = { express: deps.express.version, "@modelcontextprotocol/sdk": deps["@modelcontextprotocol/sdk"].version, + "@modelcontextprotocol/server": + deps["@modelcontextprotocol/server"].version, + "@modelcontextprotocol/server-legacy": + deps["@modelcontextprotocol/server-legacy"].version, + "@modelcontextprotocol/node": deps["@modelcontextprotocol/node"].version, "@stricli/core": deps["@stricli/core"].version, zod: deps.zod.version, ...(context.Global.Config.EvalProvider @@ -22,11 +27,9 @@ function templateDependencies(): string { : {}), }; - if (context.Global.Config.CloudflareEnabled) { - defaultDependencies["agents"] = deps.agents.version; - } - if (context.Global.Config.GramEnabled) { + defaultDependencies["@modelcontextprotocol/client"] = + deps["@modelcontextprotocol/client"].version; defaultDependencies["@gram-ai/functions"] = deps["@gram-ai/functions"].version; } @@ -67,6 +70,10 @@ function templateDevDependencies(): string { sdkHasTests(context.Global.AST) ) { defaultDependencies["vitest"] = deps.vitest.version; + if (!context.Global.Config.GramEnabled) { + defaultDependencies["@modelcontextprotocol/client"] = + deps["@modelcontextprotocol/client"].version; + } } return renderDependencies( diff --git a/templates/templates/mcp-typescript/includes/mcp-server.ts b/templates/templates/mcp-typescript/includes/mcp-server.ts index 533c9eda4..e5fb87b81 100644 --- a/templates/templates/mcp-typescript/includes/mcp-server.ts +++ b/templates/templates/mcp-typescript/includes/mcp-server.ts @@ -863,11 +863,12 @@ function templateMCPFlagHeaderOverrides(security: TypeDef): string { if (seen.has(flagName)) continue; seen.add(flagName); - // Express/Node.js normalizes req.headers keys to lowercase, so we must - // lowercase the field name to match. const headerName = originalFieldName(field).toLowerCase(); const fieldType = field.Type.Type.toString(); - const header = `req.headers["${headerName}"]`; + const header = + headerName === flagName + ? `req.headers["${flagName}"]` + : `(req.headers["${flagName}"] ?? req.headers["${headerName}"])`; const fallback = `cliFlags["${flagName}"]`; let headerExpr: string; @@ -881,7 +882,7 @@ function templateMCPFlagHeaderOverrides(security: TypeDef): string { headerExpr = `(typeof ${header} === "string" ? Number(${header}) : undefined)`; break; case "array": - headerExpr = `(typeof ${header} === "string" ? ${header}.split(",") : undefined)`; + headerExpr = `(typeof ${header} === "string" ? (${header} as string).split(",") : undefined)`; break; case "enum": headerExpr = `(${header} as string as typeof ${fallback})`; diff --git a/templates/templates/mcp-typescript/includes/templating.ts b/templates/templates/mcp-typescript/includes/templating.ts index 41197bf04..64f8b3c5a 100644 --- a/templates/templates/mcp-typescript/includes/templating.ts +++ b/templates/templates/mcp-typescript/includes/templating.ts @@ -722,8 +722,46 @@ function addHeadersToConfigVSCode(config: any, headerEntries: any[]) { return config; } +function mcpRemoteHeaders( + runtime: "node" | "cloudflare", + transport: "http" | "sse" = "http", +) { + const sdk = context.Global.AST.MainSDK; + const security = sdk.Security?.Type; + const nodeHeader = (fieldName: string) => { + const name = sanitizeMCPCLIFlag(fieldName); + return { name, env: caser().ToSNAKE(name) }; + }; + const entries = + runtime === "cloudflare" + ? getRemoteServerHeaders(security) + .flat() + .map((header) => ({ + name: header.headerName, + env: caser().ToSNAKE(header.fieldName), + })) + : (security ? unnestSecurityEnvFields(security) : []).map((field) => + nodeHeader(field.Name), + ); + if (security && runtime === "node" && transport === "http") { + for (const field of sdk.Globals?.Fields ?? []) { + entries.push(nodeHeader(templateGlobalFieldName(field.Name))); + } + for (const variable of sdk.Servers?.GetVariables() ?? []) { + entries.push(nodeHeader(variable.Name)); + } + } + return [...new Map(entries.map((header) => [header.name, header])).values()]; +} + +registerTemplateFunc( + "templateMcpLandingPageHeaders", + (runtime: "node" | "cloudflare", transport: "http" | "sse" = "http") => + JSON.stringify(mcpRemoteHeaders(runtime, transport)), +); + function mcpRemoteConfigCommand( - endpointType: string = "sse", + endpointType: string = "mcp", clientType: string = "default", url?: string, ) { @@ -733,10 +771,11 @@ function mcpRemoteConfigCommand( "https://example-cloudflare-worker.com"; const headers: string[] = []; - const flags = gatherMCPFlags(); - for (const flag of flags) { - let envVar = `${caser().ToSNAKE(flag.Name)}`; - headers.push("--header", `${flag.Name}:\${${envVar}}`); + for (const header of mcpRemoteHeaders( + context.Global.Config.CloudflareEnabled ? "cloudflare" : "node", + endpointType === "sse" ? "sse" : "http", + )) { + headers.push("--header", `${header.name}:\${${header.env}}`); } return [ @@ -772,7 +811,7 @@ function mcpIDEConfigObject( executor?: Executor; } = {}, ) { - const { location = "readme", endpoint = "sse", executor = "npx" } = options; + const { location = "readme", endpoint = "mcp", executor = "npx" } = options; if (context.Global.Config.CloudflareEnabled && location === "landingpage") { const command = mcpRemoteConfigCommand(endpoint); @@ -787,7 +826,7 @@ function templateMcpIDEConfig(options: MCPIDEConfigOptions = {}) { const { location = "readme", format = "json", - endpoint = "sse", + endpoint = "mcp", executor = "npx", } = options; @@ -888,16 +927,17 @@ registerTemplateFunc( function mcpHttpHeadersTOML( options: { escapeForTemplateLiteral?: boolean } = {}, ) { - const flags = gatherMCPFlags(); - if (flags.length === 0) { + const fields = mcpRemoteHeaders( + context.Global.Config.CloudflareEnabled ? "cloudflare" : "node", + ); + if (fields.length === 0) { return "http_headers = { }"; } const headers: string[] = []; - for (const flag of flags) { - const envVar = `${caser().ToSNAKE(flag.Name)}`; - const placeholder = `YOUR_${envVar}`; - headers.push(`"${flag.Name}" = "${placeholder}"`); + for (const field of fields) { + const placeholder = `YOUR_${field.env}`; + headers.push(`"${field.name}" = "${placeholder}"`); } return `http_headers = { ${headers.join(", ")} }`; @@ -910,8 +950,8 @@ function mcpRemoteConfigJS() { let js = `{ "mcpServers": { "${mcpName()}": { - "type": "sse", - "url": \`\${o}/sse\``; + "type": "http", + "url": \`\${o}/mcp\``; if (headerEntries.length > 0) { js += `, @@ -946,12 +986,14 @@ function mcpRemoteConfigJS() { return js; } -function templateMcpCursorInstallationURL() { - const configString = base64Encode( - JSON.stringify( - mcpIDEConfigObject({ location: "landingpage", executor: "npx" }), - ), - ); +function templateMcpCursorInstallationURL(runtimeConfig = false) { + const configString = runtimeConfig + ? "${encodedConfig}" + : base64Encode( + JSON.stringify( + mcpIDEConfigObject({ location: "landingpage", executor: "npx" }), + ), + ); const name = mcpName(); return `cursor://anysphere.cursor-deeplink/mcp/install?name=${name}&config=${configString}`; } @@ -969,12 +1011,14 @@ registerTemplateFunc( templateMcpCursorInstallationButton, ); -function templateMcpVSCodeInstallationURL() { - const configString = base64Encode( - JSON.stringify( - mcpIDEConfigObject({ location: "landingpage", executor: "npx" }), - ), - ); +function templateMcpVSCodeInstallationURL(runtimeConfig = false) { + const configString = runtimeConfig + ? "${encodedConfig}" + : base64Encode( + JSON.stringify( + mcpIDEConfigObject({ location: "landingpage", executor: "npx" }), + ), + ); const name = mcpName(); return `vscode://ms-vscode.vscode-mcp/install?name=${name}&config=${configString}`; } @@ -1019,21 +1063,15 @@ function templateMcpCLICommand(options: MCPCLICommandOptions) { // HTML format: uses ${o} placeholder for dynamic URL (for landing page) if (format === "html") { - const headers = getRemoteServerHeaders( - context.Global.AST.MainSDK.Security?.Type, - ); - const headerArgs = headers.flatMap((header) => - header.map((h) => `--header "${h.headerName}: ..."`), - ); return [ client, "mcp", "add", "--transport", - "sse", + "${transport}", mcpName(), - `\${o}/sse`, - ...headerArgs, + `\${o}/\${endpoint}`, + "${cliHeaders}", ].join(" "); } @@ -1046,9 +1084,9 @@ function templateMcpCLICommand(options: MCPCLICommandOptions) { "mcp", "add", "--transport", - "sse", + "http", mcpName(), - `${cloudflareURL}/sse`, + `${cloudflareURL}/mcp`, ]; } else { // Local configuration diff --git a/templates/templates/mcp-typescript/mcp-server/cli/eval/impl.ts.stmpl b/templates/templates/mcp-typescript/mcp-server/cli/eval/impl.ts.stmpl index c40b9058d..f09307181 100644 --- a/templates/templates/mcp-typescript/mcp-server/cli/eval/impl.ts.stmpl +++ b/templates/templates/mcp-typescript/mcp-server/cli/eval/impl.ts.stmpl @@ -12,7 +12,7 @@ import { } from "ai"; import { {{ .Local.EvalProvider.ImportName }} } from "{{ .Local.EvalProvider.Package }}"; import { createMCPClient } from "@ai-sdk/mcp"; -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { InMemoryTransport } from "@modelcontextprotocol/server"; import * as readline from "node:readline/promises"; import { LocalContext } from "../../cli.js"; import { diff --git a/templates/templates/mcp-typescript/mcp-server/cli/serve/impl.ts.stmpl b/templates/templates/mcp-typescript/mcp-server/cli/serve/impl.ts.stmpl index 8089b1ed2..79f80c9d6 100644 --- a/templates/templates/mcp-typescript/mcp-server/cli/serve/impl.ts.stmpl +++ b/templates/templates/mcp-typescript/mcp-server/cli/serve/impl.ts.stmpl @@ -4,8 +4,8 @@ {{- $usageLocation := "mcp-server/cli/serve" -}} {{- $serverMap := (and $globalSDK.Servers $globalSDK.Servers.ServerMap) -}} {{- $hasServers := (and $globalSDK.Servers $globalSDK.Servers.HasAbsoluteURL) -}} -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { toNodeHandler } from "@modelcontextprotocol/node"; +import { createMcpHandler } from "@modelcontextprotocol/server"; import express from "express"; import { LocalContext } from "../../cli.js"; import { @@ -19,7 +19,7 @@ import { buildAnnotationFilter } from "../../tools.js"; import { buildSDK } from "../../tools.js"; {{- end }} {{ genImports }} -import { landingPageExpress } from "../../../landing-page.js"; +import { landingPageExpressHTTP } from "../../../landing-page.js"; interface ServeCommandFlags extends MCPServerFlags { readonly port: number; @@ -43,7 +43,7 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { // Enable CORS for cross-origin requests app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); - res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); res.header("Access-Control-Allow-Headers", "*"); if (req.method === "OPTIONS") { res.sendStatus(204); @@ -54,20 +54,11 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { app.use(express.json()); - app.post("/mcp", async (req, res) => { + const handler = createMcpHandler(({{ if $globalSecurity }}{ requestInfo }{{ end }}) => { {{- if $globalSecurity }} - const headers = new Headers(); - for (const [key, value] of Object.entries(req.headers)) { - if (Array.isArray(value)) { - for (const v of value) headers.append(key, v); - } else if (value !== undefined) { - headers.set(key, value); - } - } + const headers = requestInfo?.headers ?? new Headers(); {{- end }} - const transport = new StreamableHTTPServerTransport({}); - const { server: mcpServer } = createMCPServer({ logger, allowedTools: cliFlags.tool, @@ -106,11 +97,37 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { }); }; - await mcpServer.connect(transport as Transport); - await transport.handleRequest(req, res, req.body); + return mcpServer; + }); + const nodeHandler = toNodeHandler(handler); + const activeResponses = new Set>(); + let closing = false; + app.use((_req, res, next) => { + if (closing) { + res.set("Connection", "close").status(503).send("Server is shutting down"); + return; + } + next(); + }); + app.all("/mcp", (req, res) => { + // Notification streams remain open until handler.close(). + if (req.method !== "GET" && req.body?.method !== "subscriptions/listen") { + let finish!: () => void; + const completed = new Promise((resolve) => { finish = resolve; }); + const done = () => { + res.off("finish", done); + res.off("close", done); + activeResponses.delete(completed); + finish(); + }; + activeResponses.add(completed); + res.once("finish", done); + res.once("close", done); + } + return nodeHandler(req, res, req.body); }); - app.get("/", landingPageExpress); + app.get("/", landingPageExpressHTTP); const httpServer = app.listen(cliFlags.port, "0.0.0.0", () => { const ha = httpServer.address(); @@ -118,7 +135,11 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { logger.info("MCP Streamable HTTP server started", { host }); }); - const shutdown = () => { + const shutdown = async () => { + if (closing) { + process.exit(1); + } + closing = true; logger.info("Shutting down HTTP server"); const timer = setTimeout(() => { @@ -126,11 +147,14 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { process.exit(1); }, 5000); - httpServer.close(() => { - clearTimeout(timer); - logger.info("Graceful shutdown complete"); - process.exit(0); - }); + const stopped = new Promise((resolve) => httpServer.close(() => resolve())); + await Promise.all(activeResponses); + await handler.close(); + httpServer.closeAllConnections(); + await stopped; + clearTimeout(timer); + logger.info("Graceful shutdown complete"); + process.exit(0); }; process.on("SIGTERM", shutdown); diff --git a/templates/templates/mcp-typescript/mcp-server/cli/start/impl.ts.stmpl b/templates/templates/mcp-typescript/mcp-server/cli/start/impl.ts.stmpl index c95bc107f..e70b339aa 100644 --- a/templates/templates/mcp-typescript/mcp-server/cli/start/impl.ts.stmpl +++ b/templates/templates/mcp-typescript/mcp-server/cli/start/impl.ts.stmpl @@ -2,8 +2,8 @@ {{- $globalSDK := .Global.AST.MainSDK -}} {{- $globalSecurity := $globalSDK.Security -}} {{- $usageLocation := "mcp-server/cli/start" -}} -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { SSEServerTransport } from "@modelcontextprotocol/server-legacy/sse"; +import { serveStdio } from "@modelcontextprotocol/server/stdio"; import express from "express"; import { LocalContext } from "../../cli.js"; import { @@ -48,18 +48,21 @@ export async function main(this: LocalContext, flags: StartCommandFlags) { async function startStdio(flags: StartCommandFlags) { const logger = createConsoleLogger(flags["log-level"]); - const transport = new StdioServerTransport(); - const { server } = {{ templateString "mcp-server/create-mcp-server.ts.stmpl" .Local }}; - await server.connect(transport); + const createServer = {{ templateString "mcp-server/create-mcp-server.ts.stmpl" (dict "Factory" true "Scopes" .Local.Scopes "UsageLocation" .Local.UsageLocation) }}; + const handle = serveStdio(createServer); const abort = async () => { - await server.close(); + await handle.close(); process.exit(0); }; process.on("SIGTERM", abort); process.on("SIGINT", abort); } +function createMCPServerFactory(options: Parameters[0]) { + return () => createMCPServer(options).server; +} + async function startSSE(cliFlags: StartCommandFlags) { const logger = createConsoleLogger(cliFlags["log-level"]); const app = express(); diff --git a/templates/templates/mcp-typescript/mcp-server/create-mcp-server.ts.stmpl b/templates/templates/mcp-typescript/mcp-server/create-mcp-server.ts.stmpl index a4acdae03..e8cd8224a 100644 --- a/templates/templates/mcp-typescript/mcp-server/create-mcp-server.ts.stmpl +++ b/templates/templates/mcp-typescript/mcp-server/create-mcp-server.ts.stmpl @@ -2,7 +2,7 @@ {{- $globalSecurity := $globalSDK.Security }} {{- $serverMap := (and $globalSDK.Servers $globalSDK.Servers.ServerMap) -}} {{- $usageLocation := .Local.UsageLocation -}} -createMCPServer({ +{{ if .Local.Factory }}createMCPServerFactory{{ else }}createMCPServer{{ end }}({ logger, allowedTools: flags.tool, dynamic: flags.mode === "dynamic", diff --git a/templates/templates/mcp-typescript/mcp-server/gram.ts.stmpl b/templates/templates/mcp-typescript/mcp-server/gram.ts.stmpl index 6ecaccb35..1442619a5 100644 --- a/templates/templates/mcp-typescript/mcp-server/gram.ts.stmpl +++ b/templates/templates/mcp-typescript/mcp-server/gram.ts.stmpl @@ -5,13 +5,100 @@ {{- $hasServers := (and $globalSDK.Servers $globalSDK.Servers.HasAbsoluteURL) -}} // DO NOT EDIT: This file is auto-generated by Speakeasy. // Gram deployment wrapper for MCP server. -import { withGram } from "@gram-ai/functions/mcp"; +import type { Manifest, ManifestVariables } from "@gram-ai/functions"; +import type { WrappedMCPServer } from "@gram-ai/functions/mcp"; +import { Client, ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/client"; +import { InMemoryTransport, McpServer } from "@modelcontextprotocol/server"; import { createConsoleLogger } from "./console-logger.js"; import { MCPServerFlags } from "./flags.js"; import { createMCPServer } from "./server.js"; import { buildAnnotationFilter } from "./tools.js"; {{ genImports }} +export async function wrapGramServer( + server: McpServer, + options?: { variables?: Variables }, +): Promise { + const client = new Client({ name: "gram-functions-mcp", version: "0.0.0" }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const capabilities = client.getServerCapabilities(); + const listedTools = capabilities?.tools + ? await client.listTools().catch((error: unknown) => { + if (error instanceof ProtocolError && error.code === ProtocolErrorCode.MethodNotFound) { + return { tools: [] }; + } + throw error; + }) + : { tools: [] }; + const listedResources = capabilities?.resources + ? await client.listResources().catch((error: unknown) => { + if (error instanceof ProtocolError && error.code === ProtocolErrorCode.MethodNotFound) { + return { resources: [] }; + } + throw error; + }) + : { resources: [] }; + const variables = options?.variables; + const tools: NonNullable = listedTools.tools.map((tool) => ({ + name: tool.name, + ...(tool.description === undefined ? {} : { description: tool.description }), + inputSchema: tool.inputSchema, + ...(tool.annotations === undefined ? {} : { + annotations: { + ...(tool.annotations.title === undefined ? {} : { title: tool.annotations.title }), + ...(tool.annotations.readOnlyHint === undefined ? {} : { readOnlyHint: tool.annotations.readOnlyHint }), + ...(tool.annotations.destructiveHint === undefined ? {} : { destructiveHint: tool.annotations.destructiveHint }), + ...(tool.annotations.idempotentHint === undefined ? {} : { idempotentHint: tool.annotations.idempotentHint }), + ...(tool.annotations.openWorldHint === undefined ? {} : { openWorldHint: tool.annotations.openWorldHint }), + }, + }), + ...(variables === undefined ? {} : { variables }), + meta: { ...tool._meta, "gram.ai/kind": "mcp-passthrough" }, + })); + const resources: NonNullable = listedResources.resources.map((resource) => ({ + name: resource.name, + description: resource.description, + uri: resource.uri, + mimeType: resource.mimeType, + title: resource.title, + ...(variables === undefined ? {} : { variables }), + meta: { ...resource._meta, "gram.ai/kind": "mcp-passthrough" }, + })); + + return { + manifest: () => ({ version: "0.0.0", tools, resources }), + async handleToolCall(call) { + const response = await client.callTool({ + name: call.name, + ...(call.input === undefined ? {} : { arguments: call.input }), + ...(call._meta === undefined ? {} : { _meta: call._meta }), + }); + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json; mcp=tools_call" }, + }); + }, + async handleResources(call) { + const response = await client.readResource({ + uri: call.uri, + ...(call._meta === undefined ? {} : { _meta: call._meta }), + }); + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }, + }; + } catch (error) { + await client.close(); + await server.close(); + throw error; + } +} + const logger = createConsoleLogger("info"); // Build flags object from environment variables (Gram injects these) @@ -60,7 +147,7 @@ const flags: MCPServerFlags = { // Reuse existing createMCPServer template - it reads from `flags` const { server } = {{ templateString "mcp-server/create-mcp-server.ts.stmpl" .Local }}; -export default await withGram(server, { +export default await wrapGramServer(server, { variables: { {{- /* Security variables */}} {{- if $globalSecurity }} diff --git a/templates/templates/mcp-typescript/mcp-server/resource-template.ts.stmpl b/templates/templates/mcp-typescript/mcp-server/resource-template.ts.stmpl index 2f07b08dc..2fa9f46ca 100644 --- a/templates/templates/mcp-typescript/mcp-server/resource-template.ts.stmpl +++ b/templates/templates/mcp-typescript/mcp-server/resource-template.ts.stmpl @@ -8,7 +8,7 @@ {{ genImports }} {{- addImport "./resources.js" "ResourceTemplateDefinition" }} {{- addImport "./resources.js" "formatResult" }} -{{- addImport "@modelcontextprotocol/sdk/server/mcp.js" "ResourceTemplate" }} +{{- addImport "@modelcontextprotocol/server" "ResourceTemplate" }} {{- if $hasCustomFile }} {{- addImport (printf "./%s.custom.js" .Local.Filename) "customizer" "aliasImport" }} @@ -48,7 +48,7 @@ export const {{ sanitizeMCPPrimitiveExport $op }}: ResourceTemplateDefinition = {{ sanitizeFieldName .Name }}, {{- end }} {{- end }} - { fetchOptions: { signal: extra.signal } }, + { fetchOptions: { signal: extra.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/templates/templates/mcp-typescript/mcp-server/resource.ts.stmpl b/templates/templates/mcp-typescript/mcp-server/resource.ts.stmpl index d7883e565..0521ec430 100644 --- a/templates/templates/mcp-typescript/mcp-server/resource.ts.stmpl +++ b/templates/templates/mcp-typescript/mcp-server/resource.ts.stmpl @@ -24,7 +24,7 @@ export const {{ sanitizeMCPPrimitiveExport $op }}: ResourceDefinition = { {{- addFuncImport $op $usageLocation }} const [result, apiCall] = await {{ $funcName }}( client, - { fetchOptions: { signal: extra.signal } }, + { fetchOptions: { signal: extra.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/templates/templates/mcp-typescript/mcp-server/server.ts.stmpl b/templates/templates/mcp-typescript/mcp-server/server.ts.stmpl index 18ca15e4b..a3fe85223 100644 --- a/templates/templates/mcp-typescript/mcp-server/server.ts.stmpl +++ b/templates/templates/mcp-typescript/mcp-server/server.ts.stmpl @@ -7,7 +7,7 @@ {{- $coreName := sanitizeCoreSDKName }} {{- $hasServerExtensionFile := fileExists "src/mcp-server/server.extensions.ts" }} {{- $hasServers := (and $globalSDK.Servers $globalSDK.Servers.HasAbsoluteURL) -}} -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import { {{$coreName}} } from "../core.js"; import type { ConsoleLogger } from "./console-logger.js"; import { MCPScope } from "./scopes.js"; diff --git a/templates/templates/mcp-typescript/mcp-server/tool.ts.stmpl b/templates/templates/mcp-typescript/mcp-server/tool.ts.stmpl index 473575232..ca2160aa0 100644 --- a/templates/templates/mcp-typescript/mcp-server/tool.ts.stmpl +++ b/templates/templates/mcp-typescript/mcp-server/tool.ts.stmpl @@ -50,7 +50,7 @@ export const {{ sanitizeMCPPrimitiveExport $op }}: ToolDefinition{{ if $hasArgs {{- range $field := $op.Arguments.Sorted }} {{ sanitizeAccessor "args" (sanitizeFieldName $field.Name) }}, {{- end }} - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/templates/templates/mcp-typescript/package.json.stmpl b/templates/templates/mcp-typescript/package.json.stmpl index c1a9188ac..ad4a16cb1 100644 --- a/templates/templates/mcp-typescript/package.json.stmpl +++ b/templates/templates/mcp-typescript/package.json.stmpl @@ -54,7 +54,6 @@ }, {{- end }} "overrides": { - "@modelcontextprotocol/sdk": "1.26.0", "hono": ">=4.12.34", "fast-uri": ">=4.1.2 <5.0.0", "js-yaml": ">=4.3.1 <5.0.0", @@ -65,5 +64,8 @@ }, "devDependencies": { {{ templateDevDependencies | indent 4 | trim }} + }, + "engines": { + "node": "{{ if .Global.Config.GramEnabled }}>=22.18.0{{ else }}>=20{{ end }}" } } diff --git a/templates/templates/mcp-typescript/readme.stmpl b/templates/templates/mcp-typescript/readme.stmpl index aa3c6a975..eae70ae7b 100644 --- a/templates/templates/mcp-typescript/readme.stmpl +++ b/templates/templates/mcp-typescript/readme.stmpl @@ -56,6 +56,9 @@ Then install with Claude Code CLI: {{ templateMcpCLICommand (dict "client" "claude" "cloudflareURL" "http://localhost:8787") }} ``` +The worker serves MCP over Streamable HTTP at `/mcp` and legacy HTTP+SSE at `/sse`. Existing clients can keep their endpoints. Legacy Streamable HTTP and SSE clients retain session state in Durable Objects; modern protocol requests are handled statelessly. + +Keep the Durable Object binding, exported class, and `v1` migration in `wrangler.toml`: legacy Streamable HTTP and SSE sessions require them. Only modern protocol requests are stateless. Removing the binding disables legacy sessions; deleting the namespace also permanently destroys its stored data. {{end}} diff --git a/templates/templates/mcp-typescript/readme/installation.stmpl b/templates/templates/mcp-typescript/readme/installation.stmpl index 9b66f4af0..7207f9fcc 100644 --- a/templates/templates/mcp-typescript/readme/installation.stmpl +++ b/templates/templates/mcp-typescript/readme/installation.stmpl @@ -1,4 +1,6 @@ {{- $flags := generateMCPFlagsUsage -}} + +The MCP server requires Node.js {{ if .Global.Config.GramEnabled }}22.18.0 or later because Gram is enabled{{ else }}20 or later{{ end }}. {{- $packageName := .Global.Config.PackageName -}} {{- if not .Global.Config.Published }} @@ -106,4 +108,4 @@ For a full list of server arguments, run: npx {{ $packageName }} --help ``` - \ No newline at end of file + diff --git a/templates/templates/mcp-typescript/tests/primary/auth_additional.test.ts.stmpl b/templates/templates/mcp-typescript/tests/primary/auth_additional.test.ts.stmpl index e38ecad06..71f9245de 100644 --- a/templates/templates/mcp-typescript/tests/primary/auth_additional.test.ts.stmpl +++ b/templates/templates/mcp-typescript/tests/primary/auth_additional.test.ts.stmpl @@ -7,6 +7,109 @@ import { authHoistedSecurityAccessTokenOnly } from "../funcs/authHoistedSecurity import { authHoistedSecurityAccessTokenFirst } from "../funcs/authHoistedSecurityAccessTokenFirst.js"; import { authHoistedSecurityApiKeyFirst } from "../funcs/authHoistedSecurityApiKeyFirst.js"; import { authHoistedSecurityBasicHttpOnly } from "../funcs/authHoistedSecurityBasicHttpOnly.js"; +import type { MCPServerFlags } from "../mcp-server/flags.js"; +import { buildSDK } from "../mcp-server/tools.js"; +import { landingPage, landingPageHTML } from "../landing-page.js"; + +const quietLogger = { level: "error" }; + +test.each(["sse", "http"] as const)("Node %s setup examples use accepted authentication headers", (transport) => { + const html = landingPageHTML("https://mcp.example.test", transport); + for (const id of ["claude-cli-cmd", "gemini-config"]) { + const snippet = html.match(new RegExp(`id="${id}">([^<]+)`))?.[1]; + expect(snippet).toContain('--header "access-token: ..."'); + expect(snippet).not.toContain('--header "authorization: ..."'); + const header = snippet?.match(/--header "(access-token):/i)?.[1]; + expect(header).toBeDefined(); + expect(resolvedOptions({ [header!]: "request-token" }, {}, true).security?.["accessToken"]).toBe("request-token"); + } + const config = JSON.parse(html.match(/id="raw-config">([\s\S]*?)<\/code>/)![1]!); + if (transport === "http") { + expect(config.args).toContain("global-header-param:${GLOBAL_HEADER_PARAM}"); + expect(config.args).toContain("api-hostname:${API_HOSTNAME}"); + expect(html).toContain('"global-header-param" = "YOUR_GLOBAL_HEADER_PARAM"'); + expect(html).toContain('"api-hostname" = "YOUR_API_HOSTNAME"'); + expect(resolvedOptions({ "global-header-param": "true", "api-hostname": "api.example.test" }, {}, true)).toMatchObject({ globalHeaderParam: true, hostname: "api.example.test" }); + } else { + expect(config.args.some((arg: string) => arg.startsWith("global-header-param:") || arg.startsWith("api-hostname:"))).toBe(false); + } + expect(config.args.some((arg: string) => arg.startsWith("server-index:") || arg.startsWith("server-url:"))).toBe(false); + expect(html).toContain(`id="server-url">https://mcp.example.test/${transport === "sse" ? "sse" : "mcp"}`); +}); + +test("Worker setup examples and install links use the Worker authentication headers", async () => { + const html = await landingPage(new Request("https://mcp.example.test/")).text(); + const raw = html.match(/id="raw-config">([\s\S]*?)<\/code>/)?.[1]; + const config = JSON.parse(raw!); + expect(config.args).toContain("authorization:${ACCESS_TOKEN}"); + expect(config.args.some((arg: string) => arg.startsWith("access-token:"))).toBe(false); + expect(html).toContain('--header "authorization: ..."'); + expect(html).toContain('"authorization" = "YOUR_ACCESS_TOKEN"'); + const installLinks = [...html.matchAll(/href="((?:cursor|vscode):[^"]+)"/g)]; + expect(installLinks.length).toBeGreaterThan(0); + for (const [, link] of installLinks) { + const encoded = new URL(link!).searchParams.get("config"); + expect(JSON.parse(Buffer.from(encoded!, "base64").toString())).toEqual(config); + } +}); + +function resolvedOptions(headers: Record, flags: Partial, disableStaticAuth = false) { + const sdk = buildSDK(new Headers(headers), flags as MCPServerFlags, disableStaticAuth, quietLogger); + return sdk._options as { security?: Record; globalHeaderParam?: boolean }; +} + +test("MCP server resolves unset credentials to empty strings and leaves other options undefined", () => { + const options = resolvedOptions({}, {}); + expect(options.security?.["accessToken"]).toBe(""); + expect(options.security?.["basicHttp"]).toBeUndefined(); + expect(options.globalHeaderParam).toBeUndefined(); +}); + +test.each([false, true])("MCP server applies schema defaults to absent settings (static auth disabled=%s)", (disableStaticAuth) => { + const options = resolvedOptions({ "client-id": "client", "client-secret": "secret" }, {}, disableStaticAuth); + expect(options.security?.["clientCredentials"]).toMatchObject({ + clientID: "client", + clientSecret: "secret", + tokenURL: "/clientcredentials/token?expires_in=90", + }); + expect(options.security?.["basicHttp"]).toBeUndefined(); + expect(options.globalHeaderParam).toBeUndefined(); +}); + +test("MCP server preserves header and flag precedence over schema defaults", () => { + const credentials = { "client-id": "client", "client-secret": "secret" }; + const flags = { "token-url": "https://example.com/static-token" }; + expect(resolvedOptions(credentials, flags).security?.["clientCredentials"]).toMatchObject({ + tokenURL: "https://example.com/static-token", + }); + expect(resolvedOptions({ ...credentials, "token-url": "https://example.com/header-token" }, flags).security?.["clientCredentials"]).toMatchObject({ + tokenURL: "https://example.com/header-token", + }); + expect(resolvedOptions(credentials, flags, true).security?.["clientCredentials"]).toMatchObject({ + tokenURL: "/clientcredentials/token?expires_in=90", + }); +}); + +test("MCP server uses static credentials from flags", () => { + const options = resolvedOptions({}, { "access-token": "static-token", username: "user", password: "pass" }); + expect(options.security?.["accessToken"]).toBe("static-token"); + expect(options.security?.["basicHttp"]).toEqual({ username: "user", password: "pass" }); +}); + +test("MCP server request headers override static credentials", () => { + const options = resolvedOptions({ "access-token": "header-token" }, { "access-token": "static-token" }); + expect(options.security?.["accessToken"]).toBe("header-token"); +}); + +test("MCP server ignores static credentials when static auth is disabled", () => { + const options = resolvedOptions({}, { "access-token": "static-token" }, true); + expect(options.security?.["accessToken"]).toBe(""); +}); + +test("MCP server validates enum request headers", () => { + expect(resolvedOptions({ "global-header-param": "true" }, {}).globalHeaderParam).toBe(true); + expect(() => resolvedOptions({ "global-header-param": "maybe" }, {})).toThrow(); +}); test("Global Security Fields Ordering", async () => { recordTest("auth-global-security-fields-ordering"); diff --git a/templates/templates/mcp-typescript/tests/primary/compatibility_additional.test.ts b/templates/templates/mcp-typescript/tests/primary/compatibility_additional.test.ts new file mode 100644 index 000000000..7dcb131fd --- /dev/null +++ b/templates/templates/mcp-typescript/tests/primary/compatibility_additional.test.ts @@ -0,0 +1,204 @@ +import { Client } from "@modelcontextprotocol/client"; +import { InMemoryTransport, McpServer } from "@modelcontextprotocol/server"; +import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { + ErrorCode, + ListRootsResultSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { + mcpServerContext, + type MCPServerContext, +} from "../mcp-server/shared.js"; +import { expect, test } from "vitest"; +import { SDKCore } from "../core.js"; +import { createConsoleLogger } from "../mcp-server/console-logger.js"; +import { createRegisterResourceTemplate } from "../mcp-server/resources.js"; +import { + createRegisterTool, + registerDynamicTools, +} from "../mcp-server/tools.js"; + +async function withClient( + server: McpServer, + run: (client: Client) => Promise, +) { + const client = new Client( + { name: "compatibility-client", version: "1.0.0" }, + { capabilities: { roots: {} }, versionNegotiation: { mode: "legacy" } }, + ); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + await run(client); + } finally { + await client.close(); + await server.close(); + } +} + +test("v1 resource templates retain discovery, completion, and reads", async () => { + const server = new McpServer({ + name: "compatibility-server", + version: "1.0.0", + }); + createRegisterResourceTemplate( + createConsoleLogger("error"), + server, + () => new SDKCore(), + new Set(), + )({ + name: "widget", + description: "Read a widget", + resource: new ResourceTemplate("widget://{name}", { + list: (extra) => { + expect(extra.signal).toBeInstanceOf(AbortSignal); + expect(extra.requestId).toBeDefined(); + return { resources: [{ name: "example", uri: "widget://example" }] }; + }, + complete: { name: (value) => [value + "ample"] }, + }), + read: (_sdk, uri, vars, extra) => { + expect(extra.signal).toBe(extra.mcpReq.signal); + expect(extra.requestId).toBe(extra.mcpReq.id); + return { + contents: [{ uri: uri.toString(), text: String(vars["name"]) }], + }; + }, + }); + await withClient(server, async (client) => { + expect(await client.listResources()).toMatchObject({ + resources: [{ uri: "widget://example" }], + }); + expect(await client.listResourceTemplates()).toMatchObject({ + resourceTemplates: [{ uriTemplate: "widget://{name}" }], + }); + expect( + await client.complete({ + ref: { type: "ref/resource", uri: "widget://{name}" }, + argument: { name: "name", value: "ex" }, + }), + ).toMatchObject({ completion: { values: ["example"] } }); + expect( + await client.readResource({ uri: "widget://example" }), + ).toMatchObject({ contents: [{ text: "example" }] }); + }); +}); + +test.each([false, true])( + "legacy tool context preserves cancellation (dynamic=%s)", + async (dynamic) => { + const server = new McpServer({ + name: "compatibility-server", + version: "1.0.0", + }); + const logger = createConsoleLogger("error"); + const getSDK = () => new SDKCore(); + const scopes = new Set(); + const [tool, , toolMap] = createRegisterTool( + logger, + server, + getSDK, + scopes, + undefined, + dynamic, + ); + let started!: () => void; + const running = new Promise((resolve) => { + started = resolve; + }); + let cancelled!: () => void; + const aborted = new Promise((resolve) => { + cancelled = resolve; + }); + let context: MCPServerContext | undefined; + tool({ + name: "held", + description: "Wait for cancellation", + annotations: { + title: "Held", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + tool: async (_sdk, extra) => { + context = extra; + expect(extra.signal).toBe(extra.mcpReq.signal); + expect(extra.requestId).toBe(extra.mcpReq.id); + expect(extra.sendNotification).toBeTypeOf("function"); + extra.signal.addEventListener("abort", cancelled, { once: true }); + started(); + await aborted; + return { content: [{ type: "text", text: "cancelled" }] }; + }, + }); + if (dynamic) registerDynamicTools(logger, server, getSDK, toolMap, scopes); + await withClient(server, async (client) => { + const controller = new AbortController(); + const pending = client + .callTool( + dynamic + ? { + name: "execute_tool", + arguments: { name: "held", arguments: {} }, + } + : { name: "held" }, + { signal: controller.signal }, + ) + .catch((error: unknown) => error); + await running; + controller.abort(); + await aborted; + await expect( + context!.sendNotification({ + method: "notifications/tools/list_changed", + }), + ).resolves.toBeUndefined(); + await expect( + context!.sendRequest({ method: "roots/list" }, ListRootsResultSchema), + ).rejects.toMatchObject({ code: ErrorCode.ConnectionClosed }); + expect(await pending).toBeInstanceOf(Error); + }); + }, + 10000, +); + +test.each([false, true])( + "legacy sendRequest validates v1 result schemas (invalid=%s)", + async (invalid) => { + const server = new McpServer({ + name: "compatibility-server", + version: "1.0.0", + }); + server.registerTool("roots", {}, async (ctx) => { + const schema = invalid + ? ListRootsResultSchema.refine(() => false) + : ListRootsResultSchema; + const result = await mcpServerContext(ctx).sendRequest( + { method: "roots/list" }, + schema, + ); + return { content: [{ type: "text", text: JSON.stringify(result) }] }; + }); + await withClient(server, async (client) => { + client.setRequestHandler("roots/list", async () => ({ + roots: [{ uri: "file:///example", name: "Example" }], + })); + const result = await client.callTool({ name: "roots" }); + if (invalid) expect(result.isError).toBe(true); + else { + expect(result.isError).not.toBe(true); + expect(result.content).toEqual([ + { + type: "text", + text: JSON.stringify({ + roots: [{ uri: "file:///example", name: "Example" }], + }), + }, + ]); + } + }); + }, +); diff --git a/templates/templates/mcp-typescript/tests/primary/dynamic_tools_additional.test.ts b/templates/templates/mcp-typescript/tests/primary/dynamic_tools_additional.test.ts index abf52a802..9e44eb6a8 100644 --- a/templates/templates/mcp-typescript/tests/primary/dynamic_tools_additional.test.ts +++ b/templates/templates/mcp-typescript/tests/primary/dynamic_tools_additional.test.ts @@ -1,251 +1,1207 @@ +import { createServer } from "node:http"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { serveStdio } from "@modelcontextprotocol/server/stdio"; +import { + Client, + StreamableHTTPClientTransport, +} from "@modelcontextprotocol/client"; +import { + completable, + createMcpHandler, + InMemoryTransport, + McpServer, + ResourceTemplate, +} from "@modelcontextprotocol/server"; import { describe, expect, test, vi } from "vitest"; import { z } from "zod"; +import { bigint } from "../types/bigint.js"; +import { mcpInputSchema } from "../mcp-server/shared.js"; import { SDKCore } from "../core.js"; +import { landingPage, landingPageHTML } from "../landing-page.js"; import { createConsoleLogger } from "../mcp-server/console-logger.js"; +import { createRegisterPrompt } from "../mcp-server/prompts.js"; +import { + createRegisterResource, + createRegisterResourceTemplate, +} from "../mcp-server/resources.js"; import { MCPScope } from "../mcp-server/scopes.js"; -import { registerDynamicTools, ToolDefinition } from "../mcp-server/tools.js"; - -// Regression tests: dynamic mode `execute_tool` must accept tool -// arguments under a nested `arguments` object, matching the MCP spec's -// canonical `tools/call` shape (`{ name, arguments }`). Previously, args were -// wrapped under an `input` field typed as an opaque `record`, -// which produced JSON Schema that LLMs could not reliably populate. - -type CapturedHandler = { - config: { inputSchema?: unknown }; - handler: ( - args: Record, - ctx: { signal: AbortSignal }, - ) => Promise<{ - isError?: boolean; - content: Array<{ type: string; text: string }>; - }>; +import { + createRegisterTool, + registerDynamicTools, +} from "../mcp-server/tools.js"; + +const annotations = { + title: "Example", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, }; -function makeHarness() { - const handlers = new Map(); - const mockServer = { - registerTool( - name: string, - config: { inputSchema?: unknown }, - handler: CapturedHandler["handler"], - ) { - handlers.set(name, { config, handler }); - }, - }; +test.each(["sse", "http"] as const)( + "landing page configurations use the active transport (%s)", + (transport) => { + const origin = "https://mcp.example.test"; + const endpoint = origin + (transport === "sse" ? "/sse" : "/mcp"); + const html = landingPageHTML(origin, transport); + const raw = html.match(/id="raw-config">([\s\S]*?)<\/code>/)?.[1]; + expect(raw).toBeDefined(); + const config = JSON.parse(raw!); + expect(config.args[2]).toBe(endpoint); + if (transport === "http") { + expect(html).toContain(`url = "${endpoint}"`); + } else { + expect(html).toContain(`command = "npx"`); + expect(html).toContain(`args = ${JSON.stringify(config.args)}`); + expect(html).not.toContain(`url = "${endpoint}"`); + } + for (const client of ["claude", "gemini"]) { + expect(html).toContain(`${client} mcp add --transport ${transport}`); + } + const links = [...html.matchAll(/href="((?:cursor|vscode):[^"]+)"/g)]; + expect(links.length).toBeGreaterThan(0); + for (const [, link] of links) { + const encoded = new URL(link!).searchParams.get("config"); + expect(encoded).toBeTruthy(); + expect(JSON.parse(Buffer.from(encoded!, "base64").toString())).toEqual( + config, + ); + } + expect(landingPageHTML(origin)).toContain(`${origin}/sse`); + }, +); + +test("Worker landing page advertises Streamable HTTP", async () => { + const response = landingPage(new Request("https://mcp.example.test/")); + expect(await response.text()).toContain("https://mcp.example.test/mcp"); +}); + +function makeHarness( + dynamic: boolean, + register?: (tool: ReturnType[0]) => void, +) { + const server = new McpServer({ name: "registration-test", version: "1.0.0" }); + const logger = createConsoleLogger("error"); + const getSDK = () => new SDKCore(); + const scopes = new Set(); + const [tool, , toolMap] = createRegisterTool( + logger, + server, + getSDK, + scopes, + undefined, + dynamic, + ); + const stub = vi + .fn() + .mockResolvedValue({ content: [{ type: "text", text: "stub-result" }] }); + const noArgsStub = vi + .fn() + .mockResolvedValue({ content: [{ type: "text", text: "no-args-result" }] }); + if (register) { + register(tool); + } else { + tool({ + name: "stub-tool", + description: "Accept a nested request", + annotations, + args: { request: z.object({ name: z.string() }) }, + tool: stub, + }); + tool({ + name: "no-args-tool", + description: "Accept no arguments", + annotations, + tool: noArgsStub, + }); + tool({ + name: "transform-tool", + description: "Transform input values", + annotations, + args: { + amount: z.union([z.string(), z.number()]).transform(Number), + price: z.string().transform((v) => v), + when: z + .union([z.date(), z.string().transform((v) => new Date(v))]) + .transform((v) => v.toISOString()), + }, + tool: (_sdk, args) => ({ + content: [{ type: "text", text: JSON.stringify(args) }], + }), + }); + tool({ + name: "bigint-default-tool", + description: "Preserve nested bigint defaults", + annotations, + args: { + request: z + .object({ + amount: bigint().default(12345678901234567890n), + amounts: z.array(bigint()).default([12345678901234567890n]), + }) + .default({ + amount: 12345678901234567890n, + amounts: [12345678901234567890n], + }), + }, + tool: (_sdk, args) => ({ + content: [ + { + type: "text", + text: JSON.stringify(args, (_key, value: unknown) => + typeof value === "bigint" ? value.toString() : value, + ), + }, + ], + }), + }); + } + if (dynamic) registerDynamicTools(logger, server, getSDK, toolMap, scopes); + return { server, logger, getSDK, scopes, stub, noArgsStub }; +} + +async function withClient( + server: McpServer, + run: (client: Client) => Promise, +) { + const client = new Client({ name: "registration-client", version: "1.0.0" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + await run(client); + } finally { + await client.close(); + await server.close(); + } +} - const stub = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "stub-result" }], +describe("MCP registration", () => { + test("lists and invokes tools through the v2 protocol", async () => { + const { server, stub, noArgsStub } = makeHarness(false); + await withClient(server, async (client) => { + const { tools } = await client.listTools(); + expect(tools.map((tool) => tool.name)).toEqual( + expect.arrayContaining(["stub-tool", "no-args-tool", "transform-tool"]), + ); + const result = await client.callTool({ + name: "stub-tool", + arguments: { request: { name: "Example" } }, + }); + expect(result.isError).toBeFalsy(); + expect(stub.mock.calls[0]?.[1]).toEqual({ request: { name: "Example" } }); + expect(stub.mock.calls[0]?.[2].mcpReq.signal).toBeInstanceOf(AbortSignal); + const invalid = await client.callTool({ + name: "stub-tool", + arguments: { request: { name: 42 } }, + }); + expect(invalid.isError).toBe(true); + expect(stub).toHaveBeenCalledTimes(1); + await client.callTool({ name: "no-args-tool" }); + expect(noArgsStub.mock.calls[0]?.[1].mcpReq.signal).toBeInstanceOf( + AbortSignal, + ); + const transformed = await client.callTool({ + name: "transform-tool", + arguments: { + amount: "12.5", + price: "8", + when: "2026-01-01T00:00:00.000Z", + }, + }); + expect(transformed.content).toEqual([ + { + type: "text", + text: JSON.stringify({ + amount: 12.5, + price: "8", + when: "2026-01-01T00:00:00.000Z", + }), + }, + ]); + }); }); - const argsShape = { request: z.object({ name: z.string() }) }; - const stubToolDef: ToolDefinition = { - name: "stub-tool", - description: "Stub tool for execute_tool unit tests", - annotations: { - title: "Stub", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - args: argsShape, - // deno-lint-ignore no-explicit-any - tool: stub as any, - }; + test("registers prompts and static and templated resources", async () => { + const { server, logger, getSDK, scopes } = makeHarness(false); + const prompt = createRegisterPrompt(logger, server, getSDK, scopes); + prompt({ + name: "greeting", + args: { name: z.string() }, + prompt: (_sdk, args, ctx) => { + expect(ctx.mcpReq.signal).toBeInstanceOf(AbortSignal); + return { + messages: [ + { role: "user", content: { type: "text", text: args.name } }, + ], + }; + }, + }); + prompt({ + name: "empty", + prompt: (_sdk, ctx) => { + expect(ctx.mcpReq.signal).toBeInstanceOf(AbortSignal); + return { messages: [] }; + }, + }); + createRegisterResource( + logger, + server, + getSDK, + scopes, + )({ + name: "static", + resource: "test://static", + read: (_sdk, uri, ctx) => { + expect(ctx.mcpReq.signal).toBeInstanceOf(AbortSignal); + return { contents: [{ uri: uri.href, text: "static" }] }; + }, + }); + createRegisterResourceTemplate( + logger, + server, + getSDK, + scopes, + )({ + name: "template", + description: "Named resource", + resource: new ResourceTemplate("test://items/{name}", { + list: undefined, + }), + read: (_sdk, uri, vars, ctx) => { + expect(ctx.mcpReq.signal).toBeInstanceOf(AbortSignal); + return { contents: [{ uri: uri.href, text: String(vars["name"]) }] }; + }, + }); + await withClient(server, async (client) => { + expect((await client.listPrompts()).prompts.map((p) => p.name)).toEqual([ + "greeting", + "empty", + ]); + expect( + ( + await client.getPrompt({ + name: "greeting", + arguments: { name: "Example" }, + }) + ).messages[0]?.content, + ).toEqual({ type: "text", text: "Example" }); + expect((await client.getPrompt({ name: "empty" })).messages).toEqual([]); + expect((await client.listResources()).resources[0]?.uri).toBe( + "test://static", + ); + expect( + (await client.listResourceTemplates()).resourceTemplates[0] + ?.uriTemplate, + ).toBe("test://items/{name}"); + expect( + (await client.readResource({ uri: "test://static" })).contents, + ).toEqual([{ uri: "test://static", text: "static" }]); + expect( + (await client.readResource({ uri: "test://items/example" })).contents, + ).toEqual([{ uri: "test://items/example", text: "example" }]); + }); + }); +}); - const noArgsStub = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "no-args-result" }], +describe("dynamic MCP tools", () => { + test("advertises schemas and preserves nested arguments during execution", async () => { + const { server, stub, noArgsStub } = makeHarness(true); + await withClient(server, async (client) => { + const { tools } = await client.listTools(); + const execute = tools.find((tool) => tool.name === "execute_tool"); + expect(execute?.inputSchema.properties).toHaveProperty("name"); + expect(execute?.inputSchema.properties).toHaveProperty("arguments"); + expect(tools.some((tool) => tool.name === "stub-tool")).toBe(false); + const result = await client.callTool({ + name: "execute_tool", + arguments: { + name: "stub-tool", + arguments: { request: { name: "Example" } }, + }, + }); + expect(result.isError).toBeFalsy(); + expect(stub.mock.calls[0]?.[1]).toEqual({ request: { name: "Example" } }); + expect(stub.mock.calls[0]?.[2].mcpReq.signal).toBeInstanceOf(AbortSignal); + await client.callTool({ + name: "execute_tool", + arguments: { name: "no-args-tool" }, + }); + expect(noArgsStub.mock.calls[0]?.[1].mcpReq.signal).toBeInstanceOf( + AbortSignal, + ); + }); }); - const noArgsToolDef: ToolDefinition = { - name: "no-args-tool", - description: "Tool that takes no arguments", - annotations: { - title: "NoArgs", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - // deno-lint-ignore no-explicit-any - tool: noArgsStub as any, - }; + test("rejects invalid nested input and unknown tools without invoking handlers", async () => { + const { server, stub } = makeHarness(true); + await withClient(server, async (client) => { + const invalid = await client.callTool({ + name: "execute_tool", + arguments: { name: "stub-tool", arguments: { request: { name: 42 } } }, + }); + expect(invalid.isError).toBe(true); + expect(invalid.content).toEqual([ + expect.objectContaining({ + text: expect.stringContaining("Invalid input"), + }), + ]); + const unknown = await client.callTool({ + name: "execute_tool", + arguments: { name: "unknown" }, + }); + expect(unknown.isError).toBe(true); + expect(stub).not.toHaveBeenCalled(); + }); + }); - // deno-lint-ignore no-explicit-any - const toolMap = new Map>(); - toolMap.set("stub-tool", stubToolDef); - toolMap.set("no-args-tool", noArgsToolDef); + test("describes transform-bearing inputs and dispatches their parsed values", async () => { + const { server } = makeHarness(true); + await withClient(server, async (client) => { + const description = await client.callTool({ + name: "describe_tool_input", + arguments: { tool_names: ["transform-tool"] }, + }); + expect(description.isError).toBeFalsy(); + expect(description.content).toEqual([ + expect.objectContaining({ text: expect.stringContaining('"amount"') }), + ]); + const result = await client.callTool({ + name: "execute_tool", + arguments: { + name: "transform-tool", + arguments: { + amount: "12.5", + price: "8", + when: "2026-01-01T00:00:00.000Z", + }, + }, + }); + expect(result.content).toEqual([ + { + type: "text", + text: JSON.stringify({ + amount: 12.5, + price: "8", + when: "2026-01-01T00:00:00.000Z", + }), + }, + ]); + }); + }); +}); - registerDynamicTools( - createConsoleLogger("error"), - // deno-lint-ignore no-explicit-any - mockServer as any, - () => ({}) as SDKCore, - // deno-lint-ignore no-explicit-any - toolMap as any, - new Set(), +describe("MCP protocol eras", () => { + test.each(["legacy", "modern"] as const)( + "HTTP serves %s clients", + async (era) => { + const handler = createMcpHandler(() => makeHarness(false).server); + const client = new Client( + { name: "http-test", version: "1.0.0" }, + { + versionNegotiation: { + mode: era === "modern" ? { pin: "2026-07-28" } : "legacy", + }, + }, + ); + const transport = new StreamableHTTPClientTransport( + new URL("http://localhost/mcp"), + { + fetch: (input, init) => handler.fetch(new Request(input, init)), + }, + ); + try { + await client.connect(transport); + expect( + (await client.listTools()).tools.some( + (tool) => tool.name === "stub-tool", + ), + ).toBe(true); + expect( + (await client.callTool({ name: "no-args-tool" })).content, + ).toEqual([{ type: "text", text: "no-args-result" }]); + } finally { + await client.close(); + await handler.close(); + } + }, ); - return { handlers, stub, noArgsStub }; -} - -describe("dynamic mode execute_tool", () => { - test("accepts tool arguments under nested `arguments` field", async () => { - const { handlers, stub } = makeHarness(); - const execute = handlers.get("execute_tool"); - expect(execute).toBeDefined(); + test.each(["legacy", "modern"] as const)( + "stdio entry serves %s clients", + async (era) => { + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + const handle = serveStdio(() => makeHarness(false).server, { + transport: serverTransport, + }); + const client = new Client( + { name: "stdio-test", version: "1.0.0" }, + { + versionNegotiation: { + mode: era === "modern" ? { pin: "2026-07-28" } : "legacy", + }, + }, + ); + try { + await client.connect(clientTransport); + expect( + (await client.listTools()).tools.some( + (tool) => tool.name === "stub-tool", + ), + ).toBe(true); + expect( + (await client.callTool({ name: "no-args-tool" })).content, + ).toEqual([{ type: "text", text: "no-args-result" }]); + } finally { + await client.close(); + await handle.close(); + } + }, + ); +}); - const result = await execute!.handler( - { name: "stub-tool", arguments: { request: { name: "Alice" } } }, - { signal: new AbortController().signal }, +test("bigint defaults remain precise in listed and dynamically described schemas", async () => { + const { server } = makeHarness(false); + await withClient(server, async (client) => { + const listed = (await client.listTools()).tools.find( + (tool) => tool.name === "bigint-default-tool", ); - - expect(stub).toHaveBeenCalledTimes(1); - // tool(client, validatedInput, ctx) — validated input is second arg - expect(stub.mock.calls[0]?.[1]).toEqual({ request: { name: "Alice" } }); + expect(JSON.stringify(listed?.inputSchema)).toContain( + '"12345678901234567890"', + ); + expect(listed?.inputSchema.properties?.["request"]).toMatchObject({ + default: { + amount: "12345678901234567890", + amounts: ["12345678901234567890"], + }, + properties: { + amount: { default: "12345678901234567890" }, + amounts: { default: ["12345678901234567890"] }, + }, + }); + const result = await client.callTool({ name: "bigint-default-tool" }); + expect(result.content).toEqual([ + { + type: "text", + text: '{"request":{"amount":"12345678901234567890","amounts":["12345678901234567890"]}}', + }, + ]); + }); + const dynamic = makeHarness(true); + await withClient(dynamic.server, async (client) => { + const result = await client.callTool({ + name: "describe_tool_input", + arguments: { tool_names: ["bigint-default-tool"] }, + }); expect(result.isError).toBeFalsy(); + expect(result.content).toEqual([ + expect.objectContaining({ + text: expect.stringContaining('"12345678901234567890"'), + }), + ]); }); +}); - test("rejects invalid inner arguments with a schema error", async () => { - const { handlers, stub } = makeHarness(); - const execute = handlers.get("execute_tool")!; - - const result = await execute.handler( - { - name: "stub-tool", - arguments: { request: { name: 42 } }, - }, - { signal: new AbortController().signal }, - ); +test("schema conversion preserves recursive references and runtime bigint defaults", async () => { + type Item = { amount: bigint | string; children?: Item[] | undefined }; + const item: z.ZodType = z + .lazy(() => + z.object({ + amount: bigint().default(12345678901234567890n), + children: z.array(item).optional(), + }), + ) + .describe("Recursive item"); + const schema = mcpInputSchema({ item }); + const json = schema["~standard"].jsonSchema.input({ + target: "draft-2020-12", + }); + expect(JSON.stringify(json)).toContain('"12345678901234567890"'); + expect(JSON.stringify(json)).toContain('"$ref"'); + expect(JSON.stringify(json)).toContain("Recursive item"); + expect(await schema["~standard"].validate({ item: {} })).toEqual({ + value: { item: { amount: 12345678901234567890n } }, + }); + expect(item.parse({}).amount).toBe(12345678901234567890n); +}); - expect(stub).not.toHaveBeenCalled(); - expect(result.isError).toBe(true); - expect(result.content[0]?.text).toContain("Invalid input"); +test("schema conversion does not advertise string defaults for bigint-only inputs", () => { + const schema = mcpInputSchema({ + amount: z.bigint().default(12345678901234567890n), + }); + const json = schema["~standard"].jsonSchema.input({ + target: "draft-2020-12", }); + expect(json).toMatchObject({ properties: { amount: {} } }); + expect(JSON.stringify(json)).not.toContain('"default"'); +}); - test("inputSchema advertises `name` and `arguments` meta-fields", () => { - const { handlers } = makeHarness(); - const { config } = handlers.get("execute_tool")!; - const shape = config.inputSchema as Record; +test("schema conversion advertises ISO dates and keeps original runtime defaults", async () => { + const date = new Date("2026-01-01T00:00:00.000Z"); + const shape = { + request: z + .object({ + when: z + .union([z.date(), z.string().transform((value) => new Date(value))]) + .default(date), + amount: bigint().default(12345678901234567890n), + }) + .default({ when: date, amount: 12345678901234567890n }), + }; + const schema = mcpInputSchema(shape); + const json = schema["~standard"].jsonSchema.input({ + target: "draft-2020-12", + }); + expect(json).toMatchObject({ + properties: { + request: { + default: { + when: "2026-01-01T00:00:00.000Z", + amount: "12345678901234567890", + }, + properties: { + when: { default: "2026-01-01T00:00:00.000Z" }, + amount: { default: "12345678901234567890" }, + }, + }, + }, + }); + expect(JSON.parse(JSON.stringify(json))).toEqual(json); + const parsed = await schema["~standard"].validate({}); + expect(parsed).toEqual({ + value: { request: { when: date, amount: 12345678901234567890n } }, + }); + expect(shape.request.parse(undefined).when).toBeInstanceOf(Date); +}); - expect(shape["name"]).toBeDefined(); - const argumentsSchema = shape["arguments"]; - expect(argumentsSchema).toBeDefined(); +test("schema conversion is lazy and observes current default factories", async () => { + let amount = 12345678901234567890n; + const defaultValue = vi.fn(() => amount); + const shape = { amount: bigint().default(defaultValue) }; + const schema = mcpInputSchema(shape); + expect(await schema["~standard"].validate({ amount: "42" })).toEqual({ + value: { amount: 42n }, + }); + expect(defaultValue).not.toHaveBeenCalled(); - // `arguments` must accept an arbitrary object payload (loose) so the - // nested target-tool args survive validation. - const parsed = argumentsSchema!.parse({ request: { name: "Alice" } }); - expect(parsed).toEqual({ request: { name: "Alice" } }); + const options = { target: "draft-2020-12" as const }; + expect(schema["~standard"].jsonSchema.input(options)).toMatchObject({ + properties: { amount: { default: "12345678901234567890" } }, + }); + amount = 12345678901234567891n; + expect(schema["~standard"].jsonSchema.input(options)).toMatchObject({ + properties: { amount: { default: "12345678901234567891" } }, + }); + expect(schema["~standard"].jsonSchema.output(options)).toMatchObject({ + properties: { amount: { default: "12345678901234567891" } }, }); - test("reports unknown tool name without invoking stub", async () => { - const { handlers, stub } = makeHarness(); - const execute = handlers.get("execute_tool")!; + defaultValue.mockClear(); + expect(await schema["~standard"].validate({})).toEqual({ value: { amount } }); + expect(defaultValue).toHaveBeenCalledTimes(1); +}); - const result = await execute.handler( - { name: "does-not-exist" }, - { signal: new AbortController().signal }, +test("prompt completion preserves input validation, transforms, and defaults", async () => { + const server = new McpServer({ name: "prompt-test", version: "1.0.0" }); + const client = new Client({ name: "prompt-client", version: "1.0.0" }); + const complete = vi.fn((value: string) => + ["typescript", "python"].filter((language) => language.startsWith(value)), + ); + const handler = vi.fn( + (args: { language: string; count: number; label: string }) => ({ + messages: [ + { + role: "user" as const, + content: { type: "text" as const, text: JSON.stringify(args) }, + }, + ], + }), + ); + createRegisterPrompt( + createConsoleLogger("error"), + server, + () => new SDKCore(), + new Set(), + )({ + name: "review", + args: { + language: completable(z.string().describe("Language"), complete), + count: z.string().regex(/^\d+$/).transform(Number), + label: z.string().default("example"), + suggestion: completable(z.string(), complete).optional(), + }, + prompt: (_sdk, args) => handler(args), + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + expect((await client.listPrompts()).prompts[0]?.arguments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "language", + description: "Language", + required: true, + }), + expect.objectContaining({ name: "label", required: false }), + ]), ); + expect( + await client.complete({ + ref: { type: "ref/prompt", name: "review" }, + argument: { name: "language", value: "type" }, + context: { arguments: { count: "7" } }, + }), + ).toMatchObject({ completion: { values: ["typescript"] } }); + expect(complete).toHaveBeenCalledWith("type", { + arguments: { count: "7" }, + }); + expect( + await client.complete({ + ref: { type: "ref/prompt", name: "review" }, + argument: { name: "suggestion", value: "py" }, + }), + ).toMatchObject({ completion: { values: ["python"] } }); + expect( + await client.getPrompt({ + name: "review", + arguments: { language: "typescript", count: "7" }, + }), + ).toMatchObject({ + messages: [ + { + content: { + text: JSON.stringify({ + language: "typescript", + count: 7, + label: "example", + }), + }, + }, + ], + }); + await expect( + client.getPrompt({ + name: "review", + arguments: { language: "typescript", count: "invalid" }, + }), + ).rejects.toThrow(); + expect(handler).toHaveBeenCalledTimes(1); + } finally { + await client.close(); + await server.close(); + } +}); - expect(stub).not.toHaveBeenCalled(); - expect(result.isError).toBe(true); - expect(result.content[0]?.text).toContain("Unknown tool: does-not-exist"); - }); +test.each([false, true])( + "async tool validation preserves static/dynamic parity (dynamic=%s)", + async (dynamic) => { + const handler = vi.fn((args: { count: number; name: string }) => ({ + content: [{ type: "text" as const, text: JSON.stringify(args) }], + })); + const { server } = makeHarness(dynamic, (tool) => { + tool({ + name: "async-input", + description: "Validate and transform input asynchronously", + annotations, + args: { + count: z.number().refine(async (value) => value % 2 === 0), + name: z.string().transform(async (value) => value.toUpperCase()), + }, + tool: (_sdk, args) => handler(args), + }); + }); + await withClient(server, async (client) => { + const call = (count: number) => + client.callTool({ + name: dynamic ? "execute_tool" : "async-input", + arguments: dynamic + ? { name: "async-input", arguments: { count, name: "example" } } + : { count, name: "example" }, + }); + expect((await call(4)).isError).toBeFalsy(); + expect(handler).toHaveBeenLastCalledWith({ count: 4, name: "EXAMPLE" }); + expect((await call(3)).isError).toBe(true); + expect(handler).toHaveBeenCalledTimes(1); + }); + }, +); - test("executes a no-args tool when `arguments` is omitted", async () => { - const { handlers, noArgsStub } = makeHarness(); - const execute = handlers.get("execute_tool")!; +test.each([false, true])( + "BigInt catch fallbacks remain serializable and preserve runtime values (dynamic=%s)", + async (dynamic) => { + const amount = 12345678901234567890n; + const fallback = vi.fn(() => ({ amount })); + const shape = { + request: z.object({ amount: bigint() }).catch(fallback), + bigintOnly: z.bigint().catch(amount), + }; + const { server } = makeHarness(dynamic, (tool) => { + tool({ + name: "catch-input", + description: "Recover from invalid input", + annotations, + args: shape, + tool: (_sdk, args) => { + expect(args).toEqual({ request: { amount }, bigintOnly: amount }); + return { + content: [{ type: "text", text: String(args.request.amount) }], + }; + }, + }); + expect(fallback).not.toHaveBeenCalled(); + }); + await withClient(server, async (client) => { + const json = mcpInputSchema(shape)["~standard"].jsonSchema.input({ + target: "draft-2020-12", + }); + expect(json).toMatchObject({ + properties: { request: { default: { amount: String(amount) } } }, + }); + expect(JSON.parse(JSON.stringify(json))).toEqual(json); + expect( + (json["properties"] as Record)["bigintOnly"], + ).not.toHaveProperty("default"); + if (dynamic) { + const described = await client.callTool({ + name: "describe_tool_input", + arguments: { tool_names: ["catch-input"] }, + }); + expect(described.isError).toBeFalsy(); + expect(JSON.stringify(described)).toContain(String(amount)); + } else { + const listed = await client.listTools(); + expect(listed.tools[0]?.inputSchema).toMatchObject(json); + } + expect( + await client.callTool({ + name: dynamic ? "execute_tool" : "catch-input", + arguments: dynamic + ? { + name: "catch-input", + arguments: { request: null, bigintOnly: null }, + } + : { request: null, bigintOnly: null }, + }), + ).toMatchObject({ content: [{ text: String(amount) }] }); + }); + }, +); - const result = await execute.handler( - { name: "no-args-tool" }, - { signal: new AbortController().signal }, - ); +test.each([false, true])( + "context-dependent catch factories preserve discovery and runtime fallback (dynamic=%s)", + async (dynamic) => { + const field = z.string().catch((ctx) => `fallback:${String(ctx.value)}`); + const shape = { value: field }; + const { server } = makeHarness(dynamic, (tool) => { + tool({ + name: "context-catch", + description: "Recover using the invalid input", + annotations, + args: shape, + tool: (_sdk, args) => ({ + content: [{ type: "text", text: args.value }], + }), + }); + }); + await withClient(server, async (client) => { + const standard = mcpInputSchema(shape)["~standard"]; + for (const direction of ["input", "output"] as const) { + const json = standard.jsonSchema[direction]({ + target: "draft-2020-12", + }); + expect( + (json["properties"] as Record)["value"], + ).not.toHaveProperty("default"); + expect(JSON.parse(JSON.stringify(json))).toEqual(json); + } + const listed = await client.listTools(); + expect(listed.tools.length).toBeGreaterThan(0); + if (dynamic) { + const described = await client.callTool({ + name: "describe_tool_input", + arguments: { tool_names: ["context-catch"] }, + }); + expect(described.isError).toBeFalsy(); + expect(JSON.stringify(described)).not.toContain("default"); + } else { + expect(listed.tools[0]?.inputSchema).toMatchObject({ + properties: { value: { type: "string" } }, + }); + expect( + (listed.tools[0]?.inputSchema.properties as Record)[ + "value" + ], + ).not.toHaveProperty("default"); + } + for (const value of [42, "valid"]) { + expect( + await client.callTool({ + name: dynamic ? "execute_tool" : "context-catch", + arguments: dynamic + ? { name: "context-catch", arguments: { value } } + : { value }, + }), + ).toMatchObject({ content: [{ text: field.parse(value) }] }); + } + expect(field.parse(42)).toBe("fallback:42"); + }); + }, +); - expect(noArgsStub).toHaveBeenCalledTimes(1); - expect(result.isError).toBeFalsy(); - expect(result.content[0]?.text).toBe("no-args-result"); +test("generated stdio accepts legacy initialization after modern discovery", async () => { + const toolName = "parameters-duplicate-path-param"; + const transport = new StdioClientTransport({ + command: process.execPath, + args: [ + fileURLToPath(new URL("../../bin/mcp-server.js", import.meta.url)), + "start", + "--transport", + "stdio", + "--tool", + toolName, + ], + stderr: "pipe", }); -}); - -// Regression test: dynamic mode `describe_tool_input` must not -// fail for tools whose generated schemas contain Zod `.transform()` pipelines -// (e.g. numeric coercion, decimals, date-time fields). Previously the default -// `z.toJSONSchema` call threw "Transforms cannot be represented in JSON Schema" -// and the tool was returned with `isError: true`, making dynamic mode unusable. -describe("dynamic mode describe_tool_input", () => { - function makeTransformHarness() { - const handlers = new Map(); - const mockServer = { - registerTool( - name: string, - config: { inputSchema?: unknown }, - handler: CapturedHandler["handler"], - ) { - handlers.set(name, { config, handler }); + const messages: unknown[] = []; + transport.onmessage = (message) => messages.push(message); + const expectResult = (id: number, result: unknown) => + vi.waitFor( + () => { + expect(messages).toContainEqual( + expect.objectContaining({ id, result }), + ); }, - }; - - // Mirrors the kinds of pipelines emitted by the generator: numeric coercion - // from string, a decimal-style string transform, and a date-time union. - const argsShape = { - amount: z - .union([z.string(), z.number()]) - .transform((v) => (typeof v === "string" ? parseFloat(v) : v)), - price: z.string().transform((v) => v), - when: z - .union([z.date(), z.string().transform((v) => new Date(v))]) - .transform((v) => v.toISOString()), - }; - const transformToolDef: ToolDefinition = { - name: "transform-tool", - description: "Tool whose schema relies on Zod transforms", - annotations: { - title: "Transform", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, + { timeout: 10000 }, + ); + try { + await transport.start(); + await transport.send({ + jsonrpc: "2.0", + id: 1, + method: "server/discover", + params: { + _meta: { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + name: "fallback-test", + version: "1.0.0", + }, + "io.modelcontextprotocol/clientCapabilities": {}, + }, }, - args: argsShape, - // deno-lint-ignore no-explicit-any - tool: vi.fn() as any, - }; - - // deno-lint-ignore no-explicit-any - const toolMap = new Map>(); - toolMap.set("transform-tool", transformToolDef); - - registerDynamicTools( - createConsoleLogger("error"), - // deno-lint-ignore no-explicit-any - mockServer as any, - () => ({}) as SDKCore, - // deno-lint-ignore no-explicit-any - toolMap as any, - new Set(), + }); + await expectResult(1, expect.any(Object)); + await transport.send({ + jsonrpc: "2.0", + id: 2, + method: "initialize", + params: { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { name: "fallback-test", version: "1.0.0" }, + }, + }); + await expectResult( + 2, + expect.objectContaining({ protocolVersion: "2025-06-18" }), ); - - return { handlers }; + await transport.send({ + jsonrpc: "2.0", + method: "notifications/initialized", + }); + await transport.send({ jsonrpc: "2.0", id: 3, method: "tools/list" }); + await expectResult( + 3, + expect.objectContaining({ + tools: [expect.objectContaining({ name: toolName })], + }), + ); + } finally { + await transport.close(); } +}, 30000); - test("returns JSON Schema for transform-bearing tools without erroring", async () => { - const { handlers } = makeTransformHarness(); - const describe = handlers.get("describe_tool_input"); - expect(describe).toBeDefined(); - - const result = await describe!.handler( - { tool_names: ["transform-tool"] }, - { signal: new AbortController().signal }, +test.each([ + ["legacy", "static"], + ["legacy", "dynamic"], + ["modern", "static"], + ["modern", "dynamic"], +] as const)( + "generated stdio supports concurrent calls and cancellation (%s, %s)", + async (era, mode) => { + let requestCount = 0; + let signalStarted!: () => void; + let signalClosed!: () => void; + const started = new Promise((resolve) => { + signalStarted = resolve; + }); + const closed = new Promise((resolve) => { + signalClosed = resolve; + }); + const upstream = createServer((request, response) => { + requestCount++; + response.setHeader("content-type", "application/json"); + if (request.url?.includes("/slow/")) { + signalStarted(); + const timer = setTimeout(() => response.end("{}"), 5000); + response.on("close", () => { + clearTimeout(timer); + signalClosed(); + }); + } else { + const url = request.url; + setTimeout( + () => response.end(JSON.stringify({ url })), + requestCount % 7, + ); + } + }); + await new Promise((resolve) => + upstream.listen(0, "127.0.0.1", resolve), + ); + const address = upstream.address(); + if (typeof address !== "object" || address === null) + throw new Error("No upstream address"); + const toolName = "parameters-duplicate-path-param"; + const transport = new StdioClientTransport({ + command: process.execPath, + args: [ + fileURLToPath(new URL("../../bin/mcp-server.js", import.meta.url)), + "start", + "--transport", + "stdio", + "--server-url", + "http://127.0.0.1:" + address.port, + "--tool", + toolName, + ...(mode === "dynamic" ? ["--mode", "dynamic"] : []), + ], + stderr: "pipe", + }); + let stderr = ""; + transport.stderr?.on("data", (chunk: Buffer) => { + stderr += chunk.toString(); + }); + const client = new Client( + { name: "transport-test", version: "1.0.0" }, + { + versionNegotiation: { + mode: era === "modern" ? { pin: "2026-07-28" } : "legacy", + }, + }, ); + const toolCall = (param1: unknown) => { + const arguments_ = { request: { param1, param2: "tail" } }; + return mode === "dynamic" + ? { + name: "execute_tool", + arguments: { name: toolName, arguments: arguments_ }, + } + : { name: toolName, arguments: arguments_ }; + }; + try { + await client.connect(transport); + const { tools } = await client.listTools(); + expect(tools.map((tool) => tool.name)).toEqual( + mode === "dynamic" + ? expect.arrayContaining([ + "list_tools", + "describe_tool_input", + "execute_tool", + ]) + : [toolName], + ); + const results = await Promise.all( + Array.from({ length: 20 }, (_, index) => + client.callTool(toolCall("item-" + index)), + ), + ); + for (const [index, result] of results.entries()) { + expect(result.isError, stderr).not.toBe(true); + const content = result.content?.[0]; + expect(content?.type).toBe("text"); + if (content?.type !== "text") throw new Error("Expected text result"); + expect(JSON.parse(content.text)).toEqual({ + url: "/anything/params/item-" + index + "/tail/item-" + index, + }); + } + const beforeInvalid = requestCount; + expect((await client.callTool(toolCall(42))).isError).toBe(true); + expect(requestCount).toBe(beforeInvalid); - expect(result.isError).toBeFalsy(); - const text = result.content[0]?.text ?? ""; - expect(text).not.toContain("Transforms cannot be represented"); - expect(text).toContain(''); - // The input side of the coercion union is surfaced for the LLM. - expect(text).toContain("amount"); - expect(text).toContain("price"); - expect(text).toContain("when"); - }); -}); + const controller = new AbortController(); + const pending = client + .callTool(toolCall("slow"), { signal: controller.signal }) + .then( + () => false, + () => true, + ); + let startTimer: ReturnType | undefined; + try { + await Promise.race([ + started, + pending.then(() => { + throw new Error("Tool call completed before reaching upstream"); + }), + new Promise((_, reject) => { + startTimer = setTimeout( + () => reject(new Error("Tool call did not reach upstream")), + 2000, + ); + }), + ]); + } finally { + clearTimeout(startTimer); + } + controller.abort(); + expect(await pending).toBe(true); + let timer: ReturnType | undefined; + try { + await Promise.race([ + closed, + new Promise((_, reject) => { + timer = setTimeout( + () => reject(new Error("Upstream request was not cancelled")), + 1500, + ); + }), + ]); + } finally { + clearTimeout(timer); + } + expect( + (await client.callTool(toolCall("after-cancel"))).isError, + ).not.toBe(true); + } finally { + await client.close(); + upstream.closeAllConnections(); + await new Promise((resolve) => upstream.close(() => resolve())); + } + }, + 15000, +); + +test.each(["legacy", "modern"] as const)( + "HTTP shutdown stops accepting connections before draining tool calls (%s)", + async (era) => { + let finishRequest: (() => void) | undefined; + const upstream = createServer((_request, response) => { + finishRequest = () => { + if (response.writableEnded) return; + response.setHeader("content-type", "application/json"); + response.end("{}"); + }; + }); + await new Promise((resolve, reject) => { + upstream.once("error", reject); + upstream.listen(0, "127.0.0.1", () => { + upstream.off("error", reject); + resolve(); + }); + }); + const address = upstream.address(); + if (typeof address !== "object" || address === null) + throw new Error("No upstream address"); + const child = spawn( + process.execPath, + [ + fileURLToPath(new URL("../../bin/mcp-server.js", import.meta.url)), + "serve", + "--port", + "0", + "--server-url", + `http://127.0.0.1:${address.port}`, + "--tool", + "parameters-duplicate-path-param", + ], + { stdio: ["ignore", "ignore", "pipe"] }, + ); + let output = ""; + child.stderr.on("data", (chunk: Buffer) => { + output += chunk.toString(); + }); + let spawnError: Error | undefined; + const exited = new Promise((resolve) => { + child.once("exit", resolve); + child.once("error", (error) => { + spawnError = error; + resolve(null); + }); + }); + const client = new Client( + { name: "shutdown-test", version: "1.0.0" }, + { + versionNegotiation: { + mode: era === "modern" ? { pin: "2026-07-28" } : "legacy", + }, + }, + ); + try { + await vi.waitFor( + () => { + if (spawnError) throw spawnError; + expect(output, output).toMatch(/0\.0\.0\.0:(\d+)/); + }, + { timeout: 5000 }, + ); + const origin = "http://127.0.0.1:" + output.match(/0\.0\.0\.0:(\d+)/)![1]; + const preflight = await fetch(origin + "/mcp", { + method: "OPTIONS", + headers: { + origin: "https://app.example.test", + "access-control-request-method": "DELETE", + }, + }); + expect(preflight.status).toBe(204); + expect(preflight.headers.get("access-control-allow-methods")).toContain( + "DELETE", + ); + expect(await (await fetch(origin)).text()).toContain(origin + "/mcp"); + await client.connect( + new StreamableHTTPClientTransport(new URL(origin + "/mcp")), + ); + const pending = client + .callTool({ + name: "parameters-duplicate-path-param", + arguments: { request: { param1: "held", param2: "tail" } }, + }) + .catch((error: unknown) => ({ isError: true, error })); + await vi.waitFor(() => expect(finishRequest).toBeDefined(), { + timeout: 2000, + }); + child.kill("SIGTERM"); + await vi.waitFor( + async () => { + await expect( + fetch(origin, { signal: AbortSignal.timeout(500) }), + ).rejects.toThrow(); + }, + { timeout: 1500 }, + ); + finishRequest!(); + expect((await pending).isError, output).not.toBe(true); + await vi.waitFor(() => expect(child.exitCode, output).toBe(0), { + timeout: 3000, + }); + } finally { + finishRequest?.(); + await client.close(); + if (child.exitCode === null) child.kill("SIGTERM"); + const timer = setTimeout(() => child.kill("SIGKILL"), 5000); + try { + await exited; + } finally { + clearTimeout(timer); + upstream.closeAllConnections(); + await new Promise((resolve) => upstream.close(() => resolve())); + } + } + }, + 15000, +); diff --git a/templates/templates/typescriptv2/config.ts b/templates/templates/typescriptv2/config.ts index 28263ac90..2cd758957 100644 --- a/templates/templates/typescriptv2/config.ts +++ b/templates/templates/typescriptv2/config.ts @@ -1,6 +1,6 @@ // @ts-ignore function getCompileDependencies( - _config: GeneratorInitialConfiguration, + config: GeneratorInitialConfiguration, ): RunnerCommandDependencies { return [ { @@ -8,7 +8,7 @@ function getCompileDependencies( version: { args: ["--version"], regex: `(?m).*?(\\d+\\.\\d+\\.\\d+).*?`, - minVersion: "18.0.0", + minVersion: config.LangCfg.enableMCPServer ? "20.0.0" : "18.0.0", }, installDocumentation: `Install Node.js by following the instructions at https://nodejs.org/en/download/.`, }, @@ -111,14 +111,53 @@ function getTemplateDependencies() { category: "runtime", condition: "zodV4Mini", }, + "@modelcontextprotocol/client": { + name: "@modelcontextprotocol/client", + version: "2.3.1", + cpe: "cpe:2.3:a:anthropic:model_context_protocol_sdk:*:*:*:*:*:node.js:*:*", + ecosystem: "npm", + category: "dev", + condition: "mcpServer", + }, "@modelcontextprotocol/sdk": { name: "@modelcontextprotocol/sdk", - version: "^1.26.0", + version: "^1.32.1", + cpe: "cpe:2.3:a:anthropic:model_context_protocol_sdk:*:*:*:*:*:node.js:*:*", + ecosystem: "npm", + category: "runtime", + condition: "mcpServer", + }, + "@modelcontextprotocol/server": { + name: "@modelcontextprotocol/server", + version: "^2.3.1", + cpe: "cpe:2.3:a:anthropic:model_context_protocol_sdk:*:*:*:*:*:node.js:*:*", + ecosystem: "npm", + category: "runtime", + condition: "mcpServer", + }, + "@modelcontextprotocol/server-legacy": { + name: "@modelcontextprotocol/server-legacy", + version: "^2.3.1", cpe: "cpe:2.3:a:anthropic:model_context_protocol_sdk:*:*:*:*:*:node.js:*:*", ecosystem: "npm", category: "runtime", condition: "mcpServer", }, + zodMCP: { + name: "zod", + version: "^3.25.28", + cpe: "cpe:2.3:a:colinhacks:zod:*:*:*:*:*:node.js:*:*", + ecosystem: "npm", + category: "runtime", + condition: "mcpServer", + }, + "zod-to-json-schema": { + name: "zod-to-json-schema", + version: "^3.25.2", + ecosystem: "npm", + category: "runtime", + condition: "mcpServer", + }, "decimal.js": { name: "decimal.js", version: "^10.4.3", diff --git a/templates/templates/typescriptv2/includes/dependencies.ts b/templates/templates/typescriptv2/includes/dependencies.ts index 0f91eebe7..6f0015355 100644 --- a/templates/templates/typescriptv2/includes/dependencies.ts +++ b/templates/templates/typescriptv2/includes/dependencies.ts @@ -85,6 +85,10 @@ function templateDevDependencies(): string { sdkHasTests(context.Global.AST) ) { defaultDependencies["vitest"] = deps.vitest.version; + if (isMCPServerEnabled()) { + defaultDependencies["@modelcontextprotocol/client"] = + deps["@modelcontextprotocol/client"].version; + } defaultDependencies["@types/node"] = deps["@types/node"].version; } @@ -119,6 +123,13 @@ function templateDependencies(): string { if (isMCPServerEnabled()) { defaultDependencies["@modelcontextprotocol/sdk"] = deps["@modelcontextprotocol/sdk"].version; + defaultDependencies["@modelcontextprotocol/server"] = + deps["@modelcontextprotocol/server"].version; + defaultDependencies["@modelcontextprotocol/server-legacy"] = + deps["@modelcontextprotocol/server-legacy"].version; + defaultDependencies["zod-to-json-schema"] = + deps["zod-to-json-schema"].version; + defaultDependencies["zod"] = deps.zodMCP.version; } if (isZodV4Mini()) { @@ -191,6 +202,20 @@ function templateAdditionalPackageJSON(): string { } if (isMCPServerEnabled()) { + const engines = values.engines || {}; + const nodeRange = engines.node || ""; + values.engines = { + ...engines, + node: nodeRange + .split("||") + .map((range: string) => { + const branch = range + .trim() + .replace(/^(\S+)\s+-\s+(\S+)$/, ">=$1 <=$2"); + return [branch, ">=20"].filter(Boolean).join(" "); + }) + .join(" || "), + }; values.bin = values.bin || {}; values.bin["mcp"] = "bin/mcp-server.js"; } diff --git a/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/prompts.ts.stmpl b/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/prompts.ts.stmpl index ac476ab54..8d39e1e6f 100644 --- a/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/prompts.ts.stmpl +++ b/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/prompts.ts.stmpl @@ -4,18 +4,18 @@ {{- addImport "./console-logger.js" "ConsoleLogger" }} {{- addImport "../core.js" $coreName }} {{- addImport "./scopes.js" "MCPScope" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "GetPromptResult" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ServerRequest" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ServerNotification" }} -{{- addImport "@modelcontextprotocol/sdk/shared/protocol.js" "RequestHandlerExtra" }} -{{- addImport "@modelcontextprotocol/sdk/server/mcp.js" "McpServer" }} +{{- addImport "@modelcontextprotocol/server" "GetPromptResult" }} +{{- addImport "./shared.js" "MCPServerContext" }} +{{- addImport "./shared.js" "mcpServerContext" }} +{{- addImport "@modelcontextprotocol/server" "McpServer" }} +{{- addImport "@modelcontextprotocol/server" "isCompletable" }} {{- addZodPackageImport "objectOutputType" }} {{- addZodPackageImport "ZodOptional" }} {{- addZodPackageImport "ZodType" }} {{- addZodPackageImport "ZodTypeAny" }} {{- addZodPackageImport "ZodTypeDef" }} +{{- addImport "./shared.js" "mcpSchema" }} -// '@modelcontextprotocol/sdk' currently does not export this type export type PromptArgsRawShape = { [k: string]: ZodType | ZodOptional>; }; @@ -29,7 +29,7 @@ export type PromptDefinition, - extra: RequestHandlerExtra, + extra: MCPServerContext, ) => GetPromptResult | Promise; } : { @@ -39,7 +39,7 @@ export type PromptDefinition, + extra: MCPServerContext, ) => GetPromptResult | Promise; }; @@ -75,27 +75,23 @@ export function createRegisterPrompt( } if (prompt.args) { - if (prompt.description) { - server.prompt(prompt.name, prompt.description, prompt.args, async (args, ctx) => - prompt.prompt(sdk, args, ctx) - ); - } else { - server.prompt(prompt.name, prompt.args, async (args, ctx) => - prompt.prompt(sdk, args, ctx) - ); - } + const shape = Object.fromEntries(Object.entries(prompt.args).map(([name, arg]) => { + let field = arg; + while (field instanceof ZodOptional && !isCompletable(field)) { + field = field.unwrap(); + } + return [name, field]; + })); + server.registerPrompt(prompt.name, { + ...(prompt.description === undefined ? {} : { description: prompt.description }), + argsSchema: { ...mcpSchema(prompt.args), shape }, + }, async (args, ctx) => prompt.prompt(sdk, args, mcpServerContext(ctx))); } else { - if (prompt.description) { - server.prompt(prompt.name, prompt.description, async (ctx) => - prompt.prompt(sdk, ctx) - ); - } else { - server.prompt(prompt.name, async (ctx) => - prompt.prompt(sdk, ctx) - ); - } + server.registerPrompt(prompt.name, { + ...(prompt.description === undefined ? {} : { description: prompt.description }), + }, async (ctx) => prompt.prompt(sdk, mcpServerContext(ctx))); } logger.debug("Registered prompt", { name: prompt.name }); }; -} \ No newline at end of file +} diff --git a/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/resources.ts.stmpl b/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/resources.ts.stmpl index 544fde8dc..b8995eab7 100644 --- a/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/resources.ts.stmpl +++ b/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/resources.ts.stmpl @@ -1,5 +1,6 @@ {{- recurse 1 -}} {{- $coreName := sanitizeCoreSDKName }} +import type { ResourceTemplate as LegacyResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; {{ genImports }} {{- addImport "./shared.js" "valueToBase64" }} {{- addImport "./shared.js" "isBinaryData" }} @@ -7,19 +8,18 @@ {{- addImport "./scopes.js" "MCPScope" }} {{- addImport "./console-logger.js" "ConsoleLogger" }} {{- addImport "../core.js" $coreName }} -{{- addImport "@modelcontextprotocol/sdk/shared/uriTemplate.js" "Variables" }} -{{- addImport "@modelcontextprotocol/sdk/shared/protocol.js" "RequestHandlerExtra" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ServerRequest" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ServerNotification" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ReadResourceResult" }} -{{- addImport "@modelcontextprotocol/sdk/server/mcp.js" "ResourceTemplate" }} -{{- addImport "@modelcontextprotocol/sdk/server/mcp.js" "McpServer" }} -{{- addImport "@modelcontextprotocol/sdk/server/mcp.js" "ResourceMetadata" }} +{{- addImport "@modelcontextprotocol/server" "Variables" }} +{{- addImport "./shared.js" "MCPServerContext" }} +{{- addImport "./shared.js" "mcpServerContext" }} +{{- addImport "@modelcontextprotocol/server" "ReadResourceResult" }} +{{- addImport "@modelcontextprotocol/server" "ResourceTemplate" }} +{{- addImport "@modelcontextprotocol/server" "McpServer" }} +{{- addImport "@modelcontextprotocol/server" "ResourceMetadata" }} export type ReadResourceCallback = ( client: {{$coreName}}, uri: URL, - extra: RequestHandlerExtra, + extra: MCPServerContext, ) => ReadResourceResult | Promise; export type ResourceDefinition = { @@ -35,7 +35,7 @@ export type ReadResourceTemplateCallback = ( client: {{$coreName}}, uri: URL, vars: Variables, - extra: RequestHandlerExtra, + extra: MCPServerContext, ) => ReadResourceResult | Promise; export type ResourceTemplateDefinition = { @@ -43,7 +43,7 @@ export type ResourceTemplateDefinition = { description: string; metadata?: ResourceMetadata; scopes?: MCPScope[]; - resource: ResourceTemplate; + resource: ResourceTemplate | LegacyResourceTemplate; read: ReadResourceTemplateCallback; }; @@ -119,11 +119,11 @@ export function createRegisterResource( description: resource.description, }; - server.resource( + server.registerResource( resource.name, resource.resource, metadata, - async (uri, ctx) => resource.read(sdk, uri, ctx), + async (uri, ctx) => resource.read(sdk, uri, mcpServerContext(ctx)), ); logger.debug("Registered resource", { name: resource.name }); @@ -151,13 +151,27 @@ export function createRegisterResourceTemplate( description: resource.description, }; - server.resource( + server.registerResource( resource.name, - resource.resource, + normalizeResourceTemplate(resource.resource), metadata, - async (uri, vars, ctx) => resource.read(sdk, uri, vars, ctx), + async (uri, vars, ctx) => resource.read(sdk, uri, vars, mcpServerContext(ctx)), ); logger.debug("Registered resource template", { name: resource.name }); }; } + +function normalizeResourceTemplate(resource: ResourceTemplate | LegacyResourceTemplate): ResourceTemplate { + if (resource instanceof ResourceTemplate) return resource; + const complete: NonNullable[1]["complete"]> = {}; + for (const variable of resource.uriTemplate.variableNames) { + const callback = resource.completeCallback(variable); + if (callback) complete[variable] = callback; + } + const list = resource.listCallback; + return new ResourceTemplate(resource.uriTemplate.toString(), { + list: list ? (ctx) => list(mcpServerContext(ctx)) : undefined, + complete, + }); +} diff --git a/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/shared.ts.stmpl b/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/shared.ts.stmpl index 475f5d02f..5d4ed4b98 100644 --- a/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/shared.ts.stmpl +++ b/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/shared.ts.stmpl @@ -2,6 +2,110 @@ {{- genImports -}} {{- addZodPackageImport -}} {{- addImport "../lib/base64.js" "bytesToBase64" -}} +{{- addImport "zod-to-json-schema" "zodToJsonSchema" -}} +{{- addImport "@modelcontextprotocol/server" "StandardSchemaWithJSON" -}} + +import type { ServerContext } from "@modelcontextprotocol/server"; +import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; +import type { ServerRequest, ServerNotification } from "@modelcontextprotocol/sdk/types.js"; +import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; + +export type MCPServerContext = ServerContext & RequestHandlerExtra; + +export function mcpServerContext(ctx: ServerContext): MCPServerContext { + return { + ...ctx, + signal: ctx.mcpReq.signal, + requestId: ctx.mcpReq.id, + ...(ctx.mcpReq._meta ? { _meta: ctx.mcpReq._meta } : {}), + ...(ctx.http?.authInfo ? { authInfo: ctx.http.authInfo } : {}), + ...(ctx.http?.req ? { requestInfo: { headers: Object.fromEntries(ctx.http.req.headers), url: new URL(ctx.http.req.url) } } : {}), + ...(ctx.http?.closeSSE ? { closeSSEStream: ctx.http.closeSSE } : {}), + ...(ctx.http?.closeStandaloneSSE ? { closeStandaloneSSEStream: ctx.http.closeStandaloneSSE } : {}), + sendNotification: async (notification) => { + if (!ctx.mcpReq.signal.aborted) await ctx.mcpReq.notify(notification); + }, + sendRequest: async (request, resultSchema, options) => { + if (ctx.mcpReq.signal.aborted) throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled"); + return (ctx.mcpReq.send as MCPServerContext["sendRequest"])(request, resultSchema, options); + }, + }; +} + +export function mcpSchema( + shape: A, +): StandardSchemaWithJSON>, z.output>> { + const schema = z.object(shape); + const jsonSchema = (direction: "input" | "output", target: string): Record => { + if (target !== "draft-07" && target !== "draft-2020-12") { + throw new Error(`Unsupported JSON Schema target: ${target}`); + } + const result = JSON.parse(JSON.stringify( + zodToJsonSchema(schema, { + target: target === "draft-07" ? "jsonSchema7" : "jsonSchema2019-09", + pipeStrategy: direction, + effectStrategy: direction === "output" ? "any" : "input", + strictUnions: direction === "input", + }), + (_key, value) => typeof value === "bigint" ? value.toString() : value, + )); + return target === "draft-2020-12" ? jsonSchema2020(result) : result; + }; + + return { + "~standard": { + version: 1, + vendor: "zod", + jsonSchema: { input: ({ target }) => jsonSchema("input", target), output: ({ target }) => jsonSchema("output", target) }, + validate: async (value) => { + const result = await schema.safeParseAsync(value); + return result.success + ? { value: result.data } + : { issues: result.error.issues }; + }, + }, + }; +} + +function jsonSchema2020(schema: Record): Record { + const paths = new Map(); + const references: Record[] = []; + const visit = (value: unknown, from: string, to: string): void => { + if (!value || typeof value !== "object" || Array.isArray(value)) return; + const node = value as Record; + paths.set(from, to); + if (typeof node["$ref"] === "string") references.push(node); + const entries = Object.entries(node); + const tuple = Array.isArray(node["items"]); + if (tuple) { + const items = node["items"] as unknown[]; + if (items.length) node["prefixItems"] = items; + delete node["items"]; + if (node["additionalItems"] !== undefined) node["items"] = node["additionalItems"]; + delete node["additionalItems"]; + } + for (const [key, child] of entries) { + const nextKey = tuple && key === "items" ? "prefixItems" : tuple && key === "additionalItems" ? "items" : key; + if (["properties", "patternProperties", "definitions", "$defs"].includes(key)) { + for (const [name, item] of Object.entries(child as Record)) { + const escapedName = name.replace(/~/g, "~0").replace(/\//g, "~1"); + visit(item, `${from}/${key}/${name}`, `${to}/${nextKey}/${escapedName}`); + } + } else if (["allOf", "anyOf", "oneOf", "prefixItems"].includes(key) || (tuple && key === "items")) { + (child as unknown[]).forEach((item, index) => visit(item, `${from}/${key}/${index}`, `${to}/${nextKey}/${index}`)); + } else if (["items", "additionalItems", "additionalProperties", "propertyNames", "contains", "not", "if", "then", "else", "unevaluatedProperties", "unevaluatedItems"].includes(key)) { + visit(child, `${from}/${key}`, `${to}/${nextKey}`); + } + } + }; + visit(schema, "#", "#"); + for (const reference of references) { + const ref = reference["$ref"] as string; + reference["$ref"] = paths.get(ref) ?? ref; + } + schema["$schema"] = "https://json-schema.org/draft/2020-12/schema"; + return schema; +} type BinaryData = | Uint8Array diff --git a/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/tools.ts.stmpl b/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/tools.ts.stmpl index 25005ef25..fdf24679e 100644 --- a/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/tools.ts.stmpl +++ b/templates/templates/typescriptv2/mcp-server/auxiliary/src/mcp-server/tools.ts.stmpl @@ -7,14 +7,14 @@ {{- addImport "./scopes.js" "MCPScope" }} {{- addImport "./console-logger.js" "ConsoleLogger" }} {{- addImport "../core.js" $coreName }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "CallToolResult" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ServerRequest" }} -{{- addImport "@modelcontextprotocol/sdk/types.js" "ServerNotification" }} -{{- addImport "@modelcontextprotocol/sdk/shared/protocol.js" "RequestHandlerExtra" }} -{{- addImport "@modelcontextprotocol/sdk/server/mcp.js" "McpServer" }} +{{- addImport "@modelcontextprotocol/server" "CallToolResult" }} +{{- addImport "./shared.js" "MCPServerContext" }} +{{- addImport "./shared.js" "mcpServerContext" }} +{{- addImport "@modelcontextprotocol/server" "McpServer" }} {{- addZodPackageImport "objectOutputType" }} {{- addZodPackageImport "ZodTypeAny" }} {{- addZodPackageImport "ZodRawShape" }} +{{- addImport "./shared.js" "mcpSchema" }} export type ToolDefinition = Args extends ZodRawShape @@ -26,7 +26,7 @@ export type ToolDefinition = tool: ( client: {{$coreName}}, args: objectOutputType, - extra: RequestHandlerExtra + extra: MCPServerContext ) => CallToolResult | Promise; } : { @@ -36,7 +36,7 @@ export type ToolDefinition = args?: undefined; tool: ( client: {{$coreName}}, - extra: RequestHandlerExtra + extra: MCPServerContext ) => CallToolResult | Promise; }; @@ -118,13 +118,14 @@ export function createRegisterTool( } if (tool.args) { - server.tool(tool.name, tool.description, tool.args, async (args, ctx) => { - return tool.tool(sdk, args, ctx); - }); + server.registerTool(tool.name, { + description: tool.description, + inputSchema: mcpSchema(tool.args), + }, async (args, ctx) => tool.tool(sdk, args, mcpServerContext(ctx))); } else { - server.tool(tool.name, tool.description, async (ctx) => { - return tool.tool(sdk, ctx); - }); + server.registerTool(tool.name, { + description: tool.description, + }, async (ctx) => tool.tool(sdk, mcpServerContext(ctx))); } logger.debug("Registered tool", { name: tool.name }); diff --git a/templates/templates/typescriptv2/mcp-server/cli/start/impl.ts.stmpl b/templates/templates/typescriptv2/mcp-server/cli/start/impl.ts.stmpl index 8585daaf0..bc05c3f6a 100644 --- a/templates/templates/typescriptv2/mcp-server/cli/start/impl.ts.stmpl +++ b/templates/templates/typescriptv2/mcp-server/cli/start/impl.ts.stmpl @@ -5,8 +5,8 @@ {{- $hasServers := (and $globalSDK.Servers $globalSDK.Servers.HasAbsoluteURL) -}} {{- $serverVariables := and $globalSDK.Servers (gt (len $globalSDK.Servers.GetVariables) 0) -}} {{- $serverMap := (and $globalSDK.Servers $globalSDK.Servers.ServerMap) -}} -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { SSEServerTransport } from "@modelcontextprotocol/server-legacy/sse"; +import { serveStdio } from "@modelcontextprotocol/server/stdio"; import express from "express"; import { LocalContext } from "../../cli.js"; import { @@ -86,9 +86,8 @@ export async function main(this: LocalContext, flags: StartCommandFlags) { async function startStdio(flags: StartCommandFlags) { const logger = createConsoleLogger(flags["log-level"]); - const transport = new StdioServerTransport(); - const server = {{templateString "mcp-server/create-mcp-server.ts.stmpl" .Local}}; - await server.connect(transport); + const createServer = {{templateString "mcp-server/create-mcp-server.ts.stmpl" (dict "Factory" true "Scopes" .Local.Scopes "UsageLocation" .Local.UsageLocation)}}; + const server = serveStdio(createServer); const abort = async () => { await server.close(); @@ -98,6 +97,10 @@ async function startStdio(flags: StartCommandFlags) { process.on("SIGINT", abort); } +function createMCPServerFactory(options: Parameters[0]) { + return () => createMCPServer(options); +} + async function startSSE(flags: StartCommandFlags) { const logger = createConsoleLogger(flags["log-level"]); const app = express(); diff --git a/templates/templates/typescriptv2/mcp-server/create-mcp-server.ts.stmpl b/templates/templates/typescriptv2/mcp-server/create-mcp-server.ts.stmpl index 90ea5fa14..0a326aca5 100644 --- a/templates/templates/typescriptv2/mcp-server/create-mcp-server.ts.stmpl +++ b/templates/templates/typescriptv2/mcp-server/create-mcp-server.ts.stmpl @@ -1,7 +1,7 @@ {{- $globalSDK := .Global.AST.MainSDK -}} {{- $globalSecurity := $globalSDK.Security }} {{- $serverMap := (and $globalSDK.Servers $globalSDK.Servers.ServerMap) -}} -createMCPServer({ +{{ if .Local.Factory }}createMCPServerFactory{{ else }}createMCPServer{{ end }}({ logger, allowedTools: flags.tool, {{- if gt (len .Local.Scopes) 0 }} @@ -32,4 +32,4 @@ createMCPServer({ {{ sanitizeFieldName $name }}: {{mcpflag "flags" $name}}, {{- end}} {{- end}} -}) \ No newline at end of file +}) diff --git a/templates/templates/typescriptv2/mcp-server/resource-template.ts.stmpl b/templates/templates/typescriptv2/mcp-server/resource-template.ts.stmpl index a2d82fc07..210b79d5c 100644 --- a/templates/templates/typescriptv2/mcp-server/resource-template.ts.stmpl +++ b/templates/templates/typescriptv2/mcp-server/resource-template.ts.stmpl @@ -8,7 +8,7 @@ {{ genImports }} {{- addImport "../resources.js" "ResourceTemplateDefinition" }} {{- addImport "../resources.js" "formatResult" }} -{{- addImport "@modelcontextprotocol/sdk/server/mcp.js" "ResourceTemplate" }} +{{- addImport "@modelcontextprotocol/server" "ResourceTemplate" }} {{- if $hasCustomFile }} {{- addImport (printf "./%s.custom.js" .Local.Filename) "customizer" "aliasImport" }} @@ -48,7 +48,7 @@ export const {{sanitizeMCPPrimitiveExport $op}}: ResourceTemplateDefinition = { {{ sanitizeFieldName .Name }}, {{- end }} {{- end }} - { {{constants.fetchOptions}}: { signal: extra.signal } }, + { {{constants.fetchOptions}}: { signal: extra.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/templates/templates/typescriptv2/mcp-server/resource.ts.stmpl b/templates/templates/typescriptv2/mcp-server/resource.ts.stmpl index 0179a2e61..fed26aa97 100644 --- a/templates/templates/typescriptv2/mcp-server/resource.ts.stmpl +++ b/templates/templates/typescriptv2/mcp-server/resource.ts.stmpl @@ -24,7 +24,7 @@ export const {{sanitizeMCPPrimitiveExport $op}}: ResourceDefinition = { {{- addFuncImport $op $usageLocation }} const [result, apiCall] = await {{$funcName}}( client, - { {{constants.fetchOptions}}: { signal: extra.signal } }, + { {{constants.fetchOptions}}: { signal: extra.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/templates/templates/typescriptv2/mcp-server/server.ts.stmpl b/templates/templates/typescriptv2/mcp-server/server.ts.stmpl index c17d6d7bc..9ed3e84d2 100644 --- a/templates/templates/typescriptv2/mcp-server/server.ts.stmpl +++ b/templates/templates/typescriptv2/mcp-server/server.ts.stmpl @@ -7,7 +7,7 @@ {{- $coreName := sanitizeCoreSDKName }} {{- $hasServerExtensionFile := fileExists "src/mcp-server/server.extensions.ts" }} {{- $hasServers := (and $globalSDK.Servers $globalSDK.Servers.HasAbsoluteURL) -}} -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import { {{$coreName}} } from "../core.js"; import type { ConsoleLogger } from "./console-logger.js"; import { MCPScope } from "./scopes.js"; diff --git a/templates/templates/typescriptv2/mcp-server/tool.ts.stmpl b/templates/templates/typescriptv2/mcp-server/tool.ts.stmpl index ab91c0580..3e64ec1ce 100644 --- a/templates/templates/typescriptv2/mcp-server/tool.ts.stmpl +++ b/templates/templates/typescriptv2/mcp-server/tool.ts.stmpl @@ -49,7 +49,7 @@ export const {{sanitizeMCPPrimitiveExport $op}}: ToolDefinition{{if $hasArgs}} { expect(tool$resourceUpdateResource.scopes).toEqual(["write"]); expect(tool$resourceDeleteResource.scopes).toEqual(["destructive", "write"]); }); + +test("MCP v2 advertises Zod 3 inputs and preserves validation and transforms", async () => { + const server = new McpServer({ name: "schema-test", version: "1.0.0" }); + const client = new Client({ name: "schema-client", version: "1.0.0" }); + const sdk = new SDKCore(); + const logger = createConsoleLogger("error"); + const registerTool = createRegisterTool(logger, server, sdk, new Set()); + const handler = vi.fn( + (args: { count: number; label: string }, signal: AbortSignal) => { + expect(signal).toBeInstanceOf(AbortSignal); + return { + content: [{ type: "text" as const, text: JSON.stringify(args) }], + }; + }, + ); + registerTool({ + name: "convert-input", + description: "Validate and transform an input", + args: { + count: z.string().regex(/^\d+$/).transform(Number), + label: z.string().default("example"), + choice: z.union([z.string(), z.instanceof(Date)]).optional(), + }, + tool: (_sdk, args, ctx) => handler(args, ctx.mcpReq.signal), + }); + registerTool({ + name: "no-input", + description: "Accept no arguments", + tool: (_sdk, ctx) => ({ + content: [{ type: "text", text: String(ctx.mcpReq.signal.aborted) }], + }), + }); + const registerPrompt = createRegisterPrompt(logger, server, sdk, new Set()); + registerPrompt({ + name: "greeting", + args: { name: z.string() }, + prompt: (_sdk, args) => ({ + messages: [{ role: "user", content: { type: "text", text: args.name } }], + }), + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const { tools } = await client.listTools(); + expect( + tools.find((tool) => tool.name === "convert-input")?.inputSchema, + ).toMatchObject({ + type: "object", + properties: { + count: { type: "string" }, + label: { default: "example" }, + choice: { anyOf: [{ type: "string" }] }, + }, + required: ["count"], + }); + expect( + await client.callTool({ + name: "convert-input", + arguments: { count: "42" }, + }), + ).toMatchObject({ + content: [ + { type: "text", text: JSON.stringify({ count: 42, label: "example" }) }, + ], + }); + expect( + await client.callTool({ + name: "convert-input", + arguments: { count: "invalid" }, + }), + ).toMatchObject({ isError: true }); + expect(handler).toHaveBeenCalledTimes(1); + expect(await client.callTool({ name: "no-input" })).toMatchObject({ + content: [{ type: "text", text: "false" }], + }); + expect((await client.listPrompts()).prompts).toMatchObject([ + { name: "greeting", arguments: [{ name: "name", required: true }] }, + ]); + expect( + await client.getPrompt({ + name: "greeting", + arguments: { name: "example" }, + }), + ).toMatchObject({ + messages: [{ role: "user", content: { type: "text", text: "example" } }], + }); + } finally { + await client.close(); + await server.close(); + } +}); + +test("MCP v2 serializes date and bigint defaults without changing runtime values", async () => { + const server = new McpServer({ name: "defaults-test", version: "1.0.0" }); + const client = new Client({ name: "defaults-client", version: "1.0.0" }); + const date = new Date("2025-01-01T00:00:00Z"); + const id = BigInt("12345678901234567890"); + const handler = vi.fn((args: { value: { date: Date; id: bigint } }) => { + expect(args.value).toEqual({ date, id }); + return { content: [{ type: "text" as const, text: "defaults preserved" }] }; + }); + createRegisterTool( + createConsoleLogger("error"), + server, + new SDKCore(), + new Set(), + )({ + name: "nested-defaults", + description: "Preserve nested default values", + args: { + value: z.object({ date: z.date(), id: z.bigint() }).default({ date, id }), + }, + tool: (_sdk, args) => handler(args), + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const { tools } = await client.listTools(); + expect(tools[0]?.inputSchema).toMatchObject({ + properties: { + value: { default: { date: date.toISOString(), id: id.toString() } }, + }, + }); + expect( + await client.callTool({ name: "nested-defaults", arguments: {} }), + ).toMatchObject({ + content: [{ type: "text", text: "defaults preserved" }], + }); + expect(handler).toHaveBeenCalledOnce(); + } finally { + await client.close(); + await server.close(); + } +}); + +test("prompt completion preserves input validation and transforms", async () => { + const server = new McpServer({ name: "prompt-test", version: "1.0.0" }); + const client = new Client({ name: "prompt-client", version: "1.0.0" }); + const complete = vi.fn((value: string) => + ["typescript", "python"].filter((language) => language.startsWith(value)), + ); + const handler = vi.fn( + (args: { + language: string; + count: string; + label?: string | undefined; + }) => ({ + messages: [ + { + role: "user" as const, + content: { type: "text" as const, text: JSON.stringify(args) }, + }, + ], + }), + ); + createRegisterPrompt( + createConsoleLogger("error"), + server, + new SDKCore(), + new Set(), + )({ + name: "review", + args: { + language: completable(z.string().describe("Language"), complete), + count: z + .string() + .regex(/^\d+$/) + .transform((value) => `count:${value}`), + label: completable(z.string(), complete).optional(), + }, + prompt: (_sdk, args) => handler(args), + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + expect((await client.listPrompts()).prompts[0]?.arguments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "language", + description: "Language", + required: true, + }), + expect.objectContaining({ name: "label", required: false }), + ]), + ); + expect( + await client.complete({ + ref: { type: "ref/prompt", name: "review" }, + argument: { name: "language", value: "type" }, + context: { arguments: { count: "7" } }, + }), + ).toMatchObject({ completion: { values: ["typescript"] } }); + expect(complete).toHaveBeenCalledWith("type", { + arguments: { count: "7" }, + }); + expect( + await client.complete({ + ref: { type: "ref/prompt", name: "review" }, + argument: { name: "label", value: "py" }, + }), + ).toMatchObject({ completion: { values: ["python"] } }); + expect( + await client.getPrompt({ + name: "review", + arguments: { language: "typescript", count: "7" }, + }), + ).toMatchObject({ + messages: [ + { + content: { + text: JSON.stringify({ + language: "typescript", + count: "count:7", + }), + }, + }, + ], + }); + await expect( + client.getPrompt({ + name: "review", + arguments: { language: "typescript", count: "invalid" }, + }), + ).rejects.toThrow(); + expect(handler).toHaveBeenCalledTimes(1); + } finally { + await client.close(); + await server.close(); + } +}); diff --git a/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts b/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts new file mode 100644 index 000000000..6f46f35f7 --- /dev/null +++ b/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts @@ -0,0 +1,542 @@ +import { fileURLToPath } from "node:url"; +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { InMemoryTransport, McpServer } from "@modelcontextprotocol/server"; +import { AjvJsonSchemaValidator } from "@modelcontextprotocol/server/validators/ajv"; +import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { + ErrorCode, + ListRootsResultSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { + mcpSchema, + mcpServerContext, + type MCPServerContext, +} from "../mcp-server/shared.js"; +import { expect, expectTypeOf, test, vi } from "vitest"; +import { SDKCore } from "../core.js"; +import { createConsoleLogger } from "../mcp-server/console-logger.js"; +import { + createRegisterResource, + createRegisterResourceTemplate, +} from "../mcp-server/resources.js"; +import { createRegisterTool } from "../mcp-server/tools.js"; +import { createRegisterPrompt } from "../mcp-server/prompts.js"; +import { z as rootZod } from "zod"; +import { z } from "zod/v3"; + +async function withClient( + server: McpServer, + run: (client: Client) => Promise, +) { + const client = new Client( + { name: "compatibility-client", version: "1.0.0" }, + { capabilities: { roots: {} }, versionNegotiation: { mode: "legacy" } }, + ); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + await run(client); + } finally { + await client.close(); + await server.close(); + } +} + +test("extension tools accept schemas from the root Zod import", async () => { + const server = new McpServer({ name: "root-zod", version: "1.0.0" }); + createRegisterTool( + createConsoleLogger("error"), + server, + new SDKCore(), + new Set(), + )({ + name: "length", + description: "Return input length", + args: { + value: rootZod + .string() + .default("example") + .transform((value) => value.length), + enabled: rootZod.boolean().default(true), + }, + tool: (_sdk, args) => { + expectTypeOf(args.value).toEqualTypeOf(); + expectTypeOf(args.enabled).toEqualTypeOf(); + return { + content: [ + { type: "text", text: String(args.enabled ? args.value : 0) }, + ], + }; + }, + }); + await withClient(server, async (client) => { + expect(await client.listTools()).toMatchObject({ + tools: [{ inputSchema: { properties: { value: { type: "string" } } } }], + }); + expect( + await client.callTool({ name: "length", arguments: {} }), + ).toMatchObject({ + content: [{ text: "7" }], + }); + expect( + await client.callTool({ name: "length", arguments: { value: "four" } }), + ).toMatchObject({ content: [{ text: "4" }] }); + expect( + await client.callTool({ name: "length", arguments: { value: 4 } }), + ).toMatchObject({ isError: true }); + }); +}); + +test("v1 resource templates retain discovery, completion, and reads", async () => { + const server = new McpServer({ + name: "compatibility-server", + version: "1.0.0", + }); + createRegisterResourceTemplate( + createConsoleLogger("error"), + server, + new SDKCore(), + new Set(), + )({ + name: "widget", + description: "Read a widget", + resource: new ResourceTemplate("widget://{name}", { + list: (extra) => { + expect(extra.signal).toBeInstanceOf(AbortSignal); + expect(extra.requestId).toBeDefined(); + return { resources: [{ name: "example", uri: "widget://example" }] }; + }, + complete: { name: (value) => [value + "ample"] }, + }), + read: (_sdk, uri, vars, extra) => { + expect(extra.signal).toBe(extra.mcpReq.signal); + expect(extra.requestId).toBe(extra.mcpReq.id); + return { + contents: [{ uri: uri.toString(), text: String(vars["name"]) }], + }; + }, + }); + await withClient(server, async (client) => { + expect(await client.listResources()).toMatchObject({ + resources: [{ uri: "widget://example" }], + }); + expect(await client.listResourceTemplates()).toMatchObject({ + resourceTemplates: [{ uriTemplate: "widget://{name}" }], + }); + expect( + await client.complete({ + ref: { type: "ref/resource", uri: "widget://{name}" }, + argument: { name: "name", value: "ex" }, + }), + ).toMatchObject({ completion: { values: ["example"] } }); + expect( + await client.readResource({ uri: "widget://example" }), + ).toMatchObject({ contents: [{ text: "example" }] }); + }); +}); + +test("legacy tool context preserves cancellation", async () => { + const server = new McpServer({ + name: "compatibility-server", + version: "1.0.0", + }); + const logger = createConsoleLogger("error"); + const tool = createRegisterTool(logger, server, new SDKCore(), new Set()); + let started!: () => void; + const running = new Promise((resolve) => { + started = resolve; + }); + let cancelled!: () => void; + const aborted = new Promise((resolve) => { + cancelled = resolve; + }); + let context: MCPServerContext | undefined; + tool({ + name: "held", + description: "Wait for cancellation", + tool: async (_sdk, extra) => { + context = extra; + expect(extra.signal).toBe(extra.mcpReq.signal); + expect(extra.requestId).toBe(extra.mcpReq.id); + expect(extra.sendNotification).toBeTypeOf("function"); + extra.signal.addEventListener("abort", cancelled, { once: true }); + started(); + await aborted; + return { content: [{ type: "text", text: "cancelled" }] }; + }, + }); + await withClient(server, async (client) => { + const controller = new AbortController(); + const pending = client + .callTool({ name: "held" }, { signal: controller.signal }) + .catch((error: unknown) => error); + await running; + controller.abort(); + await aborted; + await expect( + context!.sendNotification({ + method: "notifications/tools/list_changed", + }), + ).resolves.toBeUndefined(); + await expect( + context!.sendRequest({ method: "roots/list" }, ListRootsResultSchema), + ).rejects.toMatchObject({ code: ErrorCode.ConnectionClosed }); + expect(await pending).toBeInstanceOf(Error); + }); +}, 10000); + +test.each([false, true])( + "legacy sendRequest validates v1 result schemas (invalid=%s)", + async (invalid) => { + const server = new McpServer({ + name: "compatibility-server", + version: "1.0.0", + }); + server.registerTool("roots", {}, async (ctx) => { + const schema = invalid + ? ListRootsResultSchema.refine(() => false) + : ListRootsResultSchema; + const result = await mcpServerContext(ctx).sendRequest( + { method: "roots/list" }, + schema, + ); + return { content: [{ type: "text", text: JSON.stringify(result) }] }; + }); + await withClient(server, async (client) => { + client.setRequestHandler("roots/list", async () => ({ + roots: [{ uri: "file:///example", name: "Example" }], + })); + const result = await client.callTool({ name: "roots" }); + if (invalid) expect(result.isError).toBe(true); + else { + expect(result.isError).not.toBe(true); + expect(result.content).toEqual([ + { + type: "text", + text: JSON.stringify({ + roots: [{ uri: "file:///example", name: "Example" }], + }), + }, + ]); + } + }); + }, +); + +test("optional MCP prompts expose legacy context with and without arguments", async () => { + const server = new McpServer({ name: "prompt-context", version: "1.0.0" }); + const register = createRegisterPrompt( + createConsoleLogger("error"), + server, + new SDKCore(), + new Set(), + ); + const result = (extra: MCPServerContext) => { + expect(extra.signal).toBe(extra.mcpReq.signal); + expect(extra.requestId).toBe(extra.mcpReq.id); + expect(extra.sendRequest).toBeTypeOf("function"); + expect(extra.sendNotification).toBeTypeOf("function"); + expect(extra._meta).toEqual({ example: "metadata" }); + return { messages: [] }; + }; + register({ + name: "with-args", + args: { value: z.string() }, + prompt: (_sdk, _args, extra) => result(extra), + }); + register({ name: "without-args", prompt: (_sdk, extra) => result(extra) }); + await withClient(server, async (client) => { + await client.getPrompt({ + name: "with-args", + arguments: { value: "example" }, + _meta: { example: "metadata" }, + }); + await client.getPrompt({ + name: "without-args", + _meta: { example: "metadata" }, + }); + }); +}); + +test("MCP schemas distinguish pipeline input and output while preserving parsing", async () => { + const shape = { + count: z.string().transform(Number).pipe(z.number().int().min(1)), + }; + const schema = mcpSchema(shape)["~standard"]; + const input = schema.jsonSchema.input({ target: "draft-2020-12" }); + const output = schema.jsonSchema.output({ target: "draft-2020-12" }); + expect(input).toMatchObject({ properties: { count: { type: "string" } } }); + expect(output).toMatchObject({ + properties: { count: { type: "integer", minimum: 1 } }, + }); + expect(await schema.validate({ count: "7" })).toEqual({ + value: { count: 7 }, + }); + expect(await schema.validate({ count: "0" })).toHaveProperty("issues"); + const server = new McpServer({ name: "pipeline-test", version: "1.0.0" }); + server.registerTool( + "pipeline", + { inputSchema: mcpSchema(shape), outputSchema: mcpSchema(shape) }, + async (args) => ({ + content: [{ type: "text", text: String(args.count) }], + structuredContent: { count: args.count }, + }), + ); + await withClient(server, async (client) => { + const listed = (await client.listTools()).tools[0]; + expect(listed?.inputSchema).toMatchObject(input); + expect(listed?.outputSchema).toMatchObject(output); + }); +}); + +test("MCP output schemas do not claim input types for transforms or discard unknown union outputs", async () => { + const schema = mcpSchema({ + length: z.string().transform((value) => value.length), + choice: z.union([ + z.string().transform((value) => value.length), + z.boolean(), + ]), + })["~standard"]; + const input = schema.jsonSchema.input({ target: "draft-2020-12" }); + expect(input).toMatchObject({ + properties: { + length: { type: "string" }, + choice: { anyOf: [{ type: "string" }, { type: "boolean" }] }, + }, + }); + const output = schema.jsonSchema.output({ target: "draft-2020-12" }); + expect(output["properties"]).toEqual({ + length: {}, + choice: { anyOf: [{}, { type: "boolean" }] }, + }); + expect(await schema.validate({ length: "four", choice: "six" })).toEqual({ + value: { length: 4, choice: 3 }, + }); +}); + +test.each(["input", "output"] as const)( + "MCP %s schemas honor JSON Schema dialects for tuples", + (direction) => { + const converter = mcpSchema({ + fixed: z.tuple([z.string(), z.number()]), + rest: z.tuple([z.string()]).rest(z.number()), + empty: z.tuple([]), + })["~standard"].jsonSchema[direction]; + for (const target of ["draft-07", "draft-2020-12"] as const) { + const schema = converter({ target }); + expect(schema["$schema"]).toBe( + target === "draft-07" + ? "http://json-schema.org/draft-07/schema#" + : "https://json-schema.org/draft/2020-12/schema", + ); + const validate = new AjvJsonSchemaValidator().getValidator(schema); + const valid = { + fixed: ["example", 1], + rest: ["example", 2, 3], + empty: [], + }; + expect(validate(valid).valid).toBe(true); + expect(validate({ ...valid, fixed: ["example", "wrong"] }).valid).toBe( + false, + ); + expect(validate({ ...valid, fixed: ["example", 1, 2] }).valid).toBe( + false, + ); + expect(validate({ ...valid, rest: ["example", "wrong"] }).valid).toBe( + false, + ); + expect(validate({ ...valid, empty: [1] }).valid).toBe(false); + } + }, +); + +test("MCP draft 2020-12 schemas preserve recursive references relocated inside tuples", () => { + type Node = { value: string; children?: Node[] | undefined }; + const node: z.ZodType = z.lazy(() => + z.object({ value: z.string(), children: z.array(node).optional() }), + ); + const schema = mcpSchema({ pair: z.tuple([node, node]), same: node })[ + "~standard" + ].jsonSchema.input({ target: "draft-2020-12" }); + const validate = new AjvJsonSchemaValidator().getValidator(schema); + const valid = { + pair: [{ value: "one", children: [{ value: "nested" }] }, { value: "two" }], + same: { value: "three" }, + }; + expect(validate(valid).valid).toBe(true); + expect(validate({ ...valid, same: { value: 3 } }).valid).toBe(false); + expect( + validate({ + ...valid, + pair: [{ value: "one", children: [{ value: 1 }] }, { value: "two" }], + }).valid, + ).toBe(false); +}); + +test("MCP dialect conversion leaves default values untouched and rejects unsupported dialects", () => { + const value = { + items: [{ type: "string" }], + additionalItems: false, + $ref: "#/properties/example/items/0", + }; + const converter = mcpSchema({ example: z.any().default(value) })["~standard"] + .jsonSchema; + expect(converter.input({ target: "draft-2020-12" })).toMatchObject({ + properties: { example: { default: value } }, + }); + expect(() => converter.input({ target: "openapi-3.0" })).toThrow( + "Unsupported JSON Schema target", + ); + expect(() => converter.output({ target: "openapi-3.0" })).toThrow( + "Unsupported JSON Schema target", + ); +}); + +test.each(["a/b", "a~b"])( + "MCP draft 2020-12 references escape property name %s", + (name) => { + const value = z.object({ value: z.string() }); + const schema = mcpSchema({ [name]: value, same: value })[ + "~standard" + ].jsonSchema.input({ target: "draft-2020-12" }); + const validate = new AjvJsonSchemaValidator().getValidator(schema); + expect( + validate({ [name]: { value: "one" }, same: { value: "two" } }).valid, + ).toBe(true); + expect( + validate({ [name]: { value: "one" }, same: { value: 2 } }).valid, + ).toBe(false); + }, +); + +test("tools with arguments and static resources preserve legacy context", async () => { + const server = new McpServer({ name: "callback-context", version: "1.0.0" }); + const logger = createConsoleLogger("error"); + const sdk = new SDKCore(); + const check = (extra: MCPServerContext) => { + expect(extra.signal).toBe(extra.mcpReq.signal); + expect(extra.requestId).toBe(extra.mcpReq.id); + expect(extra._meta).toEqual({ example: "metadata" }); + expect(extra.sendRequest).toBeTypeOf("function"); + expect(extra.sendNotification).toBeTypeOf("function"); + }; + createRegisterTool( + logger, + server, + sdk, + new Set(), + )({ + name: "length", + description: "Return input length", + args: { value: z.string().transform((value) => value.length) }, + tool: (_sdk, args, extra) => { + check(extra); + expect(args.value).toBe(4); + return { content: [{ type: "text", text: String(args.value) }] }; + }, + }); + createRegisterResource( + logger, + server, + sdk, + new Set(), + )({ + name: "example", + resource: "example://static", + read: (_sdk, uri, extra) => { + check(extra); + return { contents: [{ uri: uri.toString(), text: "example" }] }; + }, + }); + await withClient(server, async (client) => { + expect( + await client.callTool({ + name: "length", + arguments: { value: "four" }, + _meta: { example: "metadata" }, + }), + ).toMatchObject({ content: [{ text: "4" }] }); + expect( + await client.readResource({ + uri: "example://static", + _meta: { example: "metadata" }, + }), + ).toMatchObject({ contents: [{ text: "example" }] }); + }); +}); + +test("generated stdio accepts legacy initialization after modern discovery", async () => { + const toolName = "parameters-duplicate-path-param"; + const transport = new StdioClientTransport({ + command: process.execPath, + args: [ + fileURLToPath(new URL("../../bin/mcp-server.js", import.meta.url)), + "start", + "--transport", + "stdio", + "--tool", + toolName, + ], + stderr: "pipe", + }); + const messages: unknown[] = []; + transport.onmessage = (message) => messages.push(message); + const expectResult = (id: number, result: unknown) => + vi.waitFor( + () => { + expect(messages).toContainEqual( + expect.objectContaining({ id, result }), + ); + }, + { timeout: 10000 }, + ); + try { + await transport.start(); + await transport.send({ + jsonrpc: "2.0", + id: 1, + method: "server/discover", + params: { + _meta: { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + name: "fallback-test", + version: "1.0.0", + }, + "io.modelcontextprotocol/clientCapabilities": {}, + }, + }, + }); + await expectResult(1, expect.any(Object)); + await transport.send({ + jsonrpc: "2.0", + id: 2, + method: "initialize", + params: { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { name: "fallback-test", version: "1.0.0" }, + }, + }); + await expectResult( + 2, + expect.objectContaining({ protocolVersion: "2025-06-18" }), + ); + await transport.send({ + jsonrpc: "2.0", + method: "notifications/initialized", + }); + await transport.send({ jsonrpc: "2.0", id: 3, method: "tools/list" }); + await expectResult( + 3, + expect.objectContaining({ + tools: [expect.objectContaining({ name: toolName })], + }), + ); + } finally { + await transport.close(); + } +}, 30000); diff --git a/zSDKs/mcp-typescript/.speakeasy/gen.lock b/zSDKs/mcp-typescript/.speakeasy/gen.lock index 483a11aad..4b83243b1 100644 --- a/zSDKs/mcp-typescript/.speakeasy/gen.lock +++ b/zSDKs/mcp-typescript/.speakeasy/gen.lock @@ -58,7 +58,7 @@ trackedFiles: manifest.json: last_write_checksum: sha1:21c466edb4930e35172f9684728e020312e88dea package.json: - last_write_checksum: sha1:4656382e8095f3ea4cc61ac36f9a78cdbf312e56 + last_write_checksum: sha1:4b0ecf94b179c07fabd75eac7a9f7ebf37868fbc server.json: last_write_checksum: sha1:cdd68ed3cc00a507a2bc71e85823fe10482e2527 src/__tests__/assertions.ts: @@ -448,7 +448,7 @@ trackedFiles: src/__tests__/testhelpers.ts: last_write_checksum: sha1:6ad9dca651df4525ccd2ac06d7d436cc22b22632 src/cloudflare-worker/cloudflare-worker.ts: - last_write_checksum: sha1:7bd187eec20b94929fce5739b4de2291bf1ece97 + last_write_checksum: sha1:6976157dc5332acab394ceb93d300aba0a13037d src/core.ts: last_write_checksum: sha1:ce840032c45608f5f4022b32bcce32cb69c5ed26 src/funcs/binaryAndStringUpload.ts: @@ -554,7 +554,7 @@ trackedFiles: src/hooks/types.ts: last_write_checksum: sha1:29372122898cc523fa47576f3f6608a28ec6080e src/landing-page.ts: - last_write_checksum: sha1:223a8a48ebafe1deb3b9a59969ac88c5bb2ca75d + last_write_checksum: sha1:2980781114eda92848b5c2ddc22e9b6f46891ca2 src/lib/base64.ts: last_write_checksum: sha1:e9f04a037018040361043104960982f7c22db52d src/lib/config.ts: @@ -590,127 +590,127 @@ trackedFiles: src/mcp-server/cli/eval/command.ts: last_write_checksum: sha1:7cdb90760c01ebddf9a45332c2a4ad7aa007e81e src/mcp-server/cli/eval/impl.ts: - last_write_checksum: sha1:03b798d772bef9f8410462cbf9febe0ddb38a1bc + last_write_checksum: sha1:920db7e0c362d612bd13af1e9c8b8524db7fb14b src/mcp-server/cli/serve/command.ts: last_write_checksum: sha1:6700155fe9c6850f35d9ff03834ab3be59d4d273 src/mcp-server/cli/serve/impl.ts: - last_write_checksum: sha1:26170c74302e8cce33722205881ba042714cb25c + last_write_checksum: sha1:9b74d072bf6f8022e92d5e678994da5ff7652405 src/mcp-server/cli/start/command.ts: last_write_checksum: sha1:0b1d5a70b4fbce726352d22a584ed4a531cb0a52 src/mcp-server/cli/start/impl.ts: - last_write_checksum: sha1:0d20774a12c409d4772473d2dbad3f8c2bfcd53b + last_write_checksum: sha1:4dc914eb22abec4dc5da655e7547d9e2b7a48063 src/mcp-server/console-logger.ts: last_write_checksum: sha1:0c4b9b7c79ad0a81359210cb31b9ee4d887d5de1 src/mcp-server/extensions.ts: - last_write_checksum: sha1:27de1e2b8aaedc8965d1d4c4d4c500df1f28b483 + last_write_checksum: sha1:2198b49e3178b217db6d044a2884d71eaf92521f src/mcp-server/flags.ts: last_write_checksum: sha1:1fa1d39e1e57206a5b21ba9870d88d7a0484ba07 src/mcp-server/gram.ts: - last_write_checksum: sha1:2186a9602c6c628f791f26aafbeba1e457980870 + last_write_checksum: sha1:31ed2676816ede1f8c2bc2a34916316b5a709d1b src/mcp-server/mcp-server.ts: last_write_checksum: sha1:e7f95d828d67b60820bf240e141577d19c406900 src/mcp-server/prompts.ts: - last_write_checksum: sha1:d5928503611775dde2dcc5272e35f8065b35b920 + last_write_checksum: sha1:1d4a68d460607f30b1ff0ecca3227da67fdf2048 src/mcp-server/resources.ts: - last_write_checksum: sha1:13dc124e896d588f80a3711f4e32fc170416552e + last_write_checksum: sha1:6b77aec8f30a10add2fef0fad4786599ddfbd2a5 src/mcp-server/scopes.ts: last_write_checksum: sha1:77d0eb95785e84f9fd0b27c4612f8c6470489fe4 src/mcp-server/server.ts: - last_write_checksum: sha1:5ad3a65ce75dd80625e93c814bc09e00e91b1234 + last_write_checksum: sha1:0534f866ec7aca03b33990729d84b65997c47a92 src/mcp-server/shared.ts: - last_write_checksum: sha1:19c9034032819a14f15c430de4350c8aba99d725 + last_write_checksum: sha1:1832c294677c567750b516748fca219a05da582f src/mcp-server/tools.ts: - last_write_checksum: sha1:a29c85128ae9ee0aa1e04a03799df9c23734cf2a + last_write_checksum: sha1:38e6a68a329c1179dfce2178c24e2dd81ec0e0a8 src/mcp-server/tools/binaryAndStringUpload.ts: - last_write_checksum: sha1:d33738c81094e8c30b6bc79f53e08a22da4274f9 + last_write_checksum: sha1:fbf82c10210eec4825988311ef4326b79e33f93b src/mcp-server/tools/createUser.ts: - last_write_checksum: sha1:cb1dca4414eed423da807a1242ab4bef9bd1f86c + last_write_checksum: sha1:85feb27c38b9b6bf910544b035946f5f4484653d src/mcp-server/tools/createWithUnion.ts: - last_write_checksum: sha1:2cd2ed8a825887a57287031f38d7baf42d56e999 + last_write_checksum: sha1:aa8b82c22f17c2d1330bc4ab614ac1a1997e306b src/mcp-server/tools/deleteUser.ts: - last_write_checksum: sha1:29c8129452373ae48235e5d563eb46a8aa6081f7 + last_write_checksum: sha1:5374136ffc16dfd60048544eada5f68d42299e80 src/mcp-server/tools/getBinaryDefaultResponse.ts: - last_write_checksum: sha1:c17cf969d45ceb35168df991e265144e1ea627d2 + last_write_checksum: sha1:0a6a9da29179bff6ae00b27c3f6cc0cb720f3dfd src/mcp-server/tools/getDuplicateExportCollision.ts: - last_write_checksum: sha1:4ceb6bb222e0d003b28690d7d27ddf3be27d175d + last_write_checksum: sha1:105243b016cbb335343b79d49935cc20662f9f06 src/mcp-server/tools/getEmptyObjectError.ts: - last_write_checksum: sha1:fe668187a47ec0838b7eeb6f5fd95ec03f05c9e8 + last_write_checksum: sha1:9358635623baaf5285347d577f0803659d3e18ed src/mcp-server/tools/getErrorInUnion.ts: - last_write_checksum: sha1:bd4a80036ed9c638f021541637c73d2c0964179c + last_write_checksum: sha1:4d201d0c238622ff88b50105645573ef06e65057 src/mcp-server/tools/getErrorOnlyExample.ts: - last_write_checksum: sha1:8c4d4e7b9b77a20abc7a2a5fdb2ecfab93f6e925 + last_write_checksum: sha1:915d2071c5054dda36da48469a5faac18fb98f0c src/mcp-server/tools/getFullyFlattenedRequest.ts: - last_write_checksum: sha1:9f6a46cec360a345d5dcc11a2d04a1ae2d8ae718 + last_write_checksum: sha1:5b62d927ba72c208d0afb0fdfeb779da7c538d3a src/mcp-server/tools/getNamedPrimitiveUnion.ts: - last_write_checksum: sha1:c4037e16c3e520efff3be4a40bdd25fb74b32459 + last_write_checksum: sha1:76c1ba80f847b93587ad9a86125fd310444efa5f src/mcp-server/tools/getNestedIntegerString.ts: - last_write_checksum: sha1:550dca2ae377a827310a94259dd9e5bda5d066db + last_write_checksum: sha1:4ee1320f06a7e9768cb91d9adc8c48b4bfc30627 src/mcp-server/tools/getPolymorphism.ts: - last_write_checksum: sha1:ae2c6a58e0411fe4fe2013194755cee0d1960c7e + last_write_checksum: sha1:ca93f1b65c37fa68d40842e91dbdbf01709b8056 src/mcp-server/tools/getPositionalDefault.ts: - last_write_checksum: sha1:6c863eb9ade026faf530c94b20636e2c6203c16f + last_write_checksum: sha1:79663530428fe7bb485f81ca2832be802363b70d src/mcp-server/tools/getRequestBodyFlattenedAway.ts: - last_write_checksum: sha1:1286331c7c92594da9abfb11b2e37341eee4e505 + last_write_checksum: sha1:ae96e983dcd28b57d00a5fb4a181e769b888ab3e src/mcp-server/tools/getUnionErrors.ts: - last_write_checksum: sha1:dc159520c1c7d338a7468ea530738a713373207f + last_write_checksum: sha1:cc6561ccaab13610733eda33f3b6609636df1518 src/mcp-server/tools/getUser.ts: - last_write_checksum: sha1:20681fc2bd9ee5a8f681a43d18ef3c6efc4daf24 + last_write_checksum: sha1:f18e2e6937d69e880438d573b6a2a7e9f2cf8431 src/mcp-server/tools/groupRootGroupOp.ts: - last_write_checksum: sha1:4add899726975deb6473cdbc8efe9e8c52875b41 + last_write_checksum: sha1:470ab2ddf7bb9186d8580d360a931721791dcaa1 src/mcp-server/tools/groupSubGroupEmptyTailNestedGroupOp.ts: - last_write_checksum: sha1:b1e7c5ba454c0328d85e7dcee301269eb0507cad + last_write_checksum: sha1:207560eb33b868982dcfef45a690fc15167181ed src/mcp-server/tools/groupSubGroupSubGroupOp.ts: - last_write_checksum: sha1:5cd6bc8830d7f37946d4ae50ddeae853f33b7908 + last_write_checksum: sha1:a1c69308dfc4fd48087686145f54f0b179674a54 src/mcp-server/tools/login.ts: - last_write_checksum: sha1:94f679839719f31c4f1f6efab1367a0060de839e + last_write_checksum: sha1:f8d5cd31c7eff27af43c4754d0d9f48515ef5132 src/mcp-server/tools/namespaceTestsConflictsCreateNamespaceConflict.ts: - last_write_checksum: sha1:616b537a6f264ef490bd00e6a918ce2b756298f1 + last_write_checksum: sha1:bbcd945e53bf74fd6491e8141a9be1be5be044de src/mcp-server/tools/namespaceTestsConflictsGetNamespaceConflict.ts: - last_write_checksum: sha1:f3a0bbebc6e495f4a54e30eae7aba38647c2ffc3 + last_write_checksum: sha1:c5c312d66f9236ba5dc724f46789448c02840bd5 src/mcp-server/tools/namespaceTestsConflictsGetPetOwners.ts: - last_write_checksum: sha1:0dd8589d988a7ab7cb8064ac99d08f18161f9bd1 + last_write_checksum: sha1:b70696e5846ed96a4a5c94781ddbbef84430e51a src/mcp-server/tools/namespaceTestsConflictsGetTripleNamespaceConflict.ts: - last_write_checksum: sha1:5dbe28e871b893a73681b49f1a130f09765f141c + last_write_checksum: sha1:4f46d0e16ec321e497b02f0ab9dcfb5e54d90ee2 src/mcp-server/tools/namespaceTestsConflictsPutNamespaceConflict.ts: - last_write_checksum: sha1:9c3c689f6a62ca998334eb3adb0ecd591afcd7a7 + last_write_checksum: sha1:d2bf49e758f20d9af9bd8b31f2651cecf3ab856f src/mcp-server/tools/namespaceTestsSingleBarGetSingleNamespaceBarPet.ts: - last_write_checksum: sha1:1a717ea56e9b149190f6d3fc07525288cdb0b2e0 + last_write_checksum: sha1:60fc48597e25314715ff7dac2852efef2040630c src/mcp-server/tools/namespaceTestsSingleFooCreateSingleNamespaceFooPet.ts: - last_write_checksum: sha1:7593d41717c8d8fe5781d2c12b32c0c98cd52476 + last_write_checksum: sha1:9b95a098dc0c9d9f5b4ece0f78da8abb10d0f222 src/mcp-server/tools/namespaceTestsSingleFooGetSingleNamespaceFooPet.ts: - last_write_checksum: sha1:2ccd9ab27117f6f5479b91d36b08930c79476a30 + last_write_checksum: sha1:61868b75f4b9409a09e9ac79a6f291b40c0610ed src/mcp-server/tools/namespaceTestsTypesGetNamespaceAnimal.ts: - last_write_checksum: sha1:7941c7f400047fa313a28c2f97874e463716465e + last_write_checksum: sha1:9308ce20805fdb8ce958319c1a4cc74f8afb3860 src/mcp-server/tools/namespaceTestsTypesGetNamespaceOrganization.ts: - last_write_checksum: sha1:fbdb90d9968d99976f2ce3400a8ae466e3ef61d0 + last_write_checksum: sha1:47aca656dbf67d50a75c37e5f79fd43a64720d81 src/mcp-server/tools/namespaceTestsTypesGetNamespaceTypes.ts: - last_write_checksum: sha1:dce3adf869b4333afa7da1136303b620fb6cbc35 + last_write_checksum: sha1:db4c5be151f71819d5c6faf66a2a0356a653a714 src/mcp-server/tools/namespaceTestsTypesGetNamespaceVehicle.ts: - last_write_checksum: sha1:045c22d2b67d01e19fad3ba2bfaf4bb611a56e22 + last_write_checksum: sha1:71b6a1f463c30b324d11b59e28588df3270c3ad5 src/mcp-server/tools/operationWithLeadingAndTrailingUnderscores.ts: - last_write_checksum: sha1:92ab371a553590d7a19fdbc90eeae1101132c058 + last_write_checksum: sha1:16cd0325a29b330a36e33c9816699542f4a5abda src/mcp-server/tools/parenthesesInPathAllowed.ts: - last_write_checksum: sha1:f2a8b08bfc5e51f0c21ae8d1f163ad15e779aa5b + last_write_checksum: sha1:6a43f0c9bf073e2b9521be36694a398000a995c2 src/mcp-server/tools/postFile.ts: - last_write_checksum: sha1:3a1abc3355adae8c302df3b82bf92d171e35cb49 + last_write_checksum: sha1:763f7c461be2b98f6f85e5e231c72b15895cfac9 src/mcp-server/tools/tag1Deprecated1.ts: - last_write_checksum: sha1:486ae72628dd0e9ee787117ffd5c265e25688fbb + last_write_checksum: sha1:33713cde8d799d7f50c6260eff03658fcfaa625b src/mcp-server/tools/tag1ListTest1.ts: - last_write_checksum: sha1:e327c139953f93229383da7b9bee14da01159fbe + last_write_checksum: sha1:86edf15c47b6c518736ffc497b0ecbde89c7c099 src/mcp-server/tools/tag1PostFileWithEncoding.ts: - last_write_checksum: sha1:b1ac58efcf95b286f0feab137a6f73e2bad87f77 + last_write_checksum: sha1:f392b273ec320d7884f4598272ce3c4c0e93c815 src/mcp-server/tools/testEndpoint.ts: - last_write_checksum: sha1:95e959dabaa9c5227b40ebafa123bba41bfd24b5 + last_write_checksum: sha1:072cfa653366f9d0743c8a990198847113eb2adb src/mcp-server/tools/testEnumFormats.ts: - last_write_checksum: sha1:f1784b88c0df69828e1c87c2ef51ef338eea550c + last_write_checksum: sha1:9bc8721c02f553ce33a643b0e06f1a453c2154b8 src/mcp-server/tools/testGroupTag2PostTest.ts: - last_write_checksum: sha1:b2e58a78f8edfb4ec8741b96a1b8b68694bb38a7 + last_write_checksum: sha1:6b701bda178e4a93e0dbb5f4d43407d84dcad937 src/mcp-server/tools/updateUser.ts: - last_write_checksum: sha1:042c667c8df47b6b88c5f422ab52379d3f1ae414 + last_write_checksum: sha1:2b762d4d2dcbc4c935fb41a481442d78c8b42592 src/mcp-server/tools/urlValidationStressTest.ts: - last_write_checksum: sha1:dc7746ef18469648070b3843457c5838d529c05a + last_write_checksum: sha1:652a7dd06339f2bf4bc7807739a3882b7dd34103 src/mcp-server/tools/validate.ts: - last_write_checksum: sha1:81c79e86eb08cd1e9541e4b0ab8a665ed22fda65 + last_write_checksum: sha1:83c5fae09be700880bf6fd98ef171fbcdf05b88b src/models/aloof.ts: last_write_checksum: sha1:1fe23bbccb1cdd6590012f582148848c387fd3d8 src/models/approvalconfig.ts: @@ -952,11 +952,11 @@ trackedFiles: tsconfig.json: last_write_checksum: sha1:8a87ada8f75aac886f4923082db03a051e54489d worker-configuration.d.ts: - last_write_checksum: sha1:b81ef22bbf147612f086758e5ee28665c4d14c9c + last_write_checksum: sha1:f8d48c4a57bd2c2d46a4e7afa7a1b6bd90c7fba4 worker-configuration.ts: last_write_checksum: sha1:94268ed98a74734682ee4a0466bb2fa6877a17a2 wrangler.toml: - last_write_checksum: sha1:0ff13e4a580ef5de74e0de52aae1bbc3972538ea + last_write_checksum: sha1:51a13caee14c5630de3e1fccba2d3f1ee556f887 examples: _operation_with_leading_and_trailing_underscores_: speakeasy-default-operation-with-leading-and-trailing-underscores: diff --git a/zSDKs/mcp-typescript/README.md b/zSDKs/mcp-typescript/README.md index 41de8a17c..bf903005d 100644 --- a/zSDKs/mcp-typescript/README.md +++ b/zSDKs/mcp-typescript/README.md @@ -36,6 +36,7 @@ For more information about the API: [Speakeasy Docs](https://speakeasy.com/docs) ## Installation +The MCP server requires Node.js 22.18.0 or later because Gram is enabled. > [!TIP] > To finish publishing your MCP Server to npm and others you must [run your first generation action](https://www.speakeasy.com/docs/github-setup#step-by-step-guide). @@ -57,7 +58,7 @@ The MCP bundle package includes the MCP server and all necessary configuration.
Cursor -[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=SDK&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZW1vdGVAMC4xLjI1IiwiaHR0cHM6Ly9vcGVuYXBpLW1jcC1zZXJ2ZXIuZXhhbXBsZS53b3JrZXJzLmRldi9zc2UiLCItLWhlYWRlciIsInNlcnZlci1pbmRleDoke1NFUlZFUl9JTkRFWH0iLCItLWhlYWRlciIsInN1YmRvbWFpbjoke1NVQkRPTUFJTn0iLCItLWhlYWRlciIsImFwaS12ZXJzaW9uOiR7QVBJX1ZFUlNJT059IiwiLS1oZWFkZXIiLCJhcGktaG9zdC1uYW1lOiR7QVBJX0hPU1RfTkFNRX0iLCItLWhlYWRlciIsImFwaS1wb3J0OiR7QVBJX1BPUlR9IiwiLS1oZWFkZXIiLCJ1c2VybmFtZToke1VTRVJOQU1FfSIsIi0taGVhZGVyIiwicGFzc3dvcmQ6JHtQQVNTV09SRH0iLCItLWhlYWRlciIsImJlYXJlci1hdXRoOiR7QkVBUkVSX0FVVEh9IiwiLS1oZWFkZXIiLCJteS1hcGkta2V5OiR7TVlfQVBJX0tFWX0iLCItLWhlYWRlciIsIm9hdXRoMjoke09BVVRIMn0iLCItLWhlYWRlciIsImFwcC1pZDoke0FQUF9JRH0iLCItLWhlYWRlciIsInNlY3JldDoke1NFQ1JFVH0iLCItLWhlYWRlciIsIm1vYmlsZS1hdXRoOiR7TU9CSUxFX0FVVEh9IiwiLS1oZWFkZXIiLCJjbGllbnQtY3JlZGVudGlhbHM6JHtDTElFTlRfQ1JFREVOVElBTFN9IiwiLS1oZWFkZXIiLCJxdWVyeS1wYXJhbTE6JHtRVUVSWV9QQVJBTTF9IiwiLS1oZWFkZXIiLCJkZXByZWNhdGVkLXF1ZXJ5LXBhcmFtMToke0RFUFJFQ0FURURfUVVFUllfUEFSQU0xfSIsIi0taGVhZGVyIiwiZGVwcmVjYXRlZC1xdWVyeS1wYXJhbTI6JHtERVBSRUNBVEVEX1FVRVJZX1BBUkFNMn0iXX0=) +[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=SDK&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZW1vdGVAMC4xLjI1IiwiaHR0cHM6Ly9vcGVuYXBpLW1jcC1zZXJ2ZXIuZXhhbXBsZS53b3JrZXJzLmRldi9tY3AiLCItLWhlYWRlciIsIngtdXNlcm5hbWU6JHtVU0VSTkFNRX0iLCItLWhlYWRlciIsIngtcGFzc3dvcmQ6JHtQQVNTV09SRH0iLCItLWhlYWRlciIsImF1dGhvcml6YXRpb246JHtCRUFSRVJfQVVUSH0iLCItLWhlYWRlciIsImFwaV9rZXk6JHtNWV9BUElfS0VZfSIsIi0taGVhZGVyIiwieC1vYXV0aDI6JHtPQVVUSDJ9IiwiLS1oZWFkZXIiLCJ4LWFwcC1pZDoke0FQUF9JRH0iLCItLWhlYWRlciIsIngtc2VjcmV0OiR7U0VDUkVUfSIsIi0taGVhZGVyIiwieC1tb2JpbGUtYXV0aDoke01PQklMRV9BVVRIfSIsIi0taGVhZGVyIiwieC1jbGllbnQtY3JlZGVudGlhbHM6JHtDTElFTlRfQ1JFREVOVElBTFN9Il19) Or manually: @@ -186,7 +187,7 @@ Refer to [Official Windsurf documentation](https://docs.windsurf.com/windsurf/ca
VS Code -[![Install in VS Code](https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20SDK%20MCP&color=0098FF)](vscode://ms-vscode.vscode-mcp/install?name=SDK&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZW1vdGVAMC4xLjI1IiwiaHR0cHM6Ly9vcGVuYXBpLW1jcC1zZXJ2ZXIuZXhhbXBsZS53b3JrZXJzLmRldi9zc2UiLCItLWhlYWRlciIsInNlcnZlci1pbmRleDoke1NFUlZFUl9JTkRFWH0iLCItLWhlYWRlciIsInN1YmRvbWFpbjoke1NVQkRPTUFJTn0iLCItLWhlYWRlciIsImFwaS12ZXJzaW9uOiR7QVBJX1ZFUlNJT059IiwiLS1oZWFkZXIiLCJhcGktaG9zdC1uYW1lOiR7QVBJX0hPU1RfTkFNRX0iLCItLWhlYWRlciIsImFwaS1wb3J0OiR7QVBJX1BPUlR9IiwiLS1oZWFkZXIiLCJ1c2VybmFtZToke1VTRVJOQU1FfSIsIi0taGVhZGVyIiwicGFzc3dvcmQ6JHtQQVNTV09SRH0iLCItLWhlYWRlciIsImJlYXJlci1hdXRoOiR7QkVBUkVSX0FVVEh9IiwiLS1oZWFkZXIiLCJteS1hcGkta2V5OiR7TVlfQVBJX0tFWX0iLCItLWhlYWRlciIsIm9hdXRoMjoke09BVVRIMn0iLCItLWhlYWRlciIsImFwcC1pZDoke0FQUF9JRH0iLCItLWhlYWRlciIsInNlY3JldDoke1NFQ1JFVH0iLCItLWhlYWRlciIsIm1vYmlsZS1hdXRoOiR7TU9CSUxFX0FVVEh9IiwiLS1oZWFkZXIiLCJjbGllbnQtY3JlZGVudGlhbHM6JHtDTElFTlRfQ1JFREVOVElBTFN9IiwiLS1oZWFkZXIiLCJxdWVyeS1wYXJhbTE6JHtRVUVSWV9QQVJBTTF9IiwiLS1oZWFkZXIiLCJkZXByZWNhdGVkLXF1ZXJ5LXBhcmFtMToke0RFUFJFQ0FURURfUVVFUllfUEFSQU0xfSIsIi0taGVhZGVyIiwiZGVwcmVjYXRlZC1xdWVyeS1wYXJhbTI6JHtERVBSRUNBVEVEX1FVRVJZX1BBUkFNMn0iXX0=) +[![Install in VS Code](https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20SDK%20MCP&color=0098FF)](vscode://ms-vscode.vscode-mcp/install?name=SDK&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZW1vdGVAMC4xLjI1IiwiaHR0cHM6Ly9vcGVuYXBpLW1jcC1zZXJ2ZXIuZXhhbXBsZS53b3JrZXJzLmRldi9tY3AiLCItLWhlYWRlciIsIngtdXNlcm5hbWU6JHtVU0VSTkFNRX0iLCItLWhlYWRlciIsIngtcGFzc3dvcmQ6JHtQQVNTV09SRH0iLCItLWhlYWRlciIsImF1dGhvcml6YXRpb246JHtCRUFSRVJfQVVUSH0iLCItLWhlYWRlciIsImFwaV9rZXk6JHtNWV9BUElfS0VZfSIsIi0taGVhZGVyIiwieC1vYXV0aDI6JHtPQVVUSDJ9IiwiLS1oZWFkZXIiLCJ4LWFwcC1pZDoke0FQUF9JRH0iLCItLWhlYWRlciIsIngtc2VjcmV0OiR7U0VDUkVUfSIsIi0taGVhZGVyIiwieC1tb2JpbGUtYXV0aDoke01PQklMRV9BVVRIfSIsIi0taGVhZGVyIiwieC1jbGllbnQtY3JlZGVudGlhbHM6JHtDTElFTlRfQ1JFREVOVElBTFN9Il19) Or manually: @@ -372,6 +373,9 @@ Then install with Claude Code CLI: claude mcp add SDK -- npx -y openapi start --server-index 0 --subdomain api --api-version 1 --api-host-name localhost --api-port 8080 --username --password --bearer-auth --my-api-key --oauth2 --app-id --secret --mobile-auth --client-credentials --query-param1 --deprecated-query-param1 --deprecated-query-param2 ``` +The worker serves MCP over Streamable HTTP at `/mcp` and legacy HTTP+SSE at `/sse`. Existing clients can keep their endpoints. Legacy Streamable HTTP and SSE clients retain session state in Durable Objects; modern protocol requests are handled statelessly. + +Keep the Durable Object binding, exported class, and `v1` migration in `wrangler.toml`: legacy Streamable HTTP and SSE sessions require them. Only modern protocol requests are stateless. Removing the binding disables legacy sessions; deleting the namespace also permanently destroys its stored data. diff --git a/zSDKs/mcp-typescript/package-lock.json b/zSDKs/mcp-typescript/package-lock.json index f152e8194..4bab20aa3 100644 --- a/zSDKs/mcp-typescript/package-lock.json +++ b/zSDKs/mcp-typescript/package-lock.json @@ -11,13 +11,16 @@ "@ai-sdk/mcp": "^1.0.19", "@ai-sdk/openai": "^3.0.26", "@gram-ai/functions": "^0.12.1", - "@modelcontextprotocol/sdk": "1.26.0", + "@modelcontextprotocol/client": "2.3.1", + "@modelcontextprotocol/node": "2.1.1", + "@modelcontextprotocol/sdk": "^1.32.1", + "@modelcontextprotocol/server": "2.3.1", + "@modelcontextprotocol/server-legacy": "2.3.1", "@stricli/core": "^1.1.2", - "agents": "0.3.10", "ai": "^6.0.77", "decimal.js": "^10.5.0", "express": "^5.1.0", - "zod": "^4.0.0" + "zod": "^4.2.0" }, "bin": { "mcp": "bin/mcp-server.js", @@ -25,7 +28,7 @@ }, "devDependencies": { "@anthropic-ai/mcpb": "^1.2.0", - "@cloudflare/workers-types": "^4.20250712.0", + "@cloudflare/workers-types": "^4.20250712.0 || ^5.20261006.1", "@eslint/js": "^9.26.0", "@types/bun": "^1.2.13", "@types/express": "^5.0.1", @@ -37,6 +40,9 @@ "typescript-eslint": "^8.31.1", "vitest": "^3.2.6", "wrangler": "^4.59.1" + }, + "engines": { + "node": ">=22.18.0" } }, "node_modules/@ai-sdk/gateway": { @@ -149,50 +155,6 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.9.3", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", - "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", - "license": "MIT", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/philsturgeon" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.2.tgz", - "integrity": "sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==", - "license": "MIT", - "dependencies": { - "core-js-pure": "^3.48.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cfworker/json-schema": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", - "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", - "license": "MIT" - }, "node_modules/@clack/core": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz", @@ -214,34 +176,6 @@ "sisteransi": "^1.0.5" } }, - "node_modules/@cloudflare/ai-chat": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@cloudflare/ai-chat/-/ai-chat-0.0.6.tgz", - "integrity": "sha512-XDJP7ywORzQd17f09hMY1n3+bQsLw+BmXh7HzuWGb7gBBbD23OlQXccuQQqFNBYHi+IfIpe9YdGbHaUGsfCUwA==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "agents": "^0.3.10", - "ai": "^6.0.0", - "react": "^19.0.0", - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/@cloudflare/codemode": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@cloudflare/codemode/-/codemode-0.0.6.tgz", - "integrity": "sha512-P8ba7fgyeOOEODgU+lyUj82P89VfslKExsdF6nPGRebIbgiCbbpoLHBmBtdRRsIasVHUhceSazJxIS6dg70yRA==", - "license": "MIT", - "peer": true, - "dependencies": { - "zod-to-ts": "^2.0.0" - }, - "peerDependencies": { - "agents": "^0.3.7", - "ai": "^6.0.0", - "zod": "^3.25.0 || ^4.0.0" - } - }, "node_modules/@cloudflare/kv-asset-handler": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", @@ -357,6 +291,7 @@ "version": "4.20260413.1", "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260413.1.tgz", "integrity": "sha512-4FFHIVIk645Wf20eCVfe0eM3ERsEw98DFng76QZf1C1JMgIVlfSV2gZF1EyXxNVwOG0RM/CBlu07+u/Z/0Oq9Q==", + "dev": true, "license": "MIT OR Apache-2.0" }, "node_modules/@cspotcode/source-map-support": { @@ -1913,12 +1848,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, "node_modules/@logtape/logtape": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/@logtape/logtape/-/logtape-1.3.7.tgz", @@ -1940,13 +1869,59 @@ "@logtape/logtape": "^1.3.7" } }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.3.1.tgz", + "integrity": "sha512-mIGZXpHsjnZ6lD+gD/WCMpR5k8yVQQ8nNFH1N0Srf7AvnwTUMYD6pvTY8ng2+He5FfV7YMZLmrjL7cLa/cc3dQ==", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.3.1", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.8", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.3.1.tgz", + "integrity": "sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g==", + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.1.1.tgz", + "integrity": "sha512-EY92OAXN2xYL7tNS/MVsWJW1v8VHAPwl9B4Ca3pCrjmvqyC+S1C/6UHsMxF2GoFPraS5Yyg56vFz9brVpFojMA==", + "license": "Apache-2.0", + "dependencies": { + "@hono/node-server": "^1.19.9", + "hono": "^4.11.4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.3.0" + } + }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "version": "1.32.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.1.tgz", + "integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -1980,6 +1955,46 @@ } } }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.3.1.tgz", + "integrity": "sha512-e59MfWuSssj6DoQ96JCTqdW1AXT5/XtnTl0WWd6MU1Ka434fFUGGosCLJbGNySDhHGvxQWoRWK137FXYFkYNWw==", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.3.1", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/server-legacy": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server-legacy/-/server-legacy-2.3.1.tgz", + "integrity": "sha512-+cuB+hl7s8DiFXIt13le3cD2wZPOeDYwlycAkRMPljqqNbOLekGRiyslx8WXfqaXSCveS1eDD8/B8rmxz9BZFQ==", + "deprecated": "This package is a frozen copy of v1's SSE transport and OAuth Authorization Server helpers for migration purposes only. Use StreamableHTTP from @modelcontextprotocol/server and a dedicated OAuth server in production. Will not receive new features.", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.3.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "express-rate-limit": "^8.2.1", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "express": "^4.18.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + } + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -2683,12 +2698,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/mute-stream": { @@ -3170,53 +3180,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agents": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/agents/-/agents-0.3.10.tgz", - "integrity": "sha512-hKj3nbej14GA2SgE6/stQheJF35LCS7DxMuHG0QDl1/npnnECYyG0Yf5DXl6AvJAZOFNDwT3iEzKi8yLZngPDA==", - "license": "MIT", - "dependencies": { - "@cfworker/json-schema": "^4.1.1", - "@modelcontextprotocol/sdk": "1.25.2", - "cron-schedule": "^6.0.0", - "escape-html": "^1.0.3", - "json-schema": "^0.4.0", - "json-schema-to-typescript": "^15.0.4", - "mimetext": "^3.0.28", - "nanoid": "^5.1.6", - "partyserver": "^0.1.2", - "partysocket": "1.1.11", - "yargs": "^18.0.0" - }, - "bin": { - "agents": "dist/cli/index.js" - }, - "peerDependencies": { - "@ai-sdk/openai": "^3.0.0", - "@ai-sdk/react": "^3.0.0", - "@cloudflare/ai-chat": "^0.0.6", - "@cloudflare/codemode": "^0.0.6", - "ai": "^6.0.0", - "react": "^19.0.0", - "viem": ">=2.0.0", - "x402": "^0.7.1", - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "@ai-sdk/openai": { - "optional": true - }, - "@ai-sdk/react": { - "optional": true - }, - "viem": { - "optional": true - }, - "x402": { - "optional": true - } - } - }, "node_modules/ai": { "version": "6.0.158", "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.158.tgz", @@ -3236,9 +3199,9 @@ } }, "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -3348,6 +3311,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/assertion-error": { @@ -3749,99 +3713,6 @@ "node": ">= 12" } }, - "node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "license": "ISC", - "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3933,17 +3804,6 @@ "node": ">=6.6.0" } }, - "node_modules/core-js-pure": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", - "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -3992,15 +3852,6 @@ "node": ">= 14" } }, - "node_modules/cron-schedule": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cron-schedule/-/cron-schedule-6.0.0.tgz", - "integrity": "sha512-BoZaseYGXOo5j5HUwTaegIog3JJbuH4BbrY9A1ArLjXpy+RWb3mV28F/9Gv1dDA7E2L8kngWva4NWisnLTyfgQ==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4203,15 +4054,6 @@ "@esbuild/win32-x64": "0.27.7" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -4438,12 +4280,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-polyfill": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz", - "integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==", - "license": "MIT" - }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -4484,9 +4320,9 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -4618,9 +4454,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.2.tgz", - "integrity": "sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.2.1.tgz", + "integrity": "sha512-TmHQgewjHtMq1E5QKA0tOE0yeYGQs25KZC/ziJpubRtWI15W92e6vPFydWOeZBVROSHBYw/QhD9d5OefdD6LDg==", "funding": [ { "type": "github", @@ -4637,6 +4473,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -4831,27 +4668,6 @@ "node": ">= 12" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5142,6 +4958,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5160,6 +4977,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -5222,12 +5040,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/js-base64": { - "version": "3.7.8", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", - "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", - "license": "BSD-3-Clause" - }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -5239,6 +5051,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, "funding": [ { "type": "github", @@ -5270,29 +5083,6 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "license": "(AFL-2.1 OR BSD-3-Clause)" }, - "node_modules/json-schema-to-typescript": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", - "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", - "license": "MIT", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.5.5", - "@types/json-schema": "^7.0.15", - "@types/lodash": "^4.17.7", - "is-glob": "^4.0.3", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "minimist": "^1.2.8", - "prettier": "^3.2.5", - "tinyglobby": "^0.2.9" - }, - "bin": { - "json2ts": "dist/src/cli.js" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -5512,43 +5302,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/mimetext": { - "version": "3.0.28", - "resolved": "https://registry.npmjs.org/mimetext/-/mimetext-3.0.28.tgz", - "integrity": "sha512-eQXpbNrtxLCjUtiVbR/qR09dbPgZ2o+KR1uA7QKqGhbn8QV7HIL16mXXsobBL4/8TqoYh1us31kfz+dNfCev9g==", - "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.20260409.0", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260409.0.tgz", @@ -5583,15 +5336,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -5617,24 +5361,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/nanoid": { - "version": "5.1.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", - "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5800,27 +5526,6 @@ "node": ">= 0.8" } }, - "node_modules/partyserver": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/partyserver/-/partyserver-0.1.5.tgz", - "integrity": "sha512-kaE3GYaYWFc70EJQDQEhyYbO2Wczz/NgsFXerfjRo0t2s7ZxL1XggWT+HkMrdEyqbZOv3b66CV93WG0Lcg/ThQ==", - "license": "ISC", - "dependencies": { - "nanoid": "^5.1.6" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.20240729.0" - } - }, - "node_modules/partysocket": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.1.11.tgz", - "integrity": "sha512-P0EtOQiAwvLriqLgdThcSaREfz3bP77LkLSdmXq680BosPKvGSoGTh/d0g3S+UNmaqcw89Ad7JXHHKyRx3xU9Q==", - "license": "MIT", - "dependencies": { - "event-target-polyfill": "^0.0.4" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5893,6 +5598,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -5968,21 +5674,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz", - "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==", - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -6074,16 +5765,6 @@ "node": ">= 0.10" } }, - "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -6693,6 +6374,7 @@ "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -6839,6 +6521,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7908,91 +7591,6 @@ } } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", - "license": "MIT", - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -8090,17 +7688,6 @@ "zod": "^3.25.28 || ^4" } }, - "node_modules/zod-to-ts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/zod-to-ts/-/zod-to-ts-2.0.0.tgz", - "integrity": "sha512-aHsUgIl+CQutKAxtRNeZslLCLXoeuSq+j5HU7q3kvi/c2KIAo6q4YjT7/lwFfACxLB923ELHYMkHmlxiqFy4lw==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "typescript": "^5.0.0", - "zod": "^3.25.0 || ^4.0.0" - } - }, "node_modules/zx": { "version": "8.8.5-lite", "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.5-lite.tgz", diff --git a/zSDKs/mcp-typescript/package.json b/zSDKs/mcp-typescript/package.json index 35df4863e..01cfd0333 100644 --- a/zSDKs/mcp-typescript/package.json +++ b/zSDKs/mcp-typescript/package.json @@ -29,16 +29,18 @@ "@ai-sdk/mcp": "^1.0.19", "@ai-sdk/openai": "^3.0.26", "@gram-ai/functions": "^0.12.1", - "@modelcontextprotocol/sdk": "1.26.0", + "@modelcontextprotocol/client": "2.3.1", + "@modelcontextprotocol/node": "2.1.1", + "@modelcontextprotocol/sdk": "^1.32.1", + "@modelcontextprotocol/server": "2.3.1", + "@modelcontextprotocol/server-legacy": "2.3.1", "@stricli/core": "^1.1.2", - "agents": "0.3.10", "ai": "^6.0.77", "decimal.js": "^10.5.0", "express": "^5.1.0", - "zod": "^4.0.0" + "zod": "^4.2.0" }, "overrides": { - "@modelcontextprotocol/sdk": "1.26.0", "hono": ">=4.12.34", "fast-uri": ">=4.1.2 <5.0.0", "js-yaml": ">=4.3.1 <5.0.0", @@ -49,7 +51,7 @@ }, "devDependencies": { "@anthropic-ai/mcpb": "^1.2.0", - "@cloudflare/workers-types": "^4.20250712.0", + "@cloudflare/workers-types": "^4.20250712.0 || ^5.20261006.1", "@eslint/js": "^9.26.0", "@types/bun": "^1.2.13", "@types/express": "^5.0.1", @@ -61,5 +63,8 @@ "typescript-eslint": "^8.31.1", "vitest": "^3.2.6", "wrangler": "^4.59.1" + }, + "engines": { + "node": ">=22.18.0" } } diff --git a/zSDKs/mcp-typescript/src/cloudflare-worker/cloudflare-worker.ts b/zSDKs/mcp-typescript/src/cloudflare-worker/cloudflare-worker.ts index 2bcf184bd..3d4886dd1 100644 --- a/zSDKs/mcp-typescript/src/cloudflare-worker/cloudflare-worker.ts +++ b/zSDKs/mcp-typescript/src/cloudflare-worker/cloudflare-worker.ts @@ -2,95 +2,487 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { McpAgent } from "agents/mcp"; -import type { Env } from "../../worker-configuration.js"; +import { + createMcpHandler, + isInitializeRequest, + isLegacyRequest, + legacyStatelessFallback, + WebStandardStreamableHTTPServerTransport, +} from "@modelcontextprotocol/server"; +import { SSEServerTransport } from "@modelcontextprotocol/server-legacy/sse"; +import { AsyncLocalStorage } from "node:async_hooks"; import { SDKCore } from "../core.js"; import { landingPage } from "../landing-page.js"; import { createConsoleLogger } from "../mcp-server/console-logger.js"; import { createMCPServer } from "../mcp-server/server.js"; -interface State {} +function getSDK(headers: Headers | undefined) { + const getHeader = (name: string) => headers?.get(name) ?? ""; -type Props = Record; + return new SDKCore({ + debugLogger: { + log: (...args) => console.log(...args), + group: (...args) => console.group(...args), + groupEnd: (...args) => console.groupEnd(...args), + }, + security: async () => ({ + Option1: { + username: getHeader("x-username"), + password: getHeader("x-password"), + }, + Option2: { + bearerAuth: getHeader("authorization"), + MyApiKey: getHeader("api_key"), + }, + Option3: { + oauth2: getHeader("x-oauth2"), + }, + Option4: { + appId: getHeader("x-app-id"), + secret: getHeader("x-secret"), + }, + Option5: { + mobileAuth: getHeader("x-mobile-auth"), + }, + Option6: { + clientCredentials: getHeader("x-client-credentials"), + }, + Option7: { + MyApiKey: getHeader("api_key"), + }, + }), + }); +} + +function createServer(getHeaders: () => Headers | undefined) { + const { server } = createMCPServer({ + logger: createConsoleLogger("debug"), + getSDK: () => getSDK(getHeaders()), + }); + return server; +} + +const PROPS_HEADER = "x-mcp-worker-props"; + +type Credentials = { headers: Headers; authoritative: boolean }; + +function credentials(request: Request): Credentials { + const props = request.headers.get(PROPS_HEADER); + if (props == null) return { headers: request.headers, authoritative: false }; + try { + return { + headers: new Headers(JSON.parse(props) as Record), + authoritative: true, + }; + } catch { + return { headers: new Headers(), authoritative: true }; + } +} + +function credentialHeaders(request: Request | undefined): Headers | undefined { + return request ? credentials(request).headers : undefined; +} + +const modernHandler = createMcpHandler( + ({ requestInfo }) => createServer(() => credentialHeaders(requestInfo)), + { legacy: "reject" }, +); + +const statelessLegacyHandler = legacyStatelessFallback( + ({ requestInfo }) => createServer(() => credentialHeaders(requestInfo)), +); + +type CorsOptions = { + origin?: string; + methods?: string; + headers?: string; + exposeHeaders?: string; + maxAge?: number; +}; + +type ServeOptions = { + binding?: string; + corsOptions?: CorsOptions; + transport?: "streamable-http" | "sse"; + jurisdiction?: DurableObjectJurisdiction; +}; + +type WorkerProps = Record | undefined; -export class OpenapiMCP extends McpAgent { - server!: McpServer; +function corsHeaders(request: Request, cors: CorsOptions = {}): Headers { + return new Headers({ + "Access-Control-Allow-Origin": cors.origin ?? "*", + "Access-Control-Allow-Methods": cors.methods + ?? "GET, POST, DELETE, OPTIONS", + "Access-Control-Allow-Headers": cors.headers + ?? request.headers.get("Access-Control-Request-Headers") + ?? "Content-Type, Accept, mcp-session-id, mcp-protocol-version", + "Access-Control-Expose-Headers": cors.exposeHeaders + ?? "mcp-session-id, mcp-protocol-version", + "Access-Control-Max-Age": String(cors.maxAge ?? 86400), + }); +} + +function withCors( + response: Response, + request: Request, + cors?: CorsOptions, +): Response { + const headers = new Headers(response.headers); + corsHeaders(request, cors).forEach((value, name) => headers.set(name, value)); + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); +} + +function sessionNotFound(): Response { + return Response.json( + { + jsonrpc: "2.0", + error: { code: -32001, message: "Session not found" }, + id: null, + }, + { status: 404 }, + ); +} + +function sessionObject( + namespace: DurableObjectNamespace, + sessionId: string | null, +) { + if (!sessionId) return undefined; + try { + return namespace.get(namespace.idFromString(sessionId)); + } catch { + return undefined; + } +} + +function forwardRequest( + request: Request, + pathname: string, + props: WorkerProps, + search: Record = {}, +): Request { + const url = new URL(request.url); + url.pathname = pathname; + for (const [name, value] of Object.entries(search)) { + url.searchParams.set(name, value); + } + const forwarded = new Request(url.toString(), request as RequestInit); + forwarded.headers.delete(PROPS_HEADER); + if (props) { + const values = Object.fromEntries( + Object.entries(props).filter(([, value]) => typeof value === "string"), + ); + forwarded.headers.set(PROPS_HEADER, JSON.stringify(values)); + } + return forwarded; +} - async init() { - const { server } = createMCPServer({ - logger: createConsoleLogger("debug"), - getSDK: () => this.getSDK(), +function matchesPath(path: string, pathname: string): boolean { + return path === "/" || pathname === path; +} + +function messagePath(basePath: string): string { + return `${basePath.replace(/\/+$/, "")}/message`; +} + +async function startsSession(request: Request): Promise { + if (request.method !== "POST") return false; + const body: unknown = await request.clone().json().catch(() => undefined); + return Array.isArray(body) + ? body.some((message) => isInitializeRequest(message)) + : isInitializeRequest(body); +} + +async function legacyStreamableHttp( + request: Request, + namespace: DurableObjectNamespace, +): Promise { + const sessionId = request.headers.get("mcp-session-id"); + if (sessionId) { + return sessionObject(namespace, sessionId)?.fetch(request) + ?? sessionNotFound(); + } + if (await startsSession(request)) { + return namespace.get(namespace.newUniqueId()).fetch(request); + } + return statelessLegacyHandler(request); +} + +async function handleStreamableHttp( + request: Request, + namespace: DurableObjectNamespace, + props: WorkerProps, + cors?: CorsOptions, +): Promise { + if (request.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: corsHeaders(request, cors), }); + } + const forwarded = forwardRequest(request, "/mcp", props); + const response = (await isLegacyRequest(forwarded)) + ? await legacyStreamableHttp(forwarded, namespace) + : await modernHandler.fetch(forwarded); + return withCors(response, request, cors); +} - this.server = server; +async function handleSse( + request: Request, + namespace: DurableObjectNamespace, + basePath: string, + props: WorkerProps, + cors?: CorsOptions, +): Promise { + if (request.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: corsHeaders(request, cors), + }); + } + const url = new URL(request.url); + let response: Response; + if (url.pathname === messagePath(basePath)) { + const session = sessionObject(namespace, url.searchParams.get("session")); + response = session + ? await session.fetch(forwardRequest(request, "/sse/message", props)) + : new Response("Session not found", { status: 404 }); + } else if (matchesPath(basePath, url.pathname) && request.method === "GET") { + response = await namespace.get(namespace.newUniqueId()).fetch( + forwardRequest(request, "/sse", props, { + endpoint: messagePath(basePath), + }), + ); + } else { + response = new Response("Not Found", { status: 404 }); + } + return withCors(response, request, cors); +} + +function bindingNamespace( + env: Env, + options: ServeOptions, +): DurableObjectNamespace { + const bindings = env as unknown as Record; + const name = options.binding + ?? ("MCP_OBJECT" in bindings ? "MCP_OBJECT" : "OPENAPI_MCP"); + const namespace = bindings[name] as DurableObjectNamespace | undefined; + if (!namespace || typeof namespace.newUniqueId !== "function") { + throw new Error( + `Durable Object binding ${name} was not found. Check the wrangler configuration.`, + ); } + return options.jurisdiction + ? namespace.jurisdiction(options.jurisdiction) + : namespace; +} + +type SseResponse = ConstructorParameters[1]; + +function sseStream( + request: Request, +): { + response: SseResponse; + body: ReadableStream; + headers: () => HeadersInit; +} { + const { readable, writable } = new TransformStream(); + const writer = writable.getWriter(); + const encoder = new TextEncoder(); + const closeListeners: Array<() => void> = []; + let headers: HeadersInit = {}; + let closed = false; + const notifyClosed = () => { + if (closed) return; + closed = true; + for (const listener of closeListeners) listener(); + }; + writer.closed.then(notifyClosed, notifyClosed); + request.signal.addEventListener("abort", () => { + void writer.abort().catch(() => {}); + notifyClosed(); + }); + const response = { + writeHead(_status: number, head?: HeadersInit) { + headers = head ?? {}; + return response; + }, + write(chunk: string) { + writer.write(encoder.encode(chunk)).catch(notifyClosed); + return true; + }, + end() { + writer.close().catch(() => {}); + return response; + }, + on(event: string, listener: () => void) { + if (event === "close") closeListeners.push(listener); + return response; + }, + }; + return { + response: response as unknown as SseResponse, + body: readable, + headers: () => headers, + }; +} + +const requestCredentials = new AsyncLocalStorage(); + +export class OpenapiMCP { + readonly #sessionId: string; + #sessionCredentials: Credentials | undefined; + #streamableHttp: WebStandardStreamableHTTPServerTransport | undefined; + #sse: SSEServerTransport | undefined; - getSDK() { - const getHeader = (name: string) => this.props?.[name] ?? ""; + constructor(state: DurableObjectState) { + this.#sessionId = state.id.toString(); + } - const sdk = new SDKCore({ - debugLogger: { - log: (...args) => console.log(...args), - group: (...args) => console.group(...args), - groupEnd: (...args) => console.groupEnd(...args), + static serve(path: string, options: ServeOptions = {}) { + return { + fetch: async ( + request: Request, + env: Env, + ctx?: { props?: Record }, + ): Promise => { + const namespace = bindingNamespace(env, options); + if (options.transport === "sse") { + return handleSse( + request, + namespace, + path, + ctx?.props, + options.corsOptions, + ); + } + if (!matchesPath(path, new URL(request.url).pathname)) { + return new Response("Not Found", { status: 404 }); + } + return handleStreamableHttp( + request, + namespace, + ctx?.props, + options.corsOptions, + ); }, - security: async () => ({ - Option1: { - username: getHeader("x-username"), - password: getHeader("x-password"), - }, - Option2: { - bearerAuth: getHeader("authorization"), - MyApiKey: getHeader("api_key"), - }, - Option3: { - oauth2: getHeader("x-oauth2"), - }, - Option4: { - appId: getHeader("x-app-id"), - secret: getHeader("x-secret"), - }, - Option5: { - mobileAuth: getHeader("x-mobile-auth"), - }, - Option6: { - clientCredentials: getHeader("x-client-credentials"), - }, - Option7: { - MyApiKey: getHeader("api_key"), + }; + } + + static serveSSE(path: string, options: Omit = {}) { + return this.serve(path, { ...options, transport: "sse" }); + } + + static mount(path: string, options: Omit = {}) { + return this.serveSSE(path, options); + } + + async fetch(request: Request): Promise { + return requestCredentials.run( + credentials(request), + () => this.#route(request), + ); + } + + #requestHeaders(): Headers | undefined { + const current = requestCredentials.getStore(); + const session = this.#sessionCredentials; + if (!current) return session?.headers; + if (current.authoritative || !session || session.authoritative) { + return current.headers; + } + const merged = new Headers(session.headers); + current.headers.forEach((value, name) => merged.set(name, value)); + return merged; + } + + async #route(request: Request): Promise { + const { pathname } = new URL(request.url); + if (pathname === "/mcp") return this.#handleStreamableHttp(request); + if (pathname === "/sse" && request.method === "GET") { + return this.#openSse(request); + } + if (pathname === "/sse/message" && request.method === "POST") { + return this.#handleSseMessage(request); + } + return new Response("Not Found", { status: 404 }); + } + + async #handleStreamableHttp(request: Request): Promise { + if (!this.#streamableHttp) { + if (request.headers.has("mcp-session-id")) return sessionNotFound(); + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => this.#sessionId, + onsessionclosed: () => { + this.#streamableHttp = undefined; }, - }), + }); + this.#sessionCredentials = credentials(request); + await createServer(() => this.#requestHeaders()).connect(transport); + this.#streamableHttp = transport; + } + return this.#streamableHttp.handleRequest(request); + } + + async #openSse(request: Request): Promise { + if (this.#sse) return new Response("Session already open", { status: 409 }); + const stream = sseStream(request); + const requested = new URL(request.url).searchParams.get("endpoint"); + const endpoint = requested?.startsWith("/") && !requested.startsWith("//") + ? requested + : "/sse/message"; + const transport = new SSEServerTransport( + `${endpoint}?session=${encodeURIComponent(this.#sessionId)}`, + stream.response, + ); + this.#sessionCredentials = credentials(request); + const server = createServer(() => this.#requestHeaders()); + transport.onclose = () => { + this.#sse = undefined; + }; + await server.connect(transport); + this.#sse = transport; + return new Response(stream.body, { + status: 200, + headers: stream.headers(), }); - return sdk; + } + + async #handleSseMessage(request: Request): Promise { + const sessionId = new URL(request.url).searchParams.get("sessionId"); + if (!this.#sse || sessionId !== this.#sse.sessionId) { + return new Response("Session not found", { status: 404 }); + } + let message: unknown; + try { + message = await request.json(); + } catch { + return new Response("Invalid JSON", { status: 400 }); + } + try { + await this.#sse.handleMessage(message, { request }); + } catch { + return new Response("Invalid message", { status: 400 }); + } + return new Response("Accepted", { status: 202 }); } } export default { - async fetch(request: Request, env: Env, ctx: any) { + async fetch(request: Request, env: Env) { const url = new URL(request.url); - // Pass auth headers through to the MCP agent - const headers: Record = {}; - for (const [key, value] of request.headers.entries()) { - headers[key.toLowerCase()] = value; - } - ctx.props = headers; - - if (url.pathname === "/sse" || url.pathname.startsWith("/sse/")) { - return OpenapiMCP.serveSSE("/sse", { binding: "OPENAPI_MCP" }).fetch( - request, - env, - ctx, - ); + if (url.pathname === "/mcp") { + return handleStreamableHttp(request, env.OPENAPI_MCP, undefined); } - if (url.pathname === "/mcp") { - return OpenapiMCP.serve("/mcp", { binding: "OPENAPI_MCP" }).fetch( - request, - env, - ctx, - ); + if (url.pathname === "/sse" || url.pathname === "/sse/message") { + return handleSse(request, env.OPENAPI_MCP, "/sse", undefined); } // Landing page diff --git a/zSDKs/mcp-typescript/src/landing-page.ts b/zSDKs/mcp-typescript/src/landing-page.ts index 6aa004f4d..32a594322 100644 --- a/zSDKs/mcp-typescript/src/landing-page.ts +++ b/zSDKs/mcp-typescript/src/landing-page.ts @@ -11,13 +11,28 @@ import { toolNames } from "./tool-names.js"; // Cloudflare-specific wrapper export function landingPage(req: Request): Response { const origin = new URL(req.url).origin; - return new Response(landingPageHTML(origin), { + return new Response(landingPageHTML(origin, "http", "cloudflare"), { headers: { "Content-Type": "text/html" }, }); } // express wrapper export function landingPageExpress(req: ExpressRequest, res: ExpressResponse) { + sendLandingPage(req, res, "sse"); +} + +export function landingPageExpressHTTP( + req: ExpressRequest, + res: ExpressResponse, +) { + sendLandingPage(req, res, "http"); +} + +function sendLandingPage( + req: ExpressRequest, + res: ExpressResponse, + transport: "sse" | "http", +) { const proto = req.get("x-forwarded-proto")?.split(",")[0]?.trim() || req.protocol; const host = req.get("host"); @@ -25,75 +40,96 @@ export function landingPageExpress(req: ExpressRequest, res: ExpressResponse) { res.status(400).send("Missing Host header"); return; } - res.type("html").send(landingPageHTML(`${proto}://${host}`)); + res.type("html").send(landingPageHTML(`${proto}://${host}`, transport)); } -export function landingPageHTML(origin: string): string { +export function landingPageHTML( + origin: string, + transport: "sse" | "http" = "sse", + runtime: "node" | "cloudflare" = "node", +): string { const o = origin; + const endpoint = transport === "sse" ? "sse" : "mcp"; + const headers: Array<{ name: string; env: string }> = runtime === "cloudflare" + ? [ + { "name": "x-username", "env": "USERNAME" }, + { "name": "x-password", "env": "PASSWORD" }, + { "name": "authorization", "env": "BEARER_AUTH" }, + { "name": "api_key", "env": "MY_API_KEY" }, + { "name": "x-oauth2", "env": "OAUTH2" }, + { "name": "x-app-id", "env": "APP_ID" }, + { "name": "x-secret", "env": "SECRET" }, + { "name": "x-mobile-auth", "env": "MOBILE_AUTH" }, + { "name": "x-client-credentials", "env": "CLIENT_CREDENTIALS" }, + ] + : transport === "sse" + ? [ + { "name": "username", "env": "USERNAME" }, + { "name": "password", "env": "PASSWORD" }, + { "name": "bearer-auth", "env": "BEARER_AUTH" }, + { "name": "my-api-key", "env": "MY_API_KEY" }, + { "name": "oauth2", "env": "OAUTH2" }, + { "name": "app-id", "env": "APP_ID" }, + { "name": "secret", "env": "SECRET" }, + { "name": "mobile-auth", "env": "MOBILE_AUTH" }, + { "name": "client-credentials", "env": "CLIENT_CREDENTIALS" }, + ] + : [ + { "name": "username", "env": "USERNAME" }, + { "name": "password", "env": "PASSWORD" }, + { "name": "bearer-auth", "env": "BEARER_AUTH" }, + { "name": "my-api-key", "env": "MY_API_KEY" }, + { "name": "oauth2", "env": "OAUTH2" }, + { "name": "app-id", "env": "APP_ID" }, + { "name": "secret", "env": "SECRET" }, + { "name": "mobile-auth", "env": "MOBILE_AUTH" }, + { "name": "client-credentials", "env": "CLIENT_CREDENTIALS" }, + { "name": "query-param1", "env": "QUERY_PARAM1" }, + { "name": "deprecated-query-param1", "env": "DEPRECATED_QUERY_PARAM1" }, + { "name": "deprecated-query-param2", "env": "DEPRECATED_QUERY_PARAM2" }, + { "name": "lone-query-param", "env": "LONE_QUERY_PARAM" }, + { "name": "subdomain", "env": "SUBDOMAIN" }, + { "name": "api-version", "env": "API_VERSION" }, + { "name": "api-host-name", "env": "API_HOST_NAME" }, + { "name": "api-port", "env": "API_PORT" }, + ]; const mcpConfig = { - "command": "npx", - "args": [ + command: "npx", + args: [ "-y", "mcp-remote@0.1.25", - `${o}/sse`, - "--header", - "server-index:${SERVER_INDEX}", - "--header", - "subdomain:${SUBDOMAIN}", - "--header", - "api-version:${API_VERSION}", - "--header", - "api-host-name:${API_HOST_NAME}", - "--header", - "api-port:${API_PORT}", - "--header", - "username:${USERNAME}", - "--header", - "password:${PASSWORD}", - "--header", - "bearer-auth:${BEARER_AUTH}", - "--header", - "my-api-key:${MY_API_KEY}", - "--header", - "oauth2:${OAUTH2}", - "--header", - "app-id:${APP_ID}", - "--header", - "secret:${SECRET}", - "--header", - "mobile-auth:${MOBILE_AUTH}", - "--header", - "client-credentials:${CLIENT_CREDENTIALS}", - "--header", - "query-param1:${QUERY_PARAM1}", - "--header", - "deprecated-query-param1:${DEPRECATED_QUERY_PARAM1}", - "--header", - "deprecated-query-param2:${DEPRECATED_QUERY_PARAM2}", + `${o}/${endpoint}`, + ...headers.flatMap(({ name, env }) => ["--header", `${name}:\${${env}}`]), ], - "env": { - "SERVER_INDEX": "YOUR_VALUE_HERE", - "SUBDOMAIN": "YOUR_VALUE_HERE", - "API_VERSION": "YOUR_VALUE_HERE", - "API_HOST_NAME": "YOUR_VALUE_HERE", - "API_PORT": "YOUR_VALUE_HERE", - "USERNAME": "YOUR_VALUE_HERE", - "PASSWORD": "YOUR_VALUE_HERE", - "BEARER_AUTH": "YOUR_VALUE_HERE", - "MY_API_KEY": "YOUR_VALUE_HERE", - "OAUTH2": "YOUR_VALUE_HERE", - "APP_ID": "YOUR_VALUE_HERE", - "SECRET": "YOUR_VALUE_HERE", - "MOBILE_AUTH": "YOUR_VALUE_HERE", - "CLIENT_CREDENTIALS": "YOUR_VALUE_HERE", - "QUERY_PARAM1": "YOUR_VALUE_HERE", - "DEPRECATED_QUERY_PARAM1": "YOUR_VALUE_HERE", - "DEPRECATED_QUERY_PARAM2": "YOUR_VALUE_HERE", - }, + env: Object.fromEntries(headers.map(({ env }) => [env, "YOUR_VALUE_HERE"])), }; - const codexConfig = `[mcp_servers.SDK] -url = "${o}/sse" -http_headers = { "server-index" = "YOUR_SERVER_INDEX", "subdomain" = "YOUR_SUBDOMAIN", "api-version" = "YOUR_API_VERSION", "api-host-name" = "YOUR_API_HOST_NAME", "api-port" = "YOUR_API_PORT", "username" = "YOUR_USERNAME", "password" = "YOUR_PASSWORD", "bearer-auth" = "YOUR_BEARER_AUTH", "my-api-key" = "YOUR_MY_API_KEY", "oauth2" = "YOUR_OAUTH2", "app-id" = "YOUR_APP_ID", "secret" = "YOUR_SECRET", "mobile-auth" = "YOUR_MOBILE_AUTH", "client-credentials" = "YOUR_CLIENT_CREDENTIALS", "query-param1" = "YOUR_QUERY_PARAM1", "deprecated-query-param1" = "YOUR_DEPRECATED_QUERY_PARAM1", "deprecated-query-param2" = "YOUR_DEPRECATED_QUERY_PARAM2" }`; + const cliHeaders = headers.map(({ name }) => `--header "${name}: ..."`).join( + " ", + ); + const codexConfig = transport === "sse" + ? `[mcp_servers.SDK] +command = ${JSON.stringify(mcpConfig.command)} +args = ${JSON.stringify(mcpConfig.args)} +env = { ${ + Object.entries(mcpConfig.env).map(([name, value]) => + `${JSON.stringify(name)} = ${JSON.stringify(value)}` + ).join(", ") + } }` + : `[mcp_servers.SDK] +url = ${JSON.stringify(`${o}/${endpoint}`)} +http_headers = { ${ + headers.map(({ name, env }) => + `${JSON.stringify(name)} = ${JSON.stringify(`YOUR_${env}`)}` + ).join(", ") + } }`; + const encodedConfig = encodeURIComponent( + btoa( + Array.from( + new TextEncoder().encode(JSON.stringify(mcpConfig)), + byte => String.fromCharCode(byte), + ).join(""), + ), + ); return ` @@ -731,7 +767,7 @@ http_headers = { "server-index" = "YOUR_SERVER_INDEX", "subdomain" = "YOUR_SUBDO - +
@@ -907,7 +943,7 @@ http_headers = { "server-index" = "YOUR_SERVER_INDEX", "subdomain" = "YOUR_SUBDO - ${o}/mcp + ${o}/${endpoint}
@@ -964,7 +1000,7 @@ http_headers = { "server-index" = "YOUR_SERVER_INDEX", "subdomain" = "YOUR_SUBDO - claude mcp add --transport sse SDK https://openapi-mcp-server.example.workers.dev/sse + claude mcp add --transport ${transport} SDK ${o}/${endpoint} ${cliHeaders} @@ -1015,7 +1051,7 @@ http_headers = { "server-index" = "YOUR_SERVER_INDEX", "subdomain" = "YOUR_SUBDO - gemini mcp add --transport sse SDK https://openapi-mcp-server.example.workers.dev/sse + gemini mcp add --transport ${transport} SDK ${o}/${endpoint} ${cliHeaders} diff --git a/zSDKs/mcp-typescript/src/mcp-server/cli/eval/impl.ts b/zSDKs/mcp-typescript/src/mcp-server/cli/eval/impl.ts index 3650bc124..4c32270ef 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/cli/eval/impl.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/cli/eval/impl.ts @@ -4,7 +4,7 @@ import { createMCPClient } from "@ai-sdk/mcp"; import { openai } from "@ai-sdk/openai"; -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { InMemoryTransport } from "@modelcontextprotocol/server"; import { generateText, type LanguageModel, diff --git a/zSDKs/mcp-typescript/src/mcp-server/cli/serve/impl.ts b/zSDKs/mcp-typescript/src/mcp-server/cli/serve/impl.ts index 560a674af..1aff07525 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/cli/serve/impl.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/cli/serve/impl.ts @@ -2,8 +2,8 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { toNodeHandler } from "@modelcontextprotocol/node"; +import { createMcpHandler } from "@modelcontextprotocol/server"; import express from "express"; import { LocalContext } from "../../cli.js"; import { @@ -15,7 +15,7 @@ import { createMCPServer } from "../../server.js"; import { buildAnnotationFilter } from "../../tools.js"; import { buildSDK } from "../../tools.js"; -import { landingPageExpress } from "../../../landing-page.js"; +import { landingPageExpressHTTP } from "../../../landing-page.js"; interface ServeCommandFlags extends MCPServerFlags { readonly port: number; @@ -39,7 +39,7 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { // Enable CORS for cross-origin requests app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); - res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); res.header("Access-Control-Allow-Headers", "*"); if (req.method === "OPTIONS") { res.sendStatus(204); @@ -50,17 +50,8 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { app.use(express.json()); - app.post("/mcp", async (req, res) => { - const headers = new Headers(); - for (const [key, value] of Object.entries(req.headers)) { - if (Array.isArray(value)) { - for (const v of value) headers.append(key, v); - } else if (value !== undefined) { - headers.set(key, value); - } - } - - const transport = new StreamableHTTPServerTransport({}); + const handler = createMcpHandler(({ requestInfo }) => { + const headers = requestInfo?.headers ?? new Headers(); const { server: mcpServer } = createMCPServer({ logger, @@ -88,11 +79,41 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { }); }; - await mcpServer.connect(transport as Transport); - await transport.handleRequest(req, res, req.body); + return mcpServer; + }); + const nodeHandler = toNodeHandler(handler); + const activeResponses = new Set>(); + let closing = false; + app.use((_req, res, next) => { + if (closing) { + res.set("Connection", "close").status(503).send( + "Server is shutting down", + ); + return; + } + next(); + }); + app.all("/mcp", (req, res) => { + // Notification streams remain open until handler.close(). + if (req.method !== "GET" && req.body?.method !== "subscriptions/listen") { + let finish!: () => void; + const completed = new Promise((resolve) => { + finish = resolve; + }); + const done = () => { + res.off("finish", done); + res.off("close", done); + activeResponses.delete(completed); + finish(); + }; + activeResponses.add(completed); + res.once("finish", done); + res.once("close", done); + } + return nodeHandler(req, res, req.body); }); - app.get("/", landingPageExpress); + app.get("/", landingPageExpressHTTP); const httpServer = app.listen(cliFlags.port, "0.0.0.0", () => { const ha = httpServer.address(); @@ -100,7 +121,11 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { logger.info("MCP Streamable HTTP server started", { host }); }); - const shutdown = () => { + const shutdown = async () => { + if (closing) { + process.exit(1); + } + closing = true; logger.info("Shutting down HTTP server"); const timer = setTimeout(() => { @@ -108,11 +133,16 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { process.exit(1); }, 5000); - httpServer.close(() => { - clearTimeout(timer); - logger.info("Graceful shutdown complete"); - process.exit(0); - }); + const stopped = new Promise((resolve) => + httpServer.close(() => resolve()) + ); + await Promise.all(activeResponses); + await handler.close(); + httpServer.closeAllConnections(); + await stopped; + clearTimeout(timer); + logger.info("Graceful shutdown complete"); + process.exit(0); }; process.on("SIGTERM", shutdown); diff --git a/zSDKs/mcp-typescript/src/mcp-server/cli/start/impl.ts b/zSDKs/mcp-typescript/src/mcp-server/cli/start/impl.ts index c2e143ae7..b8448632d 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/cli/start/impl.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/cli/start/impl.ts @@ -2,8 +2,8 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { SSEServerTransport } from "@modelcontextprotocol/server-legacy/sse"; +import { serveStdio } from "@modelcontextprotocol/server/stdio"; import express from "express"; import { landingPageExpress } from "../../../landing-page.js"; import { allRequired } from "../../../lib/primitives.js"; @@ -48,8 +48,7 @@ export async function main(this: LocalContext, flags: StartCommandFlags) { async function startStdio(flags: StartCommandFlags) { const logger = createConsoleLogger(flags["log-level"]); - const transport = new StdioServerTransport(); - const { server } = createMCPServer({ + const createServer = createMCPServerFactory({ logger, allowedTools: flags.tool, dynamic: flags.mode === "dynamic", @@ -80,16 +79,22 @@ async function startStdio(flags: StartCommandFlags) { HostName: flags["api-host-name"], PORT: flags["api-port"], }); - await server.connect(transport); + const handle = serveStdio(createServer); const abort = async () => { - await server.close(); + await handle.close(); process.exit(0); }; process.on("SIGTERM", abort); process.on("SIGINT", abort); } +function createMCPServerFactory( + options: Parameters[0], +) { + return () => createMCPServer(options).server; +} + async function startSSE(cliFlags: StartCommandFlags) { const logger = createConsoleLogger(cliFlags["log-level"]); const app = express(); @@ -119,17 +124,23 @@ async function startSSE(cliFlags: StartCommandFlags) { // Security fields can be overridden via headers "username": (req.headers["username"] as string) ?? cliFlags["username"], "password": (req.headers["password"] as string) ?? cliFlags["password"], - "bearer-auth": (req.headers["bearerauth"] as string) - ?? cliFlags["bearer-auth"], - "my-api-key": (req.headers["myapikey"] as string) - ?? cliFlags["my-api-key"], + "bearer-auth": + ((req.headers["bearer-auth"] ?? req.headers["bearerauth"]) as string) + ?? cliFlags["bearer-auth"], + "my-api-key": + ((req.headers["my-api-key"] ?? req.headers["myapikey"]) as string) + ?? cliFlags["my-api-key"], "oauth2": (req.headers["oauth2"] as string) ?? cliFlags["oauth2"], - "app-id": (req.headers["appid"] as string) ?? cliFlags["app-id"], + "app-id": ((req.headers["app-id"] ?? req.headers["appid"]) as string) + ?? cliFlags["app-id"], "secret": (req.headers["secret"] as string) ?? cliFlags["secret"], - "mobile-auth": (req.headers["mobileauth"] as string) - ?? cliFlags["mobile-auth"], - "client-credentials": (req.headers["clientcredentials"] as string) - ?? cliFlags["client-credentials"], + "mobile-auth": + ((req.headers["mobile-auth"] ?? req.headers["mobileauth"]) as string) + ?? cliFlags["mobile-auth"], + "client-credentials": + ((req.headers["client-credentials"] + ?? req.headers["clientcredentials"]) as string) + ?? cliFlags["client-credentials"], }; // Create a new MCP server for this connection with its auth diff --git a/zSDKs/mcp-typescript/src/mcp-server/extensions.ts b/zSDKs/mcp-typescript/src/mcp-server/extensions.ts index 78394e413..61db0f1b8 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/extensions.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/extensions.ts @@ -2,18 +2,16 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { ZodRawShapeCompat } from "@modelcontextprotocol/sdk/server/zod-compat.js"; +import * as z from "zod"; import { PromptDefinition } from "./prompts.js"; import { ResourceDefinition, ResourceTemplateDefinition } from "./resources.js"; import { ToolDefinition } from "./tools.js"; export type Register = { - tool:
( - def: ToolDefinition, - ) => void; + tool: (def: ToolDefinition) => void; resource: (def: ResourceDefinition) => void; resourceTemplate: (def: ResourceTemplateDefinition) => void; - prompt: ( + prompt: ( prompt: PromptDefinition, ) => void; }; diff --git a/zSDKs/mcp-typescript/src/mcp-server/gram.ts b/zSDKs/mcp-typescript/src/mcp-server/gram.ts index c8045c4c7..9e1464e52 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/gram.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/gram.ts @@ -4,7 +4,14 @@ // DO NOT EDIT: This file is auto-generated by Speakeasy. // Gram deployment wrapper for MCP server. -import { withGram } from "@gram-ai/functions/mcp"; +import type { Manifest, ManifestVariables } from "@gram-ai/functions"; +import type { WrappedMCPServer } from "@gram-ai/functions/mcp"; +import { + Client, + ProtocolError, + ProtocolErrorCode, +} from "@modelcontextprotocol/client"; +import { InMemoryTransport, McpServer } from "@modelcontextprotocol/server"; import { ServerPORT$zodSchema } from "../lib/config.js"; import { allRequired } from "../lib/primitives.js"; import { createConsoleLogger } from "./console-logger.js"; @@ -12,6 +19,112 @@ import { MCPServerFlags } from "./flags.js"; import { createMCPServer } from "./server.js"; import { buildAnnotationFilter } from "./tools.js"; +export async function wrapGramServer( + server: McpServer, + options?: { variables?: Variables }, +): Promise { + const client = new Client({ name: "gram-functions-mcp", version: "0.0.0" }); + const [clientTransport, serverTransport] = InMemoryTransport + .createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const capabilities = client.getServerCapabilities(); + const listedTools = capabilities?.tools + ? await client.listTools().catch((error: unknown) => { + if ( + error instanceof ProtocolError + && error.code === ProtocolErrorCode.MethodNotFound + ) { + return { tools: [] }; + } + throw error; + }) + : { tools: [] }; + const listedResources = capabilities?.resources + ? await client.listResources().catch((error: unknown) => { + if ( + error instanceof ProtocolError + && error.code === ProtocolErrorCode.MethodNotFound + ) { + return { resources: [] }; + } + throw error; + }) + : { resources: [] }; + const variables = options?.variables; + const tools: NonNullable = listedTools.tools.map(( + tool, + ) => ({ + name: tool.name, + ...(tool.description === undefined + ? {} + : { description: tool.description }), + inputSchema: tool.inputSchema, + ...(tool.annotations === undefined ? {} : { + annotations: { + ...(tool.annotations.title === undefined + ? {} + : { title: tool.annotations.title }), + ...(tool.annotations.readOnlyHint === undefined + ? {} + : { readOnlyHint: tool.annotations.readOnlyHint }), + ...(tool.annotations.destructiveHint === undefined + ? {} + : { destructiveHint: tool.annotations.destructiveHint }), + ...(tool.annotations.idempotentHint === undefined + ? {} + : { idempotentHint: tool.annotations.idempotentHint }), + ...(tool.annotations.openWorldHint === undefined + ? {} + : { openWorldHint: tool.annotations.openWorldHint }), + }, + }), + ...(variables === undefined ? {} : { variables }), + meta: { ...tool._meta, "gram.ai/kind": "mcp-passthrough" }, + })); + const resources: NonNullable = listedResources + .resources.map((resource) => ({ + name: resource.name, + description: resource.description, + uri: resource.uri, + mimeType: resource.mimeType, + title: resource.title, + ...(variables === undefined ? {} : { variables }), + meta: { ...resource._meta, "gram.ai/kind": "mcp-passthrough" }, + })); + + return { + manifest: () => ({ version: "0.0.0", tools, resources }), + async handleToolCall(call) { + const response = await client.callTool({ + name: call.name, + ...(call.input === undefined ? {} : { arguments: call.input }), + ...(call._meta === undefined ? {} : { _meta: call._meta }), + }); + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json; mcp=tools_call" }, + }); + }, + async handleResources(call) { + const response = await client.readResource({ + uri: call.uri, + ...(call._meta === undefined ? {} : { _meta: call._meta }), + }); + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }, + }; + } catch (error) { + await client.close(); + await server.close(); + throw error; + } +} + const logger = createConsoleLogger("info"); // Build flags object from environment variables (Gram injects these) @@ -76,7 +189,7 @@ const { server } = createMCPServer({ PORT: flags["api-port"], }); -export default await withGram(server, { +export default await wrapGramServer(server, { variables: { SPK_USERNAME: { description: `Username`, diff --git a/zSDKs/mcp-typescript/src/mcp-server/prompts.ts b/zSDKs/mcp-typescript/src/mcp-server/prompts.ts index 0015bbdb5..d77201705 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/prompts.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/prompts.ts @@ -2,32 +2,28 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { - ShapeOutput, - ZodRawShapeCompat, -} from "@modelcontextprotocol/sdk/server/zod-compat.js"; -import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { - GetPromptResult, - ServerNotification, - ServerRequest, -} from "@modelcontextprotocol/sdk/types.js"; +import { GetPromptResult, McpServer } from "@modelcontextprotocol/server"; +import * as z from "zod"; import { SDKCore } from "../core.js"; import { ConsoleLogger } from "./console-logger.js"; import { MCPScope } from "./scopes.js"; +import { + mcpInputSchema, + MCPServerContext, + mcpServerContext, +} from "./shared.js"; export type PromptDefinition< - Args extends undefined | ZodRawShapeCompat = undefined, -> = Args extends ZodRawShapeCompat ? { + Args extends undefined | z.ZodRawShape = undefined, +> = Args extends z.ZodRawShape ? { name: string; description?: string; scopes?: MCPScope[]; args: Args; prompt: ( client: SDKCore, - args: ShapeOutput, - extra: RequestHandlerExtra, + args: z.output>, + extra: MCPServerContext, ) => GetPromptResult | Promise; } : { @@ -37,7 +33,7 @@ export type PromptDefinition< args?: undefined; prompt: ( client: SDKCore, - extra: RequestHandlerExtra, + extra: MCPServerContext, ) => GetPromptResult | Promise; }; @@ -61,10 +57,8 @@ export function createRegisterPrompt( server: McpServer, getSDK: () => SDKCore, allowedScopes: Set, -): ( - prompt: PromptDefinition, -) => void { - return ( +): (prompt: PromptDefinition) => void { + return ( prompt: PromptDefinition, ): void => { const scopes = prompt.scopes ?? []; @@ -80,30 +74,23 @@ export function createRegisterPrompt( } if (prompt.args) { - if (prompt.description) { - server.prompt( - prompt.name, - prompt.description, - prompt.args, - async (args, ctx) => prompt.prompt(getSDK(), args, ctx), - ); - } else { - server.prompt( - prompt.name, - prompt.args, - async (args, ctx) => prompt.prompt(getSDK(), args, ctx), - ); - } + server.registerPrompt( + prompt.name, + { + ...(prompt.description === undefined + ? {} + : { description: prompt.description }), + argsSchema: { ...mcpInputSchema(prompt.args), shape: prompt.args }, + }, + async (args, ctx) => + prompt.prompt(getSDK(), args, mcpServerContext(ctx)), + ); } else { - if (prompt.description) { - server.prompt( - prompt.name, - prompt.description, - async (ctx) => prompt.prompt(getSDK(), ctx), - ); - } else { - server.prompt(prompt.name, async (ctx) => prompt.prompt(getSDK(), ctx)); - } + server.registerPrompt(prompt.name, { + ...(prompt.description === undefined + ? {} + : { description: prompt.description }), + }, async (ctx) => prompt.prompt(getSDK(), mcpServerContext(ctx))); } logger.debug("Registered prompt", { name: prompt.name }); diff --git a/zSDKs/mcp-typescript/src/mcp-server/resources.ts b/zSDKs/mcp-typescript/src/mcp-server/resources.ts index 274007f7a..3ae296085 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/resources.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/resources.ts @@ -2,27 +2,23 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ +import type { ResourceTemplate as LegacyResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer, + ReadResourceResult, ResourceMetadata, ResourceTemplate, -} from "@modelcontextprotocol/sdk/server/mcp.js"; -import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { Variables } from "@modelcontextprotocol/sdk/shared/uriTemplate.js"; -import { - ReadResourceResult, - ServerNotification, - ServerRequest, -} from "@modelcontextprotocol/sdk/types.js"; + Variables, +} from "@modelcontextprotocol/server"; import { SDKCore } from "../core.js"; import { ConsoleLogger } from "./console-logger.js"; import { MCPScope } from "./scopes.js"; -import { valueToBase64 } from "./shared.js"; +import { MCPServerContext, mcpServerContext, valueToBase64 } from "./shared.js"; export type ReadResourceCallback = ( client: SDKCore, uri: URL, - extra: RequestHandlerExtra, + extra: MCPServerContext, ) => ReadResourceResult | Promise; export type ResourceDefinition = { @@ -38,7 +34,7 @@ export type ReadResourceTemplateCallback = ( client: SDKCore, uri: URL, vars: Variables, - extra: RequestHandlerExtra, + extra: MCPServerContext, ) => ReadResourceResult | Promise; export type ResourceTemplateDefinition = { @@ -46,7 +42,7 @@ export type ResourceTemplateDefinition = { description: string; metadata?: ResourceMetadata; scopes?: MCPScope[]; - resource: ResourceTemplate; + resource: ResourceTemplate | LegacyResourceTemplate; read: ReadResourceTemplateCallback; }; @@ -94,11 +90,11 @@ export function createRegisterResource( description: resource.description, }; - server.resource( + server.registerResource( resource.name, resource.resource, metadata, - async (uri, ctx) => resource.read(getSDK(), uri, ctx), + async (uri, ctx) => resource.read(getSDK(), uri, mcpServerContext(ctx)), ); logger.debug("Registered resource", { name: resource.name }); @@ -129,13 +125,32 @@ export function createRegisterResourceTemplate( description: resource.description, }; - server.resource( + server.registerResource( resource.name, - resource.resource, + normalizeResourceTemplate(resource.resource), metadata, - async (uri, vars, ctx) => resource.read(getSDK(), uri, vars, ctx), + async (uri, vars, ctx) => + resource.read(getSDK(), uri, vars, mcpServerContext(ctx)), ); logger.debug("Registered resource template", { name: resource.name }); }; } + +function normalizeResourceTemplate( + resource: ResourceTemplate | LegacyResourceTemplate, +): ResourceTemplate { + if (resource instanceof ResourceTemplate) return resource; + const complete: NonNullable< + ConstructorParameters[1]["complete"] + > = {}; + for (const variable of resource.uriTemplate.variableNames) { + const callback = resource.completeCallback(variable); + if (callback) complete[variable] = callback; + } + const list = resource.listCallback; + return new ResourceTemplate(resource.uriTemplate.toString(), { + list: list ? (ctx) => list(mcpServerContext(ctx)) : undefined, + complete, + }); +} diff --git a/zSDKs/mcp-typescript/src/mcp-server/server.ts b/zSDKs/mcp-typescript/src/mcp-server/server.ts index b489c1bca..d84ad4162 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/server.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/server.ts @@ -2,7 +2,7 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import { SDKCore } from "../core.js"; import { SDKOptions } from "../lib/config.js"; import type { ConsoleLogger } from "./console-logger.js"; diff --git a/zSDKs/mcp-typescript/src/mcp-server/shared.ts b/zSDKs/mcp-typescript/src/mcp-server/shared.ts index bb74a7478..9867264b5 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/shared.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/shared.ts @@ -2,8 +2,209 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ +import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; +import type { + ServerNotification, + ServerRequest, +} from "@modelcontextprotocol/sdk/types.js"; +import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; +import type { + ServerContext, + StandardSchemaWithJSON, +} from "@modelcontextprotocol/server"; import * as z from "zod"; +export type MCPServerContext = + & ServerContext + & RequestHandlerExtra; + +export function mcpServerContext(ctx: ServerContext): MCPServerContext { + return { + ...ctx, + signal: ctx.mcpReq.signal, + requestId: ctx.mcpReq.id, + ...(ctx.mcpReq._meta ? { _meta: ctx.mcpReq._meta } : {}), + ...(ctx.http?.authInfo ? { authInfo: ctx.http.authInfo } : {}), + ...(ctx.http?.req + ? { + requestInfo: { + headers: Object.fromEntries(ctx.http.req.headers), + url: new URL(ctx.http.req.url), + }, + } + : {}), + ...(ctx.http?.closeSSE ? { closeSSEStream: ctx.http.closeSSE } : {}), + ...(ctx.http?.closeStandaloneSSE + ? { closeStandaloneSSEStream: ctx.http.closeStandaloneSSE } + : {}), + sendNotification: async (notification) => { + if (!ctx.mcpReq.signal.aborted) await ctx.mcpReq.notify(notification); + }, + sendRequest: async (request, resultSchema, options) => { + if (ctx.mcpReq.signal.aborted) { + throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled"); + } + return (ctx.mcpReq.send as MCPServerContext["sendRequest"])( + request, + resultSchema, + options, + ); + }, + }; +} + +export function mcpInputSchema( + shape: Shape, +): StandardSchemaWithJSON< + z.input>, + z.output> +> { + const schema = z.object(shape); + const standard = schema["~standard"]; + return { + "~standard": { + ...standard, + jsonSchema: { + input: (options) => { + const source = jsonSchemaSource(schema); + return source.schema["~standard"].jsonSchema.input({ + ...options, + libraryOptions: { + ...options.libraryOptions, + metadata: source.metadata, + override: source.override, + unrepresentable: "any", + }, + }); + }, + output: (options) => { + const source = jsonSchemaSource(schema); + return source.schema["~standard"].jsonSchema.output({ + ...options, + libraryOptions: { + ...options.libraryOptions, + metadata: source.metadata, + override: source.override, + unrepresentable: "any", + }, + }); + }, + }, + }, + }; +} + +// Zod serializes defaults before its override runs; clone only the conversion schema to preserve runtime defaults. +function jsonSchemaSource(schema: z.ZodType) { + const registry = z.registry(); + const copies = new WeakMap(); + const defaults = new WeakMap(); + const jsonValue = (value: unknown, onNormalized?: () => void): unknown => + value === undefined ? undefined : JSON.parse( + JSON.stringify( + value, + function(this: Record, key, item: unknown) { + if (this[key] instanceof Date) onNormalized?.(); + if (typeof item !== "bigint") return item; + onNormalized?.(); + return item.toString(); + }, + ), + ); + const copy = (value: unknown): unknown => { + if (typeof value !== "object" || value === null) return value; + if (copies.has(value)) return copies.get(value); + if (value instanceof z.ZodType) { + const def = copy(value.def) as z.ZodType["def"]; + const cloned = value.clone(def); + copies.set(value, cloned); + const metadata = value.meta(); + if (metadata) { + registry.add(cloned, jsonValue(metadata) as typeof metadata); + } + if (value instanceof z.ZodDefault || value instanceof z.ZodPrefault) { + let hasNormalizedDefault = false; + const normalized = jsonValue(value.def.defaultValue, () => { + hasNormalizedDefault = true; + }); + if (hasNormalizedDefault) { + defaults.set(cloned, undefined); + try { + if (z.safeParse(value.def.innerType, normalized).success) { + defaults.set(cloned, normalized); + } + } catch { + return cloned; + } + } + } + if (value instanceof z.ZodCatch) { + (def as z.ZodCatch["def"]).catchValue = (ctx) => { + let fallback: unknown; + try { + fallback = value.def.catchValue(ctx); + } catch (error) { + if (ctx !== undefined) throw error; + defaults.set(cloned, undefined); + return undefined; + } + let hasNormalizedDefault = false; + const normalized = jsonValue(fallback, () => { + hasNormalizedDefault = true; + }); + if (hasNormalizedDefault) { + defaults.set(cloned, undefined); + try { + if (z.safeParse(value.def.innerType, normalized).success) { + defaults.set(cloned, normalized); + } + } catch { + return normalized; + } + } + return normalized; + }; + } + return cloned; + } + if (Array.isArray(value)) { + const cloned: unknown[] = []; + copies.set(value, cloned); + for (const item of value) cloned.push(copy(item)); + return cloned; + } + if (Object.getPrototypeOf(value) !== Object.prototype) return value; + const cloned: Record = {}; + copies.set(value, cloned); + for (const key of Object.keys(value)) { + const descriptor = Object.getOwnPropertyDescriptor(value, key)!; + const read = () => { + const item: unknown = Reflect.get(value, key); + if (key === "defaultValue") return jsonValue(item); + if (key === "getter" && typeof item === "function") { + return () => copy(item()); + } + return copy(item); + }; + if (descriptor.get) { + Object.defineProperty(cloned, key, { enumerable: true, get: read }); + } else { + cloned[key] = read(); + } + } + return cloned; + }; + const override: NonNullable = ( + { zodSchema, jsonSchema }, + ) => { + if (!defaults.has(zodSchema)) return; + const value = defaults.get(zodSchema); + if (value === undefined) delete jsonSchema.default; + else jsonSchema.default = value; + }; + return { schema: copy(schema) as z.ZodType, metadata: registry, override }; +} + type BinaryData = | Uint8Array | ArrayBuffer diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools.ts b/zSDKs/mcp-typescript/src/mcp-server/tools.ts index 14f37e5e1..8fa78a3fa 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools.ts @@ -2,17 +2,7 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { - ShapeOutput, - ZodRawShapeCompat, -} from "@modelcontextprotocol/sdk/server/zod-compat.js"; -import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { - CallToolResult, - ServerNotification, - ServerRequest, -} from "@modelcontextprotocol/sdk/types.js"; +import { CallToolResult, McpServer } from "@modelcontextprotocol/server"; import * as z from "zod"; import { SDKCore } from "../core.js"; import { ServerPORT$zodSchema } from "../lib/config.js"; @@ -20,7 +10,12 @@ import { allRequired } from "../lib/primitives.js"; import { ConsoleLogger } from "./console-logger.js"; import { MCPServerFlags } from "./flags.js"; import { MCPScope, mcpScopes } from "./scopes.js"; -import { valueToBase64 } from "./shared.js"; +import { + mcpInputSchema, + MCPServerContext, + mcpServerContext, + valueToBase64, +} from "./shared.js"; export type MCPToolAnnotationFilter = { readOnlyHint?: boolean; @@ -66,6 +61,11 @@ export function buildAnnotationFilter( return filter; } +type ZodRawShapeCompat = z.ZodRawShape; +type ShapeOutput = z.output< + z.ZodObject +>; + export type ToolDefinition< Args extends undefined | ZodRawShapeCompat = undefined, > = Args extends ZodRawShapeCompat ? { @@ -83,7 +83,7 @@ export type ToolDefinition< tool: ( client: SDKCore, args: ShapeOutput, - extra: RequestHandlerExtra, + extra: MCPServerContext, ) => CallToolResult | Promise; } : { @@ -100,7 +100,7 @@ export type ToolDefinition< }; tool: ( client: SDKCore, - extra: RequestHandlerExtra, + extra: MCPServerContext, ) => CallToolResult | Promise; }; @@ -138,16 +138,13 @@ export function createRegisterTool( dynamic?: boolean, annotationFilter?: MCPToolAnnotationFilter, ): [ - (tool: ToolDefinition) => void, + (tool: ToolDefinition) => void, Array<{ name: string; description: string }>, - Map>, + Map>, ] { const tools: Array<{ name: string; description: string }> = []; - const toolMap = new Map< - string, - ToolDefinition - >(); - const registerTool = ( + const toolMap = new Map>(); + const registerTool = ( tool: ToolDefinition, ): void => { if (allowedTools && !allowedTools.has(tool.name)) { @@ -194,10 +191,7 @@ export function createRegisterTool( } } - toolMap.set( - tool.name, - tool as ToolDefinition, - ); + toolMap.set(tool.name, tool as ToolDefinition); if (dynamic) { logger.debug("Collected tool for dynamic mode", { name: tool.name }); @@ -210,11 +204,11 @@ export function createRegisterTool( tool.name, { description: tool.description, - inputSchema: tool.args, + inputSchema: mcpInputSchema(tool.args), annotations: tool.annotations, }, async (args, ctx) => { - return tool.tool(getSDK(), args, ctx); + return tool.tool(getSDK(), args, mcpServerContext(ctx)); }, ); } else { @@ -225,7 +219,7 @@ export function createRegisterTool( annotations: tool.annotations, }, async (ctx) => { - return tool.tool(getSDK(), ctx); + return tool.tool(getSDK(), mcpServerContext(ctx)); }, ); } @@ -240,7 +234,7 @@ export function createRegisterTool( function matchesSearchTerms( terms: string[], name: string, - def: ToolDefinition, + def: ToolDefinition, ): boolean { if (terms.length === 0) { return true; @@ -263,14 +257,14 @@ export function registerDynamicTools( logger: ConsoleLogger, server: McpServer, getSDK: () => SDKCore, - toolMap: Map>, + toolMap: Map>, allowedScopes: Set, ): void { // 1. list_tools server.registerTool("list_tools", { description: "List available tools. Optionally filter by search terms that match against tool name, description, and scopes.", - inputSchema: { + inputSchema: z.object({ search_terms: z.array(z.string()).optional().describe( [ "Filter the list of tools.", @@ -279,7 +273,7 @@ export function registerDynamicTools( "If not provided, all tools are returned.", ].join("\n"), ), - }, + }), annotations: { title: "List Tools", readOnlyHint: true, @@ -314,11 +308,11 @@ export function registerDynamicTools( server.registerTool("describe_tool_input", { description: "Get the input schema for one or more tools. It is a good idea to call this tool first to understand how to successfully call execute_tool.", - inputSchema: { + inputSchema: z.object({ tool_names: z.array(z.string()).describe( "The names of the tools to describe", ), - }, + }), annotations: { title: "Describe Tool", readOnlyHint: true, @@ -343,11 +337,8 @@ export function registerDynamicTools( let schemaText = `\n\n`; if (def.args) { - const jsonSchema = z.toJSONSchema(z.object(def.args), { - target: "draft-2020-12", - io: "input", - unrepresentable: "any", - }); + const jsonSchema = mcpInputSchema(def.args)["~standard"].jsonSchema + .input({ target: "draft-2020-12" }); schemaText += JSON.stringify(jsonSchema, null, 2); } else { schemaText += "This tool takes no input parameters."; @@ -368,12 +359,12 @@ export function registerDynamicTools( server.registerTool("execute_tool", { description: "Execute a tool by name with its arguments. If executing a given tool for the first time, it is recommended to call describe_tool_input first to understand the expected `arguments` shape.", - inputSchema: { + inputSchema: z.object({ name: z.string().describe("The name of the tool to execute"), arguments: z.looseObject({}).optional().describe( "Arguments for the target tool as a JSON object, matching the schema returned by describe_tool_input.", ), - }, + }), annotations: { title: "Execute Tool", readOnlyHint: false, @@ -392,7 +383,9 @@ export function registerDynamicTools( let validatedInput: Record = {}; if (def.args) { - const vres = z.object(def.args).safeParse(args.arguments ?? {}); + const vres = await z.object(def.args).safeParseAsync( + args.arguments ?? {}, + ); if (vres.success) { validatedInput = vres.data; } else { @@ -411,9 +404,9 @@ export function registerDynamicTools( try { if (def.args) { - return await def.tool(getSDK(), validatedInput, ctx); + return await def.tool(getSDK(), validatedInput, mcpServerContext(ctx)); } else { - return await def.tool(getSDK(), ctx); + return await def.tool(getSDK(), mcpServerContext(ctx)); } } catch (error) { const message = error instanceof Error ? error.message : String(error); @@ -465,7 +458,11 @@ function resolveHeader( if (val != null) { return schema.parse(val); } - return disableStaticAuth ? undefined : schema.parse(cliFlagValue); + if (disableStaticAuth || cliFlagValue === undefined) { + const parsed = schema.safeParse(undefined); + return parsed.success ? parsed.data : undefined; + } + return schema.parse(cliFlagValue); } export function buildSDK( diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/binaryAndStringUpload.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/binaryAndStringUpload.ts index 5de772721..e8b3c4ee7 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/binaryAndStringUpload.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/binaryAndStringUpload.ts @@ -25,7 +25,7 @@ export const tool$binaryAndStringUpload: ToolDefinition = { const [result] = await binaryAndStringUpload( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/createUser.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/createUser.ts index 53c89efb1..143ad9226 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/createUser.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/createUser.ts @@ -29,7 +29,7 @@ different pairing scenarios for documentation generation. const [result] = await createUser( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/createWithUnion.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/createWithUnion.ts index a785e7375..3c92a7721 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/createWithUnion.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/createWithUnion.ts @@ -27,7 +27,7 @@ Test CLI generation for discriminated unions with dot-notation flags`, const [result] = await createWithUnion( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/deleteUser.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/deleteUser.ts index 73e29740d..cf6626ddc 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/deleteUser.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/deleteUser.ts @@ -25,7 +25,7 @@ export const tool$deleteUser: ToolDefinition = { const [result] = await deleteUser( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getBinaryDefaultResponse.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getBinaryDefaultResponse.ts index 7ab0db987..b12e25b52 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getBinaryDefaultResponse.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getBinaryDefaultResponse.ts @@ -18,7 +18,7 @@ export const tool$getBinaryDefaultResponse: ToolDefinition = { tool: async (client, ctx) => { const [result] = await getBinaryDefaultResponse( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getDuplicateExportCollision.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getDuplicateExportCollision.ts index 1750fb5bc..55f99709b 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getDuplicateExportCollision.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getDuplicateExportCollision.ts @@ -19,7 +19,7 @@ export const tool$getDuplicateExportCollision: ToolDefinition = { tool: async (client, ctx) => { const [result] = await getDuplicateExportCollision( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getEmptyObjectError.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getEmptyObjectError.ts index 5e5add890..af772fe1e 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getEmptyObjectError.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getEmptyObjectError.ts @@ -20,7 +20,7 @@ This endpoint tests the behavior when an error response has an empty object sche tool: async (client, ctx) => { const [result] = await getEmptyObjectError( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getErrorInUnion.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getErrorInUnion.ts index 0cfa8102a..55620d19a 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getErrorInUnion.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getErrorInUnion.ts @@ -18,7 +18,7 @@ export const tool$getErrorInUnion: ToolDefinition = { tool: async (client, ctx) => { const [result] = await getErrorInUnion( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getErrorOnlyExample.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getErrorOnlyExample.ts index 139791887..246f1c454 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getErrorOnlyExample.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getErrorOnlyExample.ts @@ -22,7 +22,7 @@ a default example for the success response.`, tool: async (client, ctx) => { const [result] = await getErrorOnlyExample( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getFullyFlattenedRequest.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getFullyFlattenedRequest.ts index 44cd06e81..1888a9dba 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getFullyFlattenedRequest.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getFullyFlattenedRequest.ts @@ -25,7 +25,7 @@ export const tool$getFullyFlattenedRequest: ToolDefinition = { const [result] = await getFullyFlattenedRequest( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getNamedPrimitiveUnion.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getNamedPrimitiveUnion.ts index 07c66ad6f..0778b1134 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getNamedPrimitiveUnion.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getNamedPrimitiveUnion.ts @@ -19,7 +19,7 @@ export const tool$getNamedPrimitiveUnion: ToolDefinition = { tool: async (client, ctx) => { const [result] = await getNamedPrimitiveUnion( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getNestedIntegerString.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getNestedIntegerString.ts index a1e12eeeb..74be56754 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getNestedIntegerString.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getNestedIntegerString.ts @@ -21,7 +21,7 @@ an integer field that should be unmarshaled from a string.`, tool: async (client, ctx) => { const [result] = await getNestedIntegerString( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getPolymorphism.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getPolymorphism.ts index 8a258bff5..b79a18352 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getPolymorphism.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getPolymorphism.ts @@ -18,7 +18,7 @@ export const tool$getPolymorphism: ToolDefinition = { tool: async (client, ctx) => { const [result] = await getPolymorphism( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getPositionalDefault.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getPositionalDefault.ts index 01d424074..c425b85b2 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getPositionalDefault.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getPositionalDefault.ts @@ -27,7 +27,7 @@ Check that a default-backed path parameter can be supplied as an argument or omi const [result] = await getPositionalDefault( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getRequestBodyFlattenedAway.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getRequestBodyFlattenedAway.ts index df4ab6c24..46bffec2f 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getRequestBodyFlattenedAway.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getRequestBodyFlattenedAway.ts @@ -25,7 +25,7 @@ export const tool$getRequestBodyFlattenedAway: ToolDefinition = { const [result] = await getRequestBodyFlattenedAway( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getUnionErrors.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getUnionErrors.ts index 927014373..b22f2a1dd 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getUnionErrors.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getUnionErrors.ts @@ -25,7 +25,7 @@ export const tool$getUnionErrors: ToolDefinition = { const [result] = await getUnionErrors( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/getUser.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/getUser.ts index 4aa707951..6e127a085 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/getUser.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/getUser.ts @@ -25,7 +25,7 @@ export const tool$getUser: ToolDefinition = { const [result] = await getUser( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/groupRootGroupOp.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/groupRootGroupOp.ts index 5d2049250..91063f59c 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/groupRootGroupOp.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/groupRootGroupOp.ts @@ -22,7 +22,7 @@ but also an operation. tool: async (client, ctx) => { const [result] = await groupRootGroupOp( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/groupSubGroupEmptyTailNestedGroupOp.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/groupSubGroupEmptyTailNestedGroupOp.ts index 4c6bf1ae3..8db27dd62 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/groupSubGroupEmptyTailNestedGroupOp.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/groupSubGroupEmptyTailNestedGroupOp.ts @@ -21,7 +21,7 @@ Notice that 'group.flattened' has no operations. tool: async (client, ctx) => { const [result] = await groupSubGroupEmptyTailNestedGroupOp( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/groupSubGroupSubGroupOp.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/groupSubGroupSubGroupOp.ts index 140ba72f7..d31686af8 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/groupSubGroupSubGroupOp.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/groupSubGroupSubGroupOp.ts @@ -18,7 +18,7 @@ export const tool$groupSubGroupSubGroupOp: ToolDefinition = { tool: async (client, ctx) => { const [result] = await groupSubGroupSubGroupOp( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/login.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/login.ts index 6562d500e..0d534d8d8 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/login.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/login.ts @@ -18,7 +18,7 @@ export const tool$login: ToolDefinition = { tool: async (client, ctx) => { const [result] = await login( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsCreateNamespaceConflict.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsCreateNamespaceConflict.ts index 7555c4b8a..3a5a521fd 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsCreateNamespaceConflict.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsCreateNamespaceConflict.ts @@ -29,7 +29,7 @@ Uses foo.Pet in the request and bar.Pet in the response.`, const [result] = await namespaceTestsConflictsCreateNamespaceConflict( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsGetNamespaceConflict.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsGetNamespaceConflict.ts index 90be85ad0..bc5a4d744 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsGetNamespaceConflict.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsGetNamespaceConflict.ts @@ -23,7 +23,7 @@ The SDK should properly import and alias both Pet types.`, tool: async (client, ctx) => { const [result] = await namespaceTestsConflictsGetNamespaceConflict( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsGetPetOwners.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsGetPetOwners.ts index 0a87fede1..78876406d 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsGetPetOwners.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsGetPetOwners.ts @@ -21,7 +21,7 @@ Returns both foo.PetOwner and bar.PetOwner in the response.`, tool: async (client, ctx) => { const [result] = await namespaceTestsConflictsGetPetOwners( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsGetTripleNamespaceConflict.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsGetTripleNamespaceConflict.ts index e001cc21f..46d55a46a 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsGetTripleNamespaceConflict.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsGetTripleNamespaceConflict.ts @@ -23,7 +23,7 @@ The SDK should properly import and alias all three Pet types.`, tool: async (client, ctx) => { const [result] = await namespaceTestsConflictsGetTripleNamespaceConflict( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsPutNamespaceConflict.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsPutNamespaceConflict.ts index c56ace454..df73e42b0 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsPutNamespaceConflict.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsConflictsPutNamespaceConflict.ts @@ -30,7 +30,7 @@ x-speakeasy-name-override and x-speakeasy-model-namespace extensions.`, const [result] = await namespaceTestsConflictsPutNamespaceConflict( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsSingleBarGetSingleNamespaceBarPet.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsSingleBarGetSingleNamespaceBarPet.ts index dcf70183e..b4e6d5724 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsSingleBarGetSingleNamespaceBarPet.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsSingleBarGetSingleNamespaceBarPet.ts @@ -22,7 +22,7 @@ No import aliasing should be needed since there's no conflict within this group. tool: async (client, ctx) => { const [result] = await namespaceTestsSingleBarGetSingleNamespaceBarPet( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsSingleFooCreateSingleNamespaceFooPet.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsSingleFooCreateSingleNamespaceFooPet.ts index e9de1f27a..3b68a0173 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsSingleFooCreateSingleNamespaceFooPet.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsSingleFooCreateSingleNamespaceFooPet.ts @@ -29,7 +29,7 @@ No import aliasing should be needed since there's no conflict within this group. const [result] = await namespaceTestsSingleFooCreateSingleNamespaceFooPet( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsSingleFooGetSingleNamespaceFooPet.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsSingleFooGetSingleNamespaceFooPet.ts index dae48d8d2..1f55de7f5 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsSingleFooGetSingleNamespaceFooPet.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsSingleFooGetSingleNamespaceFooPet.ts @@ -22,7 +22,7 @@ No import aliasing should be needed since there's no conflict within this group. tool: async (client, ctx) => { const [result] = await namespaceTestsSingleFooGetSingleNamespaceFooPet( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceAnimal.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceAnimal.ts index 1c3c06163..7bbf1c341 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceAnimal.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceAnimal.ts @@ -21,7 +21,7 @@ The response can be either a foo.Dog or foo.Cat.`, tool: async (client, ctx) => { const [result] = await namespaceTestsTypesGetNamespaceAnimal( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceOrganization.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceOrganization.ts index 4278da6d5..52ffc5e85 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceOrganization.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceOrganization.ts @@ -23,7 +23,7 @@ should inherit the foo namespace.`, tool: async (client, ctx) => { const [result] = await namespaceTestsTypesGetNamespaceOrganization( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceTypes.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceTypes.ts index 9a55225f5..281bcf4e7 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceTypes.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceTypes.ts @@ -21,7 +21,7 @@ non-discriminated unions, and models with nested inline schemas.`, tool: async (client, ctx) => { const [result] = await namespaceTestsTypesGetNamespaceTypes( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceVehicle.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceVehicle.ts index a439ebfd0..d3ac54bc8 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceVehicle.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/namespaceTestsTypesGetNamespaceVehicle.ts @@ -21,7 +21,7 @@ The response can be either a bar.Car or bar.Bike.`, tool: async (client, ctx) => { const [result] = await namespaceTestsTypesGetNamespaceVehicle( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/operationWithLeadingAndTrailingUnderscores.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/operationWithLeadingAndTrailingUnderscores.ts index 109c44872..70e88837d 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/operationWithLeadingAndTrailingUnderscores.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/operationWithLeadingAndTrailingUnderscores.ts @@ -27,7 +27,7 @@ export const tool$operationWithLeadingAndTrailingUnderscores: ToolDefinition< const [result] = await operationWithLeadingAndTrailingUnderscores( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/parenthesesInPathAllowed.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/parenthesesInPathAllowed.ts index fdedb1b46..fa877c090 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/parenthesesInPathAllowed.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/parenthesesInPathAllowed.ts @@ -37,7 +37,7 @@ and \\'escaped single quotes\\' and \\"escaped double quotes\\". const [result] = await parenthesesInPathAllowed( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/postFile.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/postFile.ts index 744460952..f0dbee588 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/postFile.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/postFile.ts @@ -28,7 +28,7 @@ It has a description.`, const [result] = await postFile( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/tag1Deprecated1.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/tag1Deprecated1.ts index d2fb53fc2..a28168d05 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/tag1Deprecated1.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/tag1Deprecated1.ts @@ -18,7 +18,7 @@ export const tool$tag1Deprecated1: ToolDefinition = { tool: async (client, ctx) => { const [result] = await tag1Deprecated1( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/tag1ListTest1.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/tag1ListTest1.ts index 41d761eea..e099317c8 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/tag1ListTest1.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/tag1ListTest1.ts @@ -28,7 +28,7 @@ It has a description.`, const [result] = await tag1ListTest1( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/tag1PostFileWithEncoding.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/tag1PostFileWithEncoding.ts index 68fd5b251..4e9b8a21d 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/tag1PostFileWithEncoding.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/tag1PostFileWithEncoding.ts @@ -32,7 +32,7 @@ handling of comma-separated content types in encoding.`, const [result] = await tag1PostFileWithEncoding( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/testEndpoint.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/testEndpoint.ts index afc6d2e32..0fde98e85 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/testEndpoint.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/testEndpoint.ts @@ -25,7 +25,7 @@ export const tool$testEndpoint: ToolDefinition = { const [result] = await testEndpoint( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/testEnumFormats.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/testEnumFormats.ts index e5d9871dd..6f6a3d94f 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/testEnumFormats.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/testEnumFormats.ts @@ -28,7 +28,7 @@ including partial map coverage and both string and integer enum types.`, const [result] = await testEnumFormats( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/testGroupTag2PostTest.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/testGroupTag2PostTest.ts index f320268f1..9db244390 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/testGroupTag2PostTest.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/testGroupTag2PostTest.ts @@ -28,7 +28,7 @@ It has a description.`, const [result] = await testGroupTag2PostTest( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/updateUser.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/updateUser.ts index 635742ae0..159c6001c 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/updateUser.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/updateUser.ts @@ -25,7 +25,7 @@ export const tool$updateUser: ToolDefinition = { const [result] = await updateUser( client, args.request, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/urlValidationStressTest.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/urlValidationStressTest.ts index efe865a42..65b76ec07 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/urlValidationStressTest.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/urlValidationStressTest.ts @@ -18,7 +18,7 @@ export const tool$urlValidationStressTest: ToolDefinition = { tool: async (client, ctx) => { const [result] = await urlValidationStressTest( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools/validate.ts b/zSDKs/mcp-typescript/src/mcp-server/tools/validate.ts index 608848c07..83c60afa1 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools/validate.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools/validate.ts @@ -18,7 +18,7 @@ export const tool$validate: ToolDefinition = { tool: async (client, ctx) => { const [result] = await validate( client, - { fetchOptions: { signal: ctx.signal } }, + { fetchOptions: { signal: ctx.mcpReq.signal } }, ).$inspect(); if (!result.ok) { diff --git a/zSDKs/mcp-typescript/worker-configuration.d.ts b/zSDKs/mcp-typescript/worker-configuration.d.ts index 2fff44a7c..dd8edb825 100644 --- a/zSDKs/mcp-typescript/worker-configuration.d.ts +++ b/zSDKs/mcp-typescript/worker-configuration.d.ts @@ -7,9 +7,8 @@ // Runtime types generated with workerd@1.20250709.0 2025-07-15 nodejs_compat declare namespace Cloudflare { interface Env { - OPENAPI_MCP: DurableObjectNamespace< - import("./src/cloudflare-worker").OpenapiMCP - >; + OPENAPI_MCP: DurableObjectNamespace; + ASSETS: Fetcher; } } interface Env extends Cloudflare.Env {} diff --git a/zSDKs/mcp-typescript/wrangler.toml b/zSDKs/mcp-typescript/wrangler.toml index 808cb0f5a..26cb46790 100644 --- a/zSDKs/mcp-typescript/wrangler.toml +++ b/zSDKs/mcp-typescript/wrangler.toml @@ -4,14 +4,13 @@ main = "src/cloudflare-worker/cloudflare-worker.ts" compatibility_date = "2025-07-15" # Enable node compatibility for MCP SDK -compatibility_flags = ["nodejs_compat"] +compatibility_flags = ["nodejs_compat", "enable_request_signal"] -# Durable Object binding for MCP Agent +# Durable Object hosting legacy SSE and Streamable HTTP sessions; existing namespaces and stored state are kept [[durable_objects.bindings]] name = "OPENAPI_MCP" class_name = "OpenapiMCP" -# Durable Object migration [[migrations]] tag = "v1" new_classes = ["OpenapiMCP"]