mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +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:
@@ -3,6 +3,7 @@ import type { BaseChatModel } from '@langchain/core/language_models/chat_models'
|
||||
import type { BaseLLM } from '@langchain/core/language_models/llms';
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import type { Tool } from '@langchain/core/tools';
|
||||
import { Toolkit } from 'langchain/agents';
|
||||
import type { BaseChatMemory } from 'langchain/memory';
|
||||
import { NodeConnectionTypes, NodeOperationError, jsonStringify } from 'n8n-workflow';
|
||||
import type {
|
||||
@@ -189,14 +190,22 @@ export const getConnectedTools = async (
|
||||
convertStructuredTool: boolean = true,
|
||||
escapeCurlyBrackets: boolean = false,
|
||||
) => {
|
||||
const connectedTools =
|
||||
((await ctx.getInputConnectionData(NodeConnectionTypes.AiTool, 0)) as Tool[]) || [];
|
||||
const connectedTools = (
|
||||
((await ctx.getInputConnectionData(NodeConnectionTypes.AiTool, 0)) as Array<Toolkit | Tool>) ??
|
||||
[]
|
||||
).flatMap((toolOrToolkit) => {
|
||||
if (toolOrToolkit instanceof Toolkit) {
|
||||
return toolOrToolkit.getTools() as Tool[];
|
||||
}
|
||||
|
||||
return toolOrToolkit;
|
||||
});
|
||||
|
||||
if (!enforceUniqueNames) return connectedTools;
|
||||
|
||||
const seenNames = new Set<string>();
|
||||
|
||||
const finalTools = [];
|
||||
const finalTools: Tool[] = [];
|
||||
|
||||
for (const tool of connectedTools) {
|
||||
const { name } = tool;
|
||||
|
||||
@@ -6,11 +6,12 @@ import { Embeddings } from '@langchain/core/embeddings';
|
||||
import type { InputValues, MemoryVariables, OutputValues } from '@langchain/core/memory';
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import { BaseRetriever } from '@langchain/core/retrievers';
|
||||
import type { Tool } from '@langchain/core/tools';
|
||||
import type { StructuredTool, Tool } from '@langchain/core/tools';
|
||||
import { VectorStore } from '@langchain/core/vectorstores';
|
||||
import { TextSplitter } from '@langchain/textsplitters';
|
||||
import type { BaseDocumentLoader } from 'langchain/dist/document_loaders/base';
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
ISupplyDataFunctions,
|
||||
@@ -94,9 +95,10 @@ export function callMethodSync<T>(
|
||||
}
|
||||
}
|
||||
|
||||
export function logWrapper(
|
||||
originalInstance:
|
||||
export function logWrapper<
|
||||
T extends
|
||||
| Tool
|
||||
| StructuredTool
|
||||
| BaseChatMemory
|
||||
| BaseChatMessageHistory
|
||||
| BaseRetriever
|
||||
@@ -108,8 +110,7 @@ export function logWrapper(
|
||||
| VectorStore
|
||||
| N8nBinaryLoader
|
||||
| N8nJsonLoader,
|
||||
executeFunctions: IExecuteFunctions | ISupplyDataFunctions,
|
||||
) {
|
||||
>(originalInstance: T, executeFunctions: IExecuteFunctions | ISupplyDataFunctions): T {
|
||||
return new Proxy(originalInstance, {
|
||||
get: (target, prop) => {
|
||||
let connectionType: NodeConnectionType | undefined;
|
||||
@@ -372,8 +373,16 @@ export function logWrapper(
|
||||
if (prop === '_call' && '_call' in target) {
|
||||
return async (query: string): Promise<string> => {
|
||||
connectionType = NodeConnectionTypes.AiTool;
|
||||
const inputData: IDataObject = { query };
|
||||
|
||||
if (target.metadata?.isFromToolkit) {
|
||||
inputData.tool = {
|
||||
name: target.name,
|
||||
description: target.description,
|
||||
};
|
||||
}
|
||||
const { index } = executeFunctions.addInputData(connectionType, [
|
||||
[{ json: { query } }],
|
||||
[{ json: inputData }],
|
||||
]);
|
||||
|
||||
const response = (await callMethodAsync.call(target, {
|
||||
@@ -384,7 +393,7 @@ export function logWrapper(
|
||||
arguments: [query],
|
||||
})) as string;
|
||||
|
||||
logAiEvent(executeFunctions, 'ai-tool-called', { query, response });
|
||||
logAiEvent(executeFunctions, 'ai-tool-called', { ...inputData, response });
|
||||
executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]);
|
||||
return response;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DynamicTool, type Tool } from '@langchain/core/tools';
|
||||
import { Toolkit } from 'langchain/agents';
|
||||
import { createMockExecuteFunction } from 'n8n-nodes-base/test/nodes/Helpers';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import type { ISupplyDataFunctions, IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
@@ -242,6 +243,34 @@ describe('getConnectedTools', () => {
|
||||
const tools = await getConnectedTools(mockExecuteFunctions, true, false);
|
||||
expect(tools[0]).toBe(mockN8nTool);
|
||||
});
|
||||
|
||||
it('should flatten tools from a toolkit', async () => {
|
||||
class MockToolkit extends Toolkit {
|
||||
tools: Tool[];
|
||||
|
||||
constructor(tools: unknown[]) {
|
||||
super();
|
||||
this.tools = tools as Tool[];
|
||||
}
|
||||
}
|
||||
const mockTools = [
|
||||
{ name: 'tool1', description: 'desc1' },
|
||||
|
||||
new MockToolkit([
|
||||
{ name: 'toolkitTool1', description: 'toolkitToolDesc1' },
|
||||
{ name: 'toolkitTool2', description: 'toolkitToolDesc2' },
|
||||
]),
|
||||
];
|
||||
|
||||
mockExecuteFunctions.getInputConnectionData = jest.fn().mockResolvedValue(mockTools);
|
||||
|
||||
const tools = await getConnectedTools(mockExecuteFunctions, false);
|
||||
expect(tools).toEqual([
|
||||
{ name: 'tool1', description: 'desc1' },
|
||||
{ name: 'toolkitTool1', description: 'toolkitToolDesc1' },
|
||||
{ name: 'toolkitTool2', description: 'toolkitToolDesc2' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unwrapNestedOutput', () => {
|
||||
|
||||
Reference in New Issue
Block a user