Suspected Critical Concurrency Bug in MCP Tool Call Handling: Only One Tool Call per Session Can Complete

Critical Concurrency Bug in MCP Tool Call Handling: Only One Tool Call per Session Can Complete

Description

There is a concurrency bug in the MCP integration (specifically in McpServer.ts under n8n/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger) that prevents multiple tool calls from being handled in parallel for the same session. This results in only one tool call being able to complete successfully; all others hang or fail.

Steps to Reproduce

  1. Set up an MCP client that issues multiple tool calls in parallel (or in rapid succession) using the same sessionId.
  2. Observe that only one of the tool calls completes; the others never resolve.

Technical Details

  • The McpServer class maintains a map called resolveFunctions where each sessionId maps to a single resolve function.
  • When a tool call is received, a new Promise is created and its resolve is stored as resolveFunctions[sessionId].
  • When the tool call completes, resolveFunctions[sessionId]() is called and then deleted.
  • If multiple tool calls are made in parallel for the same session, the resolveFunctions[sessionId] is overwritten by each new call. Only the last call’s resolver is retained.
  • When any tool call completes, it calls the latest resolver, not the one associated with its own Promise. The other Promises are never resolved, leading to a race condition.

Impact

  • Only the last tool call for a session can resolve; all others hang indefinitely.
  • This breaks workflows that rely on multiple parallel tool calls, causing unpredictable failures and hangs.

Expected Behavior

  • All tool calls for a session should be tracked independently and each should resolve its own Promise, regardless of how many are in flight.

Suggested Solution

Approach:

Instead of a queue, use a flat map that keys each resolver by a composite of sessionId and the unique messageId from the JSON-RPC protocol (e.g., ${sessionId}:${messageId}). Each tool call is tracked independently, and its resolver is retrieved and called using this composite key.

Pseudocode:


// Use a flat map with composite keys

private resolveFunctions: { [key: string]: CallableFunction } = {};

// When handling a new tool call:

const key = `${sessionId}:${messageId}`;

this.resolveFunctions[key] = resolve;

// When the tool call completes:

const key = `${sessionId}:${messageId}`;

const resolver = this.resolveFunctions[key];

if (resolver) resolver();

delete this.resolveFunctions[key];

Integration Points:

  • Extract messageId from the JSON-RPC message (parsedMessage.id).

  • In handlePostMessage, pass both sessionId and messageId when storing and retrieving the resolver.

  • In the tool call handler, use the composite key to resolve the correct Promise.

Pros:

  • True parallelism: Any number of concurrent tool calls per session are supported.

  • Order independence: Tool calls can complete in any order; each resolves its own Promise.

  • No queue management: No need to manage FIFO order or clean up empty queues.

  • Future-proof: Robust against high concurrency and out-of-order completion.

Cons:

  • Slightly more complex key management, but overall simpler and more robust for real-world concurrency.

References

  • File: n8n/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts
  • Methods: handlePostMessage, setRequestHandler(CallToolRequestSchema, ...)

Severity

High– This bug prevents correct operation of parallel tool calls and can break workflows in production.

1 Like