Skip to main content
Glama

n8n-MCP

by 88-888
http-server-n8n-mode.test.ts23.5 kB
import { describe, it, expect, beforeEach, afterEach, vi, MockedFunction } from 'vitest'; import type { Request, Response, NextFunction } from 'express'; import { SingleSessionHTTPServer } from '../../src/http-server-single-session'; // Mock dependencies vi.mock('../../src/utils/logger', () => ({ logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() } })); vi.mock('dotenv'); vi.mock('../../src/mcp/server', () => ({ N8NDocumentationMCPServer: vi.fn().mockImplementation(() => ({ connect: vi.fn().mockResolvedValue(undefined) })) })); vi.mock('@modelcontextprotocol/sdk/server/streamableHttp.js', () => ({ StreamableHTTPServerTransport: vi.fn().mockImplementation(() => ({ handleRequest: vi.fn().mockImplementation(async (req: any, res: any) => { // Simulate successful MCP response if (process.env.N8N_MODE === 'true') { res.setHeader('Mcp-Session-Id', 'single-session'); } res.status(200).json({ jsonrpc: '2.0', result: { success: true }, id: 1 }); }), close: vi.fn().mockResolvedValue(undefined) })) })); // Create a mock console manager instance const mockConsoleManager = { wrapOperation: vi.fn().mockImplementation(async (fn: () => Promise<any>) => { return await fn(); }) }; vi.mock('../../src/utils/console-manager', () => ({ ConsoleManager: vi.fn(() => mockConsoleManager) })); vi.mock('../../src/utils/url-detector', () => ({ getStartupBaseUrl: vi.fn((host: string, port: number) => `http://localhost:${port || 3000}`), formatEndpointUrls: vi.fn((baseUrl: string) => ({ health: `${baseUrl}/health`, mcp: `${baseUrl}/mcp` })), detectBaseUrl: vi.fn((req: any, host: string, port: number) => `http://localhost:${port || 3000}`) })); vi.mock('../../src/utils/version', () => ({ PROJECT_VERSION: '2.8.1' })); // Create handlers storage outside of mocks const mockHandlers: { [key: string]: any[] } = { get: [], post: [], delete: [], use: [] }; vi.mock('express', () => { // Create Express app mock inside the factory const mockExpressApp = { get: vi.fn((path: string, ...handlers: any[]) => { mockHandlers.get.push({ path, handlers }); return mockExpressApp; }), post: vi.fn((path: string, ...handlers: any[]) => { mockHandlers.post.push({ path, handlers }); return mockExpressApp; }), delete: vi.fn((path: string, ...handlers: any[]) => { // Store delete handlers in the same way as other methods if (!mockHandlers.delete) mockHandlers.delete = []; mockHandlers.delete.push({ path, handlers }); return mockExpressApp; }), use: vi.fn((handler: any) => { mockHandlers.use.push(handler); return mockExpressApp; }), set: vi.fn(), listen: vi.fn((port: number, host: string, callback?: () => void) => { if (callback) callback(); return { on: vi.fn(), close: vi.fn((cb: () => void) => cb()), address: () => ({ port: 3000 }) }; }) }; // Create a properly typed mock for express with both app factory and middleware methods interface ExpressMock { (): typeof mockExpressApp; json(): (req: any, res: any, next: any) => void; } const expressMock = vi.fn(() => mockExpressApp) as unknown as ExpressMock; expressMock.json = vi.fn(() => (req: any, res: any, next: any) => { // Mock JSON parser middleware req.body = req.body || {}; next(); }); return { default: expressMock, Request: {}, Response: {}, NextFunction: {} }; }); describe('HTTP Server n8n Mode', () => { const originalEnv = process.env; const TEST_AUTH_TOKEN = 'test-auth-token-with-more-than-32-characters'; let server: SingleSessionHTTPServer; let consoleLogSpy: any; let consoleWarnSpy: any; let consoleErrorSpy: any; beforeEach(() => { // Reset environment process.env = { ...originalEnv }; process.env.AUTH_TOKEN = TEST_AUTH_TOKEN; process.env.PORT = '0'; // Use random port for tests // Mock console methods to prevent output during tests consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); // Clear all mocks and handlers vi.clearAllMocks(); mockHandlers.get = []; mockHandlers.post = []; mockHandlers.delete = []; mockHandlers.use = []; }); afterEach(async () => { // Restore environment process.env = originalEnv; // Restore console methods consoleLogSpy.mockRestore(); consoleWarnSpy.mockRestore(); consoleErrorSpy.mockRestore(); // Shutdown server if running if (server) { await server.shutdown(); server = null as any; } }); // Helper to find a route handler function findHandler(method: 'get' | 'post' | 'delete', path: string) { const routes = mockHandlers[method]; const route = routes.find(r => r.path === path); return route ? route.handlers[route.handlers.length - 1] : null; } // Helper to create mock request/response function createMockReqRes() { const headers: { [key: string]: string } = {}; const res = { status: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis(), send: vi.fn().mockReturnThis(), setHeader: vi.fn((key: string, value: string) => { headers[key.toLowerCase()] = value; }), sendStatus: vi.fn().mockReturnThis(), headersSent: false, getHeader: (key: string) => headers[key.toLowerCase()], headers }; const req = { method: 'GET', path: '/', headers: {} as Record<string, string>, body: {}, ip: '127.0.0.1', get: vi.fn((header: string) => (req.headers as Record<string, string>)[header.toLowerCase()]) }; return { req, res }; } describe('Protocol Version Endpoint (GET /mcp)', () => { it('should return standard response when N8N_MODE is not set', async () => { delete process.env.N8N_MODE; server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('get', '/mcp'); expect(handler).toBeTruthy(); const { req, res } = createMockReqRes(); await handler(req, res); expect(res.json).toHaveBeenCalledWith({ description: 'n8n Documentation MCP Server', version: '2.8.1', endpoints: { mcp: { method: 'POST', path: '/mcp', description: 'Main MCP JSON-RPC endpoint', authentication: 'Bearer token required' }, health: { method: 'GET', path: '/health', description: 'Health check endpoint', authentication: 'None' }, root: { method: 'GET', path: '/', description: 'API information', authentication: 'None' } }, documentation: 'https://github.com/czlonkowski/n8n-mcp' }); }); it('should return protocol version when N8N_MODE=true', async () => { process.env.N8N_MODE = 'true'; server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('get', '/mcp'); expect(handler).toBeTruthy(); const { req, res } = createMockReqRes(); await handler(req, res); // When N8N_MODE is true, should return protocol version and server info expect(res.json).toHaveBeenCalledWith({ protocolVersion: '2024-11-05', serverInfo: { name: 'n8n-mcp', version: '2.8.1', capabilities: { tools: {} } } }); }); }); describe('Session ID Header (POST /mcp)', () => { it('should handle POST request when N8N_MODE is not set', async () => { delete process.env.N8N_MODE; server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('post', '/mcp'); expect(handler).toBeTruthy(); const { req, res } = createMockReqRes(); req.headers = { authorization: `Bearer ${TEST_AUTH_TOKEN}` }; req.method = 'POST'; req.body = { jsonrpc: '2.0', method: 'test', params: {}, id: 1 }; // The handler should call handleRequest which wraps the operation await handler(req, res); // Verify the ConsoleManager's wrapOperation was called expect(mockConsoleManager.wrapOperation).toHaveBeenCalled(); // In normal mode, no special headers should be set by our code // The transport handles the actual response }); it('should handle POST request when N8N_MODE=true', async () => { process.env.N8N_MODE = 'true'; server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('post', '/mcp'); expect(handler).toBeTruthy(); const { req, res } = createMockReqRes(); req.headers = { authorization: `Bearer ${TEST_AUTH_TOKEN}` }; req.method = 'POST'; req.body = { jsonrpc: '2.0', method: 'test', params: {}, id: 1 }; await handler(req, res); // Verify the ConsoleManager's wrapOperation was called expect(mockConsoleManager.wrapOperation).toHaveBeenCalled(); // In N8N_MODE, the transport mock is configured to set the Mcp-Session-Id header // This is testing that the environment variable is properly passed through }); }); describe('Error Response Format', () => { it('should use JSON-RPC error format for auth errors', async () => { delete process.env.N8N_MODE; server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('post', '/mcp'); expect(handler).toBeTruthy(); // Test missing auth header const { req, res } = createMockReqRes(); req.method = 'POST'; await handler(req, res); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ jsonrpc: '2.0', error: { code: -32001, message: 'Unauthorized' }, id: null }); }); it('should handle invalid auth token', async () => { server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('post', '/mcp'); expect(handler).toBeTruthy(); const { req, res } = createMockReqRes(); req.headers = { authorization: 'Bearer invalid-token' }; req.method = 'POST'; await handler(req, res); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ jsonrpc: '2.0', error: { code: -32001, message: 'Unauthorized' }, id: null }); }); it('should handle invalid auth header format', async () => { server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('post', '/mcp'); expect(handler).toBeTruthy(); const { req, res } = createMockReqRes(); req.headers = { authorization: 'Basic sometoken' }; // Wrong format req.method = 'POST'; await handler(req, res); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ jsonrpc: '2.0', error: { code: -32001, message: 'Unauthorized' }, id: null }); }); }); describe('Normal Mode Behavior', () => { it('should maintain standard behavior for health endpoint', async () => { // Test both with and without N8N_MODE for (const n8nMode of [undefined, 'true', 'false']) { if (n8nMode === undefined) { delete process.env.N8N_MODE; } else { process.env.N8N_MODE = n8nMode; } server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('get', '/health'); expect(handler).toBeTruthy(); const { req, res } = createMockReqRes(); await handler(req, res); expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ status: 'ok', mode: 'sdk-pattern-transports', // Updated mode name after refactoring version: '2.8.1' })); await server.shutdown(); } }); it('should maintain standard behavior for root endpoint', async () => { // Test both with and without N8N_MODE for (const n8nMode of [undefined, 'true', 'false']) { if (n8nMode === undefined) { delete process.env.N8N_MODE; } else { process.env.N8N_MODE = n8nMode; } server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('get', '/'); expect(handler).toBeTruthy(); const { req, res } = createMockReqRes(); await handler(req, res); expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ name: 'n8n Documentation MCP Server', version: '2.8.1', endpoints: expect.any(Object), authentication: expect.any(Object) })); await server.shutdown(); } }); }); describe('Edge Cases', () => { it('should handle N8N_MODE with various values', async () => { const testValues = ['true', 'TRUE', '1', 'yes', 'false', '']; for (const value of testValues) { process.env.N8N_MODE = value; server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('get', '/mcp'); expect(handler).toBeTruthy(); const { req, res } = createMockReqRes(); await handler(req, res); // Only exactly 'true' should enable n8n mode if (value === 'true') { expect(res.json).toHaveBeenCalledWith({ protocolVersion: '2024-11-05', serverInfo: { name: 'n8n-mcp', version: '2.8.1', capabilities: { tools: {} } } }); } else { expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ description: 'n8n Documentation MCP Server' })); } await server.shutdown(); } }); it('should handle OPTIONS requests for CORS', async () => { server = new SingleSessionHTTPServer(); await server.start(); const { req, res } = createMockReqRes(); req.method = 'OPTIONS'; // Call each middleware to find the CORS one for (const middleware of mockHandlers.use) { if (typeof middleware === 'function') { const next = vi.fn(); await middleware(req, res, next); if (res.sendStatus.mock.calls.length > 0) { // Found the CORS middleware - verify it was called expect(res.sendStatus).toHaveBeenCalledWith(204); // Check that CORS headers were set (order doesn't matter) const setHeaderCalls = (res.setHeader as any).mock.calls; const headerMap = new Map(setHeaderCalls); expect(headerMap.has('Access-Control-Allow-Origin')).toBe(true); expect(headerMap.has('Access-Control-Allow-Methods')).toBe(true); expect(headerMap.has('Access-Control-Allow-Headers')).toBe(true); expect(headerMap.get('Access-Control-Allow-Methods')).toBe('POST, GET, DELETE, OPTIONS'); break; } } } }); it('should validate session info methods', async () => { server = new SingleSessionHTTPServer(); await server.start(); // Initially no session let sessionInfo = server.getSessionInfo(); expect(sessionInfo.active).toBe(false); // The getSessionInfo method should return proper structure expect(sessionInfo).toHaveProperty('active'); // Test that the server instance has the expected methods expect(typeof server.getSessionInfo).toBe('function'); expect(typeof server.start).toBe('function'); expect(typeof server.shutdown).toBe('function'); }); }); describe('404 Handler', () => { it('should handle 404 errors correctly', async () => { server = new SingleSessionHTTPServer(); await server.start(); // The 404 handler is added with app.use() without a path // Find the last middleware that looks like a 404 handler const notFoundHandler = mockHandlers.use[mockHandlers.use.length - 2]; // Second to last (before error handler) const { req, res } = createMockReqRes(); req.method = 'POST'; req.path = '/nonexistent'; await notFoundHandler(req, res); expect(res.status).toHaveBeenCalledWith(404); expect(res.json).toHaveBeenCalledWith({ error: 'Not found', message: 'Cannot POST /nonexistent' }); }); it('should handle GET requests to non-existent paths', async () => { server = new SingleSessionHTTPServer(); await server.start(); const notFoundHandler = mockHandlers.use[mockHandlers.use.length - 2]; const { req, res } = createMockReqRes(); req.method = 'GET'; req.path = '/unknown-endpoint'; await notFoundHandler(req, res); expect(res.status).toHaveBeenCalledWith(404); expect(res.json).toHaveBeenCalledWith({ error: 'Not found', message: 'Cannot GET /unknown-endpoint' }); }); }); describe('Security Features', () => { it('should handle malformed authorization headers', async () => { server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('post', '/mcp'); const testCases = [ '', // Empty header 'Bearer', // Missing token 'Bearer ', // Space but no token 'InvalidFormat token', // Wrong scheme 'Bearer token with spaces' // Token with spaces ]; for (const authHeader of testCases) { const { req, res } = createMockReqRes(); req.headers = { authorization: authHeader }; req.method = 'POST'; await handler(req, res); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ jsonrpc: '2.0', error: { code: -32001, message: 'Unauthorized' }, id: null }); // Reset mocks for next test vi.clearAllMocks(); } }); it('should verify server configuration methods exist', async () => { server = new SingleSessionHTTPServer(); // Test that the server has expected methods expect(typeof server.start).toBe('function'); expect(typeof server.shutdown).toBe('function'); expect(typeof server.getSessionInfo).toBe('function'); // Basic session info structure const sessionInfo = server.getSessionInfo(); expect(sessionInfo).toHaveProperty('active'); expect(typeof sessionInfo.active).toBe('boolean'); }); it('should handle valid auth tokens properly', async () => { server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('post', '/mcp'); const { req, res } = createMockReqRes(); req.headers = { authorization: `Bearer ${TEST_AUTH_TOKEN}` }; req.method = 'POST'; req.body = { jsonrpc: '2.0', method: 'test', id: 1 }; await handler(req, res); // Should not return 401 for valid tokens - the transport handles the actual response expect(res.status).not.toHaveBeenCalledWith(401); // The actual response handling is done by the transport mock expect(mockConsoleManager.wrapOperation).toHaveBeenCalled(); }); it('should handle DELETE endpoint without session ID', async () => { server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('delete', '/mcp'); expect(handler).toBeTruthy(); // Test DELETE without Mcp-Session-Id header (not auth-related) const { req, res } = createMockReqRes(); req.method = 'DELETE'; await handler(req, res); // DELETE endpoint returns 400 for missing Mcp-Session-Id header, not 401 for auth expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith({ jsonrpc: '2.0', error: { code: -32602, message: 'Mcp-Session-Id header is required' }, id: null }); }); it('should provide proper error details for debugging', async () => { server = new SingleSessionHTTPServer(); await server.start(); const handler = findHandler('post', '/mcp'); const { req, res } = createMockReqRes(); req.method = 'POST'; // No auth header at all await handler(req, res); // Verify error response format expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ jsonrpc: '2.0', error: { code: -32001, message: 'Unauthorized' }, id: null }); }); }); describe('Express Middleware Configuration', () => { it('should configure all necessary middleware', async () => { server = new SingleSessionHTTPServer(); await server.start(); // Verify that various middleware types are configured expect(mockHandlers.use.length).toBeGreaterThan(3); // Should have JSON parser middleware const hasJsonMiddleware = mockHandlers.use.some(middleware => { // Check if it's the JSON parser by calling it and seeing if it sets req.body try { const mockReq = { body: undefined }; const mockRes = {}; const mockNext = vi.fn(); if (typeof middleware === 'function') { middleware(mockReq, mockRes, mockNext); return mockNext.mock.calls.length > 0; } } catch (e) { // Ignore errors in middleware detection } return false; }); expect(mockHandlers.use.length).toBeGreaterThan(0); }); it('should handle CORS preflight for different methods', async () => { server = new SingleSessionHTTPServer(); await server.start(); const corsTestMethods = ['POST', 'GET', 'DELETE', 'PUT']; for (const method of corsTestMethods) { const { req, res } = createMockReqRes(); req.method = 'OPTIONS'; req.headers['access-control-request-method'] = method; // Find and call CORS middleware for (const middleware of mockHandlers.use) { if (typeof middleware === 'function') { const next = vi.fn(); await middleware(req, res, next); if (res.sendStatus.mock.calls.length > 0) { expect(res.sendStatus).toHaveBeenCalledWith(204); break; } } } vi.clearAllMocks(); } }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/88-888/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server