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
- Set up an MCP client that issues multiple tool calls in parallel (or in rapid succession) using the same
sessionId
. - Observe that only one of the tool calls completes; the others never resolve.
Technical Details
- The
McpServer
class maintains a map calledresolveFunctions
where eachsessionId
maps to a singleresolve
function. - When a tool call is received, a new Promise is created and its
resolve
is stored asresolveFunctions[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 bothsessionId
andmessageId
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.