UNPKG

n8n

Version:

n8n Workflow Automation Tool

244 lines 13.3 kB
"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