diff --git a/.changeset/standard-schema-empty-issues.md b/.changeset/standard-schema-empty-issues.md new file mode 100644 index 0000000000..8bdb9df852 --- /dev/null +++ b/.changeset/standard-schema-empty-issues.md @@ -0,0 +1,6 @@ +--- +'@modelcontextprotocol/client': patch +'@modelcontextprotocol/server': patch +--- + +Treat Standard Schema validation results with an empty `issues` array as failures instead of accepting an undefined value. diff --git a/packages/core-internal/src/util/standardSchema.ts b/packages/core-internal/src/util/standardSchema.ts index ebaaa57572..11599f19d7 100644 --- a/packages/core-internal/src/util/standardSchema.ts +++ b/packages/core-internal/src/util/standardSchema.ts @@ -284,8 +284,11 @@ export async function validateStandardSchema( data: unknown ): Promise>> { const result = await schema['~standard'].validate(data); - if (result.issues && result.issues.length > 0) { - return { success: false, error: result.issues.map(i => formatIssue(i)).join(', ') }; + if (result.issues) { + // A present but empty `issues` array is still a failure, so fall back to a + // non-empty message: callers that surface `error` must not report it as success. + const message = result.issues.map(i => formatIssue(i)).join(', '); + return { success: false, error: message || 'validation failed' }; } return { success: true, data: (result as StandardSchemaV1.SuccessResult).value as StandardSchemaV1.InferOutput }; } diff --git a/packages/core-internal/test/util/standardSchema.test.ts b/packages/core-internal/test/util/standardSchema.test.ts index 8856592ff0..5c7ab4df25 100644 --- a/packages/core-internal/test/util/standardSchema.test.ts +++ b/packages/core-internal/test/util/standardSchema.test.ts @@ -1,6 +1,33 @@ import * as z from 'zod/v4'; -import { standardSchemaToJsonSchema } from '../../src/util/standardSchema'; +import { type StandardSchemaV1, standardSchemaToJsonSchema, validateStandardSchema } from '../../src/util/standardSchema'; + +describe('validateStandardSchema', () => { + test.each(['synchronous', 'asynchronous'] as const)('rejects %s failures with an empty issues array', async mode => { + const schema: StandardSchemaV1 = { + '~standard': { + version: 1, + vendor: 'empty-issues', + validate: () => (mode === 'asynchronous' ? Promise.resolve({ issues: [] }) : { issues: [] }) + } + }; + + await expect(validateStandardSchema(schema, { name: 'example' })).resolves.toEqual({ + success: false, + error: 'validation failed' + }); + }); + + test('preserves transformed values when validation succeeds', async () => { + const schema = z.string().transform(value => value.toUpperCase()); + + await expect(validateStandardSchema(schema, 'hello')).resolves.toEqual({ success: true, data: 'HELLO' }); + }); + + test('preserves undefined when validation succeeds', async () => { + await expect(validateStandardSchema(z.undefined(), undefined)).resolves.toEqual({ success: true, data: undefined }); + }); +}); describe('standardSchemaToJsonSchema', () => { test('emits type:object for plain z.object schemas', () => {