@n8n/n8n-nodes-langchain
Version:
306 lines • 12.3 kB
JavaScript
;
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