From a41f7c5cf291b4bdf1b891cc43027cf078c7a8c4 Mon Sep 17 00:00:00 2001 From: nacim-coder Date: Sun, 17 May 2026 08:15:24 +0000 Subject: [PATCH] fix: handle cancelled notifications for request id zero --- .changeset/fuzzy-birds-cancel.md | 5 +++ packages/core/src/shared/protocol.ts | 2 +- packages/core/test/shared/protocol.test.ts | 37 ++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 .changeset/fuzzy-birds-cancel.md diff --git a/.changeset/fuzzy-birds-cancel.md b/.changeset/fuzzy-birds-cancel.md new file mode 100644 index 0000000000..247fcb98b6 --- /dev/null +++ b/.changeset/fuzzy-birds-cancel.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/core': patch +--- + +Fix `notifications/cancelled` handling for request ID `0`. Previously the cancellation guard treated `0` as missing and left the first request from a protocol instance uncancellable. diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index 361bd6fc7c..c9cdc68ae4 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -409,7 +409,7 @@ export abstract class Protocol { protected abstract buildContext(ctx: BaseContext, transportInfo?: MessageExtraInfo): ContextT; private async _oncancel(notification: CancelledNotification): Promise { - if (!notification.params.requestId) { + if (notification.params.requestId === undefined) { return; } // Handle request cancellation diff --git a/packages/core/test/shared/protocol.test.ts b/packages/core/test/shared/protocol.test.ts index 619e09376a..4c70d63824 100644 --- a/packages/core/test/shared/protocol.test.ts +++ b/packages/core/test/shared/protocol.test.ts @@ -2319,6 +2319,43 @@ describe('Request Cancellation vs Task Cancellation', () => { expect(wasAborted).toBe(true); }); + test('should abort request handler when cancelling request ID 0', async () => { + await protocol.connect(transport); + + let wasAborted = false; + protocol.setRequestHandler('ping', async (_request, ctx) => { + await new Promise(resolve => setTimeout(resolve, 100)); + wasAborted = ctx.mcpReq.signal.aborted; + return {}; + }); + + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + id: 0, + method: 'ping', + params: {} + }); + } + + await new Promise(resolve => setTimeout(resolve, 10)); + + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + method: 'notifications/cancelled', + params: { + requestId: 0, + reason: 'User cancelled' + } + }); + } + + await new Promise(resolve => setTimeout(resolve, 150)); + + expect(wasAborted).toBe(true); + }); + test('should NOT automatically cancel associated tasks when notifications/cancelled is received', async () => { await protocol.connect(transport);