Skip to content

Commit fec9cce

Browse files
feat(core): custom-method overloads on setRequestHandler/setNotificationHandler; method-keyed request() return; deprecated v1 schema-arg overloads; ProtocolSpec generic
1 parent 92a5ead commit fec9cce

File tree

12 files changed

+155
-215
lines changed

12 files changed

+155
-215
lines changed

docs/migration-SKILL.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,16 +377,18 @@ 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`/`notification`, which accept any method string with a caller-supplied params/result schema. `Protocol` is now concrete and exported, so MCP-dialect protocols can subclass it directly:
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:
381381

382382
| v1 | v2 |
383383
| ------------------------------------------------------------ | ------------------------------------------------------------------------------ |
384384
| `setRequestHandler(CustomReqSchema, (req, extra) => ...)` | `setRequestHandler('vendor/method', ParamsSchema, (params, ctx) => ...)` |
385385
| `setNotificationHandler(CustomNotifSchema, n => ...)` | `setNotificationHandler('vendor/method', ParamsSchema, params => ...)` |
386386
| `this.request({ method: 'vendor/x', params }, ResultSchema)` | `this.request('vendor/x', params, ResultSchema)` |
387-
| `this.notification({ method: 'vendor/x', params })` | `this.notification('vendor/x', params)` |
387+
| `this.notification({ method: 'vendor/x', params })` | unchanged |
388388
| `class X extends Protocol<Req, Notif, Res>` | `class X extends Client` (or `Server`), or compose a `Client` instance |
389389

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+
390392
The v1 schema's `.shape.params` becomes the `ParamsSchema` argument; the `method: z.literal('...')` value becomes the string argument.
391393

392394

Lines changed: 25 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,35 @@
11
#!/usr/bin/env node
22
/**
3-
* Demonstrates calling a vendor method via the three-arg `client.request(method, params, resultSchema)`
4-
* overload and registering a vendor notification handler with the three-arg `setNotificationHandler`
5-
* overload.
3+
* Calling vendor-specific (non-spec) JSON-RPC methods from a `Client`.
64
*
7-
* Pair with: examples/server/src/customMethodExample.ts (which contains the in-memory pair).
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.
813
*/
914

10-
import { Client, InMemoryTransport, Protocol } from '@modelcontextprotocol/client';
15+
import { Client, StdioClientTransport } from '@modelcontextprotocol/client';
1116
import { z } from 'zod';
1217

13-
const SearchParams = z.object({ query: z.string() });
1418
const SearchResult = z.object({ hits: z.array(z.string()) });
1519
const ProgressParams = z.object({ stage: z.string(), pct: z.number() });
1620

17-
async function main() {
18-
const peer = new Protocol();
19-
peer.setRequestHandler('acme/search', SearchParams, async p => {
20-
await peer.notification('acme/searchProgress', { stage: 'started', pct: 0 });
21-
await peer.notification('acme/searchProgress', { stage: 'done', pct: 100 });
22-
return { hits: [p.query, p.query + '-2'] };
23-
});
24-
// The bare Protocol must respond to MCP initialize so Client.connect() completes.
25-
peer.setRequestHandler('initialize', req => ({
26-
protocolVersion: req.params.protocolVersion,
27-
capabilities: {},
28-
serverInfo: { name: 'peer', version: '1.0.0' }
29-
}));
30-
31-
const client = new Client({ name: 'c', version: '1.0.0' }, { capabilities: {} });
32-
client.setNotificationHandler('acme/searchProgress', ProgressParams, p => {
33-
console.log(`[client] progress: ${p.stage} ${p.pct}%`);
34-
});
35-
36-
const [t1, t2] = InMemoryTransport.createLinkedPair();
37-
await peer.connect(t2);
38-
await client.connect(t1);
39-
40-
const r = await client.request('acme/search', { query: 'widgets' }, SearchResult);
41-
console.log('[client] hits=' + JSON.stringify(r.hits));
42-
43-
await client.close();
44-
await peer.close();
45-
}
46-
47-
await main();
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();
Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,33 @@
11
#!/usr/bin/env node
22
/**
3-
* Demonstrates registering vendor-specific JSON-RPC methods directly on a stock `Server`, and
4-
* calling them from a bare `Protocol` peer (which is role-agnostic and so can act as the client
5-
* side without depending on the client package).
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.
614
*/
715

8-
import { InMemoryTransport, Protocol, Server } from '@modelcontextprotocol/server';
16+
import { Server, StdioServerTransport } from '@modelcontextprotocol/server';
917
import { z } from 'zod';
1018

1119
const SearchParams = z.object({ query: z.string() });
12-
const SearchResult = z.object({ hits: z.array(z.string()) });
1320
const TickParams = z.object({ n: z.number() });
1421

15-
async function main() {
16-
const server = new Server({ name: 'custom-method-server', version: '1.0.0' }, { capabilities: {} });
22+
const server = new Server({ name: 'custom-method-server', version: '1.0.0' }, { capabilities: {} });
1723

18-
server.setRequestHandler('acme/search', SearchParams, params => {
19-
console.log('[server] acme/search query=' + params.query);
20-
return { hits: [params.query, params.query + '-result'] };
21-
});
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+
});
2228

23-
server.setNotificationHandler('acme/tick', TickParams, p => {
24-
console.log('[server] acme/tick n=' + p.n);
25-
});
29+
server.setNotificationHandler('acme/tick', TickParams, p => {
30+
console.log('[server] acme/tick n=' + p.n);
31+
});
2632

27-
const peer = new Protocol();
28-
29-
const [peerTransport, serverTransport] = InMemoryTransport.createLinkedPair();
30-
await server.connect(serverTransport);
31-
await peer.connect(peerTransport);
32-
await peer.request({
33-
method: 'initialize',
34-
params: { protocolVersion: '2025-11-25', clientInfo: { name: 'peer', version: '1.0.0' }, capabilities: {} }
35-
});
36-
37-
const r = await peer.request('acme/search', { query: 'widgets' }, SearchResult);
38-
console.log('[peer] hits=' + JSON.stringify(r.hits));
39-
40-
await peer.notification('acme/tick', { n: 1 });
41-
await peer.notification('acme/tick', { n: 2 });
42-
43-
await peer.close();
44-
await server.close();
45-
}
46-
47-
await main();
33+
await server.connect(new StdioServerTransport());

examples/server/src/customMethodExtAppsExample.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ class App extends Protocol<AppSpec> {
7171
}
7272

7373
sendLog(level: string, data: unknown) {
74-
return this.notification('notifications/message', { level, data });
74+
return this.notification({ method: 'notifications/message', params: { level, data } });
7575
}
7676

7777
notifySize(width: number, height: number) {
78-
return this.notification('ui/size-changed', { width, height });
78+
return this.notification({ method: 'ui/size-changed', params: { width, height } });
7979
}
8080
}
8181

@@ -103,7 +103,7 @@ class Host extends Protocol<AppSpec> {
103103
}
104104

105105
notifyToolResult(toolName: string, text: string) {
106-
return this.notification('ui/tool-result', { toolName, text });
106+
return this.notification({ method: 'ui/tool-result', params: { toolName, text } });
107107
}
108108
}
109109

packages/client/src/client/client.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import type {
2525
MessageExtraInfo,
2626
NotificationMethod,
2727
ProtocolOptions,
28-
ProtocolSpec,
2928
ReadResourceRequest,
3029
Request,
3130
RequestMethod,
@@ -210,7 +209,7 @@ export type ClientOptions = ProtocolOptions & {
210209
* The client will automatically begin the initialization flow with the server when {@linkcode connect} is called.
211210
*
212211
*/
213-
export class Client extends Protocol<ProtocolSpec, ClientContext> {
212+
export class Client extends Protocol<ClientContext> {
214213
private _serverCapabilities?: ServerCapabilities;
215214
private _serverVersion?: Implementation;
216215
private _negotiatedProtocolVersion?: string;
@@ -613,7 +612,7 @@ export class Client extends Protocol<ProtocolSpec, ClientContext> {
613612
return this._instructions;
614613
}
615614

616-
protected override assertCapabilityForMethod(method: RequestMethod): void {
615+
protected assertCapabilityForMethod(method: RequestMethod): void {
617616
switch (method as ClientRequest['method']) {
618617
case 'logging/setLevel': {
619618
if (!this._serverCapabilities?.logging) {
@@ -676,7 +675,7 @@ export class Client extends Protocol<ProtocolSpec, ClientContext> {
676675
}
677676
}
678677

679-
protected override assertNotificationCapability(method: NotificationMethod): void {
678+
protected assertNotificationCapability(method: NotificationMethod): void {
680679
switch (method as ClientNotification['method']) {
681680
case 'notifications/roots/list_changed': {
682681
if (!this._capabilities.roots?.listChanged) {
@@ -705,7 +704,7 @@ export class Client extends Protocol<ProtocolSpec, ClientContext> {
705704
}
706705
}
707706

708-
protected override assertRequestHandlerCapability(method: string): void {
707+
protected assertRequestHandlerCapability(method: string): void {
709708
switch (method) {
710709
case 'sampling/createMessage': {
711710
if (!this._capabilities.sampling) {
@@ -744,11 +743,11 @@ export class Client extends Protocol<ProtocolSpec, ClientContext> {
744743
}
745744
}
746745

747-
protected override assertTaskCapability(method: string): void {
746+
protected assertTaskCapability(method: string): void {
748747
assertToolsCallTaskCapability(this._serverCapabilities?.tasks?.requests, method, 'Server');
749748
}
750749

751-
protected override assertTaskHandlerCapability(method: string): void {
750+
protected assertTaskHandlerCapability(method: string): void {
752751
assertClientRequestTaskCapability(this._capabilities?.tasks?.requests, method, 'Client');
753752
}
754753

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,8 @@ export { checkResourceAllowed, resourceUrlFromServerUrl } from '../../shared/aut
3838
// Metadata utilities
3939
export { getDisplayName } from '../../shared/metadataUtils.js';
4040

41-
// Role-agnostic Protocol class (concrete; Client/Server extend it). NOT mergeCapabilities.
41+
// Protocol-spec types for typed custom-method vocabularies. NOT the Protocol class itself or mergeCapabilities.
4242
export type { McpSpec, ProtocolSpec, SpecNotifications, SpecRequests } from '../../shared/protocol.js';
43-
export { Protocol } from '../../shared/protocol.js';
4443
export type { ZodLikeRequestSchema } from '../../util/compatSchema.js';
4544
export { InMemoryTransport } from '../../util/inMemory.js';
4645
export type { AnySchema, SchemaOutput } from '../../util/schema.js';

packages/core/src/index.ts

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

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

0 commit comments

Comments
 (0)