Skip to content

Commit 92a5ead

Browse files
feat(compat): add deprecated schema-argument overloads to Protocol/Client (C3/C6 in core)
- setRequestHandler/setNotificationHandler: accept Zod schema (extracts method literal, warns once) - request(): accept (req, ResultSchema, opts?) deprecated form (schema ignored) - callTool(): accept (params, ResultSchema, opts?) deprecated form - sendNotification(): non-overloaded alias for notification() (test-mock friendly) - Export AnySchema/SchemaOutput/ZodLikeRequestSchema from core/public
1 parent 0ccb4f1 commit 92a5ead

File tree

6 files changed

+165
-10
lines changed

6 files changed

+165
-10
lines changed

packages/client/src/client/client.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
AnySchema,
44
BaseContext,
55
CallToolRequest,
6+
CallToolResult,
67
ClientCapabilities,
78
ClientContext,
89
ClientNotification,
@@ -38,7 +39,8 @@ import type {
3839
TaskManagerOptions,
3940
Tool,
4041
Transport,
41-
UnsubscribeRequest
42+
UnsubscribeRequest,
43+
ZodLikeRequestSchema
4244
} from '@modelcontextprotocol/core';
4345
import {
4446
assertClientRequestTaskCapability,
@@ -49,12 +51,14 @@ import {
4951
CreateMessageResultSchema,
5052
CreateMessageResultWithToolsSchema,
5153
CreateTaskResultSchema,
54+
deprecate,
5255
ElicitRequestSchema,
5356
ElicitResultSchema,
5457
EmptyResultSchema,
5558
extractTaskManagerOptions,
5659
GetPromptResultSchema,
5760
InitializeResultSchema,
61+
isZodLikeSchema,
5862
LATEST_PROTOCOL_VERSION,
5963
ListChangedOptionsBaseSchema,
6064
ListPromptsResultSchema,
@@ -347,11 +351,19 @@ export class Client extends Protocol<ProtocolSpec, ClientContext> {
347351
paramsSchema: P,
348352
handler: (params: SchemaOutput<P>, ctx: ClientContext) => Result | Promise<Result>
349353
): void;
354+
/** @deprecated Pass the method string instead of a Zod schema. Removed in v3. */
355+
public override setRequestHandler<T extends ZodLikeRequestSchema>(
356+
requestSchema: T,
357+
handler: (request: ReturnType<T['parse']>, ctx: ClientContext) => Result | Promise<Result>
358+
): void;
350359
public override setRequestHandler(
351-
method: string,
360+
method: string | ZodLikeRequestSchema,
352361
schemaOrHandler: unknown,
353362
maybeHandler?: (params: unknown, ctx: ClientContext) => unknown
354363
): void {
364+
if (isZodLikeSchema(method)) {
365+
return super.setRequestHandler(method, schemaOrHandler as never);
366+
}
355367
if (maybeHandler !== undefined) {
356368
return super.setRequestHandler(
357369
method,
@@ -890,7 +902,24 @@ export class Client extends Protocol<ProtocolSpec, ClientContext> {
890902
* }
891903
* ```
892904
*/
893-
async callTool(params: CallToolRequest['params'], options?: RequestOptions) {
905+
async callTool(params: CallToolRequest['params'], options?: RequestOptions): Promise<CallToolResult>;
906+
/** @deprecated The result schema is resolved automatically. Removed in v3. */
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+
let options: RequestOptions | undefined;
914+
if (optionsOrSchema && typeof optionsOrSchema === 'object' && 'parse' in optionsOrSchema) {
915+
deprecate(
916+
'callTool(params, schema)',
917+
'Client.callTool(params, ResultSchema) is deprecated; the result schema is resolved automatically.'
918+
);
919+
options = maybeOptions;
920+
} else {
921+
options = optionsOrSchema as RequestOptions | undefined;
922+
}
894923
// Guard: required-task tools need experimental API
895924
if (this.isToolTaskRequired(params.name)) {
896925
throw new ProtocolError(

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ export { getDisplayName } from '../../shared/metadataUtils.js';
4141
// Role-agnostic Protocol class (concrete; Client/Server extend it). NOT mergeCapabilities.
4242
export type { McpSpec, ProtocolSpec, SpecNotifications, SpecRequests } from '../../shared/protocol.js';
4343
export { Protocol } from '../../shared/protocol.js';
44+
export type { ZodLikeRequestSchema } from '../../util/compatSchema.js';
4445
export { InMemoryTransport } from '../../util/inMemory.js';
46+
export type { AnySchema, SchemaOutput } from '../../util/schema.js';
4547
// Protocol types
4648
export type {
4749
BaseContext,

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,5 @@ export * from './validators/fromJsonSchema.js';
5050
// Core types only - implementations are exported via separate entry points
5151
export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './validators/types.js';
5252
export { deprecate } from './util/deprecate.js';
53+
export type { ZodLikeRequestSchema } from './util/compatSchema.js';
54+
export { extractMethodLiteral, isZodLikeSchema } from './util/compatSchema.js';

packages/core/src/shared/protocol.ts

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ import {
4444
ProtocolErrorCode,
4545
SUPPORTED_PROTOCOL_VERSIONS
4646
} from '../types/index.js';
47+
import type { ZodLikeRequestSchema } from '../util/compatSchema.js';
48+
import { extractMethodLiteral, isZodLikeSchema } from '../util/compatSchema.js';
49+
import { deprecate } from '../util/deprecate.js';
4750
import type { AnySchema, SchemaOutput } from '../util/schema.js';
4851
import { parseSchema } from '../util/schema.js';
4952
import type { StandardSchemaV1 } from '../util/standardSchema.js';
@@ -849,23 +852,48 @@ export class Protocol<S extends ProtocolSpec = ProtocolSpec, ContextT extends Ba
849852
resultSchema: R,
850853
options?: RequestOptions
851854
): Promise<SchemaOutput<R>>;
855+
/** @deprecated The result schema is resolved automatically from the method name. Removed in v3. */
856+
request<M extends RequestMethod>(
857+
request: { method: M; params?: Record<string, unknown> },
858+
resultSchema: ZodLikeRequestSchema | AnySchema,
859+
options?: RequestOptions
860+
): Promise<ResultTypeMap[M]>;
852861
request(
853862
requestOrMethod: { method: RequestMethod; params?: Record<string, unknown> } | string,
854-
optionsOrParams?: RequestOptions | Record<string, unknown>,
855-
resultSchema?: AnySchema,
863+
optionsOrParams?: RequestOptions | Record<string, unknown> | AnySchema | ZodLikeRequestSchema,
864+
resultSchema?: AnySchema | RequestOptions,
856865
options?: RequestOptions
857866
): Promise<unknown> {
858867
if (typeof requestOrMethod === 'string') {
859868
return this._requestWithSchema(
860869
{ method: requestOrMethod, params: optionsOrParams as Record<string, unknown> | undefined } as Request,
861-
resultSchema!,
870+
resultSchema as AnySchema,
862871
options
863872
);
864873
}
874+
// v1-compat: request(reqObj, ResultSchema, opts?) — schema arg is ignored.
875+
if (isZodLikeSchema(optionsOrParams) || (optionsOrParams && '~standard' in (optionsOrParams as object))) {
876+
deprecate(
877+
'request(req, schema)',
878+
'Protocol.request(request, ResultSchema) is deprecated; the result schema is resolved automatically from the method name.'
879+
);
880+
const schema = getResultSchema(requestOrMethod.method);
881+
return this._requestWithSchema(requestOrMethod as Request, schema, resultSchema as RequestOptions | undefined);
882+
}
865883
const schema = getResultSchema(requestOrMethod.method);
866884
return this._requestWithSchema(requestOrMethod as Request, schema, optionsOrParams as RequestOptions | undefined);
867885
}
868886

887+
/**
888+
* Non-overloaded alias for {@linkcode notification} that always takes a
889+
* `Notification` object. Exists so that test code can replace
890+
* `client.notification` with a single-signature mock without TypeScript
891+
* complaining about overload-intersection assignability.
892+
*/
893+
sendNotification(notification: Notification, options?: NotificationOptions): Promise<void> {
894+
return this.notification(notification, options);
895+
}
896+
869897
/**
870898
* Sends a request and waits for a response, using the provided schema for validation.
871899
*
@@ -1134,11 +1162,30 @@ export class Protocol<S extends ProtocolSpec = ProtocolSpec, ContextT extends Ba
11341162
paramsSchema: P,
11351163
handler: (params: SchemaOutput<P>, ctx: ContextT) => Result | Promise<Result>
11361164
): void;
1165+
/** @deprecated Pass the method string instead of a Zod schema. Removed in v3. */
1166+
setRequestHandler<T extends ZodLikeRequestSchema>(
1167+
requestSchema: T,
1168+
handler: (request: ReturnType<T['parse']>, ctx: ContextT) => Result | Promise<Result>
1169+
): void;
11371170
setRequestHandler(
1138-
method: string,
1171+
method: string | ZodLikeRequestSchema,
11391172
schemaOrHandler: AnySchema | ((request: Request, ctx: ContextT) => Result | Promise<Result>),
11401173
maybeHandler?: (params: unknown, ctx: ContextT) => unknown
11411174
): void {
1175+
if (isZodLikeSchema(method)) {
1176+
deprecate(
1177+
'setRequestHandler(schema)',
1178+
"setRequestHandler(ZodSchema, handler) is deprecated. Pass the method string, e.g. setRequestHandler('tools/call', handler)."
1179+
);
1180+
const requestSchema = method;
1181+
const methodStr = extractMethodLiteral(requestSchema);
1182+
const handler = schemaOrHandler as (request: unknown, ctx: ContextT) => Result | Promise<Result>;
1183+
this.assertRequestHandlerCapability(methodStr);
1184+
this._requestHandlers.set(methodStr, (request, ctx) =>
1185+
Promise.resolve(handler(requestSchema.parse(request), ctx))
1186+
);
1187+
return;
1188+
}
11421189
this.assertRequestHandlerCapability(method);
11431190

11441191
if (maybeHandler === undefined) {
@@ -1202,11 +1249,27 @@ export class Protocol<S extends ProtocolSpec = ProtocolSpec, ContextT extends Ba
12021249
paramsSchema: P,
12031250
handler: (params: SchemaOutput<P>) => void | Promise<void>
12041251
): void;
1252+
/** @deprecated Pass the method string instead of a Zod schema. Removed in v3. */
1253+
setNotificationHandler<T extends ZodLikeRequestSchema>(
1254+
notificationSchema: T,
1255+
handler: (notification: ReturnType<T['parse']>) => void | Promise<void>
1256+
): void;
12051257
setNotificationHandler(
1206-
method: string,
1258+
method: string | ZodLikeRequestSchema,
12071259
schemaOrHandler: AnySchema | ((notification: Notification) => void | Promise<void>),
12081260
maybeHandler?: (params: unknown) => void | Promise<void>
12091261
): void {
1262+
if (isZodLikeSchema(method)) {
1263+
deprecate(
1264+
'setNotificationHandler(schema)',
1265+
"setNotificationHandler(ZodSchema, handler) is deprecated. Pass the method string, e.g. setNotificationHandler('notifications/initialized', handler)."
1266+
);
1267+
const notificationSchema = method;
1268+
const methodStr = extractMethodLiteral(notificationSchema);
1269+
const handler = schemaOrHandler as (notification: unknown) => void | Promise<void>;
1270+
this._notificationHandlers.set(methodStr, n => Promise.resolve(handler(notificationSchema.parse(n))));
1271+
return;
1272+
}
12101273
if (maybeHandler === undefined) {
12111274
const handler = schemaOrHandler as (notification: Notification) => void | Promise<void>;
12121275
const schema = getNotificationSchema(method as NotificationMethod);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* v1-compat helpers for the deprecated schema-argument forms of
3+
* `setRequestHandler` / `setNotificationHandler` / `request`.
4+
*
5+
* v1 accepted a Zod object whose `.shape.method` is `z.literal('<method>')`.
6+
* v2 takes the method string directly. These helpers detect the v1 form and
7+
* extract the literal so the deprecated overloads can forward to the v2 path.
8+
*
9+
* @internal
10+
*/
11+
12+
/**
13+
* Minimal structural type for a Zod object schema. The `method` literal is
14+
* checked at runtime by {@link extractMethodLiteral}; the type-level constraint
15+
* is intentionally loose because zod v4's `ZodLiteral` doesn't surface `.value`
16+
* in its declared type (only at runtime).
17+
*/
18+
export interface ZodLikeRequestSchema {
19+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20+
shape: any;
21+
parse(input: unknown): unknown;
22+
}
23+
24+
/** True if `arg` looks like a Zod object schema (has `.shape` and `.parse`). */
25+
export function isZodLikeSchema(arg: unknown): arg is ZodLikeRequestSchema {
26+
return (
27+
typeof arg === 'object' &&
28+
arg !== null &&
29+
'shape' in arg &&
30+
typeof (arg as { parse?: unknown }).parse === 'function'
31+
);
32+
}
33+
34+
/**
35+
* Extracts the string value from a Zod-like schema's `shape.method` literal.
36+
* Throws if no string `method` literal is present.
37+
*/
38+
export function extractMethodLiteral(schema: ZodLikeRequestSchema): string {
39+
const methodField = (schema.shape as Record<string, unknown> | undefined)?.method as
40+
| { value?: unknown; def?: { values?: unknown[] } }
41+
| undefined;
42+
const value = methodField?.value ?? methodField?.def?.values?.[0];
43+
if (typeof value !== 'string') {
44+
throw new TypeError(
45+
'v1-compat: schema passed to setRequestHandler/setNotificationHandler is missing a string `method` literal'
46+
);
47+
}
48+
return value;
49+
}

packages/server/src/server/server.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ import type {
3636
ServerResult,
3737
TaskManagerOptions,
3838
ToolResultContent,
39-
ToolUseContent
39+
ToolUseContent,
40+
ZodLikeRequestSchema
4041
} from '@modelcontextprotocol/core';
4142
import {
4243
assertClientRequestTaskCapability,
@@ -48,6 +49,7 @@ import {
4849
CreateTaskResultSchema,
4950
ElicitResultSchema,
5051
EmptyResultSchema,
52+
isZodLikeSchema,
5153
extractTaskManagerOptions,
5254
LATEST_PROTOCOL_VERSION,
5355
ListRootsResultSchema,
@@ -236,11 +238,19 @@ export class Server extends Protocol<ProtocolSpec, ServerContext> {
236238
paramsSchema: P,
237239
handler: (params: SchemaOutput<P>, ctx: ServerContext) => Result | Promise<Result>
238240
): void;
241+
/** @deprecated Pass the method string instead of a Zod schema. Removed in v3. */
242+
public override setRequestHandler<T extends ZodLikeRequestSchema>(
243+
requestSchema: T,
244+
handler: (request: ReturnType<T['parse']>, ctx: ServerContext) => Result | Promise<Result>
245+
): void;
239246
public override setRequestHandler(
240-
method: string,
247+
method: string | ZodLikeRequestSchema,
241248
schemaOrHandler: unknown,
242249
maybeHandler?: (params: unknown, ctx: ServerContext) => unknown
243250
): void {
251+
if (isZodLikeSchema(method)) {
252+
return super.setRequestHandler(method, schemaOrHandler as never);
253+
}
244254
if (maybeHandler !== undefined) {
245255
return super.setRequestHandler(
246256
method,

0 commit comments

Comments
 (0)