Skip to content

Commit f20ca3b

Browse files
feat(core): restore v1 Zod-schema overloads on setRequestHandler/setNotificationHandler/request
Adds to Protocol (inherited by Client/Server): - setRequestHandler/setNotificationHandler(ZodSchema, handler) — the v1 form, first-class - request(req, resultSchema, opts?) — the v1 explicit-schema form, first-class - callTool(params, resultSchema?, opts?) — accepts the v1 schema arg (ignored) for source compat - removeRequestHandler/removeNotificationHandler/assertCanSetRequestHandler accept any method string Custom (non-spec) methods work via the Zod-schema form, same as v1. schema.ts/standardSchema.ts unchanged from main.
1 parent 9ed62fe commit f20ca3b

File tree

14 files changed

+411
-44
lines changed

14 files changed

+411
-44
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modelcontextprotocol/client': minor
3+
'@modelcontextprotocol/server': minor
4+
---
5+
6+
`setRequestHandler`/`setNotificationHandler` accept the v1 `(ZodSchema, handler)` form as a first-class alternative to `(methodString, handler)`. `request()` accepts an explicit result schema (`request(req, resultSchema, options?)`) and has a method-keyed return type for spec methods. `callTool(params, resultSchema?)` accepts the v1 schema arg (ignored). `removeRequestHandler`/`removeNotificationHandler`/`assertCanSetRequestHandler` accept any method string.

docs/migration-SKILL.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,15 @@ Schema to method string mapping:
377377

378378
Request/notification params remain fully typed. Remove unused schema imports after migration.
379379

380+
**Custom (non-standard) methods** — vendor extensions or sub-protocols whose method strings are not in the MCP spec — work on `Client`/`Server` directly using the same v1 Zod-schema form:
381+
382+
| Form | Notes |
383+
| ------------------------------------------------------------ | --------------------------------------------------------------------- |
384+
| `setRequestHandler(CustomReqSchema, (req, ctx) => ...)` | unchanged |
385+
| `setNotificationHandler(CustomNotifSchema, n => ...)` | unchanged |
386+
| `this.request({ method: 'vendor/x', params }, ResultSchema)` | unchanged |
387+
| `this.notification({ method: 'vendor/x', params })` | unchanged |
388+
380389
## 10. Request Handler Context Types
381390

382391
`RequestHandlerExtra` → structured context types with nested groups. Rename `extra``ctx` in all handler callbacks.

docs/migration.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,24 @@ Common method string replacements:
382382
| `ResourceListChangedNotificationSchema` | `'notifications/resources/list_changed'` |
383383
| `PromptListChangedNotificationSchema` | `'notifications/prompts/list_changed'` |
384384

385-
### `Protocol.request()`, `ctx.mcpReq.send()`, and `Client.callTool()` no longer take a schema parameter
385+
### Custom (non-standard) protocol methods
386386

387-
The public `Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` methods no longer accept a Zod result schema argument. The SDK now resolves the correct result schema internally based on the method name. This means you no longer need to import result schemas
387+
Vendor-specific methods are registered directly on `Client` or `Server` using the same Zod-schema form as v1: `setRequestHandler(zodSchemaWithMethodLiteral, handler)`. `request({ method, params }, ResultSchema)` and `notification({ method, params })` are unchanged from v1.
388+
389+
```typescript
390+
import { Server } from '@modelcontextprotocol/server';
391+
392+
const server = new Server({ name: 'app', version: '1.0.0' }, { capabilities: {} });
393+
394+
server.setRequestHandler(SearchRequestSchema, req => ({ hits: [req.params.query] }));
395+
396+
// Calling from a Client — unchanged from v1:
397+
const result = await client.request({ method: 'acme/search', params: { query: 'x' } }, SearchResult);
398+
```
399+
400+
### `Protocol.request()`, `ctx.mcpReq.send()`, and `Client.callTool()` schema parameter is now optional
401+
402+
The public `Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` methods still accept a result schema argument, but for spec methods it is optional — the SDK resolves the correct schema internally from the method name. You no longer need to import result schemas
388403
like `CallToolResultSchema` or `ElicitResultSchema` when making requests.
389404

390405
**`client.request()` — Before (v1):**

