Skip to content

Commit 5185e12

Browse files
Merge origin/fweinberger/v2-bc-register-rawshape into v2-bc-d1-base
2 parents 810e538 + 9b606ab commit 5185e12

4 files changed

Lines changed: 156 additions & 8 deletions

File tree

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/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: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
assertCompleteRequestResourceTemplate,
3333
deprecate,
3434
isStandardSchema,
35+
normalizeRawShapeSchema,
3536
promptArgumentsFromStandardSchema,
3637
ProtocolError,
3738
ProtocolErrorCode,
@@ -876,6 +877,31 @@ export class McpServer {
876877
_meta?: Record<string, unknown>;
877878
},
878879
cb: ToolCallback<InputArgs>
880+
): RegisteredTool;
881+
/** @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. */
882+
registerTool<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape | StandardSchemaWithJSON | undefined = undefined>(
883+
name: string,
884+
config: {
885+
title?: string;
886+
description?: string;
887+
inputSchema?: InputArgs;
888+
outputSchema?: OutputArgs;
889+
annotations?: ToolAnnotations;
890+
_meta?: Record<string, unknown>;
891+
},
892+
cb: LegacyToolCallback<InputArgs>
893+
): RegisteredTool;
894+
registerTool(
895+
name: string,
896+
config: {
897+
title?: string;
898+
description?: string;
899+
inputSchema?: StandardSchemaWithJSON | ZodRawShape;
900+
outputSchema?: StandardSchemaWithJSON | ZodRawShape;
901+
annotations?: ToolAnnotations;
902+
_meta?: Record<string, unknown>;
903+
},
904+
cb: ToolCallback<StandardSchemaWithJSON | undefined> | LegacyToolCallback<ZodRawShape>
879905
): RegisteredTool {
880906
if (this._registeredTools[name]) {
881907
throw new Error(`Tool ${name} is already registered`);
@@ -887,8 +913,8 @@ export class McpServer {
887913
name,
888914
title,
889915
description,
890-
inputSchema,
891-
outputSchema,
916+
normalizeRawShapeSchema(inputSchema),
917+
normalizeRawShapeSchema(outputSchema),
892918
annotations,
893919
{ taskSupport: 'forbidden' },
894920
_meta,
@@ -931,6 +957,27 @@ export class McpServer {
931957
_meta?: Record<string, unknown>;
932958
},
933959
cb: PromptCallback<Args>
960+
): RegisteredPrompt;
961+
/** @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. */
962+
registerPrompt<Args extends ZodRawShape>(
963+
name: string,
964+
config: {
965+
title?: string;
966+
description?: string;
967+
argsSchema?: Args;
968+
_meta?: Record<string, unknown>;
969+
},
970+
cb: LegacyPromptCallback<Args>
971+
): RegisteredPrompt;
972+
registerPrompt(
973+
name: string,
974+
config: {
975+
title?: string;
976+
description?: string;
977+
argsSchema?: StandardSchemaWithJSON | ZodRawShape;
978+
_meta?: Record<string, unknown>;
979+
},
980+
cb: PromptCallback<StandardSchemaWithJSON> | LegacyPromptCallback<ZodRawShape>
934981
): RegisteredPrompt {
935982
if (this._registeredPrompts[name]) {
936983
throw new Error(`Prompt ${name} is already registered`);
@@ -942,7 +989,7 @@ export class McpServer {
942989
name,
943990
title,
944991
description,
945-
argsSchema,
992+
normalizeRawShapeSchema(argsSchema),
946993
cb as PromptCallback<StandardSchemaWithJSON | undefined>,
947994
_meta
948995
);

packages/server/test/server/mcp.compat.test.ts

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/* eslint-disable @typescript-eslint/no-deprecated */
2-
import { _resetDeprecationWarnings } from '@modelcontextprotocol/core';
3-
import { z } from 'zod';
4-
import { McpServer, ResourceTemplate } from '../../src/server/mcp.js';
2+
import { _resetDeprecationWarnings, isStandardSchema } from '@modelcontextprotocol/core';
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
import * as z from 'zod/v4';
5+
import { McpServer } from '../../src/index.js';
6+
import { ResourceTemplate } from '../../src/server/mcp.js';
57

68
describe('McpServer v1-compat variadic shims', () => {
79
let warnSpy: ReturnType<typeof vi.spyOn>;
@@ -22,13 +24,11 @@ describe('McpServer v1-compat variadic shims', () => {
2224
server.tool('x', { a: z.string() }, ({ a }) => ({ content: [{ type: 'text', text: a }] }));
2325
server.tool('y', { b: z.number() }, ({ b }) => ({ content: [{ type: 'text', text: String(b) }] }));
2426

25-
// both registered
2627
// @ts-expect-error private access for test
2728
expect(server._registeredTools['x']).toBeDefined();
2829
// @ts-expect-error private access for test
2930
expect(server._registeredTools['y']).toBeDefined();
3031

31-
// deprecation warned exactly once for the .tool() shim
3232
const toolWarnings = warnSpy.mock.calls.filter((c: unknown[]) => String(c[0]).includes('McpServer.tool()'));
3333
expect(toolWarnings).toHaveLength(1);
3434
});
@@ -88,3 +88,59 @@ describe('McpServer v1-compat variadic shims', () => {
8888
});
8989
});
9090
});
91+
92+
describe('v1 compat: registerTool/registerPrompt raw-shape auto-wrap', () => {
93+
beforeEach(() => {
94+
_resetDeprecationWarnings();
95+
});
96+
97+
it('registerTool accepts a raw shape for inputSchema, auto-wraps, and warns once', () => {
98+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
99+
const server = new McpServer({ name: 't', version: '1.0.0' });
100+
101+
server.registerTool('a', { inputSchema: { x: z.number() } }, async ({ x }) => ({
102+
content: [{ type: 'text' as const, text: String(x) }]
103+
}));
104+
server.registerTool('b', { inputSchema: { y: z.number() } }, async ({ y }) => ({
105+
content: [{ type: 'text' as const, text: String(y) }]
106+
}));
107+
108+
const tools = (server as unknown as { _registeredTools: Record<string, { inputSchema?: unknown }> })._registeredTools;
109+
expect(Object.keys(tools)).toEqual(['a', 'b']);
110+
expect(isStandardSchema(tools['a']?.inputSchema)).toBe(true);
111+
112+
const rawShapeWarns = warn.mock.calls.filter(c => String(c[0]).includes('raw shape'));
113+
expect(rawShapeWarns.length).toBe(1);
114+
warn.mockRestore();
115+
});
116+
117+
it('registerTool with z.object() inputSchema does not warn', () => {
118+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
119+
const server = new McpServer({ name: 't', version: '1.0.0' });
120+
121+
server.registerTool('c', { inputSchema: z.object({ x: z.number() }) }, async ({ x }) => ({
122+
content: [{ type: 'text' as const, text: String(x) }]
123+
}));
124+
125+
const rawShapeWarns = warn.mock.calls.filter(c => String(c[0]).includes('raw shape'));
126+
expect(rawShapeWarns.length).toBe(0);
127+
warn.mockRestore();
128+
});
129+
130+
it('registerPrompt accepts a raw shape for argsSchema and warns', () => {
131+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
132+
const server = new McpServer({ name: 't', version: '1.0.0' });
133+
134+
server.registerPrompt('p', { argsSchema: { topic: z.string() } }, async ({ topic }) => ({
135+
messages: [{ role: 'user' as const, content: { type: 'text' as const, text: topic } }]
136+
}));
137+
138+
const prompts = (server as unknown as { _registeredPrompts: Record<string, { argsSchema?: unknown }> })._registeredPrompts;
139+
expect(Object.keys(prompts)).toContain('p');
140+
expect(isStandardSchema(prompts['p']?.argsSchema)).toBe(true);
141+
142+
const rawShapeWarns = warn.mock.calls.filter(c => String(c[0]).includes('raw shape'));
143+
expect(rawShapeWarns.length).toBe(1);
144+
warn.mockRestore();
145+
});
146+
});

0 commit comments

Comments
 (0)