Skip to content

Commit 9b606ab

Browse files
feat(compat): accept raw Zod shape in registerTool/registerPrompt schemas
v1 allowed passing a raw shape like `{ field: z.string() }` directly as inputSchema/outputSchema/argsSchema. v2 expects a Standard Schema (e.g. `z.object({...})`). This restores the v1 shorthand as a deprecated overload: - core: add isZodRawShape() guard + normalizeRawShapeSchema() that auto-wraps raw shapes in z.object() and emits a one-time deprecation warning via deprecate('raw-shape', ...). - server: convert registerTool/registerPrompt to overload sets with a @deprecated ZodRawShape-accepting signature so v1 callers typecheck unchanged. Implementation pipes schemas through normalizeRawShapeSchema. - server: add ZodRawShape / LegacyToolCallback / LegacyPromptCallback deprecated type aliases for the legacy callback inference. The shim warns once per process and will be removed in v3.
1 parent a33c305 commit 9b606ab

File tree

5 files changed

+176
-4
lines changed

5 files changed

+176
-4
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modelcontextprotocol/core': patch
3+
'@modelcontextprotocol/server': patch
4+
---
5+
6+
v1-compat: `registerTool`/`registerPrompt` accept a raw Zod shape (`{ field: z.string() }`) for `inputSchema`/`outputSchema`/`argsSchema` again. The shape is auto-wrapped in `z.object()` and a one-time deprecation warning is emitted. Wrap in `z.object({...})` to silence the warning; the shim will be removed in v3.

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,4 @@ export * from './validators/fromJsonSchema.js';
4949

5050
// Core types only - implementations are exported via separate entry points
5151
export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './validators/types.js';
52-
export { deprecate } from './util/deprecate.js';
52+
export { deprecate, _resetDeprecationWarnings } from './util/deprecate.js';

packages/core/src/util/standardSchema.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
/* eslint-disable @typescript-eslint/no-namespace */
88

9+
import * as z from 'zod/v4';
10+
import { deprecate } from './deprecate.js';
11+
912
// Standard Schema interfaces — vendored from https://standardschema.dev (spec v1, Jan 2025)
1013

1114
export interface StandardTypedV1<Input = unknown, Output = Input> {
@@ -136,6 +139,42 @@ export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSch
136139
return isStandardJSONSchema(schema) && isStandardSchema(schema);
137140
}
138141

142+
/**
143+
* Detects a "raw shape" — a plain object whose values are Zod (or other
144+
* Standard Schema) field schemas, e.g. `{ name: z.string() }`. v1 accepted
145+
* this shorthand directly; v2 expects `z.object({...})`. This guard powers
146+
* the deprecated auto-wrap shim in {@linkcode normalizeRawShapeSchema}.
147+
*
148+
* @internal
149+
*/
150+
export function isZodRawShape(obj: unknown): obj is Record<string, StandardSchemaV1> {
151+
if (typeof obj !== 'object' || obj === null) return false;
152+
if (isStandardSchema(obj)) return false;
153+
const values = Object.values(obj);
154+
return values.length > 0 && values.every(v => isStandardSchema(v) || (typeof v === 'object' && v !== null && '_def' in v));
155+
}
156+
157+
/**
158+
* v1-compat shim: if a tool/prompt was registered with the v1 raw-shape
159+
* shorthand `{ field: z.string() }`, wrap it in `z.object()` so the rest of
160+
* the pipeline sees a {@linkcode StandardSchemaWithJSON}. Passes through
161+
* already-wrapped schemas unchanged. Warns once per process.
162+
*
163+
* @internal
164+
*/
165+
export function normalizeRawShapeSchema(schema: unknown): StandardSchemaWithJSON | undefined {
166+
if (schema === undefined) return undefined;
167+
if (isZodRawShape(schema)) {
168+
deprecate(
169+
'raw-shape',
170+
'Passing a raw shape like `{ field: z.string() }` as inputSchema/argsSchema is deprecated. ' +
171+
'Wrap it in z.object({...}) (or your schema library equivalent). Auto-wrapping for now; this shim will be removed in v3.'
172+
);
173+
return z.object(schema as z.core.$ZodLooseShape) as StandardSchemaWithJSON;
174+
}
175+
return schema as StandardSchemaWithJSON;
176+
}
177+
139178
// JSON Schema conversion
140179

