Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silence-ajv-unknown-format-warnings.md
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 23 additions & 1 deletion src/validation/ajv-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
81 changes: 81 additions & 0 deletions test/validation/ajv-unknown-format.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading