Skip to content

Commit 3c18a2c

Browse files
feat(core): 3-arg setRequestHandler/setNotificationHandler with validator-agnostic paramsSchema
Adds the three-arg form: setRequestHandler('vendor/method', paramsSchema, (params, ctx) => …), where paramsSchema is any Standard Schema (Zod, Valibot, ArkType, etc.). Handler receives validated params; absent params normalize to {} (after stripping _meta). Same for setNotificationHandler. Ergonomic alternative to writing a full Zod request schema with method literal.
1 parent 568a3bf commit 3c18a2c

File tree

11 files changed

+270
-59
lines changed

11 files changed

+270
-59
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` gain a 3-arg `(method: string, paramsSchema, handler)` form for custom (non-spec) methods. `paramsSchema` is any Standard Schema (Zod, Valibot, ArkType, etc.); the handler receives validated `params`.

docs/migration-SKILL.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -377,14 +377,16 @@ 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 |
380+
**Custom (non-standard) methods** — vendor extensions or sub-protocols whose method strings are not in the MCP spec — work on `Client`/`Server` directly. The v1 Zod-schema forms continue to work; the three-arg `(method, paramsSchema, handler)` form is the alternative:
381+
382+
| v1 (still supported) | v2 alternative |
383+
| ------------------------------------------------------------ | ------------------------------------------------------------------------ |
384+
| `setRequestHandler(CustomReqSchema, (req, ctx) => ...)` | `setRequestHandler('vendor/method', ParamsSchema, (params, ctx) => ...)` |
385+
| `setNotificationHandler(CustomNotifSchema, n => ...)` | `setNotificationHandler('vendor/method', ParamsSchema, params => ...)` |
386+
| `this.request({ method: 'vendor/x', params }, ResultSchema)` | unchanged |
387+
| `this.notification({ method: 'vendor/x', params })` | unchanged |
388+
389+
For the three-arg form, the v1 schema's `.shape.params` becomes the `ParamsSchema` argument and the `method: z.literal('...')` value becomes the string argument.
388390

389391
## 10. Request Handler Context Types
390392

docs/migration.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,15 +384,19 @@ Common method string replacements:
384384

385385
### Custom (non-standard) protocol methods
386386

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.
387+
Vendor-specific methods are registered directly on `Client` or `Server`. The v1 form `setRequestHandler(zodSchemaWithMethodLiteral, handler)` continues to work; the three-arg `(methodString, paramsSchema, handler)` form is the v2 alternative. `request({ method, params }, ResultSchema)` and `notification({ method, params })` are unchanged from v1.
388388

389389
```typescript
390390
import { Server } from '@modelcontextprotocol/server';
391391

392392
const server = new Server({ name: 'app', version: '1.0.0' }, { capabilities: {} });
393393

394+
// v1 form (still supported):
394395
server.setRequestHandler(SearchRequestSchema, req => ({ hits: [req.params.query] }));
395396

397+
// v2 alternative — pass method string + params schema; handler receives validated params:
398+
server.setRequestHandler('acme/search', SearchParams, params => ({ hits: [params.query] }));
399+
396400
// Calling from a Client — unchanged from v1:
397401
const result = await client.request({ method: 'acme/search', params: { query: 'x' } }, SearchResult);
398402
```

examples/client/src/customMethodExample.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
* Calling vendor-specific (non-spec) JSON-RPC methods from a `Client`.
44
*
55
* - 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)`
6+
* - Send a custom notification: `client.notification({ method, params })` (unchanged from v1)
7+
* - Receive a custom notification: 3-arg `client.setNotificationHandler(method, paramsSchema, handler)`
8+
*
9+
* These overloads are on `Client` and `Server` directly — you do NOT need a raw
10+
* `Protocol` instance for custom methods.
811
*
912
* Pair with the server in examples/server/src/customMethodExample.ts.
1013
*/
@@ -13,16 +16,12 @@ import { Client, StdioClientTransport } from '@modelcontextprotocol/client';
1316
import { z } from 'zod';
1417

1518
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-
});
19+
const ProgressParams = z.object({ stage: z.string(), pct: z.number() });
2120

2221
const client = new Client({ name: 'custom-method-client', version: '1.0.0' }, { capabilities: {} });
2322

