diff --git a/.changeset/fix-zod-object-additional-properties-schema.md b/.changeset/fix-zod-object-additional-properties-schema.md new file mode 100644 index 0000000000..11ae1365f0 --- /dev/null +++ b/.changeset/fix-zod-object-additional-properties-schema.md @@ -0,0 +1,6 @@ +--- +"@modelcontextprotocol/core": patch +"@modelcontextprotocol/test-integration": patch +--- + +fix: align zod object schemas with stripped properties diff --git a/packages/core/src/util/standardSchema.ts b/packages/core/src/util/standardSchema.ts index ee1a630670..92e72dfc36 100644 --- a/packages/core/src/util/standardSchema.ts +++ b/packages/core/src/util/standardSchema.ts @@ -188,7 +188,11 @@ export function standardSchemaToJsonSchema(schema: StandardJSONSchemaV1, io: 'in `Wrap your schema in z.object({...}) or equivalent.` ); } - return { type: 'object', ...result }; + const jsonSchema: Record = { type: 'object', ...result }; + if (jsonSchema.properties !== undefined && !('additionalProperties' in jsonSchema) && !('unevaluatedProperties' in jsonSchema)) { + jsonSchema.additionalProperties = false; + } + return jsonSchema; } // Validation diff --git a/packages/core/test/util/standardSchema.test.ts b/packages/core/test/util/standardSchema.test.ts index 6c3de99d77..57f6c8a325 100644 --- a/packages/core/test/util/standardSchema.test.ts +++ b/packages/core/test/util/standardSchema.test.ts @@ -39,4 +39,28 @@ describe('standardSchemaToJsonSchema', () => { expect(keys.filter(k => k === 'type')).toHaveLength(1); expect(result.type).toBe('object'); }); + + test('marks default z.object schemas as not accepting additional properties', () => { + const schema = z.object({ message: z.string() }); + const result = standardSchemaToJsonSchema(schema, 'input'); + + expect(result.additionalProperties).toBe(false); + }); + + test('preserves schemas that explicitly allow additional properties', () => { + const schema = z.object({ message: z.string() }).passthrough(); + const result = standardSchemaToJsonSchema(schema, 'input'); + + expect(result.additionalProperties).toEqual({}); + }); + + test('does not add root additionalProperties to union schemas', () => { + const schema = z.discriminatedUnion('action', [ + z.object({ action: z.literal('create'), name: z.string() }), + z.object({ action: z.literal('delete'), id: z.string() }) + ]); + const result = standardSchemaToJsonSchema(schema, 'input'); + + expect(result.additionalProperties).toBeUndefined(); + }); }); diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 92af09744c..660ea5e5e0 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -934,6 +934,7 @@ describe('Zod v4', () => { expect(result.tools[0]!.name).toBe('test'); expect(result.tools[0]!.inputSchema).toMatchObject({ type: 'object', + additionalProperties: false, properties: { name: { type: 'string' }, value: { type: 'number' }