From 4db24ae317d214210d8cf3c1ddb41701ba5c3149 Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:12:11 +0200 Subject: [PATCH] Fix debouncing for related request ID zero --- .changeset/related-request-id-zero.md | 7 +++++++ packages/core-internal/src/shared/protocol.ts | 3 ++- .../core-internal/test/shared/protocol.test.ts | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .changeset/related-request-id-zero.md diff --git a/.changeset/related-request-id-zero.md b/.changeset/related-request-id-zero.md new file mode 100644 index 0000000000..9deb5f59c0 --- /dev/null +++ b/.changeset/related-request-id-zero.md @@ -0,0 +1,7 @@ +--- +'@modelcontextprotocol/client': patch +'@modelcontextprotocol/server': patch +'@modelcontextprotocol/server-legacy': patch +--- + +Treat numeric related request ID `0` as present when deciding whether to debounce notifications. Request-associated notifications for the first request ID are no longer coalesced, and send failures now reject the notification promise consistently with other request IDs. diff --git a/packages/core-internal/src/shared/protocol.ts b/packages/core-internal/src/shared/protocol.ts index ffab6e8df8..b3d1a061f2 100644 --- a/packages/core-internal/src/shared/protocol.ts +++ b/packages/core-internal/src/shared/protocol.ts @@ -1610,7 +1610,8 @@ export abstract class Protocol { const debouncedMethods = this._options?.debouncedNotificationMethods ?? []; // A notification can only be debounced if it's in the list AND it's "simple" // (i.e., has no parameters and no related request ID that could be lost). - const canDebounce = debouncedMethods.includes(notification.method) && !notification.params && !options?.relatedRequestId; + const canDebounce = + debouncedMethods.includes(notification.method) && !notification.params && options?.relatedRequestId === undefined; if (canDebounce) { // If a notification of this type is already scheduled, do nothing. diff --git a/packages/core-internal/test/shared/protocol.test.ts b/packages/core-internal/test/shared/protocol.test.ts index 2ecdc40adc..6609a2f0fe 100644 --- a/packages/core-internal/test/shared/protocol.test.ts +++ b/packages/core-internal/test/shared/protocol.test.ts @@ -656,6 +656,22 @@ describe('protocol tests', () => { expect(sendSpy).toHaveBeenCalledWith(expect.any(Object), { relatedRequestId: 'req-2' }); }); + it('should NOT debounce a notification with relatedRequestId 0', async () => { + // ARRANGE + protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced_with_numeric_id'] }); + await protocol.connect(transport); + + // ACT + const first = protocol.notification({ method: 'test/debounced_with_numeric_id' }, { relatedRequestId: 0 }); + const second = protocol.notification({ method: 'test/debounced_with_numeric_id' }, { relatedRequestId: 0 }); + await Promise.all([first, second]); + + // ASSERT + expect(sendSpy).toHaveBeenCalledTimes(2); + expect(sendSpy).toHaveBeenNthCalledWith(1, expect.any(Object), { relatedRequestId: 0 }); + expect(sendSpy).toHaveBeenNthCalledWith(2, expect.any(Object), { relatedRequestId: 0 }); + }); + it('should clear pending debounced notifications on connection close', async () => { // ARRANGE protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced'] });