Skip to content

Commit d6a02c8

Browse files
fix(core): ensure standardSchemaToJsonSchema emits type:object (#1796)
1 parent a39a9eb commit d6a02c8

File tree

3 files changed

+65
-1
lines changed

3 files changed

+65
-1
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/core': patch
3+
---
4+
5+
Ensure `standardSchemaToJsonSchema` emits `type: "object"` at the root, fixing discriminated-union tool/prompt schemas that previously produced `{oneOf: [...]}` without the MCP-required top-level type. Also throws a clear error when given an explicitly non-object schema (e.g. `z.string()`). Fixes #1643.

packages/core/src/util/standardSchema.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,25 @@ export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSch
138138

139139
// JSON Schema conversion
140140

141+
/**
142+
* Converts a StandardSchema to JSON Schema for use as an MCP tool/prompt schema.
143+
*
144+
* MCP requires `type: "object"` at the root of tool inputSchema/outputSchema and
145+
* prompt argument schemas. Zod's discriminated unions emit `{oneOf: [...]}` without
146+
* a top-level `type`, so this function defaults `type` to `"object"` when absent.
147+
*
148+
* Throws if the schema has an explicit non-object `type` (e.g. `z.string()`),
149+
* since that cannot satisfy the MCP spec.
150+
*/
141151
export function standardSchemaToJsonSchema(schema: StandardJSONSchemaV1, io: 'input' | 'output' = 'input'): Record<string, unknown> {
142-
return schema['~standard'].jsonSchema[io]({ target: 'draft-2020-12' });
152+
const result = schema['~standard'].jsonSchema[io]({ target: 'draft-2020-12' });
153+
if (result.type !== undefined && result.type !== 'object') {
154+
throw new Error(
155+
`MCP tool and prompt schemas must describe objects (got type: ${JSON.stringify(result.type)}). ` +
156+
`Wrap your schema in z.object({...}) or equivalent.`
157+
);
158+
}
159+
return { type: 'object', ...result };
143160
}
144161

145162
// Validation
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as z from 'zod/v4';
2+
3+
import { standardSchemaToJsonSchema } from '../../src/util/standardSchema.js';
4+
5+
describe('standardSchemaToJsonSchema', () => {
6+
test('emits type:object for plain z.object schemas', () => {
7+
const schema = z.object({ name: z.string(), age: z.number() });
8+
const result = standardSchemaToJsonSchema(schema, 'input');
9+
10+
expect(result.type).toBe('object');
11+
expect(result.properties).toBeDefined();
12+
});
13+
14+
test('emits type:object for discriminated unions', () => {
15+
const schema = z.discriminatedUnion('action', [
16+
z.object({ action: z.literal('create'), name: z.string() }),
17+
z.object({ action: z.literal('delete'), id: z.string() })
18+
]);
19+
const result = standardSchemaToJsonSchema(schema, 'input');
20+
21+
expect(result.type).toBe('object');
22+
// Zod emits oneOf for discriminated unions; the catchall on Tool.inputSchema
23+
// accepts it, but the top-level type must be present per MCP spec.
24+
expect(result.oneOf ?? result.anyOf).toBeDefined();
25+
});
26+
27+
test('throws for schemas with explicit non-object type', () => {
28+
expect(() => standardSchemaToJsonSchema(z.string(), 'input')).toThrow(/must describe objects/);
29+
expect(() => standardSchemaToJsonSchema(z.array(z.string()), 'input')).toThrow(/must describe objects/);
30+
expect(() => standardSchemaToJsonSchema(z.number(), 'input')).toThrow(/must describe objects/);
31+
});
32+
33+
test('preserves existing type:object without modification', () => {
34+
const schema = z.object({ x: z.string() });
35+
const result = standardSchemaToJsonSchema(schema, 'input');
36+
37+
// Spread order means zod's own type:"object" wins; verify no double-wrap.
38+
const keys = Object.keys(result);
39+
expect(keys.filter(k => k === 'type')).toHaveLength(1);
40+
expect(result.type).toBe('object');
41+
});
42+
});

0 commit comments

Comments
 (0)