UNPKG

@n8n/n8n-nodes-langchain

Version:
306 lines 12.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getAllTools = getAllTools; exports.mapToNodeOperationError = mapToNodeOperationError; exports.connectMcpClient = connectMcpClient; exports.getAuthHeaders = getAuthHeaders; exports.tryRefreshOAuth2Token = tryRefreshOAuth2Token; exports.connectMcpClientForCredential = connectMcpClientForCredential; exports.isStructuredContent = isStructuredContent; const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js"); const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js"); const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js"); const ai_utilities_1 = require("@n8n/ai-utilities"); const result_1 = require("@n8n/utils/result"); const n8n_workflow_1 = require("n8n-workflow"); const types_1 = require("./types"); async function getAllTools(client, cursor) { const { tools, nextCursor } = await client.listTools({ cursor }); if (nextCursor) { return tools.concat(await getAllTools(client, nextCursor)); } return tools; } function safeCreateUrl(url, baseUrl) { try { return (0, result_1.createResultOk)(new URL(url, baseUrl)); } catch (error) { return (0, result_1.createResultError)(error); } } function normalizeAndValidateUrl(input) { const withProtocol = !/^https?:\/\//i.test(input) ? `https://${input}` : input; const parsedUrl = safeCreateUrl(withProtocol); if (!parsedUrl.ok) { return (0, result_1.createResultError)(parsedUrl.error); } return parsedUrl; } function errorHasCode(error, code) { return (!!error && typeof error === 'object' && (('code' in error && Number(error.code) === code) || ('message' in error && typeof error.message === 'string' && error.message.includes(code.toString())))); } function isUnauthorizedError(error) { return errorHasCode(error, 401); } function isForbiddenError(error) { return errorHasCode(error, 403); } function mapToNodeOperationError(node, error) { switch (error.type) { case 'cancelled': return new n8n_workflow_1.NodeOperationError(node, error.error, { message: 'Execution was cancelled', }); case 'invalid_url': return new n8n_workflow_1.NodeOperationError(node, error.error, { message: 'Could not connect to your MCP server. The provided URL is invalid.', }); case 'auth': return new n8n_workflow_1.NodeOperationError(node, error.error, { message: 'Could not connect to your MCP server. Authentication failed.', description: error.error.message, }); case 'connection': default: return new n8n_workflow_1.NodeOperationError(node, error.error, { message: 'Could not connect to your MCP server', description: error.error.message, }); } } async function connectMcpClient({ headers, serverTransport, endpointUrl, name, version, onUnauthorized, signal, allowedDomains, secureEgressFilter, }) { const endpoint = normalizeAndValidateUrl(endpointUrl); if (!endpoint.ok) { return (0, result_1.createResultError)({ type: 'invalid_url', error: endpoint.error }); } const authFetch = createAuthFetch(headers, secureEgressFilter, onUnauthorized, allowedDomains); const client = new index_js_1.Client({ name, version: version.toString() }, { capabilities: {} }); let onAbort; if (signal) { onAbort = () => { Promise.resolve(client.close()).catch(() => { }); }; signal.addEventListener('abort', onAbort, { once: true }); const originalClose = client.close.bind(client); client.close = async () => { if (onAbort && signal) { signal.removeEventListener('abort', onAbort); onAbort = undefined; } await originalClose(); }; } if (signal?.aborted) { if (onAbort && signal) { signal.removeEventListener('abort', onAbort); onAbort = undefined; } return (0, result_1.createResultError)({ type: 'cancelled', error: new Error('Execution was cancelled'), }); } if (serverTransport === 'httpStreamable') { try { const transport = new streamableHttp_js_1.StreamableHTTPClientTransport(endpoint.result, { fetch: authFetch, ...(signal ? { requestInit: { signal } } : {}), }); await client.connect(transport); return (0, result_1.createResultOk)(client); } catch (error) { const connectionError = error instanceof Error ? error : new Error(String(error)); if ((signal && connectionError.name === 'AbortError') || signal?.aborted) { if (onAbort && signal) { signal.removeEventListener('abort', onAbort); onAbort = undefined; } return (0, result_1.createResultError)({ type: 'cancelled', error: connectionError }); } if (onAbort && signal) { signal.removeEventListener('abort', onAbort); onAbort = undefined; } if (isUnauthorizedError(error) || isForbiddenError(error)) { return (0, result_1.createResultError)({ type: 'auth', error: error }); } else { return (0, result_1.createResultError)({ type: 'connection', error: error }); } } } try { const sseTransport = new sse_js_1.SSEClientTransport(endpoint.result, { eventSourceInit: { fetch: async (url, init) => await authFetch(url, { ...init, headers: { ...headersToRecord(init?.headers), Accept: 'text/event-stream', }, }), }, fetch: authFetch, ...(signal ? { requestInit: { signal } } : {}), }); await client.connect(sseTransport); return (0, result_1.createResultOk)(client); } catch (error) { const connectionError = error instanceof Error ? error : new Error(String(error)); if ((signal && connectionError.name === 'AbortError') || signal?.aborted) { if (onAbort && signal) { signal.removeEventListener('abort', onAbort); onAbort = undefined; } return (0, result_1.createResultError)({ type: 'cancelled', error: connectionError }); } if (onAbort && signal) { signal.removeEventListener('abort', onAbort); onAbort = undefined; } if (isUnauthorizedError(error) || isForbiddenError(error)) { return (0, result_1.createResultError)({ type: 'auth', error: error }); } else { return (0, result_1.createResultError)({ type: 'connection', error: error }); } } } function headersToRecord(headers) { return headers ? Object.fromEntries(new Headers(headers).entries()) : {}; } function createAuthFetch(initialHeaders, secureEgressFilter, onUnauthorized, allowedDomains) { const secureLookup = secureEgressFilter.createSecureLookup(); return (0, ai_utilities_1.createRefreshingAuthFetch)({ baseFetch: async (input, init) => await (0, ai_utilities_1.proxyFetch)({ input, init, lookup: secureLookup }), initialHeaders, ...(onUnauthorized ? { refreshHeaders: async (current) => await onUnauthorized(Object.fromEntries(current.entries())), } : {}), assertAllowedUrl: async (hopUrl) => { (0, n8n_workflow_1.assertUrlAllowed)({ url: hopUrl, allowedDomains }); const result = await secureEgressFilter.validateUrl(hopUrl); if (!result.ok) throw result.error; }, }); } async function getAuthHeaders(ctx, authentication) { if (authentication === 'none') return {}; let credentialType; if ((0, types_1.isMcpOAuth2Authentication)(authentication)) { credentialType = authentication; } else { const credentialTypes = { headerAuth: 'httpHeaderAuth', bearerAuth: 'httpBearerAuth', multipleHeadersAuth: 'httpMultipleHeadersAuth', }; credentialType = credentialTypes[authentication]; if (!credentialType) return {}; } const credentials = await ctx .getCredentials(credentialType) .catch(() => null); if (!credentials) return {}; if ((0, types_1.isMcpOAuth2Authentication)(authentication) && (0, n8n_workflow_1.shouldRefreshMcpOAuth2Token)(credentials.oauthTokenData, credentials.grantType)) { const refreshedHeaders = await tryRefreshOAuth2Token(ctx, authentication); if (refreshedHeaders) return { headers: refreshedHeaders, credentials }; } const headers = (0, n8n_workflow_1.getMcpAuthHeaders)(authentication, credentials); return Object.keys(headers).length > 0 ? { headers, credentials } : { credentials }; } async function tryRefreshOAuth2Token(ctx, authentication, headers) { if (!(0, types_1.isMcpOAuth2Authentication)(authentication)) { return null; } let access_token = null; try { const result = (await ctx.helpers.refreshOAuth2Token.call(ctx, authentication)); access_token = result?.access_token; } catch (error) { return null; } if (!access_token) { return null; } if (!headers) { return { Authorization: `Bearer ${access_token}`, }; } const headersWithoutAuthorization = Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== 'authorization')); return { ...headersWithoutAuthorization, Authorization: `Bearer ${access_token}`, }; } async function connectMcpClientForCredential(ctx, config) { const node = ctx.getNode(); const { headers, credentials } = await getAuthHeaders(ctx, config.authentication); const isOAuth2 = (0, types_1.isMcpOAuth2Authentication)(config.authentication); let endpointUrl = config.endpointUrl; let serverTransport = config.serverTransport; let authHeaders = headers; let allowedDomains; if (config.registryCredential) { if (!credentials) { throw new n8n_workflow_1.NodeOperationError(node, 'No MCP OAuth2 credential type found'); } const prepared = config.registryCredential.prepareConnection({ connection: config.registryCredential.connection, credentialType: config.registryCredential.credentialType, credentialData: credentials, headers, }); if (!prepared.ok) { throw new n8n_workflow_1.NodeOperationError(node, prepared.error.message); } endpointUrl = prepared.value.endpointUrl; serverTransport = prepared.value.transport; authHeaders = prepared.value.headers; allowedDomains = prepared.value.allowedDomains; } else if (credentials) { allowedDomains = (0, n8n_workflow_1.assertCredentialAllowsUrl)({ node, credentialData: credentials, url: endpointUrl, surface: config.surface, }); } return await connectMcpClient({ serverTransport, endpointUrl, headers: authHeaders, allowedDomains, secureEgressFilter: ctx.helpers.getSecureEgressFilter(), name: node.type, version: node.typeVersion, onUnauthorized: isOAuth2 ? async (h) => await tryRefreshOAuth2Token(ctx, config.authentication, h) : undefined, signal: config.signal, }); } function isStructuredContent(value) { return (value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value)); } //# sourceMappingURL=utils.js.map