UNPKG

n8n

Version:

n8n Workflow Automation Tool

379 lines • 18.1 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InstanceAiMcpRegistryService = void 0; const agents_1 = require("@n8n/agents"); const backend_common_1 = require("@n8n/backend-common"); const backend_network_1 = require("@n8n/backend-network"); const config_1 = require("@n8n/config"); const di_1 = require("@n8n/di"); const typeorm_1 = require("@n8n/typeorm"); const node_crypto_1 = require("node:crypto"); const credentials_finder_service_1 = require("../../../credentials/credentials-finder.service"); const credentials_service_1 = require("../../../credentials/credentials.service"); const conflict_error_1 = require("../../../errors/response-errors/conflict.error"); const not_found_error_1 = require("../../../errors/response-errors/not-found.error"); const event_service_1 = require("../../../events/event.service"); const mcp_registry_service_1 = require("../../../modules/mcp-registry/registry/mcp-registry.service"); const oauth_service_1 = require("../../../oauth/oauth.service"); const ai_proxy_fetch_1 = require("../../../utils/ai-proxy-fetch"); const auth_fetch_1 = require("../../../utils/auth-fetch"); const instance_ai_mcp_registry_connection_repository_1 = require("../repositories/instance-ai-mcp-registry-connection.repository"); function readString(data, key) { const value = data[key]; return typeof value === 'string' && value.length > 0 ? value : undefined; } function readAccessToken(tokenData) { return readString(tokenData, 'accessToken') ?? readString(tokenData, 'access_token'); } function readOAuthTokenData(data) { const tokenData = data.oauthTokenData; return (0, backend_common_1.isObjectLiteral)(tokenData) ? tokenData : null; } function getPreferredRemote(remotes) { const streamable = remotes.find((remote) => remote.type === 'streamable-http'); if (streamable?.url) { return { transport: 'streamableHttp', endpointUrl: streamable.url }; } const sse = remotes.find((remote) => remote.type === 'sse'); if (sse?.url) { return { transport: 'sse', endpointUrl: sse.url }; } return null; } const MCP_REGISTRY_SERVER_PREFIX = 'mcp_'; const MAX_MCP_SERVER_NAME_LENGTH = 24; function buildServerName(serverSlug, sequence) { const safeSlug = serverSlug.replace(/[^A-Za-z0-9_-]/g, '_'); const baseName = `${MCP_REGISTRY_SERVER_PREFIX}${safeSlug}`; if (sequence <= 1) { return baseName.slice(0, MAX_MCP_SERVER_NAME_LENGTH); } const suffix = `_${sequence}`; const maxBaseLength = Math.max(0, MAX_MCP_SERVER_NAME_LENGTH - suffix.length); return `${baseName.slice(0, maxBaseLength)}${suffix}`; } function normalizeTools(tools) { if (!tools) { return []; } return [...new Set(tools.filter((tool) => tool.length > 0))]; } function resolveToolFilter(payload, current) { if (payload.inclusionMode === undefined) { return current; } if (payload.inclusionMode === 'all') { return null; } if (payload.inclusionMode === 'selected') { return { mode: 'allow', tools: normalizeTools(payload.selectedTools) }; } return { mode: 'exclude', tools: normalizeTools(payload.excludedTools) }; } function stripMcpServerPrefix(toolName, serverName) { const prefix = `${serverName}_`; return toolName.startsWith(prefix) ? toolName.slice(prefix.length) : toolName; } function toToolResponse(tool, serverName) { const response = { name: tool.mcpToolName ?? stripMcpServerPrefix(tool.name, serverName), }; if (tool.description) response.description = tool.description; return response; } let InstanceAiMcpRegistryService = class InstanceAiMcpRegistryService { constructor(logger, connectionRepository, mcpRegistryService, credentialsFinderService, credentialsService, oauthService, eventService, outboundHttp, ssrfConfig, ssrfProtectionService) { this.connectionRepository = connectionRepository; this.mcpRegistryService = mcpRegistryService; this.credentialsFinderService = credentialsFinderService; this.credentialsService = credentialsService; this.oauthService = oauthService; this.eventService = eventService; this.outboundHttp = outboundHttp; this.ssrfConfig = ssrfConfig; this.ssrfProtectionService = ssrfProtectionService; this.logger = logger.scoped('instance-ai'); } async listConnectionsForUser(user) { return await this.connectionRepository.findBy({ userId: user.id }); } async createConnection(user, input) { const server = await this.mcpRegistryService.get(input.serverSlug); if (!server) { throw new not_found_error_1.NotFoundError(`Unknown MCP registry server: ${input.serverSlug}`); } const existing = await this.connectionRepository.findOneBy({ userId: user.id, serverSlug: input.serverSlug, }); if (existing) { throw new conflict_error_1.ConflictError('This MCP server is already connected. Disconnect first to use a different credential.'); } const credential = await this.credentialsFinderService.findCredentialForUser(input.credentialId, user, ['credential:read']); if (!credential) { throw new not_found_error_1.NotFoundError('Credential not found or not accessible'); } const entity = this.connectionRepository.create({ id: (0, node_crypto_1.randomUUID)(), userId: user.id, serverSlug: input.serverSlug, credentialId: input.credentialId, }); try { const connection = await this.connectionRepository.save(entity); this.eventService.emit('instance-ai-mcp-registry-connection-created', { userId: user.id, serverSlug: input.serverSlug, }); return { connection, credential, server }; } catch (error) { if (isUniqueConstraintViolation(error)) { throw new conflict_error_1.ConflictError('A connection for this MCP server with this credential already exists'); } throw error; } } async deleteConnection(user, id) { const connection = await this.connectionRepository.findOneBy({ id, userId: user.id }); if (!connection) { throw new not_found_error_1.NotFoundError('MCP registry connection not found'); } await this.connectionRepository.delete({ id }); this.eventService.emit('instance-ai-mcp-registry-connection-deleted', { userId: user.id, serverSlug: connection.serverSlug, }); } async updateConnection(user, id, payload) { const connection = await this.connectionRepository.findOneBy({ id, userId: user.id }); if (!connection) { throw new not_found_error_1.NotFoundError('MCP registry connection not found'); } if (payload.credentialId) { await this.swapCredential(user, connection, payload.credentialId); } connection.toolFilter = resolveToolFilter(payload, connection.toolFilter); return await this.connectionRepository.save(connection); } async listConnectionTools(user, id) { const connection = await this.connectionRepository.findOneBy({ id, userId: user.id }); if (!connection) { throw new not_found_error_1.NotFoundError('MCP registry connection not found'); } const server = await this.mcpRegistryService.get(connection.serverSlug); if (!server) { throw new not_found_error_1.NotFoundError(`Unknown MCP registry server: ${connection.serverSlug}`); } const resolvedServer = this.resolveRegistryServer(connection.id, connection.serverSlug, connection.credentialId, server.authType, server.remotes); if (!resolvedServer) return []; const aiMcpFetch = (0, ai_proxy_fetch_1.createAiMcpFetch)(this.outboundHttp, this.ssrfConfig, this.ssrfProtectionService); const requestFetch = await this.buildRegistryServerFetch(resolvedServer, user, connection.id, aiMcpFetch); if (!requestFetch) return []; const serverName = buildServerName(resolvedServer.serverSlug, 1); const client = new agents_1.McpClient([ { name: serverName, url: resolvedServer.endpointUrl, transport: resolvedServer.transport, fetch: requestFetch, connectionTimeoutMs: 10_000, }, ]); try { return (await client.listTools()).map((tool) => toToolResponse(tool, serverName)); } finally { await client.close().catch((error) => { this.logger.warn('Failed to close MCP client after listing tools', { connectionId: connection.id, serverSlug: connection.serverSlug, error, }); }); } } async getRegistryMcpServers(user) { const connections = await this.connectionRepository.findBy({ userId: user.id }); if (connections.length === 0) { return []; } const sortedConnections = connections.sort((left, right) => left.id.localeCompare(right.id)); const slugs = [...new Set(sortedConnections.map((connection) => connection.serverSlug))]; const servers = await this.mcpRegistryService.getBySlugs(slugs); const serverBySlug = new Map(servers.map((server) => [server.slug, server])); const slugCounts = new Map(); const aiMcpFetch = (0, ai_proxy_fetch_1.createAiMcpFetch)(this.outboundHttp, this.ssrfConfig, this.ssrfProtectionService); const resolved = []; for (const connection of sortedConnections) { const server = serverBySlug.get(connection.serverSlug); if (!server) { this.logger.warn('Skipping MCP registry connection with missing server slug', { connectionId: connection.id, serverSlug: connection.serverSlug, userId: user.id, }); continue; } const resolvedServer = this.resolveRegistryServer(connection.id, connection.serverSlug, connection.credentialId, server.authType, server.remotes); if (!resolvedServer) { continue; } const nextCount = (slugCounts.get(resolvedServer.serverSlug) ?? 0) + 1; slugCounts.set(resolvedServer.serverSlug, nextCount); const serverConfig = { name: buildServerName(resolvedServer.serverSlug, nextCount), url: resolvedServer.endpointUrl, transport: resolvedServer.transport, cacheKey: `registry-connection:${connection.id}`, toolFilter: connection.toolFilter ?? undefined, metadata: { serverSlug: resolvedServer.serverSlug, userId: user.id }, }; if (resolvedServer.authType === 'oauth2') { const oauth2FetchContext = await this.buildOAuth2FetchContext(resolvedServer, user, connection.id); if (!oauth2FetchContext) { continue; } const requestFetch = await this.buildRegistryServerFetch(resolvedServer, user, connection.id, aiMcpFetch); if (!requestFetch) { continue; } serverConfig.fetch = requestFetch; } resolved.push(serverConfig); } return resolved; } resolveRegistryServer(connectionId, serverSlug, credentialId, authType, remotes) { const remote = getPreferredRemote(remotes); if (!remote) { this.logger.warn('Skipping MCP registry connection without supported remote transport', { connectionId, serverSlug, credentialId, }); return null; } return { serverSlug, credentialId, authType, endpointUrl: remote.endpointUrl, transport: remote.transport, }; } async buildRegistryServerFetch(config, user, connectionId, baseFetch) { if (config.authType !== 'oauth2') { return baseFetch; } const oauth2FetchContext = await this.buildOAuth2FetchContext(config, user, connectionId); if (!oauth2FetchContext) { return null; } return (0, auth_fetch_1.createAuthFetch)({ baseFetch, initialHeaders: { Authorization: `Bearer ${oauth2FetchContext.accessToken}` }, onUnauthorized: async () => { if (!oauth2FetchContext.projectId) { return null; } return await this.oauthService.refreshOAuth2CredentialById(oauth2FetchContext.credentialId, oauth2FetchContext.projectId); }, allowedDomains: (0, auth_fetch_1.resolveAllowedDomains)(oauth2FetchContext.credentialData), }); } async buildOAuth2FetchContext(config, user, connectionId) { const credentialWithData = await this.getCredentialWithData(config.credentialId, user); if (!credentialWithData) { this.logger.warn('Skipping MCP registry connection with inaccessible credential', { connectionId, serverSlug: config.serverSlug, credentialId: config.credentialId, userId: user.id, }); return null; } const tokenData = readOAuthTokenData(credentialWithData.data); if (!tokenData) { this.logger.warn('Skipping MCP registry connection without OAuth2 token data', { connectionId, serverSlug: config.serverSlug, credentialId: config.credentialId, }); return null; } const accessToken = readAccessToken(tokenData); if (!accessToken) { this.logger.warn('Skipping MCP registry connection without access token', { connectionId, serverSlug: config.serverSlug, credentialId: config.credentialId, }); return null; } const projectId = credentialWithData.credential.shared?.[0]?.projectId ?? null; if (!projectId) { this.logger.warn('Skipping OAuth2 token refresh for credential without project sharing', { connectionId, serverSlug: config.serverSlug, credentialId: config.credentialId, }); } return { credentialId: config.credentialId, accessToken, projectId, credentialData: credentialWithData.data, }; } async getCredentialWithData(credentialId, user) { const credential = await this.credentialsFinderService.findCredentialForUser(credentialId, user, ['credential:read']); if (!credential) { return null; } const data = await this.credentialsService.decrypt(credential, true); if (!(0, backend_common_1.isObjectLiteral)(data) || Object.keys(data).length === 0) { return null; } return { credential, data }; } async swapCredential(user, connection, newCredentialId) { const currentCredential = await this.credentialsFinderService.findCredentialForUser(connection.credentialId, user, ['credential:read']); if (!currentCredential) { throw new not_found_error_1.NotFoundError('Credential not found or not accessible'); } const newCredential = await this.credentialsFinderService.findCredentialForUser(newCredentialId, user, ['credential:read']); if (!newCredential) { throw new not_found_error_1.NotFoundError('Credential not found or not accessible'); } if (currentCredential.type !== newCredential.type) { throw new conflict_error_1.ConflictError('Cannot change credential to a different type'); } connection.credentialId = newCredentialId; } }; exports.InstanceAiMcpRegistryService = InstanceAiMcpRegistryService; exports.InstanceAiMcpRegistryService = InstanceAiMcpRegistryService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, instance_ai_mcp_registry_connection_repository_1.InstanceAiMcpRegistryConnectionRepository, mcp_registry_service_1.McpRegistryService, credentials_finder_service_1.CredentialsFinderService, credentials_service_1.CredentialsService, oauth_service_1.OauthService, event_service_1.EventService, backend_network_1.OutboundHttp, config_1.SsrfProtectionConfig, backend_network_1.SsrfProtectionService]) ], InstanceAiMcpRegistryService); function isUniqueConstraintViolation(error) { if (!(error instanceof typeorm_1.QueryFailedError)) return false; const driverError = error.driverError; const code = driverError?.code; return code === '23505' || code === 'SQLITE_CONSTRAINT_UNIQUE'; } //# sourceMappingURL=instance-ai-mcp-registry.service.js.map