n8n
Version:
n8n Workflow Automation Tool
244 lines • 13.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const jest_mock_extended_1 = require("jest-mock-extended");
const n8n_core_1 = require("n8n-core");
const logger = (0, jest_mock_extended_1.mock)();
const mcp_registry_node_loader_1 = require("./mcp-registry-node-loader");
const node_description_transform_1 = require("./node-description-transform");
const mock_servers_1 = require("./registry/mock-servers");
const baseDescription = {
displayName: 'MCP Registry Client (internal)',
name: 'mcpRegistryClientTool',
hidden: true,
group: ['output'],
version: 1,
description: 'Runtime backing for MCP registry-derived nodes',
defaults: { name: 'MCP Registry Client' },
codex: {
categories: ['AI'],
subcategories: { AI: ['Model Context Protocol'] },
alias: ['MCP', 'Model Context Protocol'],
},
inputs: [],
outputs: [],
credentials: [{ name: 'mcpOAuth2Api', required: true }],
properties: [
{ displayName: 'Endpoint URL', name: 'endpointUrl', type: 'hidden', default: '' },
{
displayName: 'Server Transport',
name: 'serverTransport',
type: 'hidden',
default: 'httpStreamable',
},
],
};
function createBaseNodeClass() {
const baseInstance = {
description: baseDescription,
methods: {
loadOptions: {
getTools: jest.fn(),
},
},
};
return baseInstance;
}
function createLoadNodesAndCredentials(options) {
const baseNode = createBaseNodeClass();
const sourcePath = '/path/to/McpRegistryClientTool.node.js';
const langchainLoader = (0, jest_mock_extended_1.mock)();
langchainLoader.getNode.mockImplementation((nodeType) => {
if (options?.withBaseNode !== false && nodeType === node_description_transform_1.MCP_REGISTRY_BASE_NODE_NAME) {
return { type: baseNode, sourcePath };
}
throw new Error('not found');
});
const loaders = options?.withLangchainLoader === false ? {} : { [node_description_transform_1.LANGCHAIN_PACKAGE_NAME]: langchainLoader };
const loadNodesAndCredentials = (0, jest_mock_extended_1.mock)({
loaders,
});
return { loadNodesAndCredentials, baseNode, sourcePath };
}
function createServiceWithServers(servers) {
const service = (0, jest_mock_extended_1.mock)();
service.getAll.mockReturnValue(servers);
return service;
}
describe('McpRegistryNodeLoader', () => {
describe('packageName', () => {
it('matches MCP_REGISTRY_PACKAGE_NAME', () => {
const { loadNodesAndCredentials } = createLoadNodesAndCredentials();
const service = createServiceWithServers([]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
expect(loader.packageName).toBe(node_description_transform_1.MCP_REGISTRY_PACKAGE_NAME);
});
});
describe('loadAll', () => {
it('populates `types`, `known`, registers synthetic nodes and credentials for each supported server', async () => {
const { loadNodesAndCredentials, sourcePath } = createLoadNodesAndCredentials();
const service = createServiceWithServers([mock_servers_1.notionMockServer]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
await loader.loadAll();
expect(loader.types.nodes).toHaveLength(1);
expect(loader.types.nodes[0]).toMatchObject({
name: 'notion',
displayName: 'Notion MCP',
});
expect(loader.types.credentials).toHaveLength(1);
expect(loader.types.credentials[0]).toMatchObject({
name: 'notionMcpOAuth2Api',
displayName: 'Notion MCP OAuth2',
});
const loadedNode = loader.getNode('notion');
const loadedCredential = loader.getCredential('notionMcpOAuth2Api');
expect(loadedNode).toBeDefined();
expect(loadedNode.sourcePath).toBe(sourcePath);
expect(loadedCredential).toBeDefined();
expect(loadedCredential.sourcePath).toBe('');
expect(loader.known.nodes.notion).toEqual({
className: 'McpRegistryClientTool',
sourcePath,
});
expect(loader.known.credentials.notionMcpOAuth2Api).toEqual({
className: 'McpRegistryApi',
sourcePath: '',
extends: ['mcpOAuth2Api'],
supportedNodes: ['notion'],
});
});
it('inherits prototype methods from the base node class on synthetic nodes', async () => {
const { loadNodesAndCredentials, baseNode } = createLoadNodesAndCredentials();
const service = createServiceWithServers([mock_servers_1.notionMockServer]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
await loader.loadAll();
const synthetic = loader.getNode('notion').type;
expect(synthetic.methods).toBe(baseNode.methods);
expect(synthetic.description.name).toBe('notion');
expect(synthetic.description.displayName).toBe('Notion MCP');
});
it('skips servers whose remote selection returns null', async () => {
const unsupportedServer = {
...mock_servers_1.notionMockServer,
slug: 'no-remotes',
title: 'No Remotes',
remotes: [],
};
const { loadNodesAndCredentials } = createLoadNodesAndCredentials();
const service = createServiceWithServers([mock_servers_1.notionMockServer, unsupportedServer]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
await loader.loadAll();
expect(loader.types.nodes).toHaveLength(1);
expect(loader.types.nodes[0].name).toBe('notion');
expect(loader.types.credentials).toHaveLength(1);
expect(loader.types.credentials[0].name).toBe('notionMcpOAuth2Api');
expect(() => loader.getNode('noRemotes')).toThrow(n8n_core_1.UnrecognizedNodeTypeError);
expect(() => loader.getCredential('noRemotesMcpOAuth2Api')).toThrow(n8n_core_1.UnrecognizedCredentialTypeError);
});
it('no-ops when the langchain loader is missing', async () => {
const { loadNodesAndCredentials } = createLoadNodesAndCredentials({
withLangchainLoader: false,
});
const service = createServiceWithServers([mock_servers_1.notionMockServer]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
await loader.loadAll();
expect(loader.types.nodes).toHaveLength(0);
expect(loader.types.credentials).toHaveLength(0);
});
it('no-ops when the base node is not registered on the langchain loader', async () => {
const { loadNodesAndCredentials } = createLoadNodesAndCredentials({
withBaseNode: false,
});
const service = createServiceWithServers([mock_servers_1.notionMockServer]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
await loader.loadAll();
expect(loader.types.nodes).toHaveLength(0);
expect(loader.types.credentials).toHaveLength(0);
});
it('resets prior state before loading', async () => {
const { loadNodesAndCredentials } = createLoadNodesAndCredentials();
const service = createServiceWithServers([mock_servers_1.notionMockServer]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
await loader.loadAll();
await loader.loadAll();
expect(loader.types.nodes).toHaveLength(1);
expect(loader.types.credentials).toHaveLength(1);
});
it('requests deprecated servers from the registry so existing workflows keep loading', async () => {
const { loadNodesAndCredentials } = createLoadNodesAndCredentials();
const service = createServiceWithServers([mock_servers_1.notionMockServer]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
await loader.loadAll();
expect(service.getAll).toHaveBeenCalledWith({ includeDeprecated: true });
});
});
describe('getNode', () => {
it('returns the synthetic LoadedClass for a known type', async () => {
const { loadNodesAndCredentials } = createLoadNodesAndCredentials();
const service = createServiceWithServers([mock_servers_1.notionMockServer]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
await loader.loadAll();
const result = loader.getNode('notion');
expect(result.type).toBeDefined();
expect(result.type.description.name).toBe('notion');
});
it('throws UnrecognizedNodeTypeError for an unknown type', () => {
const { loadNodesAndCredentials } = createLoadNodesAndCredentials();
const service = createServiceWithServers([]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
expect(() => loader.getNode('unknown')).toThrow(n8n_core_1.UnrecognizedNodeTypeError);
});
});
describe('getCredential', () => {
it('returns the credential for a known credential type', async () => {
const { loadNodesAndCredentials } = createLoadNodesAndCredentials();
const service = createServiceWithServers([mock_servers_1.notionMockServer]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
await loader.loadAll();
const result = loader.getCredential('notionMcpOAuth2Api');
expect(result.type).toBeDefined();
expect(result.type.name).toBe('notionMcpOAuth2Api');
});
it('throws UnrecognizedCredentialTypeError for an unknown credential type', () => {
const { loadNodesAndCredentials } = createLoadNodesAndCredentials();
const service = createServiceWithServers([]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
expect(() => loader.getCredential('unknown')).toThrow(n8n_core_1.UnrecognizedCredentialTypeError);
});
});
describe('state management', () => {
it('reset clears known, types, and registered node types', async () => {
const { loadNodesAndCredentials } = createLoadNodesAndCredentials();
const service = createServiceWithServers([mock_servers_1.notionMockServer]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
await loader.loadAll();
loader.reset();
expect(loader.types.nodes).toEqual([]);
expect(loader.types.credentials).toEqual([]);
expect(loader.known.nodes).toEqual({});
expect(loader.known.credentials).toEqual({});
expect(() => loader.getNode('notion')).toThrow(n8n_core_1.UnrecognizedNodeTypeError);
expect(() => loader.getCredential('notionMcpOAuth2Api')).toThrow(n8n_core_1.UnrecognizedCredentialTypeError);
});
it('releaseTypes only clears types', async () => {
const { loadNodesAndCredentials } = createLoadNodesAndCredentials();
const service = createServiceWithServers([mock_servers_1.notionMockServer]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
await loader.loadAll();
loader.releaseTypes();
expect(loader.types.nodes).toEqual([]);
expect(loader.types.credentials).toEqual([]);
expect(loader.getNode('notion')).toBeDefined();
expect(loader.getCredential('notionMcpOAuth2Api')).toBeDefined();
});
it('ensureTypesLoaded calls loadAll only when types are empty', async () => {
const { loadNodesAndCredentials } = createLoadNodesAndCredentials();
const service = createServiceWithServers([mock_servers_1.notionMockServer]);
const loader = new mcp_registry_node_loader_1.McpRegistryNodeLoader(service, loadNodesAndCredentials, logger);
await loader.ensureTypesLoaded();
expect(service.getAll).toHaveBeenCalledTimes(1);
await loader.ensureTypesLoaded();
expect(service.getAll).toHaveBeenCalledTimes(1);
});
});
});
//# sourceMappingURL=mcp-registry-node-loader.test.js.map