Skip to content

Commit 62e2044

Browse files
feat(core): custom-method overloads on setRequestHandler/setNotificationHandler/request; export Protocol + ProtocolSpec for typed custom vocabularies
Adds to Protocol (inherited by Client/Server): - 3-arg setRequestHandler/setNotificationHandler(method: string, paramsSchema, handler) for custom methods (validator-agnostic) - setRequestHandler/setNotificationHandler(ZodSchema, handler) — v1 form, first-class - request(req, resultSchema) — v1 form, first-class - Method-keyed request() return: request({method:M}) returns ResultTypeMap[M] - ProtocolSpec generic Protocol<ContextT, SpecT = McpSpec> with typed-vocabulary overloads for subclassers Protocol stays abstract; now exported (was reachable in v1 via deep imports).
1 parent 9ed62fe commit 62e2044

17 files changed

Lines changed: 762 additions & 61 deletions

File tree

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 non-spec methods, and accept the v1 `(ZodSchema, handler)` form as a first-class alternative. `request()` gains a string-form `(method, params, resultSchema)` overload and a method-keyed return type for the object form. `callTool(params, resultSchema?)` accepts the v1 schema arg (ignored). `Protocol` gains a reserved second generic `SpecT extends ProtocolSpec = McpSpec` for typed custom-method vocabularies; the class remains abstract.

.changeset/deprecate-helper.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/core': patch
3+
---
4+
5+
Add internal `deprecate()` warn-once helper for v1-compat shims.

docs/migration-SKILL.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,19 @@ 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. 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, extra) => ...)` | `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+
| `class X extends Protocol<Req, Notif, Res>` | `class X extends Client` (or `Server`), or compose a `Client` instance |
389+
390+
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.
391+
392+
380393
## 10. Request Handler Context Types
381394

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

