diff --git a/jest.config.js b/jest.config.js index f8f621c8b..1ea3839c4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,4 +13,5 @@ export default { "/node_modules/(?!eventsource)/" ], testPathIgnorePatterns: ["/node_modules/", "/dist/"], + modulePathIgnorePatterns: ["/dist"], }; diff --git a/package-lock.json b/package-lock.json index 1e0b12ed7..f8b256ab4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,8 +19,7 @@ "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^4.0.17" }, "devDependencies": { "@eslint/js": "^9.8.0", @@ -6639,22 +6638,13 @@ } }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz", + "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz", - "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } } } } diff --git a/package.json b/package.json index 697b051be..7859f2c62 100644 --- a/package.json +++ b/package.json @@ -72,8 +72,7 @@ "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^4.0.17" }, "devDependencies": { "@eslint/js": "^9.8.0", diff --git a/src/client/index.test.ts b/src/client/index.test.ts index abd0c34e4..41fb4c1aa 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -2,7 +2,7 @@ /* eslint-disable no-constant-binary-expression */ /* eslint-disable @typescript-eslint/no-unused-expressions */ import { Client } from "./index.js"; -import { z } from "zod"; +import { z } from "zod/v4"; import { RequestSchema, NotificationSchema, diff --git a/src/examples/server/jsonResponseStreamableHttp.ts b/src/examples/server/jsonResponseStreamableHttp.ts index d6501d275..1dfed4f65 100644 --- a/src/examples/server/jsonResponseStreamableHttp.ts +++ b/src/examples/server/jsonResponseStreamableHttp.ts @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { CallToolResult, isInitializeRequest } from '../../types.js'; import cors from 'cors'; diff --git a/src/examples/server/mcpServerOutputSchema.ts b/src/examples/server/mcpServerOutputSchema.ts index 75bfe6900..1a43d24e2 100644 --- a/src/examples/server/mcpServerOutputSchema.ts +++ b/src/examples/server/mcpServerOutputSchema.ts @@ -6,7 +6,7 @@ import { McpServer } from "../../server/mcp.js"; import { StdioServerTransport } from "../../server/stdio.js"; -import { z } from "zod"; +import { z } from "zod/v4"; const server = new McpServer( { diff --git a/src/examples/server/simpleSseServer.ts b/src/examples/server/simpleSseServer.ts index f8bdd4662..4b34a41b6 100644 --- a/src/examples/server/simpleSseServer.ts +++ b/src/examples/server/simpleSseServer.ts @@ -1,7 +1,7 @@ import express, { Request, Response } from 'express'; import { McpServer } from '../../server/mcp.js'; import { SSEServerTransport } from '../../server/sse.js'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { CallToolResult } from '../../types.js'; /** diff --git a/src/examples/server/simpleStatelessStreamableHttp.ts b/src/examples/server/simpleStatelessStreamableHttp.ts index b5a1e291e..e41b059e0 100644 --- a/src/examples/server/simpleStatelessStreamableHttp.ts +++ b/src/examples/server/simpleStatelessStreamableHttp.ts @@ -1,7 +1,7 @@ import express, { Request, Response } from 'express'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js'; import cors from 'cors'; diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 98f9d351c..c347aa879 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -1,6 +1,6 @@ import express, { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js'; diff --git a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts index e097ca70e..653cc8b16 100644 --- a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts +++ b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto"; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { SSEServerTransport } from '../../server/sse.js'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { CallToolResult, isInitializeRequest } from '../../types.js'; import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; import cors from 'cors'; diff --git a/src/examples/server/toolWithSampleServer.ts b/src/examples/server/toolWithSampleServer.ts index 44e5cecbb..bf480a858 100644 --- a/src/examples/server/toolWithSampleServer.ts +++ b/src/examples/server/toolWithSampleServer.ts @@ -3,7 +3,7 @@ import { McpServer } from "../../server/mcp.js"; import { StdioServerTransport } from "../../server/stdio.js"; -import { z } from "zod"; +import { z } from "zod/v4"; const mcpServer = new McpServer({ name: "tools-with-sample-server", diff --git a/src/integration-tests/stateManagementStreamableHttp.test.ts b/src/integration-tests/stateManagementStreamableHttp.test.ts index 4a191134b..48ceac2e8 100644 --- a/src/integration-tests/stateManagementStreamableHttp.test.ts +++ b/src/integration-tests/stateManagementStreamableHttp.test.ts @@ -6,7 +6,7 @@ import { StreamableHTTPClientTransport } from '../client/streamableHttp.js'; import { McpServer } from '../server/mcp.js'; import { StreamableHTTPServerTransport } from '../server/streamableHttp.js'; import { CallToolResultSchema, ListToolsResultSchema, ListResourcesResultSchema, ListPromptsResultSchema, LATEST_PROTOCOL_VERSION } from '../types.js'; -import { z } from 'zod'; +import { z } from 'zod/v4'; describe('Streamable HTTP Transport Session Management', () => { // Function to set up the server with optional session management diff --git a/src/integration-tests/taskResumability.test.ts b/src/integration-tests/taskResumability.test.ts index efd2611f8..e5402b95e 100644 --- a/src/integration-tests/taskResumability.test.ts +++ b/src/integration-tests/taskResumability.test.ts @@ -6,7 +6,7 @@ import { StreamableHTTPClientTransport } from '../client/streamableHttp.js'; import { McpServer } from '../server/mcp.js'; import { StreamableHTTPServerTransport } from '../server/streamableHttp.js'; import { CallToolResultSchema, LoggingMessageNotificationSchema } from '../types.js'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { InMemoryEventStore } from '../examples/shared/inMemoryEventStore.js'; diff --git a/src/server/auth/handlers/authorize.ts b/src/server/auth/handlers/authorize.ts index 126ce006b..81078e0de 100644 --- a/src/server/auth/handlers/authorize.ts +++ b/src/server/auth/handlers/authorize.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { z } from "zod"; +import { z } from "zod/v4"; import express from "express"; import { OAuthServerProvider } from "../provider.js"; import { rateLimit, Options as RateLimitOptions } from "express-rate-limit"; @@ -25,7 +25,9 @@ export type AuthorizationHandlerOptions = { // Parameters that must be validated in order to issue redirects. const ClientAuthorizationParamsSchema = z.object({ client_id: z.string(), - redirect_uri: z.string().optional().refine((value) => value === undefined || URL.canParse(value), { message: "redirect_uri must be a valid URL" }), + redirect_uri: z.string().optional().refine((value) => value === undefined || URL.canParse(value), { + error: "redirect_uri must be a valid URL" +}), }); // Parameters that must be validated for a successful authorization request. Failure can be reported to the redirect URI. @@ -35,7 +37,7 @@ const RequestAuthorizationParamsSchema = z.object({ code_challenge_method: z.literal("S256"), scope: z.string().optional(), state: z.string().optional(), - resource: z.string().url().optional(), + resource: z.url().optional(), }); export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: AuthorizationHandlerOptions): RequestHandler { diff --git a/src/server/auth/handlers/token.ts b/src/server/auth/handlers/token.ts index b2ab74391..caf36d0d1 100644 --- a/src/server/auth/handlers/token.ts +++ b/src/server/auth/handlers/token.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import express, { RequestHandler } from "express"; import { OAuthServerProvider } from "../provider.js"; import cors from "cors"; @@ -32,13 +32,13 @@ const AuthorizationCodeGrantSchema = z.object({ code: z.string(), code_verifier: z.string(), redirect_uri: z.string().optional(), - resource: z.string().url().optional(), + resource: z.url().optional(), }); const RefreshTokenGrantSchema = z.object({ refresh_token: z.string(), scope: z.string().optional(), - resource: z.string().url().optional(), + resource: z.url().optional(), }); export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHandlerOptions): RequestHandler { diff --git a/src/server/auth/middleware/clientAuth.ts b/src/server/auth/middleware/clientAuth.ts index ecd9a7b65..682cb269f 100644 --- a/src/server/auth/middleware/clientAuth.ts +++ b/src/server/auth/middleware/clientAuth.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { RequestHandler } from "express"; import { OAuthRegisteredClientsStore } from "../clients.js"; import { OAuthClientInformationFull } from "../../../shared/auth.js"; diff --git a/src/server/completable.test.ts b/src/server/completable.test.ts index 6040ff3f6..66184128d 100644 --- a/src/server/completable.test.ts +++ b/src/server/completable.test.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { completable } from "./completable.js"; describe("completable", () => { diff --git a/src/server/completable.ts b/src/server/completable.ts index 652eaf72e..d83c0c888 100644 --- a/src/server/completable.ts +++ b/src/server/completable.ts @@ -1,13 +1,4 @@ -import { - ZodTypeAny, - ZodTypeDef, - ZodType, - ParseInput, - ParseReturnType, - RawCreateParams, - ZodErrorMap, - ProcessedCreateParams, -} from "zod"; +import { ZodTypeAny } from "zod/v4"; export enum McpZodTypeKind { Completable = "McpCompletable", @@ -17,82 +8,38 @@ export type CompleteCallback = ( value: T["_input"], context?: { arguments?: Record; - }, + } ) => T["_input"][] | Promise; -export interface CompletableDef - extends ZodTypeDef { +export interface CompletableDef { type: T; complete: CompleteCallback; typeName: McpZodTypeKind.Completable; } -export class Completable extends ZodType< - T["_output"], - CompletableDef, - T["_input"] -> { - _parse(input: ParseInput): ParseReturnType { - const { ctx } = this._processInputParams(input); - const data = ctx.data; - return this._def.type._parse({ - data, - path: ctx.path, - parent: ctx, - }); - } - - unwrap() { - return this._def.type; - } - - static create = ( - type: T, - params: RawCreateParams & { - complete: CompleteCallback; - }, - ): Completable => { - return new Completable({ - type, - typeName: McpZodTypeKind.Completable, - complete: params.complete, - ...processCreateParams(params), - }); - }; -} - /** * Wraps a Zod type to provide autocompletion capabilities. Useful for, e.g., prompt arguments in MCP. */ export function completable( schema: T, - complete: CompleteCallback, -): Completable { - return Completable.create(schema, { ...schema._def, complete }); -} - -// Not sure why this isn't exported from Zod: -// https://github.com/colinhacks/zod/blob/f7ad26147ba291cb3fb257545972a8e00e767470/src/types.ts#L130 -function processCreateParams(params: RawCreateParams): ProcessedCreateParams { - if (!params) return {}; - const { errorMap, invalid_type_error, required_error, description } = params; - if (errorMap && (invalid_type_error || required_error)) { - throw new Error( - `Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`, - ); + complete: CompleteCallback +): T & { + _def: (T extends { _def: infer D } ? D : unknown) & CompletableDef; +} { + const target = schema as unknown as { _def?: Record }; + const originalDef = (target._def ?? {}) as Record; + // Only mutate the existing _def object to respect read-only property semantics + if ( + (originalDef as { typeName?: unknown }).typeName !== + McpZodTypeKind.Completable + ) { + (originalDef as { typeName?: McpZodTypeKind; type?: ZodTypeAny }).typeName = + McpZodTypeKind.Completable; + (originalDef as { typeName?: McpZodTypeKind; type?: ZodTypeAny }).type = + schema; } - if (errorMap) return { errorMap: errorMap, description }; - const customMap: ZodErrorMap = (iss, ctx) => { - const { message } = params; - - if (iss.code === "invalid_enum_value") { - return { message: message ?? ctx.defaultError }; - } - if (typeof ctx.data === "undefined") { - return { message: message ?? required_error ?? ctx.defaultError }; - } - if (iss.code !== "invalid_type") return { message: ctx.defaultError }; - return { message: message ?? invalid_type_error ?? ctx.defaultError }; + (originalDef as { complete?: CompleteCallback }).complete = complete; + return schema as unknown as T & { + _def: (T extends { _def: infer D } ? D : unknown) & CompletableDef; }; - return { errorMap: customMap, description }; } diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 46205d726..ac5449ae6 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -2,7 +2,7 @@ /* eslint-disable no-constant-binary-expression */ /* eslint-disable @typescript-eslint/no-unused-expressions */ import { Server } from "./index.js"; -import { z } from "zod"; +import { z } from "zod/v4"; import { RequestSchema, NotificationSchema, diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 791facef1..2890d7269 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1,16 +1,15 @@ import { Server, ServerOptions } from "./index.js"; -import { zodToJsonSchema } from "zod-to-json-schema"; +// Lightweight replacement for zod-to-json-schema tailored for this SDK's needs. +// Produces a minimal JSON Schema subset: { type: "object", properties, required? } +// Supports primitives, enums, and nested objects. Sufficient for our tool list output tests. import { z, ZodRawShape, ZodObject, ZodString, - AnyZodObject, - ZodTypeAny, ZodType, - ZodTypeDef, ZodOptional, -} from "zod"; +} from "zod/v4"; import { Implementation, Tool, @@ -42,11 +41,12 @@ import { ServerNotification, ToolAnnotations, } from "../types.js"; -import { Completable, CompletableDef } from "./completable.js"; +import { CompletableDef, McpZodTypeKind } from "./completable.js"; import { UriTemplate, Variables } from "../shared/uriTemplate.js"; import { RequestHandlerExtra } from "../shared/protocol.js"; import { Transport } from "../shared/transport.js"; + /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. * For advanced usage (like sending notifications or setting custom request handlers), use the underlying @@ -101,39 +101,34 @@ export class McpServer { this.server.registerCapabilities({ tools: { - listChanged: true - } - }) + listChanged: true, + }, + }); this.server.setRequestHandler( ListToolsRequestSchema, (): ListToolsResult => ({ - tools: Object.entries(this._registeredTools).filter( - ([, tool]) => tool.enabled, - ).map( - ([name, tool]): Tool => { + tools: Object.entries(this._registeredTools) + .filter(([, tool]) => tool.enabled) + .map(([name, tool]): Tool => { const toolDefinition: Tool = { name, title: tool.title, description: tool.description, inputSchema: tool.inputSchema - ? (zodToJsonSchema(tool.inputSchema, { - strictUnions: true, - }) as Tool["inputSchema"]) + ? z.toJSONSchema(tool.inputSchema) as Tool["inputSchema"] : EMPTY_OBJECT_JSON_SCHEMA, annotations: tool.annotations, }; if (tool.outputSchema) { - toolDefinition.outputSchema = zodToJsonSchema( + toolDefinition.outputSchema = z.toJSONSchema( tool.outputSchema, - { strictUnions: true } ) as Tool["outputSchema"]; } return toolDefinition; - }, - ), + }), }), ); @@ -288,12 +283,22 @@ export class McpServer { } const field = prompt.argsSchema.shape[request.params.argument.name]; - if (!(field instanceof Completable)) { + const defLike = (field as unknown as { _def?: { typeName?: unknown } }) + ._def; + if (!defLike || defLike.typeName !== McpZodTypeKind.Completable) { return EMPTY_COMPLETION_RESULT; } - const def: CompletableDef = field._def; - const suggestions = await def.complete(request.params.argument.value, request.params.context); + const def = (field as unknown as { _def: CompletableDef })._def; + const ctx = request.params.context; + const ctxForComplete = + ctx && typeof ctx.arguments !== "string" + ? { arguments: ctx.arguments } + : undefined; + const suggestions = await def.complete( + request.params.argument.value, + ctxForComplete, + ); return createCompletionResult(suggestions); } @@ -324,7 +329,15 @@ export class McpServer { return EMPTY_COMPLETION_RESULT; } - const suggestions = await completer(request.params.argument.value, request.params.context); + const ctx = request.params.context; + const ctxForComplete = + ctx && typeof ctx.arguments !== "string" + ? { arguments: ctx.arguments } + : undefined; + const suggestions = await completer( + request.params.argument.value, + ctxForComplete, + ); return createCompletionResult(suggestions); } @@ -347,22 +360,20 @@ export class McpServer { this.server.registerCapabilities({ resources: { - listChanged: true - } - }) + listChanged: true, + }, + }); this.server.setRequestHandler( ListResourcesRequestSchema, async (request, extra) => { - const resources = Object.entries(this._registeredResources).filter( - ([_, resource]) => resource.enabled, - ).map( - ([uri, resource]) => ({ + const resources = Object.entries(this._registeredResources) + .filter(([_, resource]) => resource.enabled) + .map(([uri, resource]) => ({ uri, name: resource.name, ...resource.metadata, - }), - ); + })); const templateResources: Resource[] = []; for (const template of Object.values( @@ -458,17 +469,16 @@ export class McpServer { this.server.registerCapabilities({ prompts: { - listChanged: true - } - }) + listChanged: true, + }, + }); this.server.setRequestHandler( ListPromptsRequestSchema, (): ListPromptsResult => ({ - prompts: Object.entries(this._registeredPrompts).filter( - ([, prompt]) => prompt.enabled, - ).map( - ([name, prompt]): Prompt => { + prompts: Object.entries(this._registeredPrompts) + .filter(([, prompt]) => prompt.enabled) + .map(([name, prompt]): Prompt => { return { name, title: prompt.title, @@ -477,8 +487,7 @@ export class McpServer { ? promptArgumentsFromSchema(prompt.argsSchema) : undefined, }; - }, - ), + }), }), ); @@ -529,7 +538,11 @@ export class McpServer { /** * Registers a resource `name` at a fixed URI, which will use the given callback to respond to read requests. */ - resource(name: string, uri: string, readCallback: ReadResourceCallback): RegisteredResource; + resource( + name: string, + uri: string, + readCallback: ReadResourceCallback, + ): RegisteredResource; /** * Registers a resource `name` at a fixed URI with metadata, which will use the given callback to respond to read requests. @@ -584,7 +597,7 @@ export class McpServer { undefined, uriOrTemplate, metadata, - readCallback as ReadResourceCallback + readCallback as ReadResourceCallback, ); this.setResourceRequestHandlers(); @@ -600,7 +613,7 @@ export class McpServer { undefined, uriOrTemplate, metadata, - readCallback as ReadResourceTemplateCallback + readCallback as ReadResourceTemplateCallback, ); this.setResourceRequestHandlers(); @@ -617,19 +630,19 @@ export class McpServer { name: string, uriOrTemplate: string, config: ResourceMetadata, - readCallback: ReadResourceCallback + readCallback: ReadResourceCallback, ): RegisteredResource; registerResource( name: string, uriOrTemplate: ResourceTemplate, config: ResourceMetadata, - readCallback: ReadResourceTemplateCallback + readCallback: ReadResourceTemplateCallback, ): RegisteredResourceTemplate; registerResource( name: string, uriOrTemplate: string | ResourceTemplate, config: ResourceMetadata, - readCallback: ReadResourceCallback | ReadResourceTemplateCallback + readCallback: ReadResourceCallback | ReadResourceTemplateCallback, ): RegisteredResource | RegisteredResourceTemplate { if (typeof uriOrTemplate === "string") { if (this._registeredResources[uriOrTemplate]) { @@ -641,7 +654,7 @@ export class McpServer { (config as BaseMetadata).title, uriOrTemplate, config, - readCallback as ReadResourceCallback + readCallback as ReadResourceCallback, ); this.setResourceRequestHandlers(); @@ -657,7 +670,7 @@ export class McpServer { (config as BaseMetadata).title, uriOrTemplate, config, - readCallback as ReadResourceTemplateCallback + readCallback as ReadResourceTemplateCallback, ); this.setResourceRequestHandlers(); @@ -671,7 +684,7 @@ export class McpServer { title: string | undefined, uri: string, metadata: ResourceMetadata | undefined, - readCallback: ReadResourceCallback + readCallback: ReadResourceCallback, ): RegisteredResource { const registeredResource: RegisteredResource = { name, @@ -684,15 +697,21 @@ export class McpServer { remove: () => registeredResource.update({ uri: null }), update: (updates) => { if (typeof updates.uri !== "undefined" && updates.uri !== uri) { - delete this._registeredResources[uri] - if (updates.uri) this._registeredResources[updates.uri] = registeredResource + delete this._registeredResources[uri]; + if (updates.uri) + this._registeredResources[updates.uri] = registeredResource; } - if (typeof updates.name !== "undefined") registeredResource.name = updates.name - if (typeof updates.title !== "undefined") registeredResource.title = updates.title - if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata - if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback - if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled - this.sendResourceListChanged() + if (typeof updates.name !== "undefined") + registeredResource.name = updates.name; + if (typeof updates.title !== "undefined") + registeredResource.title = updates.title; + if (typeof updates.metadata !== "undefined") + registeredResource.metadata = updates.metadata; + if (typeof updates.callback !== "undefined") + registeredResource.readCallback = updates.callback; + if (typeof updates.enabled !== "undefined") + registeredResource.enabled = updates.enabled; + this.sendResourceListChanged(); }, }; this._registeredResources[uri] = registeredResource; @@ -704,7 +723,7 @@ export class McpServer { title: string | undefined, template: ResourceTemplate, metadata: ResourceMetadata | undefined, - readCallback: ReadResourceTemplateCallback + readCallback: ReadResourceTemplateCallback, ): RegisteredResourceTemplate { const registeredResourceTemplate: RegisteredResourceTemplate = { resourceTemplate: template, @@ -717,15 +736,22 @@ export class McpServer { remove: () => registeredResourceTemplate.update({ name: null }), update: (updates) => { if (typeof updates.name !== "undefined" && updates.name !== name) { - delete this._registeredResourceTemplates[name] - if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate + delete this._registeredResourceTemplates[name]; + if (updates.name) + this._registeredResourceTemplates[updates.name] = + registeredResourceTemplate; } - if (typeof updates.title !== "undefined") registeredResourceTemplate.title = updates.title - if (typeof updates.template !== "undefined") registeredResourceTemplate.resourceTemplate = updates.template - if (typeof updates.metadata !== "undefined") registeredResourceTemplate.metadata = updates.metadata - if (typeof updates.callback !== "undefined") registeredResourceTemplate.readCallback = updates.callback - if (typeof updates.enabled !== "undefined") registeredResourceTemplate.enabled = updates.enabled - this.sendResourceListChanged() + if (typeof updates.title !== "undefined") + registeredResourceTemplate.title = updates.title; + if (typeof updates.template !== "undefined") + registeredResourceTemplate.resourceTemplate = updates.template; + if (typeof updates.metadata !== "undefined") + registeredResourceTemplate.metadata = updates.metadata; + if (typeof updates.callback !== "undefined") + registeredResourceTemplate.readCallback = updates.callback; + if (typeof updates.enabled !== "undefined") + registeredResourceTemplate.enabled = updates.enabled; + this.sendResourceListChanged(); }, }; this._registeredResourceTemplates[name] = registeredResourceTemplate; @@ -737,7 +763,7 @@ export class McpServer { title: string | undefined, description: string | undefined, argsSchema: PromptArgsRawShape | undefined, - callback: PromptCallback + callback: PromptCallback, ): RegisteredPrompt { const registeredPrompt: RegisteredPrompt = { title, @@ -750,15 +776,21 @@ export class McpServer { remove: () => registeredPrompt.update({ name: null }), update: (updates) => { if (typeof updates.name !== "undefined" && updates.name !== name) { - delete this._registeredPrompts[name] - if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt + delete this._registeredPrompts[name]; + if (updates.name) + this._registeredPrompts[updates.name] = registeredPrompt; } - if (typeof updates.title !== "undefined") registeredPrompt.title = updates.title - if (typeof updates.description !== "undefined") registeredPrompt.description = updates.description - if (typeof updates.argsSchema !== "undefined") registeredPrompt.argsSchema = z.object(updates.argsSchema) - if (typeof updates.callback !== "undefined") registeredPrompt.callback = updates.callback - if (typeof updates.enabled !== "undefined") registeredPrompt.enabled = updates.enabled - this.sendPromptListChanged() + if (typeof updates.title !== "undefined") + registeredPrompt.title = updates.title; + if (typeof updates.description !== "undefined") + registeredPrompt.description = updates.description; + if (typeof updates.argsSchema !== "undefined") + registeredPrompt.argsSchema = z.object(updates.argsSchema); + if (typeof updates.callback !== "undefined") + registeredPrompt.callback = updates.callback; + if (typeof updates.enabled !== "undefined") + registeredPrompt.enabled = updates.enabled; + this.sendPromptListChanged(); }, }; this._registeredPrompts[name] = registeredPrompt; @@ -772,7 +804,7 @@ export class McpServer { inputSchema: ZodRawShape | undefined, outputSchema: ZodRawShape | undefined, annotations: ToolAnnotations | undefined, - callback: ToolCallback + callback: ToolCallback, ): RegisteredTool { const registeredTool: RegisteredTool = { title, @@ -789,24 +821,31 @@ export class McpServer { remove: () => registeredTool.update({ name: null }), update: (updates) => { if (typeof updates.name !== "undefined" && updates.name !== name) { - delete this._registeredTools[name] - if (updates.name) this._registeredTools[updates.name] = registeredTool + delete this._registeredTools[name]; + if (updates.name) + this._registeredTools[updates.name] = registeredTool; } - if (typeof updates.title !== "undefined") registeredTool.title = updates.title - if (typeof updates.description !== "undefined") registeredTool.description = updates.description - if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = z.object(updates.paramsSchema) - if (typeof updates.callback !== "undefined") registeredTool.callback = updates.callback - if (typeof updates.annotations !== "undefined") registeredTool.annotations = updates.annotations - if (typeof updates.enabled !== "undefined") registeredTool.enabled = updates.enabled - this.sendToolListChanged() + if (typeof updates.title !== "undefined") + registeredTool.title = updates.title; + if (typeof updates.description !== "undefined") + registeredTool.description = updates.description; + if (typeof updates.paramsSchema !== "undefined") + registeredTool.inputSchema = z.object(updates.paramsSchema); + if (typeof updates.callback !== "undefined") + registeredTool.callback = updates.callback; + if (typeof updates.annotations !== "undefined") + registeredTool.annotations = updates.annotations; + if (typeof updates.enabled !== "undefined") + registeredTool.enabled = updates.enabled; + this.sendToolListChanged(); }, }; this._registeredTools[name] = registeredTool; this.setToolRequestHandlers(); - this.sendToolListChanged() + this.sendToolListChanged(); - return registeredTool + return registeredTool; } /** @@ -822,7 +861,7 @@ export class McpServer { /** * Registers a tool taking either a parameter schema for validation or annotations for additional metadata. * This unified overload handles both `tool(name, paramsSchema, cb)` and `tool(name, annotations, cb)` cases. - * + * * Note: We use a union type for the second parameter because TypeScript cannot reliably disambiguate * between ToolAnnotations and ZodRawShape during overload resolution, as both are plain object types. */ @@ -834,9 +873,9 @@ export class McpServer { /** * Registers a tool `name` (with a description) taking either parameter schema or annotations. - * This unified overload handles both `tool(name, description, paramsSchema, cb)` and + * This unified overload handles both `tool(name, description, paramsSchema, cb)` and * `tool(name, description, annotations, cb)` cases. - * + * * Note: We use a union type for the third parameter because TypeScript cannot reliably disambiguate * between ToolAnnotations and ZodRawShape during overload resolution, as both are plain object types. */ @@ -868,7 +907,6 @@ export class McpServer { cb: ToolCallback, ): RegisteredTool; - /** * tool() implementation. Parses arguments passed to overrides defined above. */ @@ -900,7 +938,12 @@ export class McpServer { inputSchema = rest.shift() as ZodRawShape; // Check if the next arg is potentially annotations - if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !(isZodRawShape(rest[0]))) { + if ( + rest.length > 1 && + typeof rest[0] === "object" && + rest[0] !== null && + !isZodRawShape(rest[0]) + ) { // Case: tool(name, paramsSchema, annotations, cb) // Or: tool(name, description, paramsSchema, annotations, cb) annotations = rest.shift() as ToolAnnotations; @@ -914,7 +957,15 @@ export class McpServer { } const callback = rest[0] as ToolCallback; - return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, callback) + return this._createRegisteredTool( + name, + undefined, + description, + inputSchema, + outputSchema, + annotations, + callback, + ); } /** @@ -929,13 +980,14 @@ export class McpServer { outputSchema?: OutputArgs; annotations?: ToolAnnotations; }, - cb: ToolCallback + cb: ToolCallback, ): RegisteredTool { if (this._registeredTools[name]) { throw new Error(`Tool ${name} is already registered`); } - const { title, description, inputSchema, outputSchema, annotations } = config; + const { title, description, inputSchema, outputSchema, annotations } = + config; return this._createRegisteredTool( name, @@ -944,7 +996,7 @@ export class McpServer { inputSchema, outputSchema, annotations, - cb as ToolCallback + cb as ToolCallback, ); } @@ -956,7 +1008,11 @@ export class McpServer { /** * Registers a zero-argument prompt `name` (with a description) which will run the given function when the client calls it. */ - prompt(name: string, description: string, cb: PromptCallback): RegisteredPrompt; + prompt( + name: string, + description: string, + cb: PromptCallback, + ): RegisteredPrompt; /** * Registers a prompt `name` accepting the given arguments, which must be an object containing named properties associated with Zod schemas. When the client calls it, the function will be run with the parsed and validated arguments. @@ -998,13 +1054,13 @@ export class McpServer { undefined, description, argsSchema, - cb + cb, ); this.setPromptRequestHandlers(); - this.sendPromptListChanged() + this.sendPromptListChanged(); - return registeredPrompt + return registeredPrompt; } /** @@ -1017,7 +1073,7 @@ export class McpServer { description?: string; argsSchema?: Args; }, - cb: PromptCallback + cb: PromptCallback, ): RegisteredPrompt { if (this._registeredPrompts[name]) { throw new Error(`Prompt ${name} is already registered`); @@ -1030,11 +1086,11 @@ export class McpServer { title, description, argsSchema, - cb as PromptCallback + cb as PromptCallback, ); this.setPromptRequestHandlers(); - this.sendPromptListChanged() + this.sendPromptListChanged(); return registeredPrompt; } @@ -1044,7 +1100,7 @@ export class McpServer { * @returns True if the server is connected */ isConnected() { - return this.server.transport !== undefined + return this.server.transport !== undefined; } /** @@ -1150,34 +1206,38 @@ export class ResourceTemplate { */ export type ToolCallback = Args extends ZodRawShape - ? ( - args: z.objectOutputType, - extra: RequestHandlerExtra, - ) => CallToolResult | Promise - : (extra: RequestHandlerExtra) => CallToolResult | Promise; + ? ( + args: z.infer>, + extra: RequestHandlerExtra, + ) => CallToolResult | Promise + : ( + extra: RequestHandlerExtra, + ) => CallToolResult | Promise; export type RegisteredTool = { title?: string; description?: string; - inputSchema?: AnyZodObject; - outputSchema?: AnyZodObject; + inputSchema?: ZodObject; + outputSchema?: ZodObject; annotations?: ToolAnnotations; callback: ToolCallback; enabled: boolean; enable(): void; disable(): void; - update( - updates: { - name?: string | null, - title?: string, - description?: string, - paramsSchema?: InputArgs, - outputSchema?: OutputArgs, - annotations?: ToolAnnotations, - callback?: ToolCallback, - enabled?: boolean - }): void - remove(): void + update< + InputArgs extends ZodRawShape, + OutputArgs extends ZodRawShape, + >(updates: { + name?: string | null; + title?: string; + description?: string; + paramsSchema?: InputArgs; + outputSchema?: OutputArgs; + annotations?: ToolAnnotations; + callback?: ToolCallback; + enabled?: boolean; + }): void; + remove(): void; }; const EMPTY_OBJECT_JSON_SCHEMA = { @@ -1197,10 +1257,14 @@ function isZodRawShape(obj: unknown): obj is ZodRawShape { } function isZodTypeLike(value: unknown): value is ZodType { - return value !== null && - typeof value === 'object' && - 'parse' in value && typeof value.parse === 'function' && - 'safeParse' in value && typeof value.safeParse === 'function'; + return ( + value !== null && + typeof value === "object" && + "parse" in value && + typeof value.parse === "function" && + "safeParse" in value && + typeof value.safeParse === "function" + ); } /** @@ -1231,8 +1295,15 @@ export type RegisteredResource = { enabled: boolean; enable(): void; disable(): void; - update(updates: { name?: string, title?: string, uri?: string | null, metadata?: ResourceMetadata, callback?: ReadResourceCallback, enabled?: boolean }): void - remove(): void + update(updates: { + name?: string; + title?: string; + uri?: string | null; + metadata?: ResourceMetadata; + callback?: ReadResourceCallback; + enabled?: boolean; + }): void; + remove(): void; }; /** @@ -1252,24 +1323,29 @@ export type RegisteredResourceTemplate = { enabled: boolean; enable(): void; disable(): void; - update(updates: { name?: string | null, title?: string, template?: ResourceTemplate, metadata?: ResourceMetadata, callback?: ReadResourceTemplateCallback, enabled?: boolean }): void - remove(): void + update(updates: { + name?: string | null; + title?: string; + template?: ResourceTemplate; + metadata?: ResourceMetadata; + callback?: ReadResourceTemplateCallback; + enabled?: boolean; + }): void; + remove(): void; }; -type PromptArgsRawShape = { - [k: string]: - | ZodType - | ZodOptional>; -}; +type PromptArgsRawShape = Record>; export type PromptCallback< Args extends undefined | PromptArgsRawShape = undefined, > = Args extends PromptArgsRawShape ? ( - args: z.objectOutputType, - extra: RequestHandlerExtra, - ) => GetPromptResult | Promise - : (extra: RequestHandlerExtra) => GetPromptResult | Promise; + args: z.infer>, + extra: RequestHandlerExtra, + ) => GetPromptResult | Promise + : ( + extra: RequestHandlerExtra, + ) => GetPromptResult | Promise; export type RegisteredPrompt = { title?: string; @@ -1279,8 +1355,15 @@ export type RegisteredPrompt = { enabled: boolean; enable(): void; disable(): void; - update(updates: { name?: string | null, title?: string, description?: string, argsSchema?: Args, callback?: PromptCallback, enabled?: boolean }): void - remove(): void + update(updates: { + name?: string | null; + title?: string; + description?: string; + argsSchema?: Args; + callback?: PromptCallback; + enabled?: boolean; + }): void; + remove(): void; }; function promptArgumentsFromSchema( diff --git a/src/server/sse.test.ts b/src/server/sse.test.ts index a7f180961..aa10a534a 100644 --- a/src/server/sse.test.ts +++ b/src/server/sse.test.ts @@ -4,7 +4,7 @@ import { SSEServerTransport } from './sse.js'; import { McpServer } from './mcp.js'; import { createServer, type Server } from "node:http"; import { AddressInfo } from "node:net"; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { CallToolResult, JSONRPCMessage } from 'src/types.js'; const createMockResponse = () => { diff --git a/src/server/streamableHttp.test.ts b/src/server/streamableHttp.test.ts index 3a0a5c066..9c5ccc3cf 100644 --- a/src/server/streamableHttp.test.ts +++ b/src/server/streamableHttp.test.ts @@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto"; import { EventStore, StreamableHTTPServerTransport, EventId, StreamId } from "./streamableHttp.js"; import { McpServer } from "./mcp.js"; import { CallToolResult, JSONRPCMessage } from "../types.js"; -import { z } from "zod"; +import { z } from "zod/v4"; import { AuthInfo } from "./auth/types.js"; async function getFreePort() { diff --git a/src/server/title.test.ts b/src/server/title.test.ts index 3f64570b8..f9d9e46a7 100644 --- a/src/server/title.test.ts +++ b/src/server/title.test.ts @@ -1,7 +1,7 @@ import { Server } from "./index.js"; import { Client } from "../client/index.js"; import { InMemoryTransport } from "../inMemory.js"; -import { z } from "zod"; +import { z } from "zod/v4"; import { McpServer, ResourceTemplate } from "./mcp.js"; describe("Title field backwards compatibility", () => { diff --git a/src/shared/auth.ts b/src/shared/auth.ts index 886eb1084..e2228d2dc 100644 --- a/src/shared/auth.ts +++ b/src/shared/auth.ts @@ -1,9 +1,10 @@ -import { z } from "zod"; +import { z } from "zod/v4"; /** * Reusable URL validation that disallows javascript: scheme */ -export const SafeUrlSchema = z.string().url() +export const SafeUrlSchema = z + .url() .superRefine((val, ctx) => { if (!URL.canParse(val)) { ctx.addIssue({ @@ -14,77 +15,75 @@ export const SafeUrlSchema = z.string().url() return z.NEVER; } - }).refine( + }) + .refine( (url) => { const u = new URL(url); - return u.protocol !== 'javascript:' && u.protocol !== 'data:' && u.protocol !== 'vbscript:'; + return ( + u.protocol !== "javascript:" && + u.protocol !== "data:" && + u.protocol !== "vbscript:" + ); }, { message: "URL cannot use javascript:, data:, or vbscript: scheme" } -); - + ); /** * RFC 9728 OAuth Protected Resource Metadata */ -export const OAuthProtectedResourceMetadataSchema = z - .object({ - resource: z.string().url(), - authorization_servers: z.array(SafeUrlSchema).optional(), - jwks_uri: z.string().url().optional(), - scopes_supported: z.array(z.string()).optional(), - bearer_methods_supported: z.array(z.string()).optional(), - resource_signing_alg_values_supported: z.array(z.string()).optional(), - resource_name: z.string().optional(), - resource_documentation: z.string().optional(), - resource_policy_uri: z.string().url().optional(), - resource_tos_uri: z.string().url().optional(), - tls_client_certificate_bound_access_tokens: z.boolean().optional(), - authorization_details_types_supported: z.array(z.string()).optional(), - dpop_signing_alg_values_supported: z.array(z.string()).optional(), - dpop_bound_access_tokens_required: z.boolean().optional(), - }) - .passthrough(); +export const OAuthProtectedResourceMetadataSchema = z.object({ + resource: z.url(), + authorization_servers: z.array(SafeUrlSchema).optional(), + jwks_uri: SafeUrlSchema.optional(), + scopes_supported: z.array(z.string()).optional(), + bearer_methods_supported: z.array(z.string()).optional(), + resource_signing_alg_values_supported: z.array(z.string()).optional(), + resource_name: z.string().optional(), + resource_documentation: z.string().optional(), + resource_policy_uri: z.url().optional(), + resource_tos_uri: z.url().optional(), + tls_client_certificate_bound_access_tokens: z.boolean().optional(), + authorization_details_types_supported: z.array(z.string()).optional(), + dpop_signing_alg_values_supported: z.array(z.string()).optional(), + dpop_bound_access_tokens_required: z.boolean().optional(), +}); /** * RFC 8414 OAuth 2.0 Authorization Server Metadata */ -export const OAuthMetadataSchema = z - .object({ - issuer: z.string(), - authorization_endpoint: SafeUrlSchema, - token_endpoint: SafeUrlSchema, - registration_endpoint: SafeUrlSchema.optional(), - scopes_supported: z.array(z.string()).optional(), - response_types_supported: z.array(z.string()), - response_modes_supported: z.array(z.string()).optional(), - grant_types_supported: z.array(z.string()).optional(), - token_endpoint_auth_methods_supported: z.array(z.string()).optional(), - token_endpoint_auth_signing_alg_values_supported: z - .array(z.string()) - .optional(), - service_documentation: SafeUrlSchema.optional(), - revocation_endpoint: SafeUrlSchema.optional(), - revocation_endpoint_auth_methods_supported: z.array(z.string()).optional(), - revocation_endpoint_auth_signing_alg_values_supported: z - .array(z.string()) - .optional(), - introspection_endpoint: z.string().optional(), - introspection_endpoint_auth_methods_supported: z - .array(z.string()) - .optional(), - introspection_endpoint_auth_signing_alg_values_supported: z - .array(z.string()) - .optional(), - code_challenge_methods_supported: z.array(z.string()).optional(), - }) - .passthrough(); +export const OAuthMetadataSchema = z.looseObject({ + issuer: z.string(), + authorization_endpoint: SafeUrlSchema, + token_endpoint: SafeUrlSchema, + registration_endpoint: SafeUrlSchema.optional(), + scopes_supported: z.array(z.string()).optional(), + response_types_supported: z.array(z.string()), + response_modes_supported: z.array(z.string()).optional(), + grant_types_supported: z.array(z.string()).optional(), + token_endpoint_auth_methods_supported: z.array(z.string()).optional(), + token_endpoint_auth_signing_alg_values_supported: z + .array(z.string()) + .optional(), + service_documentation: SafeUrlSchema.optional(), + revocation_endpoint: SafeUrlSchema.optional(), + revocation_endpoint_auth_methods_supported: z.array(z.string()).optional(), + revocation_endpoint_auth_signing_alg_values_supported: z + .array(z.string()) + .optional(), + introspection_endpoint: z.string().optional(), + introspection_endpoint_auth_methods_supported: z.array(z.string()).optional(), + introspection_endpoint_auth_signing_alg_values_supported: z + .array(z.string()) + .optional(), + code_challenge_methods_supported: z.array(z.string()).optional(), +}); /** * OpenID Connect Discovery 1.0 Provider Metadata * see: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata */ export const OpenIdProviderMetadataSchema = z - .object({ + .looseObject({ issuer: z.string(), authorization_endpoint: SafeUrlSchema, token_endpoint: SafeUrlSchema, @@ -144,26 +143,23 @@ export const OpenIdProviderDiscoveryMetadataSchema = /** * OAuth 2.1 token response */ -export const OAuthTokensSchema = z - .object({ - access_token: z.string(), - id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect - token_type: z.string(), - expires_in: z.number().optional(), - scope: z.string().optional(), - refresh_token: z.string().optional(), - }) - .strip(); +export const OAuthTokensSchema = z.object({ + access_token: z.string(), + id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect + token_type: z.string(), + expires_in: z.number().optional(), + scope: z.string().optional(), + refresh_token: z.string().optional(), +}); /** * OAuth 2.1 error response */ -export const OAuthErrorResponseSchema = z - .object({ - error: z.string(), - error_description: z.string().optional(), - error_uri: z.string().optional(), - }); +export const OAuthErrorResponseSchema = z.object({ + error: z.string(), + error_description: z.string().optional(), + error_uri: z.string().optional(), +}); /** * RFC 7591 OAuth 2.0 Dynamic Client Registration metadata @@ -185,7 +181,7 @@ export const OAuthClientMetadataSchema = z.object({ software_id: z.string().optional(), software_version: z.string().optional(), software_statement: z.string().optional(), -}).strip(); +}); /** * RFC 7591 OAuth 2.0 Dynamic Client Registration client information @@ -195,12 +191,14 @@ export const OAuthClientInformationSchema = z.object({ client_secret: z.string().optional(), client_id_issued_at: z.number().optional(), client_secret_expires_at: z.number().optional(), -}).strip(); +}); /** * RFC 7591 OAuth 2.0 Dynamic Client Registration full response (client information plus metadata) */ -export const OAuthClientInformationFullSchema = OAuthClientMetadataSchema.merge(OAuthClientInformationSchema); +export const OAuthClientInformationFullSchema = OAuthClientMetadataSchema.merge( + OAuthClientInformationSchema +); /** * RFC 7591 OAuth 2.0 Dynamic Client Registration error response @@ -208,7 +206,7 @@ export const OAuthClientInformationFullSchema = OAuthClientMetadataSchema.merge( export const OAuthClientRegistrationErrorSchema = z.object({ error: z.string(), error_description: z.string().optional(), -}).strip(); +}); /** * RFC 7009 OAuth 2.0 Token Revocation request @@ -216,20 +214,36 @@ export const OAuthClientRegistrationErrorSchema = z.object({ export const OAuthTokenRevocationRequestSchema = z.object({ token: z.string(), token_type_hint: z.string().optional(), -}).strip(); +}); export type OAuthMetadata = z.infer; -export type OpenIdProviderMetadata = z.infer; -export type OpenIdProviderDiscoveryMetadata = z.infer; +export type OpenIdProviderMetadata = z.infer< + typeof OpenIdProviderMetadataSchema +>; +export type OpenIdProviderDiscoveryMetadata = z.infer< + typeof OpenIdProviderDiscoveryMetadataSchema +>; export type OAuthTokens = z.infer; export type OAuthErrorResponse = z.infer; export type OAuthClientMetadata = z.infer; -export type OAuthClientInformation = z.infer; -export type OAuthClientInformationFull = z.infer; -export type OAuthClientRegistrationError = z.infer; -export type OAuthTokenRevocationRequest = z.infer; -export type OAuthProtectedResourceMetadata = z.infer; +export type OAuthClientInformation = z.infer< + typeof OAuthClientInformationSchema +>; +export type OAuthClientInformationFull = z.infer< + typeof OAuthClientInformationFullSchema +>; +export type OAuthClientRegistrationError = z.infer< + typeof OAuthClientRegistrationErrorSchema +>; +export type OAuthTokenRevocationRequest = z.infer< + typeof OAuthTokenRevocationRequestSchema +>; +export type OAuthProtectedResourceMetadata = z.infer< + typeof OAuthProtectedResourceMetadataSchema +>; // Unified type for authorization server metadata -export type AuthorizationServerMetadata = OAuthMetadata | OpenIdProviderDiscoveryMetadata; +export type AuthorizationServerMetadata = + | OAuthMetadata + | OpenIdProviderDiscoveryMetadata; diff --git a/src/shared/protocol-transport-handling.test.ts b/src/shared/protocol-transport-handling.test.ts index 3baa9b638..4dd46851b 100644 --- a/src/shared/protocol-transport-handling.test.ts +++ b/src/shared/protocol-transport-handling.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test, beforeEach } from "@jest/globals"; import { Protocol } from "./protocol.js"; import { Transport } from "./transport.js"; import { Request, Notification, Result, JSONRPCMessage } from "../types.js"; -import { z } from "zod"; +import { z } from "zod/v4"; // Mock Transport class class MockTransport implements Transport { diff --git a/src/shared/protocol.test.ts b/src/shared/protocol.test.ts index f4e74c8bb..e57983a6e 100644 --- a/src/shared/protocol.test.ts +++ b/src/shared/protocol.test.ts @@ -1,4 +1,4 @@ -import { ZodType, z } from "zod"; +import { ZodType, z } from "zod/v4"; import { ClientCapabilities, ErrorCode, diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 7df190ba1..cf1f61600 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -1,4 +1,4 @@ -import { ZodLiteral, ZodObject, ZodType, z } from "zod"; +import { ZodLiteral, ZodObject, ZodType, z } from "zod/v4"; import { CancelledNotificationSchema, ClientCapabilities, diff --git a/src/types.ts b/src/types.ts index 323e37389..ccf75b4f0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { z, ZodTypeAny } from "zod"; +import { z, ZodTypeAny } from "zod/v4"; import { AuthInfo } from "./server/auth/types.js"; export const LATEST_PROTOCOL_VERSION = "2025-06-18"; @@ -16,73 +16,64 @@ export const JSONRPC_VERSION = "2.0"; /** * A progress token, used to associate progress notifications with the original request. */ -export const ProgressTokenSchema = z.union([z.string(), z.number().int()]); +export const ProgressTokenSchema = z.union([z.string(), z.int()]); /** * An opaque token used to represent a cursor for pagination. */ export const CursorSchema = z.string(); -const RequestMetaSchema = z - .object({ - /** - * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. - */ - progressToken: z.optional(ProgressTokenSchema), - }) - .passthrough(); +const RequestMetaSchema = z.looseObject({ + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + */ + progressToken: z.optional(ProgressTokenSchema), +}); -const BaseRequestParamsSchema = z - .object({ - _meta: z.optional(RequestMetaSchema), - }) - .passthrough(); +const BaseRequestParamsSchema = z.looseObject({ + _meta: z.optional(RequestMetaSchema), +}); export const RequestSchema = z.object({ method: z.string(), params: z.optional(BaseRequestParamsSchema), }); -const BaseNotificationParamsSchema = z - .object({ - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); +const BaseNotificationParamsSchema = z.looseObject({ + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); export const NotificationSchema = z.object({ method: z.string(), params: z.optional(BaseNotificationParamsSchema), }); -export const ResultSchema = z - .object({ - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); +export const ResultSchema = z.looseObject({ + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); /** * A uniquely identifying ID for a request in JSON-RPC. */ -export const RequestIdSchema = z.union([z.string(), z.number().int()]); +export const RequestIdSchema = z.union([z.string(), z.int()]); /** * A request that expects a response. */ export const JSONRPCRequestSchema = z - .object({ + .strictObject({ jsonrpc: z.literal(JSONRPC_VERSION), id: RequestIdSchema, }) - .merge(RequestSchema) - .strict(); + .extend(RequestSchema.shape); export const isJSONRPCRequest = (value: unknown): value is JSONRPCRequest => JSONRPCRequestSchema.safeParse(value).success; @@ -91,11 +82,10 @@ export const isJSONRPCRequest = (value: unknown): value is JSONRPCRequest => * A notification which does not expect a response. */ export const JSONRPCNotificationSchema = z - .object({ + .strictObject({ jsonrpc: z.literal(JSONRPC_VERSION), }) - .merge(NotificationSchema) - .strict(); + .extend(NotificationSchema.shape); export const isJSONRPCNotification = ( value: unknown @@ -105,13 +95,11 @@ export const isJSONRPCNotification = ( /** * A successful (non-error) response to a request. */ -export const JSONRPCResponseSchema = z - .object({ - jsonrpc: z.literal(JSONRPC_VERSION), - id: RequestIdSchema, - result: ResultSchema, - }) - .strict(); +export const JSONRPCResponseSchema = z.strictObject({ + jsonrpc: z.literal(JSONRPC_VERSION), + id: RequestIdSchema, + result: ResultSchema, +}); export const isJSONRPCResponse = (value: unknown): value is JSONRPCResponse => JSONRPCResponseSchema.safeParse(value).success; @@ -135,26 +123,24 @@ export enum ErrorCode { /** * A response to a request that indicates an error occurred. */ -export const JSONRPCErrorSchema = z - .object({ - jsonrpc: z.literal(JSONRPC_VERSION), - id: RequestIdSchema, - error: z.object({ - /** - * The error type that occurred. - */ - code: z.number().int(), - /** - * A short description of the error. The message SHOULD be limited to a concise single sentence. - */ - message: z.string(), - /** - * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). - */ - data: z.optional(z.unknown()), - }), - }) - .strict(); +export const JSONRPCErrorSchema = z.strictObject({ + jsonrpc: z.literal(JSONRPC_VERSION), + id: RequestIdSchema, + error: z.strictObject({ + /** + * The error type that occurred. + */ + code: z.int(), + /** + * A short description of the error. The message SHOULD be limited to a concise single sentence. + */ + message: z.string(), + /** + * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). + */ + data: z.optional(z.unknown()), + }), +}); export const isJSONRPCError = (value: unknown): value is JSONRPCError => JSONRPCErrorSchema.safeParse(value).success; @@ -203,21 +189,19 @@ export const CancelledNotificationSchema = NotificationSchema.extend({ /** * Base metadata interface for common properties across resources, tools, prompts, and implementations. */ -export const BaseMetadataSchema = z - .object({ - /** Intended for programmatic or logical use, but used as a display name in past specs or fallback */ - name: z.string(), - /** - * Intended for UI and end-user contexts — optimized to be human-readable and easily understood, - * even by those unfamiliar with domain-specific terminology. - * - * If not provided, the name should be used for display (except for Tool, - * where `annotations.title` should be given precedence over using `name`, - * if present). - */ - title: z.optional(z.string()), - }) - .passthrough(); +export const BaseMetadataSchema = z.looseObject({ + /** Intended for programmatic or logical use, but used as a display name in past specs or fallback */ + name: z.string(), + /** + * Intended for UI and end-user contexts — optimized to be human-readable and easily understood, + * even by those unfamiliar with domain-specific terminology. + * + * If not provided, the name should be used for display (except for Tool, + * where `annotations.title` should be given precedence over using `name`, + * if present). + */ + title: z.optional(z.string()), +}); /* Initialization */ /** @@ -230,35 +214,31 @@ export const ImplementationSchema = BaseMetadataSchema.extend({ /** * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. */ -export const ClientCapabilitiesSchema = z - .object({ - /** - * Experimental, non-standard capabilities that the client supports. - */ - experimental: z.optional(z.object({}).passthrough()), - /** - * Present if the client supports sampling from an LLM. - */ - sampling: z.optional(z.object({}).passthrough()), - /** - * Present if the client supports eliciting user input. - */ - elicitation: z.optional(z.object({}).passthrough()), - /** - * Present if the client supports listing roots. - */ - roots: z.optional( - z - .object({ - /** - * Whether the client supports issuing notifications for changes to the roots list. - */ - listChanged: z.optional(z.boolean()), - }) - .passthrough(), - ), - }) - .passthrough(); +export const ClientCapabilitiesSchema = z.looseObject({ + /** + * Experimental, non-standard capabilities that the client supports. + */ + experimental: z.optional(z.looseObject({})), + /** + * Present if the client supports sampling from an LLM. + */ + sampling: z.optional(z.looseObject({})), + /** + * Present if the client supports eliciting user input. + */ + elicitation: z.optional(z.looseObject({})), + /** + * Present if the client supports listing roots. + */ + roots: z.optional( + z.looseObject({ + /** + * Whether the client supports issuing notifications for changes to the roots list. + */ + listChanged: z.optional(z.boolean()), + }) + ), +}); /** * This request is sent from the client to the server when it first connects, asking it to begin initialization. @@ -275,73 +255,66 @@ export const InitializeRequestSchema = RequestSchema.extend({ }), }); -export const isInitializeRequest = (value: unknown): value is InitializeRequest => +export const isInitializeRequest = ( + value: unknown +): value is InitializeRequest => InitializeRequestSchema.safeParse(value).success; - /** * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. */ -export const ServerCapabilitiesSchema = z - .object({ - /** - * Experimental, non-standard capabilities that the server supports. - */ - experimental: z.optional(z.object({}).passthrough()), - /** - * Present if the server supports sending log messages to the client. - */ - logging: z.optional(z.object({}).passthrough()), - /** - * Present if the server supports sending completions to the client. - */ - completions: z.optional(z.object({}).passthrough()), - /** - * Present if the server offers any prompt templates. - */ - prompts: z.optional( - z - .object({ - /** - * Whether this server supports issuing notifications for changes to the prompt list. - */ - listChanged: z.optional(z.boolean()), - }) - .passthrough(), - ), - /** - * Present if the server offers any resources to read. - */ - resources: z.optional( - z - .object({ - /** - * Whether this server supports clients subscribing to resource updates. - */ - subscribe: z.optional(z.boolean()), - - /** - * Whether this server supports issuing notifications for changes to the resource list. - */ - listChanged: z.optional(z.boolean()), - }) - .passthrough(), - ), - /** - * Present if the server offers any tools to call. - */ - tools: z.optional( - z - .object({ - /** - * Whether this server supports issuing notifications for changes to the tool list. - */ - listChanged: z.optional(z.boolean()), - }) - .passthrough(), - ), - }) - .passthrough(); +export const ServerCapabilitiesSchema = z.looseObject({ + /** + * Experimental, non-standard capabilities that the server supports. + */ + experimental: z.optional(z.looseObject({})), + /** + * Present if the server supports sending log messages to the client. + */ + logging: z.optional(z.looseObject({})), + /** + * Present if the server supports sending completions to the client. + */ + completions: z.optional(z.looseObject({})), + /** + * Present if the server offers any prompt templates. + */ + prompts: z.optional( + z.looseObject({ + /** + * Whether this server supports issuing notifications for changes to the prompt list. + */ + listChanged: z.optional(z.boolean()), + }) + ), + /** + * Present if the server offers any resources to read. + */ + resources: z.optional( + z.looseObject({ + /** + * Whether this server supports clients subscribing to resource updates. + */ + subscribe: z.optional(z.boolean()), + + /** + * Whether this server supports issuing notifications for changes to the resource list. + */ + listChanged: z.optional(z.boolean()), + }) + ), + /** + * Present if the server offers any tools to call. + */ + tools: z.optional( + z.looseObject({ + /** + * Whether this server supports issuing notifications for changes to the tool list. + */ + listChanged: z.optional(z.boolean()), + }) + ), +}); /** * After receiving an initialize request from the client, the server sends this response. @@ -368,7 +341,9 @@ export const InitializedNotificationSchema = NotificationSchema.extend({ method: z.literal("notifications/initialized"), }); -export const isInitializedNotification = (value: unknown): value is InitializedNotification => +export const isInitializedNotification = ( + value: unknown +): value is InitializedNotification => InitializedNotificationSchema.safeParse(value).success; /* Ping */ @@ -380,22 +355,20 @@ export const PingRequestSchema = RequestSchema.extend({ }); /* Progress notifications */ -export const ProgressSchema = z - .object({ - /** - * The progress thus far. This should increase every time progress is made, even if the total is unknown. - */ - progress: z.number(), - /** - * Total number of items to process (or total progress required), if known. - */ - total: z.optional(z.number()), - /** - * An optional message describing the current progress. - */ - message: z.optional(z.string()), - }) - .passthrough(); +export const ProgressSchema = z.looseObject({ + /** + * The progress thus far. This should increase every time progress is made, even if the total is unknown. + */ + progress: z.number(), + /** + * Total number of items to process (or total progress required), if known. + */ + total: z.optional(z.number()), + /** + * An optional message describing the current progress. + */ + message: z.optional(z.string()), +}); /** * An out-of-band notification used to inform the receiver of a progress update for a long-running request. @@ -433,23 +406,21 @@ export const PaginatedResultSchema = ResultSchema.extend({ /** * The contents of a specific resource or sub-resource. */ -export const ResourceContentsSchema = z - .object({ - /** - * The URI of this resource. - */ - uri: z.string(), - /** - * The MIME type of this resource, if known. - */ - mimeType: z.optional(z.string()), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); +export const ResourceContentsSchema = z.looseObject({ + /** + * The URI of this resource. + */ + uri: z.string(), + /** + * The MIME type of this resource, if known. + */ + mimeType: z.optional(z.string()), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); export const TextResourceContentsSchema = ResourceContentsSchema.extend({ /** @@ -458,24 +429,25 @@ export const TextResourceContentsSchema = ResourceContentsSchema.extend({ text: z.string(), }); - /** * A Zod schema for validating Base64 strings that is more performant and * robust for very large inputs than the default regex-based check. It avoids * stack overflows by using the native `atob` function for validation. */ const Base64Schema = z.string().refine( - (val) => { - try { - // atob throws a DOMException if the string contains characters - // that are not part of the Base64 character set. - atob(val); - return true; - } catch { - return false; - } - }, - { message: "Invalid Base64 string" }, + (val) => { + try { + // atob throws a DOMException if the string contains characters + // that are not part of the Base64 character set. + atob(val); + return true; + } catch { + return false; + } + }, + { + error: "Invalid Base64 string", + } ); export const BlobResourceContentsSchema = ResourceContentsSchema.extend({ @@ -510,7 +482,7 @@ export const ResourceSchema = BaseMetadataSchema.extend({ * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. */ - _meta: z.optional(z.object({}).passthrough()), + _meta: z.optional(z.looseObject({})), }); /** @@ -538,7 +510,7 @@ export const ResourceTemplateSchema = BaseMetadataSchema.extend({ * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. */ - _meta: z.optional(z.object({}).passthrough()), + _meta: z.optional(z.looseObject({})), }); /** @@ -561,7 +533,7 @@ export const ListResourcesResultSchema = PaginatedResultSchema.extend({ export const ListResourceTemplatesRequestSchema = PaginatedRequestSchema.extend( { method: z.literal("resources/templates/list"), - }, + } ); /** @@ -589,7 +561,7 @@ export const ReadResourceRequestSchema = RequestSchema.extend({ */ export const ReadResourceResultSchema = ResultSchema.extend({ contents: z.array( - z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), + z.union([TextResourceContentsSchema, BlobResourceContentsSchema]) ), }); @@ -643,22 +615,20 @@ export const ResourceUpdatedNotificationSchema = NotificationSchema.extend({ /** * Describes an argument that a prompt can accept. */ -export const PromptArgumentSchema = z - .object({ - /** - * The name of the argument. - */ - name: z.string(), - /** - * A human-readable description of the argument. - */ - description: z.optional(z.string()), - /** - * Whether this argument must be provided. - */ - required: z.optional(z.boolean()), - }) - .passthrough(); +export const PromptArgumentSchema = z.looseObject({ + /** + * The name of the argument. + */ + name: z.string(), + /** + * A human-readable description of the argument. + */ + description: z.optional(z.string()), + /** + * Whether this argument must be provided. + */ + required: z.optional(z.boolean()), +}); /** * A prompt or prompt template that the server offers. @@ -676,7 +646,7 @@ export const PromptSchema = BaseMetadataSchema.extend({ * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. */ - _meta: z.optional(z.object({}).passthrough()), + _meta: z.optional(z.looseObject({})), }); /** @@ -706,89 +676,81 @@ export const GetPromptRequestSchema = RequestSchema.extend({ /** * Arguments to use for templating the prompt. */ - arguments: z.optional(z.record(z.string())), + arguments: z.record(z.string(), z.string()).optional(), }), }); /** * Text provided to or from an LLM. */ -export const TextContentSchema = z - .object({ - type: z.literal("text"), - /** - * The text content of the message. - */ - text: z.string(), +export const TextContentSchema = z.looseObject({ + type: z.literal("text"), + /** + * The text content of the message. + */ + text: z.string(), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); /** * An image provided to or from an LLM. */ -export const ImageContentSchema = z - .object({ - type: z.literal("image"), - /** - * The base64-encoded image data. - */ - data: Base64Schema, - /** - * The MIME type of the image. Different providers may support different image types. - */ - mimeType: z.string(), +export const ImageContentSchema = z.looseObject({ + type: z.literal("image"), + /** + * The base64-encoded image data. + */ + data: Base64Schema, + /** + * The MIME type of the image. Different providers may support different image types. + */ + mimeType: z.string(), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); /** * An Audio provided to or from an LLM. */ -export const AudioContentSchema = z - .object({ - type: z.literal("audio"), - /** - * The base64-encoded audio data. - */ - data: Base64Schema, - /** - * The MIME type of the audio. Different providers may support different audio types. - */ - mimeType: z.string(), +export const AudioContentSchema = z.looseObject({ + type: z.literal("audio"), + /** + * The base64-encoded audio data. + */ + data: Base64Schema, + /** + * The MIME type of the audio. Different providers may support different audio types. + */ + mimeType: z.string(), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); /** * The contents of a resource, embedded into a prompt or tool call result. */ -export const EmbeddedResourceSchema = z - .object({ - type: z.literal("resource"), - resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); +export const EmbeddedResourceSchema = z.looseObject({ + type: z.literal("resource"), + resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); /** * A resource that the server is capable of reading, included in a prompt or tool call result. @@ -813,12 +775,10 @@ export const ContentBlockSchema = z.union([ /** * Describes a message returned as part of a prompt. */ -export const PromptMessageSchema = z - .object({ - role: z.enum(["user", "assistant"]), - content: ContentBlockSchema, - }) - .passthrough(); +export const PromptMessageSchema = z.looseObject({ + role: z.enum(["user", "assistant"]), + content: ContentBlockSchema, +}); /** * The server's response to a prompts/get request from the client. @@ -849,51 +809,49 @@ export const PromptListChangedNotificationSchema = NotificationSchema.extend({ * Clients should never make tool use decisions based on ToolAnnotations * received from untrusted servers. */ -export const ToolAnnotationsSchema = z - .object({ - /** - * A human-readable title for the tool. - */ - title: z.optional(z.string()), +export const ToolAnnotationsSchema = z.looseObject({ + /** + * A human-readable title for the tool. + */ + title: z.optional(z.string()), - /** - * If true, the tool does not modify its environment. - * - * Default: false - */ - readOnlyHint: z.optional(z.boolean()), + /** + * If true, the tool does not modify its environment. + * + * Default: false + */ + readOnlyHint: z.optional(z.boolean()), - /** - * If true, the tool may perform destructive updates to its environment. - * If false, the tool performs only additive updates. - * - * (This property is meaningful only when `readOnlyHint == false`) - * - * Default: true - */ - destructiveHint: z.optional(z.boolean()), + /** + * If true, the tool may perform destructive updates to its environment. + * If false, the tool performs only additive updates. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: true + */ + destructiveHint: z.optional(z.boolean()), - /** - * If true, calling the tool repeatedly with the same arguments - * will have no additional effect on the its environment. - * - * (This property is meaningful only when `readOnlyHint == false`) - * - * Default: false - */ - idempotentHint: z.optional(z.boolean()), + /** + * If true, calling the tool repeatedly with the same arguments + * will have no additional effect on the its environment. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: false + */ + idempotentHint: z.optional(z.boolean()), - /** - * If true, this tool may interact with an "open world" of external - * entities. If false, the tool's domain of interaction is closed. - * For example, the world of a web search tool is open, whereas that - * of a memory tool is not. - * - * Default: true - */ - openWorldHint: z.optional(z.boolean()), - }) - .passthrough(); + /** + * If true, this tool may interact with an "open world" of external + * entities. If false, the tool's domain of interaction is closed. + * For example, the world of a web search tool is open, whereas that + * of a memory tool is not. + * + * Default: true + */ + openWorldHint: z.optional(z.boolean()), +}); /** * Definition for a tool the client can call. @@ -906,24 +864,21 @@ export const ToolSchema = BaseMetadataSchema.extend({ /** * A JSON Schema object defining the expected parameters for the tool. */ - inputSchema: z - .object({ - type: z.literal("object"), - properties: z.optional(z.object({}).passthrough()), - required: z.optional(z.array(z.string())), - }) - .passthrough(), + inputSchema: z.looseObject({ + type: z.literal("object"), + properties: z.optional(z.looseObject({})), + required: z.optional(z.array(z.string())), + }), /** * An optional JSON Schema object defining the structure of the tool's output returned in * the structuredContent field of a CallToolResult. */ outputSchema: z.optional( - z.object({ + z.looseObject({ type: z.literal("object"), - properties: z.optional(z.object({}).passthrough()), + properties: z.optional(z.looseObject({})), required: z.optional(z.array(z.string())), }) - .passthrough() ), /** * Optional additional tool information. @@ -934,7 +889,7 @@ export const ToolSchema = BaseMetadataSchema.extend({ * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. */ - _meta: z.optional(z.object({}).passthrough()), + _meta: z.optional(z.looseObject({})), }); /** @@ -968,7 +923,7 @@ export const CallToolResultSchema = ResultSchema.extend({ * * If the Tool defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema. */ - structuredContent: z.object({}).passthrough().optional(), + structuredContent: z.looseObject({}).optional(), /** * Whether the tool call ended in an error. @@ -993,7 +948,7 @@ export const CallToolResultSchema = ResultSchema.extend({ export const CompatibilityCallToolResultSchema = CallToolResultSchema.or( ResultSchema.extend({ toolResult: z.unknown(), - }), + }) ); /** @@ -1003,7 +958,7 @@ export const CallToolRequestSchema = RequestSchema.extend({ method: z.literal("tools/call"), params: BaseRequestParamsSchema.extend({ name: z.string(), - arguments: z.optional(z.record(z.unknown())), + arguments: z.record(z.string(), z.unknown()).optional(), }), }); @@ -1067,48 +1022,42 @@ export const LoggingMessageNotificationSchema = NotificationSchema.extend({ /** * Hints to use for model selection. */ -export const ModelHintSchema = z - .object({ - /** - * A hint for a model name. - */ - name: z.string().optional(), - }) - .passthrough(); +export const ModelHintSchema = z.looseObject({ + /** + * A hint for a model name. + */ + name: z.string().optional(), +}); /** * The server's preferences for model selection, requested of the client during sampling. */ -export const ModelPreferencesSchema = z - .object({ - /** - * Optional hints to use for model selection. - */ - hints: z.optional(z.array(ModelHintSchema)), - /** - * How much to prioritize cost when selecting a model. - */ - costPriority: z.optional(z.number().min(0).max(1)), - /** - * How much to prioritize sampling speed (latency) when selecting a model. - */ - speedPriority: z.optional(z.number().min(0).max(1)), - /** - * How much to prioritize intelligence and capabilities when selecting a model. - */ - intelligencePriority: z.optional(z.number().min(0).max(1)), - }) - .passthrough(); +export const ModelPreferencesSchema = z.looseObject({ + /** + * Optional hints to use for model selection. + */ + hints: z.optional(z.array(ModelHintSchema)), + /** + * How much to prioritize cost when selecting a model. + */ + costPriority: z.optional(z.number().min(0).max(1)), + /** + * How much to prioritize sampling speed (latency) when selecting a model. + */ + speedPriority: z.optional(z.number().min(0).max(1)), + /** + * How much to prioritize intelligence and capabilities when selecting a model. + */ + intelligencePriority: z.optional(z.number().min(0).max(1)), +}); /** * Describes a message issued to or received from an LLM API. */ -export const SamplingMessageSchema = z - .object({ - role: z.enum(["user", "assistant"]), - content: z.union([TextContentSchema, ImageContentSchema, AudioContentSchema]), - }) - .passthrough(); +export const SamplingMessageSchema = z.looseObject({ + role: z.enum(["user", "assistant"]), + content: z.union([TextContentSchema, ImageContentSchema, AudioContentSchema]), +}); /** * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. @@ -1129,12 +1078,12 @@ export const CreateMessageRequestSchema = RequestSchema.extend({ /** * The maximum number of tokens to sample, as requested by the server. The client MAY choose to sample fewer tokens than requested. */ - maxTokens: z.number().int(), + maxTokens: z.int(), stopSequences: z.optional(z.array(z.string())), /** * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. */ - metadata: z.optional(z.object({}).passthrough()), + metadata: z.optional(z.looseObject({})), /** * The server's preferences for which model to select. */ @@ -1154,13 +1103,13 @@ export const CreateMessageResultSchema = ResultSchema.extend({ * The reason why sampling stopped. */ stopReason: z.optional( - z.enum(["endTurn", "stopSequence", "maxTokens"]).or(z.string()), + z.enum(["endTurn", "stopSequence", "maxTokens"]).or(z.string()) ), role: z.enum(["user", "assistant"]), content: z.discriminatedUnion("type", [ TextContentSchema, ImageContentSchema, - AudioContentSchema + AudioContentSchema, ]), }); @@ -1168,54 +1117,46 @@ export const CreateMessageResultSchema = ResultSchema.extend({ /** * Primitive schema definition for boolean fields. */ -export const BooleanSchemaSchema = z - .object({ - type: z.literal("boolean"), - title: z.optional(z.string()), - description: z.optional(z.string()), - default: z.optional(z.boolean()), - }) - .passthrough(); +export const BooleanSchemaSchema = z.looseObject({ + type: z.literal("boolean"), + title: z.optional(z.string()), + description: z.optional(z.string()), + default: z.optional(z.boolean()), +}); /** * Primitive schema definition for string fields. */ -export const StringSchemaSchema = z - .object({ - type: z.literal("string"), - title: z.optional(z.string()), - description: z.optional(z.string()), - minLength: z.optional(z.number()), - maxLength: z.optional(z.number()), - format: z.optional(z.enum(["email", "uri", "date", "date-time"])), - }) - .passthrough(); +export const StringSchemaSchema = z.looseObject({ + type: z.literal("string"), + title: z.optional(z.string()), + description: z.optional(z.string()), + minLength: z.optional(z.number()), + maxLength: z.optional(z.number()), + format: z.optional(z.enum(["email", "uri", "date", "date-time"])), +}); /** * Primitive schema definition for number fields. */ -export const NumberSchemaSchema = z - .object({ - type: z.enum(["number", "integer"]), - title: z.optional(z.string()), - description: z.optional(z.string()), - minimum: z.optional(z.number()), - maximum: z.optional(z.number()), - }) - .passthrough(); +export const NumberSchemaSchema = z.looseObject({ + type: z.enum(["number", "integer"]), + title: z.optional(z.string()), + description: z.optional(z.string()), + minimum: z.optional(z.number()), + maximum: z.optional(z.number()), +}); /** * Primitive schema definition for enum fields. */ -export const EnumSchemaSchema = z - .object({ - type: z.literal("string"), - title: z.optional(z.string()), - description: z.optional(z.string()), - enum: z.array(z.string()), - enumNames: z.optional(z.array(z.string())), - }) - .passthrough(); +export const EnumSchemaSchema = z.looseObject({ + type: z.literal("string"), + title: z.optional(z.string()), + description: z.optional(z.string()), + enum: z.array(z.string()), + enumNames: z.optional(z.array(z.string())), +}); /** * Union of all primitive schema definitions. @@ -1241,13 +1182,11 @@ export const ElicitRequestSchema = RequestSchema.extend({ /** * The schema for the requested user input. */ - requestedSchema: z - .object({ - type: z.literal("object"), - properties: z.record(z.string(), PrimitiveSchemaDefinitionSchema), - required: z.optional(z.array(z.string())), - }) - .passthrough(), + requestedSchema: z.looseObject({ + type: z.literal("object"), + properties: z.record(z.string(), PrimitiveSchemaDefinitionSchema), + required: z.optional(z.array(z.string())), + }), }), }); @@ -1262,22 +1201,22 @@ export const ElicitResultSchema = ResultSchema.extend({ /** * The collected user input content (only present if action is "accept"). */ - content: z.optional(z.record(z.string(), z.unknown())), + content: z + .record(z.string(), z.union([z.string(), z.number(), z.boolean()])) + .optional(), }); /* Autocomplete */ /** * A reference to a resource or resource template definition. */ -export const ResourceTemplateReferenceSchema = z - .object({ - type: z.literal("ref/resource"), - /** - * The URI or URI template of the resource. - */ - uri: z.string(), - }) - .passthrough(); +export const ResourceTemplateReferenceSchema = z.looseObject({ + type: z.literal("ref/resource"), + /** + * The URI or URI template of the resource. + */ + uri: z.string(), +}); /** * @deprecated Use ResourceTemplateReferenceSchema instead @@ -1287,15 +1226,13 @@ export const ResourceReferenceSchema = ResourceTemplateReferenceSchema; /** * Identifies a prompt. */ -export const PromptReferenceSchema = z - .object({ - type: z.literal("ref/prompt"), - /** - * The name of the prompt or prompt template - */ - name: z.string(), - }) - .passthrough(); +export const PromptReferenceSchema = z.looseObject({ + type: z.literal("ref/prompt"), + /** + * The name of the prompt or prompt template + */ + name: z.string(), +}); /** * A request from the client to the server, to ask for completion options. @@ -1307,24 +1244,22 @@ export const CompleteRequestSchema = RequestSchema.extend({ /** * The argument's information */ - argument: z - .object({ - /** - * The name of the argument - */ - name: z.string(), - /** - * The value of the argument to use for completion matching. - */ - value: z.string(), - }) - .passthrough(), + argument: z.looseObject({ + /** + * The name of the argument + */ + name: z.string(), + /** + * The value of the argument to use for completion matching. + */ + value: z.string(), + }), context: z.optional( z.object({ /** * Previously-resolved variables in a URI template or prompt. */ - arguments: z.optional(z.record(z.string(), z.string())), + arguments: z.record(z.string(), z.string()).optional(), }) ), }), @@ -1334,46 +1269,42 @@ export const CompleteRequestSchema = RequestSchema.extend({ * The server's response to a completion/complete request */ export const CompleteResultSchema = ResultSchema.extend({ - completion: z - .object({ - /** - * An array of completion values. Must not exceed 100 items. - */ - values: z.array(z.string()).max(100), - /** - * The total number of completion options available. This can exceed the number of values actually sent in the response. - */ - total: z.optional(z.number().int()), - /** - * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. - */ - hasMore: z.optional(z.boolean()), - }) - .passthrough(), + completion: z.looseObject({ + /** + * An array of completion values. Must not exceed 100 items. + */ + values: z.array(z.string()).max(100), + /** + * The total number of completion options available. This can exceed the number of values actually sent in the response. + */ + total: z.optional(z.int()), + /** + * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. + */ + hasMore: z.optional(z.boolean()), + }), }); /* Roots */ /** * Represents a root directory or file that the server can operate on. */ -export const RootSchema = z - .object({ - /** - * The URI identifying the root. This *must* start with file:// for now. - */ - uri: z.string().startsWith("file://"), - /** - * An optional name for the root. - */ - name: z.optional(z.string()), +export const RootSchema = z.looseObject({ + /** + * The URI identifying the root. This *must* start with file:// for now. + */ + uri: z.string().startsWith("file://"), + /** + * An optional name for the root. + */ + name: z.optional(z.string()), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); /** * Sent from the server to request a list of root URIs from the client. @@ -1462,7 +1393,7 @@ export class McpError extends Error { constructor( public readonly code: number, message: string, - public readonly data?: unknown, + public readonly data?: unknown ) { super(`MCP error ${code}: ${message}`); this.name = "McpError"; @@ -1543,7 +1474,9 @@ export type ClientCapabilities = Infer; export type InitializeRequest = Infer; export type ServerCapabilities = Infer; export type InitializeResult = Infer; -export type InitializedNotification = Infer; +export type InitializedNotification = Infer< + typeof InitializedNotificationSchema +>; /* Ping */ export type PingRequest = Infer; @@ -1564,14 +1497,22 @@ export type Resource = Infer; export type ResourceTemplate = Infer; export type ListResourcesRequest = Infer; export type ListResourcesResult = Infer; -export type ListResourceTemplatesRequest = Infer; -export type ListResourceTemplatesResult = Infer; +export type ListResourceTemplatesRequest = Infer< + typeof ListResourceTemplatesRequestSchema +>; +export type ListResourceTemplatesResult = Infer< + typeof ListResourceTemplatesResultSchema +>; export type ReadResourceRequest = Infer; export type ReadResourceResult = Infer; -export type ResourceListChangedNotification = Infer; +export type ResourceListChangedNotification = Infer< + typeof ResourceListChangedNotificationSchema +>; export type SubscribeRequest = Infer; export type UnsubscribeRequest = Infer; -export type ResourceUpdatedNotification = Infer; +export type ResourceUpdatedNotification = Infer< + typeof ResourceUpdatedNotificationSchema +>; /* Prompts */ export type PromptArgument = Infer; @@ -1587,7 +1528,9 @@ export type ResourceLink = Infer; export type ContentBlock = Infer; export type PromptMessage = Infer; export type GetPromptResult = Infer; -export type PromptListChangedNotification = Infer; +export type PromptListChangedNotification = Infer< + typeof PromptListChangedNotificationSchema +>; /* Tools */ export type ToolAnnotations = Infer; @@ -1595,14 +1538,20 @@ export type Tool = Infer; export type ListToolsRequest = Infer; export type ListToolsResult = Infer; export type CallToolResult = Infer; -export type CompatibilityCallToolResult = Infer; +export type CompatibilityCallToolResult = Infer< + typeof CompatibilityCallToolResultSchema +>; export type CallToolRequest = Infer; -export type ToolListChangedNotification = Infer; +export type ToolListChangedNotification = Infer< + typeof ToolListChangedNotificationSchema +>; /* Logging */ export type LoggingLevel = Infer; export type SetLevelRequest = Infer; -export type LoggingMessageNotification = Infer; +export type LoggingMessageNotification = Infer< + typeof LoggingMessageNotificationSchema +>; /* Sampling */ export type SamplingMessage = Infer; @@ -1614,12 +1563,16 @@ export type BooleanSchema = Infer; export type StringSchema = Infer; export type NumberSchema = Infer; export type EnumSchema = Infer; -export type PrimitiveSchemaDefinition = Infer; +export type PrimitiveSchemaDefinition = Infer< + typeof PrimitiveSchemaDefinitionSchema +>; export type ElicitRequest = Infer; export type ElicitResult = Infer; /* Autocomplete */ -export type ResourceTemplateReference = Infer; +export type ResourceTemplateReference = Infer< + typeof ResourceTemplateReferenceSchema +>; /** * @deprecated Use ResourceTemplateReference instead */ @@ -1632,7 +1585,9 @@ export type CompleteResult = Infer; export type Root = Infer; export type ListRootsRequest = Infer; export type ListRootsResult = Infer; -export type RootsListChangedNotification = Infer; +export type RootsListChangedNotification = Infer< + typeof RootsListChangedNotificationSchema +>; /* Client messages */ export type ClientRequest = Infer;