mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(MCP Client Tool Node): Add MCP Client Tool Node to connect to MCP servers over SSE (#14464)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> Co-authored-by: JP van Oosten <jp@n8n.io>
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
NodeOperationError,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type ISupplyDataFunctions,
|
||||
type SupplyData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { logWrapper } from '@utils/logWrapper';
|
||||
import { getConnectionHintNoticeField } from '@utils/sharedFields';
|
||||
|
||||
import { getTools } from './loadOptions';
|
||||
import type { McpAuthenticationOption, McpToolIncludeMode } from './types';
|
||||
import {
|
||||
connectMcpClient,
|
||||
createCallTool,
|
||||
getAllTools,
|
||||
getAuthHeaders,
|
||||
getSelectedTools,
|
||||
McpToolkit,
|
||||
mcpToolToDynamicTool,
|
||||
} from './utils';
|
||||
|
||||
export class McpClientTool implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'MCP Client Tool',
|
||||
name: 'mcpClientTool',
|
||||
icon: {
|
||||
light: 'file:../mcp.svg',
|
||||
dark: 'file:../mcp.dark.svg',
|
||||
},
|
||||
group: ['output'],
|
||||
version: 1,
|
||||
description: 'Connect tools from an MCP Server',
|
||||
defaults: {
|
||||
name: 'MCP Client',
|
||||
},
|
||||
codex: {
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Model Context Protocol', 'Tools'],
|
||||
},
|
||||
alias: ['Model Context Protocol', 'MCP Client'],
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.mcpclienttool/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
inputs: [],
|
||||
outputs: [{ type: NodeConnectionTypes.AiTool, displayName: 'Tools' }],
|
||||
credentials: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-class-description-credentials-name-unsuffixed
|
||||
name: 'httpBearerAuth',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['bearerAuth'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'httpHeaderAuth',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['headerAuth'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
getConnectionHintNoticeField([NodeConnectionTypes.AiAgent]),
|
||||
{
|
||||
displayName: 'SSE Endpoint',
|
||||
name: 'sseEndpoint',
|
||||
type: 'string',
|
||||
description: 'SSE Endpoint of your MCP server',
|
||||
placeholder: 'e.g. https://my-mcp-server.ai/sse',
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Bearer Auth',
|
||||
value: 'bearerAuth',
|
||||
},
|
||||
{
|
||||
name: 'Header Auth',
|
||||
value: 'headerAuth',
|
||||
},
|
||||
{
|
||||
name: 'None',
|
||||
value: 'none',
|
||||
},
|
||||
],
|
||||
default: 'none',
|
||||
description: 'The way to authenticate with your SSE endpoint',
|
||||
},
|
||||
{
|
||||
displayName: 'Credentials',
|
||||
name: 'credentials',
|
||||
type: 'credentials',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['headerAuth', 'bearerAuth'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Tools to Include',
|
||||
name: 'include',
|
||||
type: 'options',
|
||||
description: 'How to select the tools you want to be exposed to the AI Agent',
|
||||
default: 'all',
|
||||
options: [
|
||||
{
|
||||
name: 'All',
|
||||
value: 'all',
|
||||
description: 'Also include all unchanged fields from the input',
|
||||
},
|
||||
{
|
||||
name: 'Selected',
|
||||
value: 'selected',
|
||||
description: 'Also include the tools listed in the parameter "Tools to Include"',
|
||||
},
|
||||
{
|
||||
name: 'All Except',
|
||||
value: 'except',
|
||||
description: 'Exclude the tools listed in the parameter "Tools to Exclude"',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Tools to Include',
|
||||
name: 'includeTools',
|
||||
type: 'multiOptions',
|
||||
default: [],
|
||||
description:
|
||||
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTools',
|
||||
loadOptionsDependsOn: ['sseEndpoint'],
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
include: ['selected'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Tools to Exclude',
|
||||
name: 'excludeTools',
|
||||
type: 'multiOptions',
|
||||
default: [],
|
||||
description:
|
||||
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTools',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
include: ['except'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
getTools,
|
||||
},
|
||||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const authentication = this.getNodeParameter(
|
||||
'authentication',
|
||||
itemIndex,
|
||||
) as McpAuthenticationOption;
|
||||
const sseEndpoint = this.getNodeParameter('sseEndpoint', itemIndex) as string;
|
||||
const node = this.getNode();
|
||||
const { headers } = await getAuthHeaders(this, authentication);
|
||||
const client = await connectMcpClient({
|
||||
sseEndpoint,
|
||||
headers,
|
||||
name: node.type,
|
||||
version: node.typeVersion,
|
||||
});
|
||||
|
||||
const setError = (message: string, description?: string): SupplyData => {
|
||||
const error = new NodeOperationError(node, message, { itemIndex, description });
|
||||
this.addOutputData(NodeConnectionTypes.AiTool, itemIndex, error);
|
||||
throw error;
|
||||
};
|
||||
|
||||
if (!client.ok) {
|
||||
this.logger.error('McpClientTool: Failed to connect to MCP Server', {
|
||||
error: client.error,
|
||||
});
|
||||
|
||||
switch (client.error.type) {
|
||||
case 'invalid_url':
|
||||
return setError('Could not connect to your MCP server. The provided URL is invalid.');
|
||||
case 'connection':
|
||||
default:
|
||||
return setError('Could not connect to your MCP server');
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug('McpClientTool: Successfully connected to MCP Server');
|
||||
|
||||
const mode = this.getNodeParameter('include', itemIndex) as McpToolIncludeMode;
|
||||
const includeTools = this.getNodeParameter('includeTools', itemIndex, []) as string[];
|
||||
const excludeTools = this.getNodeParameter('excludeTools', itemIndex, []) as string[];
|
||||
|
||||
const allTools = await getAllTools(client.result);
|
||||
const mcpTools = getSelectedTools({
|
||||
tools: allTools,
|
||||
mode,
|
||||
includeTools,
|
||||
excludeTools,
|
||||
});
|
||||
|
||||
if (!mcpTools.length) {
|
||||
return setError(
|
||||
'MCP Server returned no tools',
|
||||
'Connected successfully to your MCP server but it returned an empty list of tools.',
|
||||
);
|
||||
}
|
||||
|
||||
const tools = mcpTools.map((tool) =>
|
||||
logWrapper(
|
||||
mcpToolToDynamicTool(
|
||||
tool,
|
||||
createCallTool(tool.name, client.result, (error) => {
|
||||
this.logger.error(`McpClientTool: Tool "${tool.name}" failed to execute`, { error });
|
||||
throw new NodeOperationError(node, `Failed to execute tool "${tool.name}"`, {
|
||||
description: error,
|
||||
});
|
||||
}),
|
||||
),
|
||||
this,
|
||||
),
|
||||
);
|
||||
|
||||
this.logger.debug(`McpClientTool: Connected to MCP Server with ${tools.length} tools`);
|
||||
|
||||
const toolkit = new McpToolkit(tools);
|
||||
|
||||
return { response: toolkit, closeFunction: async () => await client.result.close() };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user