Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions src/server/zod-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export interface ZodV4Internal {
value?: unknown;
values?: unknown[];
shape?: Record<string, AnySchema> | (() => Record<string, AnySchema>);
description?: string;
};
};
value?: unknown;
Expand Down Expand Up @@ -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;
}
Comment on lines -225 to -228
Copy link
Contributor Author

@felixweinberger felixweinberger Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In v4 .describe() stores in globalRegistry with the .description getter reading from it directly via v4Schema.description rather than on _def so we don't need this.

https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/schemas.ts#L223-L230

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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ?? v3Schema._def?.description is unnecessary because ZodType on v3 already has this getter: https://github.com/colinhacks/zod/blob/main/packages/zod/src/v3/types.ts#L164-L166

get description(): string | undefined {
  return this._def.description;
}

return (schema as { description?: string }).description;
}

/**
Expand Down
65 changes: 65 additions & 0 deletions test/issues/test_1277_zod_v4_description.test.ts
Original file line number Diff line number Diff line change
@@ -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' }
]);
});
});
Loading