feat(MCP Server Trigger Node): Support for Streamable HTTP transport in MCP Server (#15833)

This commit is contained in:
Yiorgis Gozadinos
2025-06-13 09:33:39 +02:00
committed by GitHub
parent eff0571f42
commit 8d6e796b92
10 changed files with 567 additions and 138 deletions

View File

@@ -5,7 +5,7 @@ import { NodeConnectionTypes, Node } from 'n8n-workflow';
import { getConnectedTools, nodeNameToToolName } from '@utils/helpers';
import type { CompressionResponse } from './FlushingSSEServerTransport';
import type { CompressionResponse } from './FlushingTransport';
import { McpServerManager } from './McpServer';
const MCP_SSE_SETUP_PATH = 'sse';
@@ -20,9 +20,10 @@ export class McpTrigger extends Node {
dark: 'file:../mcp.dark.svg',
},
group: ['trigger'],
version: [1, 1.1],
version: [1, 1.1, 2],
description: 'Expose n8n tools as an MCP Server endpoint',
activationMessage: 'You can now connect your MCP Clients to the SSE URL.',
activationMessage:
'You can now connect your MCP Clients to the URL, using SSE or Streamable HTTP transports.',
defaults: {
name: 'MCP Server Trigger',
},
@@ -109,7 +110,7 @@ export class McpTrigger extends Node {
httpMethod: 'GET',
responseMode: 'onReceived',
isFullPath: true,
path: `={{$parameter["path"]}}/${MCP_SSE_SETUP_PATH}`,
path: `={{$parameter["path"]}}{{parseFloat($nodeVersion)<2 ? '/${MCP_SSE_SETUP_PATH}' : ''}}`,
nodeType: 'mcp',
ndvHideMethod: true,
ndvHideUrl: false,
@@ -119,7 +120,7 @@ export class McpTrigger extends Node {
httpMethod: 'POST',
responseMode: 'onReceived',
isFullPath: true,
path: `={{$parameter["path"]}}/${MCP_SSE_MESSAGES_PATH}`,
path: `={{$parameter["path"]}}{{parseFloat($nodeVersion)<2 ? '/${MCP_SSE_MESSAGES_PATH}' : ''}}`,
nodeType: 'mcp',
ndvHideMethod: true,
ndvHideUrl: true,
@@ -151,22 +152,32 @@ export class McpTrigger extends Node {
if (webhookName === 'setup') {
// Sets up the transport and opens the long-lived connection. This resp
// will stay streaming, and is the channel that sends the events
const postUrl = req.path.replace(
new RegExp(`/${MCP_SSE_SETUP_PATH}$`),
`/${MCP_SSE_MESSAGES_PATH}`,
);
await mcpServerManager.createServerAndTransport(serverName, postUrl, resp);
// Prior to version 2.0, we use different paths for the setup and messages.
const postUrl =
node.typeVersion < 2
? req.path.replace(new RegExp(`/${MCP_SSE_SETUP_PATH}$`), `/${MCP_SSE_MESSAGES_PATH}`)
: req.path;
await mcpServerManager.createServerWithSSETransport(serverName, postUrl, resp);
return { noWebhookResponse: true };
} else if (webhookName === 'default') {
// This is the command-channel, and is actually executing the tools. This
// sends the response back through the long-lived connection setup in the
// 'setup' call
const connectedTools = await getConnectedTools(context, true);
// Here we handle POST requests. These can be either
// 1) Client calls in an established session using the SSE transport, or
// 2) Client calls in an established session using the StreamableHTTPServerTransport
// 3) Session setup requests using the StreamableHTTPServerTransport
const wasToolCall = await mcpServerManager.handlePostMessage(req, resp, connectedTools);
if (wasToolCall) return { noWebhookResponse: true, workflowData: [[{ json: {} }]] };
// Check if there is a session and a transport is already established
const sessionId = mcpServerManager.getSessionId(req);
if (sessionId && mcpServerManager.getTransport(sessionId)) {
const connectedTools = await getConnectedTools(context, true);
const wasToolCall = await mcpServerManager.handlePostMessage(req, resp, connectedTools);
if (wasToolCall) return { noWebhookResponse: true, workflowData: [[{ json: {} }]] };
} else {
// If no session is established, this is a setup request
// for the StreamableHTTPServerTransport, so we create a new transport
await mcpServerManager.createServerWithStreamableHTTPTransport(serverName, resp, req);
}
return { noWebhookResponse: true };
}