From 2cb1ccbfdc7ceeed54dae3d80f3ebe90e2f4494b Mon Sep 17 00:00:00 2001 From: David Ahmann Date: Wed, 25 Feb 2026 15:00:33 -0500 Subject: [PATCH] fix(everything): use collision-resistant request and operation ids (#3404) --- src/everything/__tests__/tools.test.ts | 23 +++++++++++++++++++ .../tools/trigger-long-running-operation.ts | 7 ++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/everything/__tests__/tools.test.ts b/src/everything/__tests__/tools.test.ts index dbe463b2a5..5a4355464a 100644 --- a/src/everything/__tests__/tools.test.ts +++ b/src/everything/__tests__/tools.test.ts @@ -333,6 +333,7 @@ describe('Tools', () => { ); expect(result.content[0].text).toContain('Long running operation completed'); + expect(result.content[0].text).toContain('Operation ID:'); expect(result.content[0].text).toContain('Duration: 0.1 seconds'); expect(result.content[0].text).toContain('Steps: 2'); }, 10000); @@ -358,6 +359,28 @@ describe('Tools', () => { expect.any(Object) ); }, 10000); + + it('should generate a collision-resistant request id when missing', async () => { + const { mockServer, handlers } = createMockServer(); + registerTriggerLongRunningOperationTool(mockServer); + + const handler = handlers.get('trigger-long-running-operation')!; + await handler( + { duration: 0.1, steps: 1 }, + { _meta: { progressToken: 'token-generated' } } + ); + + expect(mockServer.server.notification).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'notifications/progress', + }), + expect.objectContaining({ + relatedRequestId: expect.stringMatching( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ + ), + }) + ); + }, 10000); }); describe('get-resource-links', () => { diff --git a/src/everything/tools/trigger-long-running-operation.ts b/src/everything/tools/trigger-long-running-operation.ts index 8af45ce60b..49c3bf4929 100644 --- a/src/everything/tools/trigger-long-running-operation.ts +++ b/src/everything/tools/trigger-long-running-operation.ts @@ -1,3 +1,4 @@ +import { randomUUID } from "node:crypto"; import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; @@ -42,6 +43,8 @@ export const registerTriggerLongRunningOperationTool = (server: McpServer) => { const { duration, steps } = validatedArgs; const stepDuration = duration / steps; const progressToken = extra._meta?.progressToken; + const requestId = extra.requestId ?? randomUUID(); + const operationId = randomUUID(); for (let i = 1; i < steps + 1; i++) { await new Promise((resolve) => @@ -58,7 +61,7 @@ export const registerTriggerLongRunningOperationTool = (server: McpServer) => { progressToken, }, }, - { relatedRequestId: extra.requestId } + { relatedRequestId: requestId } ); } } @@ -67,7 +70,7 @@ export const registerTriggerLongRunningOperationTool = (server: McpServer) => { content: [ { type: "text", - text: `Long running operation completed. Duration: ${duration} seconds, Steps: ${steps}.`, + text: `Long running operation completed. Operation ID: ${operationId}. Duration: ${duration} seconds, Steps: ${steps}.`, }, ], };