diff --git a/.changeset/register-rawshape-compat.md b/.changeset/register-rawshape-compat.md
new file mode 100644
index 000000000..bca0d9057
--- /dev/null
+++ b/.changeset/register-rawshape-compat.md
@@ -0,0 +1,6 @@
+---
+'@modelcontextprotocol/core': patch
+'@modelcontextprotocol/server': patch
+---
+
+`registerTool`/`registerPrompt` accept a raw Zod shape (`{ field: z.string() }`) for `inputSchema`/`outputSchema`/`argsSchema` in addition to a wrapped Standard Schema. Raw shapes are auto-wrapped with `z.object()`. The raw-shape overloads are `@deprecated`; prefer wrapping with `z.object()`.
diff --git a/packages/core/src/util/standardSchema.ts b/packages/core/src/util/standardSchema.ts
index 9817dc39a..4228bb25a 100644
--- a/packages/core/src/util/standardSchema.ts
+++ b/packages/core/src/util/standardSchema.ts
@@ -6,6 +6,8 @@
/* eslint-disable @typescript-eslint/no-namespace */
+import * as z from 'zod/v4';
+
// Standard Schema interfaces — vendored from https://standardschema.dev (spec v1, Jan 2025)
export interface StandardTypedV1 {
@@ -136,6 +138,45 @@ export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSch
return isStandardJSONSchema(schema) && isStandardSchema(schema);
}
+function isZodSchema(v: unknown): v is z.ZodType {
+ if (typeof v !== 'object' || v === null) return false;
+ if ('_def' in v) return true;
+ return isStandardSchema(v) && (v as StandardSchemaV1)['~standard'].vendor === 'zod';
+}
+
+/**
+ * Detects a "raw shape" — a plain object whose values are Zod field schemas,
+ * e.g. `{ name: z.string() }`. Powers the auto-wrap in
+ * {@linkcode normalizeRawShapeSchema}, which wraps with `z.object()`, so only
+ * Zod values are supported.
+ *
+ * @internal
+ */
+export function isZodRawShape(obj: unknown): obj is Record {
+ if (typeof obj !== 'object' || obj === null) return false;
+ if (isStandardSchema(obj)) return false;
+ // [].every() is true, so an empty object is a valid raw shape (matches v1).
+ return Object.values(obj).every(v => isZodSchema(v));
+}
+
+/**
+ * Accepts either a {@linkcode StandardSchemaWithJSON} or a raw Zod shape
+ * `{ field: z.string() }` and returns a {@linkcode StandardSchemaWithJSON}.
+ * Raw shapes are wrapped with `z.object()` so the rest of the pipeline sees a
+ * uniform schema type; already-wrapped schemas pass through unchanged.
+ *
+ * @internal
+ */
+export function normalizeRawShapeSchema(
+ schema: StandardSchemaWithJSON | Record | undefined
+): StandardSchemaWithJSON | undefined {
+ if (schema === undefined) return undefined;
+ if (isZodRawShape(schema)) {
+ return z.object(schema) as StandardSchemaWithJSON;
+ }
+ return schema;
+}
+
// JSON Schema conversion
/**
diff --git a/packages/core/test/util/standardSchema.test.ts b/packages/core/test/util/standardSchema.test.ts
index 6c3de99d7..de081b975 100644
--- a/packages/core/test/util/standardSchema.test.ts
+++ b/packages/core/test/util/standardSchema.test.ts
@@ -1,6 +1,37 @@
import * as z from 'zod/v4';
-import { standardSchemaToJsonSchema } from '../../src/util/standardSchema.js';
+import { isZodRawShape, normalizeRawShapeSchema, standardSchemaToJsonSchema } from '../../src/util/standardSchema.js';
+
+describe('isZodRawShape', () => {
+ test('treats empty object as a raw shape (matches v1)', () => {
+ expect(isZodRawShape({})).toBe(true);
+ });
+ test('detects raw shape with zod fields', () => {
+ expect(isZodRawShape({ a: z.string() })).toBe(true);
+ });
+ test('rejects a Standard Schema instance', () => {
+ expect(isZodRawShape(z.object({ a: z.string() }))).toBe(false);
+ });
+ test('rejects a shape with non-Zod Standard Schema fields', () => {
+ const nonZod = { '~standard': { version: 1, vendor: 'arktype', validate: () => ({ value: 'x' }) } };
+ expect(isZodRawShape({ a: nonZod })).toBe(false);
+ });
+});
+
+describe('normalizeRawShapeSchema', () => {
+ test('wraps empty raw shape into z.object({})', () => {
+ const wrapped = normalizeRawShapeSchema({});
+ expect(wrapped).toBeDefined();
+ expect(standardSchemaToJsonSchema(wrapped!, 'input').type).toBe('object');
+ });
+ test('passes through an already-wrapped Standard Schema unchanged', () => {
+ const schema = z.object({ a: z.string() });
+ expect(normalizeRawShapeSchema(schema)).toBe(schema);
+ });
+ test('returns undefined for undefined input', () => {
+ expect(normalizeRawShapeSchema(undefined)).toBeUndefined();
+ });
+});
describe('standardSchemaToJsonSchema', () => {
test('emits type:object for plain z.object schemas', () => {
diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts
index 6c2699997..4caa125c4 100644
--- a/packages/server/src/server/mcp.ts
+++ b/packages/server/src/server/mcp.ts
@@ -30,6 +30,7 @@ import type {
import {
assertCompleteRequestPrompt,
assertCompleteRequestResourceTemplate,
+ normalizeRawShapeSchema,
promptArgumentsFromStandardSchema,
ProtocolError,
ProtocolErrorCode,
@@ -39,6 +40,7 @@ import {
validateStandardSchema
} from '@modelcontextprotocol/core';
+import type * as z from 'zod/v4';
import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js';
import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcpServer.js';
import { getCompleter, isCompletable } from './completable.js';
@@ -873,6 +875,31 @@ export class McpServer {
_meta?: Record;
},
cb: ToolCallback
+ ): RegisteredTool;
+ /** @deprecated Wrap with `z.object({...})` instead. Raw-shape form: `inputSchema`/`outputSchema` may be a plain `{ field: z.string() }` record; it is auto-wrapped with `z.object()`. */
+ registerTool(
+ name: string,
+ config: {
+ title?: string;
+ description?: string;
+ inputSchema?: InputArgs;
+ outputSchema?: OutputArgs;
+ annotations?: ToolAnnotations;
+ _meta?: Record;
+ },
+ cb: LegacyToolCallback
+ ): RegisteredTool;
+ registerTool(
+ name: string,
+ config: {
+ title?: string;
+ description?: string;
+ inputSchema?: StandardSchemaWithJSON | ZodRawShape;
+ outputSchema?: StandardSchemaWithJSON | ZodRawShape;
+ annotations?: ToolAnnotations;
+ _meta?: Record;
+ },
+ cb: ToolCallback | LegacyToolCallback
): RegisteredTool {
if (this._registeredTools[name]) {
throw new Error(`Tool ${name} is already registered`);
@@ -884,8 +911,8 @@ export class McpServer {
name,
title,
description,
- inputSchema,
- outputSchema,
+ normalizeRawShapeSchema(inputSchema),
+ normalizeRawShapeSchema(outputSchema),
annotations,
{ taskSupport: 'forbidden' },
_meta,
@@ -928,6 +955,27 @@ export class McpServer {
_meta?: Record;
},
cb: PromptCallback
+ ): RegisteredPrompt;
+ /** @deprecated Wrap with `z.object({...})` instead. Raw-shape form: `argsSchema` may be a plain `{ field: z.string() }` record; it is auto-wrapped with `z.object()`. */
+ registerPrompt(
+ name: string,
+ config: {
+ title?: string;
+ description?: string;
+ argsSchema?: Args;
+ _meta?: Record;
+ },
+ cb: LegacyPromptCallback
+ ): RegisteredPrompt;
+ registerPrompt(
+ name: string,
+ config: {
+ title?: string;
+ description?: string;
+ argsSchema?: StandardSchemaWithJSON | ZodRawShape;
+ _meta?: Record;
+ },
+ cb: PromptCallback | LegacyPromptCallback
): RegisteredPrompt {
if (this._registeredPrompts[name]) {
throw new Error(`Prompt ${name} is already registered`);
@@ -939,7 +987,7 @@ export class McpServer {
name,
title,
description,
- argsSchema,
+ normalizeRawShapeSchema(argsSchema),
cb as PromptCallback,
_meta
);
@@ -1062,6 +1110,26 @@ export class ResourceTemplate {
}
}
+/**
+ * A plain record of Zod field schemas, e.g. `{ name: z.string() }`. Accepted by
+ * `registerTool`/`registerPrompt` as a shorthand; auto-wrapped with `z.object()`.
+ * Zod schemas only — `z.object()` cannot wrap other Standard Schema libraries.
+ */
+export type ZodRawShape = Record;
+
+/** Infers the parsed-output type of a {@linkcode ZodRawShape}. */
+export type InferRawShape = { [K in keyof S]: z.output };
+
+/** {@linkcode ToolCallback} variant used when `inputSchema` is a {@linkcode ZodRawShape}. */
+export type LegacyToolCallback = Args extends ZodRawShape
+ ? (args: InferRawShape, ctx: ServerContext) => CallToolResult | Promise
+ : (ctx: ServerContext) => CallToolResult | Promise;
+
+/** {@linkcode PromptCallback} variant used when `argsSchema` is a {@linkcode ZodRawShape}. */
+export type LegacyPromptCallback = Args extends ZodRawShape
+ ? (args: InferRawShape, ctx: ServerContext) => GetPromptResult | Promise
+ : (ctx: ServerContext) => GetPromptResult | Promise;
+
export type BaseToolCallback<
SendResultT extends Result,
Ctx extends ServerContext,
diff --git a/packages/server/test/server/mcp.compat.test.ts b/packages/server/test/server/mcp.compat.test.ts
new file mode 100644
index 000000000..8cfd70b27
--- /dev/null
+++ b/packages/server/test/server/mcp.compat.test.ts
@@ -0,0 +1,101 @@
+import type { JSONRPCMessage } from '@modelcontextprotocol/core';
+import { InMemoryTransport, isStandardSchema, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core';
+import { describe, expect, it, vi } from 'vitest';
+import * as z from 'zod/v4';
+import { McpServer } from '../../src/index.js';
+
+describe('registerTool/registerPrompt accept raw Zod shape (auto-wrapped)', () => {
+ it('registerTool accepts a raw shape for inputSchema and auto-wraps it', () => {
+ const server = new McpServer({ name: 't', version: '1.0.0' });
+
+ server.registerTool('a', { inputSchema: { x: z.number() } }, async ({ x }) => ({
+ content: [{ type: 'text' as const, text: String(x) }]
+ }));
+ server.registerTool('b', { inputSchema: { y: z.number() } }, async ({ y }) => ({
+ content: [{ type: 'text' as const, text: String(y) }]
+ }));
+
+ const tools = (server as unknown as { _registeredTools: Record })._registeredTools;
+ expect(Object.keys(tools)).toEqual(['a', 'b']);
+ // raw shape was wrapped into a Standard Schema (z.object)
+ expect(isStandardSchema(tools['a']?.inputSchema)).toBe(true);
+ });
+
+ it('registerTool accepts a raw shape for outputSchema and auto-wraps it', () => {
+ const server = new McpServer({ name: 't', version: '1.0.0' });
+
+ server.registerTool('out', { inputSchema: { n: z.number() }, outputSchema: { result: z.string() } }, async ({ n }) => ({
+ content: [{ type: 'text' as const, text: String(n) }],
+ structuredContent: { result: String(n) }
+ }));
+
+ const tools = (server as unknown as { _registeredTools: Record })._registeredTools;
+ expect(isStandardSchema(tools['out']?.outputSchema)).toBe(true);
+ });
+
+ it('registerTool with z.object() inputSchema also works (passthrough, no auto-wrap)', () => {
+ const server = new McpServer({ name: 't', version: '1.0.0' });
+
+ server.registerTool('c', { inputSchema: z.object({ x: z.number() }) }, async ({ x }) => ({
+ content: [{ type: 'text' as const, text: String(x) }]
+ }));
+
+ const tools = (server as unknown as { _registeredTools: Record })._registeredTools;
+ expect(isStandardSchema(tools['c']?.inputSchema)).toBe(true);
+ });
+
+ it('registerPrompt accepts a raw shape for argsSchema', () => {
+ const server = new McpServer({ name: 't', version: '1.0.0' });
+
+ server.registerPrompt('p', { argsSchema: { topic: z.string() } }, async ({ topic }) => ({
+ messages: [{ role: 'user' as const, content: { type: 'text' as const, text: topic } }]
+ }));
+
+ const prompts = (server as unknown as { _registeredPrompts: Record })._registeredPrompts;
+ expect(Object.keys(prompts)).toContain('p');
+ expect(isStandardSchema(prompts['p']?.argsSchema)).toBe(true);
+ });
+
+ it('callback receives validated, typed args end-to-end via tools/call', async () => {
+ const server = new McpServer({ name: 't', version: '1.0.0' });
+
+ let received: { x: number } | undefined;
+ server.registerTool('echo', { inputSchema: { x: z.number() } }, async args => {
+ received = args;
+ return { content: [{ type: 'text' as const, text: String(args.x) }] };
+ });
+
+ const [client, srv] = InMemoryTransport.createLinkedPair();
+ await server.connect(srv);
+ await client.start();
+
+ const responses: JSONRPCMessage[] = [];
+ client.onmessage = m => responses.push(m);
+
+ await client.send({
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'initialize',
+ params: {
+ protocolVersion: LATEST_PROTOCOL_VERSION,
+ capabilities: {},
+ clientInfo: { name: 'c', version: '1.0.0' }
+ }
+ } as JSONRPCMessage);
+ await client.send({ jsonrpc: '2.0', method: 'notifications/initialized' } as JSONRPCMessage);
+ await client.send({
+ jsonrpc: '2.0',
+ id: 2,
+ method: 'tools/call',
+ params: { name: 'echo', arguments: { x: 7 } }
+ } as JSONRPCMessage);
+
+ await vi.waitFor(() => expect(responses.some(r => 'id' in r && r.id === 2)).toBe(true));
+
+ expect(received).toEqual({ x: 7 });
+ const result = responses.find(r => 'id' in r && r.id === 2) as { result?: { content: Array<{ text: string }> } };
+ expect(result.result?.content[0]?.text).toBe('7');
+
+ await server.close();
+ });
+});