examples/client/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Most clients expect a server to be running. Start one from [`../server/README.md
3636
| Client credentials (M2M) | Machine-to-machine OAuth client credentials example. | [`src/simpleClientCredentials.ts`](src/simpleClientCredentials.ts) |
3737
| URL elicitation client | Drives URL-mode elicitation flows (sensitive input in a browser). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
3838
| Task interactive client | Demonstrates task-based execution + interactive server→client requests. | [`src/simpleTaskInteractiveClient.ts`](src/simpleTaskInteractiveClient.ts) |
39+
| Custom (non-standard) methods client | Sends `acme/*` custom requests and handles custom server notifications. | [`src/customMethodExample.ts`](src/customMethodExample.ts) |
3940

4041
## URL elicitation example (server + client)
4142

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Calling vendor-specific (non-spec) JSON-RPC methods from a `Client`.
4+
*
5+
* - Send a custom request: `client.request({ method, params }, resultSchema)`
6+
* - Send a custom notification: `client.notification({ method, params })`
7+
* - Receive a custom notification: `client.setNotificationHandler(ZodSchemaWithMethodLiteral, handler)`
8+
*
9+
* Pair with the server in examples/server/src/customMethodExample.ts.
10+
*/
11+
12+
import { Client, StdioClientTransport } from '@modelcontextprotocol/client';
13+
import { z } from 'zod';
14+
15+
const SearchResult = z.object({ hits: z.array(z.string()) });
16+
17+
const ProgressNotification = z.object({
18+
method: z.literal('acme/searchProgress'),
19+
params: z.object({ stage: z.string(), pct: z.number() })
20+
});
21+
22+
const client = new Client({ name: 'custom-method-client', version: '1.0.0' }, { capabilities: {} });
23+
24+
client.setNotificationHandler(ProgressNotification, n => {
25+
console.log(`[client] progress: ${n.params.stage} ${n.params.pct}%`);
26+
});
27+
28+
await client.connect(new StdioClientTransport({ command: 'node', args: ['../server/dist/customMethodExample.js'] }));
29+
30+
const r = await client.request({ method: 'acme/search', params: { query: 'widgets' } }, SearchResult);
31+
console.log('[client] hits=' + JSON.stringify(r.hits));
32+
33+
await client.notification({ method: 'acme/tick', params: { n: 1 } });
34+
await client.notification({ method: 'acme/tick', params: { n: 2 } });
35+
36+
await client.close();

examples/server/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pnpm tsx src/simpleStreamableHttp.ts
3838
| Task interactive server | Task-based execution with interactive server→client requests. | [`src/simpleTaskInteractive.ts`](src/simpleTaskInteractive.ts) |
3939
| Hono Streamable HTTP server | Streamable HTTP server built with Hono instead of Express. | [`src/honoWebStandardStreamableHttp.ts`](src/honoWebStandardStreamableHttp.ts) |
4040
| SSE polling demo server | Legacy SSE server intended for polling demos. | [`src/ssePollingExample.ts`](src/ssePollingExample.ts) |
41+
| Custom (non-standard) methods server | Registers `acme/*` custom request handlers and sends custom notifications. | [`src/customMethodExample.ts`](src/customMethodExample.ts) |
4142

4243
## OAuth demo flags (Streamable HTTP server)
4344

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Registering vendor-specific (non-spec) JSON-RPC methods on a `Server`.
4+
*
5+
* Custom methods use the Zod-schema form of `setRequestHandler` / `setNotificationHandler`:
6+
* pass a Zod object schema whose `method` field is `z.literal('<method>')`. The same overload
7+
* is available on `Client` (for server→client custom methods).
8+
*
9+
* To call these from the client side, use:
10+
* await client.request({ method: 'acme/search', params: { query: 'widgets' } }, SearchResult)
11+
* await client.notification({ method: 'acme/tick', params: { n: 1 } })
12+
* See examples/client/src/customMethodExample.ts.
13+
*/
14+
15+
import { Server, StdioServerTransport } from '@modelcontextprotocol/server';
16+
import { z } from 'zod';
17+
18+
const SearchRequest = z.object({
19+
method: z.literal('acme/search'),
20+
params: z.object({ query: z.string() })
21+
});
22+
23+
const TickNotification = z.object({
24+
method: z.literal('acme/tick'),
25+
params: z.object({ n: z.number() })
26+
});
27+
28+
const server = new Server({ name: 'custom-method-server', version: '1.0.0' }, { capabilities: {} });
29+
30+
server.setRequestHandler(SearchRequest, request => {
31+
console.log('[server] acme/search query=' + request.params.query);
32+
return { hits: [request.params.query, request.params.query + '-result'] };
33+
});
34+
35+
server.setNotificationHandler(TickNotification, n => {
36+
console.log('[server] acme/tick n=' + n.params.n);
37+
});
38+
39+
await server.connect(new StdioServerTransport());

packages/client/src/client/client.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/client/_shims'
22
import type {
33
BaseContext,
44
CallToolRequest,
5+
CallToolResult,
56
ClientCapabilities,
67
ClientContext,
78
ClientNotification,
@@ -24,16 +25,19 @@ import type {
2425
NotificationMethod,
2526
ProtocolOptions,
2627
ReadResourceRequest,
28+
Request,
2729
RequestMethod,
2830
RequestOptions,
2931
RequestTypeMap,
32+
Result,
3033
ResultTypeMap,
3134
ServerCapabilities,
3235
SubscribeRequest,
3336
TaskManagerOptions,
3437
Tool,
3538
Transport,
36-
UnsubscribeRequest
39+
UnsubscribeRequest,
40+
ZodLikeRequestSchema
3741
} from '@modelcontextprotocol/core';
3842
import {
3943
assertClientRequestTaskCapability,
@@ -50,6 +54,7 @@ import {
5054
extractTaskManagerOptions,
5155
GetPromptResultSchema,
5256
InitializeResultSchema,
57+
isZodLikeSchema,
5358
LATEST_PROTOCOL_VERSION,
5459
ListChangedOptionsBaseSchema,
5560
ListPromptsResultSchema,
@@ -336,9 +341,21 @@ export class Client extends Protocol<ClientContext> {
336341
public override setRequestHandler<M extends RequestMethod>(
337342
method: M,
338343
handler: (request: RequestTypeMap[M], ctx: ClientContext) => ResultTypeMap[M] | Promise<ResultTypeMap[M]>
339-
): void {
344+
): void;
345+
public override setRequestHandler<T extends ZodLikeRequestSchema>(
346+
requestSchema: T,
347+
handler: (request: ReturnType<T['parse']>, ctx: ClientContext) => Result | Promise<Result>
348+
): void;
349+
public override setRequestHandler(method: string | ZodLikeRequestSchema, schemaHandler: unknown): void {
350+
if (isZodLikeSchema(method)) {
351+
return this._registerCompatRequestHandler(
352+
method,
353+
schemaHandler as (request: unknown, ctx: ClientContext) => Result | Promise<Result>
354+
);
355+
}
356+
const handler = schemaHandler as (request: Request, ctx: ClientContext) => ClientResult | Promise<ClientResult>;
340357
if (method === 'elicitation/create') {
341-
const wrappedHandler = async (request: RequestTypeMap[M], ctx: ClientContext): Promise<ClientResult> => {
358+
const wrappedHandler = async (request: Request, ctx: ClientContext): Promise<ClientResult> => {
342359
const validatedRequest = parseSchema(ElicitRequestSchema, request);
343360
if (!validatedRequest.success) {
344361
// Type guard: if success is false, error is guaranteed to exist
@@ -404,11 +421,11 @@ export class Client extends Protocol<ClientContext> {
404421
};
405422

406423
// Install the wrapped handler
407-
return super.setRequestHandler(method, wrappedHandler);
424+
return super.setRequestHandler(method as RequestMethod, wrappedHandler);
408425
}
409426

410427
if (method === 'sampling/createMessage') {
411-
const wrappedHandler = async (request: RequestTypeMap[M], ctx: ClientContext): Promise<ClientResult> => {
428+
const wrappedHandler = async (request: Request, ctx: ClientContext): Promise<ClientResult> => {
412429
const validatedRequest = parseSchema(CreateMessageRequestSchema, request);
413430
if (!validatedRequest.success) {
414431
const errorMessage =
@@ -447,11 +464,11 @@ export class Client extends Protocol<ClientContext> {
447464
};
448465

449466
// Install the wrapped handler
450-
return super.setRequestHandler(method, wrappedHandler);
467+
return super.setRequestHandler(method as RequestMethod, wrappedHandler);
451468
}
452469

453470
// Other handlers use default behavior
454-
return super.setRequestHandler(method, handler);
471+
return super.setRequestHandler(method as RequestMethod, handler);
455472
}
456473

457474
protected assertCapability(capability: keyof ServerCapabilities, method: string): void {
@@ -867,7 +884,18 @@ export class Client extends Protocol<ClientContext> {
867884
* }
868885
* ```
869886
*/
870-
async callTool(params: CallToolRequest['params'], options?: RequestOptions) {
887+
async callTool(params: CallToolRequest['params'], options?: RequestOptions): Promise<CallToolResult>;
888+
/** Result schema is resolved automatically; the second argument is accepted for v1 source compatibility and ignored. */
889+
async callTool(params: CallToolRequest['params'], resultSchema: unknown, options?: RequestOptions): Promise<CallToolResult>;
890+
async callTool(
891+
params: CallToolRequest['params'],
892+
optionsOrSchema?: RequestOptions | unknown,
893+
maybeOptions?: RequestOptions
894+
): Promise<CallToolResult> {
895+
const options: RequestOptions | undefined =
896+
optionsOrSchema && typeof optionsOrSchema === 'object' && 'parse' in optionsOrSchema
897+
? maybeOptions
898+
: (optionsOrSchema as RequestOptions | undefined);
871899
// Guard: required-task tools need experimental API
872900
if (this.isToolTaskRequired(params.name)) {
873901
throw new ProtocolError(

packages/core/src/exports/public/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export type {
4949
ServerContext
5050
} from '../../shared/protocol.js';
5151
export { DEFAULT_REQUEST_TIMEOUT_MSEC } from '../../shared/protocol.js';
52+
export type { ZodLikeRequestSchema } from '../../util/compatSchema.js';
5253

5354
// Task manager types (NOT TaskManager class itself — internal)
5455
export type { RequestTaskStore, TaskContext, TaskManagerOptions, TaskRequestOptions } from '../../shared/taskManager.js';

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,6 @@ export * from './validators/fromJsonSchema.js';
4848
*/
4949

5050
// Core types only - implementations are exported via separate entry points
51+
export type { ZodLikeRequestSchema } from './util/compatSchema.js';
52+
export { extractMethodLiteral, isZodLikeSchema } from './util/compatSchema.js';
5153
export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './validators/types.js';

0 commit comments

Comments
 (0)