From 1a0a9061b8bc6c4ea101ef2643747d4a912f78d3 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 10 Dec 2025 17:25:35 +0000 Subject: [PATCH] Fix Zod v4 schema description extraction Zod v4 stores descriptions in `z.globalRegistry`, not in `._zod.def.description`. Both v3 and v4 expose a `.description` getter, so simplify to just use that. Also removes the incorrect `description` field from `ZodV4Internal` interface. Fixes #1277 --- src/server/zod-compat.ts | 12 ++-- .../test_1277_zod_v4_description.test.ts | 65 +++++++++++++++++++ 2 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 test/issues/test_1277_zod_v4_description.test.ts diff --git a/src/server/zod-compat.ts b/src/server/zod-compat.ts index 04ee5361f..9d25a5efc 100644 --- a/src/server/zod-compat.ts +++ b/src/server/zod-compat.ts @@ -35,7 +35,6 @@ export interface ZodV4Internal { value?: unknown; values?: unknown[]; shape?: Record | (() => Record); - description?: string; }; }; value?: unknown; @@ -220,15 +219,12 @@ export function getParseErrorMessage(error: unknown): string { /** * Gets the description from a schema, if available. * Works with both Zod v3 and v4. + * + * Both versions expose a `.description` getter that returns the description + * from their respective internal storage (v3: _def, v4: globalRegistry). */ export function getSchemaDescription(schema: AnySchema): string | undefined { - if (isZ4Schema(schema)) { - const v4Schema = schema as unknown as ZodV4Internal; - return v4Schema._zod?.def?.description; - } - const v3Schema = schema as unknown as ZodV3Internal; - // v3 may have description on the schema itself or in _def - return (schema as { description?: string }).description ?? v3Schema._def?.description; + return (schema as { description?: string }).description; } /** diff --git a/test/issues/test_1277_zod_v4_description.test.ts b/test/issues/test_1277_zod_v4_description.test.ts new file mode 100644 index 000000000..2b8448647 --- /dev/null +++ b/test/issues/test_1277_zod_v4_description.test.ts @@ -0,0 +1,65 @@ +/** + * Regression test for https://github.com/modelcontextprotocol/typescript-sdk/issues/1277 + * + * Zod v4 stores `.describe()` descriptions directly on the schema object, + * not in `._zod.def.description`. This test verifies that descriptions are + * correctly extracted for prompt arguments. + */ + +import { Client } from '../../src/client/index.js'; +import { InMemoryTransport } from '../../src/inMemory.js'; +import { ListPromptsResultSchema } from '../../src/types.js'; +import { McpServer } from '../../src/server/mcp.js'; +import { zodTestMatrix, type ZodMatrixEntry } from '../../src/__fixtures__/zodTestMatrix.js'; + +describe.each(zodTestMatrix)('Issue #1277: $zodVersionLabel', (entry: ZodMatrixEntry) => { + const { z } = entry; + + test('should preserve argument descriptions from .describe()', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + mcpServer.prompt( + 'test', + { + name: z.string().describe('The user name'), + value: z.string().describe('The value to set') + }, + async ({ name, value }) => ({ + messages: [ + { + role: 'assistant', + content: { + type: 'text', + text: `${name}: ${value}` + } + } + ] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + const result = await client.request( + { + method: 'prompts/list' + }, + ListPromptsResultSchema + ); + + expect(result.prompts).toHaveLength(1); + expect(result.prompts[0].name).toBe('test'); + expect(result.prompts[0].arguments).toEqual([ + { name: 'name', required: true, description: 'The user name' }, + { name: 'value', required: true, description: 'The value to set' } + ]); + }); +});