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:
Elias Meire
2025-04-09 17:31:53 +02:00
committed by GitHub
parent b52f9f0f6c
commit 34252f53f9
24 changed files with 926 additions and 35 deletions

View File

@@ -6,6 +6,7 @@ import type {
SimplifiedNodeType,
} from '@/Interface';
import {
AI_CATEGORY_MCP_NODES,
AI_CATEGORY_ROOT_NODES,
AI_CATEGORY_TOOLS,
AI_CODE_NODE_TYPE,
@@ -222,16 +223,17 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
const aiSubNodes = difference(aiNodes, aiRootNodes);
aiSubNodes.forEach((node) => {
const section = node.properties.codex?.subcategories?.[AI_SUBCATEGORY]?.[0];
const subcategories = node.properties.codex?.subcategories ?? {};
const section = subcategories[AI_SUBCATEGORY]?.[0];
if (section) {
const subSection = node.properties.codex?.subcategories?.[section]?.[0];
const subSection = subcategories[section]?.[0];
const sectionKey = subSection ?? section;
const currentItems = sectionsMap.get(sectionKey)?.items ?? [];
const isSubnodesSection =
!node.properties.codex?.subcategories?.[AI_SUBCATEGORY].includes(
AI_CATEGORY_ROOT_NODES,
);
const isSubnodesSection = !(
subcategories[AI_SUBCATEGORY].includes(AI_CATEGORY_ROOT_NODES) ||
subcategories[AI_SUBCATEGORY].includes(AI_CATEGORY_MCP_NODES)
);
let title = section;
if (isSubnodesSection) {

View File

@@ -57,12 +57,18 @@ export function subcategorizeItems(items: SimplifiedNodeType[]) {
// Only some subcategories are allowed
let subcategories: string[] = [DEFAULT_SUBCATEGORY];
WHITE_LISTED_SUBCATEGORIES.forEach((category) => {
const matchedSubcategories = WHITE_LISTED_SUBCATEGORIES.flatMap((category) => {
if (item.codex?.categories?.includes(category)) {
subcategories = item.codex?.subcategories?.[category] ?? [];
return item.codex?.subcategories?.[category] ?? [];
}
return [];
});
if (matchedSubcategories.length > 0) {
subcategories = matchedSubcategories;
}
subcategories.forEach((subcategory: string) => {
if (!acc[subcategory]) {
acc[subcategory] = [];

View File

@@ -118,6 +118,7 @@ export const BAMBOO_HR_NODE_TYPE = 'n8n-nodes-base.bambooHr';
export const CALENDLY_TRIGGER_NODE_TYPE = 'n8n-nodes-base.calendlyTrigger';
export const CODE_NODE_TYPE = 'n8n-nodes-base.code';
export const AI_CODE_NODE_TYPE = '@n8n/n8n-nodes-langchain.code';
export const AI_MCP_TOOL_NODE_TYPE = '@n8n/n8n-nodes-langchain.mcpClientTool';
export const CRON_NODE_TYPE = 'n8n-nodes-base.cron';
export const CLEARBIT_NODE_TYPE = 'n8n-nodes-base.clearbit';
export const FILTER_NODE_TYPE = 'n8n-nodes-base.filter';
@@ -298,6 +299,7 @@ export const AI_CATEGORY_DOCUMENT_LOADERS = 'Document Loaders';
export const AI_CATEGORY_TEXT_SPLITTERS = 'Text Splitters';
export const AI_CATEGORY_OTHER_TOOLS = 'Other Tools';
export const AI_CATEGORY_ROOT_NODES = 'Root Nodes';
export const AI_CATEGORY_MCP_NODES = 'Model Context Protocol';
export const AI_UNCATEGORIZED_CATEGORY = 'Miscellaneous';
export const AI_CODE_TOOL_LANGCHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.toolCode';
export const AI_WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.toolWorkflow';

View File

@@ -46,7 +46,12 @@ function sanitizeFromAiParameterName(s: string) {
}
// nodeName | [nodeName, highestUnsupportedVersion]
const NODE_DENYLIST = ['toolCode', 'toolHttpRequest', ['toolWorkflow', 1.2]] as const;
const NODE_DENYLIST = [
'toolCode',
'toolHttpRequest',
'mcpClientTool',
['toolWorkflow', 1.2],
] as const;
const PATH_DENYLIST = [
'parameters.name',

View File

@@ -1,4 +1,5 @@
import {
AI_MCP_TOOL_NODE_TYPE,
LIST_LIKE_NODE_OPERATIONS,
MAIN_HEADER_TABS,
NODE_POSITION_CONFLICT_ALLOWLIST,
@@ -281,7 +282,11 @@ export function getGenericHints({
const nodeHints: NodeHint[] = [];
// tools hints
if (node?.type.toLocaleLowerCase().includes('tool') && hasNodeRun) {
if (
node?.type.toLocaleLowerCase().includes('tool') &&
node?.type !== AI_MCP_TOOL_NODE_TYPE &&
hasNodeRun
) {
const stringifiedParameters = JSON.stringify(workflowNode.parameters);
if (!stringifiedParameters.includes('$fromAI')) {
nodeHints.push({