feat(MCP Client Tool Node): Add support for HTTP Streamable Transport (#15454)

This commit is contained in:
KGuillaume-chaps
2025-07-18 13:27:21 +02:00
committed by GitHub
parent 6fd45eb10d
commit a5d14a2509
4 changed files with 102 additions and 23 deletions

View File

@@ -11,7 +11,7 @@ import { logWrapper } from '@utils/logWrapper';
import { getConnectionHintNoticeField } from '@utils/sharedFields';
import { getTools } from './loadOptions';
import type { McpAuthenticationOption, McpToolIncludeMode } from './types';
import type { McpServerTransport, McpAuthenticationOption, McpToolIncludeMode } from './types';
import {
connectMcpClient,
createCallTool,
@@ -31,7 +31,7 @@ export class McpClientTool implements INodeType {
dark: 'file:../mcp.dark.svg',
},
group: ['output'],
version: 1,
version: [1, 1.1],
description: 'Connect tools from an MCP Server',
defaults: {
name: 'MCP Client',
@@ -83,6 +83,47 @@ export class McpClientTool implements INodeType {
placeholder: 'e.g. https://my-mcp-server.ai/sse',
default: '',
required: true,
displayOptions: {
show: {
'@version': [1],
},
},
},
{
displayName: 'Endpoint',
name: 'endpointUrl',
type: 'string',
description: 'Endpoint of your MCP server',
placeholder: 'e.g. https://my-mcp-server.ai/mcp',
default: '',
required: true,
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 1.1 } }],
},
},
},
{
displayName: 'Server Transport',
name: 'serverTransport',
type: 'options',
options: [
{
name: 'Server Sent Events (Deprecated)',
value: 'sse',
},
{
name: 'HTTP Streamable',
value: 'httpStreamable',
},
],
default: 'sse',
description: 'The transport used by your endpoint',
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 1.1 } }],
},
},
},
{
displayName: 'Authentication',
@@ -103,7 +144,7 @@ export class McpClientTool implements INodeType {
},
],
default: 'none',
description: 'The way to authenticate with your SSE endpoint',
description: 'The way to authenticate with your endpoint',
},
{
displayName: 'Credentials',
@@ -187,11 +228,22 @@ export class McpClientTool implements INodeType {
'authentication',
itemIndex,
) as McpAuthenticationOption;
const sseEndpoint = this.getNodeParameter('sseEndpoint', itemIndex) as string;
const node = this.getNode();
let serverTransport: McpServerTransport;
let endpointUrl: string;
if (node.typeVersion === 1) {
serverTransport = 'sse';
endpointUrl = this.getNodeParameter('sseEndpoint', itemIndex) as string;
} else {
serverTransport = this.getNodeParameter('serverTransport', itemIndex) as McpServerTransport;
endpointUrl = this.getNodeParameter('endpointUrl', itemIndex) as string;
}
const { headers } = await getAuthHeaders(this, authentication);
const client = await connectMcpClient({
sseEndpoint,
serverTransport,
endpointUrl,
headers,
name: node.type,
version: node.typeVersion,

View File

@@ -4,16 +4,25 @@ import {
NodeOperationError,
} from 'n8n-workflow';
import type { McpAuthenticationOption } from './types';
import type { McpAuthenticationOption, McpServerTransport } from './types';
import { connectMcpClient, getAllTools, getAuthHeaders } from './utils';
export async function getTools(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const authentication = this.getNodeParameter('authentication') as McpAuthenticationOption;
const sseEndpoint = this.getNodeParameter('sseEndpoint') as string;
const node = this.getNode();
let serverTransport: McpServerTransport;
let endpointUrl: string;
if (node.typeVersion === 1) {
serverTransport = 'sse';
endpointUrl = this.getNodeParameter('sseEndpoint') as string;
} else {
serverTransport = this.getNodeParameter('serverTransport') as McpServerTransport;
endpointUrl = this.getNodeParameter('endpointUrl') as string;
}
const { headers } = await getAuthHeaders(this, authentication);
const client = await connectMcpClient({
sseEndpoint,
serverTransport,
endpointUrl,
headers,
name: node.type,
version: node.typeVersion,

View File

@@ -2,6 +2,8 @@ import type { JSONSchema7 } from 'json-schema';
export type McpTool = { name: string; description?: string; inputSchema: JSONSchema7 };
export type McpServerTransport = 'sse' | 'httpStreamable';
export type McpToolIncludeMode = 'all' | 'selected' | 'except';
export type McpAuthenticationOption = 'none' | 'headerAuth' | 'bearerAuth';

View File

@@ -1,6 +1,7 @@
import { DynamicStructuredTool, type DynamicStructuredToolInput } from '@langchain/core/tools';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { CompatibilityCallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
import { Toolkit } from 'langchain/agents';
import {
@@ -14,7 +15,12 @@ import { z } from 'zod';
import { convertJsonSchemaToZod } from '@utils/schemaParsing';
import type { McpAuthenticationOption, McpTool, McpToolIncludeMode } from './types';
import type {
McpAuthenticationOption,
McpTool,
McpServerTransport,
McpToolIncludeMode,
} from './types';
export async function getAllTools(client: Client, cursor?: string): Promise<McpTool[]> {
const { tools, nextCursor } = await client.listTools({ cursor });
@@ -145,23 +151,39 @@ type ConnectMcpClientError =
| { type: 'connection'; error: Error };
export async function connectMcpClient({
headers,
sseEndpoint,
serverTransport,
endpointUrl,
name,
version,
}: {
sseEndpoint: string;
serverTransport: McpServerTransport;
endpointUrl: string;
headers?: Record<string, string>;
name: string;
version: number;
}): Promise<Result<Client, ConnectMcpClientError>> {
try {
const endpoint = normalizeAndValidateUrl(sseEndpoint);
const endpoint = normalizeAndValidateUrl(endpointUrl);
if (!endpoint.ok) {
return createResultError({ type: 'invalid_url', error: endpoint.error });
}
const transport = new SSEClientTransport(endpoint.result, {
const client = new Client({ name, version: version.toString() }, { capabilities: { tools: {} } });
if (serverTransport === 'httpStreamable') {
try {
const transport = new StreamableHTTPClientTransport(endpoint.result, {
requestInit: { headers },
});
await client.connect(transport);
return createResultOk(client);
} catch (error) {
return createResultError({ type: 'connection', error });
}
}
try {
const sseTransport = new SSEClientTransport(endpoint.result, {
eventSourceInit: {
fetch: async (url, init) =>
await fetch(url, {
@@ -174,13 +196,7 @@ export async function connectMcpClient({
},
requestInit: { headers },
});
const client = new Client(
{ name, version: version.toString() },
{ capabilities: { tools: {} } },
);
await client.connect(transport);
await client.connect(sseTransport);
return createResultOk(client);
} catch (error) {
return createResultError({ type: 'connection', error });