Repository: n8n-io/n8n
Pull request #15064: feat(MCP Server Trigger Node): Handle multiple tool calls in mcp server trigger
Author: login: ggozad
Labels: name: n8n team
description: Authored by the n8n team
name: Released
Created: 2025-05-02T14:16:01Z | Merged: 2025-05-07T19:37:07Z

## Summary

Handle case of multiple tool calls in MCP Server trigger.

## Related Linear tickets, Github issues, and Community forum posts

Linear: https://linear.app/n8n/issue/AI-899/community-issue-mcp-trigger-isnt-able-to-execute-multiple-tools-at-the

Fixes #14777

## Review / Merge checklist

- [X] PR title and summary are descriptive. ([conventions](../blob/master/.github/pull_request_title_conventions.md)) <!--
   **Remember, the title automatically goes into the changelog.
   Use `(no-changelog)` otherwise.**
-->
- [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created.
- [X] Tests included. <!--
   A bug is not considered fixed, unless a test is added to prevent it from happening again.
   A feature is not complete without tests.
-->
- [ ] PR Labeled with `release/backport` (if the PR is an urgent fix that needs to be backported)


Linked issue #14777: n8n MCP trigger isn't able to execute multiple tools at the same time
### Bug Description

When i use mcp trigger in my n8n , and in your tools i put some "Call another workflow" tools , and call more than 1 at the same time, i get this error :

![Image](https://github.com/user-attachments/assets/30ef992b-e9e5-48c2-ba11-fbb3a1f8ff78)



1- tool :  

![Image](https://github.com/user-attachments/assets/84bb7902-2f1d-4d2b-814e-6a4ab8a24fdb)

2- tool : 

![Image](https://github.com/user-attachments/assets/071a9b72-78e4-4103-84e4-d17df4a44f8a)

3- tool

![Image](https://github.com/user-attachments/assets/155e5e35-bc65-4311-bd90-45935226b674)

So in above example, the first tool get an error , and the 2 a 3 tools works well... But in this case of the example , the tool number 1 actually executed, but still I got the error, and i belive that this error maybe interrupt the tool response to my agent, even the tool executing.

This error in print are the error that a get in Ai Agent , but for example when i try to search this executions in mcp trigger workflow i only found the tools that had no error. But when i got to Tools Worflows , i can found the execution that get's error, but it works fine ... i really don't know what it happens in this case .. 

![Image](https://github.com/user-attachments/assets/d9b113d9-6b37-40a3-a250-e75f8c687a04)


And in some cenarios even though it gives an error, it runs the tool, but sometimes not ... and i really don't know what to do , because i need multiple tool calling , but with this error is very unreliable.. Someone know what this is can be ?

Observations: i use n8n queue mode , but i try to use n8n regular mode and n8n cloud to test , and all of that get the same result.

### To Reproduce

all the infos are on description

### Expected behavior

all the infos are on description

### Operating System

ubuntu 22.04

### n8n Version

1.88.0

### Node.js Version

22.14.0

### Database

PostgreSQL

### Execution mode

queue
Linked issue comments: user: login: Joffcom
created_at: 2025-04-21T21:58:47Z
body: Hey @viniciusLOCampos,

We have created an internal ticket to look into this which we will be tracking as "GHC-1678"
author_association: MEMBER
user: login: GleisonEm
created_at: 2025-04-22T13:18:03Z
body: Hello @viniciusLOCampos @Joffcom , I noticed that this error happens when you try to queue mcp calls, if you do this synchronously, it can be solved for now

This image shows the call asynchronously, 3 times the same mcp server, got the same error as you, Only the first call is always successfully executed in this way

![ Imagem](https://github.com/user-attachments/assets/5a9b565f-a5a8-4c3c-9470-2d7f3f3d2a3b)

This image calls the mcp server with synchronization calls, we don't get an error

![ Imagem](https://github.com/user-attachments/assets/6c31f42d-1903-49b3-a083-ac9a4616e745)

Solution for me, for now

I have a n8n + traefik setup and I tried in every way to configure docker-compose so that we have only one instance of n8n that handles webhooks that contain "/mcp" but without success.

I managed to solve the problem in the agent prompt, here is what the original prompt was like before and with the error:

"When the `messageText` describes a clear expense (e.g.: "I spent X on Y", "I paid Z for W", "I bought A for B", "R$15 for lunch"). Consult the TOOL details in the AVAILABLE TOOLS section of the **`register_expense_tool`** tool and call the tool, after executing the tool, only if the tool execution is executed successfully, return something like "OK. Expense registered!" use emojis. If the tool execution had errors, send a message informing the user_name that requested it, use emojis."

I managed to solve it in two ways.

The most "complex" would be to create multiple instances of the mcp client tool and the tool used on the mcp server. For each replica of the mcp client tool, we use a different server tool because n8n reports an error if we have more than one mcp client tool using the same mcp server tool. With this slight adjustment to the prompt, I asked it to send each expense record to a different tool. The prompt was adjusted for solution 1:

"When the `messageText` describes a clear expense (e.g. "I spent X on Y", "I paid Z for W", "I bought A for B", "R$15 for lunch"). See the TOOL details in the AVAILABLE TOOLS section of the **`register_expense_tool`** tools and call the tool. If there is more than one expense record, send each one to a different register_expense_tool tool. You have 3 available. After executing the tool, only if the tool is executed execute successfully, return something like "OK. Expense registered!" use emojis. If the tool execution had errors, send a message informing the user_name that requested it, use emojis."

Solution 2 (simpler):

Leave only 1 mcp client tool and 1 mcp server tool as you should already be doing and define in the prompt that if there is more than one expense we will call the tools synchronously, waiting for one to finish to call it again with another expense record, with this we avoid the queue of mcp see requests in n8n and we have no problem, here is the prompt adjusted for this solution:

"When the `messageText` describes a clear expense (e.g.: "I spent X on Y", "I paid Z for W", "I bought A for B", "R$15 for lunch"). Consult the TOOL details in the AVAILABLE TOOLS section of the **`register_expense_tool`** tool and call the tool, if there is more than one expense record call the tool synchronously, wait for an expense to be recorded to call the tool again for the next one, after executing the tool, only if the execution of the tool is executed successfully, return something like "OK. Expense registered!" and the number of records inserted, use emojis. If the tool execution had errors, send a message informing the user_name that requested it, use emojis."

author_association: NONE
user: login: viniciusLOCampos
created_at: 2025-04-22T17:44:42Z
body: > Hello [@viniciusLOCampos](https://github.com/viniciusLOCampos) [@Joffcom](https://github.com/Joffcom) , I noticed that this error happens when you try to queue mcp calls, if you do this synchronously, it can be solved for now
> 
> This image shows the call asynchronously, 3 times the same mcp server, got the same error as you, Only the first call is always successfully executed in this way
> 
> ![ Imagem](https://github.com/user-attachments/assets/5a9b565f-a5a8-4c3c-9470-2d7f3f3d2a3b)
> 
> This image calls the mcp server with synchronization calls, we don't get an error
> 
> ![ Imagem](https://github.com/user-attachments/assets/6c31f42d-1903-49b3-a083-ac9a4616e745)
> 
> Solution for me, for now
> 
> I have a n8n + traefik setup and I tried in every way to configure docker-compose so that we have only one instance of n8n that handles webhooks that contain "/mcp" but without success.
> 
> I managed to solve the problem in the agent prompt, here is what the original prompt was like before and with the error:
> 
> "When the `messageText` describes a clear expense (e.g.: "I spent X on Y", "I paid Z for W", "I bought A for B", "R$15 for lunch"). Consult the TOOL details in the AVAILABLE TOOLS section of the **`register_expense_tool`** tool and call the tool, after executing the tool, only if the tool execution is executed successfully, return something like "OK. Expense registered!" use emojis. If the tool execution had errors, send a message informing the user_name that requested it, use emojis."
> 
> I managed to solve it in two ways.
> 
> The most "complex" would be to create multiple instances of the mcp client tool and the tool used on the mcp server. For each replica of the mcp client tool, we use a different server tool because n8n reports an error if we have more than one mcp client tool using the same mcp server tool. With this slight adjustment to the prompt, I asked it to send each expense record to a different tool. The prompt was adjusted for solution 1:
> 
> "When the `messageText` describes a clear expense (e.g. "I spent X on Y", "I paid Z for W", "I bought A for B", "R$15 for lunch"). See the TOOL details in the AVAILABLE TOOLS section of the **`register_expense_tool`** tools and call the tool. If there is more than one expense record, send each one to a different register_expense_tool tool. You have 3 available. After executing the tool, only if the tool is executed execute successfully, return something like "OK. Expense registered!" use emojis. If the tool execution had errors, send a message informing the user_name that requested it, use emojis."
> 
> Solution 2 (simpler):
> 
> Leave only 1 mcp client tool and 1 mcp server tool as you should already be doing and define in the prompt that if there is more than one expense we will call the tools synchronously, waiting for one to finish to call it again with another expense record, with this we avoid the queue of mcp see requests in n8n and we have no problem, here is the prompt adjusted for this solution:
> 
> "When the `messageText` describes a clear expense (e.g.: "I spent X on Y", "I paid Z for W", "I bought A for B", "R$15 for lunch"). Consult the TOOL details in the AVAILABLE TOOLS section of the **`register_expense_tool`** tool and call the tool, if there is more than one expense record call the tool synchronously, wait for an expense to be recorded to call the tool again for the next one, after executing the tool, only if the execution of the tool is executed successfully, return something like "OK. Expense registered!" and the number of records inserted, use emojis. If the tool execution had errors, send a message informing the user_name that requested it, use emojis."

Hmmm this solution to insert syncron tasks works for me ! 

But n8n mods , this is a alternative solution , i want that n8n solve this problem natively , because this isn't the normal behavior that we need. So if some mod can keep this bug post or create some internal task to solve natively I would appreciate
author_association: NONE
user: login: GleisonEm
created_at: 2025-04-22T18:20:16Z
body: > create

Really, it's a bug, there are several people with this problem, I've already found some, it's because it's something difficult to reproduce and maybe not the n8n team hasn't taken so much knowledge yet, I detailed the process to see if it helps them in something, let's wait for a resolution..
author_association: NONE
user: login: radamesbispo
created_at: 2025-04-23T18:28:00Z
body: when i try to send more than one  product to shopping cart tool I get this kind of error.... 
author_association: NONE
user: login: GleisonEm
created_at: 2025-04-23T19:34:05Z
body: > when i try to send more than one product to shopping cart tool I get this kind of error....

for now adjusting using prompt to force synchronous calls in tool, look comments above  
author_association: NONE
user: login: radamesbispo
created_at: 2025-04-26T16:52:12Z
body: > > when i try to send more than one product to shopping cart tool I get this kind of error....
> 
> for now adjusting using prompt to force synchronous calls in tool, look comments above

if I was using the openAi API not using the n8n node yes exist a parameter to force force synchronous, I mean if I code the system to perform certain way for sure it will work. if I only request in prompt, sometimes it works and sometimes not... the situation is a error by n8n implementation... so, I prefer not use the MCP in this situation and code another solution. maybe in future when this bug fix.... but thanks for suggestion and your time....
author_association: NONE
user: login: jeanpaul
created_at: 2025-04-30T09:35:03Z
body: Hi! thanks for the bug-report. We're aware of the issue, and have also identified a potential fix. I'm now waiting to see when the next release of the MCP SDK is coming out. A recent change there will make it much easier for us to fix this issue. If the next release takes too long, we'll manage it ourselves :)
author_association: CONTRIBUTOR

Diff:
diff --git a/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts b/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts
index 4c94fb0ed663a..34a6ed7803f40 100644
--- a/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts
+++ b/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts
@@ -1,7 +1,11 @@
 import type { Tool } from '@langchain/core/tools';
 import { Server } from '@modelcontextprotocol/sdk/server/index.js';
 import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
-import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
+import type {
+	JSONRPCMessage,
+	ServerRequest,
+	ServerNotification,
+} from '@modelcontextprotocol/sdk/types.js';
 import {
 	JSONRPCMessageSchema,
 	ListToolsRequestSchema,
@@ -33,6 +37,20 @@ function wasToolCall(body: string) {
 	}
 }
 
+/**
+ * Extracts the request ID from a JSONRPC message
+ * Returns undefined if the message doesn't have an ID or can't be parsed
+ */
+function getRequestId(body: string): string | undefined {
+	try {
+		const message: unknown = JSON.parse(body);
+		const parsedMessage: JSONRPCMessage = JSONRPCMessageSchema.parse(message);
+		return 'id' in parsedMessage ? String(parsedMessage.id) : undefined;
+	} catch {
+		return undefined;
+	}
+}
+
 export class McpServer {
 	servers: { [sessionId: string]: Server } = {};
 
@@ -42,7 +60,7 @@ export class McpServer {
 
 	private tools: { [sessionId: string]: Tool[] } = {};
 
-	private resolveFunctions: { [sessionId: string]: CallableFunction } = {};
+	private resolveFunctions: { [callId: string]: CallableFunction } = {};
 
 	constructor(logger: Logger) {
 		this.logger = logger;
@@ -59,7 +77,6 @@ export class McpServer {
 		resp.on('close', async () => {
 			this.logger.debug(`Deleting transport for ${sessionId}`);
 			delete this.tools[sessionId];
-			delete this.resolveFunctions[sessionId];
 			delete this.transports[sessionId];
 			delete this.servers[sessionId];
 		});
@@ -75,16 +92,25 @@ export class McpServer {
 	async handlePostMessage(req: express.Request, resp: CompressionResponse, connectedTools: Tool[]) {
 		const sessionId = req.query.sessionId as string;
 		const transport = this.transports[sessionId];
-		this.tools[sessionId] = connectedTools;
 		if (transport) {
 			// We need to add a promise here because the `handlePostMessage` will send something to the
 			// MCP Server, that will run in a different context. This means that the return will happen
 			// almost immediately, and will lead to marking the sub-node as "running" in the final execution
-			await new Promise(async (resolve) => {
-				this.resolveFunctions[sessionId] = resolve;
-				await transport.handlePostMessage(req, resp, req.rawBody.toString());
-			});
-			delete this.resolveFunctions[sessionId];
+			const bodyString = req.rawBody.toString();
+			const messageId = getRequestId(bodyString);
+
+			// Use session & message ID if available, otherwise fall back to sessionId
+			const callId = messageId ? `${sessionId}_${messageId}` : sessionId;
+			this.tools[sessionId] = connectedTools;
+
+			try {
+				await new Promise(async (resolve) => {
+					this.resolveFunctions[callId] = resolve;
+					await transport.handlePostMessage(req, resp, bodyString);
+				});
+			} finally {
+				delete this.resolveFunctions[callId];
+			}
 		} else {
 			this.logger.warn(`No transport found for session ${sessionId}`);
 			resp.status(401).send('No transport found for sessionId');
@@ -94,8 +120,6 @@ export class McpServer {
 			resp.flush();
 		}
 
-		delete this.tools[sessionId]; // Clean up to avoid keeping all tools in memory
-
 		return wasToolCall(req.rawBody.toString());
 	}
 
@@ -110,57 +134,68 @@ export class McpServer {
 			},
 		);
 
-		server.setRequestHandler(ListToolsRequestSchema, async (_, extra: RequestHandlerExtra) => {
-			if (!extra.sessionId) {
-				throw new OperationalError('Require a sessionId for the listing of tools');
-			}
-
-			return {
-				tools: this.tools[extra.sessionId].map((tool) => {
-					return {
-						name: tool.name,
-						description: tool.description,
-						// Allow additional properties on tool call input
-						inputSchema: zodToJsonSchema(tool.schema, { removeAdditionalStrategy: 'strict' }),
-					};
-				}),
-			};
-		});
-
-		server.setRequestHandler(CallToolRequestSchema, async (request, extra: RequestHandlerExtra) => {
-			if (!request.params?.name || !request.params?.arguments) {
-				throw new OperationalError('Require a name and arguments for the tool call');
-			}
-			if (!extra.sessionId) {
-				throw new OperationalError('Require a sessionId for the tool call');
-			}
-
-			const requestedTool: Tool | undefined = this.tools[extra.sessionId].find(
-				(tool) => tool.name === request.params.name,
-			);
-			if (!requestedTool) {
-				throw new OperationalError('Tool not found');
-			}
+		server.setRequestHandler(
+			ListToolsRequestSchema,
+			async (_, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
+				if (!extra.sessionId) {
+					throw new OperationalError('Require a sessionId for the listing of tools');
+				}
 
-			try {
-				const result = await requestedTool.invoke(request.params.arguments);
+				return {
+					tools: this.tools[extra.sessionId].map((tool) => {
+						return {
+							name: tool.name,
+							description: tool.description,
+							// Allow additional properties on tool call input
+							inputSchema: zodToJsonSchema(tool.schema, { removeAdditionalStrategy: 'strict' }),
+						};
+					}),
+				};
+			},
+		);
 
-				this.resolveFunctions[extra.sessionId]();
+		server.setRequestHandler(
+			CallToolRequestSchema,
+			async (request, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
+				if (!request.params?.name || !request.params?.arguments) {
+					throw new OperationalError('Require a name and arguments for the tool call');
+				}
+				if (!extra.sessionId) {
+					throw new OperationalError('Require a sessionId for the tool call');
+				}
 
-				this.logger.debug(`Got request for ${requestedTool.name}, and executed it.`);
+				const callId = extra.requestId ? `${extra.sessionId}_${extra.requestId}` : extra.sessionId;
 
-				if (typeof result === 'object') {
-					return { content: [{ type: 'text', text: JSON.stringify(result) }] };
+				const requestedTool: Tool | undefined = this.tools[extra.sessionId].find(
+					(tool) => tool.name === request.params.name,
+				);
+				if (!requestedTool) {
+					throw new OperationalError('Tool not found');
 				}
-				if (typeof result === 'string') {
-					return { content: [{ type: 'text', text: result }] };
+
+				try {
+					const result = await requestedTool.invoke(request.params.arguments);
+					if (this.resolveFunctions[callId]) {
+						this.resolveFunctions[callId]();
+					} else {
+						this.logger.warn(`No resolve function found for ${callId}`);
+					}
+
+					this.logger.debug(`Got request for ${requestedTool.name}, and executed it.`);
+
+					if (typeof result === 'object') {
+						return { content: [{ type: 'text', text: JSON.stringify(result) }] };
+					}
+					if (typeof result === 'string') {
+						return { content: [{ type: 'text', text: result }] };
+					}
+					return { content: [{ type: 'text', text: String(result) }] };
+				} catch (error) {
+					this.logger.error(`Error while executing Tool ${requestedTool.name}: ${error}`);
+					return { isError: true, content: [{ type: 'text', text: `Error: ${error.message}` }] };
 				}
-				return { content: [{ type: 'text', text: String(result) }] };
-			} catch (error) {
-				this.logger.error(`Error while executing Tool ${requestedTool.name}: ${error}`);
-				return { isError: true, content: [{ type: 'text', text: `Error: ${error.message}` }] };
-			}
-		});
+			},
+		);
 
 		server.onclose = () => {
 			this.logger.debug('Closing MCP Server');
diff --git a/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/__test__/McpServer.test.ts b/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/__test__/McpServer.test.ts
index beddc18fecdce..a698b2bd0322b 100644
--- a/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/__test__/McpServer.test.ts
+++ b/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/__test__/McpServer.test.ts
@@ -78,7 +78,7 @@ describe('McpServer', () => {
 		it('should call transport.handlePostMessage when transport exists', async () => {
 			mockTransport.handlePostMessage.mockImplementation(async () => {
 				// @ts-expect-error private property `resolveFunctions`
-				mcpServer.resolveFunctions[sessionId]();
+				mcpServer.resolveFunctions[`${sessionId}_123`]();
 			});
 
 			// Add the transport directly
@@ -110,6 +110,61 @@ describe('McpServer', () => {
 			expect(mockResponse.flush).toHaveBeenCalled();
 		});
 
+		it('should handle multiple tool calls with different ids', async () => {
+			const firstId = 123;
+			const secondId = 456;
+
+			mockTransport.handlePostMessage.mockImplementation(async () => {
+				const requestKey = mockRequest.rawBody?.toString().includes(`"id":${firstId}`)
+					? `${sessionId}_${firstId}`
+					: `${sessionId}_${secondId}`;
+				// @ts-expect-error private property `resolveFunctions`
+				mcpServer.resolveFunctions[requestKey]();
+			});
+
+			// Add the transport directly
+			mcpServer.transports[sessionId] = mockTransport;
+
+			// First tool call
+			mockRequest.rawBody = Buffer.from(
+				JSON.stringify({
+					jsonrpc: '2.0',
+					method: 'tools/call',
+					id: firstId,
+					params: { name: 'mockTool', arguments: { param: 'first call' } },
+				}),
+			);
+
+			// Handle first tool call
+			const firstResult = await mcpServer.handlePostMessage(mockRequest, mockResponse, [mockTool]);
+			expect(firstResult).toBe(true);
+			expect(mockTransport.handlePostMessage).toHaveBeenCalledWith(
+				mockRequest,
+				mockResponse,
+				expect.any(String),
+			);
+
+			// Second tool call with different id
+			mockRequest.rawBody = Buffer.from(
+				JSON.stringify({
+					jsonrpc: '2.0',
+					method: 'tools/call',
+					id: secondId,
+					params: { name: 'mockTool', arguments: { param: 'second call' } },
+				}),
+			);
+
+			// Handle second tool call
+			const secondResult = await mcpServer.handlePostMessage(mockRequest, mockResponse, [mockTool]);
+			expect(secondResult).toBe(true);
+
+			// Verify transport's handlePostMessage was called twice
+			expect(mockTransport.handlePostMessage).toHaveBeenCalledTimes(2);
+
+			// Verify flush was called for both requests
+			expect(mockResponse.flush).toHaveBeenCalledTimes(2);
+		});
+
 		it('should return 401 when transport does not exist', async () => {
 			// Call without setting up transport
 			await mcpServer.handlePostMessage(mockRequest, mockResponse, [mockTool]);
diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json
index 5424f84612409..d174db66eac09 100644
--- a/packages/@n8n/nodes-langchain/package.json
+++ b/packages/@n8n/nodes-langchain/package.json
@@ -173,7 +173,7 @@
     "@langchain/qdrant": "0.1.1",
     "@langchain/redis": "0.1.0",
     "@langchain/textsplitters": "0.1.0",
-    "@modelcontextprotocol/sdk": "1.9.0",
+    "@modelcontextprotocol/sdk": "1.11.0",
     "@mozilla/readability": "0.6.0",
     "@n8n/client-oauth2": "workspace:*",
     "@n8n/json-schema-to-zod": "workspace:*",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4ac988f344760..0553ff299da1f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -755,8 +755,8 @@ importers:
         specifier: 0.1.0
         version: 0.1.0(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))
       '@modelcontextprotocol/sdk':
-        specifier: 1.9.0
-        version: 1.9.0
+        specifier: 1.11.0
+        version: 1.11.0
       '@mozilla/readability':
         specifier: 0.6.0
         version: 0.6.0
@@ -4775,8 +4775,8 @@ packages:
     peerDependencies:
       zod: '>= 3'
 
-  '@modelcontextprotocol/sdk@1.9.0':
-    resolution: {integrity: sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==}
+  '@modelcontextprotocol/sdk@1.11.0':
+    resolution: {integrity: sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==}
     engines: {node: '>=18'}
 
   '@mongodb-js/saslprep@1.1.9':
@@ -17207,7 +17207,7 @@ snapshots:
     dependencies:
       zod: 3.24.1
 
-  '@modelcontextprotocol/sdk@1.9.0':
+  '@modelcontextprotocol/sdk@1.11.0':
     dependencies:
       content-type: 1.0.5
       cors: 2.8.5


Reviews:
user: login: cubic-dev-ai[bot]
body: mrge found 3 issues across 3 files. View them in [mrge.io](https://mrge.io/pr/n8n-io/n8n/pull/15064)
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T14:18:30Z
user: login: jeanpaul
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:30:25Z
user: login: OlegIvaniv
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T11:01:36Z
user: login: jeanpaul
body: Looks good 🚀 
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:52:53Z
user: login: jeanpaul
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:53:56Z
user: login: ggozad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:39:58Z
user: login: jeanpaul
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T16:06:11Z

Review comments:
diff_hunk: @@ -110,57 +133,65 @@ export class McpServer {
 			},
 		);
 
-		server.setRequestHandler(ListToolsRequestSchema, async (_, extra: RequestHandlerExtra) => {
-			if (!extra.sessionId) {
-				throw new OperationalError('Require a sessionId for the listing of tools');
-			}
-
-			return {
-				tools: this.tools[extra.sessionId].map((tool) => {
-					return {
-						name: tool.name,
-						description: tool.description,
-						// Allow additional properties on tool call input
-						inputSchema: zodToJsonSchema(tool.schema, { removeAdditionalStrategy: 'strict' }),
-					};
-				}),
-			};
-		});
+		server.setRequestHandler(
+			ListToolsRequestSchema,
+			async (_, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
+				if (!extra.sessionId) {
+					throw new OperationalError('Require a sessionId for the listing of tools');
+				}
 
-		server.setRequestHandler(CallToolRequestSchema, async (request, extra: RequestHandlerExtra) => {
-			if (!request.params?.name || !request.params?.arguments) {
-				throw new OperationalError('Require a name and arguments for the tool call');
-			}
-			if (!extra.sessionId) {
-				throw new OperationalError('Require a sessionId for the tool call');
-			}
+				return {
+					tools: this.tools[extra.sessionId].map((tool) => {
+						return {
+							name: tool.name,
+							description: tool.description,
+							// Allow additional properties on tool call input
+							inputSchema: zodToJsonSchema(tool.schema, { removeAdditionalStrategy: 'strict' }),
+						};
+					}),
+				};
+			},
+		);
 
-			const requestedTool: Tool | undefined = this.tools[extra.sessionId].find(
-				(tool) => tool.name === request.params.name,
-			);
-			if (!requestedTool) {
-				throw new OperationalError('Tool not found');
-			}
+		server.setRequestHandler(
+			CallToolRequestSchema,
+			async (request, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
+				if (!request.params?.name || !request.params?.arguments) {
+					throw new OperationalError('Require a name and arguments for the tool call');
+				}
+				if (!extra.sessionId) {
+					throw new OperationalError('Require a sessionId for the tool call');
+				}
 
-			try {
-				const result = await requestedTool.invoke(request.params.arguments);
+				const callId = extra.requestId ? `${extra.sessionId}_${extra.requestId}` : extra.sessionId;
 
-				this.resolveFunctions[extra.sessionId]();
+				const requestedTool: Tool | undefined = this.tools[callId].find(
path: packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts
user: login: cubic-dev-ai[bot]
body: <!-- metadata:{"confidence":9,"steps":[{"text":"I'll review the changed lines in the PR to identify any issues. Let me first understand what this code is doing and the purpose of the changes.","toolCalls":[{"args":{"command":"grep -r \"MCP\" --include=\"*.ts\" packages/@n8n/nodes-langchain/nodes/mcp/ | head -n 10"},"toolName":"executeCommand"}]},{"text":"Let me look at the McpTrigger node to understand its purpose:","toolCalls":[{"args":{"pattern":"packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/*.ts"},"toolName":"listFiles"}]},{"text":"Let me check the McpTrigger node to understand the context better:","toolCalls":[{"args":{"command":"grep -A 10 \"description\" packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpTrigger.node.ts"},"toolName":"executeCommand"}]},{"text":"Now I understand the context better. This is a node that exposes n8n tools as an MCP (Model Context Protocol) Server endpoint. The PR is addressing an issue where the server wasn't able to handle multiple tool calls.\n\nLet me analyze the changes in detail:","toolCalls":[{"args":{"command":"grep -r \"RequestHandlerExtra\" --include=\"*.ts\" packages/@n8n/nodes-langchain/"},"toolName":"executeCommand"}]},{"text":"Now let me review the changes for potential issues:\n\n1. The main change is to support multiple tool calls by using a combination of sessionId and requestId as a unique identifier (callId) instead of just sessionId.\n2. A new function `getRequestId` has been added to extract the request ID from a JSONRPC message.\n3. The `resolveFunctions` and `tools` objects now use callId instead of just sessionId.\n4. The code now cleans up tools after each call to avoid memory leaks.\n\nLet me check for any issues in these changes:","toolCalls":[{"args":{"command":"grep -r \"extra.requestId\" --include=\"*.ts\" packages/@n8n/nodes-langchain/"},"toolName":"executeCommand"}]},{"text":"Let me check the type definition for RequestHandlerExtra to confirm it has a requestId property:","toolCalls":[{"args":{"command":"grep -A 10 \"RequestHandlerExtra\" node_modules/@modelcontextprotocol/sdk/shared/protocol.d.ts 2>/dev/null || echo \"File not found\""},"toolName":"executeCommand"}]}]} -->
Potential TypeError if this.tools[callId] is undefined
created_at: 2025-05-02T14:18:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,57 +133,65 @@
 			},
 		);
 
-		server.setRequestHandler(ListToolsRequestSchema, async (_, extra: RequestHandlerExtra) => {
-			if (!extra.sessionId) {
-				throw new OperationalError('Require a sessionId for the listing of tools');
-			}
-
-			return {
-				tools: this.tools[extra.sessionId].map((tool) => {
-					return {
-						name: tool.name,
-						description: tool.description,
-						// Allow additional properties on tool call input
-						inputSchema: zodToJsonSchema(tool.schema, { removeAdditionalStrategy: 'strict' }),
-					};
-				}),
-			};
-		});
+		server.setRequestHandler(
+			ListToolsRequestSchema,
+			async (_, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
+				if (!extra.sessionId) {
+					throw new OperationalError('Require a sessionId for the listing of tools');
+				}
 
-		server.setRequestHandler(CallToolRequestSchema, async (request, extra: RequestHandlerExtra) => {
-			if (!request.params?.name || !request.params?.arguments) {
-				throw new OperationalError('Require a name and arguments for the tool call');
-			}
-			if (!extra.sessionId) {
-				throw new OperationalError('Require a sessionId for the tool call');
-			}
+				return {
+					tools: this.tools[extra.sessionId].map((tool) => {
+						return {
+							name: tool.name,
+							description: tool.description,
+							// Allow additional properties on tool call input
+							inputSchema: zodToJsonSchema(tool.schema, { removeAdditionalStrategy: 'strict' }),
+						};
+					}),
+				};
+			},
+		);
 
-			const requestedTool: Tool | undefined = this.tools[extra.sessionId].find(
-				(tool) => tool.name === request.params.name,
-			);
-			if (!requestedTool) {
-				throw new OperationalError('Tool not found');
-			}
+		server.setRequestHandler(
+			CallToolRequestSchema,
+			async (request, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
+				if (!request.params?.name || !request.params?.arguments) {
+					throw new OperationalError('Require a name and arguments for the tool call');
+				}
+				if (!extra.sessionId) {
+					throw new OperationalError('Require a sessionId for the tool call');
+				}
 
-			try {
-				const result = await requestedTool.invoke(request.params.arguments);
+				const callId = extra.requestId ? `${extra.sessionId}_${extra.requestId}` : extra.sessionId;
 
-				this.resolveFunctions[extra.sessionId]();
+				const requestedTool: Tool | undefined = this.tools[callId].find(
+					(tool) => tool.name === request.params.name,
+				);
+				if (!requestedTool) {
+					throw new OperationalError('Tool not found');
+				}
 
-				this.logger.debug(`Got request for ${requestedTool.name}, and executed it.`);
+				try {
+					const result = await requestedTool.invoke(request.params.arguments);
 
-				if (typeof result === 'object') {
-					return { content: [{ type: 'text', text: JSON.stringify(result) }] };
-				}
-				if (typeof result === 'string') {
-					return { content: [{ type: 'text', text: result }] };
+					this.resolveFunctions[callId]();
path: packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts
user: login: cubic-dev-ai[bot]
body: <!-- metadata:{"confidence":9,"steps":[{"text":"I'll review the changed lines in the PR to identify any issues. Let me first understand what this code is doing and the purpose of the changes.","toolCalls":[{"args":{"command":"grep -r \"MCP\" --include=\"*.ts\" packages/@n8n/nodes-langchain/nodes/mcp/ | head -n 10"},"toolName":"executeCommand"}]},{"text":"Let me look at the McpTrigger node to understand its purpose:","toolCalls":[{"args":{"pattern":"packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/*.ts"},"toolName":"listFiles"}]},{"text":"Let me check the McpTrigger node to understand the context better:","toolCalls":[{"args":{"command":"grep -A 10 \"description\" packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpTrigger.node.ts"},"toolName":"executeCommand"}]},{"text":"Now I understand the context better. This is a node that exposes n8n tools as an MCP (Model Context Protocol) Server endpoint. The PR is addressing an issue where the server wasn't able to handle multiple tool calls.\n\nLet me analyze the changes in detail:","toolCalls":[{"args":{"command":"grep -r \"RequestHandlerExtra\" --include=\"*.ts\" packages/@n8n/nodes-langchain/"},"toolName":"executeCommand"}]},{"text":"Now let me review the changes for potential issues:\n\n1. The main change is to support multiple tool calls by using a combination of sessionId and requestId as a unique identifier (callId) instead of just sessionId.\n2. A new function `getRequestId` has been added to extract the request ID from a JSONRPC message.\n3. The `resolveFunctions` and `tools` objects now use callId instead of just sessionId.\n4. The code now cleans up tools after each call to avoid memory leaks.\n\nLet me check for any issues in these changes:","toolCalls":[{"args":{"command":"grep -r \"extra.requestId\" --include=\"*.ts\" packages/@n8n/nodes-langchain/"},"toolName":"executeCommand"}]},{"text":"Let me check the type definition for RequestHandlerExtra to confirm it has a requestId property:","toolCalls":[{"args":{"command":"grep -A 10 \"RequestHandlerExtra\" node_modules/@modelcontextprotocol/sdk/shared/protocol.d.ts 2>/dev/null || echo \"File not found\""},"toolName":"executeCommand"}]}]} -->
Potential TypeError if this.resolveFunctions[callId] is undefined
created_at: 2025-05-02T14:18:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,6 +110,60 @@ describe('McpServer', () => {
 			expect(mockResponse.flush).toHaveBeenCalled();
 		});
 
+		it('should handle multiple tool calls with different ids', async () => {
+			const firstId = 123;
+			const secondId = 456;
+
+			mockTransport.handlePostMessage.mockImplementation(async () => {
+				const requestKey = mockRequest.rawBody?.toString().includes(`"id":${firstId}`)
path: packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/__test__/McpServer.test.ts
user: login: cubic-dev-ai[bot]
body: <!-- metadata:{"confidence":8,"steps":[{"text":"I'll review the changed code in the PR, focusing on the lines that were added or modified. Let me first understand what the code is doing by examining the changes.","toolCalls":[{"args":{"pattern":"packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/**/*.ts"},"toolName":"listFiles"}]},{"text":"Let me look at the McpServer.ts file to understand the implementation:","toolCalls":[{"args":{"command":"cat packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts"},"toolName":"executeCommand"}]},{"text":"Now I understand the context better. Let me analyze the changes in the PR:\n\n1. The first change is modifying the test to use `mcpServer.resolveFunctions[`${sessionId}_123`]()` instead of `mcpServer.resolveFunctions[sessionId]()`.\n\n2. The second change is adding a new test case that verifies the server can handle multiple tool calls with different IDs.\n\nLet's check if there are any issues with these changes:","toolCalls":[{"args":{"command":"grep -n \"resolveFunctions\" packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts"},"toolName":"executeCommand"}]},{"text":"","toolCalls":[{"args":{"command":"grep -n \"callId\" packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts"},"toolName":"executeCommand"}]},{"text":"After reviewing the code and the changes, I can see that the PR is addressing a bug where the MCP Server wasn't properly handling multiple tool calls with different IDs. The changes in the test file reflect the correct behavior that should be happening in the implementation.\n\nThe key change is that the test now correctly uses a composite key (`${sessionId}_${id}`) for the `resolveFunctions` object, which matches how the implementation in `McpServer.ts` works. This allows multiple tool calls to be tracked independently.\n\nLet me check if there are any issues with the implementation:","toolCalls":[{"args":{"command":"grep -n \"delete this.resolveFunctions\" packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts"},"toolName":"executeCommand"}]},{"text":"I notice that in the `connectTransport` method, there's a cleanup that uses `delete this.resolveFunctions[sessionId]`, but the actual storage now uses `${sessionId}_${messageId}` as the key. Let me check if this could be an issue:","toolCalls":[{"args":{"command":"grep -n -A 10 \"resp.on('close'\" packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts"},"toolName":"executeCommand"}]}]} -->
Optional chaining on rawBody could lead to runtime errors if rawBody is undefined
created_at: 2025-05-02T14:18:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,57 +133,69 @@ export class McpServer {
 			},
 		);
 
-		server.setRequestHandler(ListToolsRequestSchema, async (_, extra: RequestHandlerExtra) => {
-			if (!extra.sessionId) {
-				throw new OperationalError('Require a sessionId for the listing of tools');
-			}
-
-			return {
-				tools: this.tools[extra.sessionId].map((tool) => {
-					return {
-						name: tool.name,
-						description: tool.description,
-						// Allow additional properties on tool call input
-						inputSchema: zodToJsonSchema(tool.schema, { removeAdditionalStrategy: 'strict' }),
-					};
-				}),
-			};
-		});
-
-		server.setRequestHandler(CallToolRequestSchema, async (request, extra: RequestHandlerExtra) => {
-			if (!request.params?.name || !request.params?.arguments) {
-				throw new OperationalError('Require a name and arguments for the tool call');
-			}
-			if (!extra.sessionId) {
-				throw new OperationalError('Require a sessionId for the tool call');
-			}
+		server.setRequestHandler(
+			ListToolsRequestSchema,
+			async (_, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
+				if (!extra.sessionId) {
+					throw new OperationalError('Require a sessionId for the listing of tools');
+				}
 
-			const requestedTool: Tool | undefined = this.tools[extra.sessionId].find(
-				(tool) => tool.name === request.params.name,
-			);
-			if (!requestedTool) {
-				throw new OperationalError('Tool not found');
-			}
+				return {
+					tools: this.tools[extra.sessionId].map((tool) => {
path: packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts
user: login: jeanpaul
body: We should use `sessionId` in tools -- it's now returning some tools that are attached to `sessionId` (potentially in a message that's about initialisation), but `requestId` is always different. However, above this, you use `callId` to attach tools. Can you please revert that?
created_at: 2025-05-02T15:17:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -75,16 +93,23 @@ export class McpServer {
 	async handlePostMessage(req: express.Request, resp: CompressionResponse, connectedTools: Tool[]) {
 		const sessionId = req.query.sessionId as string;
 		const transport = this.transports[sessionId];
-		this.tools[sessionId] = connectedTools;
 		if (transport) {
 			// We need to add a promise here because the `handlePostMessage` will send something to the
 			// MCP Server, that will run in a different context. This means that the return will happen
 			// almost immediately, and will lead to marking the sub-node as "running" in the final execution
+			const bodyString = req.rawBody.toString();
+			const messageId = getRequestId(bodyString);
+
+			// Use session & message ID if available, otherwise fall back to sessionId
+			const callId = messageId ? `${sessionId}_${messageId}` : sessionId;
+			this.tools[callId] = connectedTools;
path: packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts
user: login: jeanpaul
body: `this.tools` should use `sessionId` (see my other comment)
created_at: 2025-05-02T15:26:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,57 +133,65 @@
 			},
 		);
 
-		server.setRequestHandler(ListToolsRequestSchema, async (_, extra: RequestHandlerExtra) => {
-			if (!extra.sessionId) {
-				throw new OperationalError('Require a sessionId for the listing of tools');
-			}
-
-			return {
-				tools: this.tools[extra.sessionId].map((tool) => {
-					return {
-						name: tool.name,
-						description: tool.description,
-						// Allow additional properties on tool call input
-						inputSchema: zodToJsonSchema(tool.schema, { removeAdditionalStrategy: 'strict' }),
-					};
-				}),
-			};
-		});
+		server.setRequestHandler(
+			ListToolsRequestSchema,
+			async (_, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
+				if (!extra.sessionId) {
+					throw new OperationalError('Require a sessionId for the listing of tools');
+				}
 
-		server.setRequestHandler(CallToolRequestSchema, async (request, extra: RequestHandlerExtra) => {
-			if (!request.params?.name || !request.params?.arguments) {
-				throw new OperationalError('Require a name and arguments for the tool call');
-			}
-			if (!extra.sessionId) {
-				throw new OperationalError('Require a sessionId for the tool call');
-			}
+				return {
+					tools: this.tools[extra.sessionId].map((tool) => {
+						return {
+							name: tool.name,
+							description: tool.description,
+							// Allow additional properties on tool call input
+							inputSchema: zodToJsonSchema(tool.schema, { removeAdditionalStrategy: 'strict' }),
+						};
+					}),
+				};
+			},
+		);
 
-			const requestedTool: Tool | undefined = this.tools[extra.sessionId].find(
-				(tool) => tool.name === request.params.name,
-			);
-			if (!requestedTool) {
-				throw new OperationalError('Tool not found');
-			}
+		server.setRequestHandler(
+			CallToolRequestSchema,
+			async (request, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
+				if (!request.params?.name || !request.params?.arguments) {
+					throw new OperationalError('Require a name and arguments for the tool call');
+				}
+				if (!extra.sessionId) {
+					throw new OperationalError('Require a sessionId for the tool call');
+				}
 
-			try {
-				const result = await requestedTool.invoke(request.params.arguments);
+				const callId = extra.requestId ? `${extra.sessionId}_${extra.requestId}` : extra.sessionId;
 
-				this.resolveFunctions[extra.sessionId]();
+				const requestedTool: Tool | undefined = this.tools[callId].find(
+					(tool) => tool.name === request.params.name,
+				);
+				if (!requestedTool) {
+					throw new OperationalError('Tool not found');
+				}
 
-				this.logger.debug(`Got request for ${requestedTool.name}, and executed it.`);
+				try {
+					const result = await requestedTool.invoke(request.params.arguments);
 
-				if (typeof result === 'object') {
-					return { content: [{ type: 'text', text: JSON.stringify(result) }] };
-				}
-				if (typeof result === 'string') {
-					return { content: [{ type: 'text', text: result }] };
+					this.resolveFunctions[callId]();
path: packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts
user: login: jeanpaul
body: I agree with this comment! We should probably have a conditional resolve, and if it's not found, don't resolve it? maybe print a warning...
created_at: 2025-05-02T15:28:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -75,16 +93,23 @@ export class McpServer {
 	async handlePostMessage(req: express.Request, resp: CompressionResponse, connectedTools: Tool[]) {
 		const sessionId = req.query.sessionId as string;
 		const transport = this.transports[sessionId];
-		this.tools[sessionId] = connectedTools;
 		if (transport) {
 			// We need to add a promise here because the `handlePostMessage` will send something to the
 			// MCP Server, that will run in a different context. This means that the return will happen
 			// almost immediately, and will lead to marking the sub-node as "running" in the final execution
+			const bodyString = req.rawBody.toString();
+			const messageId = getRequestId(bodyString);
+
+			// Use session & message ID if available, otherwise fall back to sessionId
+			const callId = messageId ? `${sessionId}_${messageId}` : sessionId;
+			this.tools[sessionId] = connectedTools;
+
 			await new Promise(async (resolve) => {
-				this.resolveFunctions[sessionId] = resolve;
-				await transport.handlePostMessage(req, resp, req.rawBody.toString());
+				this.resolveFunctions[callId] = resolve;
+				await transport.handlePostMessage(req, resp, bodyString);
 			});
-			delete this.resolveFunctions[sessionId];
+			delete this.resolveFunctions[callId];
+			delete this.tools[sessionId]; // Clean up to avoid keeping all tools in memory
path: packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts
user: login: jeanpaul
body: Only thing is... Should we do this only on clean-up of the session?
created_at: 2025-05-06T08:53:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -75,16 +93,23 @@ export class McpServer {
 	async handlePostMessage(req: express.Request, resp: CompressionResponse, connectedTools: Tool[]) {
 		const sessionId = req.query.sessionId as string;
 		const transport = this.transports[sessionId];
-		this.tools[sessionId] = connectedTools;
 		if (transport) {
 			// We need to add a promise here because the `handlePostMessage` will send something to the
 			// MCP Server, that will run in a different context. This means that the return will happen
 			// almost immediately, and will lead to marking the sub-node as "running" in the final execution
+			const bodyString = req.rawBody.toString();
+			const messageId = getRequestId(bodyString);
+
+			// Use session & message ID if available, otherwise fall back to sessionId
+			const callId = messageId ? `${sessionId}_${messageId}` : sessionId;
+			this.tools[sessionId] = connectedTools;
+
 			await new Promise(async (resolve) => {
-				this.resolveFunctions[sessionId] = resolve;
-				await transport.handlePostMessage(req, resp, req.rawBody.toString());
+				this.resolveFunctions[callId] = resolve;
+				await transport.handlePostMessage(req, resp, bodyString);
 			});
-			delete this.resolveFunctions[sessionId];
+			delete this.resolveFunctions[callId];
+			delete this.tools[sessionId]; // Clean up to avoid keeping all tools in memory
path: packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts
user: login: ggozad
body: Yep. Added a `try/finally` when cleaning the `resolveFunctions` to make sure we always clean them up.
created_at: 2025-05-07T14:39:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-05-02T15:59:45Z
body: ## [Codecov](https://app.codecov.io/gh/n8n-io/n8n/pull/15064?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) Report
Attention: Patch coverage is `31.91489%` with `32 lines` in your changes missing coverage. Please review.
| [Files with missing lines](https://app.codecov.io/gh/n8n-io/n8n/pull/15064?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) | Patch % | Lines |
|---|---|---|
| [.../nodes-langchain/nodes/mcp/McpTrigger/McpServer.ts](https://app.codecov.io/gh/n8n-io/n8n/pull/15064?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io#diff-cGFja2FnZXMvQG44bi9ub2Rlcy1sYW5nY2hhaW4vbm9kZXMvbWNwL01jcFRyaWdnZXIvTWNwU2VydmVyLnRz) | 31.91% | [30 Missing and 2 partials :warning: ](https://app.codecov.io/gh/n8n-io/n8n/pull/15064?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) |

:loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)
author_association: NONE
user: login: ggozad
created_at: 2025-05-05T09:46:24Z
body: This has been tested with 3 tools with a client based on a month-old MCP python SDK as well as the latest.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-05T11:10:31Z
body: :white_check_mark: All Cypress E2E specs passed
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-06T09:01:41Z
body: :white_check_mark: All Cypress E2E specs passed
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-07T16:16:05Z
body: :white_check_mark: All Cypress E2E specs passed
author_association: CONTRIBUTOR
user: login: janober
created_at: 2025-05-12T16:08:16Z
body: Got released with [`n8n@1.93.0`](https://github.com/n8n-io/n8n/releases/tag/n8n%401.93.0)
author_association: MEMBER