From ae8e2827093a480245c9333e5b01abf266041d77 Mon Sep 17 00:00:00 2001 From: Kiran Magic Date: Thu, 21 May 2026 06:07:48 +0530 Subject: [PATCH] fix(core): preserve related request id zero for debounce --- .../fix-related-request-id-zero-debounce.md | 5 +++++ packages/core/src/shared/protocol.ts | 3 ++- packages/core/test/shared/protocol.test.ts | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-related-request-id-zero-debounce.md diff --git a/.changeset/fix-related-request-id-zero-debounce.md b/.changeset/fix-related-request-id-zero-debounce.md new file mode 100644 index 0000000000..47fead2e09 --- /dev/null +++ b/.changeset/fix-related-request-id-zero-debounce.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/core': patch +--- + +Preserve `relatedRequestId: 0` when deciding whether notifications can be debounced. Request id `0` is valid, so request-associated notifications with that id now bypass debounce like other related notifications. diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index ed78cc68d0..c38ee9d5ba 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -832,7 +832,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/test/shared/protocol.test.ts b/packages/core/test/shared/protocol.test.ts index 6e77430d61..0cd0ee83b4 100644 --- a/packages/core/test/shared/protocol.test.ts +++ b/packages/core/test/shared/protocol.test.ts @@ -654,6 +654,21 @@ describe('protocol tests', () => { expect(sendSpy).toHaveBeenCalledWith(expect.any(Object), { relatedRequestId: 'req-2' }); }); + it('should NOT debounce a notification that has relatedRequestId 0', async () => { + // ARRANGE + protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced_with_options'] }); + await protocol.connect(transport); + + // ACT + const firstNotification = protocol.notification({ method: 'test/debounced_with_options' }, { relatedRequestId: 0 }); + const secondNotification = protocol.notification({ method: 'test/debounced_with_options' }, { relatedRequestId: 0 }); + await Promise.all([firstNotification, secondNotification]); + + // ASSERT + expect(sendSpy).toHaveBeenCalledTimes(2); + expect(sendSpy).toHaveBeenCalledWith(expect.any(Object), { relatedRequestId: 0 }); + }); + it('should clear pending debounced notifications on connection close', async () => { // ARRANGE protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced'] });