From ac7cf30e019cde54905bf09b5d3fe1c6ba42f9b9 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Thu, 6 Mar 2025 21:27:28 +0000 Subject: [PATCH] Feature/Custom MCP (#4136) * add mcp tools * add custom MCP --- .../nodes/tools/MCP/CustomMCP/CustomMCP.ts | 129 ++++++++++++++++++ .../nodes/tools/MCP/CustomMCP/customMCP.png | Bin 0 -> 4134 bytes packages/components/nodes/tools/MCP/core.ts | 4 +- 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts create mode 100644 packages/components/nodes/tools/MCP/CustomMCP/customMCP.png diff --git a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts new file mode 100644 index 000000000..548c595ec --- /dev/null +++ b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts @@ -0,0 +1,129 @@ +import { Tool } from '@langchain/core/tools' +import { INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface' +import { MCPToolkit } from '../core' + +const mcpServerConfig = `{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/files"] +}` + +class Custom_MCP implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + documentation: string + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Custom MCP' + this.name = 'customMCP' + this.version = 1.0 + this.type = 'Custom MCP Tool' + this.icon = 'customMCP.png' + this.category = 'Tools (MCP)' + this.description = 'Custom MCP Config' + this.documentation = 'https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search' + this.inputs = [ + { + label: 'MCP Server Config', + name: 'mcpServerConfig', + type: 'code', + hideCodeExecute: true, + placeholder: mcpServerConfig + }, + { + label: 'Available Actions', + name: 'mcpActions', + type: 'asyncMultiOptions', + loadMethod: 'listActions', + refresh: true + } + ] + this.baseClasses = ['Tool'] + } + + //@ts-ignore + loadMethods = { + listActions: async (nodeData: INodeData): Promise => { + try { + const toolset = await this.getTools(nodeData) + toolset.sort((a: any, b: any) => a.name.localeCompare(b.name)) + + return toolset.map(({ name, ...rest }) => ({ + label: name.toUpperCase(), + name: name, + description: rest.description || name + })) + } catch (error) { + return [ + { + label: 'No Available Actions', + name: 'error', + description: 'No available actions, please check your API key and refresh' + } + ] + } + } + } + + async init(nodeData: INodeData): Promise { + const tools = await this.getTools(nodeData) + + const _mcpActions = nodeData.inputs?.mcpActions + let mcpActions = [] + if (_mcpActions) { + try { + mcpActions = typeof _mcpActions === 'string' ? JSON.parse(_mcpActions) : _mcpActions + } catch (error) { + console.error('Error parsing mcp actions:', error) + } + } + + return tools.filter((tool: any) => mcpActions.includes(tool.name)) + } + + async getTools(nodeData: INodeData): Promise { + const mcpServerConfig = nodeData.inputs?.mcpServerConfig as string + + if (!mcpServerConfig) { + throw new Error('MCP Server Config is required') + } + + try { + let serverParams + if (typeof mcpServerConfig === 'object') { + serverParams = mcpServerConfig + } else if (typeof mcpServerConfig === 'string') { + const serverParamsString = convertToValidJSONString(mcpServerConfig) + serverParams = JSON.parse(serverParamsString) + } + + const toolkit = new MCPToolkit(serverParams, 'stdio') + await toolkit.initialize() + + const tools = toolkit.tools ?? [] + + return tools as Tool[] + } catch (error) { + throw new Error(`Invalid MCP Server Config: ${error}`) + } + } +} + +function convertToValidJSONString(inputString: string) { + try { + const jsObject = Function('return ' + inputString)() + return JSON.stringify(jsObject, null, 2) + } catch (error) { + console.error('Error converting to JSON:', error) + return '' + } +} + +module.exports = { nodeClass: Custom_MCP } diff --git a/packages/components/nodes/tools/MCP/CustomMCP/customMCP.png b/packages/components/nodes/tools/MCP/CustomMCP/customMCP.png new file mode 100644 index 0000000000000000000000000000000000000000..6950234610ac4376b3fbe01c3d56c7820ee1fb67 GIT binary patch literal 4134 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 dzo~y)`gh8qZ|JN%Bv9K+d7SieYd(H4?O%>-3a