docs/migration.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,30 @@ 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`. 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.
388+
389+
```typescript
390+
import { Server } from '@modelcontextprotocol/server';
391+
392+
const server = new Server({ name: 'app', version: '1.0.0' }, { capabilities: {} });
393+
394+
// v1 form (still supported):
395+
server.setRequestHandler(SearchRequestSchema, req => ({ hits: [req.params.query] }));
396+
397+
// v2 alternative — pass method string + params schema; handler receives validated params:
398+
server.setRequestHandler('acme/search', SearchParams, params => ({ hits: [params.query] }));
399+
400+
// Calling from a Client — unchanged from v1:
401+
const result = await client.request({ method: 'acme/search', params: { query: 'x' } }, SearchResult);
402+
```
403+
404+
If you previously subclassed `Protocol<SendRequestT, SendNotificationT, SendResultT>` for a custom dialect, extend `Client` or `Server` instead (or compose one). `Protocol` remains abstract and is not exported.
405+
406+
### `Protocol.request()`, `ctx.mcpReq.send()`, and `Client.callTool()` schema parameter is now optional
407+
408+
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
388409
like `CallToolResultSchema` or `ElicitResultSchema` when making requests.
389410

390411
**`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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 })` (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.
11+
*
12+
* Pair with the server in examples/server/src/customMethodExample.ts.
13+
*/
14+
15+
import { Client, StdioClientTransport } from '@modelcontextprotocol/client';
16+
import { z } from 'zod';
17+
18+
const SearchResult = z.object({ hits: z.array(z.string()) });
19+
const ProgressParams = z.object({ stage: z.string(), pct: z.number() });
20+
21+
const client = new Client({ name: 'custom-method-client', version: '1.0.0' }, { capabilities: {} });
22+
23+
client.setNotificationHandler('acme/searchProgress', ProgressParams, p => {
24+
console.log(`[client] progress: ${p.stage} ${p.pct}%`);
25+
});
26+
27+
await client.connect(new StdioClientTransport({ command: 'node', args: ['../server/dist/customMethodExample.js'] }));
28+
29+
const r = await client.request({ method: 'acme/search', params: { query: 'widgets' } }, SearchResult);
30+
console.log('[client] hits=' + JSON.stringify(r.hits));
31+
32+
await client.notification({ method: 'acme/tick', params: { n: 1 } });
33+
await client.notification({ method: 'acme/tick', params: { n: 2 } });
34+
35+
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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Registering vendor-specific (non-spec) JSON-RPC methods on a `Server`.
4+
*
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.
9+
*
10+
* To call these from the client side, use:
11+
* await client.request({ method: 'acme/search', params: { query: 'widgets' } }, SearchResult)
12+
* await client.notification({ method: 'acme/tick', params: { n: 1 } })
13+
* See examples/client/src/customMethodExample.ts.
14+
*/
15+
16+
import { Server, StdioServerTransport } from '@modelcontextprotocol/server';
17+
import { z } from 'zod';
18+
19+
const SearchParams = z.object({ query: z.string() });
20+
const TickParams = z.object({ n: z.number() });
21+
22+
const server = new Server({ name: 'custom-method-server', version: '1.0.0' }, { capabilities: {} });
23+
24+
server.setRequestHandler('acme/search', SearchParams, params => {
25+
console.log('[server] acme/search query=' + params.query);
26+
return { hits: [params.query, params.query + '-result'] };
27+
});
28+
29+
server.setNotificationHandler('acme/tick', TickParams, p => {
30+
console.log('[server] acme/tick n=' + p.n);
31+
});
32+
33+
await server.connect(new StdioServerTransport());

packages/client/src/client/client.ts

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/client/_shims';
22
import type {
3+
AnySchema,
34
BaseContext,
45
CallToolRequest,
6+
CallToolResult,
57
ClientCapabilities,
68
ClientContext,
79
ClientNotification,
@@ -24,16 +26,20 @@ import type {
2426
NotificationMethod,
2527
ProtocolOptions,
2628
ReadResourceRequest,
29+
Request,
2730
RequestMethod,
2831
RequestOptions,
2932
RequestTypeMap,
33+
Result,
3034
ResultTypeMap,
35+
SchemaOutput,
3136
ServerCapabilities,
3237
SubscribeRequest,
3338
TaskManagerOptions,
3439
Tool,
3540
Transport,
36-
UnsubscribeRequest
41+
UnsubscribeRequest,
42+
ZodLikeRequestSchema
3743
} from '@modelcontextprotocol/core';
3844
import {
3945
assertClientRequestTaskCapability,
@@ -50,6 +56,7 @@ import {
5056
extractTaskManagerOptions,
5157
GetPromptResultSchema,
5258
InitializeResultSchema,
59+
isZodLikeSchema,
5360
LATEST_PROTOCOL_VERSION,
5461
ListChangedOptionsBaseSchema,
5562
ListPromptsResultSchema,
@@ -336,9 +343,37 @@ export class Client extends Protocol<ClientContext> {
336343
public override setRequestHandler<M extends RequestMethod>(
337344
method: M,
338345
handler: (request: RequestTypeMap[M], ctx: ClientContext) => ResultTypeMap[M] | Promise<ResultTypeMap[M]>
346+
): void;
347+
public override setRequestHandler<P extends AnySchema>(
348+
method: string,
349+
paramsSchema: P,
350+
handler: (params: SchemaOutput<P>, ctx: ClientContext) => Result | Promise<Result>
351+
): void;
352+
public override setRequestHandler<T extends ZodLikeRequestSchema>(
353+
requestSchema: T,
354+
handler: (request: ReturnType<T['parse']>, ctx: ClientContext) => Result | Promise<Result>
355+
): void;
356+
public override setRequestHandler(
357+
method: string | ZodLikeRequestSchema,
358+
schemaOrHandler: unknown,
359+
maybeHandler?: (params: unknown, ctx: ClientContext) => unknown
339360
): void {
361+
if (isZodLikeSchema(method)) {
362+
return this._registerCompatRequestHandler(
363+
method,
364+
schemaOrHandler as (request: unknown, ctx: ClientContext) => Result | Promise<Result>
365+
);
366+
}
367+
if (maybeHandler !== undefined) {
368+
return super.setRequestHandler(
369+
method,
370+
schemaOrHandler as AnySchema,
371+
maybeHandler as (params: unknown, ctx: ClientContext) => Result | Promise<Result>
372+
);
373+
}
374+
const handler = schemaOrHandler as (request: Request, ctx: ClientContext) => ClientResult | Promise<ClientResult>;
340375
if (method === 'elicitation/create') {
341-
const wrappedHandler = async (request: RequestTypeMap[M], ctx: ClientContext): Promise<ClientResult> => {
376+
const wrappedHandler = async (request: Request, ctx: ClientContext): Promise<ClientResult> => {
342377
const validatedRequest = parseSchema(ElicitRequestSchema, request);
343378
if (!validatedRequest.success) {
344379
// Type guard: if success is false, error is guaranteed to exist
@@ -404,11 +439,11 @@ export class Client extends Protocol<ClientContext> {
404439
};
405440

406441
// Install the wrapped handler
407-
return super.setRequestHandler(method, wrappedHandler);
442+
return super.setRequestHandler(method as RequestMethod, wrappedHandler);
408443
}
409444

410445
if (method === 'sampling/createMessage') {
411-
const wrappedHandler = async (request: RequestTypeMap[M], ctx: ClientContext): Promise<ClientResult> => {
446+
const wrappedHandler = async (request: Request, ctx: ClientContext): Promise<ClientResult> => {
412447
const validatedRequest = parseSchema(CreateMessageRequestSchema, request);
413448
if (!validatedRequest.success) {
414449
const errorMessage =
@@ -447,11 +482,11 @@ export class Client extends Protocol<ClientContext> {
447482
};
448483

449484
// Install the wrapped handler
450-
return super.setRequestHandler(method, wrappedHandler);
485+
return super.setRequestHandler(method as RequestMethod, wrappedHandler);
451486
}
452487

453488
// Other handlers use default behavior
454-
return super.setRequestHandler(method, handler);
489+
return super.setRequestHandler(method as RequestMethod, handler);
455490
}
456491

457492
protected assertCapability(capability: keyof ServerCapabilities, method: string): void {
@@ -867,7 +902,18 @@ export class Client extends Protocol<ClientContext> {
867902
* }
868903
* ```
869904
*/
870-
async callTool(params: CallToolRequest['params'], options?: RequestOptions) {
905+
async callTool(params: CallToolRequest['params'], options?: RequestOptions): Promise<CallToolResult>;
906+
/** Result schema is resolved automatically; the second argument is accepted for v1 source compatibility and ignored. */
907+
async callTool(params: CallToolRequest['params'], resultSchema: unknown, options?: RequestOptions): Promise<CallToolResult>;
908+
async callTool(
909+
params: CallToolRequest['params'],
910+
optionsOrSchema?: RequestOptions | unknown,
911+
maybeOptions?: RequestOptions
912+
): Promise<CallToolResult> {
913+
const options: RequestOptions | undefined =
914+
optionsOrSchema && typeof optionsOrSchema === 'object' && 'parse' in optionsOrSchema
915+
? maybeOptions
916+
: (optionsOrSchema as RequestOptions | undefined);
871917
// Guard: required-task tools need experimental API
872918
if (this.isToolTaskRequired(params.name)) {
873919
throw new ProtocolError(
@@ -1011,7 +1057,7 @@ export class Client extends Protocol<ClientContext> {
10111057
options: ListChangedOptions<T>,
10121058
fetcher: () => Promise<T[]>
10131059
): void {
1014-
// Validate options using Zod schema (validates autoRefresh and debounceMs)
1060+
// Validate options (autoRefresh and debounceMs)
10151061
const parseResult = parseSchema(ListChangedOptionsBaseSchema, options);
10161062
if (!parseResult.success) {
10171063
throw new Error(`Invalid ${listType} listChanged options: ${parseResult.error.message}`);

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
* This module defines the stable, public-facing API surface. Client and server
55
* packages re-export from here so that end users only see supported symbols.
66
*
7-
* Internal utilities (Protocol class, stdio parsing, schema helpers, etc.)
8-
* remain available via the internal barrel (@modelcontextprotocol/core) for
9-
* use by client/server packages.
7+
* Internal utilities (mergeCapabilities, schema helpers, etc.) remain available
8+
* via the internal barrel (@modelcontextprotocol/core) for use by client/server packages.
109
*/
1110

1211
// Auth error classes
@@ -38,7 +37,13 @@ export { checkResourceAllowed, resourceUrlFromServerUrl } from '../../shared/aut
3837
// Metadata utilities
3938
export { getDisplayName } from '../../shared/metadataUtils.js';
4039

41-
// Protocol types (NOT the Protocol class itself or mergeCapabilities)
40+
// Protocol class (abstract — subclass for custom vocabularies) + ProtocolSpec types. NOT mergeCapabilities.
41+
export type { McpSpec, ProtocolSpec, SpecNotifications, SpecRequests } from '../../shared/protocol.js';
42+
export { Protocol } from '../../shared/protocol.js';
43+
export type { ZodLikeRequestSchema } from '../../util/compatSchema.js';
44+
export { InMemoryTransport } from '../../util/inMemory.js';
45+
export type { AnySchema, SchemaOutput } from '../../util/schema.js';
46+
// Protocol types
4247
export type {
4348
BaseContext,
4449
ClientContext,

0 commit comments

Comments
 (0)