UNPKG

@n8n/n8n-nodes-langchain

Version:
465 lines 22.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MCP_LIST_TOOLS_REQUEST_MARKER = exports.McpServer = void 0; const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const json_schema_1 = require("@n8n/ai-utilities/json-schema"); const config_1 = require("@n8n/config"); const di_1 = require("@n8n/di"); const crypto_1 = require("crypto"); const n8n_workflow_1 = require("n8n-workflow"); const ExecutionCoordinator_1 = require("./execution/ExecutionCoordinator"); const PendingCallsManager_1 = require("./execution/PendingCallsManager"); const QueuedExecutionStrategy_1 = require("./execution/QueuedExecutionStrategy"); const MessageFormatter_1 = require("./protocol/MessageFormatter"); const MessageParser_1 = require("./protocol/MessageParser"); const types_1 = require("./protocol/types"); Object.defineProperty(exports, "MCP_LIST_TOOLS_REQUEST_MARKER", { enumerable: true, get: function () { return types_1.MCP_LIST_TOOLS_REQUEST_MARKER; } }); const InMemorySessionStore_1 = require("./session/InMemorySessionStore"); const SessionManager_1 = require("./session/SessionManager"); const StreamableHttpTransport_1 = require("./transport/StreamableHttpTransport"); const TransportFactory_1 = require("./transport/TransportFactory"); const ELICITATION_TIMEOUT_MS = 300_000; function toolDescriptors(tools) { return tools.map((tool) => ({ name: tool.name, description: tool.description, inputSchema: (0, json_schema_1.zodToDraft202012)(tool.schema, { removeAdditionalStrategy: 'strict' }), })); } class McpServer { constructor(logger) { this.resolveFunctions = {}; this.pendingResponses = {}; this.pendingGateResults = {}; this.logger = logger; this.sessionManager = new SessionManager_1.SessionManager(new InMemorySessionStore_1.InMemorySessionStore()); this.transportFactory = new TransportFactory_1.TransportFactory(); this.pendingCallsManager = new PendingCallsManager_1.PendingCallsManager(); this.executionCoordinator = new ExecutionCoordinator_1.ExecutionCoordinator(); const config = di_1.Container.get(config_1.McpServerConfig); this.idleTtlMs = config.sessionIdleTtl; this.sweepIntervalMs = config.sessionSweepInterval; this.logger.debug('McpServer created'); } static instance(logger) { if (!McpServer.instance_) { McpServer.instance_ = new McpServer(logger); McpServer.instance_.startSweep(); logger.debug('Created singleton McpServer'); } return McpServer.instance_; } async handleSetupRequest(_req, resp, serverName, postUrl, tools, instructions) { const server = this.createServer(serverName, instructions); const transport = this.transportFactory.createSSE(postUrl, resp); await this.setupSession(server, transport, tools, resp); } async handleStreamableHttpSetup(req, resp, serverName, tools, instructions) { const server = this.createServer(serverName, instructions); const transport = this.transportFactory.createStreamableHttp({ sessionIdGenerator: () => (0, crypto_1.randomUUID)(), onsessioninitialized: async (sessionId) => { this.logger.debug(`New session initialized: ${sessionId}`); await this.sessionManager.registerSession(sessionId, server, transport, tools); transport.onclose = async () => { this.logger.debug(`Deleting transport for ${sessionId}`); await this.cleanupSession(sessionId); }; }, }, resp); this.setupHandlers(server); await server.connect(transport); await transport.handleRequest(req, resp, req.body); resp.flush?.(); } async handlePostMessage(req, resp, tools, serverName, gateResult, instructions) { const sessionId = this.getSessionId(req); if (sessionId) this.sessionManager.touch(sessionId); let transport = sessionId ? this.sessionManager.getTransport(sessionId) : undefined; const rawBody = req.rawBody.toString(); let toolCallInfo = MessageParser_1.MessageParser.extractToolCallInfo(rawBody); let messageId; if (toolCallInfo) { const tool = tools.find((t) => t.name === toolCallInfo.toolName); if (tool?.metadata?.sourceNodeName && typeof tool.metadata.sourceNodeName === 'string') { toolCallInfo = { ...toolCallInfo, sourceNodeName: tool.metadata.sourceNodeName }; } } if (sessionId && !transport && req.headers['mcp-session-id'] && serverName) { this.logger.debug(`Recreating StreamableHTTP transport for session ${sessionId} on this main instance`); const recreated = await this.recreateStreamableHttpTransport(sessionId, serverName, tools, resp, instructions); if (!recreated) { resp.status(404).send('Session not found'); return { wasToolCall: false }; } transport = this.sessionManager.getTransport(sessionId); } const isToolCall = MessageParser_1.MessageParser.isToolCall(rawBody); const isListToolsRequest = MessageParser_1.MessageParser.isListToolsRequest(rawBody); if (sessionId && !transport && req.query.sessionId && this.executionCoordinator.isQueueMode() && (isToolCall || isListToolsRequest)) { this.logger.debug(`SSE queue mode: forwarding ${isToolCall ? 'tool call' : 'list tools'} for session ${sessionId} via pub/sub`); const message = (0, n8n_workflow_1.jsonParse)(rawBody); messageId = MessageParser_1.MessageParser.getRequestId(message); resp.status(202).send('Accepted'); return { wasToolCall: isToolCall, toolCallInfo, messageId, relaySessionId: isListToolsRequest ? sessionId : undefined, needsListToolsRelay: isListToolsRequest, }; } if (sessionId && transport) { const message = (0, n8n_workflow_1.jsonParse)(rawBody); messageId = MessageParser_1.MessageParser.getRequestId(message); const callId = messageId ? `${sessionId}_${messageId}` : sessionId; this.sessionManager.setTools(sessionId, tools); if (gateResult) { this.pendingGateResults[callId] = gateResult; } try { await new Promise((resolve) => { this.resolveFunctions[callId] = resolve; const requestHandled = transport.handleRequest(req, resp, message); if (isToolCall && transport.transportType === 'sse') { void requestHandled.catch(() => resolve()); } else { void requestHandled.finally(resolve); } }); } finally { delete this.resolveFunctions[callId]; delete this.pendingGateResults[callId]; } } else { this.logger.warn(`No transport found for session ${sessionId}`); resp.status(401).send('No transport found for sessionId'); } resp.flush?.(); const wasGated = !!gateResult && !gateResult.readyToExecute; return { wasToolCall: MessageParser_1.MessageParser.isToolCall(rawBody) && !wasGated, toolCallInfo, messageId, }; } async handleDeleteRequest(req, resp) { const sessionId = this.getSessionId(req); if (!sessionId) { resp.status(400).send('No sessionId provided'); return; } const transport = this.sessionManager.getTransport(sessionId); if (transport) { this.pendingCallsManager.cleanupBySessionId(sessionId); if (transport instanceof StreamableHttpTransport_1.StreamableHttpTransport) { await transport.handleRequest(req, resp); return; } resp.status(405).send('Method Not Allowed'); return; } resp.status(404).send('Session not found'); } getSessionId(req) { return (req.query.sessionId ?? req.headers['mcp-session-id']); } getMcpMetadata(req) { const sessionId = this.getSessionId(req); if (!sessionId) return undefined; const message = (0, n8n_workflow_1.jsonParse)(req.rawBody.toString()); const messageId = MessageParser_1.MessageParser.getRequestId(message); return { sessionId, messageId: messageId ?? '' }; } storePendingResponse(sessionId, messageId) { const transport = this.sessionManager.getTransport(sessionId); if (!transport) { this.logger.warn(`Cannot store pending response: no transport for session ${sessionId}`); return; } const callId = messageId ? `${sessionId}_${messageId}` : sessionId; this.pendingResponses[callId] = { sessionId, messageId, transport, createdAt: new Date(), }; } hasSession(sessionId) { return this.getTransport(sessionId) !== undefined; } handleWorkerResponse(sessionId, messageId, result) { const callId = messageId ? `${sessionId}_${messageId}` : sessionId; const pending = this.pendingResponses[callId]; const isListToolsRequest = typeof result === 'object' && result !== null && '_listToolsRequest' in result && result._listToolsRequest; if (isListToolsRequest) { const transport = this.sessionManager.getTransport(sessionId); if (transport && transport.transportType === 'sse' && messageId) { this.logger.debug(`SSE queue mode: handling relayed list tools request for session ${sessionId}`); const response = { jsonrpc: '2.0', id: messageId, result: { tools: toolDescriptors(this.sessionManager.getTools(sessionId) ?? []) }, }; void transport.send(response); } return; } const strategy = this.executionCoordinator.getStrategy(); if (strategy instanceof QueuedExecutionStrategy_1.QueuedExecutionStrategy) { if (strategy.resolveToolCall(callId, result)) { } else { const transport = this.sessionManager.getTransport(sessionId); if (transport && transport.transportType === 'sse' && messageId) { this.logger.debug(`SSE queue mode: sending response directly via transport for session ${sessionId}`); const formattedResult = MessageFormatter_1.MessageFormatter.formatToolResult(result, MessageFormatter_1.MessageFormatter.isErrorResult(result)); const response = { jsonrpc: '2.0', id: messageId, result: formattedResult, }; void transport.send(response); } } } if (this.resolveFunctions[callId]) { this.resolveFunctions[callId](); delete this.resolveFunctions[callId]; } if (pending) { delete this.pendingResponses[callId]; } } removePendingResponse(sessionId, messageId) { const callId = messageId ? `${sessionId}_${messageId}` : sessionId; delete this.pendingResponses[callId]; } hasPendingResponse(sessionId, messageId) { const callId = messageId ? `${sessionId}_${messageId}` : sessionId; return callId in this.pendingResponses; } get pendingResponseCount() { return Object.keys(this.pendingResponses).length; } setSessionStore(store) { this.sessionManager.setStore(store); } setExecutionStrategy(strategy) { this.executionCoordinator.setStrategy(strategy); } startSweep() { if (this.sweepTimer) return; this.sweepTimer = setInterval(() => { void this.runSweep(); }, this.sweepIntervalMs); this.sweepTimer.unref?.(); } stopSweep() { if (this.sweepTimer) { clearInterval(this.sweepTimer); this.sweepTimer = undefined; } } async runSweep() { for (const sessionId of this.sessionManager.getIdleSessions(this.idleTtlMs)) { if (this.sessionManager.getTransport(sessionId)?.transportType !== 'streamableHttp') continue; if (this.hasInFlightWork(sessionId)) continue; try { this.logger.debug(`Evicting idle MCP session ${sessionId}`); await this.cleanupSession(sessionId); } catch (error) { this.logger.error(`Failed to evict idle MCP session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`); } } } hasInFlightWork(sessionId) { if (this.pendingCallsManager.hasForSession(sessionId)) return true; const ownsSession = (callId) => callId === sessionId || callId.startsWith(`${sessionId}_`); if (Object.keys(this.resolveFunctions).some(ownsSession)) return true; return Object.values(this.pendingResponses).some((pending) => pending.sessionId === sessionId); } isQueueMode() { return this.executionCoordinator.isQueueMode(); } getTransport(sessionId) { return this.sessionManager.getTransport(sessionId); } getTools(sessionId) { return this.sessionManager.getTools(sessionId); } getPendingCallsManager() { return this.pendingCallsManager; } createServer(serverName, instructions) { return new index_js_1.Server({ name: serverName, version: '0.1.0' }, { capabilities: { tools: {} }, instructions }); } async setupSession(server, transport, tools, resp) { this.setupHandlers(server); const sessionId = transport.sessionId; await this.sessionManager.registerSession(sessionId, server, transport, tools); resp.on('close', async () => { this.logger.debug(`Deleting transport for ${sessionId}`); await this.cleanupSession(sessionId); }); await server.connect(transport); resp.flush?.(); } async cleanupSession(sessionId) { this.pendingCallsManager.cleanupBySessionId(sessionId); for (const callId of Object.keys(this.pendingResponses)) { if (this.pendingResponses[callId].sessionId === sessionId) { if (this.resolveFunctions[callId]) { this.resolveFunctions[callId](); delete this.resolveFunctions[callId]; } delete this.pendingResponses[callId]; } } await this.sessionManager.destroySession(sessionId); } async recreateStreamableHttpTransport(sessionId, serverName, tools, resp, instructions) { const isValid = await this.sessionManager.isSessionValid(sessionId); if (!isValid) { this.logger.warn(`Rejecting recreate request for invalid session: ${sessionId}`); return false; } const server = this.createServer(serverName, instructions); const transport = this.transportFactory.recreateStreamableHttp(sessionId, resp); await this.sessionManager.registerSession(sessionId, server, transport, tools); transport.onclose = async () => { this.logger.debug(`Deleting recreated transport for ${sessionId}`); await this.cleanupSession(sessionId); }; this.setupHandlers(server); await server.connect(transport); return true; } clientSupportsUrlElicitation(server) { const elicitation = server.getClientCapabilities()?.elicitation; return Boolean(elicitation && typeof elicitation === 'object' && 'url' in elicitation); } async handleCredentialGate(server, gateResult, callId) { const missing = gateResult.credentials.filter((c) => c.status !== 'configured'); const connectable = missing.filter((c) => !!c.authorizationUrl); const canElicit = connectable.length > 0 && connectable.length === missing.length && this.clientSupportsUrlElicitation(server); if (canElicit) { try { const outcomes = []; for (const cred of connectable) { const { action } = await server.elicitInput({ mode: 'url', elicitationId: (0, crypto_1.randomUUID)(), url: cred.authorizationUrl, message: `Connect ${cred.credentialName} (${cred.credentialType}) to run this tool.`, }, { timeout: ELICITATION_TIMEOUT_MS }); outcomes.push({ credentialName: cred.credentialName, credentialType: cred.credentialType, action, }); } if (this.resolveFunctions[callId]) this.resolveFunctions[callId](); return MessageFormatter_1.MessageFormatter.formatCredentialGateElicited(outcomes); } catch (error) { this.logger.warn(`Credential gate elicitation failed, falling back to text response: ${error instanceof Error ? error.message : String(error)}`); } } if (this.resolveFunctions[callId]) this.resolveFunctions[callId](); return MessageFormatter_1.MessageFormatter.formatCredentialGate(gateResult); } setupHandlers(server) { server.setRequestHandler(types_js_1.ListToolsRequestSchema, (_, extra) => { if (!extra.sessionId) { throw new n8n_workflow_1.OperationalError('Require a sessionId for the listing of tools'); } return { tools: toolDescriptors(this.sessionManager.getTools(extra.sessionId) ?? []) }; }); server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request, extra) => { if (!extra.sessionId) { throw new n8n_workflow_1.OperationalError('Require a sessionId for the tool call'); } const callId = extra.requestId ? `${extra.sessionId}_${extra.requestId}` : extra.sessionId; try { if (!request.params?.name || !request.params?.arguments) { throw new n8n_workflow_1.OperationalError('Require a name and arguments for the tool call'); } const toolName = request.params.name; const toolArguments = typeof request.params.arguments === 'object' && request.params.arguments !== null ? request.params.arguments : {}; const tools = this.sessionManager.getTools(extra.sessionId) ?? []; const requestedTool = tools.find((tool) => tool.name === toolName); if (!requestedTool) { throw new n8n_workflow_1.OperationalError('Tool not found'); } const gateResult = this.pendingGateResults[callId]; if (gateResult && !gateResult.readyToExecute) { return await this.handleCredentialGate(server, gateResult, callId); } try { if (this.executionCoordinator.isQueueMode()) { const requestId = extra.requestId?.toString() ?? ''; this.storePendingResponse(extra.sessionId, requestId); if (this.resolveFunctions[callId]) { this.resolveFunctions[callId](); } const strategy = this.executionCoordinator.getStrategy(); const result = await strategy.executeTool(requestedTool, toolArguments, { sessionId: extra.sessionId, messageId: requestId, }); return MessageFormatter_1.MessageFormatter.formatToolResult(result, MessageFormatter_1.MessageFormatter.isErrorResult(result)); } const result = await this.executionCoordinator.executeTool(requestedTool, toolArguments, { sessionId: extra.sessionId, messageId: extra.requestId?.toString(), }); return MessageFormatter_1.MessageFormatter.formatToolResult(result, MessageFormatter_1.MessageFormatter.isErrorResult(result)); } catch (error) { const errorObject = error instanceof Error ? error : new Error(String(error)); this.logger.error(`Error while executing Tool ${toolName}: ${errorObject.message}`, { error: errorObject, }); return MessageFormatter_1.MessageFormatter.formatError(errorObject); } } finally { this.resolveFunctions[callId]?.(); } }); server.onclose = () => { this.logger.debug('Closing MCP Server'); }; server.onerror = (error) => { this.logger.error(`MCP Error: ${error instanceof Error ? error.message : String(error)}`); }; } } exports.McpServer = McpServer; //# sourceMappingURL=McpServer.js.map