From 0ca48672d1c0d27f30b521aa6bca9d7be46c079b Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 17 Nov 2025 10:09:09 +0200 Subject: [PATCH 1/6] passthrough removal part 2 - 0 passthrough on zod types --- src/server/title.test.ts | 2 +- src/spec.types.test.ts | 71 +++++++----------- src/types.ts | 156 +++++++++++++++++++++------------------ 3 files changed, 110 insertions(+), 119 deletions(-) diff --git a/src/server/title.test.ts b/src/server/title.test.ts index 7f0feedc8..af37e8b29 100644 --- a/src/server/title.test.ts +++ b/src/server/title.test.ts @@ -76,7 +76,7 @@ describe('Title field backwards compatibility', () => { expect(prompts.prompts[0].name).toBe('test-prompt'); expect(prompts.prompts[0].title).toBe('Test Prompt Display Name'); expect(prompts.prompts[0].description).toBe('A test prompt'); - }); + }, 15_000); it('should work with prompts using registerPrompt', async () => { const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts index 2417e6b1d..b06b9e59a 100644 --- a/src/spec.types.test.ts +++ b/src/spec.types.test.ts @@ -169,19 +169,19 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - ProgressNotification: (sdk: RemovePassthrough>, spec: SpecTypes.ProgressNotification) => { + ProgressNotification: (sdk: WithJSONRPC, spec: SpecTypes.ProgressNotification) => { sdk = spec; spec = sdk; }, - SubscribeRequest: (sdk: RemovePassthrough>, spec: SpecTypes.SubscribeRequest) => { + SubscribeRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.SubscribeRequest) => { sdk = spec; spec = sdk; }, - UnsubscribeRequest: (sdk: RemovePassthrough>, spec: SpecTypes.UnsubscribeRequest) => { + UnsubscribeRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.UnsubscribeRequest) => { sdk = spec; spec = sdk; }, - PaginatedRequest: (sdk: RemovePassthrough>, spec: SpecTypes.PaginatedRequest) => { + PaginatedRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.PaginatedRequest) => { sdk = spec; spec = sdk; }, @@ -189,7 +189,7 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - ListRootsRequest: (sdk: RemovePassthrough>, spec: SpecTypes.ListRootsRequest) => { + ListRootsRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListRootsRequest) => { sdk = spec; spec = sdk; }, @@ -261,7 +261,7 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - ClientNotification: (sdk: RemovePassthrough>, spec: SpecTypes.ClientNotification) => { + ClientNotification: (sdk: WithJSONRPC, spec: SpecTypes.ClientNotification) => { sdk = spec; spec = sdk; }, @@ -285,7 +285,7 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - ListToolsRequest: (sdk: RemovePassthrough>, spec: SpecTypes.ListToolsRequest) => { + ListToolsRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListToolsRequest) => { sdk = spec; spec = sdk; }, @@ -297,42 +297,36 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - CallToolRequest: (sdk: RemovePassthrough>, spec: SpecTypes.CallToolRequest) => { + CallToolRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CallToolRequest) => { sdk = spec; spec = sdk; }, - ToolListChangedNotification: ( - sdk: RemovePassthrough>, - spec: SpecTypes.ToolListChangedNotification - ) => { + ToolListChangedNotification: (sdk: WithJSONRPC, spec: SpecTypes.ToolListChangedNotification) => { sdk = spec; spec = sdk; }, ResourceListChangedNotification: ( - sdk: RemovePassthrough>, + sdk: WithJSONRPC, spec: SpecTypes.ResourceListChangedNotification ) => { sdk = spec; spec = sdk; }, PromptListChangedNotification: ( - sdk: RemovePassthrough>, + sdk: WithJSONRPC, spec: SpecTypes.PromptListChangedNotification ) => { sdk = spec; spec = sdk; }, RootsListChangedNotification: ( - sdk: RemovePassthrough>, + sdk: WithJSONRPC, spec: SpecTypes.RootsListChangedNotification ) => { sdk = spec; spec = sdk; }, - ResourceUpdatedNotification: ( - sdk: RemovePassthrough>, - spec: SpecTypes.ResourceUpdatedNotification - ) => { + ResourceUpdatedNotification: (sdk: WithJSONRPC, spec: SpecTypes.ResourceUpdatedNotification) => { sdk = spec; spec = sdk; }, @@ -344,25 +338,19 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - SetLevelRequest: (sdk: RemovePassthrough>, spec: SpecTypes.SetLevelRequest) => { + SetLevelRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.SetLevelRequest) => { sdk = spec; spec = sdk; }, - PingRequest: (sdk: RemovePassthrough>, spec: SpecTypes.PingRequest) => { + PingRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.PingRequest) => { sdk = spec; spec = sdk; }, - InitializedNotification: ( - sdk: RemovePassthrough>, - spec: SpecTypes.InitializedNotification - ) => { + InitializedNotification: (sdk: WithJSONRPC, spec: SpecTypes.InitializedNotification) => { sdk = spec; spec = sdk; }, - ListResourcesRequest: ( - sdk: RemovePassthrough>, - spec: SpecTypes.ListResourcesRequest - ) => { + ListResourcesRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListResourcesRequest) => { sdk = spec; spec = sdk; }, @@ -371,7 +359,7 @@ const sdkTypeChecks = { spec = sdk; }, ListResourceTemplatesRequest: ( - sdk: RemovePassthrough>, + sdk: WithJSONRPCRequest, spec: SpecTypes.ListResourceTemplatesRequest ) => { sdk = spec; @@ -381,10 +369,7 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - ReadResourceRequest: ( - sdk: RemovePassthrough>, - spec: SpecTypes.ReadResourceRequest - ) => { + ReadResourceRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ReadResourceRequest) => { sdk = spec; spec = sdk; }, @@ -420,7 +405,7 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - ListPromptsRequest: (sdk: RemovePassthrough>, spec: SpecTypes.ListPromptsRequest) => { + ListPromptsRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListPromptsRequest) => { sdk = spec; spec = sdk; }, @@ -428,7 +413,7 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - GetPromptRequest: (sdk: RemovePassthrough>, spec: SpecTypes.GetPromptRequest) => { + GetPromptRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.GetPromptRequest) => { sdk = spec; spec = sdk; }, @@ -543,28 +528,22 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - ClientRequest: ( - sdk: RemovePassthrough>, - spec: FixSpecClientRequest - ) => { + ClientRequest: (sdk: WithJSONRPCRequest, spec: FixSpecClientRequest) => { sdk = spec; spec = sdk; }, - ServerRequest: (sdk: RemovePassthrough>, spec: SpecTypes.ServerRequest) => { + ServerRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ServerRequest) => { sdk = spec; spec = sdk; }, LoggingMessageNotification: ( - sdk: RemovePassthrough>>, + sdk: MakeUnknownsNotOptional>, spec: SpecTypes.LoggingMessageNotification ) => { sdk = spec; spec = sdk; }, - ServerNotification: ( - sdk: MakeUnknownsNotOptional>>, - spec: SpecTypes.ServerNotification - ) => { + ServerNotification: (sdk: MakeUnknownsNotOptional>, spec: SpecTypes.ServerNotification) => { sdk = spec; spec = sdk; }, diff --git a/src/types.ts b/src/types.ts index 66cc34941..0b957350e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,17 +28,12 @@ export const ProgressTokenSchema = z.union([z.string(), z.number().int()]); */ 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: ProgressTokenSchema.optional() - }) +const RequestMetaSchema = z.object({ /** - * Passthrough required here because we want to allow any additional fields to be added to the request meta. + * 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. */ - .passthrough(); + progressToken: ProgressTokenSchema.optional() +}); /** * Common params for any request. @@ -52,7 +47,16 @@ const BaseRequestParamsSchema = z.object({ export const RequestSchema = z.object({ method: z.string(), - params: BaseRequestParamsSchema.passthrough().optional() + params: BaseRequestParamsSchema.optional() +}); + +/** + * Generic request schema that allows any additional fields to be added to the params. + * + * Used in {@link JSONRPCRequestSchema} for generic shape matching. + */ +export const RequestSchemaGeneric = RequestSchema.extend({ + params: z.intersection(RequestSchema.shape.params, z.record(z.string(), z.unknown()).optional()) }); const NotificationsParamsSchema = z.object({ @@ -65,22 +69,32 @@ const NotificationsParamsSchema = z.object({ export const NotificationSchema = z.object({ method: z.string(), - params: NotificationsParamsSchema.passthrough().optional() + params: NotificationsParamsSchema.optional() }); -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.record(z.string(), z.unknown()).optional() - }) +/** + * Generic notification schema that allows any additional fields to be added to the params. + * + * Used in {@link JSONRPCNotificationSchema} for generic shape matching. + */ +export const NotificationSchemaGeneric = NotificationSchema.extend({ + params: z.intersection(NotificationSchema.shape.params, z.record(z.string(), z.unknown()).optional()) +}); + +export const ResultSchema = z.object({ /** - * Passthrough required here because we want to allow any additional fields to be added to the result. + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. */ - .passthrough(); + _meta: z.record(z.string(), z.unknown()).optional() +}); +/** + * Generic result schema that allows any additional fields to be added to the result. + * + * Used in {@link JSONRPCResponseSchema} for generic shape matching. + */ +export const ResultSchemaGeneric = z.intersection(ResultSchema, z.record(z.string(), z.unknown()).optional()); /** * A uniquely identifying ID for a request in JSON-RPC. */ @@ -94,7 +108,7 @@ export const JSONRPCRequestSchema = z jsonrpc: z.literal(JSONRPC_VERSION), id: RequestIdSchema }) - .merge(RequestSchema) + .merge(RequestSchemaGeneric) .strict(); export const isJSONRPCRequest = (value: unknown): value is JSONRPCRequest => JSONRPCRequestSchema.safeParse(value).success; @@ -106,7 +120,7 @@ export const JSONRPCNotificationSchema = z .object({ jsonrpc: z.literal(JSONRPC_VERSION) }) - .merge(NotificationSchema) + .merge(NotificationSchemaGeneric) .strict(); export const isJSONRPCNotification = (value: unknown): value is JSONRPCNotification => JSONRPCNotificationSchema.safeParse(value).success; @@ -118,7 +132,7 @@ export const JSONRPCResponseSchema = z .object({ jsonrpc: z.literal(JSONRPC_VERSION), id: RequestIdSchema, - result: ResultSchema + result: ResultSchemaGeneric }) .strict(); @@ -159,7 +173,7 @@ export const JSONRPCErrorSchema = z /** * 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()) + data: z.unknown().optional() }) }) .strict(); @@ -347,14 +361,14 @@ export const ServerCapabilitiesSchema = z.object({ /** * Present if the server offers any prompt templates. */ - prompts: z.optional( - z.object({ + prompts: z + .object({ /** * Whether this server supports issuing notifications for changes to the prompt list. */ - listChanged: z.optional(z.boolean()) + listChanged: z.boolean().optional() }) - ), + .optional(), /** * Present if the server offers any resources to read. */ @@ -429,11 +443,11 @@ export const ProgressSchema = z.object({ /** * Total number of items to process (or total progress required), if known. */ - total: z.optional(z.number()), + total: z.number().optional(), /** * An optional message describing the current progress. */ - message: z.optional(z.string()) + message: z.string().optional() }); export const ProgressNotificationParamsSchema = NotificationsParamsSchema.merge(ProgressSchema).extend({ @@ -470,7 +484,7 @@ export const PaginatedResultSchema = ResultSchema.extend({ * An opaque token representing the pagination position after the last returned result. * If present, there may be more results available. */ - nextCursor: z.optional(CursorSchema) + nextCursor: CursorSchema.optional() }); /* Resources */ @@ -485,7 +499,7 @@ export const ResourceContentsSchema = z.object({ /** * The MIME type of this resource, if known. */ - mimeType: z.optional(z.string()), + mimeType: z.string().optional(), /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -540,18 +554,18 @@ export const ResourceSchema = BaseMetadataSchema.extend({ * * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. */ - description: z.optional(z.string()), + description: z.string().optional(), /** * The MIME type of this resource, if known. */ - mimeType: z.optional(z.string()), + mimeType: z.string().optional(), /** * 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.record(z.string(), z.unknown()).optional() }).merge(IconsSchema); /** @@ -568,18 +582,18 @@ export const ResourceTemplateSchema = BaseMetadataSchema.extend({ * * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. */ - description: z.optional(z.string()), + description: z.string().optional(), /** * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. */ - mimeType: z.optional(z.string()), + mimeType: z.string().optional(), /** * 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.record(z.string(), z.unknown()).optional() }).merge(IconsSchema); /** @@ -694,11 +708,11 @@ export const PromptArgumentSchema = z.object({ /** * A human-readable description of the argument. */ - description: z.optional(z.string()), + description: z.string().optional(), /** * Whether this argument must be provided. */ - required: z.optional(z.boolean()) + required: z.boolean().optional() }); /** @@ -708,16 +722,16 @@ export const PromptSchema = BaseMetadataSchema.extend({ /** * An optional description of what this prompt provides */ - description: z.optional(z.string()), + description: z.string().optional(), /** * A list of arguments to use for templating the prompt. */ - arguments: z.optional(z.array(PromptArgumentSchema)), + arguments: z.array(PromptArgumentSchema).optional(), /** * 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.record(z.string(), z.unknown()).optional() }).merge(IconsSchema); /** @@ -862,7 +876,7 @@ export const GetPromptResultSchema = ResultSchema.extend({ /** * An optional description for the prompt. */ - description: z.optional(z.string()), + description: z.string().optional(), messages: z.array(PromptMessageSchema) }); @@ -942,7 +956,7 @@ export const ToolSchema = BaseMetadataSchema.extend({ inputSchema: z.object({ type: z.literal('object'), properties: z.record(z.string(), AssertObjectSchema).optional(), - required: z.optional(z.array(z.string())) + required: z.array(z.string()).optional() }), /** * An optional JSON Schema object defining the structure of the tool's output returned in @@ -952,17 +966,17 @@ export const ToolSchema = BaseMetadataSchema.extend({ .object({ type: z.literal('object'), properties: z.record(z.string(), AssertObjectSchema).optional(), - required: z.optional(z.array(z.string())), + required: z.array(z.string()).optional(), /** * Not in the MCP specification, but added to support the Ajv validator while removing .passthrough() which previously allowed additionalProperties to be passed through. */ - additionalProperties: z.optional(z.boolean()) + additionalProperties: z.boolean().optional() }) .optional(), /** * Optional additional tool information. */ - annotations: z.optional(ToolAnnotationsSchema), + annotations: ToolAnnotationsSchema.optional(), /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) @@ -1018,7 +1032,7 @@ export const CallToolResultSchema = ResultSchema.extend({ * server does not support tool calls, or any other exceptional conditions, * should be reported as an MCP error response. */ - isError: z.optional(z.boolean()) + isError: z.boolean().optional() }); /** @@ -1041,7 +1055,7 @@ export const CallToolRequestParamsSchema = BaseRequestParamsSchema.extend({ /** * Arguments to pass to the tool. */ - arguments: z.optional(z.record(z.string(), z.unknown())) + arguments: z.record(z.string(), z.unknown()).optional() }); /** @@ -1125,19 +1139,19 @@ export const ModelPreferencesSchema = z.object({ /** * Optional hints to use for model selection. */ - hints: z.optional(z.array(ModelHintSchema)), + hints: z.array(ModelHintSchema).optional(), /** * How much to prioritize cost when selecting a model. */ - costPriority: z.optional(z.number().min(0).max(1)), + costPriority: z.number().min(0).max(1).optional(), /** * How much to prioritize sampling speed (latency) when selecting a model. */ - speedPriority: z.optional(z.number().min(0).max(1)), + speedPriority: z.number().min(0).max(1).optional(), /** * How much to prioritize intelligence and capabilities when selecting a model. */ - intelligencePriority: z.optional(z.number().min(0).max(1)) + intelligencePriority: z.number().min(0).max(1).optional() }); /** @@ -1197,7 +1211,7 @@ export const CreateMessageResultSchema = ResultSchema.extend({ /** * The reason why sampling stopped. */ - stopReason: z.optional(z.enum(['endTurn', 'stopSequence', 'maxTokens']).or(z.string())), + stopReason: z.enum(['endTurn', 'stopSequence', 'maxTokens']).or(z.string()).optional(), role: z.enum(['user', 'assistant']), content: z.discriminatedUnion('type', [TextContentSchema, ImageContentSchema, AudioContentSchema]) }); @@ -1457,22 +1471,20 @@ export function assertCompleteRequestResourceTemplate(request: CompleteRequest): * 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.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.number().int().optional(), + /** + * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. + */ + hasMore: z.boolean().optional() + }) }); /* Roots */ From 7d6210c6f7c84be1620e27a54a4a924eab56b986 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 17 Nov 2025 10:11:30 +0200 Subject: [PATCH 2/6] clean up --- src/server/title.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/title.test.ts b/src/server/title.test.ts index af37e8b29..7f0feedc8 100644 --- a/src/server/title.test.ts +++ b/src/server/title.test.ts @@ -76,7 +76,7 @@ describe('Title field backwards compatibility', () => { expect(prompts.prompts[0].name).toBe('test-prompt'); expect(prompts.prompts[0].title).toBe('Test Prompt Display Name'); expect(prompts.prompts[0].description).toBe('A test prompt'); - }, 15_000); + }); it('should work with prompts using registerPrompt', async () => { const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); From e232939f8801b3ff4fa3990c5117afac0b40acb9 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 17 Nov 2025 12:39:30 +0200 Subject: [PATCH 3/6] save commit --- src/client/index.test.ts | 4 +++ src/client/index.ts | 18 ++++++------- src/server/index.ts | 15 +++++------ src/server/mcp.test.ts | 1 + src/shared/protocol.test.ts | 20 +++++++-------- src/shared/protocol.ts | 12 ++++++--- src/spec.types.test.ts | 16 +++++++++++- src/types.ts | 50 +++++++++++++++++++++++-------------- 8 files changed, 87 insertions(+), 49 deletions(-) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index 70508ebaa..11a00acb5 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -822,6 +822,7 @@ describe('outputSchema validation', () => { server.setRequestHandler(CallToolRequestSchema, async request => { if (request.params.name === 'test-tool') { return { + content: [], structuredContent: { result: 'success', count: 42 } }; } @@ -897,6 +898,7 @@ describe('outputSchema validation', () => { if (request.params.name === 'test-tool') { // Return invalid structured content (count is string instead of number) return { + content: [], structuredContent: { result: 'success', count: 'not a number' } }; } @@ -1124,6 +1126,7 @@ describe('outputSchema validation', () => { server.setRequestHandler(CallToolRequestSchema, async request => { if (request.params.name === 'complex-tool') { return { + content: [], structuredContent: { name: 'John Doe', age: 30, @@ -1209,6 +1212,7 @@ describe('outputSchema validation', () => { if (request.params.name === 'strict-tool') { // Return structured content with extra property return { + content: [], structuredContent: { name: 'John', extraField: 'not allowed' diff --git a/src/client/index.ts b/src/client/index.ts index 5770f9d7f..ddcabe956 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -7,7 +7,6 @@ import { type ClientNotification, type ClientRequest, type ClientResult, - type CompatibilityCallToolResultSchema, type CompleteRequest, CompleteResultSchema, EmptyResultSchema, @@ -27,18 +26,19 @@ import { ListToolsResultSchema, type LoggingLevel, McpError, - type Notification, type ReadResourceRequest, ReadResourceResultSchema, - type Request, - type Result, type ServerCapabilities, SUPPORTED_PROTOCOL_VERSIONS, type SubscribeRequest, type Tool, type UnsubscribeRequest, ElicitResultSchema, - ElicitRequestSchema + ElicitRequestSchema, + type RequestGeneric, + type NotificationGeneric, + type ResultGeneric, + Result } from '../types.js'; import { AjvJsonSchemaValidator } from '../validation/ajv-provider.js'; import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator } from '../validation/types.js'; @@ -149,9 +149,9 @@ export type ClientOptions = ProtocolOptions & { * ``` */ export class Client< - RequestT extends Request = Request, - NotificationT extends Notification = Notification, - ResultT extends Result = Result + RequestT extends RequestGeneric = RequestGeneric, + NotificationT extends NotificationGeneric = NotificationGeneric, + ResultT extends ResultGeneric = Result > extends Protocol { private _serverCapabilities?: ServerCapabilities; private _serverVersion?: Implementation; @@ -461,7 +461,7 @@ export class Client< async callTool( params: CallToolRequest['params'], - resultSchema: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema = CallToolResultSchema, + resultSchema: typeof CallToolResultSchema = CallToolResultSchema, options?: RequestOptions ) { const result = await this.request({ method: 'tools/call', params }, resultSchema, options); diff --git a/src/server/index.ts b/src/server/index.ts index 47b5f538f..0c61a014f 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -20,16 +20,17 @@ import { LoggingLevelSchema, type LoggingMessageNotification, McpError, - type Notification, - type Request, type ResourceUpdatedNotification, - type Result, type ServerCapabilities, type ServerNotification, type ServerRequest, type ServerResult, SetLevelRequestSchema, - SUPPORTED_PROTOCOL_VERSIONS + SUPPORTED_PROTOCOL_VERSIONS, + type RequestGeneric, + type NotificationGeneric, + type ResultGeneric, + Result } from '../types.js'; import { AjvJsonSchemaValidator } from '../validation/ajv-provider.js'; import type { JsonSchemaType, jsonSchemaValidator } from '../validation/types.js'; @@ -104,9 +105,9 @@ export type ServerOptions = ProtocolOptions & { * @deprecated Use `McpServer` instead for the high-level API. Only use `Server` for advanced use cases. */ export class Server< - RequestT extends Request = Request, - NotificationT extends Notification = Notification, - ResultT extends Result = Result + RequestT extends RequestGeneric = RequestGeneric, + NotificationT extends NotificationGeneric = NotificationGeneric, + ResultT extends ResultGeneric = Result > extends Protocol { private _clientCapabilities?: ClientCapabilities; private _clientVersion?: Implementation; diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index e2291481a..a56d3787d 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -4273,6 +4273,7 @@ describe('elicitInput()', () => { expect(checkAvailability).toHaveBeenCalledWith('ABC Restaurant', '2024-12-25', 2); expect(findAlternatives).not.toHaveBeenCalled(); + expect(result.content).toEqual([ { type: 'text', diff --git a/src/shared/protocol.test.ts b/src/shared/protocol.test.ts index b47de8c55..b57a62ade 100644 --- a/src/shared/protocol.test.ts +++ b/src/shared/protocol.test.ts @@ -1,5 +1,5 @@ import { ZodType, z } from 'zod'; -import { ClientCapabilities, ErrorCode, McpError, Notification, Request, Result, ServerCapabilities } from '../types.js'; +import { ClientCapabilities, ErrorCode, McpError, NotificationGeneric, RequestGeneric, Result, ServerCapabilities } from '../types.js'; import { Protocol, mergeCapabilities } from './protocol.js'; import { Transport } from './transport.js'; import { MockInstance } from 'vitest'; @@ -18,14 +18,14 @@ class MockTransport implements Transport { } describe('protocol tests', () => { - let protocol: Protocol; + let protocol: Protocol; let transport: MockTransport; let sendSpy: MockInstance; beforeEach(() => { transport = new MockTransport(); sendSpy = vi.spyOn(transport, 'send'); - protocol = new (class extends Protocol { + protocol = new (class extends Protocol { protected assertCapabilityForMethod(): void {} protected assertNotificationCapability(): void {} protected assertRequestHandlerCapability(): void {} @@ -479,7 +479,7 @@ describe('protocol tests', () => { it('should NOT debounce a notification that has parameters', async () => { // ARRANGE - protocol = new (class extends Protocol { + protocol = new (class extends Protocol { protected assertCapabilityForMethod(): void {} protected assertNotificationCapability(): void {} protected assertRequestHandlerCapability(): void {} @@ -500,7 +500,7 @@ describe('protocol tests', () => { it('should NOT debounce a notification that has a relatedRequestId', async () => { // ARRANGE - protocol = new (class extends Protocol { + protocol = new (class extends Protocol { protected assertCapabilityForMethod(): void {} protected assertNotificationCapability(): void {} protected assertRequestHandlerCapability(): void {} @@ -519,7 +519,7 @@ describe('protocol tests', () => { it('should clear pending debounced notifications on connection close', async () => { // ARRANGE - protocol = new (class extends Protocol { + protocol = new (class extends Protocol { protected assertCapabilityForMethod(): void {} protected assertNotificationCapability(): void {} protected assertRequestHandlerCapability(): void {} @@ -543,7 +543,7 @@ describe('protocol tests', () => { it('should debounce multiple synchronous calls when params property is omitted', async () => { // ARRANGE - protocol = new (class extends Protocol { + protocol = new (class extends Protocol { protected assertCapabilityForMethod(): void {} protected assertNotificationCapability(): void {} protected assertRequestHandlerCapability(): void {} @@ -570,7 +570,7 @@ describe('protocol tests', () => { it('should debounce calls when params is explicitly undefined', async () => { // ARRANGE - protocol = new (class extends Protocol { + protocol = new (class extends Protocol { protected assertCapabilityForMethod(): void {} protected assertNotificationCapability(): void {} protected assertRequestHandlerCapability(): void {} @@ -595,7 +595,7 @@ describe('protocol tests', () => { it('should send non-debounced notifications immediately and multiple times', async () => { // ARRANGE - protocol = new (class extends Protocol { + protocol = new (class extends Protocol { protected assertCapabilityForMethod(): void {} protected assertNotificationCapability(): void {} protected assertRequestHandlerCapability(): void {} @@ -628,7 +628,7 @@ describe('protocol tests', () => { it('should handle sequential batches of debounced notifications correctly', async () => { // ARRANGE - protocol = new (class extends Protocol { + protocol = new (class extends Protocol { protected assertCapabilityForMethod(): void {} protected assertNotificationCapability(): void {} protected assertRequestHandlerCapability(): void {} diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 48cad896f..2e6150754 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -19,11 +19,13 @@ import { ProgressNotificationSchema, Request, RequestId, - Result, ServerCapabilities, RequestMeta, MessageExtraInfo, - RequestInfo + RequestInfo, + type RequestGeneric, + type NotificationGeneric, + type ResultGeneric } from '../types.js'; import { Transport, TransportSendOptions } from './transport.js'; import { AuthInfo } from '../server/auth/types.js'; @@ -171,7 +173,11 @@ type TimeoutInfo = { * Implements MCP protocol framing on top of a pluggable transport, including * features like request/response linking, notifications, and progress. */ -export abstract class Protocol { +export abstract class Protocol< + SendRequestT extends RequestGeneric, + SendNotificationT extends NotificationGeneric, + SendResultT extends ResultGeneric +> { private _transport?: Transport; private _requestMessageId = 0; private _requestHandlers: Map< diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts index b06b9e59a..ad43b4eb2 100644 --- a/src/spec.types.test.ts +++ b/src/spec.types.test.ts @@ -62,6 +62,12 @@ type MakeUnknownsNotOptional = } : T; +/** + * Spec Patches + * + * Temporary spec type patches, to be used until the spec is updated to fix the issues. + */ + // Targeted fix: in spec, treat ClientCapabilities.elicitation?: object as Record type FixSpecClientCapabilities = T extends { elicitation?: object } ? Omit & { elicitation?: Record } @@ -75,6 +81,14 @@ type FixSpecInitializeRequest = T extends { params: infer P } ? Omit = T extends { params: infer P } ? Omit & { params: FixSpecInitializeRequestParams

} : T; +// ElicitResult - spec does not allow undefined values in the content object +type SpecElicitResultPatched = Omit & { + action: SpecTypes.ElicitResult['action']; + content?: { + [key: string]: string | number | boolean | string[] | undefined; + }; +}; + const sdkTypeChecks = { RequestParams: (sdk: SDKTypes.RequestParams, spec: SpecTypes.RequestParams) => { sdk = spec; @@ -205,7 +219,7 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - ElicitResult: (sdk: SDKTypes.ElicitResult, spec: SpecTypes.ElicitResult) => { + ElicitResult: (sdk: SDKTypes.ElicitResult, spec: SpecElicitResultPatched) => { sdk = spec; spec = sdk; }, diff --git a/src/types.ts b/src/types.ts index 0b957350e..2bb995d3a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,7 +42,7 @@ const BaseRequestParamsSchema = z.object({ /** * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. */ - _meta: RequestMetaSchema.optional() + _meta: z.intersection(RequestMetaSchema.optional(), z.record(z.string(), z.unknown()).optional()).optional() }); export const RequestSchema = z.object({ @@ -56,9 +56,11 @@ export const RequestSchema = z.object({ * Used in {@link JSONRPCRequestSchema} for generic shape matching. */ export const RequestSchemaGeneric = RequestSchema.extend({ - params: z.intersection(RequestSchema.shape.params, z.record(z.string(), z.unknown()).optional()) + params: z.intersection(RequestSchema.shape.params.optional(), z.record(z.string(), z.unknown()).optional()) }); +export type RequestGeneric = ExpandRecursively>; + const NotificationsParamsSchema = z.object({ /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) @@ -81,13 +83,17 @@ export const NotificationSchemaGeneric = NotificationSchema.extend({ params: z.intersection(NotificationSchema.shape.params, z.record(z.string(), z.unknown()).optional()) }); -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.record(z.string(), z.unknown()).optional() -}); +export type NotificationGeneric = z.infer; + +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.record(z.string(), z.unknown()).optional() + }) + .catchall(z.unknown()); /** * Generic result schema that allows any additional fields to be added to the result. @@ -95,6 +101,8 @@ export const ResultSchema = z.object({ * Used in {@link JSONRPCResponseSchema} for generic shape matching. */ export const ResultSchemaGeneric = z.intersection(ResultSchema, z.record(z.string(), z.unknown()).optional()); + +export type ResultGeneric = ExpandRecursively>; /** * A uniquely identifying ID for a request in JSON-RPC. */ @@ -1035,14 +1043,18 @@ export const CallToolResultSchema = ResultSchema.extend({ isError: z.boolean().optional() }); -/** - * CallToolResultSchema extended with backwards compatibility to protocol version 2024-10-07. - */ -export const CompatibilityCallToolResultSchema = CallToolResultSchema.or( - ResultSchema.extend({ - toolResult: z.unknown() - }) -); +// const LegacyCallToolResultSchema = ResultSchema.extend({ +// toolResult: z.unknown() +// }); + +// export type LegacyCallToolResult = z.infer; + +// export const isLegacyCallToolResult = (value: unknown): value is LegacyCallToolResult => LegacyCallToolResultSchema.safeParse(value).success; + +// /** +// * CallToolResultSchema extended with backwards compatibility to protocol version 2024-10-07. +// */ +// export const CompatibilityCallToolResultSchema = CallToolResultSchema.or(LegacyCallToolResultSchema); /** * Parameters for a `tools/call` request. @@ -1389,7 +1401,7 @@ export const ElicitResultSchema = ResultSchema.extend({ * The submitted form data, only present when action is "accept". * Contains values matching the requested schema. */ - content: z.record(z.union([z.string(), z.number(), z.boolean(), z.array(z.string())])).optional() + content: z.record(z.union([z.string(), z.number(), z.boolean(), z.array(z.string()), z.undefined()])).optional() }); /* Autocomplete */ @@ -1733,7 +1745,7 @@ export type ListToolsRequest = Infer; export type ListToolsResult = Infer; export type CallToolRequestParams = Infer; export type CallToolResult = Infer; -export type CompatibilityCallToolResult = Infer; +// export type CompatibilityCallToolResult = Infer; export type CallToolRequest = Infer; export type ToolListChangedNotification = Infer; From 1f52271c39cb387d075b8db099434b328f25cee2 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 17 Nov 2025 13:54:25 +0200 Subject: [PATCH 4/6] clean up --- src/client/index.test.ts | 1 - src/client/index.ts | 5 ++--- src/server/index.ts | 5 ++--- src/shared/protocol.ts | 4 ++-- src/types.ts | 10 +--------- 5 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index 11a00acb5..2a6b7f231 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -822,7 +822,6 @@ describe('outputSchema validation', () => { server.setRequestHandler(CallToolRequestSchema, async request => { if (request.params.name === 'test-tool') { return { - content: [], structuredContent: { result: 'success', count: 42 } }; } diff --git a/src/client/index.ts b/src/client/index.ts index ddcabe956..7c242e46c 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -37,8 +37,7 @@ import { ElicitRequestSchema, type RequestGeneric, type NotificationGeneric, - type ResultGeneric, - Result + type Result } from '../types.js'; import { AjvJsonSchemaValidator } from '../validation/ajv-provider.js'; import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator } from '../validation/types.js'; @@ -151,7 +150,7 @@ export type ClientOptions = ProtocolOptions & { export class Client< RequestT extends RequestGeneric = RequestGeneric, NotificationT extends NotificationGeneric = NotificationGeneric, - ResultT extends ResultGeneric = Result + ResultT extends Result = Result > extends Protocol { private _serverCapabilities?: ServerCapabilities; private _serverVersion?: Implementation; diff --git a/src/server/index.ts b/src/server/index.ts index 0c61a014f..9c6fb1385 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -29,8 +29,7 @@ import { SUPPORTED_PROTOCOL_VERSIONS, type RequestGeneric, type NotificationGeneric, - type ResultGeneric, - Result + type Result } from '../types.js'; import { AjvJsonSchemaValidator } from '../validation/ajv-provider.js'; import type { JsonSchemaType, jsonSchemaValidator } from '../validation/types.js'; @@ -107,7 +106,7 @@ export type ServerOptions = ProtocolOptions & { export class Server< RequestT extends RequestGeneric = RequestGeneric, NotificationT extends NotificationGeneric = NotificationGeneric, - ResultT extends ResultGeneric = Result + ResultT extends Result = Result > extends Protocol { private _clientCapabilities?: ClientCapabilities; private _clientVersion?: Implementation; diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 2e6150754..91c5ade58 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -25,7 +25,7 @@ import { RequestInfo, type RequestGeneric, type NotificationGeneric, - type ResultGeneric + type Result } from '../types.js'; import { Transport, TransportSendOptions } from './transport.js'; import { AuthInfo } from '../server/auth/types.js'; @@ -176,7 +176,7 @@ type TimeoutInfo = { export abstract class Protocol< SendRequestT extends RequestGeneric, SendNotificationT extends NotificationGeneric, - SendResultT extends ResultGeneric + SendResultT extends Result > { private _transport?: Transport; private _requestMessageId = 0; diff --git a/src/types.ts b/src/types.ts index 2bb995d3a..7ccd19c0f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -95,14 +95,6 @@ export const ResultSchema = z }) .catchall(z.unknown()); -/** - * Generic result schema that allows any additional fields to be added to the result. - * - * Used in {@link JSONRPCResponseSchema} for generic shape matching. - */ -export const ResultSchemaGeneric = z.intersection(ResultSchema, z.record(z.string(), z.unknown()).optional()); - -export type ResultGeneric = ExpandRecursively>; /** * A uniquely identifying ID for a request in JSON-RPC. */ @@ -140,7 +132,7 @@ export const JSONRPCResponseSchema = z .object({ jsonrpc: z.literal(JSONRPC_VERSION), id: RequestIdSchema, - result: ResultSchemaGeneric + result: ResultSchema }) .strict(); From fe1b145441d08ba85eafd2d43d6286fa5f53ebde Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 17 Nov 2025 13:55:28 +0200 Subject: [PATCH 5/6] clean up --- src/client/index.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index 2a6b7f231..70508ebaa 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -897,7 +897,6 @@ describe('outputSchema validation', () => { if (request.params.name === 'test-tool') { // Return invalid structured content (count is string instead of number) return { - content: [], structuredContent: { result: 'success', count: 'not a number' } }; } @@ -1125,7 +1124,6 @@ describe('outputSchema validation', () => { server.setRequestHandler(CallToolRequestSchema, async request => { if (request.params.name === 'complex-tool') { return { - content: [], structuredContent: { name: 'John Doe', age: 30, @@ -1211,7 +1209,6 @@ describe('outputSchema validation', () => { if (request.params.name === 'strict-tool') { // Return structured content with extra property return { - content: [], structuredContent: { name: 'John', extraField: 'not allowed' From bcec7dc4215dd366b0d0ac93f68c8e5c85f1b673 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 17 Nov 2025 13:57:30 +0200 Subject: [PATCH 6/6] remove CompatibilityCallToolResultSchema - coming from protocol 2024-10-07, which had previous breaking changes --- src/types.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/types.ts b/src/types.ts index 7ccd19c0f..ee50809d9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1035,19 +1035,6 @@ export const CallToolResultSchema = ResultSchema.extend({ isError: z.boolean().optional() }); -// const LegacyCallToolResultSchema = ResultSchema.extend({ -// toolResult: z.unknown() -// }); - -// export type LegacyCallToolResult = z.infer; - -// export const isLegacyCallToolResult = (value: unknown): value is LegacyCallToolResult => LegacyCallToolResultSchema.safeParse(value).success; - -// /** -// * CallToolResultSchema extended with backwards compatibility to protocol version 2024-10-07. -// */ -// export const CompatibilityCallToolResultSchema = CallToolResultSchema.or(LegacyCallToolResultSchema); - /** * Parameters for a `tools/call` request. */ @@ -1737,7 +1724,6 @@ export type ListToolsRequest = Infer; export type ListToolsResult = Infer; export type CallToolRequestParams = Infer; export type CallToolResult = Infer; -// export type CompatibilityCallToolResult = Infer; export type CallToolRequest = Infer; export type ToolListChangedNotification = Infer;