first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,349 @@
import {
createMockLogger,
createMockRequest,
createMockRequestWithSessionId,
createMockRequestWithHeaderSessionId,
createMockResponse,
createMockTool,
createMockTransport,
createValidToolCallMessage,
createListToolsMessage,
createMockServer,
MCP_SESSION_ID_HEADER,
} from './helpers';
import { QueuedExecutionStrategy } from '../execution/QueuedExecutionStrategy';
import { McpServer } from '../McpServer';
import { InMemorySessionStore } from '../session/InMemorySessionStore';
describe('McpServer', () => {
let mcpServer: McpServer;
let mockLogger: ReturnType<typeof createMockLogger>;
beforeEach(() => {
// Reset singleton for testing
(McpServer as unknown as { instance_: McpServer | undefined }).instance_ = undefined;
mockLogger = createMockLogger();
mcpServer = McpServer.instance(mockLogger);
});
afterEach(() => {
// Clean up singleton
(McpServer as unknown as { instance_: McpServer | undefined }).instance_ = undefined;
});
describe('singleton pattern', () => {
it('should return same instance for subsequent calls', () => {
const instance1 = McpServer.instance(mockLogger);
const instance2 = McpServer.instance(mockLogger);
expect(instance1).toBe(instance2);
});
it('should log debug message when creating singleton', () => {
expect(mockLogger.debug).toHaveBeenCalledWith('McpServer created');
expect(mockLogger.debug).toHaveBeenCalledWith('Created singleton McpServer');
});
});
describe('getSessionId', () => {
it('should extract sessionId from query parameters', () => {
const req = createMockRequestWithSessionId('session-123', '{}');
expect(mcpServer.getSessionId(req)).toBe('session-123');
});
it('should extract sessionId from mcp-session-id header', () => {
const req = createMockRequestWithHeaderSessionId('header-session-456');
expect(mcpServer.getSessionId(req)).toBe('header-session-456');
});
it('should prefer query parameter over header', () => {
const req = createMockRequest({
query: { sessionId: 'query-session' },
headers: { [MCP_SESSION_ID_HEADER]: 'header-session' },
});
expect(mcpServer.getSessionId(req)).toBe('query-session');
});
it('should return undefined when no sessionId present', () => {
const req = createMockRequest({ query: {}, headers: {} });
expect(mcpServer.getSessionId(req)).toBeUndefined();
});
});
describe('getMcpMetadata', () => {
it('should extract sessionId and messageId from request', () => {
const rawBody = '{"jsonrpc":"2.0","id":"msg-123","method":"test"}';
const req = createMockRequestWithSessionId('session-1', rawBody);
const metadata = mcpServer.getMcpMetadata(req);
expect(metadata).toEqual({
sessionId: 'session-1',
messageId: 'msg-123',
});
});
it('should return empty messageId when not present in body', () => {
const rawBody = '{"jsonrpc":"2.0","method":"notification"}';
const req = createMockRequestWithSessionId('session-1', rawBody);
const metadata = mcpServer.getMcpMetadata(req);
expect(metadata).toEqual({
sessionId: 'session-1',
messageId: '',
});
});
it('should return undefined when no sessionId', () => {
const req = createMockRequest({ query: {}, headers: {} });
expect(mcpServer.getMcpMetadata(req)).toBeUndefined();
});
});
describe('handlePostMessage', () => {
it('should return 401 when no transport found for session', async () => {
const response = createMockResponse();
const request = createMockRequestWithSessionId('non-existent', '{}');
await mcpServer.handlePostMessage(request, response, []);
expect(response.status).toHaveBeenCalledWith(401);
expect(response.send).toHaveBeenCalledWith('No transport found for sessionId');
});
it('should identify tool call messages', async () => {
const response = createMockResponse();
const toolCallBody = createValidToolCallMessage('get_weather', { city: 'London' });
const request = createMockRequestWithSessionId('non-existent', toolCallBody);
const result = await mcpServer.handlePostMessage(request, response, []);
expect(result.wasToolCall).toBe(true);
expect(result.toolCallInfo).toEqual({
toolName: 'get_weather',
arguments: { city: 'London' },
});
});
it('should add sourceNodeName from tool metadata', async () => {
const response = createMockResponse();
const tools = [
createMockTool('get_weather', {
metadata: { sourceNodeName: 'Weather Node' },
}),
];
const toolCallBody = createValidToolCallMessage('get_weather', { city: 'London' });
const request = createMockRequestWithSessionId('non-existent', toolCallBody);
const result = await mcpServer.handlePostMessage(request, response, tools);
expect(result.toolCallInfo).toEqual({
toolName: 'get_weather',
arguments: { city: 'London' },
sourceNodeName: 'Weather Node',
});
});
it('should identify non-tool-call messages', async () => {
const response = createMockResponse();
const listToolsBody = createListToolsMessage();
const request = createMockRequestWithSessionId('non-existent', listToolsBody);
const result = await mcpServer.handlePostMessage(request, response, []);
expect(result.wasToolCall).toBe(false);
});
});
describe('handleDeleteRequest', () => {
it('should return 400 when no sessionId provided', async () => {
const response = createMockResponse();
const request = {
query: {},
headers: {},
rawBody: Buffer.from('{}'),
} as unknown as Parameters<typeof mcpServer.handleDeleteRequest>[0];
await mcpServer.handleDeleteRequest(request, response);
expect(response.status).toHaveBeenCalledWith(400);
expect(response.send).toHaveBeenCalledWith('No sessionId provided');
});
it('should return 404 when session not found', async () => {
const response = createMockResponse();
const request = createMockRequestWithSessionId('non-existent', '{}');
await mcpServer.handleDeleteRequest(request, response);
expect(response.status).toHaveBeenCalledWith(404);
expect(response.send).toHaveBeenCalledWith('Session not found');
});
});
describe('configuration', () => {
it('should allow setting custom session store', () => {
const customStore = new InMemorySessionStore();
mcpServer.setSessionStore(customStore);
// Verify the store is used (indirect test)
expect(mcpServer).toBeDefined();
});
it('should allow setting execution strategy', () => {
const queuedStrategy = new QueuedExecutionStrategy(mcpServer.getPendingCallsManager());
mcpServer.setExecutionStrategy(queuedStrategy);
expect(mcpServer.isQueueMode()).toBe(true);
});
it('should not be in queue mode by default', () => {
expect(mcpServer.isQueueMode()).toBe(false);
});
});
describe('pending response management', () => {
it('should track and check pending responses', async () => {
// First register a session with transport
const sessionId = 'test-session';
const transport = createMockTransport(sessionId);
const server = createMockServer();
// Access private sessionManager to register session
const sessionManager = (
mcpServer as unknown as {
sessionManager: {
registerSession: (s: string, srv: unknown, tr: unknown) => Promise<void>;
};
}
).sessionManager;
await sessionManager.registerSession(sessionId, server, transport);
mcpServer.storePendingResponse(sessionId, 'msg-1');
expect(mcpServer.hasPendingResponse(sessionId, 'msg-1')).toBe(true);
expect(mcpServer.hasPendingResponse(sessionId, 'msg-2')).toBe(false);
expect(mcpServer.pendingResponseCount).toBe(1);
});
it('should remove pending responses', async () => {
const sessionId = 'test-session';
const transport = createMockTransport(sessionId);
const server = createMockServer();
const sessionManager = (
mcpServer as unknown as {
sessionManager: {
registerSession: (s: string, srv: unknown, tr: unknown) => Promise<void>;
};
}
).sessionManager;
await sessionManager.registerSession(sessionId, server, transport);
mcpServer.storePendingResponse(sessionId, 'msg-1');
mcpServer.removePendingResponse(sessionId, 'msg-1');
expect(mcpServer.hasPendingResponse(sessionId, 'msg-1')).toBe(false);
expect(mcpServer.pendingResponseCount).toBe(0);
});
it('should handle pending response without messageId', async () => {
const sessionId = 'test-session';
const transport = createMockTransport(sessionId);
const server = createMockServer();
const sessionManager = (
mcpServer as unknown as {
sessionManager: {
registerSession: (s: string, srv: unknown, tr: unknown) => Promise<void>;
};
}
).sessionManager;
await sessionManager.registerSession(sessionId, server, transport);
mcpServer.storePendingResponse(sessionId, '');
expect(mcpServer.hasPendingResponse(sessionId, '')).toBe(true);
});
it('should warn when storing pending response without transport', () => {
mcpServer.storePendingResponse('no-transport-session', 'msg-1');
expect(mockLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Cannot store pending response'),
);
});
});
describe('getTransport', () => {
it('should return undefined for unregistered session', () => {
expect(mcpServer.getTransport('non-existent')).toBeUndefined();
});
it('should return transport for registered session', async () => {
const sessionId = 'test-session';
const transport = createMockTransport(sessionId);
const server = createMockServer();
const sessionManager = (
mcpServer as unknown as {
sessionManager: {
registerSession: (s: string, srv: unknown, tr: unknown) => Promise<void>;
};
}
).sessionManager;
await sessionManager.registerSession(sessionId, server, transport);
expect(mcpServer.getTransport(sessionId)).toBe(transport);
});
});
describe('getTools', () => {
it('should return undefined for session without tools', () => {
expect(mcpServer.getTools('non-existent')).toBeUndefined();
});
});
describe('getPendingCallsManager', () => {
it('should return the pending calls manager', () => {
const manager = mcpServer.getPendingCallsManager();
expect(manager).toBeDefined();
expect(typeof manager.waitForResult).toBe('function');
expect(typeof manager.resolve).toBe('function');
expect(typeof manager.reject).toBe('function');
});
});
describe('handleWorkerResponse', () => {
it('should handle list tools request marker', async () => {
const sessionId = 'test-session';
const transport = createMockTransport(sessionId, 'sse');
const server = createMockServer();
const sessionManager = (
mcpServer as unknown as {
sessionManager: {
registerSession: (
s: string,
srv: unknown,
tr: unknown,
tools?: unknown[],
) => Promise<void>;
};
}
).sessionManager;
await sessionManager.registerSession(sessionId, server, transport, [
createMockTool('test-tool'),
]);
mcpServer.handleWorkerResponse(sessionId, 'msg-1', { _listToolsRequest: true });
// Should have attempted to send tools list via transport
expect(transport.send).toHaveBeenCalled();
});
});
});
@@ -0,0 +1,424 @@
import { mock } from 'jest-mock-extended';
import type { INode, IWebhookFunctions, ICredentialDataDecryptedObject } from 'n8n-workflow';
import { createMockLogger, createMockRequest, createMockResponse } from './helpers';
import { McpTrigger } from '../McpTrigger.node';
import { McpServer } from '../McpServer';
// Mock the McpServer
jest.mock('../McpServer', () => ({
McpServer: {
instance: jest.fn(),
},
MCP_LIST_TOOLS_REQUEST_MARKER: 'mcp_list_tools_request',
}));
// Mock webhook utils from nodes-base
jest.mock('n8n-nodes-base/dist/nodes/Webhook/utils', () => ({
validateWebhookAuthentication: jest.fn(),
}));
// Mock getConnectedTools from utils
jest.mock('@utils/helpers', () => ({
getConnectedTools: jest.fn().mockResolvedValue([]),
}));
describe('McpTrigger', () => {
let mcpTrigger: McpTrigger;
let mockMcpServer: jest.Mocked<McpServer>;
let mockContext: jest.Mocked<IWebhookFunctions>;
let mockLogger: ReturnType<typeof createMockLogger>;
beforeEach(() => {
mcpTrigger = new McpTrigger();
mockLogger = createMockLogger();
mockMcpServer = {
handleSetupRequest: jest.fn().mockResolvedValue(undefined),
handlePostMessage: jest.fn().mockResolvedValue({
wasToolCall: false,
toolCallInfo: undefined,
messageId: undefined,
relaySessionId: undefined,
needsListToolsRelay: false,
}),
handleDeleteRequest: jest.fn().mockResolvedValue(undefined),
handleStreamableHttpSetup: jest.fn().mockResolvedValue(undefined),
getSessionId: jest.fn().mockReturnValue(undefined),
} as unknown as jest.Mocked<McpServer>;
(McpServer.instance as jest.Mock).mockReturnValue(mockMcpServer);
mockContext = mock<IWebhookFunctions>({
getWebhookName: jest.fn().mockReturnValue('setup'),
getRequestObject: jest.fn(),
getResponseObject: jest.fn(),
getNode: jest.fn(),
logger: mockLogger,
getCredentials: jest.fn().mockResolvedValue({} as ICredentialDataDecryptedObject),
});
});
afterEach(() => {
jest.clearAllMocks();
});
describe('description', () => {
it('should have the correct node metadata', () => {
expect(mcpTrigger.description.name).toBe('mcpTrigger');
expect(mcpTrigger.description.displayName).toBe('MCP Server Trigger');
expect(mcpTrigger.description.group).toContain('trigger');
});
it('should support multiple versions', () => {
expect(mcpTrigger.description.version).toEqual([1, 1.1, 2]);
});
it('should have authentication options', () => {
const authParam = mcpTrigger.description.properties?.find((p) => p.name === 'authentication');
expect(authParam).toBeDefined();
expect(authParam?.type).toBe('options');
expect(authParam?.options).toHaveLength(3);
});
it('should define webhook endpoints', () => {
const webhooks = mcpTrigger.description.webhooks;
expect(webhooks).toHaveLength(3);
const setupWebhook = webhooks?.find((w) => w.name === 'setup');
expect(setupWebhook?.httpMethod).toBe('GET');
const defaultWebhooks = webhooks?.filter((w) => w.name === 'default');
expect(defaultWebhooks).toHaveLength(2);
expect(defaultWebhooks?.map((w) => w.httpMethod)).toEqual(['POST', 'DELETE']);
});
});
describe('webhook - setup (GET)', () => {
it('should handle setup request for version 1', async () => {
const req = createMockRequest({ path: '/webhook/sse' });
const resp = createMockResponse();
const node = mock<INode>({
typeVersion: 1,
name: 'MCP Server Trigger',
});
mockContext.getWebhookName.mockReturnValue('setup');
mockContext.getRequestObject.mockReturnValue(req as never);
mockContext.getResponseObject.mockReturnValue(resp as never);
mockContext.getNode.mockReturnValue(node);
const result = await mcpTrigger.webhook(mockContext);
expect(mockMcpServer.handleSetupRequest).toHaveBeenCalled();
expect(result).toEqual({ noWebhookResponse: true });
});
it('should use n8n-mcp-server name for version 1', async () => {
const req = createMockRequest({ path: '/webhook/sse' });
const resp = createMockResponse();
const node = mock<INode>({
typeVersion: 1,
name: 'MCP Server Trigger',
});
mockContext.getWebhookName.mockReturnValue('setup');
mockContext.getRequestObject.mockReturnValue(req as never);
mockContext.getResponseObject.mockReturnValue(resp as never);
mockContext.getNode.mockReturnValue(node);
await mcpTrigger.webhook(mockContext);
expect(mockMcpServer.handleSetupRequest).toHaveBeenCalledWith(
req,
resp,
'n8n-mcp-server',
expect.any(String),
expect.any(Array),
);
});
it('should use sanitized node name for version > 1', async () => {
const req = createMockRequest({ path: '/webhook' });
const resp = createMockResponse();
const node = mock<INode>({
typeVersion: 2,
name: 'My Custom MCP Server',
});
mockContext.getWebhookName.mockReturnValue('setup');
mockContext.getRequestObject.mockReturnValue(req as never);
mockContext.getResponseObject.mockReturnValue(resp as never);
mockContext.getNode.mockReturnValue(node);
await mcpTrigger.webhook(mockContext);
// nodeNameToToolName converts "My Custom MCP Server" to a sanitized name
expect(mockMcpServer.handleSetupRequest).toHaveBeenCalledWith(
req,
resp,
expect.stringMatching(/^[a-z0-9_-]+$/i),
expect.any(String),
expect.any(Array),
);
});
it('should compute correct POST URL for version 1', async () => {
const req = createMockRequest({ path: '/webhook/sse' });
const resp = createMockResponse();
const node = mock<INode>({
typeVersion: 1,
name: 'MCP Server Trigger',
});
mockContext.getWebhookName.mockReturnValue('setup');
mockContext.getRequestObject.mockReturnValue(req as never);
mockContext.getResponseObject.mockReturnValue(resp as never);
mockContext.getNode.mockReturnValue(node);
await mcpTrigger.webhook(mockContext);
expect(mockMcpServer.handleSetupRequest).toHaveBeenCalledWith(
req,
resp,
expect.any(String),
'/webhook/messages',
expect.any(Array),
);
});
it('should use same path as POST URL for version 2', async () => {
const req = createMockRequest({ path: '/webhook' });
const resp = createMockResponse();
const node = mock<INode>({
typeVersion: 2,
name: 'MCP Server Trigger',
});
mockContext.getWebhookName.mockReturnValue('setup');
mockContext.getRequestObject.mockReturnValue(req as never);
mockContext.getResponseObject.mockReturnValue(resp as never);
mockContext.getNode.mockReturnValue(node);
await mcpTrigger.webhook(mockContext);
expect(mockMcpServer.handleSetupRequest).toHaveBeenCalledWith(
req,
resp,
expect.any(String),
'/webhook',
expect.any(Array),
);
});
});
describe('webhook - default POST', () => {
it('should handle POST with existing session', async () => {
const req = createMockRequest({ method: 'POST', query: { sessionId: 'test-session' } });
const resp = createMockResponse();
const node = mock<INode>({
typeVersion: 2,
name: 'MCP Server Trigger',
});
mockMcpServer.getSessionId.mockReturnValue('test-session');
mockContext.getWebhookName.mockReturnValue('default');
mockContext.getRequestObject.mockReturnValue(req as never);
mockContext.getResponseObject.mockReturnValue(resp as never);
mockContext.getNode.mockReturnValue(node);
const result = await mcpTrigger.webhook(mockContext);
expect(mockMcpServer.handlePostMessage).toHaveBeenCalled();
expect(result).toEqual({ noWebhookResponse: true });
});
it('should return workflow data when tool call is detected', async () => {
const req = createMockRequest({ method: 'POST', query: { sessionId: 'test-session' } });
const resp = createMockResponse();
const node = mock<INode>({
typeVersion: 2,
name: 'MCP Server Trigger',
});
mockMcpServer.getSessionId.mockReturnValue('test-session');
mockMcpServer.handlePostMessage.mockResolvedValue({
wasToolCall: true,
toolCallInfo: { toolName: 'test-tool', arguments: { arg1: 'value1' } },
messageId: 'msg-123',
relaySessionId: undefined,
needsListToolsRelay: false,
});
mockContext.getWebhookName.mockReturnValue('default');
mockContext.getRequestObject.mockReturnValue(req as never);
mockContext.getResponseObject.mockReturnValue(resp as never);
mockContext.getNode.mockReturnValue(node);
const result = await mcpTrigger.webhook(mockContext);
expect(result).toEqual({
noWebhookResponse: true,
workflowData: [
[
{
json: {
mcpToolCall: { toolName: 'test-tool', arguments: { arg1: 'value1' } },
mcpMessageId: 'msg-123',
},
},
],
],
});
});
it('should handle Streamable HTTP setup when no session exists', async () => {
const req = createMockRequest({ method: 'POST' });
const resp = createMockResponse();
const node = mock<INode>({
typeVersion: 2,
name: 'MCP Server Trigger',
});
mockMcpServer.getSessionId.mockReturnValue(undefined);
mockContext.getWebhookName.mockReturnValue('default');
mockContext.getRequestObject.mockReturnValue(req as never);
mockContext.getResponseObject.mockReturnValue(resp as never);
mockContext.getNode.mockReturnValue(node);
const result = await mcpTrigger.webhook(mockContext);
expect(mockMcpServer.handleStreamableHttpSetup).toHaveBeenCalled();
expect(result).toEqual({ noWebhookResponse: true });
});
});
describe('webhook - default DELETE', () => {
it('should handle DELETE request', async () => {
const req = createMockRequest({ method: 'DELETE' });
const resp = createMockResponse();
const node = mock<INode>({
typeVersion: 2,
name: 'MCP Server Trigger',
});
mockContext.getWebhookName.mockReturnValue('default');
mockContext.getRequestObject.mockReturnValue(req as never);
mockContext.getResponseObject.mockReturnValue(resp as never);
mockContext.getNode.mockReturnValue(node);
const result = await mcpTrigger.webhook(mockContext);
expect(mockMcpServer.handleDeleteRequest).toHaveBeenCalledWith(req, resp);
expect(result).toEqual({ noWebhookResponse: true });
});
});
describe('authentication', () => {
it('should rethrow non-authorization errors', async () => {
const { validateWebhookAuthentication } = jest.requireMock(
'n8n-nodes-base/dist/nodes/Webhook/utils',
);
const genericError = new Error('Something went wrong');
validateWebhookAuthentication.mockRejectedValue(genericError);
const req = createMockRequest({ path: '/webhook' });
const resp = createMockResponse();
const node = mock<INode>({
typeVersion: 2,
name: 'MCP Server Trigger',
});
mockContext.getWebhookName.mockReturnValue('setup');
mockContext.getRequestObject.mockReturnValue(req as never);
mockContext.getResponseObject.mockReturnValue(resp as never);
mockContext.getNode.mockReturnValue(node);
await expect(mcpTrigger.webhook(mockContext)).rejects.toThrow('Something went wrong');
});
it('should return 401 for authentication errors', async () => {
const { WebhookAuthorizationError } = jest.requireActual(
'n8n-nodes-base/dist/nodes/Webhook/error',
);
const { validateWebhookAuthentication } = jest.requireMock(
'n8n-nodes-base/dist/nodes/Webhook/utils',
);
validateWebhookAuthentication.mockRejectedValue(
new WebhookAuthorizationError(401, 'Unauthorized'),
);
const req = createMockRequest({ path: '/webhook' });
const resp = createMockResponse();
const node = mock<INode>({
typeVersion: 2,
name: 'MCP Server Trigger',
});
mockContext.getWebhookName.mockReturnValue('setup');
mockContext.getRequestObject.mockReturnValue(req as never);
mockContext.getResponseObject.mockReturnValue(resp as never);
mockContext.getNode.mockReturnValue(node);
const result = await mcpTrigger.webhook(mockContext);
expect(resp.writeHead).toHaveBeenCalledWith(401);
expect(resp.end).toHaveBeenCalledWith('Unauthorized');
expect(result).toEqual({ noWebhookResponse: true });
});
});
describe('list tools relay', () => {
it('should return list tools relay data when needed', async () => {
// Reset validateWebhookAuthentication to resolve (not reject)
const { validateWebhookAuthentication } = jest.requireMock(
'n8n-nodes-base/dist/nodes/Webhook/utils',
);
validateWebhookAuthentication.mockResolvedValue(undefined);
const req = createMockRequest({ method: 'POST', query: { sessionId: 'test-session' } });
const resp = createMockResponse();
const node = mock<INode>({
typeVersion: 2,
name: 'MCP Server Trigger',
});
mockMcpServer.getSessionId.mockReturnValue('test-session');
mockMcpServer.handlePostMessage.mockResolvedValue({
wasToolCall: false,
toolCallInfo: undefined,
messageId: 'msg-456',
relaySessionId: 'relay-session-789',
needsListToolsRelay: true,
});
mockContext.getWebhookName.mockReturnValue('default');
mockContext.getRequestObject.mockReturnValue(req as never);
mockContext.getResponseObject.mockReturnValue(resp as never);
mockContext.getNode.mockReturnValue(node);
const result = await mcpTrigger.webhook(mockContext);
expect(result).toEqual({
noWebhookResponse: true,
workflowData: [
[
{
json: {
mcpListToolsRelay: {
sessionId: 'relay-session-789',
messageId: 'msg-456',
marker: 'mcp_list_tools_request',
},
},
},
],
],
});
});
});
});
@@ -0,0 +1,18 @@
export {
createMockTool,
createMockTools,
} from './mock-langchain';
export {
createMockRequest,
createMockRequestWithSessionId,
createMockRequestWithHeaderSessionId,
createMockResponse,
createValidToolCallMessage,
createListToolsMessage,
MCP_SESSION_ID_HEADER,
} from './mock-express';
export {
createMockServer,
createMockTransport,
} from './mock-mcp-sdk';
export { createMockLogger } from './mock-logger';
@@ -0,0 +1,134 @@
import type { Request } from 'express';
import type { CompressionResponse } from '../../transport/Transport';
/** MCP session ID header name */
export const MCP_SESSION_ID_HEADER = 'mcp-session-id';
/**
* Creates a mock Express Response with compression support
*/
export function createMockResponse(): jest.Mocked<CompressionResponse> {
const response = {
status: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
write: jest.fn().mockReturnThis(),
writeHead: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
getHeader: jest.fn(),
flush: jest.fn(),
on: jest.fn().mockReturnThis(),
once: jest.fn().mockReturnThis(),
removeListener: jest.fn().mockReturnThis(),
emit: jest.fn().mockReturnValue(true),
headersSent: false,
} as unknown as jest.Mocked<CompressionResponse>;
return response;
}
/**
* Creates a mock Express Request with specified properties
*/
export function createMockRequest(
options: {
sessionId?: string;
body?: unknown;
rawBody?: string;
headers?: Record<string, string>;
query?: Record<string, string>;
method?: string;
path?: string;
} = {},
): jest.Mocked<Request> & { rawBody: Buffer } {
const {
sessionId,
body = {},
rawBody = '{}',
headers = {},
query = {},
method = 'POST',
path = '/mcp',
} = options;
const finalQuery: Record<string, string> = { ...query };
const finalHeaders: Record<string, string> = { ...headers };
if (sessionId) {
if (!query.sessionId && !headers[MCP_SESSION_ID_HEADER]) {
finalQuery.sessionId = sessionId;
}
}
return {
body,
rawBody: Buffer.from(rawBody),
headers: finalHeaders,
query: finalQuery,
method,
params: {},
url: path,
path,
get: jest.fn((name: string) => finalHeaders[name.toLowerCase()]),
} as unknown as jest.Mocked<Request> & { rawBody: Buffer };
}
/**
* Creates a mock request with a specific session ID in query params
*/
export function createMockRequestWithSessionId(
sessionId: string,
rawBody: string,
): jest.Mocked<Request> & { rawBody: Buffer } {
return createMockRequest({
sessionId,
rawBody,
query: { sessionId },
});
}
/**
* Creates a valid JSONRPC tool call message body
*/
export function createValidToolCallMessage(
toolName: string,
args: Record<string, unknown>,
id: string | number = 1,
): string {
return JSON.stringify({
jsonrpc: '2.0',
id,
method: 'tools/call',
params: {
name: toolName,
arguments: args,
},
});
}
/**
* Creates a valid JSONRPC list tools request message body
*/
export function createListToolsMessage(id: string | number = 1): string {
return JSON.stringify({
jsonrpc: '2.0',
id,
method: 'tools/list',
});
}
/**
* Creates a mock request with session ID in the mcp-session-id header
*/
export function createMockRequestWithHeaderSessionId(
sessionId: string,
rawBody: string = '{}',
): jest.Mocked<Request> & { rawBody: Buffer } {
return createMockRequest({
rawBody,
headers: { [MCP_SESSION_ID_HEADER]: sessionId },
query: {},
});
}
@@ -0,0 +1,45 @@
import type { Tool } from '@langchain/core/tools';
import { z } from 'zod';
/**
* Creates a mock Tool for testing
*/
export function createMockTool(
toolName: string,
opts: {
description?: string;
invokeReturn?: unknown;
invokeError?: Error;
metadata?: Record<string, unknown>;
} = {},
): jest.Mocked<Tool> {
const {
description = `Mock tool: ${toolName}`,
invokeReturn = { result: 'success' },
invokeError,
metadata,
} = opts;
const invoke = jest.fn().mockImplementation(async () => {
await Promise.resolve();
if (invokeError) {
throw invokeError;
}
return invokeReturn;
});
return {
name: toolName,
description,
schema: z.object({}),
invoke,
metadata,
} as unknown as jest.Mocked<Tool>;
}
/**
* Creates multiple mock tools
*/
export function createMockTools(toolNames: string[]): Array<jest.Mocked<Tool>> {
return toolNames.map((n) => createMockTool(n));
}
@@ -0,0 +1,17 @@
import type { Logger } from 'n8n-workflow';
/**
* Creates a mock Logger for testing
*/
export function createMockLogger(): jest.Mocked<Logger> {
return {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
trace: jest.fn(),
log: jest.fn(),
verbose: jest.fn(),
scoped: jest.fn().mockReturnThis(),
} as unknown as jest.Mocked<Logger>;
}
@@ -0,0 +1,33 @@
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
import type { McpTransport, TransportType } from '../../transport/Transport';
/**
* Creates a mock MCP Server
*/
export function createMockServer(): jest.Mocked<Server> {
return {
connect: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
setRequestHandler: jest.fn(),
onclose: undefined,
onerror: undefined,
} as unknown as jest.Mocked<Server>;
}
/**
* Creates a mock McpTransport
*/
export function createMockTransport(
sessionId: string,
transportType: TransportType = 'sse',
): jest.Mocked<McpTransport> {
return {
transportType,
sessionId,
send: jest.fn().mockResolvedValue(undefined),
handleRequest: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
onclose: undefined,
} as unknown as jest.Mocked<McpTransport>;
}
@@ -0,0 +1,12 @@
/**
* Jest setup file for mcp/core tests
* Cleans up mocks between tests to ensure test isolation
*/
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
jest.restoreAllMocks();
});