Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 2.1k
feat(core): custom-method support (3-arg setRequestHandler + request schema overload)#1974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
690af0d10a787834a4fd5dc09b1d614dbe8d49a02b35a96289df35c2a2f2f8a5568303File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| '@modelcontextprotocol/core': minor | ||
| '@modelcontextprotocol/client': minor | ||
| '@modelcontextprotocol/server': minor | ||
| --- | ||
| Add custom (non-spec) method support: a 3-arg `setRequestHandler(method, schemas, handler)` / `setNotificationHandler(method, schemas, handler)` form for vendor-prefixed methods, and a `request(req, resultSchema)` overload (also on `ctx.mcpReq.send`) for typed custom-method results. Spec-method calls are unchanged. | ||
| Response result-schema validation failure now rejects with `SdkError(InvalidResult)` instead of a raw `ZodError`. Adds `SdkErrorCode.InvalidResult`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -120,6 +120,7 @@ Two error classes now exist: | ||
| | 403 after upscoping | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpForbidden` | | ||
| | Unexpected content type | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpUnexpectedContent` | | ||
| | Session termination failed | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpFailedToTerminateSession` | | ||
| | Response result fails schema | `ZodError` (raw) | `SdkError` with `SdkErrorCode.InvalidResult` | | ||
| New `SdkErrorCode` enum values: | ||
| @@ -130,6 +131,7 @@ New `SdkErrorCode` enum values: | ||
| - `SdkErrorCode.RequestTimeout` = `'REQUEST_TIMEOUT'` | ||
| - `SdkErrorCode.ConnectionClosed` = `'CONNECTION_CLOSED'` | ||
| - `SdkErrorCode.SendFailed` = `'SEND_FAILED'` | ||
| - `SdkErrorCode.InvalidResult` = `'INVALID_RESULT'` | ||
| - `SdkErrorCode.ClientHttpNotImplemented` = `'CLIENT_HTTP_NOT_IMPLEMENTED'` | ||
| - `SdkErrorCode.ClientHttpAuthentication` = `'CLIENT_HTTP_AUTHENTICATION'` | ||
| - `SdkErrorCode.ClientHttpForbidden` = `'CLIENT_HTTP_FORBIDDEN'` | ||
| @@ -351,6 +353,28 @@ server.setRequestHandler('initialize', async (request) => { ... }); | ||
| server.setNotificationHandler('notifications/message', (notification) => { ... }); | ||
| ``` | ||
| For custom (non-spec) methods, use the 3-arg form `(method, schemas, handler)`: | ||
| ```typescript | ||
| // v1: Zod schema with method literal | ||
| server.setRequestHandler(z.object({ method: z.literal('acme/search'), params: P }), async req => { ... }); | ||
| // v2: method string + schemas object; handler receives parsed params | ||
| server.setRequestHandler('acme/search', { params: P, result: R }, async (params, ctx) => { ... }); | ||
| client.setNotificationHandler('acme/progress', { params: P }, (params, notification) => { ... }); | ||
| ``` | ||
| The 3-arg notification handler receives the raw notification as its second argument, so `_meta` is recoverable via `notification.params?._meta`. | ||
| To send a custom-method request, pass a result schema as the second argument to `request()` (and `ctx.mcpReq.send()`): | ||
| ```typescript | ||
| // v1 | ||
| await client.request({ method: 'acme/search', params }, ResultSchema); | ||
| // v2 (unchanged; now any Standard Schema, not Zod-only) | ||
| await client.request({ method: 'acme/search', params }, ResultSchema); | ||
| ``` | ||
claude[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| Schema to method string mapping: | ||
| | v1 Schema | v2 Method String | | ||
| @@ -406,9 +430,9 @@ Request/notification params remain fully typed. Remove unused schema imports aft | ||
| | `ctx.mcpReq.elicitInput(params, options?)` | Elicit user input (form or URL) | `server.elicitInput(...)` from within handler | | ||
| | `ctx.mcpReq.requestSampling(params, options?)` | Request LLM sampling from client | `server.createMessage(...)` from within handler | | ||
| ## 11. Schema parameter removed from `request()`, `send()`, and `callTool()` | ||
| ## 11. Schema parameter removed from `request()`, `send()`, and `callTool()` (spec methods) | ||
| `Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` no longer take a Zod result schema argument. The SDK resolves the schema internally from the method name. | ||
| For **spec** methods, `Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` no longer require a Zod result schema argument. The SDK resolves the schema internally from the method name. | ||
| ```typescript | ||
| // v1: schema required | ||
| @@ -432,6 +456,8 @@ const tool = await client.callTool({ name: 'my-tool', arguments: {} }); | ||
| | `client.callTool(params, CompatibilityCallToolResultSchema)` | `client.callTool(params)` | | ||
| | `client.callTool(params, schema, options)` | `client.callTool(params, options)` | | ||
| For **custom (non-spec)** methods, keep the result-schema argument — see §9. Only apply the rewrites above when `req.method` is a spec method. | ||
| Remove unused schema imports: `CallToolResultSchema`, `CompatibilityCallToolResultSchema`, `ElicitResultSchema`, `CreateMessageResultSchema`, etc., when they were only used in `request()`/`send()`/`callTool()` calls. | ||
| If `CallToolResultSchema` was used for **runtime validation** (not just as a `request()` argument), replace with the `isCallToolResult` type guard: | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /** | ||
| * Custom (non-spec) method example: a client that sends `acme/search` and | ||
| * listens for `acme/searchProgress` notifications. | ||
| * | ||
| * Build `examples/server` first; this client spawns the server via stdio. | ||
| */ | ||
| import { Client } from '@modelcontextprotocol/client'; | ||
| import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; | ||
| import { z } from 'zod/v4'; | ||
| const SearchResult = z.object({ items: z.array(z.string()) }); | ||
| const SearchProgressParams = z.object({ stage: z.string(), pct: z.number() }); | ||
| const client = new Client({ name: 'acme-search-client', version: '0.0.0' }); | ||
| client.setNotificationHandler('acme/searchProgress', { params: SearchProgressParams }, params => { | ||
| console.log(`[progress] ${params.stage} ${Math.round(params.pct * 100)}%`); | ||
| }); | ||
| await client.connect(new StdioClientTransport({ command: 'node', args: ['../server/dist/customMethodExample.js'] })); | ||
| const result = await client.request({ method: 'acme/search', params: { query: 'mcp', limit: 3 } }, SearchResult); | ||
| console.log('items:', result.items); | ||
| await client.close(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| /** | ||
| * Custom (non-spec) method example: a server that handles a vendor-prefixed | ||
| * `acme/search` request and emits `acme/searchProgress` notifications. | ||
| * | ||
| * Spawned via stdio by `examples/client/src/customMethodExample.ts`; do not run standalone. | ||
| */ | ||
| import { McpServer } from '@modelcontextprotocol/server'; | ||
| import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; | ||
| import { z } from 'zod/v4'; | ||
| const SearchParams = z.object({ query: z.string(), limit: z.number().int().default(10) }); | ||
| const SearchResult = z.object({ items: z.array(z.string()) }); | ||
| const mcp = new McpServer({ name: 'acme-search', version: '0.0.0' }); | ||
| mcp.server.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, ctx) => { | ||
| await ctx.mcpReq.notify({ method: 'acme/searchProgress', params: { stage: 'start', pct: 0 } }); | ||
| const items = Array.from({ length: params.limit }, (_, i) => `${params.query}-${i}`); | ||
| await ctx.mcpReq.notify({ method: 'acme/searchProgress', params: { stage: 'done', pct: 1 } }); | ||
| return { items }; | ||
| }); | ||
| await mcp.connect(new StdioServerTransport()); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -26,6 +26,8 @@ export enum SdkErrorCode { | ||
| ConnectionClosed='CONNECTION_CLOSED', | ||
| /** Failed to send message */ | ||
| SendFailed='SEND_FAILED', | ||
| /** Response result failed local schema validation */ | ||
| InvalidResult='INVALID_RESULT', | ||
claude[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // Transport errors | ||
| ClientHttpNotImplemented='CLIENT_HTTP_NOT_IMPLEMENTED', | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -45,6 +45,7 @@ export type { | ||
| NotificationOptions, | ||
| ProgressCallback, | ||
| ProtocolOptions, | ||
| RequestHandlerSchemas, | ||
claude[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| RequestOptions, | ||
| ServerContext | ||
| } from '../../shared/protocol.js'; | ||
| @@ -137,7 +138,7 @@ export { isTerminal } from '../../experimental/tasks/interfaces.js'; | ||
| export { InMemoryTaskMessageQueue, InMemoryTaskStore } from '../../experimental/tasks/stores/inMemory.js'; | ||
| // Validator types and classes | ||
| export type { StandardSchemaWithJSON } from '../../util/standardSchema.js'; | ||
| export type { StandardSchemaV1, StandardSchemaWithJSON } from '../../util/standardSchema.js'; | ||
| export { AjvJsonSchemaValidator } from '../../validators/ajvProvider.js'; | ||
| export type { CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider.js'; | ||
| // fromJsonSchema is intentionally NOT exported here — the server and client packages | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /** | ||
| * Type-checked examples for `protocol.ts`. | ||
| * | ||
| * These examples are synced into JSDoc comments via the sync-snippets script. | ||
| * Each function's region markers define the code snippet that appears in the docs. | ||
| * | ||
| * @module | ||
| */ | ||
| import * as z from 'zod/v4'; | ||
| import type { BaseContext, Protocol } from './protocol.js'; | ||
| /** | ||
| * Example: registering a handler for a custom (non-spec) request method. | ||
| */ | ||
| function Protocol_setRequestHandler_customMethod(protocol: Protocol<BaseContext>) { | ||
| //#region Protocol_setRequestHandler_customMethod | ||
| const SearchParams = z.object({ query: z.string(), limit: z.number().optional() }); | ||
| const SearchResult = z.object({ hits: z.array(z.string()) }); | ||
| protocol.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, _ctx) => { | ||
| return { hits: [`result for ${params.query}`] }; | ||
| }); | ||
| //#endregion Protocol_setRequestHandler_customMethod | ||
| void protocol; | ||
| } | ||
| void Protocol_setRequestHandler_customMethod; |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.