mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
* feat: Remove 9 low-value tools and consolidate n8n_health_check (v2.25.0) Telemetry-driven tool cleanup to improve API clarity: **Removed Tools (9):** - list_nodes - Use search_nodes instead - list_ai_tools - Use search_nodes with isAITool filter - list_tasks - Low usage (0.02%) - get_database_statistics - Use n8n_health_check - list_templates - Use search_templates or get_templates_for_task - get_node_as_tool_info - Documented in get_node - validate_workflow_connections - Use validate_workflow - validate_workflow_expressions - Use validate_workflow - n8n_list_available_tools - Use n8n_health_check - n8n_diagnostic - Merged into n8n_health_check **Consolidated Tool:** - n8n_health_check now supports mode='diagnostic' for detailed troubleshooting **Tool Count:** - Before: 38 tools - After: 31 tools (18% reduction) Concieved by Romuald Członkowski - www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: cleanup stale references and update tests after tool removal - Remove handleListAvailableTools dead code from handlers-n8n-manager.ts - Update error messages to reference n8n_health_check(mode="diagnostic") instead of n8n_diagnostic - Update tool counts in diagnostic messages (14 doc tools, 31 total) - Fix error-handling.test.ts to use valid tools (search_nodes, tools_documentation) - Remove obsolete list-tools.test.ts integration tests - Remove unused ListToolsResponse type from response-types.ts - Update tools.ts QUICK REFERENCE to remove list_nodes references - Update tools-documentation.ts to remove references to removed tools - Update tool-docs files to remove stale relatedTools references - Fix tools.test.ts to not test removed tools (list_nodes, list_ai_tools, etc.) - Fix parameter-validation.test.ts to not test removed tools - Update handlers-n8n-manager.test.ts error message expectations All 399 MCP unit tests now pass. Conceived by Romuald Członkowski - www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: update integration tests to use valid tools after v2.25.0 removal Replaced all references to removed tools in integration tests: - list_nodes -> search_nodes - get_database_statistics -> tools_documentation - list_ai_tools -> search_nodes/tools_documentation - list_tasks -> tools_documentation - get_node_as_tool_info -> removed test section Updated test files: - tests/integration/mcp-protocol/basic-connection.test.ts - tests/integration/mcp-protocol/performance.test.ts - tests/integration/mcp-protocol/session-management.test.ts - tests/integration/mcp-protocol/test-helpers.ts - tests/integration/mcp-protocol/tool-invocation.test.ts - tests/integration/telemetry/mcp-telemetry.test.ts - tests/unit/mcp/disabled-tools.test.ts - tests/unit/mcp/tools-documentation.test.ts Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Tool consolidation v2.26.0 - reduce tools by 38% (31 → 19) Major consolidation of MCP tools using mode-based parameters for better AI agent ergonomics: Node Tools: - get_node_documentation → get_node with mode='documentation' - search_node_properties → get_node with mode='search_properties' - get_property_dependencies → removed Validation Tools: - validate_node_operation + validate_node_minimal → validate_node with mode param Template Tools: - list_node_templates → search_templates with searchMode='nodes' - search_templates_by_metadata → search_templates with searchMode='metadata' - get_templates_for_task → search_templates with searchMode='task' Workflow Getters: - n8n_get_workflow_details/structure/minimal → n8n_get_workflow with mode param Execution Tools: - n8n_list/get/delete_execution → n8n_executions with action param Test updates for all consolidated tools. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en * docs: comprehensive README update for v2.26.0 tool consolidation - Quick Start: Added hosted service (dashboard.n8n-mcp.com) as primary option - Self-hosting: Renamed options to A (npx), B (Docker), C (Local), D (Railway) - Removed: "Memory Leak Fix (v2.20.2)" section (outdated) - Removed: "Known Issues" section (outdated container management) - Claude Project Setup: Updated all tool references to v2.26.0 consolidated tools - validate_node({mode: 'minimal'|'full'}) instead of separate tools - search_templates({searchMode: ...}) unified template search - get_node({mode: 'docs'|'search_properties'}) for documentation - n8n_executions({action: ...}) unified execution management - Available MCP Tools: Updated to show 19 consolidated tools (7 core + 12 mgmt) - Recent Updates: Simplified to just link to CHANGELOG.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en * fix: update tool count from 31 to 19 in diagnostic message 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(tests): update tool count expectations for v2.26.0 Update handlers-n8n-manager.test.ts to expect new consolidated tool counts (7/12/19) after v2.26.0 tool consolidation. Conceived by Romuald Członkowski - www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
754 lines
24 KiB
TypeScript
754 lines
24 KiB
TypeScript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
|
|
import { telemetry } from '../../../src/telemetry/telemetry-manager';
|
|
import { TelemetryConfigManager } from '../../../src/telemetry/config-manager';
|
|
import { CallToolRequest, ListToolsRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
|
|
// Mock dependencies
|
|
vi.mock('../../../src/utils/logger', () => ({
|
|
Logger: vi.fn().mockImplementation(() => ({
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
})),
|
|
logger: {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
}
|
|
}));
|
|
|
|
vi.mock('../../../src/telemetry/telemetry-manager', () => ({
|
|
telemetry: {
|
|
trackSessionStart: vi.fn(),
|
|
trackToolUsage: vi.fn(),
|
|
trackToolSequence: vi.fn(),
|
|
trackError: vi.fn(),
|
|
trackSearchQuery: vi.fn(),
|
|
trackValidationDetails: vi.fn(),
|
|
trackWorkflowCreation: vi.fn(),
|
|
trackPerformanceMetric: vi.fn(),
|
|
getMetrics: vi.fn().mockReturnValue({
|
|
status: 'enabled',
|
|
initialized: true,
|
|
tracking: { eventQueueSize: 0 },
|
|
processing: { eventsTracked: 0 },
|
|
errors: { totalErrors: 0 }
|
|
})
|
|
}
|
|
}));
|
|
|
|
vi.mock('../../../src/telemetry/config-manager');
|
|
|
|
// Mock database and other dependencies
|
|
vi.mock('../../../src/database/node-repository');
|
|
vi.mock('../../../src/services/enhanced-config-validator');
|
|
vi.mock('../../../src/services/expression-validator');
|
|
vi.mock('../../../src/services/workflow-validator');
|
|
|
|
// TODO: This test needs to be refactored. It's currently mocking everything
|
|
// which defeats the purpose of an integration test. It should either:
|
|
// 1. Be moved to unit tests if we want to test with mocks
|
|
// 2. Be rewritten as a proper integration test without mocks
|
|
// Skipping for now to unblock CI - the telemetry functionality is tested
|
|
// properly in the unit tests at tests/unit/telemetry/
|
|
describe.skip('MCP Telemetry Integration', () => {
|
|
let mcpServer: N8NDocumentationMCPServer;
|
|
let mockTelemetryConfig: any;
|
|
|
|
beforeEach(() => {
|
|
// Mock TelemetryConfigManager
|
|
mockTelemetryConfig = {
|
|
isEnabled: vi.fn().mockReturnValue(true),
|
|
getUserId: vi.fn().mockReturnValue('test-user-123'),
|
|
disable: vi.fn(),
|
|
enable: vi.fn(),
|
|
getStatus: vi.fn().mockReturnValue('enabled')
|
|
};
|
|
vi.mocked(TelemetryConfigManager.getInstance).mockReturnValue(mockTelemetryConfig);
|
|
|
|
// Mock database repository
|
|
const mockNodeRepository = {
|
|
searchNodes: vi.fn().mockResolvedValue({ results: [], totalResults: 0 }),
|
|
getNodeInfo: vi.fn().mockResolvedValue(null),
|
|
getAllNodes: vi.fn().mockResolvedValue([]),
|
|
close: vi.fn()
|
|
};
|
|
vi.doMock('../../../src/database/node-repository', () => ({
|
|
NodeRepository: vi.fn().mockImplementation(() => mockNodeRepository)
|
|
}));
|
|
|
|
// Create a mock server instance to avoid initialization issues
|
|
const mockServer = {
|
|
requestHandlers: new Map(),
|
|
notificationHandlers: new Map(),
|
|
setRequestHandler: vi.fn((method: string, handler: any) => {
|
|
mockServer.requestHandlers.set(method, handler);
|
|
}),
|
|
setNotificationHandler: vi.fn((method: string, handler: any) => {
|
|
mockServer.notificationHandlers.set(method, handler);
|
|
})
|
|
};
|
|
|
|
// Set up basic handlers
|
|
mockServer.requestHandlers.set('initialize', async () => {
|
|
telemetry.trackSessionStart();
|
|
return { protocolVersion: '2024-11-05' };
|
|
});
|
|
|
|
mockServer.requestHandlers.set('tools/call', async (params: any) => {
|
|
// Use the actual tool name from the request
|
|
const toolName = params?.name || 'unknown-tool';
|
|
|
|
try {
|
|
// Call executeTool if it's been mocked
|
|
if ((mcpServer as any).executeTool) {
|
|
const result = await (mcpServer as any).executeTool(params);
|
|
|
|
// Track specific telemetry based on tool type
|
|
if (toolName === 'search_nodes') {
|
|
const query = params?.arguments?.query || '';
|
|
const totalResults = result?.totalResults || 0;
|
|
const mode = params?.arguments?.mode || 'OR';
|
|
telemetry.trackSearchQuery(query, totalResults, mode);
|
|
} else if (toolName === 'validate_workflow') {
|
|
const workflow = params?.arguments?.workflow || {};
|
|
const validationPassed = result?.isValid !== false;
|
|
telemetry.trackWorkflowCreation(workflow, validationPassed);
|
|
if (!validationPassed && result?.errors) {
|
|
result.errors.forEach((error: any) => {
|
|
telemetry.trackValidationDetails(error.nodeType || 'unknown', error.type || 'validation_error', error);
|
|
});
|
|
}
|
|
} else if (toolName === 'validate_node_operation' || toolName === 'validate_node_minimal') {
|
|
const nodeType = params?.arguments?.nodeType || 'unknown';
|
|
const errorType = result?.errors?.[0]?.type || 'validation_error';
|
|
telemetry.trackValidationDetails(nodeType, errorType, result);
|
|
}
|
|
|
|
// Simulate a duration for tool execution
|
|
const duration = params?.duration || Math.random() * 100;
|
|
telemetry.trackToolUsage(toolName, true, duration);
|
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
} else {
|
|
// Default behavior if executeTool is not mocked
|
|
telemetry.trackToolUsage(toolName, true);
|
|
return { content: [{ type: 'text', text: 'Success' }] };
|
|
}
|
|
} catch (error: any) {
|
|
telemetry.trackToolUsage(toolName, false);
|
|
telemetry.trackError(
|
|
error.constructor.name,
|
|
error.message,
|
|
toolName,
|
|
error.message
|
|
);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// Mock the N8NDocumentationMCPServer to have the server property
|
|
mcpServer = {
|
|
server: mockServer,
|
|
handleTool: vi.fn().mockResolvedValue({ content: [{ type: 'text', text: 'Success' }] }),
|
|
executeTool: vi.fn().mockResolvedValue({
|
|
results: [{ nodeType: 'nodes-base.webhook' }],
|
|
totalResults: 1
|
|
}),
|
|
close: vi.fn()
|
|
} as any;
|
|
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('Session tracking', () => {
|
|
it('should track session start on MCP initialize', async () => {
|
|
const initializeRequest = {
|
|
method: 'initialize' as const,
|
|
params: {
|
|
protocolVersion: '2024-11-05',
|
|
clientInfo: {
|
|
name: 'test-client',
|
|
version: '1.0.0'
|
|
},
|
|
capabilities: {}
|
|
}
|
|
};
|
|
|
|
// Access the private server instance for testing
|
|
const server = (mcpServer as any).server;
|
|
const initializeHandler = server.requestHandlers.get('initialize');
|
|
|
|
if (initializeHandler) {
|
|
await initializeHandler(initializeRequest.params);
|
|
}
|
|
|
|
expect(telemetry.trackSessionStart).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('Tool usage tracking', () => {
|
|
it('should track successful tool execution', async () => {
|
|
const callToolRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'search_nodes',
|
|
arguments: { query: 'webhook' }
|
|
}
|
|
};
|
|
|
|
// Mock the executeTool method to return a successful result
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockResolvedValue({
|
|
results: [{ nodeType: 'nodes-base.webhook' }],
|
|
totalResults: 1
|
|
});
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
await callToolHandler(callToolRequest.params);
|
|
}
|
|
|
|
expect(telemetry.trackToolUsage).toHaveBeenCalledWith(
|
|
'search_nodes',
|
|
true,
|
|
expect.any(Number)
|
|
);
|
|
});
|
|
|
|
it('should track failed tool execution', async () => {
|
|
const callToolRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'get_node',
|
|
arguments: { nodeType: 'invalid-node' }
|
|
}
|
|
};
|
|
|
|
// Mock the executeTool method to throw an error
|
|
const error = new Error('Node not found');
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockRejectedValue(error);
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
try {
|
|
await callToolHandler(callToolRequest.params);
|
|
} catch (e) {
|
|
// Expected to throw
|
|
}
|
|
}
|
|
|
|
expect(telemetry.trackToolUsage).toHaveBeenCalledWith('get_node', false);
|
|
expect(telemetry.trackError).toHaveBeenCalledWith(
|
|
'Error',
|
|
'Node not found',
|
|
'get_node'
|
|
);
|
|
});
|
|
|
|
it('should track tool sequences', async () => {
|
|
// Set up previous tool state
|
|
(mcpServer as any).previousTool = 'search_nodes';
|
|
(mcpServer as any).previousToolTimestamp = Date.now() - 5000;
|
|
|
|
const callToolRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'get_node',
|
|
arguments: { nodeType: 'nodes-base.webhook' }
|
|
}
|
|
};
|
|
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockResolvedValue({
|
|
nodeType: 'nodes-base.webhook',
|
|
displayName: 'Webhook'
|
|
});
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
await callToolHandler(callToolRequest.params);
|
|
}
|
|
|
|
expect(telemetry.trackToolSequence).toHaveBeenCalledWith(
|
|
'search_nodes',
|
|
'get_node',
|
|
expect.any(Number)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Search query tracking', () => {
|
|
it('should track search queries with results', async () => {
|
|
const searchRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'search_nodes',
|
|
arguments: { query: 'webhook', mode: 'OR' }
|
|
}
|
|
};
|
|
|
|
// Mock search results
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockResolvedValue({
|
|
results: [
|
|
{ nodeType: 'nodes-base.webhook', score: 0.95 },
|
|
{ nodeType: 'nodes-base.httpRequest', score: 0.8 }
|
|
],
|
|
totalResults: 2
|
|
});
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
await callToolHandler(searchRequest.params);
|
|
}
|
|
|
|
expect(telemetry.trackSearchQuery).toHaveBeenCalledWith('webhook', 2, 'OR');
|
|
});
|
|
|
|
it('should track zero-result searches', async () => {
|
|
const zeroResultRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'search_nodes',
|
|
arguments: { query: 'nonexistent', mode: 'AND' }
|
|
}
|
|
};
|
|
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockResolvedValue({
|
|
results: [],
|
|
totalResults: 0
|
|
});
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
await callToolHandler(zeroResultRequest.params);
|
|
}
|
|
|
|
expect(telemetry.trackSearchQuery).toHaveBeenCalledWith('nonexistent', 0, 'AND');
|
|
});
|
|
|
|
it('should track fallback search queries', async () => {
|
|
const fallbackRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'search_nodes',
|
|
arguments: { query: 'partial-match', mode: 'OR' }
|
|
}
|
|
};
|
|
|
|
// Mock main search with no results, triggering fallback
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockResolvedValue({
|
|
results: [{ nodeType: 'nodes-base.webhook', score: 0.6 }],
|
|
totalResults: 1,
|
|
usedFallback: true
|
|
});
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
await callToolHandler(fallbackRequest.params);
|
|
}
|
|
|
|
// Should track both main query and fallback
|
|
expect(telemetry.trackSearchQuery).toHaveBeenCalledWith('partial-match', 0, 'OR');
|
|
expect(telemetry.trackSearchQuery).toHaveBeenCalledWith('partial-match', 1, 'OR_LIKE_FALLBACK');
|
|
});
|
|
});
|
|
|
|
describe('Workflow validation tracking', () => {
|
|
it('should track successful workflow creation', async () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{ id: '1', type: 'webhook', name: 'Webhook' },
|
|
{ id: '2', type: 'httpRequest', name: 'HTTP Request' }
|
|
],
|
|
connections: {
|
|
'1': { main: [[{ node: '2', type: 'main', index: 0 }]] }
|
|
}
|
|
};
|
|
|
|
const validateRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'validate_workflow',
|
|
arguments: { workflow }
|
|
}
|
|
};
|
|
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockResolvedValue({
|
|
isValid: true,
|
|
errors: [],
|
|
warnings: [],
|
|
summary: { totalIssues: 0, criticalIssues: 0 }
|
|
});
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
await callToolHandler(validateRequest.params);
|
|
}
|
|
|
|
expect(telemetry.trackWorkflowCreation).toHaveBeenCalledWith(workflow, true);
|
|
});
|
|
|
|
it('should track validation details for failed workflows', async () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{ id: '1', type: 'invalid-node', name: 'Invalid Node' }
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const validateRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'validate_workflow',
|
|
arguments: { workflow }
|
|
}
|
|
};
|
|
|
|
const validationResult = {
|
|
isValid: false,
|
|
errors: [
|
|
{
|
|
nodeId: '1',
|
|
nodeType: 'invalid-node',
|
|
category: 'node_validation',
|
|
severity: 'error',
|
|
message: 'Unknown node type',
|
|
details: { type: 'unknown_node_type' }
|
|
}
|
|
],
|
|
warnings: [],
|
|
summary: { totalIssues: 1, criticalIssues: 1 }
|
|
};
|
|
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockResolvedValue(validationResult);
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
await callToolHandler(validateRequest.params);
|
|
}
|
|
|
|
expect(telemetry.trackValidationDetails).toHaveBeenCalledWith(
|
|
'invalid-node',
|
|
'unknown_node_type',
|
|
expect.objectContaining({
|
|
category: 'node_validation',
|
|
severity: 'error'
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Node configuration tracking', () => {
|
|
it('should track node configuration validation', async () => {
|
|
const validateNodeRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'validate_node_operation',
|
|
arguments: {
|
|
nodeType: 'nodes-base.httpRequest',
|
|
config: { url: 'https://api.example.com', method: 'GET' }
|
|
}
|
|
}
|
|
};
|
|
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockResolvedValue({
|
|
isValid: true,
|
|
errors: [],
|
|
warnings: [],
|
|
nodeConfig: { url: 'https://api.example.com', method: 'GET' }
|
|
});
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
await callToolHandler(validateNodeRequest.params);
|
|
}
|
|
|
|
// Should track the validation attempt
|
|
expect(telemetry.trackToolUsage).toHaveBeenCalledWith(
|
|
'validate_node_operation',
|
|
true,
|
|
expect.any(Number)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Performance metric tracking', () => {
|
|
it('should track slow tool executions', async () => {
|
|
const slowToolRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'search_nodes',
|
|
arguments: { query: 'http', limit: 1000 }
|
|
}
|
|
};
|
|
|
|
// Mock a slow operation
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockImplementation(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second delay
|
|
return { results: [], totalCount: 0 };
|
|
});
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
await callToolHandler(slowToolRequest.params);
|
|
}
|
|
|
|
expect(telemetry.trackToolUsage).toHaveBeenCalledWith(
|
|
'search_nodes',
|
|
true,
|
|
expect.any(Number)
|
|
);
|
|
|
|
// Verify duration is tracked (should be around 2000ms)
|
|
const trackUsageCall = vi.mocked(telemetry.trackToolUsage).mock.calls[0];
|
|
expect(trackUsageCall[2]).toBeGreaterThan(1500); // Allow some variance
|
|
});
|
|
});
|
|
|
|
describe('Tool listing and capabilities', () => {
|
|
it('should handle tool listing without telemetry interference', async () => {
|
|
const listToolsRequest: ListToolsRequest = {
|
|
method: 'tools/list',
|
|
params: {}
|
|
};
|
|
|
|
const server = (mcpServer as any).server;
|
|
const listToolsHandler = server.requestHandlers.get('tools/list');
|
|
|
|
if (listToolsHandler) {
|
|
const result = await listToolsHandler(listToolsRequest.params);
|
|
expect(result).toHaveProperty('tools');
|
|
expect(Array.isArray(result.tools)).toBe(true);
|
|
}
|
|
|
|
// Tool listing shouldn't generate telemetry events
|
|
expect(telemetry.trackToolUsage).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Error handling and telemetry', () => {
|
|
it('should track errors without breaking MCP protocol', async () => {
|
|
const errorRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'nonexistent_tool',
|
|
arguments: {}
|
|
}
|
|
};
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
try {
|
|
await callToolHandler(errorRequest.params);
|
|
} catch (error) {
|
|
// Error should be handled by MCP server
|
|
expect(error).toBeDefined();
|
|
}
|
|
}
|
|
|
|
// Should track error without throwing
|
|
expect(telemetry.trackError).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle telemetry errors gracefully', async () => {
|
|
// Mock telemetry to throw an error
|
|
vi.mocked(telemetry.trackToolUsage).mockImplementation(() => {
|
|
throw new Error('Telemetry service unavailable');
|
|
});
|
|
|
|
const callToolRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'search_nodes',
|
|
arguments: { query: 'webhook' }
|
|
}
|
|
};
|
|
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockResolvedValue({
|
|
results: [],
|
|
totalResults: 0
|
|
});
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
// Should not throw even if telemetry fails
|
|
if (callToolHandler) {
|
|
await expect(callToolHandler(callToolRequest.params)).resolves.toBeDefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Telemetry configuration integration', () => {
|
|
it('should respect telemetry disabled state', async () => {
|
|
mockTelemetryConfig.isEnabled.mockReturnValue(false);
|
|
|
|
const callToolRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'search_nodes',
|
|
arguments: { query: 'webhook' }
|
|
}
|
|
};
|
|
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockResolvedValue({
|
|
results: [],
|
|
totalResults: 0
|
|
});
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
await callToolHandler(callToolRequest.params);
|
|
}
|
|
|
|
// Should still track if telemetry manager handles disabled state
|
|
// The actual filtering happens in telemetry manager, not MCP server
|
|
expect(telemetry.trackToolUsage).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Complex workflow scenarios', () => {
|
|
it('should track comprehensive workflow validation scenario', async () => {
|
|
const complexWorkflow = {
|
|
nodes: [
|
|
{ id: '1', type: 'webhook', name: 'Webhook Trigger' },
|
|
{ id: '2', type: 'httpRequest', name: 'API Call', parameters: { url: 'https://api.example.com' } },
|
|
{ id: '3', type: 'set', name: 'Transform Data' },
|
|
{ id: '4', type: 'if', name: 'Conditional Logic' },
|
|
{ id: '5', type: 'slack', name: 'Send Notification' }
|
|
],
|
|
connections: {
|
|
'1': { main: [[{ node: '2', type: 'main', index: 0 }]] },
|
|
'2': { main: [[{ node: '3', type: 'main', index: 0 }]] },
|
|
'3': { main: [[{ node: '4', type: 'main', index: 0 }]] },
|
|
'4': { main: [[{ node: '5', type: 'main', index: 0 }]] }
|
|
}
|
|
};
|
|
|
|
const validateRequest: CallToolRequest = {
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'validate_workflow',
|
|
arguments: { workflow: complexWorkflow }
|
|
}
|
|
};
|
|
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockResolvedValue({
|
|
isValid: true,
|
|
errors: [],
|
|
warnings: [
|
|
{
|
|
nodeId: '2',
|
|
nodeType: 'httpRequest',
|
|
category: 'configuration',
|
|
severity: 'warning',
|
|
message: 'Consider adding error handling'
|
|
}
|
|
],
|
|
summary: { totalIssues: 1, criticalIssues: 0 }
|
|
});
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
await callToolHandler(validateRequest.params);
|
|
}
|
|
|
|
expect(telemetry.trackWorkflowCreation).toHaveBeenCalledWith(complexWorkflow, true);
|
|
expect(telemetry.trackToolUsage).toHaveBeenCalledWith(
|
|
'validate_workflow',
|
|
true,
|
|
expect.any(Number)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('MCP server lifecycle and telemetry', () => {
|
|
it('should handle server initialization with telemetry', async () => {
|
|
// Set up minimal environment for server creation
|
|
process.env.NODE_DB_PATH = ':memory:';
|
|
|
|
// Verify that server creation doesn't interfere with telemetry
|
|
const newServer = {} as N8NDocumentationMCPServer; // Mock instance
|
|
expect(newServer).toBeDefined();
|
|
|
|
// Telemetry should still be functional
|
|
expect(telemetry.getMetrics).toBeDefined();
|
|
expect(typeof telemetry.trackToolUsage).toBe('function');
|
|
});
|
|
|
|
it('should handle concurrent tool executions with telemetry', async () => {
|
|
const requests = [
|
|
{
|
|
method: 'tools/call' as const,
|
|
params: {
|
|
name: 'search_nodes',
|
|
arguments: { query: 'webhook' }
|
|
}
|
|
},
|
|
{
|
|
method: 'tools/call' as const,
|
|
params: {
|
|
name: 'search_nodes',
|
|
arguments: { query: 'http' }
|
|
}
|
|
},
|
|
{
|
|
method: 'tools/call' as const,
|
|
params: {
|
|
name: 'search_nodes',
|
|
arguments: { query: 'database' }
|
|
}
|
|
}
|
|
];
|
|
|
|
vi.spyOn(mcpServer as any, 'executeTool').mockResolvedValue({
|
|
results: [{ nodeType: 'test-node' }],
|
|
totalResults: 1
|
|
});
|
|
|
|
const server = (mcpServer as any).server;
|
|
const callToolHandler = server.requestHandlers.get('tools/call');
|
|
|
|
if (callToolHandler) {
|
|
await Promise.all(
|
|
requests.map(req => callToolHandler(req.params))
|
|
);
|
|
}
|
|
|
|
// All three calls should be tracked
|
|
expect(telemetry.trackToolUsage).toHaveBeenCalledTimes(3);
|
|
expect(telemetry.trackSearchQuery).toHaveBeenCalledTimes(3);
|
|
});
|
|
});
|
|
}); |