From 771e5d5ebb5f71e426d9a4f1a16253e4638f6461 Mon Sep 17 00:00:00 2001 From: Thomas Rooney Date: Wed, 7 Oct 2026 14:28:08 +0100 Subject: [PATCH 1/6] feat(mcp): upgrade generated servers to SDK v2 BREAKING CHANGE: MCP servers require Node 20 (22.18 with Gram). Custom extensions use v2 ServerContext. Cloudflare serves stateless HTTP at /mcp and retires /sse; Node legacy SSE remains supported. --- .changesets/1791371503-a3eff565.yaml | 10 + .changesets/1791371504-7b97c87e.yaml | 10 + .../cloudflare-worker.ts.stmpl | 114 +- .../src/mcp-server/extensions.ts.stmpl | 6 +- .../auxiliary/src/mcp-server/prompts.ts.stmpl | 51 +- .../src/mcp-server/resources.ts.stmpl | 22 +- .../auxiliary/src/mcp-server/shared.ts.stmpl | 120 ++ .../auxiliary/src/mcp-server/tools.ts.stmpl | 58 +- .../auxiliary/worker-configuration.d.ts.stmpl | 3 +- .../auxiliary/wrangler.toml.stmpl | 5 +- templates/templates/mcp-typescript/config.ts | 42 +- .../mcp-typescript/includes/dependencies.ts | 16 +- .../mcp-typescript/includes/templating.ts | 18 +- .../mcp-server/cli/eval/impl.ts.stmpl | 2 +- .../mcp-server/cli/serve/impl.ts.stmpl | 25 +- .../mcp-server/cli/start/impl.ts.stmpl | 13 +- .../mcp-typescript/mcp-server/gram.ts.stmpl | 91 +- .../mcp-server/resource-template.ts.stmpl | 4 +- .../mcp-server/resource.ts.stmpl | 2 +- .../mcp-typescript/mcp-server/server.ts.stmpl | 2 +- .../mcp-typescript/mcp-server/tool.ts.stmpl | 2 +- .../mcp-typescript/package.json.stmpl | 4 +- .../templates/mcp-typescript/readme.stmpl | 3 + .../mcp-typescript/readme/installation.stmpl | 4 +- .../primary/auth_additional.test.ts.stmpl | 37 + .../primary/dynamic_tools_additional.test.ts | 1173 ++++++++++++++--- templates/templates/typescriptv2/config.ts | 41 +- .../typescriptv2/includes/dependencies.ts | 27 +- .../auxiliary/src/mcp-server/prompts.ts.stmpl | 49 +- .../src/mcp-server/resources.ts.stmpl | 22 +- .../auxiliary/src/mcp-server/shared.ts.stmpl | 26 + .../auxiliary/src/mcp-server/tools.ts.stmpl | 26 +- .../mcp-server/cli/start/impl.ts.stmpl | 8 +- .../mcp-server/resource-template.ts.stmpl | 4 +- .../typescriptv2/mcp-server/resource.ts.stmpl | 2 +- .../typescriptv2/mcp-server/server.ts.stmpl | 2 +- .../typescriptv2/mcp-server/tool.ts.stmpl | 2 +- .../tests/tertiary/mcp_additional.test.ts | 249 +++- zSDKs/mcp-typescript/.speakeasy/gen.lock | 120 +- zSDKs/mcp-typescript/README.md | 8 +- zSDKs/mcp-typescript/package-lock.json | 706 ++-------- zSDKs/mcp-typescript/package.json | 12 +- .../cloudflare-worker/cloudflare-worker.ts | 161 ++- zSDKs/mcp-typescript/src/landing-page.ts | 16 +- .../src/mcp-server/cli/eval/impl.ts | 2 +- .../src/mcp-server/cli/serve/impl.ts | 25 +- .../src/mcp-server/cli/start/impl.ts | 73 +- .../src/mcp-server/extensions.ts | 8 +- zSDKs/mcp-typescript/src/mcp-server/gram.ts | 117 +- .../mcp-typescript/src/mcp-server/prompts.ts | 64 +- .../src/mcp-server/resources.ts | 20 +- zSDKs/mcp-typescript/src/mcp-server/server.ts | 2 +- zSDKs/mcp-typescript/src/mcp-server/shared.ts | 153 +++ zSDKs/mcp-typescript/src/mcp-server/tools.ts | 135 +- .../mcp-server/tools/binaryAndStringUpload.ts | 2 +- .../src/mcp-server/tools/createUser.ts | 2 +- .../src/mcp-server/tools/createWithUnion.ts | 2 +- .../src/mcp-server/tools/deleteUser.ts | 2 +- .../tools/getBinaryDefaultResponse.ts | 2 +- .../tools/getDuplicateExportCollision.ts | 2 +- .../mcp-server/tools/getEmptyObjectError.ts | 2 +- .../src/mcp-server/tools/getErrorInUnion.ts | 2 +- .../mcp-server/tools/getErrorOnlyExample.ts | 2 +- .../tools/getFullyFlattenedRequest.ts | 2 +- .../tools/getNamedPrimitiveUnion.ts | 2 +- .../tools/getNestedIntegerString.ts | 2 +- .../src/mcp-server/tools/getPolymorphism.ts | 2 +- .../mcp-server/tools/getPositionalDefault.ts | 2 +- .../tools/getRequestBodyFlattenedAway.ts | 2 +- .../src/mcp-server/tools/getUnionErrors.ts | 2 +- .../src/mcp-server/tools/getUser.ts | 2 +- .../src/mcp-server/tools/groupRootGroupOp.ts | 2 +- .../groupSubGroupEmptyTailNestedGroupOp.ts | 2 +- .../tools/groupSubGroupSubGroupOp.ts | 2 +- .../src/mcp-server/tools/login.ts | 2 +- ...ceTestsConflictsCreateNamespaceConflict.ts | 2 +- ...spaceTestsConflictsGetNamespaceConflict.ts | 2 +- .../namespaceTestsConflictsGetPetOwners.ts | 2 +- ...estsConflictsGetTripleNamespaceConflict.ts | 2 +- ...spaceTestsConflictsPutNamespaceConflict.ts | 2 +- ...eTestsSingleBarGetSingleNamespaceBarPet.ts | 2 +- ...stsSingleFooCreateSingleNamespaceFooPet.ts | 2 +- ...eTestsSingleFooGetSingleNamespaceFooPet.ts | 2 +- .../namespaceTestsTypesGetNamespaceAnimal.ts | 2 +- ...spaceTestsTypesGetNamespaceOrganization.ts | 2 +- .../namespaceTestsTypesGetNamespaceTypes.ts | 2 +- .../namespaceTestsTypesGetNamespaceVehicle.ts | 2 +- ...rationWithLeadingAndTrailingUnderscores.ts | 2 +- .../tools/parenthesesInPathAllowed.ts | 2 +- .../src/mcp-server/tools/postFile.ts | 2 +- .../src/mcp-server/tools/tag1Deprecated1.ts | 2 +- .../src/mcp-server/tools/tag1ListTest1.ts | 2 +- .../tools/tag1PostFileWithEncoding.ts | 2 +- .../src/mcp-server/tools/testEndpoint.ts | 2 +- .../src/mcp-server/tools/testEnumFormats.ts | 2 +- .../mcp-server/tools/testGroupTag2PostTest.ts | 2 +- .../src/mcp-server/tools/updateUser.ts | 2 +- .../tools/urlValidationStressTest.ts | 2 +- .../src/mcp-server/tools/validate.ts | 2 +- .../mcp-typescript/worker-configuration.d.ts | 5 +- zSDKs/mcp-typescript/wrangler.toml | 5 +- 101 files changed, 2550 insertions(+), 1467 deletions(-) create mode 100644 .changesets/1791371503-a3eff565.yaml create mode 100644 .changesets/1791371504-7b97c87e.yaml diff --git a/.changesets/1791371503-a3eff565.yaml b/.changesets/1791371503-a3eff565.yaml new file mode 100644 index 000000000..c38ea217e --- /dev/null +++ b/.changesets/1791371503-a3eff565.yaml @@ -0,0 +1,10 @@ +id: 1791371503-a3eff565 +features: + - core +targets: + - mcp-typescript +type: feat +bump: major +description: migrate MCP servers to the v2 SDK, require Node.js 20, and serve Cloudflare over stateless HTTP +author: ThomasRooney +date: "2026-10-07" 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/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..487554f8e 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,89 @@ {{- $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"; +import { createMcpHandler } from "@modelcontextprotocol/server"; 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 { } - -type Props = Record; - -export class {{ $mcpServerName }} extends McpAgent { - server!: McpServer; +function getSDK({{ if $globalSDK.Security }}request: Request | undefined{{ end }}) { +{{- if $globalSDK.Security }} + const getHeader = (name: string) => request?.headers.get(name) ?? '' +{{- end }} + + 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 }} + }); +} - async init() { - const { server } = createMCPServer({ - logger: createConsoleLogger('debug'), - getSDK: () => this.getSDK(), - }); +function sseRetiredResponse(url: URL): Response { + return Response.json( + { + error: "The HTTP+SSE transport is no longer served. Connect to the Streamable HTTP endpoint instead.", + endpoint: new URL("/mcp", url).toString(), + }, + { status: 410 }, + ); +} - this.server = server - } +function corsHeaders(request: Request): Headers { + return new Headers({ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", + "Access-Control-Allow-Headers": + request.headers.get("Access-Control-Request-Headers") ?? + "Content-Type, Accept, mcp-session-id, mcp-protocol-version", + "Access-Control-Expose-Headers": "mcp-session-id, mcp-protocol-version", + "Access-Control-Max-Age": "86400", + }); +} - getSDK() { - {{- if $globalSDK.Security }} - const getHeader = (name: string) => this.props?.[name] ?? '' - {{- end }} +function withCors(response: Response, request: Request): Response { + const headers = new Headers(response.headers); + corsHeaders(request).forEach((value, name) => headers.set(name, value)); + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); +} - 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; +// Retired session class; still exported so deployments created with it keep their Durable Object namespace. +export class {{ $mcpServerName }} { + fetch(request: Request): Response { + return sseRetiredResponse(new URL(request.url)); } } +const mcpHandler = createMcpHandler(({{ if $globalSDK.Security }}{ requestInfo }{{ end }}) => { + const { server } = createMCPServer({ + logger: createConsoleLogger('debug'), + getSDK: () => getSDK({{ if $globalSDK.Security }}requestInfo{{ end }}), + }); + return server; +}); 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; + if (url.pathname === "/mcp") { + if (request.method === "OPTIONS") { + return new Response(null, { status: 204, headers: corsHeaders(request) }); + } + return withCors(await mcpHandler.fetch(request), request); } - 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 - ); - } - - if (url.pathname === "/mcp") { - return {{ $mcpServerName }}.serve("/mcp", { binding: "{{sanitizeEnvVarName .Global.Config.PackageName}}_MCP" }).fetch( - request, - env, - ctx - ); + return withCors(sseRetiredResponse(url), request); } // Landing page 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..c72183ad8 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,25 @@ {{- recurse 1 -}} {{- $coreName := sanitizeCoreSDKName }} {{ genImports }} +{{- 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 "@modelcontextprotocol/server" "ServerContext" }} +{{- 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: ServerContext, ) => GetPromptResult | Promise; } : { @@ -31,7 +29,7 @@ export type PromptDefinition, + extra: ServerContext, ) => GetPromptResult | Promise; }; @@ -55,8 +53,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 +65,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, 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(), 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..502775dd8 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 @@ -5,19 +5,17 @@ {{- 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 "@modelcontextprotocol/server" "ServerContext" }} +{{- addImport "@modelcontextprotocol/server" "ReadResourceResult" }} export type ReadResourceCallback = ( client: {{ $coreName }}, uri: URL, - extra: RequestHandlerExtra, + extra: ServerContext, ) => ReadResourceResult | Promise; export type ResourceDefinition = { @@ -33,7 +31,7 @@ export type ReadResourceTemplateCallback = ( client: {{ $coreName }}, uri: URL, vars: Variables, - extra: RequestHandlerExtra, + extra: ServerContext, ) => ReadResourceResult | Promise; export type ResourceTemplateDefinition = { @@ -141,7 +139,7 @@ export function createRegisterResource( description: resource.description, }; - server.resource( + server.registerResource( resource.name, resource.resource, metadata, @@ -173,7 +171,7 @@ export function createRegisterResourceTemplate( description: resource.description, }; - server.resource( + server.registerResource( resource.name, resource.resource, metadata, 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..a9c3a099e 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,124 @@ import * as z from "zod"; +import type { StandardSchemaWithJSON } from "@modelcontextprotocol/server"; + +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..8c556644b 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,13 @@ {{- 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 "@modelcontextprotocol/server" "ServerContext" -}} +{{- addImport "@modelcontextprotocol/server" "CallToolResult" -}} {{- addImport "zod" "z" "aliasImport" -}} {{ genImports }} +{{- addImport "./shared.js" "mcpInputSchema" }} export type MCPToolAnnotationFilter = { readOnlyHint?: boolean; @@ -50,8 +47,8 @@ export function buildAnnotationFilter(annotations: string[] | undefined): MCPToo return filter; } -export type ToolDefinition = - Args extends ZodRawShapeCompat +export type ToolDefinition = + Args extends z.ZodRawShape ? { name: string; description: string; @@ -66,8 +63,8 @@ export type ToolDefinition, - extra: RequestHandlerExtra + args: z.output>, + extra: ServerContext ) => CallToolResult | Promise; } : { @@ -84,7 +81,7 @@ export type ToolDefinition + extra: ServerContext ) => CallToolResult | Promise; }; @@ -183,13 +180,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 +216,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,7 +229,7 @@ export function createRegisterTool( tool.name, { description: tool.description, - inputSchema: tool.args, + inputSchema: mcpInputSchema(tool.args), annotations: tool.annotations, }, async (args, ctx) => { @@ -262,7 +259,7 @@ export function createRegisterTool( function matchesSearchTerms( terms: string[], name: string, - def: ToolDefinition, + def: ToolDefinition, ): boolean { if (terms.length === 0) { return true; @@ -285,14 +282,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 +298,7 @@ export function registerDynamicTools( "If not provided, all tools are returned.", ].join("\n"), ), - }, + }), annotations: { title: "List Tools", readOnlyHint: true, @@ -331,9 +328,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 +355,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 +375,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 +399,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 { @@ -485,7 +482,10 @@ function resolveHeader( if (val != null) { return schema.parse(val); } - return disableStaticAuth ? undefined : schema.parse(cliFlagValue); + if (disableStaticAuth || cliFlagValue === undefined) { + return 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..f445f66a6 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 +# Retained so existing deployments keep their Durable Object namespace and stored state [[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..90c950456 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/.`, }, @@ -70,9 +70,31 @@ function getTemplateDependencies() { ecosystem: "npm", category: "runtime", }, - "@modelcontextprotocol/sdk": { - name: "@modelcontextprotocol/sdk", - version: "1.26.0", + "@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 +108,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 +141,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", diff --git a/templates/templates/mcp-typescript/includes/dependencies.ts b/templates/templates/mcp-typescript/includes/dependencies.ts index 1fd784f01..e84fec0a0 100644 --- a/templates/templates/mcp-typescript/includes/dependencies.ts +++ b/templates/templates/mcp-typescript/includes/dependencies.ts @@ -8,7 +8,11 @@ const deps = getTemplateDependencies(); 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 +26,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 +69,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/templating.ts b/templates/templates/mcp-typescript/includes/templating.ts index 41197bf04..7c3e50173 100644 --- a/templates/templates/mcp-typescript/includes/templating.ts +++ b/templates/templates/mcp-typescript/includes/templating.ts @@ -723,7 +723,7 @@ function addHeadersToConfigVSCode(config: any, headerEntries: any[]) { } function mcpRemoteConfigCommand( - endpointType: string = "sse", + endpointType: string = "mcp", clientType: string = "default", url?: string, ) { @@ -772,7 +772,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 +787,7 @@ function templateMcpIDEConfig(options: MCPIDEConfigOptions = {}) { const { location = "readme", format = "json", - endpoint = "sse", + endpoint = "mcp", executor = "npx", } = options; @@ -910,8 +910,8 @@ function mcpRemoteConfigJS() { let js = `{ "mcpServers": { "${mcpName()}": { - "type": "sse", - "url": \`\${o}/sse\``; + "type": "http", + "url": \`\${o}/mcp\``; if (headerEntries.length > 0) { js += `, @@ -1030,9 +1030,9 @@ function templateMcpCLICommand(options: MCPCLICommandOptions) { "mcp", "add", "--transport", - "sse", + "http", mcpName(), - `\${o}/sse`, + `\${o}/mcp`, ...headerArgs, ].join(" "); } @@ -1046,9 +1046,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..0c56c7ea1 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 { @@ -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,9 +97,10 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { }); }; - await mcpServer.connect(transport as Transport); - await transport.handleRequest(req, res, req.body); + return mcpServer; }); + const nodeHandler = toNodeHandler(handler); + app.all("/mcp", (req, res) => nodeHandler(req, res, req.body)); app.get("/", landingPageExpress); @@ -118,7 +110,7 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { logger.info("MCP Streamable HTTP server started", { host }); }); - const shutdown = () => { + const shutdown = async () => { logger.info("Shutting down HTTP server"); const timer = setTimeout(() => { @@ -126,6 +118,7 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { process.exit(1); }, 5000); + await handler.close(); httpServer.close(() => { clearTimeout(timer); logger.info("Graceful shutdown complete"); 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..36de6174b 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,12 +48,13 @@ 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 handle = serveStdio(() => { + const { server } = {{ templateString "mcp-server/create-mcp-server.ts.stmpl" .Local }}; + return server; + }); const abort = async () => { - await server.close(); + await handle.close(); process.exit(0); }; process.on("SIGTERM", abort); 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..b3d59ab7c 100644 --- a/templates/templates/mcp-typescript/package.json.stmpl +++ b/templates/templates/mcp-typescript/package.json.stmpl @@ -12,6 +12,9 @@ {{- end }} "type": "module", "sideEffects": false, + "engines": { + "node": "{{ if .Global.Config.GramEnabled }}>=22.18.0{{ else }}>=20{{ end }}" + }, {{- if .Global.Config.RepoURL }} "repository": { "type": "git", @@ -54,7 +57,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", diff --git a/templates/templates/mcp-typescript/readme.stmpl b/templates/templates/mcp-typescript/readme.stmpl index aa3c6a975..451182c53 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`. Each request is handled statelessly, so no session state is stored between requests. The legacy HTTP+SSE endpoint at `/sse` has been retired: requests to it receive HTTP `410 Gone` with a JSON body naming the `/mcp` endpoint. Point existing clients at `/mcp`. + +The Durable Object binding and its `v1` migration remain in `wrangler.toml` so that workers deployed with them keep deploying and keep their stored state. The worker no longer uses the namespace. Deleting it permanently destroys its data, so do it only deliberately: remove the binding, remove the exported class, and add a migration with `deleted_classes`. {{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..0ef9b9cc4 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,43 @@ 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"; + +const quietLogger = { level: "error" }; + +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 leaves unset optional settings undefined", () => { + const options = resolvedOptions({}, {}); + expect(options.security?.["accessToken"]).toBe(""); + expect(options.security?.["basicHttp"]).toBeUndefined(); + expect(options.globalHeaderParam).toBeUndefined(); +}); + +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/dynamic_tools_additional.test.ts b/templates/templates/mcp-typescript/tests/primary/dynamic_tools_additional.test.ts index abf52a802..a8603ef5a 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,1002 @@ +import { createServer } from "node:http"; +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 { 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 }); - }, - }; - - const stub = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "stub-result" }], - }); - - const argsShape = { request: z.object({ name: z.string() }) }; - const stubToolDef: ToolDefinition = { +function makeHarness(dynamic: boolean) { + 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" }] }); + tool({ 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, - }; - - const noArgsStub = vi.fn().mockResolvedValue({ - content: [{ type: "text", text: "no-args-result" }], + description: "Accept a nested request", + annotations, + args: { request: z.object({ name: z.string() }) }, + tool: stub, }); - - const noArgsToolDef: ToolDefinition = { + tool({ name: "no-args-tool", - description: "Tool that takes no arguments", - annotations: { - title: "NoArgs", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, + 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()), }, - // deno-lint-ignore no-explicit-any - tool: noArgsStub as any, - }; + 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 }; +} - // deno-lint-ignore no-explicit-any - const toolMap = new Map>(); - toolMap.set("stub-tool", stubToolDef); - toolMap.set("no-args-tool", noArgsToolDef); +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(); + } +} - 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 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", + }), + }, + ]); + }); + }); - return { handlers, stub, noArgsStub }; -} + 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" }]); + }); + }); +}); -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(); +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 result = await execute!.handler( - { name: "stub-tool", arguments: { request: { name: "Alice" } } }, - { signal: new AbortController().signal }, - ); + 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(); + }); + }); - expect(stub).toHaveBeenCalledTimes(1); - // tool(client, validatedInput, ctx) — validated input is second arg - expect(stub.mock.calls[0]?.[1]).toEqual({ request: { name: "Alice" } }); - expect(result.isError).toBeFalsy(); + 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", + }), + }, + ]); + }); }); +}); - test("rejects invalid inner arguments with a schema error", async () => { - const { handlers, stub } = makeHarness(); - const execute = handlers.get("execute_tool")!; +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(); + } + }, + ); - const result = await execute.handler( + 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(); + } + }, + ); +}); + +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(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([ { - name: "stub-tool", - arguments: { request: { name: 42 } }, + type: "text", + text: '{"request":{"amount":"12345678901234567890","amounts":["12345678901234567890"]}}', }, - { signal: new AbortController().signal }, - ); - - expect(stub).not.toHaveBeenCalled(); - expect(result.isError).toBe(true); - expect(result.content[0]?.text).toContain("Invalid input"); + ]); }); + 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("inputSchema advertises `name` and `arguments` meta-fields", () => { - const { handlers } = makeHarness(); - const { config } = handlers.get("execute_tool")!; - const shape = config.inputSchema as Record; - - expect(shape["name"]).toBeDefined(); - const argumentsSchema = shape["arguments"]; - expect(argumentsSchema).toBeDefined(); - - // `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" } }); +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); +}); - test("reports unknown tool name without invoking stub", async () => { - const { handlers, stub } = makeHarness(); - const execute = handlers.get("execute_tool")!; +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"'); +}); - const result = await execute.handler( - { name: "does-not-exist" }, - { signal: new AbortController().signal }, - ); +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(stub).not.toHaveBeenCalled(); - expect(result.isError).toBe(true); - expect(result.content[0]?.text).toContain("Unknown tool: does-not-exist"); +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(); - test("executes a no-args tool when `arguments` is omitted", async () => { - const { handlers, noArgsStub } = makeHarness(); - const execute = handlers.get("execute_tool")!; + 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" } }, + }); - const result = await execute.handler( - { name: "no-args-tool" }, - { signal: new AbortController().signal }, - ); + defaultValue.mockClear(); + expect(await schema["~standard"].validate({})).toEqual({ value: { amount } }); + expect(defaultValue).toHaveBeenCalledTimes(1); +}); - expect(noArgsStub).toHaveBeenCalledTimes(1); - expect(result.isError).toBeFalsy(); - expect(result.content[0]?.text).toBe("no-args-result"); +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(); + } }); -// 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 }); +test.each([false, true])( + "async tool validation preserves static/dynamic parity (dynamic=%s)", + async (dynamic) => { + const server = new McpServer({ name: "async-tool-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 handler = vi.fn((args: { count: number; name: string }) => ({ + content: [{ type: "text" as const, text: JSON.stringify(args) }], + })); + 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), + }); + if (dynamic) registerDynamicTools(logger, server, getSDK, toolMap, scopes); + 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.each([false, true])( + "BigInt catch fallbacks remain serializable and preserve runtime values (dynamic=%s)", + async (dynamic) => { + const server = new McpServer({ name: "catch-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 amount = 12345678901234567890n; + const fallback = vi.fn(() => ({ amount })); + const shape = { + request: z.object({ amount: bigint() }).catch(fallback), + bigintOnly: z.bigint().catch(amount), }; - - // 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, + 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) }], + }; }, - 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(), + }); + expect(fallback).not.toHaveBeenCalled(); + if (dynamic) registerDynamicTools(logger, server, getSDK, toolMap, scopes); + 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) }] }); + }); + }, +); + +test.each([false, true])( + "context-dependent catch factories preserve discovery and runtime fallback (dynamic=%s)", + async (dynamic) => { + const server = new McpServer({ + name: "context-catch-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, ); - - return { handlers }; - } - - 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 }, + const field = z.string().catch((ctx) => `fallback:${String(ctx.value)}`); + const shape = { value: field }; + tool({ + name: "context-catch", + description: "Recover using the invalid input", + annotations, + args: shape, + tool: (_sdk, args) => ({ content: [{ type: "text", text: args.value }] }), + }); + if (dynamic) registerDynamicTools(logger, server, getSDK, toolMap, scopes); + 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"); + }); + }, +); + +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), ); - - 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 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); + + 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, +); diff --git a/templates/templates/typescriptv2/config.ts b/templates/templates/typescriptv2/config.ts index 28263ac90..e99fb9b04 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,45 @@ function getTemplateDependencies() { category: "runtime", condition: "zodV4Mini", }, - "@modelcontextprotocol/sdk": { - name: "@modelcontextprotocol/sdk", - version: "^1.26.0", + "@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/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 || ^4.0.0", + 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..749c57776 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; } @@ -117,8 +121,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 +200,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..e0648c692 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,17 @@ {{- 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 "@modelcontextprotocol/server" "ServerContext" }} +{{- 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 +28,7 @@ export type PromptDefinition, - extra: RequestHandlerExtra, + extra: ServerContext, ) => GetPromptResult | Promise; } : { @@ -39,7 +38,7 @@ export type PromptDefinition, + extra: ServerContext, ) => GetPromptResult | Promise; }; @@ -75,27 +74,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, 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, 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..f70288fd2 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 @@ -7,19 +7,17 @@ {{- 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 "@modelcontextprotocol/server" "ServerContext" }} +{{- 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: ServerContext, ) => ReadResourceResult | Promise; export type ResourceDefinition = { @@ -35,7 +33,7 @@ export type ReadResourceTemplateCallback = ( client: {{$coreName}}, uri: URL, vars: Variables, - extra: RequestHandlerExtra, + extra: ServerContext, ) => ReadResourceResult | Promise; export type ResourceTemplateDefinition = { @@ -119,7 +117,7 @@ export function createRegisterResource( description: resource.description, }; - server.resource( + server.registerResource( resource.name, resource.resource, metadata, @@ -151,7 +149,7 @@ export function createRegisterResourceTemplate( description: resource.description, }; - server.resource( + server.registerResource( resource.name, resource.resource, metadata, 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..2ea02d200 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,32 @@ {{- genImports -}} {{- addZodPackageImport -}} {{- addImport "../lib/base64.js" "bytesToBase64" -}} +{{- addImport "zod-to-json-schema" "zodToJsonSchema" -}} +{{- addImport "@modelcontextprotocol/server" "StandardSchemaWithJSON" -}} + +export function mcpSchema( + shape: A, +): StandardSchemaWithJSON>, z.output>> { + const schema = z.object(shape); + const jsonSchema = (): Record => JSON.parse(JSON.stringify( + zodToJsonSchema(schema, { pipeStrategy: "input", strictUnions: true }), + (_key, value) => typeof value === "bigint" ? value.toString() : value, + )); + + return { + "~standard": { + version: 1, + vendor: "zod", + jsonSchema: { input: jsonSchema, output: jsonSchema }, + validate: async (value) => { + const result = await schema.safeParseAsync(value); + return result.success + ? { value: result.data } + : { issues: result.error.issues }; + }, + }, + }; +} 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..f066d32e3 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,13 @@ {{- 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 "@modelcontextprotocol/server" "ServerContext" }} +{{- addImport "@modelcontextprotocol/server" "McpServer" }} {{- addZodPackageImport "objectOutputType" }} {{- addZodPackageImport "ZodTypeAny" }} {{- addZodPackageImport "ZodRawShape" }} +{{- addImport "./shared.js" "mcpSchema" }} export type ToolDefinition = Args extends ZodRawShape @@ -26,7 +25,7 @@ export type ToolDefinition = tool: ( client: {{$coreName}}, args: objectOutputType, - extra: RequestHandlerExtra + extra: ServerContext ) => CallToolResult | Promise; } : { @@ -36,7 +35,7 @@ export type ToolDefinition = args?: undefined; tool: ( client: {{$coreName}}, - extra: RequestHandlerExtra + extra: ServerContext ) => CallToolResult | Promise; }; @@ -118,13 +117,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, 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, 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..440ba4f21 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,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 = {{templateString "mcp-server/create-mcp-server.ts.stmpl" .Local}}; - await server.connect(transport); + const server = serveStdio(() => {{templateString "mcp-server/create-mcp-server.ts.stmpl" .Local}}); const abort = async () => { await server.close(); 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/zSDKs/mcp-typescript/.speakeasy/gen.lock b/zSDKs/mcp-typescript/.speakeasy/gen.lock index 483a11aad..37570c0a7 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:b72f1651cacf0ee9b490b9640c0ce6ecec7de1be 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:735cd364a0f464eb9a4b45c753e2363b36febd9e 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:e0f0b4576efe5e3e782888935e662ec45382d8f0 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:d3918933a56c3a19df10097662efa73376544e95 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:efd248f6c2b17539c6c84fa7c1692515e6ca6dc2 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:2725797c8942b4be6e242cdfd035c1f95d023f7e src/mcp-server/resources.ts: - last_write_checksum: sha1:13dc124e896d588f80a3711f4e32fc170416552e + last_write_checksum: sha1:2503e1a4d6e63c1a7db29e371d8f38d778a17357 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:e285fbba7798f2cd4239bb90618969c7755dd032 src/mcp-server/tools.ts: - last_write_checksum: sha1:a29c85128ae9ee0aa1e04a03799df9c23734cf2a + last_write_checksum: sha1:5d51cd60639d02b2045fdf96e5742d820857cb2a 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:92b19ea633dc9ee4524a3d266411df305459eebf 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..a0cb9a134 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=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZW1vdGVAMC4xLjI1IiwiaHR0cHM6Ly9vcGVuYXBpLW1jcC1zZXJ2ZXIuZXhhbXBsZS53b3JrZXJzLmRldi9tY3AiLCItLWhlYWRlciIsInNlcnZlci1pbmRleDoke1NFUlZFUl9JTkRFWH0iLCItLWhlYWRlciIsInN1YmRvbWFpbjoke1NVQkRPTUFJTn0iLCItLWhlYWRlciIsImFwaS12ZXJzaW9uOiR7QVBJX1ZFUlNJT059IiwiLS1oZWFkZXIiLCJhcGktaG9zdC1uYW1lOiR7QVBJX0hPU1RfTkFNRX0iLCItLWhlYWRlciIsImFwaS1wb3J0OiR7QVBJX1BPUlR9IiwiLS1oZWFkZXIiLCJ1c2VybmFtZToke1VTRVJOQU1FfSIsIi0taGVhZGVyIiwicGFzc3dvcmQ6JHtQQVNTV09SRH0iLCItLWhlYWRlciIsImJlYXJlci1hdXRoOiR7QkVBUkVSX0FVVEh9IiwiLS1oZWFkZXIiLCJteS1hcGkta2V5OiR7TVlfQVBJX0tFWX0iLCItLWhlYWRlciIsIm9hdXRoMjoke09BVVRIMn0iLCItLWhlYWRlciIsImFwcC1pZDoke0FQUF9JRH0iLCItLWhlYWRlciIsInNlY3JldDoke1NFQ1JFVH0iLCItLWhlYWRlciIsIm1vYmlsZS1hdXRoOiR7TU9CSUxFX0FVVEh9IiwiLS1oZWFkZXIiLCJjbGllbnQtY3JlZGVudGlhbHM6JHtDTElFTlRfQ1JFREVOVElBTFN9IiwiLS1oZWFkZXIiLCJxdWVyeS1wYXJhbTE6JHtRVUVSWV9QQVJBTTF9IiwiLS1oZWFkZXIiLCJkZXByZWNhdGVkLXF1ZXJ5LXBhcmFtMToke0RFUFJFQ0FURURfUVVFUllfUEFSQU0xfSIsIi0taGVhZGVyIiwiZGVwcmVjYXRlZC1xdWVyeS1wYXJhbTI6JHtERVBSRUNBVEVEX1FVRVJZX1BBUkFNMn0iXX0=) 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=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZW1vdGVAMC4xLjI1IiwiaHR0cHM6Ly9vcGVuYXBpLW1jcC1zZXJ2ZXIuZXhhbXBsZS53b3JrZXJzLmRldi9tY3AiLCItLWhlYWRlciIsInNlcnZlci1pbmRleDoke1NFUlZFUl9JTkRFWH0iLCItLWhlYWRlciIsInN1YmRvbWFpbjoke1NVQkRPTUFJTn0iLCItLWhlYWRlciIsImFwaS12ZXJzaW9uOiR7QVBJX1ZFUlNJT059IiwiLS1oZWFkZXIiLCJhcGktaG9zdC1uYW1lOiR7QVBJX0hPU1RfTkFNRX0iLCItLWhlYWRlciIsImFwaS1wb3J0OiR7QVBJX1BPUlR9IiwiLS1oZWFkZXIiLCJ1c2VybmFtZToke1VTRVJOQU1FfSIsIi0taGVhZGVyIiwicGFzc3dvcmQ6JHtQQVNTV09SRH0iLCItLWhlYWRlciIsImJlYXJlci1hdXRoOiR7QkVBUkVSX0FVVEh9IiwiLS1oZWFkZXIiLCJteS1hcGkta2V5OiR7TVlfQVBJX0tFWX0iLCItLWhlYWRlciIsIm9hdXRoMjoke09BVVRIMn0iLCItLWhlYWRlciIsImFwcC1pZDoke0FQUF9JRH0iLCItLWhlYWRlciIsInNlY3JldDoke1NFQ1JFVH0iLCItLWhlYWRlciIsIm1vYmlsZS1hdXRoOiR7TU9CSUxFX0FVVEh9IiwiLS1oZWFkZXIiLCJjbGllbnQtY3JlZGVudGlhbHM6JHtDTElFTlRfQ1JFREVOVElBTFN9IiwiLS1oZWFkZXIiLCJxdWVyeS1wYXJhbTE6JHtRVUVSWV9QQVJBTTF9IiwiLS1oZWFkZXIiLCJkZXByZWNhdGVkLXF1ZXJ5LXBhcmFtMToke0RFUFJFQ0FURURfUVVFUllfUEFSQU0xfSIsIi0taGVhZGVyIiwiZGVwcmVjYXRlZC1xdWVyeS1wYXJhbTI6JHtERVBSRUNBVEVEX1FVRVJZX1BBUkFNMn0iXX0=) 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`. Each request is handled statelessly, so no session state is stored between requests. The legacy HTTP+SSE endpoint at `/sse` has been retired: requests to it receive HTTP `410 Gone` with a JSON body naming the `/mcp` endpoint. Point existing clients at `/mcp`. + +The Durable Object binding and its `v1` migration remain in `wrangler.toml` so that workers deployed with them keep deploying and keep their stored state. The worker no longer uses the namespace. Deleting it permanently destroys its data, so do it only deliberately: remove the binding, remove the exported class, and add a migration with `deleted_classes`. diff --git a/zSDKs/mcp-typescript/package-lock.json b/zSDKs/mcp-typescript/package-lock.json index f152e8194..a65bb2fe7 100644 --- a/zSDKs/mcp-typescript/package-lock.json +++ b/zSDKs/mcp-typescript/package-lock.json @@ -11,13 +11,15 @@ "@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/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", @@ -37,6 +39,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 +154,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 +175,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 +290,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 +1847,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,43 +1868,89 @@ "@logtape/logtape": "^1.3.7" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", - "license": "MIT", + "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", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", + "hono": "^4.11.4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.3.0" + } + }, + "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", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" + "zod": "^4.2.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" + "express": "^4.18.0 || ^5.0.0" }, "peerDependenciesMeta": { - "@cfworker/json-schema": { + "express": { "optional": true - }, - "zod": { - "optional": false } } }, @@ -2683,12 +2657,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 +3139,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", @@ -3235,39 +3157,6 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -3348,6 +3237,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 +3639,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 +3730,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 +3778,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 +3980,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 +4206,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 +4246,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" @@ -4595,6 +4357,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -4617,26 +4380,11 @@ "dev": true, "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==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fdir": { "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 +4579,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 +4869,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 +4888,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 +4951,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 +4962,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,41 +4994,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", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -5512,43 +5201,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 +5235,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 +5260,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 +5425,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 +5497,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 +5573,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 +5664,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", @@ -6130,15 +5710,6 @@ "node": ">=10" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -6693,6 +6264,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 +6411,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 +7481,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", @@ -8085,22 +7573,12 @@ "version": "3.25.2", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, "license": "ISC", "peerDependencies": { "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..9e4f3ed7e 100644 --- a/zSDKs/mcp-typescript/package.json +++ b/zSDKs/mcp-typescript/package.json @@ -5,6 +5,9 @@ "author": "Speakeasy", "type": "module", "sideEffects": false, + "engines": { + "node": ">=22.18.0" + }, "repository": { "type": "git", "url": "https://github.com/speakeasy-sdks/test-sdk" @@ -29,16 +32,17 @@ "@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/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", diff --git a/zSDKs/mcp-typescript/src/cloudflare-worker/cloudflare-worker.ts b/zSDKs/mcp-typescript/src/cloudflare-worker/cloudflare-worker.ts index 2bcf184bd..a86681fa5 100644 --- a/zSDKs/mcp-typescript/src/cloudflare-worker/cloudflare-worker.ts +++ b/zSDKs/mcp-typescript/src/cloudflare-worker/cloudflare-worker.ts @@ -2,95 +2,114 @@ * 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 } from "@modelcontextprotocol/server"; 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(request: Request | undefined) { + const getHeader = (name: string) => request?.headers.get(name) ?? ""; + + 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"), + }, + }), + }); +} -type Props = Record; +function sseRetiredResponse(url: URL): Response { + return Response.json( + { + error: + "The HTTP+SSE transport is no longer served. Connect to the Streamable HTTP endpoint instead.", + endpoint: new URL("/mcp", url).toString(), + }, + { status: 410 }, + ); +} -export class OpenapiMCP extends McpAgent { - server!: McpServer; +function corsHeaders(request: Request): Headers { + return new Headers({ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", + "Access-Control-Allow-Headers": + request.headers.get("Access-Control-Request-Headers") + ?? "Content-Type, Accept, mcp-session-id, mcp-protocol-version", + "Access-Control-Expose-Headers": "mcp-session-id, mcp-protocol-version", + "Access-Control-Max-Age": "86400", + }); +} - async init() { - const { server } = createMCPServer({ - logger: createConsoleLogger("debug"), - getSDK: () => this.getSDK(), - }); +function withCors(response: Response, request: Request): Response { + const headers = new Headers(response.headers); + corsHeaders(request).forEach((value, name) => headers.set(name, value)); + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); +} - this.server = server; - } - - getSDK() { - const getHeader = (name: string) => this.props?.[name] ?? ""; - - const sdk = 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"), - }, - }), - }); - return sdk; +// Retired session class; still exported so deployments created with it keep their Durable Object namespace. +export class OpenapiMCP { + fetch(request: Request): Response { + return sseRetiredResponse(new URL(request.url)); } } +const mcpHandler = createMcpHandler(({ requestInfo }) => { + const { server } = createMCPServer({ + logger: createConsoleLogger("debug"), + getSDK: () => getSDK(requestInfo), + }); + return server; +}); + 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; + if (url.pathname === "/mcp") { + if (request.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: corsHeaders(request), + }); + } + return withCors(await mcpHandler.fetch(request), request); } - 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 OpenapiMCP.serve("/mcp", { binding: "OPENAPI_MCP" }).fetch( - request, - env, - ctx, - ); + return withCors(sseRetiredResponse(url), request); } // Landing page diff --git a/zSDKs/mcp-typescript/src/landing-page.ts b/zSDKs/mcp-typescript/src/landing-page.ts index 6aa004f4d..06a5aaadb 100644 --- a/zSDKs/mcp-typescript/src/landing-page.ts +++ b/zSDKs/mcp-typescript/src/landing-page.ts @@ -35,7 +35,7 @@ export function landingPageHTML(origin: string): string { "args": [ "-y", "mcp-remote@0.1.25", - `${o}/sse`, + `${o}/mcp`, "--header", "server-index:${SERVER_INDEX}", "--header", @@ -92,7 +92,7 @@ export function landingPageHTML(origin: string): string { }, }; const codexConfig = `[mcp_servers.SDK] -url = "${o}/sse" +url = "${o}/mcp" 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" }`; return ` @@ -731,7 +731,7 @@ http_headers = { "server-index" = "YOUR_SERVER_INDEX", "subdomain" = "YOUR_SUBDO - +
@@ -964,7 +964,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 http SDK https://openapi-mcp-server.example.workers.dev/mcp
@@ -1015,7 +1015,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 http SDK https://openapi-mcp-server.example.workers.dev/mcp 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..5e4c45d94 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 { @@ -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,9 +79,10 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { }); }; - await mcpServer.connect(transport as Transport); - await transport.handleRequest(req, res, req.body); + return mcpServer; }); + const nodeHandler = toNodeHandler(handler); + app.all("/mcp", (req, res) => nodeHandler(req, res, req.body)); app.get("/", landingPageExpress); @@ -100,7 +92,7 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { logger.info("MCP Streamable HTTP server started", { host }); }); - const shutdown = () => { + const shutdown = async () => { logger.info("Shutting down HTTP server"); const timer = setTimeout(() => { @@ -108,6 +100,7 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { process.exit(1); }, 5000); + await handler.close(); httpServer.close(() => { clearTimeout(timer); logger.info("Graceful shutdown complete"); 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..7f7de401d 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,42 +48,45 @@ 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({ - logger, - allowedTools: flags.tool, - dynamic: flags.mode === "dynamic", - annotationFilter: buildAnnotationFilter(flags["tool-annotations"]), - security: { - UserPassAuth: allRequired({ - username: flags.username, - password: flags.password, - }), - Option2: allRequired({ - bearerAuth: flags["bearer-auth"], - MyApiKey: flags["my-api-key"], - }), - Option3: allRequired({ oauth2: flags.oauth2 }), - Option4: allRequired({ appId: flags["app-id"], secret: flags.secret }), - Option5: allRequired({ mobileAuth: flags["mobile-auth"] }), - Option6: allRequired({ clientCredentials: flags["client-credentials"] }), - MyApiKey: allRequired({ MyApiKey: flags["my-api-key"] }), - }, - queryParam1: flags["query-param1"], - deprecatedQueryParam1: flags["deprecated-query-param1"], - deprecatedQueryParam2: flags["deprecated-query-param2"], - loneQueryParam: flags["lone-query-param"], - serverURL: flags["server-url"], - serverIdx: flags["server-index"], - subdomain: flags.subdomain, - version: flags["api-version"], - HostName: flags["api-host-name"], - PORT: flags["api-port"], + const handle = serveStdio(() => { + const { server } = createMCPServer({ + logger, + allowedTools: flags.tool, + dynamic: flags.mode === "dynamic", + annotationFilter: buildAnnotationFilter(flags["tool-annotations"]), + security: { + UserPassAuth: allRequired({ + username: flags.username, + password: flags.password, + }), + Option2: allRequired({ + bearerAuth: flags["bearer-auth"], + MyApiKey: flags["my-api-key"], + }), + Option3: allRequired({ oauth2: flags.oauth2 }), + Option4: allRequired({ appId: flags["app-id"], secret: flags.secret }), + Option5: allRequired({ mobileAuth: flags["mobile-auth"] }), + Option6: allRequired({ + clientCredentials: flags["client-credentials"], + }), + MyApiKey: allRequired({ MyApiKey: flags["my-api-key"] }), + }, + queryParam1: flags["query-param1"], + deprecatedQueryParam1: flags["deprecated-query-param1"], + deprecatedQueryParam2: flags["deprecated-query-param2"], + loneQueryParam: flags["lone-query-param"], + serverURL: flags["server-url"], + serverIdx: flags["server-index"], + subdomain: flags.subdomain, + version: flags["api-version"], + HostName: flags["api-host-name"], + PORT: flags["api-port"], + }); + return server; }); - await server.connect(transport); const abort = async () => { - await server.close(); + await handle.close(); process.exit(0); }; process.on("SIGTERM", abort); 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..6e52f1fbd 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"; + McpServer, + ServerContext, +} 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 } 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: ServerContext, ) => GetPromptResult | Promise; } : { @@ -37,7 +33,7 @@ export type PromptDefinition< args?: undefined; prompt: ( client: SDKCore, - extra: RequestHandlerExtra, + extra: ServerContext, ) => 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,18 @@ 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, 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(), 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..2bbd47e52 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/resources.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/resources.ts @@ -4,16 +4,12 @@ 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"; + ServerContext, + Variables, +} from "@modelcontextprotocol/server"; import { SDKCore } from "../core.js"; import { ConsoleLogger } from "./console-logger.js"; import { MCPScope } from "./scopes.js"; @@ -22,7 +18,7 @@ import { valueToBase64 } from "./shared.js"; export type ReadResourceCallback = ( client: SDKCore, uri: URL, - extra: RequestHandlerExtra, + extra: ServerContext, ) => ReadResourceResult | Promise; export type ResourceDefinition = { @@ -38,7 +34,7 @@ export type ReadResourceTemplateCallback = ( client: SDKCore, uri: URL, vars: Variables, - extra: RequestHandlerExtra, + extra: ServerContext, ) => ReadResourceResult | Promise; export type ResourceTemplateDefinition = { @@ -94,7 +90,7 @@ export function createRegisterResource( description: resource.description, }; - server.resource( + server.registerResource( resource.name, resource.resource, metadata, @@ -129,7 +125,7 @@ export function createRegisterResourceTemplate( description: resource.description, }; - server.resource( + server.registerResource( resource.name, resource.resource, metadata, 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..405d919c3 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/shared.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/shared.ts @@ -2,8 +2,161 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ +import type { StandardSchemaWithJSON } from "@modelcontextprotocol/server"; import * as z from "zod"; +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..25fcb4a67 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools.ts @@ -2,17 +2,11 @@ * 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"; + McpServer, + ServerContext, +} from "@modelcontextprotocol/server"; import * as z from "zod"; import { SDKCore } from "../core.js"; import { ServerPORT$zodSchema } from "../lib/config.js"; @@ -20,7 +14,7 @@ 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, valueToBase64 } from "./shared.js"; export type MCPToolAnnotationFilter = { readOnlyHint?: boolean; @@ -66,43 +60,42 @@ export function buildAnnotationFilter( return filter; } -export type ToolDefinition< - Args extends undefined | ZodRawShapeCompat = undefined, -> = Args extends ZodRawShapeCompat ? { - name: string; - description: string; - scopes?: MCPScope[]; - args: Args; - annotations: { - title: string; - destructiveHint: boolean; - idempotentHint: boolean; - openWorldHint: boolean; - readOnlyHint: boolean; - }; - tool: ( - client: SDKCore, - args: ShapeOutput, - extra: RequestHandlerExtra, - ) => CallToolResult | Promise; - } - : { - name: string; - description: string; - scopes?: MCPScope[]; - args?: undefined; - annotations: { - title: string; - destructiveHint: boolean; - idempotentHint: boolean; - openWorldHint: boolean; - readOnlyHint: boolean; +export type ToolDefinition = + Args extends z.ZodRawShape ? { + name: string; + description: string; + scopes?: MCPScope[]; + args: Args; + annotations: { + title: string; + destructiveHint: boolean; + idempotentHint: boolean; + openWorldHint: boolean; + readOnlyHint: boolean; + }; + tool: ( + client: SDKCore, + args: z.output>, + extra: ServerContext, + ) => CallToolResult | Promise; + } + : { + name: string; + description: string; + scopes?: MCPScope[]; + args?: undefined; + annotations: { + title: string; + destructiveHint: boolean; + idempotentHint: boolean; + openWorldHint: boolean; + readOnlyHint: boolean; + }; + tool: ( + client: SDKCore, + extra: ServerContext, + ) => CallToolResult | Promise; }; - tool: ( - client: SDKCore, - extra: RequestHandlerExtra, - ) => CallToolResult | Promise; - }; // Optional function to assist with formatting tool results export async function formatResult( @@ -138,16 +131,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 +184,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,7 +197,7 @@ export function createRegisterTool( tool.name, { description: tool.description, - inputSchema: tool.args, + inputSchema: mcpInputSchema(tool.args), annotations: tool.annotations, }, async (args, ctx) => { @@ -240,7 +227,7 @@ export function createRegisterTool( function matchesSearchTerms( terms: string[], name: string, - def: ToolDefinition, + def: ToolDefinition, ): boolean { if (terms.length === 0) { return true; @@ -263,14 +250,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 +266,7 @@ export function registerDynamicTools( "If not provided, all tools are returned.", ].join("\n"), ), - }, + }), annotations: { title: "List Tools", readOnlyHint: true, @@ -314,11 +301,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 +330,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 +352,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 +376,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 { @@ -465,7 +451,10 @@ function resolveHeader( if (val != null) { return schema.parse(val); } - return disableStaticAuth ? undefined : schema.parse(cliFlagValue); + if (disableStaticAuth || cliFlagValue === undefined) { + return 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..202dc66b6 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 +# Retained so existing deployments keep their Durable Object namespace and stored state [[durable_objects.bindings]] name = "OPENAPI_MCP" class_name = "OpenapiMCP" -# Durable Object migration [[migrations]] tag = "v1" new_classes = ["OpenapiMCP"] From 6dd9987c28f94675d8a057dc0927d7d7ed2ba9a8 Mon Sep 17 00:00:00 2001 From: Thomas Rooney Date: Wed, 7 Oct 2026 16:24:54 +0100 Subject: [PATCH 2/6] fix(mcp): preserve legacy clients and extensions Keep OAuth grant credentials authoritative and isolate concurrent session requests. Preserve legacy transports and extension interfaces while generated servers move to the v2 SDK. --- ...a3eff565.yaml => 1791385688-ba585974.yaml} | 4 +- templates/perms.go | 1 + .../cloudflare-worker.ts.stmpl | 365 ++++++++++++-- .../auxiliary/src/landing-page.ts.stmpl | 37 +- .../auxiliary/src/mcp-server/prompts.ts.stmpl | 11 +- .../src/mcp-server/resources.ts.stmpl | 30 +- .../auxiliary/src/mcp-server/shared.ts.stmpl | 27 +- .../auxiliary/src/mcp-server/tools.ts.stmpl | 18 +- .../auxiliary/wrangler.toml.stmpl | 2 +- templates/templates/mcp-typescript/config.ts | 9 +- .../mcp-typescript/includes/dependencies.ts | 1 + .../mcp-typescript/includes/templating.ts | 32 +- .../mcp-server/cli/serve/impl.ts.stmpl | 49 +- .../templates/mcp-typescript/readme.stmpl | 2 +- .../primary/auth_additional.test.ts.stmpl | 27 +- .../primary/compatibility_additional.test.ts | 204 ++++++++ .../primary/dynamic_tools_additional.test.ts | 380 ++++++++++----- zSDKs/mcp-typescript/.speakeasy/gen.lock | 18 +- zSDKs/mcp-typescript/README.md | 2 +- zSDKs/mcp-typescript/package-lock.json | 115 ++++- zSDKs/mcp-typescript/package.json | 3 +- .../cloudflare-worker/cloudflare-worker.ts | 455 ++++++++++++++++-- zSDKs/mcp-typescript/src/landing-page.ts | 60 ++- .../src/mcp-server/cli/serve/impl.ts | 55 ++- .../mcp-typescript/src/mcp-server/prompts.ts | 35 +- .../src/mcp-server/resources.ts | 35 +- zSDKs/mcp-typescript/src/mcp-server/shared.ts | 50 +- zSDKs/mcp-typescript/src/mcp-server/tools.ts | 28 +- zSDKs/mcp-typescript/wrangler.toml | 2 +- 29 files changed, 1724 insertions(+), 333 deletions(-) rename .changesets/{1791371503-a3eff565.yaml => 1791385688-ba585974.yaml} (72%) create mode 100644 templates/templates/mcp-typescript/tests/primary/compatibility_additional.test.ts diff --git a/.changesets/1791371503-a3eff565.yaml b/.changesets/1791385688-ba585974.yaml similarity index 72% rename from .changesets/1791371503-a3eff565.yaml rename to .changesets/1791385688-ba585974.yaml index c38ea217e..513782174 100644 --- a/.changesets/1791371503-a3eff565.yaml +++ b/.changesets/1791385688-ba585974.yaml @@ -1,10 +1,10 @@ -id: 1791371503-a3eff565 +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 serve Cloudflare over stateless HTTP +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..769059cda 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, 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 487554f8e..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,15 +3,26 @@ {{- $globalSDK := .Global.AST.MainSDK -}} {{- $coreName := sanitizeCoreSDKName }} {{- $mcpServerName := print (sanitizeClassName .Global.Config.PackageName) "MCP" }} -import { createMcpHandler } from "@modelcontextprotocol/server"; +{{- $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 { landingPage } from "../landing-page.js" -function getSDK({{ if $globalSDK.Security }}request: Request | undefined{{ end }}) { +function getSDK({{ if $globalSDK.Security }}headers: Headers | undefined{{ end }}) { {{- if $globalSDK.Security }} - const getHeader = (name: string) => request?.headers.get(name) ?? '' + const getHeader = (name: string) => headers?.get(name) ?? '' {{- end }} return new {{ $coreName }}({ @@ -26,31 +37,76 @@ function getSDK({{ if $globalSDK.Security }}request: Request | undefined{{ end } }); } -function sseRetiredResponse(url: URL): Response { - return Response.json( - { - error: "The HTTP+SSE transport is no longer served. Connect to the Streamable HTTP endpoint instead.", - endpoint: new URL("/mcp", url).toString(), - }, - { status: 410 }, - ); +function createServer({{ if $globalSDK.Security }}getHeaders: () => Headers | undefined{{ end }}) { + const { server } = createMCPServer({ + logger: createConsoleLogger('debug'), + getSDK: () => getSDK({{ if $globalSDK.Security }}getHeaders(){{ end }}), + }); + return server; +} + +const PROPS_HEADER = "x-mcp-worker-props"; +{{- if $globalSDK.Security }} + +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; } +{{- end }} + +const modernHandler = createMcpHandler( + ({{ if $globalSDK.Security }}{ requestInfo }{{ end }}) => createServer({{ if $globalSDK.Security }}() => credentialHeaders(requestInfo){{ end }}), + { legacy: "reject" }, +); + +const statelessLegacyHandler = legacyStatelessFallback( + ({{ if $globalSDK.Security }}{ requestInfo }{{ end }}) => createServer({{ if $globalSDK.Security }}() => credentialHeaders(requestInfo){{ end }}), +); -function corsHeaders(request: Request): Headers { +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": "*", - "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", + "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": "mcp-session-id, mcp-protocol-version", - "Access-Control-Max-Age": "86400", + "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): Response { +function withCors(response: Response, request: Request, cors?: CorsOptions): Response { const headers = new Headers(response.headers); - corsHeaders(request).forEach((value, name) => headers.set(name, value)); + corsHeaders(request, cors).forEach((value, name) => headers.set(name, value)); return new Response(response.body, { status: response.status, statusText: response.statusText, @@ -58,34 +114,275 @@ function withCors(response: Response, request: Request): Response { }); } -// Retired session class; still exported so deployments created with it keep their Durable Object namespace. -export class {{ $mcpServerName }} { - fetch(request: Request): Response { - return sseRetiredResponse(new URL(request.url)); +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; } } -const mcpHandler = createMcpHandler(({{ if $globalSDK.Security }}{ requestInfo }{{ end }}) => { - const { server } = createMCPServer({ - logger: createConsoleLogger('debug'), - getSDK: () => getSDK({{ if $globalSDK.Security }}requestInfo{{ end }}), +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; +} + +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); +} + +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(); }); - return server; -}); + 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; + } + 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") { - if (request.method === "OPTIONS") { - return new Response(null, { status: 204, headers: corsHeaders(request) }); - } - return withCors(await mcpHandler.fetch(request), request); + return handleStreamableHttp(request, env.{{ $sessionBinding }}, undefined); } - if (url.pathname === "/sse" || url.pathname.startsWith("/sse/")) { - return withCors(sseRetiredResponse(url), request); + 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..873b4c215 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,41 @@ 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"), { 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"): 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 mcpConfig = {{ templateMcpIDEConfig (dict "location" "landingpage" "format" "js" "endpoint" "${endpoint}") }} + 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(", ")} }` + : `{{ templateMcpIDEConfig (dict "location" "landingpage" "format" "toml" "endpoint" "${endpoint}") }}` + const encodedConfig = encodeURIComponent(btoa(Array.from(new TextEncoder().encode(JSON.stringify(mcpConfig)), byte => String.fromCharCode(byte)).join(""))); return ` @@ -663,7 +678,7 @@ export function landingPageHTML(origin: string): string { - +
@@ -888,7 +903,7 @@ export function landingPageHTML(origin: string): string { - {{ templateMcpCLICommand (dict "client" "claude" "location" "landingpage") }} + {{ templateMcpCLICommand (dict "client" "claude" "location" "landingpage" "format" "html") }}
@@ -939,7 +954,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/prompts.ts.stmpl b/templates/templates/mcp-typescript/auxiliary/src/mcp-server/prompts.ts.stmpl index c72183ad8..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,13 +1,14 @@ {{- 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/server" "McpServer" }} -{{- addImport "@modelcontextprotocol/server" "ServerContext" }} +{{- addImport "./shared.js" "MCPServerContext" }} {{- addImport "@modelcontextprotocol/server" "GetPromptResult" }} export type PromptDefinition = @@ -19,7 +20,7 @@ export type PromptDefinition prompt: ( client: {{ $coreName }}, args: z.output>, - extra: ServerContext, + extra: MCPServerContext, ) => GetPromptResult | Promise; } : { @@ -29,7 +30,7 @@ export type PromptDefinition args?: undefined; prompt: ( client: {{ $coreName }}, - extra: ServerContext, + extra: MCPServerContext, ) => GetPromptResult | Promise; }; @@ -68,11 +69,11 @@ export function createRegisterPrompt( server.registerPrompt(prompt.name, { ...(prompt.description === undefined ? {} : { description: prompt.description }), argsSchema: { ...mcpInputSchema(prompt.args), shape: prompt.args }, - }, async (args, ctx) => prompt.prompt(getSDK(), args, ctx)); + }, async (args, ctx) => prompt.prompt(getSDK(), args, mcpServerContext(ctx))); } else { server.registerPrompt(prompt.name, { ...(prompt.description === undefined ? {} : { description: prompt.description }), - }, async (ctx) => prompt.prompt(getSDK(), ctx)); + }, 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 502775dd8..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,6 +1,8 @@ {{- 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" }} @@ -9,13 +11,13 @@ {{- addImport "@modelcontextprotocol/server" "ResourceMetadata" }} {{- addImport "@modelcontextprotocol/server" "ResourceTemplate" }} {{- addImport "@modelcontextprotocol/server" "Variables" }} -{{- addImport "@modelcontextprotocol/server" "ServerContext" }} +{{- addImport "./shared.js" "MCPServerContext" }} {{- addImport "@modelcontextprotocol/server" "ReadResourceResult" }} export type ReadResourceCallback = ( client: {{ $coreName }}, uri: URL, - extra: ServerContext, + extra: MCPServerContext, ) => ReadResourceResult | Promise; export type ResourceDefinition = { @@ -31,7 +33,7 @@ export type ReadResourceTemplateCallback = ( client: {{ $coreName }}, uri: URL, vars: Variables, - extra: ServerContext, + extra: MCPServerContext, ) => ReadResourceResult | Promise; export type ResourceTemplateDefinition = { @@ -39,7 +41,7 @@ export type ResourceTemplateDefinition = { description: string; metadata?: ResourceMetadata; scopes?: MCPScope[]; - resource: ResourceTemplate; + resource: ResourceTemplate | LegacyResourceTemplate; read: ReadResourceTemplateCallback; }; @@ -143,7 +145,7 @@ export function createRegisterResource( 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,11 +175,25 @@ export function createRegisterResourceTemplate( 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 a9c3a099e..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,5 +1,30 @@ import * as z from "zod"; -import type { StandardSchemaWithJSON } from "@modelcontextprotocol/server"; +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); 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 8c556644b..4426a0a11 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 @@ -7,11 +7,12 @@ {{- addImport "./console-logger.js" "ConsoleLogger" -}} {{- addImport "../core.js" $coreName -}} {{- addImport "@modelcontextprotocol/server" "McpServer" -}} -{{- addImport "@modelcontextprotocol/server" "ServerContext" -}} +{{- addImport "./shared.js" "MCPServerContext" -}} {{- addImport "@modelcontextprotocol/server" "CallToolResult" -}} {{- addImport "zod" "z" "aliasImport" -}} {{ genImports }} +{{- addImport "./shared.js" "mcpServerContext" }} {{- addImport "./shared.js" "mcpInputSchema" }} export type MCPToolAnnotationFilter = { @@ -64,7 +65,7 @@ export type ToolDefinition = tool: ( client: {{ $coreName }}, args: z.output>, - extra: ServerContext + extra: MCPServerContext ) => CallToolResult | Promise; } : { @@ -81,7 +82,7 @@ export type ToolDefinition = } tool: ( client: {{ $coreName }}, - extra: ServerContext + extra: MCPServerContext ) => CallToolResult | Promise; }; @@ -233,7 +234,7 @@ export function createRegisterTool( annotations: tool.annotations, }, async (args, ctx) => { - return tool.tool(getSDK(), args, ctx); + return tool.tool(getSDK(), args, mcpServerContext(ctx)); }, ); } else { @@ -244,7 +245,7 @@ export function createRegisterTool( annotations: tool.annotations, }, async (ctx) => { - return tool.tool(getSDK(), ctx); + return tool.tool(getSDK(), mcpServerContext(ctx)); }, ); } @@ -418,9 +419,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); @@ -483,7 +484,8 @@ function resolveHeader( return schema.parse(val); } if (disableStaticAuth || cliFlagValue === undefined) { - return undefined; + const parsed = schema.safeParse(undefined); + return parsed.success ? parsed.data : undefined; } return schema.parse(cliFlagValue); } diff --git a/templates/templates/mcp-typescript/auxiliary/wrangler.toml.stmpl b/templates/templates/mcp-typescript/auxiliary/wrangler.toml.stmpl index f445f66a6..cb1fd50ed 100644 --- a/templates/templates/mcp-typescript/auxiliary/wrangler.toml.stmpl +++ b/templates/templates/mcp-typescript/auxiliary/wrangler.toml.stmpl @@ -8,7 +8,7 @@ compatibility_date = "2025-07-15" # Enable node compatibility for MCP SDK compatibility_flags = ["nodejs_compat", "enable_request_signal"] -# Retained so existing deployments keep their Durable Object namespace and stored state +# 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" diff --git a/templates/templates/mcp-typescript/config.ts b/templates/templates/mcp-typescript/config.ts index 90c950456..0e3a33d70 100644 --- a/templates/templates/mcp-typescript/config.ts +++ b/templates/templates/mcp-typescript/config.ts @@ -70,6 +70,13 @@ function getTemplateDependencies() { ecosystem: "npm", category: "runtime", }, + "@modelcontextprotocol/sdk": { + name: "@modelcontextprotocol/sdk", + 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", @@ -230,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 e84fec0a0..9aa030254 100644 --- a/templates/templates/mcp-typescript/includes/dependencies.ts +++ b/templates/templates/mcp-typescript/includes/dependencies.ts @@ -8,6 +8,7 @@ const deps = getTemplateDependencies(); function templateDependencies(): string { const defaultDependencies = { express: deps.express.version, + "@modelcontextprotocol/sdk": deps["@modelcontextprotocol/sdk"].version, "@modelcontextprotocol/server": deps["@modelcontextprotocol/server"].version, "@modelcontextprotocol/server-legacy": diff --git a/templates/templates/mcp-typescript/includes/templating.ts b/templates/templates/mcp-typescript/includes/templating.ts index 7c3e50173..dbc03fa79 100644 --- a/templates/templates/mcp-typescript/includes/templating.ts +++ b/templates/templates/mcp-typescript/includes/templating.ts @@ -946,12 +946,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 +971,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}`; } @@ -1030,9 +1034,9 @@ function templateMcpCLICommand(options: MCPCLICommandOptions) { "mcp", "add", "--transport", - "http", + "${transport}", mcpName(), - `\${o}/mcp`, + `\${o}/\${endpoint}`, ...headerArgs, ].join(" "); } 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 0c56c7ea1..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 @@ -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); @@ -100,9 +100,34 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { return mcpServer; }); const nodeHandler = toNodeHandler(handler); - app.all("/mcp", (req, res) => nodeHandler(req, res, req.body)); + 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(); @@ -111,6 +136,10 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { }); const shutdown = async () => { + if (closing) { + process.exit(1); + } + closing = true; logger.info("Shutting down HTTP server"); const timer = setTimeout(() => { @@ -118,12 +147,14 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { process.exit(1); }, 5000); + const stopped = new Promise((resolve) => httpServer.close(() => resolve())); + await Promise.all(activeResponses); await handler.close(); - httpServer.close(() => { - clearTimeout(timer); - logger.info("Graceful shutdown complete"); - process.exit(0); - }); + 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/readme.stmpl b/templates/templates/mcp-typescript/readme.stmpl index 451182c53..f5af8c0cf 100644 --- a/templates/templates/mcp-typescript/readme.stmpl +++ b/templates/templates/mcp-typescript/readme.stmpl @@ -56,7 +56,7 @@ Then install with Claude Code CLI: {{ templateMcpCLICommand (dict "client" "claude" "cloudflareURL" "http://localhost:8787") }} ``` -The worker serves MCP over Streamable HTTP at `/mcp`. Each request is handled statelessly, so no session state is stored between requests. The legacy HTTP+SSE endpoint at `/sse` has been retired: requests to it receive HTTP `410 Gone` with a JSON body naming the `/mcp` endpoint. Point existing clients at `/mcp`. +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. The Durable Object binding and its `v1` migration remain in `wrangler.toml` so that workers deployed with them keep deploying and keep their stored state. The worker no longer uses the namespace. Deleting it permanently destroys its data, so do it only deliberately: remove the binding, remove the exported class, and add a migration with `deleted_classes`. 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 0ef9b9cc4..ee893a5d3 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 @@ -17,13 +17,38 @@ function resolvedOptions(headers: Record, flags: Partial; globalHeaderParam?: boolean }; } -test("MCP server leaves unset optional settings undefined", () => { +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"); 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 a8603ef5a..80f84fce0 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,4 +1,5 @@ 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"; @@ -18,6 +19,7 @@ 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 { @@ -38,7 +40,48 @@ const annotations = { openWorldHint: false, }; -function makeHarness(dynamic: boolean) { +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(); @@ -57,60 +100,64 @@ function makeHarness(dynamic: boolean) { const noArgsStub = vi .fn() .mockResolvedValue({ content: [{ type: "text", text: "no-args-result" }] }); - 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 (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 }; } @@ -654,32 +701,21 @@ test("prompt completion preserves input validation, transforms, and defaults", a test.each([false, true])( "async tool validation preserves static/dynamic parity (dynamic=%s)", async (dynamic) => { - const server = new McpServer({ name: "async-tool-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 handler = vi.fn((args: { count: number; name: string }) => ({ content: [{ type: "text" as const, text: JSON.stringify(args) }], })); - 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), + 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), + }); }); - if (dynamic) registerDynamicTools(logger, server, getSDK, toolMap, scopes); await withClient(server, async (client) => { const call = (count: number) => client.callTool({ @@ -699,38 +735,27 @@ test.each([false, true])( test.each([false, true])( "BigInt catch fallbacks remain serializable and preserve runtime values (dynamic=%s)", async (dynamic) => { - const server = new McpServer({ name: "catch-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 amount = 12345678901234567890n; const fallback = vi.fn(() => ({ amount })); const shape = { request: z.object({ amount: bigint() }).catch(fallback), bigintOnly: z.bigint().catch(amount), }; - 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) }], - }; - }, + 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(); }); - expect(fallback).not.toHaveBeenCalled(); - if (dynamic) registerDynamicTools(logger, server, getSDK, toolMap, scopes); await withClient(server, async (client) => { const json = mcpInputSchema(shape)["~standard"].jsonSchema.input({ target: "draft-2020-12", @@ -771,31 +796,19 @@ test.each([false, true])( test.each([false, true])( "context-dependent catch factories preserve discovery and runtime fallback (dynamic=%s)", async (dynamic) => { - const server = new McpServer({ - name: "context-catch-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 field = z.string().catch((ctx) => `fallback:${String(ctx.value)}`); const shape = { value: field }; - tool({ - name: "context-catch", - description: "Recover using the invalid input", - annotations, - args: shape, - tool: (_sdk, args) => ({ content: [{ type: "text", text: args.value }] }), + 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 }], + }), + }); }); - if (dynamic) registerDynamicTools(logger, server, getSDK, toolMap, scopes); await withClient(server, async (client) => { const standard = mcpInputSchema(shape)["~standard"]; for (const direction of ["input", "output"] as const) { @@ -1000,3 +1013,122 @@ test.each([ }, 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/zSDKs/mcp-typescript/.speakeasy/gen.lock b/zSDKs/mcp-typescript/.speakeasy/gen.lock index 37570c0a7..2f8c0371e 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:b72f1651cacf0ee9b490b9640c0ce6ecec7de1be + last_write_checksum: sha1:8fa6c08df7cb938fe2bd18f307a71cf870e4479b 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:735cd364a0f464eb9a4b45c753e2363b36febd9e + 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:e0f0b4576efe5e3e782888935e662ec45382d8f0 + last_write_checksum: sha1:17687c74533e4a0a9ab43d19a57b205148d46081 src/lib/base64.ts: last_write_checksum: sha1:e9f04a037018040361043104960982f7c22db52d src/lib/config.ts: @@ -594,7 +594,7 @@ trackedFiles: src/mcp-server/cli/serve/command.ts: last_write_checksum: sha1:6700155fe9c6850f35d9ff03834ab3be59d4d273 src/mcp-server/cli/serve/impl.ts: - last_write_checksum: sha1:d3918933a56c3a19df10097662efa73376544e95 + last_write_checksum: sha1:9b74d072bf6f8022e92d5e678994da5ff7652405 src/mcp-server/cli/start/command.ts: last_write_checksum: sha1:0b1d5a70b4fbce726352d22a584ed4a531cb0a52 src/mcp-server/cli/start/impl.ts: @@ -610,17 +610,17 @@ trackedFiles: src/mcp-server/mcp-server.ts: last_write_checksum: sha1:e7f95d828d67b60820bf240e141577d19c406900 src/mcp-server/prompts.ts: - last_write_checksum: sha1:2725797c8942b4be6e242cdfd035c1f95d023f7e + last_write_checksum: sha1:1d4a68d460607f30b1ff0ecca3227da67fdf2048 src/mcp-server/resources.ts: - last_write_checksum: sha1:2503e1a4d6e63c1a7db29e371d8f38d778a17357 + last_write_checksum: sha1:6b77aec8f30a10add2fef0fad4786599ddfbd2a5 src/mcp-server/scopes.ts: last_write_checksum: sha1:77d0eb95785e84f9fd0b27c4612f8c6470489fe4 src/mcp-server/server.ts: last_write_checksum: sha1:0534f866ec7aca03b33990729d84b65997c47a92 src/mcp-server/shared.ts: - last_write_checksum: sha1:e285fbba7798f2cd4239bb90618969c7755dd032 + last_write_checksum: sha1:1832c294677c567750b516748fca219a05da582f src/mcp-server/tools.ts: - last_write_checksum: sha1:5d51cd60639d02b2045fdf96e5742d820857cb2a + last_write_checksum: sha1:0c4e4d1b3e5d53d5bb1c9accc022d52bf3e80b96 src/mcp-server/tools/binaryAndStringUpload.ts: last_write_checksum: sha1:fbf82c10210eec4825988311ef4326b79e33f93b src/mcp-server/tools/createUser.ts: @@ -956,7 +956,7 @@ trackedFiles: worker-configuration.ts: last_write_checksum: sha1:94268ed98a74734682ee4a0466bb2fa6877a17a2 wrangler.toml: - last_write_checksum: sha1:92b19ea633dc9ee4524a3d266411df305459eebf + 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 a0cb9a134..16f3ea4c9 100644 --- a/zSDKs/mcp-typescript/README.md +++ b/zSDKs/mcp-typescript/README.md @@ -373,7 +373,7 @@ 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`. Each request is handled statelessly, so no session state is stored between requests. The legacy HTTP+SSE endpoint at `/sse` has been retired: requests to it receive HTTP `410 Gone` with a JSON body naming the `/mcp` endpoint. Point existing clients at `/mcp`. +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. The Durable Object binding and its `v1` migration remain in `wrangler.toml` so that workers deployed with them keep deploying and keep their stored state. The worker no longer uses the namespace. Deleting it permanently destroys its data, so do it only deliberately: remove the binding, remove the exported class, and add a migration with `deleted_classes`. diff --git a/zSDKs/mcp-typescript/package-lock.json b/zSDKs/mcp-typescript/package-lock.json index a65bb2fe7..4bab20aa3 100644 --- a/zSDKs/mcp-typescript/package-lock.json +++ b/zSDKs/mcp-typescript/package-lock.json @@ -13,6 +13,7 @@ "@gram-ai/functions": "^0.12.1", "@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", @@ -27,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", @@ -1914,6 +1915,46 @@ "@modelcontextprotocol/server": "^2.3.0" } }, + "node_modules/@modelcontextprotocol/sdk": { + "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 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/@modelcontextprotocol/server": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.3.1.tgz", @@ -3157,6 +3198,39 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/ajv": { + "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", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -4357,7 +4431,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -4380,6 +4453,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "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", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4994,6 +5083,18 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "license": "(AFL-2.1 OR BSD-3-Clause)" }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -5710,6 +5811,15 @@ "node": ">=10" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7573,7 +7683,6 @@ "version": "3.25.2", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.25.28 || ^4" diff --git a/zSDKs/mcp-typescript/package.json b/zSDKs/mcp-typescript/package.json index 9e4f3ed7e..077eb8f14 100644 --- a/zSDKs/mcp-typescript/package.json +++ b/zSDKs/mcp-typescript/package.json @@ -34,6 +34,7 @@ "@gram-ai/functions": "^0.12.1", "@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", @@ -53,7 +54,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", diff --git a/zSDKs/mcp-typescript/src/cloudflare-worker/cloudflare-worker.ts b/zSDKs/mcp-typescript/src/cloudflare-worker/cloudflare-worker.ts index a86681fa5..3d4886dd1 100644 --- a/zSDKs/mcp-typescript/src/cloudflare-worker/cloudflare-worker.ts +++ b/zSDKs/mcp-typescript/src/cloudflare-worker/cloudflare-worker.ts @@ -2,14 +2,22 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { createMcpHandler } from "@modelcontextprotocol/server"; +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"; -function getSDK(request: Request | undefined) { - const getHeader = (name: string) => request?.headers.get(name) ?? ""; +function getSDK(headers: Headers | undefined) { + const getHeader = (name: string) => headers?.get(name) ?? ""; return new SDKCore({ debugLogger: { @@ -46,32 +54,82 @@ function getSDK(request: Request | undefined) { }); } -function sseRetiredResponse(url: URL): Response { - return Response.json( - { - error: - "The HTTP+SSE transport is no longer served. Connect to the Streamable HTTP endpoint instead.", - endpoint: new URL("/mcp", url).toString(), - }, - { status: 410 }, - ); +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; } -function corsHeaders(request: Request): Headers { +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; + +function corsHeaders(request: Request, cors: CorsOptions = {}): Headers { return new Headers({ - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", - "Access-Control-Allow-Headers": - request.headers.get("Access-Control-Request-Headers") - ?? "Content-Type, Accept, mcp-session-id, mcp-protocol-version", - "Access-Control-Expose-Headers": "mcp-session-id, mcp-protocol-version", - "Access-Control-Max-Age": "86400", + "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): Response { +function withCors( + response: Response, + request: Request, + cors?: CorsOptions, +): Response { const headers = new Headers(response.headers); - corsHeaders(request).forEach((value, name) => headers.set(name, value)); + corsHeaders(request, cors).forEach((value, name) => headers.set(name, value)); return new Response(response.body, { status: response.status, statusText: response.statusText, @@ -79,37 +137,352 @@ function withCors(response: Response, request: Request): Response { }); } -// Retired session class; still exported so deployments created with it keep their Durable Object namespace. -export class OpenapiMCP { - fetch(request: Request): Response { - return sseRetiredResponse(new URL(request.url)); +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; } } -const mcpHandler = createMcpHandler(({ requestInfo }) => { - const { server } = createMCPServer({ - logger: createConsoleLogger("debug"), - getSDK: () => getSDK(requestInfo), +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; +} + +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); +} + +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(); }); - return server; -}); + 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; + + 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 { + 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(), + }); + } + + 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") { - if (request.method === "OPTIONS") { - return new Response(null, { - status: 204, - headers: corsHeaders(request), - }); - } - return withCors(await mcpHandler.fetch(request), request); + return handleStreamableHttp(request, env.OPENAPI_MCP, undefined); } - if (url.pathname === "/sse" || url.pathname.startsWith("/sse/")) { - return withCors(sseRetiredResponse(url), request); + 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 06a5aaadb..b502d9859 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"), { 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,17 +40,21 @@ 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", +): string { const o = origin; + const endpoint = transport === "sse" ? "sse" : "mcp"; const mcpConfig = { "command": "npx", "args": [ "-y", "mcp-remote@0.1.25", - `${o}/mcp`, + `${o}/${endpoint}`, "--header", "server-index:${SERVER_INDEX}", "--header", @@ -91,9 +110,26 @@ export function landingPageHTML(origin: string): string { "DEPRECATED_QUERY_PARAM2": "YOUR_VALUE_HERE", }, }; - const codexConfig = `[mcp_servers.SDK] -url = "${o}/mcp" + 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 = "${o}/${endpoint}" 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 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
- +
@@ -964,7 +1000,7 @@ http_headers = { "server-index" = "YOUR_SERVER_INDEX", "subdomain" = "YOUR_SUBDO - claude mcp add --transport http SDK https://openapi-mcp-server.example.workers.dev/mcp + claude mcp add --transport ${transport} SDK ${o}/${endpoint} --header "x-username: ..." --header "x-password: ..." --header "authorization: ..." --header "api_key: ..." --header "x-oauth2: ..." --header "x-app-id: ..." --header "x-secret: ..." --header "x-mobile-auth: ..." --header "x-client-credentials: ..." --header "api_key: ..."
@@ -1015,7 +1051,7 @@ http_headers = { "server-index" = "YOUR_SERVER_INDEX", "subdomain" = "YOUR_SUBDO - gemini mcp add --transport http SDK https://openapi-mcp-server.example.workers.dev/mcp + gemini mcp add --transport ${transport} SDK ${o}/${endpoint} --header "x-username: ..." --header "x-password: ..." --header "authorization: ..." --header "api_key: ..." --header "x-oauth2: ..." --header "x-app-id: ..." --header "x-secret: ..." --header "x-mobile-auth: ..." --header "x-client-credentials: ..." --header "api_key: ..." 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 5e4c45d94..1aff07525 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/cli/serve/impl.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/cli/serve/impl.ts @@ -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); @@ -82,9 +82,38 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { return mcpServer; }); const nodeHandler = toNodeHandler(handler); - app.all("/mcp", (req, res) => nodeHandler(req, res, req.body)); + 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(); @@ -93,6 +122,10 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { }); const shutdown = async () => { + if (closing) { + process.exit(1); + } + closing = true; logger.info("Shutting down HTTP server"); const timer = setTimeout(() => { @@ -100,12 +133,16 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) { process.exit(1); }, 5000); + const stopped = new Promise((resolve) => + httpServer.close(() => resolve()) + ); + await Promise.all(activeResponses); await handler.close(); - httpServer.close(() => { - clearTimeout(timer); - logger.info("Graceful shutdown complete"); - process.exit(0); - }); + 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/prompts.ts b/zSDKs/mcp-typescript/src/mcp-server/prompts.ts index 6e52f1fbd..d77201705 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/prompts.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/prompts.ts @@ -2,16 +2,16 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { - GetPromptResult, - McpServer, - ServerContext, -} from "@modelcontextprotocol/server"; +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 } from "./shared.js"; +import { + mcpInputSchema, + MCPServerContext, + mcpServerContext, +} from "./shared.js"; export type PromptDefinition< Args extends undefined | z.ZodRawShape = undefined, @@ -23,7 +23,7 @@ export type PromptDefinition< prompt: ( client: SDKCore, args: z.output>, - extra: ServerContext, + extra: MCPServerContext, ) => GetPromptResult | Promise; } : { @@ -33,7 +33,7 @@ export type PromptDefinition< args?: undefined; prompt: ( client: SDKCore, - extra: ServerContext, + extra: MCPServerContext, ) => GetPromptResult | Promise; }; @@ -74,18 +74,23 @@ export function createRegisterPrompt( } if (prompt.args) { - server.registerPrompt(prompt.name, { - ...(prompt.description === undefined - ? {} - : { description: prompt.description }), - argsSchema: { ...mcpInputSchema(prompt.args), shape: 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 { server.registerPrompt(prompt.name, { ...(prompt.description === undefined ? {} : { description: prompt.description }), - }, async (ctx) => prompt.prompt(getSDK(), ctx)); + }, 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 2bbd47e52..3ae296085 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/resources.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/resources.ts @@ -2,23 +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, - ServerContext, 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: ServerContext, + extra: MCPServerContext, ) => ReadResourceResult | Promise; export type ResourceDefinition = { @@ -34,7 +34,7 @@ export type ReadResourceTemplateCallback = ( client: SDKCore, uri: URL, vars: Variables, - extra: ServerContext, + extra: MCPServerContext, ) => ReadResourceResult | Promise; export type ResourceTemplateDefinition = { @@ -42,7 +42,7 @@ export type ResourceTemplateDefinition = { description: string; metadata?: ResourceMetadata; scopes?: MCPScope[]; - resource: ResourceTemplate; + resource: ResourceTemplate | LegacyResourceTemplate; read: ReadResourceTemplateCallback; }; @@ -94,7 +94,7 @@ export function createRegisterResource( 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 }); @@ -127,11 +127,30 @@ export function createRegisterResourceTemplate( 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/shared.ts b/zSDKs/mcp-typescript/src/mcp-server/shared.ts index 405d919c3..9867264b5 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/shared.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/shared.ts @@ -2,9 +2,57 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import type { StandardSchemaWithJSON } from "@modelcontextprotocol/server"; +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< diff --git a/zSDKs/mcp-typescript/src/mcp-server/tools.ts b/zSDKs/mcp-typescript/src/mcp-server/tools.ts index 25fcb4a67..856fda8ad 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools.ts @@ -2,11 +2,7 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { - CallToolResult, - McpServer, - ServerContext, -} from "@modelcontextprotocol/server"; +import { CallToolResult, McpServer } from "@modelcontextprotocol/server"; import * as z from "zod"; import { SDKCore } from "../core.js"; import { ServerPORT$zodSchema } from "../lib/config.js"; @@ -14,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 { mcpInputSchema, valueToBase64 } from "./shared.js"; +import { + mcpInputSchema, + MCPServerContext, + mcpServerContext, + valueToBase64, +} from "./shared.js"; export type MCPToolAnnotationFilter = { readOnlyHint?: boolean; @@ -76,7 +77,7 @@ export type ToolDefinition = tool: ( client: SDKCore, args: z.output>, - extra: ServerContext, + extra: MCPServerContext, ) => CallToolResult | Promise; } : { @@ -93,7 +94,7 @@ export type ToolDefinition = }; tool: ( client: SDKCore, - extra: ServerContext, + extra: MCPServerContext, ) => CallToolResult | Promise; }; @@ -201,7 +202,7 @@ export function createRegisterTool( annotations: tool.annotations, }, async (args, ctx) => { - return tool.tool(getSDK(), args, ctx); + return tool.tool(getSDK(), args, mcpServerContext(ctx)); }, ); } else { @@ -212,7 +213,7 @@ export function createRegisterTool( annotations: tool.annotations, }, async (ctx) => { - return tool.tool(getSDK(), ctx); + return tool.tool(getSDK(), mcpServerContext(ctx)); }, ); } @@ -397,9 +398,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); @@ -452,7 +453,8 @@ function resolveHeader( return schema.parse(val); } if (disableStaticAuth || cliFlagValue === undefined) { - return undefined; + const parsed = schema.safeParse(undefined); + return parsed.success ? parsed.data : undefined; } return schema.parse(cliFlagValue); } diff --git a/zSDKs/mcp-typescript/wrangler.toml b/zSDKs/mcp-typescript/wrangler.toml index 202dc66b6..26cb46790 100644 --- a/zSDKs/mcp-typescript/wrangler.toml +++ b/zSDKs/mcp-typescript/wrangler.toml @@ -6,7 +6,7 @@ compatibility_date = "2025-07-15" # Enable node compatibility for MCP SDK compatibility_flags = ["nodejs_compat", "enable_request_signal"] -# Retained so existing deployments keep their Durable Object namespace and stored state +# 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" From a74a072aaf4a068265089279befc86ff9063a640 Mon Sep 17 00:00:00 2001 From: Thomas Rooney Date: Wed, 7 Oct 2026 16:50:47 +0100 Subject: [PATCH 3/6] fix(mcp): preserve extension and setup compatibility Keep optional MCP callback contexts and resource templates compatible with v1 extensions, and distinguish schema input and output. Align setup headers with each transport while retaining SSE aliases. Preserve generated declaration layout around custom metadata. --- templates/perms.go | 1 + .../auxiliary/src/landing-page.ts.stmpl | 20 +- .../auxiliary/src/mcp-server/tools.ts.stmpl | 9 +- .../mcp-typescript/includes/mcp-server.ts | 9 +- .../mcp-typescript/includes/templating.ts | 52 ++- .../mcp-typescript/package.json.stmpl | 6 +- .../templates/mcp-typescript/readme.stmpl | 2 +- .../primary/auth_additional.test.ts.stmpl | 30 ++ templates/templates/typescriptv2/config.ts | 8 + .../typescriptv2/includes/dependencies.ts | 2 + .../auxiliary/src/mcp-server/prompts.ts.stmpl | 11 +- .../src/mcp-server/resources.ts.stmpl | 30 +- .../auxiliary/src/mcp-server/shared.ts.stmpl | 37 +- .../auxiliary/src/mcp-server/tools.ts.stmpl | 11 +- .../mcp_compatibility_additional.test.ts | 325 ++++++++++++++++++ zSDKs/mcp-typescript/.speakeasy/gen.lock | 8 +- zSDKs/mcp-typescript/README.md | 6 +- zSDKs/mcp-typescript/package.json | 6 +- zSDKs/mcp-typescript/src/landing-page.ts | 102 +++--- .../src/mcp-server/cli/start/impl.ts | 24 +- zSDKs/mcp-typescript/src/mcp-server/tools.ts | 76 ++-- 21 files changed, 607 insertions(+), 168 deletions(-) create mode 100644 templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts diff --git a/templates/perms.go b/templates/perms.go index 769059cda..0eefc687d 100644 --- a/templates/perms.go +++ b/templates/perms.go @@ -2460,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/landing-page.ts.stmpl b/templates/templates/mcp-typescript/auxiliary/src/landing-page.ts.stmpl index 873b4c215..232e299a4 100644 --- a/templates/templates/mcp-typescript/auxiliary/src/landing-page.ts.stmpl +++ b/templates/templates/mcp-typescript/auxiliary/src/landing-page.ts.stmpl @@ -6,7 +6,7 @@ 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, "http"), { + return new Response(landingPageHTML(origin, "http", "cloudflare"), { headers: { "Content-Type": "text/html" }, }); } @@ -30,16 +30,26 @@ function sendLandingPage(req: ExpressRequest, res: ExpressResponse, transport: " res.type("html").send(landingPageHTML(`${proto}://${host}`, transport)); } -export function landingPageHTML(origin: string, transport: "sse" | "http" = "sse"): 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 mcpConfig = {{ templateMcpIDEConfig (dict "location" "landingpage" "format" "js" "endpoint" "${endpoint}") }} + const headers: Array<{ name: string; env: string }> = runtime === "cloudflare" + ? {{ templateMcpLandingPageHeaders "cloudflare" }} + : {{ 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(", ")} }` - : `{{ templateMcpIDEConfig (dict "location" "landingpage" "format" "toml" "endpoint" "${endpoint}") }}` + : `[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 ` @@ -848,7 +858,7 @@ env = { ${Object.entries(mcpConfig.env).map(([name, value]) => `${JSON.stringify - ${o}/mcp + ${o}/${endpoint} 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 4426a0a11..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 @@ -48,8 +48,11 @@ export function buildAnnotationFilter(annotations: string[] | undefined): MCPToo return filter; } -export type ToolDefinition = - Args extends z.ZodRawShape +type ZodRawShapeCompat = z.ZodRawShape; +type ShapeOutput = z.output>; + +export type ToolDefinition = + Args extends ZodRawShapeCompat ? { name: string; description: string; @@ -64,7 +67,7 @@ export type ToolDefinition = } tool: ( client: {{ $coreName }}, - args: z.output>, + args: ShapeOutput, extra: MCPServerContext ) => CallToolResult | Promise; } 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 dbc03fa79..9535060d8 100644 --- a/templates/templates/mcp-typescript/includes/templating.ts +++ b/templates/templates/mcp-typescript/includes/templating.ts @@ -722,6 +722,29 @@ function addHeadersToConfigVSCode(config: any, headerEntries: any[]) { return config; } +function mcpRemoteHeaders(runtime: "node" | "cloudflare") { + const security = context.Global.AST.MainSDK.Security?.Type; + if (!security) return []; + const entries = + runtime === "cloudflare" + ? getRemoteServerHeaders(security) + .flat() + .map((header) => ({ + name: header.headerName, + env: caser().ToSNAKE(header.fieldName), + })) + : unnestSecurityEnvFields(security).map((field) => ({ + name: sanitizeMCPCLIFlag(field.Name), + env: caser().ToSNAKE(field.Name), + })); + return [...new Map(entries.map((header) => [header.name, header])).values()]; +} + +registerTemplateFunc( + "templateMcpLandingPageHeaders", + (runtime: "node" | "cloudflare") => JSON.stringify(mcpRemoteHeaders(runtime)), +); + function mcpRemoteConfigCommand( endpointType: string = "mcp", clientType: string = "default", @@ -733,10 +756,10 @@ 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", + )) { + headers.push("--header", `${header.name}:\${${header.env}}`); } return [ @@ -888,16 +911,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(", ")} }`; @@ -1023,12 +1047,6 @@ 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", @@ -1037,7 +1055,7 @@ function templateMcpCLICommand(options: MCPCLICommandOptions) { "${transport}", mcpName(), `\${o}/\${endpoint}`, - ...headerArgs, + "${cliHeaders}", ].join(" "); } diff --git a/templates/templates/mcp-typescript/package.json.stmpl b/templates/templates/mcp-typescript/package.json.stmpl index b3d59ab7c..ad4a16cb1 100644 --- a/templates/templates/mcp-typescript/package.json.stmpl +++ b/templates/templates/mcp-typescript/package.json.stmpl @@ -12,9 +12,6 @@ {{- end }} "type": "module", "sideEffects": false, - "engines": { - "node": "{{ if .Global.Config.GramEnabled }}>=22.18.0{{ else }}>=20{{ end }}" - }, {{- if .Global.Config.RepoURL }} "repository": { "type": "git", @@ -67,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 f5af8c0cf..eae70ae7b 100644 --- a/templates/templates/mcp-typescript/readme.stmpl +++ b/templates/templates/mcp-typescript/readme.stmpl @@ -58,7 +58,7 @@ Then install with Claude Code CLI: 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. -The Durable Object binding and its `v1` migration remain in `wrangler.toml` so that workers deployed with them keep deploying and keep their stored state. The worker no longer uses the namespace. Deleting it permanently destroys its data, so do it only deliberately: remove the binding, remove the exported class, and add a migration with `deleted_classes`. +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/tests/primary/auth_additional.test.ts.stmpl b/templates/templates/mcp-typescript/tests/primary/auth_additional.test.ts.stmpl index ee893a5d3..b2dac7ce8 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 @@ -9,9 +9,39 @@ import { authHoistedSecurityApiKeyFirst } from "../funcs/authHoistedSecurityApiK 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"); + } + 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 }; diff --git a/templates/templates/typescriptv2/config.ts b/templates/templates/typescriptv2/config.ts index e99fb9b04..2ad96bd12 100644 --- a/templates/templates/typescriptv2/config.ts +++ b/templates/templates/typescriptv2/config.ts @@ -119,6 +119,14 @@ function getTemplateDependencies() { category: "dev", condition: "mcpServer", }, + "@modelcontextprotocol/sdk": { + name: "@modelcontextprotocol/sdk", + 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", diff --git a/templates/templates/typescriptv2/includes/dependencies.ts b/templates/templates/typescriptv2/includes/dependencies.ts index 749c57776..6f0015355 100644 --- a/templates/templates/typescriptv2/includes/dependencies.ts +++ b/templates/templates/typescriptv2/includes/dependencies.ts @@ -121,6 +121,8 @@ function templateDependencies(): string { } if (isMCPServerEnabled()) { + defaultDependencies["@modelcontextprotocol/sdk"] = + deps["@modelcontextprotocol/sdk"].version; defaultDependencies["@modelcontextprotocol/server"] = deps["@modelcontextprotocol/server"].version; defaultDependencies["@modelcontextprotocol/server-legacy"] = 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 e0648c692..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 @@ -5,7 +5,8 @@ {{- addImport "../core.js" $coreName }} {{- addImport "./scopes.js" "MCPScope" }} {{- addImport "@modelcontextprotocol/server" "GetPromptResult" }} -{{- addImport "@modelcontextprotocol/server" "ServerContext" }} +{{- addImport "./shared.js" "MCPServerContext" }} +{{- addImport "./shared.js" "mcpServerContext" }} {{- addImport "@modelcontextprotocol/server" "McpServer" }} {{- addImport "@modelcontextprotocol/server" "isCompletable" }} {{- addZodPackageImport "objectOutputType" }} @@ -28,7 +29,7 @@ export type PromptDefinition, - extra: ServerContext, + extra: MCPServerContext, ) => GetPromptResult | Promise; } : { @@ -38,7 +39,7 @@ export type PromptDefinition GetPromptResult | Promise; }; @@ -84,11 +85,11 @@ export function createRegisterPrompt( server.registerPrompt(prompt.name, { ...(prompt.description === undefined ? {} : { description: prompt.description }), argsSchema: { ...mcpSchema(prompt.args), shape }, - }, async (args, ctx) => prompt.prompt(sdk, args, ctx)); + }, async (args, ctx) => prompt.prompt(sdk, args, mcpServerContext(ctx))); } else { server.registerPrompt(prompt.name, { ...(prompt.description === undefined ? {} : { description: prompt.description }), - }, async (ctx) => prompt.prompt(sdk, ctx)); + }, async (ctx) => prompt.prompt(sdk, mcpServerContext(ctx))); } logger.debug("Registered prompt", { name: prompt.name }); 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 f70288fd2..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" }} @@ -8,7 +9,8 @@ {{- addImport "./console-logger.js" "ConsoleLogger" }} {{- addImport "../core.js" $coreName }} {{- addImport "@modelcontextprotocol/server" "Variables" }} -{{- addImport "@modelcontextprotocol/server" "ServerContext" }} +{{- addImport "./shared.js" "MCPServerContext" }} +{{- addImport "./shared.js" "mcpServerContext" }} {{- addImport "@modelcontextprotocol/server" "ReadResourceResult" }} {{- addImport "@modelcontextprotocol/server" "ResourceTemplate" }} {{- addImport "@modelcontextprotocol/server" "McpServer" }} @@ -17,7 +19,7 @@ export type ReadResourceCallback = ( client: {{$coreName}}, uri: URL, - extra: ServerContext, + extra: MCPServerContext, ) => ReadResourceResult | Promise; export type ResourceDefinition = { @@ -33,7 +35,7 @@ export type ReadResourceTemplateCallback = ( client: {{$coreName}}, uri: URL, vars: Variables, - extra: ServerContext, + extra: MCPServerContext, ) => ReadResourceResult | Promise; export type ResourceTemplateDefinition = { @@ -41,7 +43,7 @@ export type ResourceTemplateDefinition = { description: string; metadata?: ResourceMetadata; scopes?: MCPScope[]; - resource: ResourceTemplate; + resource: ResourceTemplate | LegacyResourceTemplate; read: ReadResourceTemplateCallback; }; @@ -121,7 +123,7 @@ export function createRegisterResource( 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,11 +153,25 @@ export function createRegisterResourceTemplate( 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 2ea02d200..5a88fd357 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 @@ -5,12 +5,43 @@ {{- 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 = (): Record => JSON.parse(JSON.stringify( - zodToJsonSchema(schema, { pipeStrategy: "input", strictUnions: true }), + const jsonSchema = (direction: "input" | "output"): Record => JSON.parse(JSON.stringify( + zodToJsonSchema(schema, { + pipeStrategy: direction, + effectStrategy: direction === "output" ? "any" : "input", + strictUnions: direction === "input", + }), (_key, value) => typeof value === "bigint" ? value.toString() : value, )); @@ -18,7 +49,7 @@ export function mcpSchema( "~standard": { version: 1, vendor: "zod", - jsonSchema: { input: jsonSchema, output: jsonSchema }, + jsonSchema: { input: () => jsonSchema("input"), output: () => jsonSchema("output") }, validate: async (value) => { const result = await schema.safeParseAsync(value); return result.success 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 f066d32e3..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 @@ -8,7 +8,8 @@ {{- addImport "./console-logger.js" "ConsoleLogger" }} {{- addImport "../core.js" $coreName }} {{- addImport "@modelcontextprotocol/server" "CallToolResult" }} -{{- addImport "@modelcontextprotocol/server" "ServerContext" }} +{{- addImport "./shared.js" "MCPServerContext" }} +{{- addImport "./shared.js" "mcpServerContext" }} {{- addImport "@modelcontextprotocol/server" "McpServer" }} {{- addZodPackageImport "objectOutputType" }} {{- addZodPackageImport "ZodTypeAny" }} @@ -25,7 +26,7 @@ export type ToolDefinition = tool: ( client: {{$coreName}}, args: objectOutputType, - extra: ServerContext + extra: MCPServerContext ) => CallToolResult | Promise; } : { @@ -35,7 +36,7 @@ export type ToolDefinition = args?: undefined; tool: ( client: {{$coreName}}, - extra: ServerContext + extra: MCPServerContext ) => CallToolResult | Promise; }; @@ -120,11 +121,11 @@ export function createRegisterTool( server.registerTool(tool.name, { description: tool.description, inputSchema: mcpSchema(tool.args), - }, async (args, ctx) => tool.tool(sdk, args, ctx)); + }, async (args, ctx) => tool.tool(sdk, args, mcpServerContext(ctx))); } else { server.registerTool(tool.name, { description: tool.description, - }, async (ctx) => tool.tool(sdk, ctx)); + }, async (ctx) => tool.tool(sdk, mcpServerContext(ctx))); } logger.debug("Registered tool", { name: tool.name }); 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..05c7a8246 --- /dev/null +++ b/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts @@ -0,0 +1,325 @@ +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 { + mcpSchema, + 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 { + createRegisterResource, + createRegisterResourceTemplate, +} from "../mcp-server/resources.js"; +import { createRegisterTool } from "../mcp-server/tools.js"; +import { createRegisterPrompt } from "../mcp-server/prompts.js"; +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("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("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" }] }); + }); +}); diff --git a/zSDKs/mcp-typescript/.speakeasy/gen.lock b/zSDKs/mcp-typescript/.speakeasy/gen.lock index 2f8c0371e..4f3b6e1f5 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:8fa6c08df7cb938fe2bd18f307a71cf870e4479b + last_write_checksum: sha1:4b0ecf94b179c07fabd75eac7a9f7ebf37868fbc server.json: last_write_checksum: sha1:cdd68ed3cc00a507a2bc71e85823fe10482e2527 src/__tests__/assertions.ts: @@ -554,7 +554,7 @@ trackedFiles: src/hooks/types.ts: last_write_checksum: sha1:29372122898cc523fa47576f3f6608a28ec6080e src/landing-page.ts: - last_write_checksum: sha1:17687c74533e4a0a9ab43d19a57b205148d46081 + last_write_checksum: sha1:7a7d2f0f2e4ef76f992da14f7321ce3aa47a3bb3 src/lib/base64.ts: last_write_checksum: sha1:e9f04a037018040361043104960982f7c22db52d src/lib/config.ts: @@ -598,7 +598,7 @@ trackedFiles: src/mcp-server/cli/start/command.ts: last_write_checksum: sha1:0b1d5a70b4fbce726352d22a584ed4a531cb0a52 src/mcp-server/cli/start/impl.ts: - last_write_checksum: sha1:efd248f6c2b17539c6c84fa7c1692515e6ca6dc2 + last_write_checksum: sha1:fa434824ca40372f0aa641d676bc15c1d907b3d7 src/mcp-server/console-logger.ts: last_write_checksum: sha1:0c4b9b7c79ad0a81359210cb31b9ee4d887d5de1 src/mcp-server/extensions.ts: @@ -620,7 +620,7 @@ trackedFiles: src/mcp-server/shared.ts: last_write_checksum: sha1:1832c294677c567750b516748fca219a05da582f src/mcp-server/tools.ts: - last_write_checksum: sha1:0c4e4d1b3e5d53d5bb1c9accc022d52bf3e80b96 + last_write_checksum: sha1:38e6a68a329c1179dfce2178c24e2dd81ec0e0a8 src/mcp-server/tools/binaryAndStringUpload.ts: last_write_checksum: sha1:fbf82c10210eec4825988311ef4326b79e33f93b src/mcp-server/tools/createUser.ts: diff --git a/zSDKs/mcp-typescript/README.md b/zSDKs/mcp-typescript/README.md index 16f3ea4c9..bf903005d 100644 --- a/zSDKs/mcp-typescript/README.md +++ b/zSDKs/mcp-typescript/README.md @@ -58,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=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZW1vdGVAMC4xLjI1IiwiaHR0cHM6Ly9vcGVuYXBpLW1jcC1zZXJ2ZXIuZXhhbXBsZS53b3JrZXJzLmRldi9tY3AiLCItLWhlYWRlciIsInNlcnZlci1pbmRleDoke1NFUlZFUl9JTkRFWH0iLCItLWhlYWRlciIsInN1YmRvbWFpbjoke1NVQkRPTUFJTn0iLCItLWhlYWRlciIsImFwaS12ZXJzaW9uOiR7QVBJX1ZFUlNJT059IiwiLS1oZWFkZXIiLCJhcGktaG9zdC1uYW1lOiR7QVBJX0hPU1RfTkFNRX0iLCItLWhlYWRlciIsImFwaS1wb3J0OiR7QVBJX1BPUlR9IiwiLS1oZWFkZXIiLCJ1c2VybmFtZToke1VTRVJOQU1FfSIsIi0taGVhZGVyIiwicGFzc3dvcmQ6JHtQQVNTV09SRH0iLCItLWhlYWRlciIsImJlYXJlci1hdXRoOiR7QkVBUkVSX0FVVEh9IiwiLS1oZWFkZXIiLCJteS1hcGkta2V5OiR7TVlfQVBJX0tFWX0iLCItLWhlYWRlciIsIm9hdXRoMjoke09BVVRIMn0iLCItLWhlYWRlciIsImFwcC1pZDoke0FQUF9JRH0iLCItLWhlYWRlciIsInNlY3JldDoke1NFQ1JFVH0iLCItLWhlYWRlciIsIm1vYmlsZS1hdXRoOiR7TU9CSUxFX0FVVEh9IiwiLS1oZWFkZXIiLCJjbGllbnQtY3JlZGVudGlhbHM6JHtDTElFTlRfQ1JFREVOVElBTFN9IiwiLS1oZWFkZXIiLCJxdWVyeS1wYXJhbTE6JHtRVUVSWV9QQVJBTTF9IiwiLS1oZWFkZXIiLCJkZXByZWNhdGVkLXF1ZXJ5LXBhcmFtMToke0RFUFJFQ0FURURfUVVFUllfUEFSQU0xfSIsIi0taGVhZGVyIiwiZGVwcmVjYXRlZC1xdWVyeS1wYXJhbTI6JHtERVBSRUNBVEVEX1FVRVJZX1BBUkFNMn0iXX0=) +[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=SDK&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZW1vdGVAMC4xLjI1IiwiaHR0cHM6Ly9vcGVuYXBpLW1jcC1zZXJ2ZXIuZXhhbXBsZS53b3JrZXJzLmRldi9tY3AiLCItLWhlYWRlciIsIngtdXNlcm5hbWU6JHtVU0VSTkFNRX0iLCItLWhlYWRlciIsIngtcGFzc3dvcmQ6JHtQQVNTV09SRH0iLCItLWhlYWRlciIsImF1dGhvcml6YXRpb246JHtCRUFSRVJfQVVUSH0iLCItLWhlYWRlciIsImFwaV9rZXk6JHtNWV9BUElfS0VZfSIsIi0taGVhZGVyIiwieC1vYXV0aDI6JHtPQVVUSDJ9IiwiLS1oZWFkZXIiLCJ4LWFwcC1pZDoke0FQUF9JRH0iLCItLWhlYWRlciIsIngtc2VjcmV0OiR7U0VDUkVUfSIsIi0taGVhZGVyIiwieC1tb2JpbGUtYXV0aDoke01PQklMRV9BVVRIfSIsIi0taGVhZGVyIiwieC1jbGllbnQtY3JlZGVudGlhbHM6JHtDTElFTlRfQ1JFREVOVElBTFN9Il19) Or manually: @@ -187,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=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZW1vdGVAMC4xLjI1IiwiaHR0cHM6Ly9vcGVuYXBpLW1jcC1zZXJ2ZXIuZXhhbXBsZS53b3JrZXJzLmRldi9tY3AiLCItLWhlYWRlciIsInNlcnZlci1pbmRleDoke1NFUlZFUl9JTkRFWH0iLCItLWhlYWRlciIsInN1YmRvbWFpbjoke1NVQkRPTUFJTn0iLCItLWhlYWRlciIsImFwaS12ZXJzaW9uOiR7QVBJX1ZFUlNJT059IiwiLS1oZWFkZXIiLCJhcGktaG9zdC1uYW1lOiR7QVBJX0hPU1RfTkFNRX0iLCItLWhlYWRlciIsImFwaS1wb3J0OiR7QVBJX1BPUlR9IiwiLS1oZWFkZXIiLCJ1c2VybmFtZToke1VTRVJOQU1FfSIsIi0taGVhZGVyIiwicGFzc3dvcmQ6JHtQQVNTV09SRH0iLCItLWhlYWRlciIsImJlYXJlci1hdXRoOiR7QkVBUkVSX0FVVEh9IiwiLS1oZWFkZXIiLCJteS1hcGkta2V5OiR7TVlfQVBJX0tFWX0iLCItLWhlYWRlciIsIm9hdXRoMjoke09BVVRIMn0iLCItLWhlYWRlciIsImFwcC1pZDoke0FQUF9JRH0iLCItLWhlYWRlciIsInNlY3JldDoke1NFQ1JFVH0iLCItLWhlYWRlciIsIm1vYmlsZS1hdXRoOiR7TU9CSUxFX0FVVEh9IiwiLS1oZWFkZXIiLCJjbGllbnQtY3JlZGVudGlhbHM6JHtDTElFTlRfQ1JFREVOVElBTFN9IiwiLS1oZWFkZXIiLCJxdWVyeS1wYXJhbTE6JHtRVUVSWV9QQVJBTTF9IiwiLS1oZWFkZXIiLCJkZXByZWNhdGVkLXF1ZXJ5LXBhcmFtMToke0RFUFJFQ0FURURfUVVFUllfUEFSQU0xfSIsIi0taGVhZGVyIiwiZGVwcmVjYXRlZC1xdWVyeS1wYXJhbTI6JHtERVBSRUNBVEVEX1FVRVJZX1BBUkFNMn0iXX0=) +[![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: @@ -375,7 +375,7 @@ claude mcp add SDK -- npx -y openapi start --server-index 0 --subdomain api --ap 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. -The Durable Object binding and its `v1` migration remain in `wrangler.toml` so that workers deployed with them keep deploying and keep their stored state. The worker no longer uses the namespace. Deleting it permanently destroys its data, so do it only deliberately: remove the binding, remove the exported class, and add a migration with `deleted_classes`. +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.json b/zSDKs/mcp-typescript/package.json index 077eb8f14..01cfd0333 100644 --- a/zSDKs/mcp-typescript/package.json +++ b/zSDKs/mcp-typescript/package.json @@ -5,9 +5,6 @@ "author": "Speakeasy", "type": "module", "sideEffects": false, - "engines": { - "node": ">=22.18.0" - }, "repository": { "type": "git", "url": "https://github.com/speakeasy-sdks/test-sdk" @@ -66,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/landing-page.ts b/zSDKs/mcp-typescript/src/landing-page.ts index b502d9859..2da67143d 100644 --- a/zSDKs/mcp-typescript/src/landing-page.ts +++ b/zSDKs/mcp-typescript/src/landing-page.ts @@ -11,7 +11,7 @@ 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, "http"), { + return new Response(landingPageHTML(origin, "http", "cloudflare"), { headers: { "Content-Type": "text/html" }, }); } @@ -46,70 +46,46 @@ function sendLandingPage( 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" }, + ] + : [ + { "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" }, + ]; const mcpConfig = { - "command": "npx", - "args": [ + command: "npx", + args: [ "-y", "mcp-remote@0.1.25", `${o}/${endpoint}`, - "--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}", + ...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 cliHeaders = headers.map(({ name }) => `--header "${name}: ..."`).join( + " ", + ); const codexConfig = transport === "sse" ? `[mcp_servers.SDK] command = ${JSON.stringify(mcpConfig.command)} @@ -120,8 +96,12 @@ env = { ${ ).join(", ") } }` : `[mcp_servers.SDK] -url = "${o}/${endpoint}" -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" }`; +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( @@ -943,7 +923,7 @@ http_headers = { "server-index" = "YOUR_SERVER_INDEX", "subdomain" = "YOUR_SUBDO - ${o}/mcp + ${o}/${endpoint} @@ -1000,7 +980,7 @@ http_headers = { "server-index" = "YOUR_SERVER_INDEX", "subdomain" = "YOUR_SUBDO - claude mcp add --transport ${transport} SDK ${o}/${endpoint} --header "x-username: ..." --header "x-password: ..." --header "authorization: ..." --header "api_key: ..." --header "x-oauth2: ..." --header "x-app-id: ..." --header "x-secret: ..." --header "x-mobile-auth: ..." --header "x-client-credentials: ..." --header "api_key: ..." + claude mcp add --transport ${transport} SDK ${o}/${endpoint} ${cliHeaders} @@ -1051,7 +1031,7 @@ http_headers = { "server-index" = "YOUR_SERVER_INDEX", "subdomain" = "YOUR_SUBDO - gemini mcp add --transport ${transport} SDK ${o}/${endpoint} --header "x-username: ..." --header "x-password: ..." --header "authorization: ..." --header "api_key: ..." --header "x-oauth2: ..." --header "x-app-id: ..." --header "x-secret: ..." --header "x-mobile-auth: ..." --header "x-client-credentials: ..." --header "api_key: ..." + gemini mcp add --transport ${transport} SDK ${o}/${endpoint} ${cliHeaders} 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 7f7de401d..49158b4f3 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/cli/start/impl.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/cli/start/impl.ts @@ -122,17 +122,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/tools.ts b/zSDKs/mcp-typescript/src/mcp-server/tools.ts index 856fda8ad..8fa78a3fa 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/tools.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/tools.ts @@ -61,42 +61,48 @@ export function buildAnnotationFilter( return filter; } -export type ToolDefinition = - Args extends z.ZodRawShape ? { - name: string; - description: string; - scopes?: MCPScope[]; - args: Args; - annotations: { - title: string; - destructiveHint: boolean; - idempotentHint: boolean; - openWorldHint: boolean; - readOnlyHint: boolean; - }; - tool: ( - client: SDKCore, - args: z.output>, - extra: MCPServerContext, - ) => CallToolResult | Promise; - } - : { - name: string; - description: string; - scopes?: MCPScope[]; - args?: undefined; - annotations: { - title: string; - destructiveHint: boolean; - idempotentHint: boolean; - openWorldHint: boolean; - readOnlyHint: boolean; - }; - tool: ( - client: SDKCore, - extra: MCPServerContext, - ) => CallToolResult | Promise; +type ZodRawShapeCompat = z.ZodRawShape; +type ShapeOutput = z.output< + z.ZodObject +>; + +export type ToolDefinition< + Args extends undefined | ZodRawShapeCompat = undefined, +> = Args extends ZodRawShapeCompat ? { + name: string; + description: string; + scopes?: MCPScope[]; + args: Args; + annotations: { + title: string; + destructiveHint: boolean; + idempotentHint: boolean; + openWorldHint: boolean; + readOnlyHint: boolean; }; + tool: ( + client: SDKCore, + args: ShapeOutput, + extra: MCPServerContext, + ) => CallToolResult | Promise; + } + : { + name: string; + description: string; + scopes?: MCPScope[]; + args?: undefined; + annotations: { + title: string; + destructiveHint: boolean; + idempotentHint: boolean; + openWorldHint: boolean; + readOnlyHint: boolean; + }; + tool: ( + client: SDKCore, + extra: MCPServerContext, + ) => CallToolResult | Promise; + }; // Optional function to assist with formatting tool results export async function formatResult( From 833dbb26a5055b419d82d25dbc2345d698088d6e Mon Sep 17 00:00:00 2001 From: Thomas Rooney Date: Wed, 7 Oct 2026 17:14:10 +0100 Subject: [PATCH 4/6] fix(mcp): honor schema dialects and HTTP headers Convert tuple schemas and local references to the requested dialect without changing literal defaults or validation behavior. Advertise supported global parameters and server variables for Node HTTP while keeping SSE and Worker setup headers accurate. --- .../auxiliary/src/landing-page.ts.stmpl | 4 +- .../mcp-typescript/includes/templating.ts | 32 +++++-- .../primary/auth_additional.test.ts.stmpl | 11 +++ .../auxiliary/src/mcp-server/shared.ts.stmpl | 65 +++++++++++-- .../mcp_compatibility_additional.test.ts | 96 +++++++++++++++++++ zSDKs/mcp-typescript/.speakeasy/gen.lock | 2 +- zSDKs/mcp-typescript/src/landing-page.ts | 20 ++++ 7 files changed, 211 insertions(+), 19 deletions(-) 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 232e299a4..2044ba007 100644 --- a/templates/templates/mcp-typescript/auxiliary/src/landing-page.ts.stmpl +++ b/templates/templates/mcp-typescript/auxiliary/src/landing-page.ts.stmpl @@ -35,7 +35,9 @@ export function landingPageHTML(origin: string, transport: "sse" | "http" = "sse const endpoint = transport === "sse" ? "sse" : "mcp"; const headers: Array<{ name: string; env: string }> = runtime === "cloudflare" ? {{ templateMcpLandingPageHeaders "cloudflare" }} - : {{ templateMcpLandingPageHeaders "node" }}; + : 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}}`])], diff --git a/templates/templates/mcp-typescript/includes/templating.ts b/templates/templates/mcp-typescript/includes/templating.ts index 9535060d8..64f8b3c5a 100644 --- a/templates/templates/mcp-typescript/includes/templating.ts +++ b/templates/templates/mcp-typescript/includes/templating.ts @@ -722,9 +722,16 @@ function addHeadersToConfigVSCode(config: any, headerEntries: any[]) { return config; } -function mcpRemoteHeaders(runtime: "node" | "cloudflare") { - const security = context.Global.AST.MainSDK.Security?.Type; - if (!security) return []; +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) @@ -733,16 +740,24 @@ function mcpRemoteHeaders(runtime: "node" | "cloudflare") { name: header.headerName, env: caser().ToSNAKE(header.fieldName), })) - : unnestSecurityEnvFields(security).map((field) => ({ - name: sanitizeMCPCLIFlag(field.Name), - env: caser().ToSNAKE(field.Name), - })); + : (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") => JSON.stringify(mcpRemoteHeaders(runtime)), + (runtime: "node" | "cloudflare", transport: "http" | "sse" = "http") => + JSON.stringify(mcpRemoteHeaders(runtime, transport)), ); function mcpRemoteConfigCommand( @@ -758,6 +773,7 @@ function mcpRemoteConfigCommand( const headers: string[] = []; for (const header of mcpRemoteHeaders( context.Global.Config.CloudflareEnabled ? "cloudflare" : "node", + endpointType === "sse" ? "sse" : "http", )) { headers.push("--header", `${header.name}:\${${header.env}}`); } 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 b2dac7ce8..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 @@ -23,6 +23,17 @@ test.each(["sse", "http"] as const)("Node %s setup examples use accepted authent 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"}`); }); 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 5a88fd357..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 @@ -36,20 +36,27 @@ export function mcpSchema( shape: A, ): StandardSchemaWithJSON>, z.output>> { const schema = z.object(shape); - const jsonSchema = (direction: "input" | "output"): Record => JSON.parse(JSON.stringify( - zodToJsonSchema(schema, { - pipeStrategy: direction, - effectStrategy: direction === "output" ? "any" : "input", - strictUnions: direction === "input", - }), - (_key, value) => typeof value === "bigint" ? value.toString() : value, - )); + 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: () => jsonSchema("input"), output: () => jsonSchema("output") }, + jsonSchema: { input: ({ target }) => jsonSchema("input", target), output: ({ target }) => jsonSchema("output", target) }, validate: async (value) => { const result = await schema.safeParseAsync(value); return result.success @@ -60,6 +67,46 @@ export function mcpSchema( }; } +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 | ArrayBuffer diff --git a/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts b/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts index 05c7a8246..b32d1119e 100644 --- a/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts +++ b/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts @@ -1,5 +1,6 @@ import { Client } from "@modelcontextprotocol/client"; import { InMemoryTransport, McpServer } from "@modelcontextprotocol/server"; +import { AjvJsonSchemaValidator } from "@modelcontextprotocol/server/validators/ajv"; import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { ErrorCode, @@ -268,6 +269,101 @@ test("MCP output schemas do not claim input types for transforms or discard unkn }); }); +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"); diff --git a/zSDKs/mcp-typescript/.speakeasy/gen.lock b/zSDKs/mcp-typescript/.speakeasy/gen.lock index 4f3b6e1f5..477258428 100644 --- a/zSDKs/mcp-typescript/.speakeasy/gen.lock +++ b/zSDKs/mcp-typescript/.speakeasy/gen.lock @@ -554,7 +554,7 @@ trackedFiles: src/hooks/types.ts: last_write_checksum: sha1:29372122898cc523fa47576f3f6608a28ec6080e src/landing-page.ts: - last_write_checksum: sha1:7a7d2f0f2e4ef76f992da14f7321ce3aa47a3bb3 + last_write_checksum: sha1:2980781114eda92848b5c2ddc22e9b6f46891ca2 src/lib/base64.ts: last_write_checksum: sha1:e9f04a037018040361043104960982f7c22db52d src/lib/config.ts: diff --git a/zSDKs/mcp-typescript/src/landing-page.ts b/zSDKs/mcp-typescript/src/landing-page.ts index 2da67143d..32a594322 100644 --- a/zSDKs/mcp-typescript/src/landing-page.ts +++ b/zSDKs/mcp-typescript/src/landing-page.ts @@ -62,6 +62,18 @@ export function landingPageHTML( { "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" }, @@ -72,6 +84,14 @@ export function landingPageHTML( { "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", From a8f6555a95d38c88422ae79463c12d611b10fc13 Mon Sep 17 00:00:00 2001 From: Thomas Rooney Date: Wed, 7 Oct 2026 18:08:34 +0100 Subject: [PATCH 5/6] fix(mcp): preserve custom server configuration Keep optional MCP SDKs on Zod 3 at the package root so existing schemas retain their types. Preserve stdio option layout during regeneration while creating fresh servers for protocol fallback. --- .../mcp-server/cli/start/impl.ts.stmpl | 10 ++- .../mcp-server/create-mcp-server.ts.stmpl | 2 +- .../primary/dynamic_tools_additional.test.ts | 73 +++++++++++++++++++ templates/templates/typescriptv2/config.ts | 2 +- .../mcp_compatibility_additional.test.ts | 48 +++++++++++- zSDKs/mcp-typescript/.speakeasy/gen.lock | 2 +- .../src/mcp-server/cli/start/impl.ts | 72 +++++++++--------- 7 files changed, 166 insertions(+), 43 deletions(-) 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 36de6174b..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 @@ -48,10 +48,8 @@ export async function main(this: LocalContext, flags: StartCommandFlags) { async function startStdio(flags: StartCommandFlags) { const logger = createConsoleLogger(flags["log-level"]); - const handle = serveStdio(() => { - const { server } = {{ templateString "mcp-server/create-mcp-server.ts.stmpl" .Local }}; - return server; - }); + 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 handle.close(); @@ -61,6 +59,10 @@ async function startStdio(flags: StartCommandFlags) { 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/tests/primary/dynamic_tools_additional.test.ts b/templates/templates/mcp-typescript/tests/primary/dynamic_tools_additional.test.ts index 80f84fce0..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 @@ -854,6 +854,79 @@ test.each([false, true])( }, ); +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); + test.each([ ["legacy", "static"], ["legacy", "dynamic"], diff --git a/templates/templates/typescriptv2/config.ts b/templates/templates/typescriptv2/config.ts index 2ad96bd12..2cd758957 100644 --- a/templates/templates/typescriptv2/config.ts +++ b/templates/templates/typescriptv2/config.ts @@ -145,7 +145,7 @@ function getTemplateDependencies() { }, zodMCP: { name: "zod", - version: "^3.25.28 || ^4.0.0", + version: "^3.25.28", cpe: "cpe:2.3:a:colinhacks:zod:*:*:*:*:*:node.js:*:*", ecosystem: "npm", category: "runtime", diff --git a/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts b/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts index b32d1119e..88ad0001d 100644 --- a/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts +++ b/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts @@ -11,7 +11,7 @@ import { mcpServerContext, type MCPServerContext, } from "../mcp-server/shared.js"; -import { expect, test } from "vitest"; +import { expect, expectTypeOf, test } from "vitest"; import { SDKCore } from "../core.js"; import { createConsoleLogger } from "../mcp-server/console-logger.js"; import { @@ -20,6 +20,7 @@ import { } 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( @@ -42,6 +43,51 @@ async function withClient( } } +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", diff --git a/zSDKs/mcp-typescript/.speakeasy/gen.lock b/zSDKs/mcp-typescript/.speakeasy/gen.lock index 477258428..4b83243b1 100644 --- a/zSDKs/mcp-typescript/.speakeasy/gen.lock +++ b/zSDKs/mcp-typescript/.speakeasy/gen.lock @@ -598,7 +598,7 @@ trackedFiles: src/mcp-server/cli/start/command.ts: last_write_checksum: sha1:0b1d5a70b4fbce726352d22a584ed4a531cb0a52 src/mcp-server/cli/start/impl.ts: - last_write_checksum: sha1:fa434824ca40372f0aa641d676bc15c1d907b3d7 + last_write_checksum: sha1:4dc914eb22abec4dc5da655e7547d9e2b7a48063 src/mcp-server/console-logger.ts: last_write_checksum: sha1:0c4b9b7c79ad0a81359210cb31b9ee4d887d5de1 src/mcp-server/extensions.ts: 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 49158b4f3..b8448632d 100644 --- a/zSDKs/mcp-typescript/src/mcp-server/cli/start/impl.ts +++ b/zSDKs/mcp-typescript/src/mcp-server/cli/start/impl.ts @@ -48,42 +48,38 @@ export async function main(this: LocalContext, flags: StartCommandFlags) { async function startStdio(flags: StartCommandFlags) { const logger = createConsoleLogger(flags["log-level"]); - const handle = serveStdio(() => { - const { server } = createMCPServer({ - logger, - allowedTools: flags.tool, - dynamic: flags.mode === "dynamic", - annotationFilter: buildAnnotationFilter(flags["tool-annotations"]), - security: { - UserPassAuth: allRequired({ - username: flags.username, - password: flags.password, - }), - Option2: allRequired({ - bearerAuth: flags["bearer-auth"], - MyApiKey: flags["my-api-key"], - }), - Option3: allRequired({ oauth2: flags.oauth2 }), - Option4: allRequired({ appId: flags["app-id"], secret: flags.secret }), - Option5: allRequired({ mobileAuth: flags["mobile-auth"] }), - Option6: allRequired({ - clientCredentials: flags["client-credentials"], - }), - MyApiKey: allRequired({ MyApiKey: flags["my-api-key"] }), - }, - queryParam1: flags["query-param1"], - deprecatedQueryParam1: flags["deprecated-query-param1"], - deprecatedQueryParam2: flags["deprecated-query-param2"], - loneQueryParam: flags["lone-query-param"], - serverURL: flags["server-url"], - serverIdx: flags["server-index"], - subdomain: flags.subdomain, - version: flags["api-version"], - HostName: flags["api-host-name"], - PORT: flags["api-port"], - }); - return server; + const createServer = createMCPServerFactory({ + logger, + allowedTools: flags.tool, + dynamic: flags.mode === "dynamic", + annotationFilter: buildAnnotationFilter(flags["tool-annotations"]), + security: { + UserPassAuth: allRequired({ + username: flags.username, + password: flags.password, + }), + Option2: allRequired({ + bearerAuth: flags["bearer-auth"], + MyApiKey: flags["my-api-key"], + }), + Option3: allRequired({ oauth2: flags.oauth2 }), + Option4: allRequired({ appId: flags["app-id"], secret: flags.secret }), + Option5: allRequired({ mobileAuth: flags["mobile-auth"] }), + Option6: allRequired({ clientCredentials: flags["client-credentials"] }), + MyApiKey: allRequired({ MyApiKey: flags["my-api-key"] }), + }, + queryParam1: flags["query-param1"], + deprecatedQueryParam1: flags["deprecated-query-param1"], + deprecatedQueryParam2: flags["deprecated-query-param2"], + loneQueryParam: flags["lone-query-param"], + serverURL: flags["server-url"], + serverIdx: flags["server-index"], + subdomain: flags.subdomain, + version: flags["api-version"], + HostName: flags["api-host-name"], + PORT: flags["api-port"], }); + const handle = serveStdio(createServer); const abort = async () => { await handle.close(); @@ -93,6 +89,12 @@ async function startStdio(flags: StartCommandFlags) { 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(); From af5ccd13e14db051c7b5a3113138d0c15bc552a9 Mon Sep 17 00:00:00 2001 From: Thomas Rooney Date: Wed, 7 Oct 2026 18:41:20 +0100 Subject: [PATCH 6/6] fix(mcp): retain optional stdio customizations Keep the options object layout stable during regeneration and create fresh servers for stdio protocol fallback. --- .../mcp-server/cli/start/impl.ts.stmpl | 7 +- .../mcp-server/create-mcp-server.ts.stmpl | 4 +- .../mcp_compatibility_additional.test.ts | 77 ++++++++++++++++++- 3 files changed, 84 insertions(+), 4 deletions(-) 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 440ba4f21..bc05c3f6a 100644 --- a/templates/templates/typescriptv2/mcp-server/cli/start/impl.ts.stmpl +++ b/templates/templates/typescriptv2/mcp-server/cli/start/impl.ts.stmpl @@ -86,7 +86,8 @@ export async function main(this: LocalContext, flags: StartCommandFlags) { async function startStdio(flags: StartCommandFlags) { const logger = createConsoleLogger(flags["log-level"]); - const server = serveStdio(() => {{templateString "mcp-server/create-mcp-server.ts.stmpl" .Local}}); + 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(); @@ -96,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/tests/tertiary/mcp_compatibility_additional.test.ts b/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts index 88ad0001d..6f46f35f7 100644 --- a/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts +++ b/templates/templates/typescriptv2/tests/tertiary/mcp_compatibility_additional.test.ts @@ -1,4 +1,6 @@ +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"; @@ -11,7 +13,7 @@ import { mcpServerContext, type MCPServerContext, } from "../mcp-server/shared.js"; -import { expect, expectTypeOf, test } from "vitest"; +import { expect, expectTypeOf, test, vi } from "vitest"; import { SDKCore } from "../core.js"; import { createConsoleLogger } from "../mcp-server/console-logger.js"; import { @@ -465,3 +467,76 @@ test("tools with arguments and static resources preserve legacy context", async ).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);