Skip to content

Commit dbca75a

Browse files
feat(core): custom-method overloads + deprecated v1 schema-arg shims + ProtocolSpec generic
Adds to Protocol (inherited by Client/Server): - 3-arg setRequestHandler/setNotificationHandler(method: string, paramsSchema, handler) for custom (non-spec) methods - @deprecated setRequestHandler/setNotificationHandler(ZodSchema, handler) v1-compat overloads - @deprecated request(req, ResultSchema)/callTool(params, ResultSchema) v1-compat overloads - Method-keyed request() return type: request({method:M}) returns ResultTypeMap[M] - ProtocolSpec generic as 2nd param (Protocol<ContextT, S = McpSpec>) for typed custom vocabularies Protocol stays abstract; Client/Server are the concrete implementations. Also: migration docs + examples for custom-method overloads.
1 parent e6fbe37 commit dbca75a

20 files changed

Lines changed: 857 additions & 95 deletions

File tree

.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.

.changeset/protocol-concrete.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@modelcontextprotocol/client': minor
3+
'@modelcontextprotocol/server': minor
4+
---
5+
6+
Make `Protocol` concrete and exported. `setRequestHandler`/`setNotificationHandler`/`request`/`notification` gain a 3-arg `(method: string, paramsSchema, handler)` overload for non-standard methods alongside the spec-typed form. Optional `Protocol<S extends ProtocolSpec>` generic
7+
for declaring a typed method vocabulary. The five `assert*Capability` abstracts are now no-op virtuals (`Client`/`Server` override to enforce).

docs/migration-SKILL.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,21 @@ 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 — use the three-arg overloads of `setRequestHandler`/`setNotificationHandler`/`request`. `Protocol` is now concrete and exported, so MCP-dialect protocols can subclass it directly:
381+
382+
| v1 | v2 |
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)` | `this.request('vendor/x', params, ResultSchema)` |
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+
`notification()` keeps a single object-form signature so tests can replace it with a mock without TS overload-intersection errors. To send a custom notification, use `notification({ method: 'vendor/x', params })` (unchanged from v1).
391+
392+
The v1 schema's `.shape.params` becomes the `ParamsSchema` argument; the `method: z.literal('...')` value becomes the string argument.
393+
394+
380395
## 10. Request Handler Context Types
381396

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

docs/migration.md

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

385+
### Custom (non-standard) protocol methods
386+
387+
In v1, vendor-specific methods were registered the same way as spec methods (`setRequestHandler(zodSchemaWithMethodLiteral, handler)`), and `Protocol<SendRequestT, SendNotificationT, SendResultT>` widened the send-side type unions.
388+
389+
In v2, `Protocol` is concrete and exported. Its `setRequestHandler`, `setNotificationHandler`, `request` and `notification` each have a string-method overload that accepts any method name with a caller-supplied params/result schema:
390+
391+
**Before (v1):**
392+
393+
```typescript
394+
import { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';
395+
396+
class App extends Protocol<AppRequest, AppNotification, AppResult> {
397+
constructor() {
398+
super();
399+
this.setRequestHandler(SearchRequestSchema, req => ({ hits: [req.params.query] }));
400+
}
401+
search(query: string) {
402+
return this.request({ method: 'acme/search', params: { query } }, SearchResultSchema);
403+
}
404+
}
405+
```
406+
407+
**After (v2):**
408+
409+
```typescript
410+
import { Protocol, type ProtocolSpec } from '@modelcontextprotocol/client';
411+
412+
type AppSpec = {
413+
requests: { 'acme/search': { params: { query: string }; result: { hits: string[] } } };
414+
} satisfies ProtocolSpec;
415+
416+
class App extends Protocol<AppSpec> {
417+
constructor() {
418+
super();
419+
this.setRequestHandler('acme/search', SearchParams, params => ({ hits: [params.query] }));
420+
}
421+
search(query: string) {
422+
return this.request('acme/search', { query }, SearchResult);
423+
}
424+
}
425+
```
426+
427+
The `ProtocolSpec` type argument is optional — omit it for ad-hoc method strings without autocomplete. For a single vendor method on a stock `Client` or `Server`, call the three-arg overload directly: `server.setRequestHandler('acme/search', SearchParams, handler)`.
428+
429+
The five `assert*Capability` abstract methods are now no-op virtuals on `Protocol`, so subclasses no longer need to stub them.
430+
385431
### `Protocol.request()`, `ctx.mcpReq.send()`, and `Client.callTool()` no longer take a schema parameter
386432

387433
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

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: 3-arg `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('acme/search', { 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('acme/search', { 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());

0 commit comments

Comments
 (0)