diff --git a/.changeset/fix-debounce-list-changed-notifications.md b/.changeset/fix-debounce-list-changed-notifications.md new file mode 100644 index 0000000000..8c47acbb11 --- /dev/null +++ b/.changeset/fix-debounce-list-changed-notifications.md @@ -0,0 +1,6 @@ +--- +"@modelcontextprotocol/server": patch +"@modelcontextprotocol/test-integration": patch +--- + +fix(server): debounce list changed notifications diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index fb45fd5db6..6a4da4df89 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -47,6 +47,12 @@ import { getCompleter, isCompletable } from './completable.js'; import type { ServerOptions } from './server.js'; import { Server } from './server.js'; +const DEFAULT_DEBOUNCED_NOTIFICATION_METHODS = [ + 'notifications/resources/list_changed', + 'notifications/tools/list_changed', + 'notifications/prompts/list_changed' +]; + /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. * For advanced usage (like sending notifications or setting custom request handlers), use the underlying @@ -75,7 +81,12 @@ export class McpServer { private _experimental?: { tasks: ExperimentalMcpServerTasks }; constructor(serverInfo: Implementation, options?: ServerOptions) { - this.server = new Server(serverInfo, options); + this.server = new Server(serverInfo, { + ...options, + debouncedNotificationMethods: [ + ...new Set([...DEFAULT_DEBOUNCED_NOTIFICATION_METHODS, ...(options?.debouncedNotificationMethods ?? [])]) + ] + }); } /** diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 92af09744c..d07c45fd96 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -809,6 +809,38 @@ describe('Zod v4', () => { ]); }); + test('should debounce synchronous tool list changed notifications', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const notifications: Notification[] = []; + const client = new Client({ + name: 'test client', + version: '1.0' + }); + client.fallbackNotificationHandler = async notification => { + notifications.push(notification); + }; + + mcpServer.registerTool('initial', {}, async () => ({ + content: [{ type: 'text', text: 'Initial response' }] + })); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + for (let i = 0; i < 20; i++) { + mcpServer.registerTool(`tool-${i}`, {}, async () => ({ + content: [{ type: 'text', text: `Tool ${i} response` }] + })); + } + + await new Promise(process.nextTick); + + expect(notifications).toMatchObject([{ method: 'notifications/tools/list_changed' }]); + }); + /*** * Test: listChanged capability should default to true when not specified */