UNPKG

@n8n/n8n-nodes-langchain

Version:
300 lines 14.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.McpTrigger = void 0; const error_1 = require("n8n-nodes-base/dist/nodes/Webhook/error"); const utils_1 = require("n8n-nodes-base/dist/nodes/Webhook/utils"); const n8n_workflow_1 = require("n8n-workflow"); const helpers_1 = require("../../../utils/helpers"); const CredentialGateTool_1 = require("./CredentialGateTool"); const McpServer_1 = require("./McpServer"); const MessageParser_1 = require("./protocol/MessageParser"); async function getConnectedToolsRespectingCredentialGate(context, toolInput) { const gateResult = await context.checkTriggerCredentialStatus(); try { return { tools: await (0, helpers_1.getConnectedTools)(context, true, undefined, undefined, { inputData: toolInput, }), gateResult, }; } catch (error) { if (!gateResult || gateResult.readyToExecute) throw error; context.logger.warn(`MCP Trigger: could not build the tool list while the caller has unconnected credentials, exposing the connect-credentials tool instead: ${error instanceof Error ? error.message : String(error)}`); return { tools: [(0, CredentialGateTool_1.createCredentialGateTool)(gateResult)], gateResult }; } } const MCP_SSE_SETUP_PATH = 'sse'; const MCP_SSE_MESSAGES_PATH = 'messages'; class McpTrigger extends n8n_workflow_1.Node { constructor() { super(...arguments); this.description = { displayName: 'MCP Server Trigger', name: 'mcpTrigger', icon: { light: 'file:../mcp.svg', dark: 'file:../mcp.dark.svg', }, group: ['trigger'], version: [1, 1.1, 2, 2.1], description: 'Expose n8n tools as an MCP Server endpoint', activationMessage: 'You can now connect your MCP Clients to the URL, using SSE or Streamable HTTP transports.', defaults: { name: 'MCP Server Trigger', }, codex: { categories: ['AI', 'Core Nodes'], subcategories: { AI: ['Root Nodes', 'Model Context Protocol'], 'Core Nodes': ['Other Trigger Nodes'], }, alias: ['Model Context Protocol', 'MCP Server'], resources: { primaryDocumentation: [ { url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-langchain.mcptrigger/', }, ], }, }, triggerPanel: { header: 'Listen for MCP events', executionsHelp: { inactive: "This trigger has two modes: test and production.<br /><br /><b>Use test mode while you build your workflow</b>. Click the 'execute step' button, then make an MCP request to the test URL. The executions will show up in the editor.<br /><br /><b>Use production mode to run your workflow automatically</b>. Publish the workflow, then make requests to the production URL. These executions will show up in the <a data-key='executions'>executions list</a>, but not the editor.", active: "This trigger has two modes: test and production.<br /><br /><b>Use test mode while you build your workflow</b>. Click the 'execute step' button, then make an MCP request to the test URL. The executions will show up in the editor.<br /><br /><b>Use production mode to run your workflow automatically</b>. Since your workflow is activated, you can make requests to the production URL. These executions will show up in the <a data-key='executions'>executions list</a>, but not the editor.", }, activationHint: "Once you've finished building your workflow, run it without having to click this button by using the production URL.", }, inputs: [ { type: n8n_workflow_1.NodeConnectionTypes.AiTool, displayName: 'Tools', }, ], outputs: [], sensitiveOutputFields: ['headers.authorization', 'headers.cookie'], credentials: [ { name: 'httpBearerAuth', required: true, displayOptions: { show: { authentication: ['bearerAuth'], }, }, }, { name: 'httpHeaderAuth', required: true, displayOptions: { show: { authentication: ['headerAuth'], }, }, }, ], properties: [ { displayName: 'Authentication', name: 'authentication', type: 'options', options: [ { name: 'None', value: 'none' }, { name: 'n8n User Auth (OAuth2)', value: 'n8nOAuth2', description: 'Require user to give consent to use their n8n account', displayOptions: { show: { '@version': [{ _cnd: { gte: 2 } }] } }, }, { name: 'Bearer Auth', value: 'bearerAuth' }, { name: 'Header Auth', value: 'headerAuth' }, ], default: 'none', description: 'The way to authenticate', builderHint: { propertyHint: "Default to 'none'. n8n exposes inbound trigger URLs publicly by design. Only select an authentication method when the user explicitly asks to authenticate inbound traffic.", }, }, { displayName: 'Require Workflow Execute Permission', name: 'requireExecuteAccess', type: 'boolean', default: true, displayOptions: { show: { authentication: ['n8nOAuth2'] } }, description: 'Whether the triggering user must also have permission to execute the workflow in the project it belongs to', }, { displayName: 'Include User in Output', name: 'includeUserInOutput', type: 'boolean', default: true, displayOptions: { show: { authentication: ['n8nOAuth2'], '@version': [{ _cnd: { gte: 2.1 } }], }, }, description: "Whether to include the calling user's ID, email and name in the trigger output and in the request the connected tools receive", }, { displayName: 'Path', name: 'path', type: 'string', default: '', placeholder: 'webhook', required: true, description: 'The base path for this MCP server', }, { displayName: 'Instructions', name: 'instructions', type: 'string', typeOptions: { rows: 4 }, default: '', description: "Sent to MCP clients when they connect. Clients that support server instructions typically add them to the model's system prompt — use for guidance that spans multiple tools, such as tool-choice rules or multi-step workflows.", }, ], webhooks: [ { name: 'setup', httpMethod: 'GET', responseMode: 'onReceived', isFullPath: true, path: `={{$parameter["path"]}}{{parseFloat($nodeVersion)<2 ? '/${MCP_SSE_SETUP_PATH}' : ''}}`, nodeType: 'mcp', ndvHideMethod: true, ndvHideUrl: false, }, { name: 'default', httpMethod: 'POST', responseMode: 'onReceived', isFullPath: true, path: `={{$parameter["path"]}}{{parseFloat($nodeVersion)<2 ? '/${MCP_SSE_MESSAGES_PATH}' : ''}}`, nodeType: 'mcp', ndvHideMethod: true, ndvHideUrl: true, }, { name: 'default', httpMethod: 'DELETE', responseMode: 'onReceived', isFullPath: true, path: '={{$parameter["path"]}}', nodeType: 'mcp', ndvHideMethod: true, ndvHideUrl: true, }, ], }; } async webhook(context) { const webhookName = context.getWebhookName(); const req = context.getRequestObject(); const resp = context.getResponseObject(); let authedUser; if (context.getNodeParameter('authentication') === 'n8nOAuth2') { if (context.getNode().typeVersion < 2) { resp.writeHead(401); resp.end('OAuth2 authentication requires mcp trigger node v2.0 or higher'); return { noWebhookResponse: true }; } const authResult = await (0, n8n_workflow_1.n8nOAuth2Auth)(context, { realm: 'n8n MCP Server' }); if (authResult === 'handled') { return { noWebhookResponse: true }; } await context.establishTriggerIdentity(authResult.token, authResult.resource, authResult.user.id); authedUser = authResult.user; } else { try { await (0, utils_1.validateWebhookAuthentication)(context, 'authentication'); } catch (error) { if (error instanceof error_1.WebhookAuthorizationError) { resp.writeHead(error.responseCode); resp.end(error.message); return { noWebhookResponse: true }; } throw error; } } const node = context.getNode(); const headers = (0, n8n_workflow_1.redactedHeaders)(req); const user = authedUser && context.getNodeParameter('includeUserInOutput', true) !== false ? { id: authedUser.id, email: authedUser.email, firstName: authedUser.firstName, lastName: authedUser.lastName, } : undefined; const exposesRequest = node.typeVersion >= 2.1; const toolInput = exposesRequest ? { body: context.getBodyData(), headers, ...(user && { user }) } : undefined; const serverName = node.typeVersion > 1 ? (0, n8n_workflow_1.nodeNameToToolName)(node) : 'n8n-mcp-server'; const instructions = String(context.getNodeParameter('instructions', '') ?? '') || undefined; const mcpServer = McpServer_1.McpServer.instance(context.logger); if (webhookName === 'setup') { const postUrl = node.typeVersion < 2 ? req.path.replace(new RegExp(`/${MCP_SSE_SETUP_PATH}$`), `/${MCP_SSE_MESSAGES_PATH}`) : req.path; const { tools: connectedTools } = await getConnectedToolsRespectingCredentialGate(context, toolInput); await mcpServer.handleSetupRequest(req, resp, serverName, postUrl, connectedTools, instructions); return { noWebhookResponse: true }; } else if (webhookName === 'default') { if (req.method === 'DELETE') { await mcpServer.handleDeleteRequest(req, resp); } else { const sessionId = mcpServer.getSessionId(req); context.logger.debug('MCP POST request received for existing session'); if (sessionId) { const { tools: connectedTools, gateResult: credentialStatus } = await getConnectedToolsRespectingCredentialGate(context, toolInput); let gateResult; if (MessageParser_1.MessageParser.isToolCall(req.rawBody.toString())) { gateResult = credentialStatus; } const { wasToolCall, toolCallInfo, messageId, relaySessionId, needsListToolsRelay } = await mcpServer.handlePostMessage(req, resp, connectedTools, serverName, gateResult, instructions); if (wasToolCall) { const workflowData = { ...(toolCallInfo && { mcpToolCall: toolCallInfo }), ...(messageId && { mcpMessageId: messageId }), ...(exposesRequest && { headers, ...(user && { user }) }), }; return { noWebhookResponse: true, workflowData: [[{ json: workflowData }]], toolInput, }; } if (needsListToolsRelay && relaySessionId && messageId) { const workflowData = { mcpListToolsRelay: { sessionId: relaySessionId, messageId, marker: McpServer_1.MCP_LIST_TOOLS_REQUEST_MARKER, }, }; return { noWebhookResponse: true, workflowData: [[{ json: workflowData }]], }; } } else { const { tools: connectedTools } = await getConnectedToolsRespectingCredentialGate(context, toolInput); await mcpServer.handleStreamableHttpSetup(req, resp, serverName, connectedTools, instructions); } } return { noWebhookResponse: true }; } return { workflowData: [[{ json: {} }]] }; } } exports.McpTrigger = McpTrigger; //# sourceMappingURL=McpTrigger.node.js.map