141180
/**

packages/server/src/server/mcp.ts

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import type {
3030
import {
3131
assertCompleteRequestPrompt,
3232
assertCompleteRequestResourceTemplate,
33+
normalizeRawShapeSchema,
3334
promptArgumentsFromStandardSchema,
3435
ProtocolError,
3536
ProtocolErrorCode,
@@ -873,6 +874,31 @@ export class McpServer {
873874
_meta?: Record<string, unknown>;
874875
},
875876
cb: ToolCallback<InputArgs>
877+
): RegisteredTool;
878+
/** @deprecated Pass a Standard Schema (e.g. wrap your raw shape in `z.object({...})`). Raw-shape inputs are auto-wrapped at runtime. Removed in v3. */
879+
registerTool<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape | StandardSchemaWithJSON | undefined = undefined>(
880+
name: string,
881+
config: {
882+
title?: string;
883+
description?: string;
884+
inputSchema?: InputArgs;
885+
outputSchema?: OutputArgs;
886+
annotations?: ToolAnnotations;
887+
_meta?: Record<string, unknown>;
888+
},
889+
cb: LegacyToolCallback<InputArgs>
890+
): RegisteredTool;
891+
registerTool(
892+
name: string,
893+
config: {
894+
title?: string;
895+
description?: string;
896+
inputSchema?: StandardSchemaWithJSON | ZodRawShape;
897+
outputSchema?: StandardSchemaWithJSON | ZodRawShape;
898+
annotations?: ToolAnnotations;
899+
_meta?: Record<string, unknown>;
900+
},
901+
cb: ToolCallback<StandardSchemaWithJSON | undefined> | LegacyToolCallback<ZodRawShape>
876902
): RegisteredTool {
877903
if (this._registeredTools[name]) {
878904
throw new Error(`Tool ${name} is already registered`);
@@ -884,8 +910,8 @@ export class McpServer {
884910
name,
885911
title,
886912
description,
887-
inputSchema,
888-
outputSchema,
913+
normalizeRawShapeSchema(inputSchema),
914+
normalizeRawShapeSchema(outputSchema),
889915
annotations,
890916
{ taskSupport: 'forbidden' },
891917
_meta,
@@ -928,6 +954,27 @@ export class McpServer {
928954
_meta?: Record<string, unknown>;
929955
},
930956
cb: PromptCallback<Args>
957+
): RegisteredPrompt;
958+
/** @deprecated Pass a Standard Schema (e.g. wrap your raw shape in `z.object({...})`). Raw-shape inputs are auto-wrapped at runtime. Removed in v3. */
959+
registerPrompt<Args extends ZodRawShape>(
960+
name: string,
961+
config: {
962+
title?: string;
963+
description?: string;
964+
argsSchema?: Args;
965+
_meta?: Record<string, unknown>;
966+
},
967+
cb: LegacyPromptCallback<Args>
968+
): RegisteredPrompt;
969+
registerPrompt(
970+
name: string,
971+
config: {
972+
title?: string;
973+
description?: string;
974+
argsSchema?: StandardSchemaWithJSON | ZodRawShape;
975+
_meta?: Record<string, unknown>;
976+
},
977+
cb: PromptCallback<StandardSchemaWithJSON> | LegacyPromptCallback<ZodRawShape>
931978
): RegisteredPrompt {
932979
if (this._registeredPrompts[name]) {
933980
throw new Error(`Prompt ${name} is already registered`);
@@ -939,7 +986,7 @@ export class McpServer {
939986
name,
940987
title,
941988
description,
942-
argsSchema,
989+
normalizeRawShapeSchema(argsSchema),
943990
cb as PromptCallback<StandardSchemaWithJSON | undefined>,
944991
_meta
945992
);
@@ -1062,6 +1109,25 @@ export class ResourceTemplate {
10621109
}
10631110
}
10641111

1112+
/**
1113+
* v1 raw-shape: a plain record of field schemas, e.g. `{ name: z.string() }`.
1114+
* @deprecated Wrap in `z.object({...})` and pass to `registerTool`. Removed in v3.
1115+
*/
1116+
export type ZodRawShape = Record<string, StandardSchemaWithJSON>;
1117+
1118+
/** @deprecated Removed in v3. */
1119+
export type InferRawShape<S extends ZodRawShape> = { [K in keyof S]: StandardSchemaWithJSON.InferOutput<S[K]> };
1120+
1121+
/** @deprecated Use {@linkcode ToolCallback} with `registerTool`. Removed in v3. */
1122+
export type LegacyToolCallback<Args extends ZodRawShape | undefined> = Args extends ZodRawShape
1123+
? (args: InferRawShape<Args>, ctx: ServerContext) => CallToolResult | Promise<CallToolResult>
1124+
: (ctx: ServerContext) => CallToolResult | Promise<CallToolResult>;
1125+
1126+
/** @deprecated Use {@linkcode PromptCallback} with `registerPrompt`. Removed in v3. */
1127+
export type LegacyPromptCallback<Args extends ZodRawShape | undefined> = Args extends ZodRawShape
1128+
? (args: InferRawShape<Args>, ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>
1129+
: (ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>;
1130+
10651131
export type BaseToolCallback<
10661132
SendResultT extends Result,
10671133
Ctx extends ServerContext,
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { _resetDeprecationWarnings, isStandardSchema } from '@modelcontextprotocol/core';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
import * as z from 'zod/v4';
4+
import { McpServer } from '../../src/index.js';
5+
6+
describe('v1 compat: registerTool/registerPrompt raw-shape auto-wrap', () => {
7+
beforeEach(() => {
8+
_resetDeprecationWarnings();
9+
});
10+
11+
it('registerTool accepts a raw shape for inputSchema, auto-wraps, and warns once', () => {
12+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
13+
const server = new McpServer({ name: 't', version: '1.0.0' });
14+
15+
server.registerTool('a', { inputSchema: { x: z.number() } }, async ({ x }) => ({
16+
content: [{ type: 'text' as const, text: String(x) }]
17+
}));
18+
server.registerTool('b', { inputSchema: { y: z.number() } }, async ({ y }) => ({
19+
content: [{ type: 'text' as const, text: String(y) }]
20+
}));
21+
22+
const tools = (server as unknown as { _registeredTools: Record<string, { inputSchema?: unknown }> })._registeredTools;
23+
expect(Object.keys(tools)).toEqual(['a', 'b']);
24+
// raw shape was wrapped into a Standard Schema (z.object)
25+
expect(isStandardSchema(tools['a']?.inputSchema)).toBe(true);
26+
27+
const rawShapeWarns = warn.mock.calls.filter(c => String(c[0]).includes('raw shape'));
28+
expect(rawShapeWarns.length).toBe(1);
29+
warn.mockRestore();
30+
});
31+
32+
it('registerTool with z.object() inputSchema does not warn', () => {
33+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
34+
const server = new McpServer({ name: 't', version: '1.0.0' });
35+
36+
server.registerTool('c', { inputSchema: z.object({ x: z.number() }) }, async ({ x }) => ({
37+
content: [{ type: 'text' as const, text: String(x) }]
38+
}));
39+
40+
const rawShapeWarns = warn.mock.calls.filter(c => String(c[0]).includes('raw shape'));
41+
expect(rawShapeWarns.length).toBe(0);
42+
warn.mockRestore();
43+
});
44+
45+
it('registerPrompt accepts a raw shape for argsSchema and warns', () => {
46+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
47+
const server = new McpServer({ name: 't', version: '1.0.0' });
48+
49+
server.registerPrompt('p', { argsSchema: { topic: z.string() } }, async ({ topic }) => ({
50+
messages: [{ role: 'user' as const, content: { type: 'text' as const, text: topic } }]
51+
}));
52+
53+
const prompts = (server as unknown as { _registeredPrompts: Record<string, { argsSchema?: unknown }> })._registeredPrompts;
54+
expect(Object.keys(prompts)).toContain('p');
55+
expect(isStandardSchema(prompts['p']?.argsSchema)).toBe(true);
56+
57+
const rawShapeWarns = warn.mock.calls.filter(c => String(c[0]).includes('raw shape'));
58+
expect(rawShapeWarns.length).toBe(1);
59+
warn.mockRestore();
60+
});
61+
});

0 commit comments

Comments
 (0)