n8n
Version:
n8n Workflow Automation Tool
379 lines • 18.1 kB
JavaScript
"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