diff --git a/.changeset/silence-ajv-unknown-format-warnings.md b/.changeset/silence-ajv-unknown-format-warnings.md new file mode 100644 index 0000000000..7b9b43f940 --- /dev/null +++ b/.changeset/silence-ajv-unknown-format-warnings.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/sdk': patch +--- + +Silence Ajv's "unknown format ... ignored" warnings in the default `AjvJsonSchemaValidator` logger so vendor-specific `format` annotations (e.g. `google-duration`) no longer flood the console on every `listTools` call. Validation semantics are unchanged. diff --git a/src/validation/ajv-provider.ts b/src/validation/ajv-provider.ts index 3c2967c3a1..61bf0a0880 100644 --- a/src/validation/ajv-provider.ts +++ b/src/validation/ajv-provider.ts @@ -6,12 +6,34 @@ import Ajv from 'ajv'; import _addFormats from 'ajv-formats'; import type { JsonSchemaType, JsonSchemaValidator, JsonSchemaValidatorResult, jsonSchemaValidator } from './types.js'; +/** + * Logger for the default Ajv instance. JSON Schema treats `format` as an annotation, + * so schemas are allowed to declare formats this validator does not know about (e.g. + * vendor-specific ones like `google-duration`), and unknown formats must be ignored + * silently. Ajv's default logger emits one "unknown format ... ignored" warning per + * schema path on every compile, so a single server-side format could flood the console + * on each `listTools` call. Other warnings and errors are still surfaced on the console. + * Callers who prefer to see (or map) unknown-format diagnostics can pass their own + * pre-configured Ajv instance instead. + */ +const defaultAjvLogger = { + log: (...args: unknown[]) => console.log(...args), + warn: (...args: unknown[]) => { + const message = args[0]; + if (!(typeof message === 'string' && message.startsWith('unknown format'))) { + console.warn(...args); + } + }, + error: (...args: unknown[]) => console.error(...args) +}; + function createDefaultAjvInstance(): Ajv { const ajv = new Ajv({ strict: false, validateFormats: true, validateSchema: false, - allErrors: true + allErrors: true, + logger: defaultAjvLogger }); const addFormats = _addFormats as unknown as typeof _addFormats.default; diff --git a/test/validation/ajv-unknown-format.test.ts b/test/validation/ajv-unknown-format.test.ts new file mode 100644 index 0000000000..cd875fde78 --- /dev/null +++ b/test/validation/ajv-unknown-format.test.ts @@ -0,0 +1,81 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { AjvJsonSchemaValidator } from '../../src/validation/ajv-provider.js'; +import type { JsonSchemaType } from '../../src/validation/types.js'; + +/** + * Servers may declare vendor-specific string `format` values that this SDK does not + * know about (e.g. `google-duration` used by the Google Monitoring MCP server). + * JSON Schema treats `format` as an annotation, so those schemas must still validate, + * and Ajv's "unknown format ... ignored" warnings must not flood the console every + * time tool outputSchemas are compiled (see issue #2855). + */ +describe('AjvJsonSchemaValidator with unknown formats', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + const schemaWithUnknownFormat: JsonSchemaType = { + type: 'object', + properties: { + d: { + type: 'string', + format: 'google-duration' + } + } + }; + + it('does not warn about unknown formats', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const validator = new AjvJsonSchemaValidator(); + + validator.getValidator(schemaWithUnknownFormat); + + const unknownFormatWarnings = warnSpy.mock.calls.filter( + ([message]) => typeof message === 'string' && message.startsWith('unknown format') + ); + expect(unknownFormatWarnings).toHaveLength(0); + }); + + it('stays quiet across repeated compiles of the same schema', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const validator = new AjvJsonSchemaValidator(); + + for (let i = 0; i < 10; i++) { + validator.getValidator(schemaWithUnknownFormat); + } + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('still validates data when the format is unknown', () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}); + const validator = new AjvJsonSchemaValidator(); + const validate = validator.getValidator<{ d: string }>(schemaWithUnknownFormat); + + const result = validate({ d: '1800s' }); + expect(result.valid).toBe(true); + expect(result.data).toEqual({ d: '1800s' }); + + const invalidResult = validate({ d: 5 }); + expect(invalidResult.valid).toBe(false); + expect(invalidResult.errorMessage).toBeDefined(); + }); + + it('still validates known formats', () => { + const validator = new AjvJsonSchemaValidator(); + const validate = validator.getValidator<{ email: string }>({ + type: 'object', + properties: { + email: { + type: 'string', + format: 'email' + } + }, + required: ['email'] + }); + + expect(validate({ email: 'user@example.com' }).valid).toBe(true); + expect(validate({ email: 'not-an-email' }).valid).toBe(false); + }); +});