24-
client.setNotificationHandler(ProgressNotification, n => {
25-
console.log(`[client] progress: ${n.params.stage} ${n.params.pct}%`);
23+
client.setNotificationHandler('acme/searchProgress', ProgressParams, p => {
24+
console.log(`[client] progress: ${p.stage} ${p.pct}%`);
2625
});
2726

2827
await client.connect(new StdioClientTransport({ command: 'node', args: ['../server/dist/customMethodExample.js'] }));

examples/server/src/customMethodExample.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
/**
33
* Registering vendor-specific (non-spec) JSON-RPC methods on a `Server`.
44
*
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).
5+
* Custom methods use the 3-arg form of `setRequestHandler` / `setNotificationHandler`:
6+
* pass the method string, a params schema, and the handler. The same overload is
7+
* available on `Client` (for server→client custom methods) — you do NOT need a raw
8+
* `Protocol` instance for this.
89
*
910
* To call these from the client side, use:
1011
* await client.request({ method: 'acme/search', params: { query: 'widgets' } }, SearchResult)
@@ -15,28 +16,21 @@
1516
import { Server, StdioServerTransport } from '@modelcontextprotocol/server';
1617
import { z } from 'zod';
1718

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-
});
19+
const SearchParams = z.object({ query: z.string() });
20+
const TickParams = z.object({ n: z.number() });
2721

2822
const server = new Server({ name: 'custom-method-server', version: '1.0.0' }, { capabilities: {} });
2923

30-
server.setRequestHandler(SearchRequest, async (request, ctx) => {
31-
console.log('[server] acme/search query=' + request.params.query);
24+
server.setRequestHandler('acme/search', SearchParams, async (params, ctx) => {
25+
console.log('[server] acme/search query=' + params.query);
3226
await ctx.mcpReq.notify({ method: 'acme/searchProgress', params: { stage: 'start', pct: 0 } });
33-
const hits = [request.params.query, request.params.query + '-result'];
27+
const hits = [params.query, params.query + '-result'];
3428
await ctx.mcpReq.notify({ method: 'acme/searchProgress', params: { stage: 'done', pct: 100 } });
3529
return { hits };
3630
});
3731

38-
server.setNotificationHandler(TickNotification, n => {
39-
console.log('[server] acme/tick n=' + n.params.n);
32+
server.setNotificationHandler('acme/tick', TickParams, p => {
33+
console.log('[server] acme/tick n=' + p.n);
4034
});
4135

4236
await server.connect(new StdioServerTransport());

packages/client/src/client/client.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import type {
3232
Result,
3333
ResultTypeMap,
3434
ServerCapabilities,
35+
StandardSchemaV1,
3536
SubscribeRequest,
3637
TaskManagerOptions,
3738
Tool,
@@ -343,22 +344,34 @@ export class Client extends Protocol<ClientContext> {
343344
method: M,
344345
handler: (request: RequestTypeMap[M], ctx: ClientContext) => ResultTypeMap[M] | Promise<ResultTypeMap[M]>
345346
): void;
347+
public override setRequestHandler<P extends StandardSchemaV1>(
348+
method: string,
349+
paramsSchema: P,
350+
handler: (params: StandardSchemaV1.InferOutput<P>, ctx: ClientContext) => Result | Promise<Result>
351+
): void;
346352
/** For spec methods the method-string form is more concise; this overload is the supported call form for non-spec methods or when you want full-envelope validation. */
347353
public override setRequestHandler<T extends ZodLikeRequestSchema>(
348354
requestSchema: T,
349355
handler: (request: ReturnType<T['parse']>, ctx: ClientContext) => Result | Promise<Result>
350356
): void;
351-
public override setRequestHandler(methodOrSchema: string | ZodLikeRequestSchema, schemaHandler: unknown): void {
357+
public override setRequestHandler(
358+
methodOrSchema: string | ZodLikeRequestSchema,
359+
schemaOrHandler: unknown,
360+
maybeHandler?: (params: unknown, ctx: ClientContext) => unknown
361+
): void {
352362
let method: string;
353363
let handler: (request: Request, ctx: ClientContext) => ClientResult | Promise<ClientResult>;
354364
if (isZodLikeSchema(methodOrSchema)) {
355365
const schema = methodOrSchema;
356-
const userHandler = schemaHandler as (request: unknown, ctx: ClientContext) => Result | Promise<Result>;
366+
const userHandler = schemaOrHandler as (request: unknown, ctx: ClientContext) => Result | Promise<Result>;
357367
method = extractMethodLiteral(schema);
358368
handler = (req, ctx) => userHandler(schema.parse(req), ctx);
369+
} else if (maybeHandler === undefined) {
370+
method = methodOrSchema;
371+
handler = schemaOrHandler as (request: Request, ctx: ClientContext) => ClientResult | Promise<ClientResult>;
359372
} else {
360373
method = methodOrSchema;
361-
handler = schemaHandler as (request: Request, ctx: ClientContext) => ClientResult | Promise<ClientResult>;
374+
handler = this._wrapParamsSchemaHandler(method, schemaOrHandler as StandardSchemaV1, maybeHandler);
362375
}
363376
if (method === 'elicitation/create') {
364377
const wrappedHandler = async (request: Request, ctx: ClientContext): Promise<ClientResult> => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export { isTerminal } from '../../experimental/tasks/interfaces.js';
137137
export { InMemoryTaskMessageQueue, InMemoryTaskStore } from '../../experimental/tasks/stores/inMemory.js';
138138

139139
// Validator types and classes
140-
export type { StandardSchemaWithJSON } from '../../util/standardSchema.js';
140+
export type { StandardSchemaV1, StandardSchemaWithJSON } from '../../util/standardSchema.js';
141141
export { AjvJsonSchemaValidator } from '../../validators/ajvProvider.js';
142142
export type { CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider.js';
143143
// fromJsonSchema is intentionally NOT exported here — the server and client packages

0 commit comments

Comments
 (0)