From 3a53f24f39a8a0c764511b51957a1fcfe1c4b657 Mon Sep 17 00:00:00 2001 From: Zelys Date: Thu, 23 Apr 2026 10:51:16 -0500 Subject: [PATCH 1/2] fix(elicitation): allow extra JSON Schema keywords on primitive schemas Primitive schema Zod validators (BooleanSchemaSchema, StringSchemaSchema, NumberSchemaSchema) stripped any key not explicitly listed, rejecting valid JSON Schema keywords like `pattern`, `exclusiveMinimum`, `const`, etc. Added .passthrough() to all three so extra keys are preserved, and added [key: string]: unknown index signatures to the matching spec.types interfaces to maintain bidirectional type compatibility. Closes #1844 Co-Authored-By: Claude Sonnet 4.6 --- .../fix-elicit-primitive-passthrough.md | 9 +++ packages/core/src/types/schemas.ts | 6 +- packages/core/src/types/spec.types.ts | 3 + .../test/server/elicitation.test.ts | 65 ++++++++++++++++++- 4 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 .changeset/fix-elicit-primitive-passthrough.md diff --git a/.changeset/fix-elicit-primitive-passthrough.md b/.changeset/fix-elicit-primitive-passthrough.md new file mode 100644 index 000000000..05ac965f0 --- /dev/null +++ b/.changeset/fix-elicit-primitive-passthrough.md @@ -0,0 +1,9 @@ +--- +'@modelcontextprotocol/core': patch +--- + +Allow extra JSON Schema keywords on elicitation primitive schemas (`string`, `number`, `boolean`). + +Previously, `BooleanSchemaSchema`, `StringSchemaSchema`, and `NumberSchemaSchema` used strict Zod parsing, so any keyword not explicitly listed (e.g., `pattern`, `exclusiveMinimum`, `exclusiveMaximum`, `const`) caused the schema to be rejected. This broke real-world use cases where servers send valid JSON Schema with standard annotation or validation keywords. + +The fix adds `.passthrough()` to each primitive schema so that extra keys are preserved instead of stripped. The corresponding `BooleanSchema`, `StringSchema`, and `NumberSchema` TypeScript interfaces also gain `[key: string]: unknown` index signatures to stay in sync. diff --git a/packages/core/src/types/schemas.ts b/packages/core/src/types/schemas.ts index 86acf11d7..0c8cba9b0 100644 --- a/packages/core/src/types/schemas.ts +++ b/packages/core/src/types/schemas.ts @@ -1722,7 +1722,7 @@ export const BooleanSchemaSchema = z.object({ title: z.string().optional(), description: z.string().optional(), default: z.boolean().optional() -}); +}).passthrough(); /** * Primitive schema definition for string fields. @@ -1735,7 +1735,7 @@ export const StringSchemaSchema = z.object({ maxLength: z.number().optional(), format: z.enum(['email', 'uri', 'date', 'date-time']).optional(), default: z.string().optional() -}); +}).passthrough(); /** * Primitive schema definition for number fields. @@ -1747,7 +1747,7 @@ export const NumberSchemaSchema = z.object({ minimum: z.number().optional(), maximum: z.number().optional(), default: z.number().optional() -}); +}).passthrough(); /** * Schema for single-selection enumeration without display titles for options. diff --git a/packages/core/src/types/spec.types.ts b/packages/core/src/types/spec.types.ts index a03f21f13..367dabf9e 100644 --- a/packages/core/src/types/spec.types.ts +++ b/packages/core/src/types/spec.types.ts @@ -2875,6 +2875,7 @@ export type PrimitiveSchemaDefinition = StringSchema | NumberSchema | BooleanSch * @category `elicitation/create` */ export interface StringSchema { + [key: string]: unknown; type: 'string'; title?: string; description?: string; @@ -2891,6 +2892,7 @@ export interface StringSchema { * @category `elicitation/create` */ export interface NumberSchema { + [key: string]: unknown; type: 'number' | 'integer'; title?: string; description?: string; @@ -2906,6 +2908,7 @@ export interface NumberSchema { * @category `elicitation/create` */ export interface BooleanSchema { + [key: string]: unknown; type: 'boolean'; title?: string; description?: string; diff --git a/test/integration/test/server/elicitation.test.ts b/test/integration/test/server/elicitation.test.ts index 55c989da0..1e65e2911 100644 --- a/test/integration/test/server/elicitation.test.ts +++ b/test/integration/test/server/elicitation.test.ts @@ -161,7 +161,6 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo age: { type: 'integer', minimum: 0, maximum: 150 }, street: { type: 'string' }, city: { type: 'string' }, - // @ts-expect-error - pattern is not a valid property by MCP spec, however it is making use of the Ajv validator zipCode: { type: 'string', pattern: '^[0-9]{5}$' }, newsletter: { type: 'boolean' }, notifications: { type: 'boolean' } @@ -280,7 +279,6 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo requestedSchema: { type: 'object', properties: { - // @ts-expect-error - pattern is not a valid property by MCP spec, however it is making use of the Ajv validator zipCode: { type: 'string', pattern: '^[0-9]{5}$' } }, required: ['zipCode'] @@ -290,6 +288,69 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo await expect(server.elicitInput(formRequestParams)).rejects.toThrow(/does not match requested schema/); }); + test(`${validatorName}: should accept extra JSON Schema keys on string primitive (pattern)`, async () => { + client.setRequestHandler('elicitation/create', _request => ({ + action: 'accept', + content: { code: 'abc' } + })); + + const result = await server.elicitInput({ + mode: 'form', + message: 'Enter a code', + requestedSchema: { + type: 'object', + properties: { + code: { type: 'string', pattern: '^[a-z]+$' } + }, + required: ['code'] + } + }); + + expect(result).toEqual({ action: 'accept', content: { code: 'abc' } }); + }); + + test(`${validatorName}: should accept extra JSON Schema keys on number primitive (exclusiveMinimum)`, async () => { + client.setRequestHandler('elicitation/create', _request => ({ + action: 'accept', + content: { score: 0.5 } + })); + + const result = await server.elicitInput({ + mode: 'form', + message: 'Enter a score', + requestedSchema: { + type: 'object', + properties: { + score: { type: 'number', exclusiveMinimum: 0, exclusiveMaximum: 1 } + }, + required: ['score'] + } + }); + + expect(result).toEqual({ action: 'accept', content: { score: 0.5 } }); + }); + + test(`${validatorName}: should accept extra JSON Schema keys on boolean primitive (const)`, async () => { + client.setRequestHandler('elicitation/create', _request => ({ + action: 'accept', + content: { agreed: true } + })); + + const result = await server.elicitInput({ + mode: 'form', + message: 'Do you agree?', + requestedSchema: { + type: 'object', + properties: { + agreed: { type: 'boolean', const: true } + }, + required: ['agreed'] + } + }); + + expect(result).toEqual({ action: 'accept', content: { agreed: true } }); + }); + test(`${validatorName}: should allow decline action without validation`, async () => { client.setRequestHandler('elicitation/create', _request => ({ action: 'decline' From a88c75a0cd7de2a3eed353de2cd5324ab600b953 Mon Sep 17 00:00:00 2001 From: Zelys Date: Thu, 23 Apr 2026 11:12:13 -0500 Subject: [PATCH 2/2] style: fix Prettier formatting on passthrough schema chains Prettier formats long z.object({...}).passthrough() as a broken chain with .passthrough() on its own line. Co-Authored-By: Claude Sonnet 4.6 --- packages/core/src/types/schemas.ts | 52 +++++++++++++++++------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/core/src/types/schemas.ts b/packages/core/src/types/schemas.ts index 0c8cba9b0..b1f1d9da5 100644 --- a/packages/core/src/types/schemas.ts +++ b/packages/core/src/types/schemas.ts @@ -1717,37 +1717,43 @@ export const CreateMessageResultWithToolsSchema = ResultSchema.extend({ /** * Primitive schema definition for boolean fields. */ -export const BooleanSchemaSchema = z.object({ - type: z.literal('boolean'), - title: z.string().optional(), - description: z.string().optional(), - default: z.boolean().optional() -}).passthrough(); +export const BooleanSchemaSchema = z + .object({ + type: z.literal('boolean'), + title: z.string().optional(), + description: z.string().optional(), + default: z.boolean().optional() + }) + .passthrough(); /** * Primitive schema definition for string fields. */ -export const StringSchemaSchema = z.object({ - type: z.literal('string'), - title: z.string().optional(), - description: z.string().optional(), - minLength: z.number().optional(), - maxLength: z.number().optional(), - format: z.enum(['email', 'uri', 'date', 'date-time']).optional(), - default: z.string().optional() -}).passthrough(); +export const StringSchemaSchema = z + .object({ + type: z.literal('string'), + title: z.string().optional(), + description: z.string().optional(), + minLength: z.number().optional(), + maxLength: z.number().optional(), + format: z.enum(['email', 'uri', 'date', 'date-time']).optional(), + default: z.string().optional() + }) + .passthrough(); /** * Primitive schema definition for number fields. */ -export const NumberSchemaSchema = z.object({ - type: z.enum(['number', 'integer']), - title: z.string().optional(), - description: z.string().optional(), - minimum: z.number().optional(), - maximum: z.number().optional(), - default: z.number().optional() -}).passthrough(); +export const NumberSchemaSchema = z + .object({ + type: z.enum(['number', 'integer']), + title: z.string().optional(), + description: z.string().optional(), + minimum: z.number().optional(), + maximum: z.number().optional(), + default: z.number().optional() + }) + .passthrough(); /** * Schema for single-selection enumeration without display titles for options.