mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
feat: Tool Consolidation - Reduce MCP Tools by 38% (v2.26.0) (#439)
* 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>
This commit is contained in:
committed by
GitHub
parent
9ee4b9492f
commit
ff69e4ccca
@@ -4,72 +4,71 @@ import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
|
||||
describe('Basic MCP Connection', () => {
|
||||
it('should initialize MCP server', async () => {
|
||||
const server = new N8NDocumentationMCPServer();
|
||||
|
||||
// Test executeTool directly - it returns raw data
|
||||
const result = await server.executeTool('get_database_statistics', {});
|
||||
|
||||
// Test executeTool directly - tools_documentation returns a string
|
||||
const result = await server.executeTool('tools_documentation', {});
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('object');
|
||||
expect(result.totalNodes).toBeDefined();
|
||||
expect(result.statistics).toBeDefined();
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toContain('n8n MCP');
|
||||
|
||||
await server.shutdown();
|
||||
});
|
||||
|
||||
it('should execute list_nodes tool', async () => {
|
||||
|
||||
it('should execute search_nodes tool', async () => {
|
||||
const server = new N8NDocumentationMCPServer();
|
||||
|
||||
// First check if we have any nodes in the database
|
||||
const stats = await server.executeTool('get_database_statistics', {});
|
||||
const hasNodes = stats.totalNodes > 0;
|
||||
|
||||
const result = await server.executeTool('list_nodes', { limit: 5 });
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('object');
|
||||
expect(result.nodes).toBeDefined();
|
||||
expect(Array.isArray(result.nodes)).toBe(true);
|
||||
|
||||
if (hasNodes) {
|
||||
// If database has nodes, we should get up to 5
|
||||
expect(result.nodes.length).toBeLessThanOrEqual(5);
|
||||
expect(result.nodes.length).toBeGreaterThan(0);
|
||||
expect(result.nodes[0]).toHaveProperty('nodeType');
|
||||
expect(result.nodes[0]).toHaveProperty('displayName');
|
||||
} else {
|
||||
// In test environment with empty database, we expect empty results
|
||||
expect(result.nodes).toHaveLength(0);
|
||||
|
||||
try {
|
||||
// Search for a common node to verify database has content
|
||||
const result = await server.executeTool('search_nodes', { query: 'http', limit: 5 });
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('object');
|
||||
expect(result.results).toBeDefined();
|
||||
expect(Array.isArray(result.results)).toBe(true);
|
||||
|
||||
if (result.totalCount > 0) {
|
||||
// If database has nodes, we should get results
|
||||
expect(result.results.length).toBeLessThanOrEqual(5);
|
||||
expect(result.results.length).toBeGreaterThan(0);
|
||||
expect(result.results[0]).toHaveProperty('nodeType');
|
||||
expect(result.results[0]).toHaveProperty('displayName');
|
||||
}
|
||||
} catch (error: any) {
|
||||
// In test environment with empty database, expect appropriate error
|
||||
expect(error.message).toContain('Database is empty');
|
||||
}
|
||||
|
||||
|
||||
await server.shutdown();
|
||||
});
|
||||
|
||||
it('should search nodes', async () => {
|
||||
|
||||
it('should search nodes by keyword', async () => {
|
||||
const server = new N8NDocumentationMCPServer();
|
||||
|
||||
// First check if we have any nodes in the database
|
||||
const stats = await server.executeTool('get_database_statistics', {});
|
||||
const hasNodes = stats.totalNodes > 0;
|
||||
|
||||
const result = await server.executeTool('search_nodes', { query: 'webhook' });
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('object');
|
||||
expect(result.results).toBeDefined();
|
||||
expect(Array.isArray(result.results)).toBe(true);
|
||||
|
||||
// Only expect results if the database has nodes
|
||||
if (hasNodes) {
|
||||
expect(result.results.length).toBeGreaterThan(0);
|
||||
expect(result.totalCount).toBeGreaterThan(0);
|
||||
|
||||
// Should find webhook node
|
||||
const webhookNode = result.results.find((n: any) => n.nodeType === 'nodes-base.webhook');
|
||||
expect(webhookNode).toBeDefined();
|
||||
expect(webhookNode.displayName).toContain('Webhook');
|
||||
} else {
|
||||
// In test environment with empty database, we expect empty results
|
||||
expect(result.results).toHaveLength(0);
|
||||
expect(result.totalCount).toBe(0);
|
||||
|
||||
try {
|
||||
// Search to check if database has nodes
|
||||
const searchResult = await server.executeTool('search_nodes', { query: 'set', limit: 1 });
|
||||
const hasNodes = searchResult.totalCount > 0;
|
||||
|
||||
const result = await server.executeTool('search_nodes', { query: 'webhook' });
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('object');
|
||||
expect(result.results).toBeDefined();
|
||||
expect(Array.isArray(result.results)).toBe(true);
|
||||
|
||||
// Only expect results if the database has nodes
|
||||
if (hasNodes) {
|
||||
expect(result.results.length).toBeGreaterThan(0);
|
||||
expect(result.totalCount).toBeGreaterThan(0);
|
||||
|
||||
// Should find webhook node
|
||||
const webhookNode = result.results.find((n: any) => n.nodeType === 'nodes-base.webhook');
|
||||
expect(webhookNode).toBeDefined();
|
||||
expect(webhookNode.displayName).toContain('Webhook');
|
||||
}
|
||||
} catch (error: any) {
|
||||
// In test environment with empty database, expect appropriate error
|
||||
expect(error.message).toContain('Database is empty');
|
||||
}
|
||||
|
||||
|
||||
await server.shutdown();
|
||||
});
|
||||
});
|
||||
@@ -84,16 +84,16 @@ describe('MCP Error Handling', () => {
|
||||
|
||||
describe('Tool-Specific Errors', () => {
|
||||
describe('Node Discovery Errors', () => {
|
||||
it('should handle invalid category filter', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
category: 'invalid_category'
|
||||
it('should handle search with no matching results', async () => {
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: 'xyznonexistentnode123'
|
||||
} });
|
||||
|
||||
// Should return empty array, not error
|
||||
const result = JSON.parse((response as any).content[0].text);
|
||||
expect(result).toHaveProperty('nodes');
|
||||
expect(Array.isArray(result.nodes)).toBe(true);
|
||||
expect(result.nodes).toHaveLength(0);
|
||||
expect(result).toHaveProperty('results');
|
||||
expect(Array.isArray(result.results)).toBe(true);
|
||||
expect(result.results).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle invalid search mode', async () => {
|
||||
@@ -135,11 +135,13 @@ describe('MCP Error Handling', () => {
|
||||
});
|
||||
|
||||
describe('Validation Errors', () => {
|
||||
// v2.26.0: validate_node_operation consolidated into validate_node
|
||||
it('should handle invalid validation profile', async () => {
|
||||
try {
|
||||
await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: { method: 'GET', url: 'https://api.example.com' },
|
||||
mode: 'full',
|
||||
profile: 'invalid_profile' as any
|
||||
} });
|
||||
expect.fail('Should have thrown an error');
|
||||
@@ -279,9 +281,9 @@ describe('MCP Error Handling', () => {
|
||||
|
||||
for (let i = 0; i < requestCount; i++) {
|
||||
promises.push(
|
||||
client.callTool({ name: 'list_nodes', arguments: {
|
||||
limit: 1,
|
||||
category: i % 2 === 0 ? 'trigger' : 'transform'
|
||||
client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: i % 2 === 0 ? 'webhook' : 'http',
|
||||
limit: 1
|
||||
} })
|
||||
);
|
||||
}
|
||||
@@ -292,12 +294,14 @@ describe('MCP Error Handling', () => {
|
||||
});
|
||||
|
||||
describe('Invalid JSON Handling', () => {
|
||||
// v2.26.0: validate_node_operation consolidated into validate_node
|
||||
it('should handle invalid JSON in tool parameters', async () => {
|
||||
try {
|
||||
// Config should be an object, not a string
|
||||
await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: 'invalid json string' as any
|
||||
config: 'invalid json string' as any,
|
||||
mode: 'full'
|
||||
} });
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
@@ -320,13 +324,13 @@ describe('MCP Error Handling', () => {
|
||||
describe('Timeout Scenarios', () => {
|
||||
it('should handle rapid sequential requests', async () => {
|
||||
const start = Date.now();
|
||||
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
}
|
||||
|
||||
const duration = Date.now() - start;
|
||||
|
||||
|
||||
// Should complete reasonably quickly (under 5 seconds)
|
||||
expect(duration).toBeLessThan(5000);
|
||||
});
|
||||
@@ -410,25 +414,25 @@ describe('MCP Error Handling', () => {
|
||||
}
|
||||
|
||||
// Should still work
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: { limit: 1 } });
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: { query: 'webhook', limit: 1 } });
|
||||
expect(response).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle mixed success and failure', async () => {
|
||||
const promises = [
|
||||
client.callTool({ name: 'list_nodes', arguments: { limit: 5 } }),
|
||||
client.callTool({ name: 'search_nodes', arguments: { query: 'webhook', limit: 5 } }),
|
||||
client.callTool({ name: 'get_node', arguments: { nodeType: 'invalid' } }).catch(e => ({ error: e })),
|
||||
client.callTool({ name: 'get_database_statistics', arguments: {} }),
|
||||
client.callTool({ name: 'tools_documentation', arguments: {} }),
|
||||
client.callTool({ name: 'search_nodes', arguments: { query: '' } }).catch(e => ({ error: e })),
|
||||
client.callTool({ name: 'list_ai_tools', arguments: {} })
|
||||
client.callTool({ name: 'get_node', arguments: { nodeType: 'nodes-base.httpRequest' } })
|
||||
];
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
|
||||
// Some should succeed, some should fail
|
||||
const successes = results.filter(r => !('error' in r));
|
||||
const failures = results.filter(r => 'error' in r);
|
||||
|
||||
|
||||
expect(successes.length).toBeGreaterThan(0);
|
||||
expect(failures.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -436,14 +440,14 @@ describe('MCP Error Handling', () => {
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty responses gracefully', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
category: 'nonexistent_category'
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: 'xyznonexistentnode12345'
|
||||
} });
|
||||
|
||||
const result = JSON.parse((response as any).content[0].text);
|
||||
expect(result).toHaveProperty('nodes');
|
||||
expect(Array.isArray(result.nodes)).toBe(true);
|
||||
expect(result.nodes).toHaveLength(0);
|
||||
expect(result).toHaveProperty('results');
|
||||
expect(Array.isArray(result.results)).toBe(true);
|
||||
expect(result.results).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle special characters in parameters', async () => {
|
||||
@@ -469,14 +473,15 @@ describe('MCP Error Handling', () => {
|
||||
|
||||
it('should handle null and undefined gracefully', async () => {
|
||||
// Most tools should handle missing optional params
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: 'webhook',
|
||||
limit: undefined as any,
|
||||
category: null as any
|
||||
mode: null as any
|
||||
} });
|
||||
|
||||
const result = JSON.parse((response as any).content[0].text);
|
||||
expect(result).toHaveProperty('nodes');
|
||||
expect(Array.isArray(result.nodes)).toBe(true);
|
||||
expect(result).toHaveProperty('results');
|
||||
expect(Array.isArray(result.results)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -508,13 +513,15 @@ describe('MCP Error Handling', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// v2.26.0: validate_node_operation consolidated into validate_node
|
||||
it('should provide context for validation errors', async () => {
|
||||
const response = await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
const response = await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: {
|
||||
// Missing required fields
|
||||
method: 'INVALID_METHOD'
|
||||
}
|
||||
},
|
||||
mode: 'full'
|
||||
} });
|
||||
|
||||
const validation = JSON.parse((response as any).content[0].text);
|
||||
|
||||
@@ -23,13 +23,13 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// Verify database is populated by checking statistics
|
||||
const statsResponse = await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
if ((statsResponse as any).content && (statsResponse as any).content[0]) {
|
||||
const stats = JSON.parse((statsResponse as any).content[0].text);
|
||||
// Verify database is populated by searching for a common node
|
||||
const searchResponse = await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 1 } });
|
||||
if ((searchResponse as any).content && (searchResponse as any).content[0]) {
|
||||
const searchResult = JSON.parse((searchResponse as any).content[0].text);
|
||||
// Ensure database has nodes for testing
|
||||
if (!stats.totalNodes || stats.totalNodes === 0) {
|
||||
console.error('Database stats:', stats);
|
||||
if (!searchResult.totalCount || searchResult.totalCount === 0) {
|
||||
console.error('Search result:', searchResult);
|
||||
throw new Error('Test database not properly populated');
|
||||
}
|
||||
}
|
||||
@@ -46,13 +46,13 @@ describe('MCP Performance Tests', () => {
|
||||
const start = performance.now();
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
}
|
||||
|
||||
const duration = performance.now() - start;
|
||||
const avgTime = duration / iterations;
|
||||
|
||||
console.log(`Average response time for get_database_statistics: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(`Average response time for tools_documentation: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
|
||||
|
||||
// Environment-aware threshold (relaxed +20% for type safety overhead)
|
||||
@@ -60,20 +60,20 @@ describe('MCP Performance Tests', () => {
|
||||
expect(avgTime).toBeLessThan(threshold);
|
||||
});
|
||||
|
||||
it('should handle list operations efficiently', async () => {
|
||||
it('should handle search operations efficiently', async () => {
|
||||
const iterations = 50;
|
||||
const start = performance.now();
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await client.callTool({ name: 'list_nodes', arguments: { limit: 10 } });
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 10 } });
|
||||
}
|
||||
|
||||
const duration = performance.now() - start;
|
||||
const avgTime = duration / iterations;
|
||||
|
||||
console.log(`Average response time for list_nodes: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(`Average response time for search_nodes: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
|
||||
|
||||
|
||||
// Environment-aware threshold
|
||||
const threshold = process.env.CI ? 40 : 20;
|
||||
expect(avgTime).toBeLessThan(threshold);
|
||||
@@ -137,7 +137,7 @@ describe('MCP Performance Tests', () => {
|
||||
const promises = [];
|
||||
for (let i = 0; i < concurrentRequests; i++) {
|
||||
promises.push(
|
||||
client.callTool({ name: 'list_nodes', arguments: { limit: 5 } })
|
||||
client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 5 } })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
console.log(`Average time for ${concurrentRequests} concurrent requests: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
|
||||
|
||||
|
||||
// Concurrent requests should be more efficient than sequential
|
||||
const threshold = process.env.CI ? 25 : 10;
|
||||
expect(avgTime).toBeLessThan(threshold);
|
||||
@@ -156,11 +156,11 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
it('should handle mixed concurrent operations', async () => {
|
||||
const operations = [
|
||||
{ tool: 'list_nodes', params: { limit: 10 } },
|
||||
{ tool: 'search_nodes', params: { query: 'http' } },
|
||||
{ tool: 'get_database_statistics', params: {} },
|
||||
{ tool: 'list_ai_tools', params: {} },
|
||||
{ tool: 'list_tasks', params: {} }
|
||||
{ tool: 'search_nodes', params: { query: 'http', limit: 10 } },
|
||||
{ tool: 'search_nodes', params: { query: 'webhook' } },
|
||||
{ tool: 'tools_documentation', params: {} },
|
||||
{ tool: 'get_node', params: { nodeType: 'nodes-base.httpRequest' } },
|
||||
{ tool: 'get_node', params: { nodeType: 'nodes-base.webhook' } }
|
||||
];
|
||||
|
||||
const rounds = 10;
|
||||
@@ -186,34 +186,35 @@ describe('MCP Performance Tests', () => {
|
||||
});
|
||||
|
||||
describe('Large Data Performance', () => {
|
||||
it('should handle large node lists efficiently', async () => {
|
||||
it('should handle large search results efficiently', async () => {
|
||||
const start = performance.now();
|
||||
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
limit: 200 // Get many nodes
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: 'n8n', // Broad query to get many results
|
||||
limit: 200
|
||||
} });
|
||||
|
||||
const duration = performance.now() - start;
|
||||
|
||||
console.log(`Time to list 200 nodes: ${duration.toFixed(2)}ms`);
|
||||
|
||||
console.log(`Time to search 200 nodes: ${duration.toFixed(2)}ms`);
|
||||
|
||||
// Environment-aware threshold
|
||||
const threshold = process.env.CI ? 200 : 100;
|
||||
expect(duration).toBeLessThan(threshold);
|
||||
|
||||
// Check the response content
|
||||
expect(response).toBeDefined();
|
||||
|
||||
let nodes;
|
||||
|
||||
let results;
|
||||
if (response.content && Array.isArray(response.content) && response.content[0]) {
|
||||
// MCP standard response format
|
||||
expect(response.content[0].type).toBe('text');
|
||||
expect(response.content[0].text).toBeDefined();
|
||||
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(response.content[0].text);
|
||||
// list_nodes returns an object with nodes property
|
||||
nodes = parsed.nodes || parsed;
|
||||
// search_nodes returns an object with results property
|
||||
results = parsed.results || parsed;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse JSON:', e);
|
||||
console.error('Response text was:', response.content[0].text);
|
||||
@@ -221,18 +222,18 @@ describe('MCP Performance Tests', () => {
|
||||
}
|
||||
} else if (Array.isArray(response)) {
|
||||
// Direct array response
|
||||
nodes = response;
|
||||
} else if (response.nodes) {
|
||||
// Object with nodes property
|
||||
nodes = response.nodes;
|
||||
results = response;
|
||||
} else if (response.results) {
|
||||
// Object with results property
|
||||
results = response.results;
|
||||
} else {
|
||||
console.error('Unexpected response format:', response);
|
||||
throw new Error('Unexpected response format');
|
||||
}
|
||||
|
||||
expect(nodes).toBeDefined();
|
||||
expect(Array.isArray(nodes)).toBe(true);
|
||||
expect(nodes.length).toBeGreaterThan(100);
|
||||
|
||||
expect(results).toBeDefined();
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(results.length).toBeGreaterThan(50);
|
||||
});
|
||||
|
||||
it('should handle large workflow validation efficiently', async () => {
|
||||
@@ -301,10 +302,10 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
for (let i = 0; i < iterations; i += batchSize) {
|
||||
const promises = [];
|
||||
|
||||
|
||||
for (let j = 0; j < batchSize; j++) {
|
||||
promises.push(
|
||||
client.callTool({ name: 'get_database_statistics', arguments: {} })
|
||||
client.callTool({ name: 'tools_documentation', arguments: {} })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -330,9 +331,9 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
// Perform large operations
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await client.callTool({ name: 'list_nodes', arguments: { limit: 200 } });
|
||||
await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest'
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'n8n', limit: 200 } });
|
||||
await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest'
|
||||
} });
|
||||
}
|
||||
|
||||
@@ -359,16 +360,16 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
for (const load of loadLevels) {
|
||||
const start = performance.now();
|
||||
|
||||
|
||||
const promises = [];
|
||||
for (let i = 0; i < load; i++) {
|
||||
promises.push(
|
||||
client.callTool({ name: 'list_nodes', arguments: { limit: 1 } })
|
||||
client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 1 } })
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
|
||||
const duration = performance.now() - start;
|
||||
const avgTime = duration / load;
|
||||
|
||||
@@ -384,10 +385,10 @@ describe('MCP Performance Tests', () => {
|
||||
// Average time should not increase dramatically with load
|
||||
const firstAvg = results[0].avgTime;
|
||||
const lastAvg = results[results.length - 1].avgTime;
|
||||
|
||||
|
||||
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
|
||||
console.log(`Performance scaling - First avg: ${firstAvg.toFixed(2)}ms, Last avg: ${lastAvg.toFixed(2)}ms`);
|
||||
|
||||
|
||||
// Environment-aware scaling factor
|
||||
const scalingFactor = process.env.CI ? 3 : 2;
|
||||
expect(lastAvg).toBeLessThan(firstAvg * scalingFactor);
|
||||
@@ -403,16 +404,16 @@ describe('MCP Performance Tests', () => {
|
||||
const operation = i % 4;
|
||||
switch (operation) {
|
||||
case 0:
|
||||
promises.push(client.callTool({ name: 'list_nodes', arguments: { limit: 5 } }));
|
||||
promises.push(client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 5 } }));
|
||||
break;
|
||||
case 1:
|
||||
promises.push(client.callTool({ name: 'search_nodes', arguments: { query: 'test' } }));
|
||||
break;
|
||||
case 2:
|
||||
promises.push(client.callTool({ name: 'get_database_statistics', arguments: {} }));
|
||||
promises.push(client.callTool({ name: 'tools_documentation', arguments: {} }));
|
||||
break;
|
||||
case 3:
|
||||
promises.push(client.callTool({ name: 'list_ai_tools', arguments: {} }));
|
||||
promises.push(client.callTool({ name: 'get_node', arguments: { nodeType: 'nodes-base.set' } }));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -431,10 +432,10 @@ describe('MCP Performance Tests', () => {
|
||||
});
|
||||
|
||||
describe('Critical Path Optimization', () => {
|
||||
it('should optimize tool listing performance', async () => {
|
||||
it('should optimize search performance', async () => {
|
||||
// Warm up with multiple calls to ensure everything is initialized
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await client.callTool({ name: 'list_nodes', arguments: { limit: 1 } });
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 1 } });
|
||||
}
|
||||
|
||||
const iterations = 100;
|
||||
@@ -442,32 +443,32 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const start = performance.now();
|
||||
await client.callTool({ name: 'list_nodes', arguments: { limit: 20 } });
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 20 } });
|
||||
times.push(performance.now() - start);
|
||||
}
|
||||
|
||||
// Remove outliers (first few runs might be slower)
|
||||
times.sort((a, b) => a - b);
|
||||
const trimmedTimes = times.slice(10, -10); // Remove top and bottom 10%
|
||||
|
||||
|
||||
const avgTime = trimmedTimes.reduce((a, b) => a + b, 0) / trimmedTimes.length;
|
||||
const minTime = Math.min(...trimmedTimes);
|
||||
const maxTime = Math.max(...trimmedTimes);
|
||||
|
||||
console.log(`list_nodes performance - Avg: ${avgTime.toFixed(2)}ms, Min: ${minTime.toFixed(2)}ms, Max: ${maxTime.toFixed(2)}ms`);
|
||||
console.log(`search_nodes performance - Avg: ${avgTime.toFixed(2)}ms, Min: ${minTime.toFixed(2)}ms, Max: ${maxTime.toFixed(2)}ms`);
|
||||
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
|
||||
|
||||
// Environment-aware thresholds
|
||||
const threshold = process.env.CI ? 25 : 10;
|
||||
expect(avgTime).toBeLessThan(threshold);
|
||||
|
||||
|
||||
// Max should not be too much higher than average (no outliers)
|
||||
// More lenient in CI due to resource contention
|
||||
const maxMultiplier = process.env.CI ? 5 : 3;
|
||||
expect(maxTime).toBeLessThan(avgTime * maxMultiplier);
|
||||
});
|
||||
|
||||
it('should optimize search performance', async () => {
|
||||
it('should handle varied search queries efficiently', async () => {
|
||||
// Warm up with multiple calls
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'test' } });
|
||||
@@ -487,7 +488,7 @@ describe('MCP Performance Tests', () => {
|
||||
// Remove outliers
|
||||
times.sort((a, b) => a - b);
|
||||
const trimmedTimes = times.slice(10, -10); // Remove top and bottom 10%
|
||||
|
||||
|
||||
const avgTime = trimmedTimes.reduce((a, b) => a + b, 0) / trimmedTimes.length;
|
||||
|
||||
console.log(`search_nodes average performance: ${avgTime.toFixed(2)}ms`);
|
||||
@@ -542,7 +543,7 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
while (performance.now() - start < duration) {
|
||||
try {
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
requestCount++;
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
@@ -559,7 +560,7 @@ describe('MCP Performance Tests', () => {
|
||||
// Relaxed to 75 RPS locally to account for parallel test execution overhead
|
||||
const rpsThreshold = process.env.CI ? 50 : 75;
|
||||
expect(requestsPerSecond).toBeGreaterThan(rpsThreshold);
|
||||
|
||||
|
||||
// Error rate should be very low
|
||||
expect(errorCount).toBe(0);
|
||||
});
|
||||
@@ -591,7 +592,7 @@ describe('MCP Performance Tests', () => {
|
||||
const recoveryTimes: number[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const start = performance.now();
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
recoveryTimes.push(performance.now() - start);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('MCP Protocol Compliance', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
expectedOrder.push(i);
|
||||
requests.push(
|
||||
client.callTool({ name: 'get_database_statistics', arguments: {} })
|
||||
client.callTool({ name: 'tools_documentation', arguments: {} })
|
||||
.then(() => i)
|
||||
);
|
||||
}
|
||||
@@ -125,7 +125,7 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
it('should handle missing params gracefully', async () => {
|
||||
// Most tools should work without params
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {} });
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: { query: 'webhook' } });
|
||||
expect(response).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -147,8 +147,8 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
describe('Content Types', () => {
|
||||
it('should handle text content in tool responses', async () => {
|
||||
const response = await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
|
||||
const response = await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
|
||||
expect((response as any).content).toHaveLength(1);
|
||||
expect((response as any).content[0]).toHaveProperty('type', 'text');
|
||||
expect((response as any).content[0]).toHaveProperty('text');
|
||||
@@ -167,14 +167,15 @@ describe('MCP Protocol Compliance', () => {
|
||||
});
|
||||
|
||||
it('should handle JSON content properly', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: 'webhook',
|
||||
limit: 5
|
||||
} });
|
||||
|
||||
expect((response as any).content).toHaveLength(1);
|
||||
const content = JSON.parse((response as any).content[0].text);
|
||||
expect(content).toHaveProperty('nodes');
|
||||
expect(Array.isArray(content.nodes)).toBe(true);
|
||||
expect(content).toHaveProperty('results');
|
||||
expect(Array.isArray(content.results)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -197,10 +198,10 @@ describe('MCP Protocol Compliance', () => {
|
||||
const results: string[] = [];
|
||||
|
||||
// Start multiple requests with different delays
|
||||
const p1 = client.callTool({ name: 'get_database_statistics', arguments: {} })
|
||||
.then(() => { results.push('stats'); return 'stats'; });
|
||||
const p1 = client.callTool({ name: 'tools_documentation', arguments: {} })
|
||||
.then(() => { results.push('docs'); return 'docs'; });
|
||||
|
||||
const p2 = client.callTool({ name: 'list_nodes', arguments: { limit: 1 } })
|
||||
const p2 = client.callTool({ name: 'search_nodes', arguments: { query: 'webhook', limit: 1 } })
|
||||
.then(() => { results.push('nodes'); return 'nodes'; });
|
||||
|
||||
const p3 = client.callTool({ name: 'search_nodes', arguments: { query: 'http' } })
|
||||
@@ -216,13 +217,14 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
describe('Protocol Extensions', () => {
|
||||
it('should handle tool-specific extensions', async () => {
|
||||
// Test tool with complex params
|
||||
const response = await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
// Test tool with complex params (using consolidated validate_node from v2.26.0)
|
||||
const response = await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com'
|
||||
},
|
||||
mode: 'full',
|
||||
profile: 'runtime'
|
||||
} });
|
||||
|
||||
@@ -232,13 +234,13 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
it('should support optional parameters', async () => {
|
||||
// Call with minimal params
|
||||
const response1 = await client.callTool({ name: 'list_nodes', arguments: {} });
|
||||
|
||||
const response1 = await client.callTool({ name: 'search_nodes', arguments: { query: 'webhook' } });
|
||||
|
||||
// Call with all params
|
||||
const response2 = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
const response2 = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: 'webhook',
|
||||
limit: 10,
|
||||
category: 'trigger',
|
||||
package: 'n8n-nodes-base'
|
||||
mode: 'OR'
|
||||
} });
|
||||
|
||||
expect(response1).toBeDefined();
|
||||
@@ -255,7 +257,7 @@ describe('MCP Protocol Compliance', () => {
|
||||
await testClient.connect(clientTransport);
|
||||
|
||||
// Make a request
|
||||
const response = await testClient.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response = await testClient.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(response).toBeDefined();
|
||||
|
||||
// Close client
|
||||
@@ -263,7 +265,7 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
// Further requests should fail
|
||||
try {
|
||||
await testClient.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await testClient.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
@@ -286,7 +288,7 @@ describe('MCP Protocol Compliance', () => {
|
||||
const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
|
||||
await testClient.connect(clientTransport);
|
||||
|
||||
const response = await testClient.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response = await testClient.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(response).toBeDefined();
|
||||
|
||||
await testClient.close();
|
||||
|
||||
@@ -100,8 +100,8 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// Make some requests
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'list_nodes', arguments: { limit: 5 } });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 5 } });
|
||||
|
||||
// Clean termination
|
||||
await client.close();
|
||||
@@ -109,7 +109,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
|
||||
// Client should be closed
|
||||
try {
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect.fail('Should not be able to make requests after close');
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
@@ -133,7 +133,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// Make a request to ensure connection is active
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
|
||||
// Simulate abrupt disconnection by closing transport
|
||||
await clientTransport.close();
|
||||
@@ -141,7 +141,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
|
||||
// Further operations should fail
|
||||
try {
|
||||
await client.callTool({ name: 'list_nodes', arguments: {} });
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'http' } });
|
||||
expect.fail('Should not be able to make requests after transport close');
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
@@ -179,14 +179,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client1.connect(ct1);
|
||||
|
||||
// First session operations
|
||||
const response1 = await client1.callTool({ name: 'list_nodes', arguments: { limit: 3 } });
|
||||
const response1 = await client1.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 3 } });
|
||||
expect(response1).toBeDefined();
|
||||
expect((response1 as any).content).toBeDefined();
|
||||
expect((response1 as any).content[0]).toHaveProperty('type', 'text');
|
||||
const data1 = JSON.parse(((response1 as any).content[0] as any).text);
|
||||
// Handle both array response and object with nodes property
|
||||
const nodes1 = Array.isArray(data1) ? data1 : data1.nodes;
|
||||
expect(nodes1).toHaveLength(3);
|
||||
// Handle both array response and object with results property
|
||||
const results1 = Array.isArray(data1) ? data1 : data1.results;
|
||||
expect(results1.length).toBeLessThanOrEqual(3);
|
||||
|
||||
// Close first session completely
|
||||
await client1.close();
|
||||
@@ -204,14 +204,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client2.connect(ct2);
|
||||
|
||||
// Second session operations
|
||||
const response2 = await client2.callTool({ name: 'list_nodes', arguments: { limit: 5 } });
|
||||
const response2 = await client2.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 5 } });
|
||||
expect(response2).toBeDefined();
|
||||
expect((response2 as any).content).toBeDefined();
|
||||
expect((response2 as any).content[0]).toHaveProperty('type', 'text');
|
||||
const data2 = JSON.parse(((response2 as any).content[0] as any).text);
|
||||
// Handle both array response and object with nodes property
|
||||
const nodes2 = Array.isArray(data2) ? data2 : data2.nodes;
|
||||
expect(nodes2).toHaveLength(5);
|
||||
// Handle both array response and object with results property
|
||||
const results2 = Array.isArray(data2) ? data2 : data2.results;
|
||||
expect(results2.length).toBeLessThanOrEqual(5);
|
||||
|
||||
// Clean up
|
||||
await client2.close();
|
||||
@@ -228,9 +228,9 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
const client1 = new Client({ name: 'multi-seq-1', version: '1.0.0' }, {});
|
||||
await client1.connect(ct1);
|
||||
|
||||
const resp1 = await client1.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const resp1 = await client1.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(resp1).toBeDefined();
|
||||
|
||||
|
||||
await client1.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
@@ -239,8 +239,8 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await mcpServer.connectToTransport(st2);
|
||||
const client2 = new Client({ name: 'multi-seq-2', version: '1.0.0' }, {});
|
||||
await client2.connect(ct2);
|
||||
|
||||
const resp2 = await client2.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
|
||||
const resp2 = await client2.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(resp2).toBeDefined();
|
||||
|
||||
await client2.close();
|
||||
@@ -261,14 +261,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client1.connect(ct1);
|
||||
|
||||
// Make some requests
|
||||
await client1.callTool({ name: 'list_nodes', arguments: { limit: 10 } });
|
||||
await client1.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 10 } });
|
||||
await client1.close();
|
||||
await mcpServer1.close();
|
||||
|
||||
// Second session - should be fresh
|
||||
const mcpServer2 = new TestableN8NMCPServer();
|
||||
await mcpServer2.initialize();
|
||||
|
||||
|
||||
const [st2, ct2] = InMemoryTransport.createLinkedPair();
|
||||
await mcpServer2.connectToTransport(st2);
|
||||
|
||||
@@ -276,7 +276,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client2.connect(ct2);
|
||||
|
||||
// Should work normally
|
||||
const response = await client2.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response = await client2.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(response).toBeDefined();
|
||||
|
||||
await client2.close();
|
||||
@@ -299,7 +299,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// Quick operation
|
||||
const response = await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response = await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(response).toBeDefined();
|
||||
|
||||
// Explicit cleanup for each iteration
|
||||
@@ -392,7 +392,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
|
||||
// Light operation
|
||||
if (i % 10 === 0) {
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
}
|
||||
|
||||
// Explicit cleanup
|
||||
@@ -420,8 +420,8 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < requestCount; i++) {
|
||||
const toolName = i % 2 === 0 ? 'list_nodes' : 'get_database_statistics';
|
||||
const params = toolName === 'list_nodes' ? { limit: 1 } : {};
|
||||
const toolName = i % 2 === 0 ? 'search_nodes' : 'tools_documentation';
|
||||
const params = toolName === 'search_nodes' ? { query: 'http', limit: 1 } : {};
|
||||
promises.push(client.callTool({ name: toolName as any, arguments: params }));
|
||||
}
|
||||
|
||||
@@ -460,9 +460,9 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
}
|
||||
|
||||
// Session should still be active
|
||||
const response = await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response = await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(response).toBeDefined();
|
||||
|
||||
|
||||
await client.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
|
||||
await mcpServer.close();
|
||||
@@ -496,9 +496,9 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
});
|
||||
|
||||
// Session should still work
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: { limit: 1 } });
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 1 } });
|
||||
expect(response).toBeDefined();
|
||||
|
||||
|
||||
await client.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
|
||||
await mcpServer.close();
|
||||
@@ -539,7 +539,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
resources.clients.push(client);
|
||||
|
||||
// Make a request to ensure connection is active
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
}
|
||||
|
||||
// Verify all resources are active
|
||||
@@ -586,7 +586,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
// 3. Verify cleanup by attempting operations (should fail)
|
||||
for (let i = 0; i < resources.clients.length; i++) {
|
||||
try {
|
||||
await resources.clients[i].callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await resources.clients[i].callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect.fail('Client should be closed');
|
||||
} catch (error) {
|
||||
// Expected - client is closed
|
||||
@@ -643,9 +643,9 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
}, {});
|
||||
|
||||
await client.connect(ct1);
|
||||
|
||||
|
||||
// Initial request
|
||||
const response1 = await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response1 = await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(response1).toBeDefined();
|
||||
|
||||
// Close first client
|
||||
@@ -654,7 +654,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
|
||||
// New connection with same server
|
||||
const [st2, ct2] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
|
||||
const connectTimeout = setTimeout(() => {
|
||||
throw new Error('Second connection timeout');
|
||||
}, 3000);
|
||||
@@ -673,14 +673,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
}, {});
|
||||
|
||||
await newClient.connect(ct2);
|
||||
|
||||
|
||||
// Should work normally
|
||||
const callTimeout = setTimeout(() => {
|
||||
throw new Error('Second call timeout');
|
||||
}, 3000);
|
||||
|
||||
try {
|
||||
const response2 = await newClient.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response2 = await newClient.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
clearTimeout(callTimeout);
|
||||
expect(response2).toBeDefined();
|
||||
} catch (error) {
|
||||
|
||||
@@ -114,7 +114,7 @@ export class TestableN8NMCPServer {
|
||||
// The MCP server initializes its database lazily
|
||||
// We can trigger initialization by calling executeTool
|
||||
try {
|
||||
await this.mcpServer.executeTool('get_database_statistics', {});
|
||||
await this.mcpServer.executeTool('tools_documentation', {});
|
||||
} catch (error) {
|
||||
// Ignore errors, we just want to trigger initialization
|
||||
}
|
||||
|
||||
@@ -30,66 +30,6 @@ describe('MCP Tool Invocation', () => {
|
||||
});
|
||||
|
||||
describe('Node Discovery Tools', () => {
|
||||
describe('list_nodes', () => {
|
||||
it('should list nodes with default parameters', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {} });
|
||||
|
||||
expect((response as any).content).toHaveLength(1);
|
||||
expect((response as any).content[0].type).toBe('text');
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
// The result is an object with nodes array and totalCount
|
||||
expect(result).toHaveProperty('nodes');
|
||||
expect(result).toHaveProperty('totalCount');
|
||||
|
||||
const nodes = result.nodes;
|
||||
expect(Array.isArray(nodes)).toBe(true);
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
|
||||
// Check node structure
|
||||
const firstNode = nodes[0];
|
||||
expect(firstNode).toHaveProperty('nodeType');
|
||||
expect(firstNode).toHaveProperty('displayName');
|
||||
expect(firstNode).toHaveProperty('category');
|
||||
});
|
||||
|
||||
it('should filter nodes by category', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
category: 'trigger'
|
||||
}});
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
const nodes = result.nodes;
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
nodes.forEach((node: any) => {
|
||||
expect(node.category).toBe('trigger');
|
||||
});
|
||||
});
|
||||
|
||||
it('should limit results', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
limit: 5
|
||||
}});
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
const nodes = result.nodes;
|
||||
expect(nodes).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should filter by package', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
package: 'n8n-nodes-base'
|
||||
}});
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
const nodes = result.nodes;
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
nodes.forEach((node: any) => {
|
||||
expect(node.package).toBe('n8n-nodes-base');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('search_nodes', () => {
|
||||
it('should search nodes by keyword', async () => {
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
@@ -211,14 +151,16 @@ describe('MCP Tool Invocation', () => {
|
||||
});
|
||||
|
||||
describe('Validation Tools', () => {
|
||||
describe('validate_node_operation', () => {
|
||||
// v2.26.0: validate_node_operation consolidated into validate_node with mode parameter
|
||||
describe('validate_node', () => {
|
||||
it('should validate valid node configuration', async () => {
|
||||
const response = await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
const response = await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/data'
|
||||
}
|
||||
},
|
||||
mode: 'full'
|
||||
}});
|
||||
|
||||
const validation = JSON.parse(((response as any).content[0]).text);
|
||||
@@ -228,12 +170,13 @@ describe('MCP Tool Invocation', () => {
|
||||
});
|
||||
|
||||
it('should detect missing required fields', async () => {
|
||||
const response = await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
const response = await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: {
|
||||
method: 'GET'
|
||||
// Missing required 'url' field
|
||||
}
|
||||
},
|
||||
mode: 'full'
|
||||
}});
|
||||
|
||||
const validation = JSON.parse(((response as any).content[0]).text);
|
||||
@@ -244,11 +187,12 @@ describe('MCP Tool Invocation', () => {
|
||||
|
||||
it('should support different validation profiles', async () => {
|
||||
const profiles = ['minimal', 'runtime', 'ai-friendly', 'strict'];
|
||||
|
||||
|
||||
for (const profile of profiles) {
|
||||
const response = await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
const response = await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: { method: 'GET', url: 'https://api.example.com' },
|
||||
mode: 'full',
|
||||
profile
|
||||
}});
|
||||
|
||||
@@ -427,85 +371,8 @@ describe('MCP Tool Invocation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI Tools', () => {
|
||||
describe('list_ai_tools', () => {
|
||||
it('should list AI-capable nodes', async () => {
|
||||
const response = await client.callTool({ name: 'list_ai_tools', arguments: {} });
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
expect(result).toHaveProperty('tools');
|
||||
const aiTools = result.tools;
|
||||
expect(Array.isArray(aiTools)).toBe(true);
|
||||
expect(aiTools.length).toBeGreaterThan(0);
|
||||
|
||||
// All should have nodeType and displayName
|
||||
aiTools.forEach((tool: any) => {
|
||||
expect(tool).toHaveProperty('nodeType');
|
||||
expect(tool).toHaveProperty('displayName');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_node_as_tool_info', () => {
|
||||
it('should provide AI tool usage information', async () => {
|
||||
const response = await client.callTool({ name: 'get_node_as_tool_info', arguments: {
|
||||
nodeType: 'nodes-base.slack'
|
||||
}});
|
||||
|
||||
const info = JSON.parse(((response as any).content[0]).text);
|
||||
expect(info).toHaveProperty('nodeType');
|
||||
expect(info).toHaveProperty('isMarkedAsAITool');
|
||||
expect(info).toHaveProperty('aiToolCapabilities');
|
||||
expect(info.aiToolCapabilities).toHaveProperty('commonUseCases');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Task Templates', () => {
|
||||
// get_node_for_task was removed in v2.15.0
|
||||
// Use search_nodes({ includeExamples: true }) instead for real-world examples
|
||||
|
||||
describe('list_tasks', () => {
|
||||
it('should list all available tasks', async () => {
|
||||
const response = await client.callTool({ name: 'list_tasks', arguments: {} });
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
expect(result).toHaveProperty('totalTasks');
|
||||
expect(result).toHaveProperty('categories');
|
||||
expect(result.totalTasks).toBeGreaterThan(0);
|
||||
|
||||
// Check categories structure
|
||||
const categories = result.categories;
|
||||
expect(typeof categories).toBe('object');
|
||||
|
||||
// Check at least one category has tasks
|
||||
const hasTasksInCategories = Object.values(categories).some((tasks: any) =>
|
||||
Array.isArray(tasks) && tasks.length > 0
|
||||
);
|
||||
expect(hasTasksInCategories).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter by category', async () => {
|
||||
const response = await client.callTool({ name: 'list_tasks', arguments: {
|
||||
category: 'HTTP/API'
|
||||
}});
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
expect(result).toHaveProperty('category', 'HTTP/API');
|
||||
expect(result).toHaveProperty('tasks');
|
||||
|
||||
const httpTasks = result.tasks;
|
||||
expect(Array.isArray(httpTasks)).toBe(true);
|
||||
expect(httpTasks.length).toBeGreaterThan(0);
|
||||
|
||||
httpTasks.forEach((task: any) => {
|
||||
expect(task).toHaveProperty('task');
|
||||
expect(task).toHaveProperty('description');
|
||||
expect(task).toHaveProperty('nodeType');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
// AI Tools section removed - list_ai_tools and get_node_as_tool_info were removed in v2.25.0
|
||||
// Use search_nodes with query for finding AI-capable nodes
|
||||
|
||||
describe('Complex Tool Interactions', () => {
|
||||
it('should handle tool chaining', async () => {
|
||||
@@ -526,20 +393,20 @@ describe('MCP Tool Invocation', () => {
|
||||
});
|
||||
|
||||
it('should handle parallel tool calls', async () => {
|
||||
const tools = [
|
||||
'list_nodes',
|
||||
'get_database_statistics',
|
||||
'list_ai_tools',
|
||||
'list_tasks'
|
||||
const toolCalls = [
|
||||
{ name: 'search_nodes', arguments: { query: 'http' } },
|
||||
{ name: 'tools_documentation', arguments: {} },
|
||||
{ name: 'get_node', arguments: { nodeType: 'nodes-base.httpRequest' } },
|
||||
{ name: 'search_nodes', arguments: { query: 'webhook' } }
|
||||
];
|
||||
|
||||
const promises = tools.map(tool =>
|
||||
client.callTool({ name: tool as any, arguments: {} })
|
||||
const promises = toolCalls.map(call =>
|
||||
client.callTool(call)
|
||||
);
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
expect(responses).toHaveLength(tools.length);
|
||||
|
||||
expect(responses).toHaveLength(toolCalls.length);
|
||||
responses.forEach(response => {
|
||||
expect(response.content).toHaveLength(1);
|
||||
expect(((response as any).content[0]).type).toBe('text');
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
/**
|
||||
* Integration Tests: handleListAvailableTools
|
||||
*
|
||||
* Tests tool listing functionality.
|
||||
* Covers tool discovery and configuration status.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createMcpContext } from '../utils/mcp-context';
|
||||
import { InstanceContext } from '../../../../src/types/instance-context';
|
||||
import { handleListAvailableTools } from '../../../../src/mcp/handlers-n8n-manager';
|
||||
import { ListToolsResponse } from '../utils/response-types';
|
||||
|
||||
describe('Integration: handleListAvailableTools', () => {
|
||||
let mcpContext: InstanceContext;
|
||||
|
||||
beforeEach(() => {
|
||||
mcpContext = createMcpContext();
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// List All Tools
|
||||
// ======================================================================
|
||||
|
||||
describe('Tool Listing', () => {
|
||||
it('should list all available tools organized by category', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data).toBeDefined();
|
||||
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
// Verify tools array exists
|
||||
expect(data).toHaveProperty('tools');
|
||||
expect(Array.isArray(data.tools)).toBe(true);
|
||||
expect(data.tools.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify tool categories
|
||||
const categories = data.tools.map((cat: any) => cat.category);
|
||||
expect(categories).toContain('Workflow Management');
|
||||
expect(categories).toContain('Execution Management');
|
||||
expect(categories).toContain('System');
|
||||
|
||||
// Verify each category has tools
|
||||
data.tools.forEach(category => {
|
||||
expect(category).toHaveProperty('category');
|
||||
expect(category).toHaveProperty('tools');
|
||||
expect(Array.isArray(category.tools)).toBe(true);
|
||||
expect(category.tools.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify each tool has required fields
|
||||
category.tools.forEach(tool => {
|
||||
expect(tool).toHaveProperty('name');
|
||||
expect(tool).toHaveProperty('description');
|
||||
expect(typeof tool.name).toBe('string');
|
||||
expect(typeof tool.description).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should include API configuration status', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
// Verify configuration status
|
||||
expect(data).toHaveProperty('apiConfigured');
|
||||
expect(typeof data.apiConfigured).toBe('boolean');
|
||||
|
||||
// Since tests run with API configured, should be true
|
||||
expect(data.apiConfigured).toBe(true);
|
||||
|
||||
// Verify configuration details are present when configured
|
||||
if (data.apiConfigured) {
|
||||
expect(data).toHaveProperty('configuration');
|
||||
expect(data.configuration).toBeDefined();
|
||||
expect(data.configuration).toHaveProperty('apiUrl');
|
||||
expect(data.configuration).toHaveProperty('timeout');
|
||||
expect(data.configuration).toHaveProperty('maxRetries');
|
||||
}
|
||||
});
|
||||
|
||||
it('should include API limitations information', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
// Verify limitations are documented
|
||||
expect(data).toHaveProperty('limitations');
|
||||
expect(Array.isArray(data.limitations)).toBe(true);
|
||||
expect(data.limitations.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify limitations are informative strings
|
||||
data.limitations.forEach(limitation => {
|
||||
expect(typeof limitation).toBe('string');
|
||||
expect(limitation.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Common known limitations
|
||||
const limitationsText = data.limitations.join(' ');
|
||||
expect(limitationsText).toContain('Cannot execute workflows directly');
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// Workflow Management Tools
|
||||
// ======================================================================
|
||||
|
||||
describe('Workflow Management Tools', () => {
|
||||
it('should include all workflow management tools', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
const workflowCategory = data.tools.find(cat => cat.category === 'Workflow Management');
|
||||
expect(workflowCategory).toBeDefined();
|
||||
|
||||
const toolNames = workflowCategory!.tools.map(t => t.name);
|
||||
|
||||
// Core workflow tools
|
||||
expect(toolNames).toContain('n8n_create_workflow');
|
||||
expect(toolNames).toContain('n8n_get_workflow');
|
||||
expect(toolNames).toContain('n8n_update_workflow');
|
||||
expect(toolNames).toContain('n8n_delete_workflow');
|
||||
expect(toolNames).toContain('n8n_list_workflows');
|
||||
|
||||
// Enhanced workflow tools
|
||||
expect(toolNames).toContain('n8n_get_workflow_details');
|
||||
expect(toolNames).toContain('n8n_get_workflow_structure');
|
||||
expect(toolNames).toContain('n8n_get_workflow_minimal');
|
||||
expect(toolNames).toContain('n8n_validate_workflow');
|
||||
expect(toolNames).toContain('n8n_autofix_workflow');
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// Execution Management Tools
|
||||
// ======================================================================
|
||||
|
||||
describe('Execution Management Tools', () => {
|
||||
it('should include all execution management tools', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
const executionCategory = data.tools.find(cat => cat.category === 'Execution Management');
|
||||
expect(executionCategory).toBeDefined();
|
||||
|
||||
const toolNames = executionCategory!.tools.map(t => t.name);
|
||||
|
||||
expect(toolNames).toContain('n8n_trigger_webhook_workflow');
|
||||
expect(toolNames).toContain('n8n_get_execution');
|
||||
expect(toolNames).toContain('n8n_list_executions');
|
||||
expect(toolNames).toContain('n8n_delete_execution');
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// System Tools
|
||||
// ======================================================================
|
||||
|
||||
describe('System Tools', () => {
|
||||
it('should include system tools', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
const systemCategory = data.tools.find(cat => cat.category === 'System');
|
||||
expect(systemCategory).toBeDefined();
|
||||
|
||||
const toolNames = systemCategory!.tools.map(t => t.name);
|
||||
|
||||
expect(toolNames).toContain('n8n_health_check');
|
||||
expect(toolNames).toContain('n8n_list_available_tools');
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// Response Format Verification
|
||||
// ======================================================================
|
||||
|
||||
describe('Response Format', () => {
|
||||
it('should return complete tool list response structure', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data).toBeDefined();
|
||||
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
// Verify all required fields
|
||||
expect(data).toHaveProperty('tools');
|
||||
expect(data).toHaveProperty('apiConfigured');
|
||||
expect(data).toHaveProperty('limitations');
|
||||
|
||||
// Verify optional configuration field
|
||||
if (data.apiConfigured) {
|
||||
expect(data).toHaveProperty('configuration');
|
||||
}
|
||||
|
||||
// Verify data types
|
||||
expect(Array.isArray(data.tools)).toBe(true);
|
||||
expect(typeof data.apiConfigured).toBe('boolean');
|
||||
expect(Array.isArray(data.limitations)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,29 +19,6 @@ export interface HealthCheckResponse {
|
||||
[key: string]: any; // Allow dynamic property access for optional field checks
|
||||
}
|
||||
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ToolCategory {
|
||||
category: string;
|
||||
tools: ToolDefinition[];
|
||||
}
|
||||
|
||||
export interface ApiConfiguration {
|
||||
apiUrl: string;
|
||||
timeout: number;
|
||||
maxRetries: number;
|
||||
}
|
||||
|
||||
export interface ListToolsResponse {
|
||||
tools: ToolCategory[];
|
||||
apiConfigured: boolean;
|
||||
configuration?: ApiConfiguration | null;
|
||||
limitations: string[];
|
||||
}
|
||||
|
||||
export interface ApiStatus {
|
||||
configured: boolean;
|
||||
connected: boolean;
|
||||
|
||||
@@ -500,15 +500,15 @@ describe.skip('MCP Telemetry Integration', () => {
|
||||
const slowToolRequest: CallToolRequest = {
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'list_nodes',
|
||||
arguments: { limit: 1000 }
|
||||
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 { nodes: [], totalCount: 0 };
|
||||
return { results: [], totalCount: 0 };
|
||||
});
|
||||
|
||||
const server = (mcpServer as any).server;
|
||||
@@ -519,7 +519,7 @@ describe.skip('MCP Telemetry Integration', () => {
|
||||
}
|
||||
|
||||
expect(telemetry.trackToolUsage).toHaveBeenCalledWith(
|
||||
'list_nodes',
|
||||
'search_nodes',
|
||||
true,
|
||||
expect.any(Number)
|
||||
);
|
||||
|
||||
@@ -73,14 +73,14 @@ describe('Disabled Tools Feature (Issue #410)', () => {
|
||||
});
|
||||
|
||||
it('should parse multiple disabled tools correctly', () => {
|
||||
process.env.DISABLED_TOOLS = 'n8n_diagnostic,n8n_health_check,list_nodes';
|
||||
process.env.DISABLED_TOOLS = 'n8n_diagnostic,n8n_health_check,search_nodes';
|
||||
server = new TestableN8NMCPServer();
|
||||
const disabledTools = server.testGetDisabledTools();
|
||||
|
||||
expect(disabledTools.size).toBe(3);
|
||||
expect(disabledTools.has('n8n_diagnostic')).toBe(true);
|
||||
expect(disabledTools.has('n8n_health_check')).toBe(true);
|
||||
expect(disabledTools.has('list_nodes')).toBe(true);
|
||||
expect(disabledTools.has('search_nodes')).toBe(true);
|
||||
});
|
||||
|
||||
it('should trim whitespace from tool names', () => {
|
||||
@@ -94,14 +94,14 @@ describe('Disabled Tools Feature (Issue #410)', () => {
|
||||
});
|
||||
|
||||
it('should filter out empty entries from comma-separated list', () => {
|
||||
process.env.DISABLED_TOOLS = 'n8n_diagnostic,,n8n_health_check,,,list_nodes';
|
||||
process.env.DISABLED_TOOLS = 'n8n_diagnostic,,n8n_health_check,,,search_nodes';
|
||||
server = new TestableN8NMCPServer();
|
||||
const disabledTools = server.testGetDisabledTools();
|
||||
|
||||
expect(disabledTools.size).toBe(3);
|
||||
expect(disabledTools.has('n8n_diagnostic')).toBe(true);
|
||||
expect(disabledTools.has('n8n_health_check')).toBe(true);
|
||||
expect(disabledTools.has('list_nodes')).toBe(true);
|
||||
expect(disabledTools.has('search_nodes')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle single comma correctly', () => {
|
||||
|
||||
@@ -1031,7 +1031,7 @@ describe('handlers-n8n-manager', () => {
|
||||
'1. Verify n8n instance is running',
|
||||
'2. Check N8N_API_URL is correct',
|
||||
'3. Verify N8N_API_KEY has proper permissions',
|
||||
'4. Run n8n_diagnostic for detailed analysis',
|
||||
'4. Run n8n_health_check with mode="diagnostic" for detailed analysis',
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -1068,14 +1068,14 @@ describe('handlers-n8n-manager', () => {
|
||||
},
|
||||
toolsAvailability: {
|
||||
documentationTools: {
|
||||
count: 22,
|
||||
count: 7,
|
||||
enabled: true,
|
||||
},
|
||||
managementTools: {
|
||||
count: 16,
|
||||
count: 12,
|
||||
enabled: true,
|
||||
},
|
||||
totalAvailable: 38,
|
||||
totalAvailable: 19,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -201,63 +201,76 @@ describe('Parameter Validation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate_node_operation', () => {
|
||||
describe('validate_node (consolidated)', () => {
|
||||
it('should require nodeType and config parameters', async () => {
|
||||
await expect(server.testExecuteTool('validate_node_operation', {}))
|
||||
.rejects.toThrow('validate_node_operation: Validation failed:\n • nodeType: nodeType is required\n • config: config is required');
|
||||
await expect(server.testExecuteTool('validate_node', {}))
|
||||
.rejects.toThrow('validate_node: Validation failed:\n • nodeType: nodeType is required\n • config: config is required');
|
||||
});
|
||||
|
||||
it('should require nodeType parameter when config is provided', async () => {
|
||||
await expect(server.testExecuteTool('validate_node_operation', { config: {} }))
|
||||
.rejects.toThrow('validate_node_operation: Validation failed:\n • nodeType: nodeType is required');
|
||||
await expect(server.testExecuteTool('validate_node', { config: {} }))
|
||||
.rejects.toThrow('validate_node: Validation failed:\n • nodeType: nodeType is required');
|
||||
});
|
||||
|
||||
it('should require config parameter when nodeType is provided', async () => {
|
||||
await expect(server.testExecuteTool('validate_node_operation', { nodeType: 'nodes-base.httpRequest' }))
|
||||
.rejects.toThrow('validate_node_operation: Validation failed:\n • config: config is required');
|
||||
await expect(server.testExecuteTool('validate_node', { nodeType: 'nodes-base.httpRequest' }))
|
||||
.rejects.toThrow('validate_node: Validation failed:\n • config: config is required');
|
||||
});
|
||||
|
||||
it('should succeed with valid parameters', async () => {
|
||||
const result = await server.testExecuteTool('validate_node_operation', {
|
||||
it('should succeed with valid parameters (full mode)', async () => {
|
||||
const result = await server.testExecuteTool('validate_node', {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: { method: 'GET', url: 'https://api.example.com' }
|
||||
config: { method: 'GET', url: 'https://api.example.com' },
|
||||
mode: 'full'
|
||||
});
|
||||
expect(result).toEqual({ valid: true });
|
||||
});
|
||||
|
||||
it('should succeed with valid parameters (minimal mode)', async () => {
|
||||
const result = await server.testExecuteTool('validate_node', {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: {},
|
||||
mode: 'minimal'
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('search_node_properties', () => {
|
||||
it('should require nodeType and query parameters', async () => {
|
||||
await expect(server.testExecuteTool('search_node_properties', {}))
|
||||
.rejects.toThrow('Missing required parameters for search_node_properties: nodeType, query');
|
||||
describe('get_node mode=search_properties (consolidated)', () => {
|
||||
it('should require nodeType and propertyQuery parameters', async () => {
|
||||
await expect(server.testExecuteTool('get_node', { mode: 'search_properties' }))
|
||||
.rejects.toThrow('Missing required parameters for get_node: nodeType');
|
||||
});
|
||||
|
||||
it('should succeed with valid parameters', async () => {
|
||||
const result = await server.testExecuteTool('search_node_properties', {
|
||||
const result = await server.testExecuteTool('get_node', {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
query: 'auth'
|
||||
mode: 'search_properties',
|
||||
propertyQuery: 'auth'
|
||||
});
|
||||
expect(result).toEqual({ properties: [] });
|
||||
});
|
||||
|
||||
it('should handle optional maxResults parameter', async () => {
|
||||
const result = await server.testExecuteTool('search_node_properties', {
|
||||
it('should handle optional maxPropertyResults parameter', async () => {
|
||||
const result = await server.testExecuteTool('get_node', {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
query: 'auth',
|
||||
maxResults: 5
|
||||
mode: 'search_properties',
|
||||
propertyQuery: 'auth',
|
||||
maxPropertyResults: 5
|
||||
});
|
||||
expect(result).toEqual({ properties: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('list_node_templates', () => {
|
||||
it('should require nodeTypes parameter', async () => {
|
||||
await expect(server.testExecuteTool('list_node_templates', {}))
|
||||
.rejects.toThrow('list_node_templates: Validation failed:\n • nodeTypes: nodeTypes is required');
|
||||
describe('search_templates searchMode=by_nodes (consolidated)', () => {
|
||||
it('should require nodeTypes parameter for by_nodes searchMode', async () => {
|
||||
await expect(server.testExecuteTool('search_templates', { searchMode: 'by_nodes' }))
|
||||
.rejects.toThrow('nodeTypes array is required for searchMode=by_nodes');
|
||||
});
|
||||
|
||||
it('should succeed with valid nodeTypes array', async () => {
|
||||
const result = await server.testExecuteTool('list_node_templates', {
|
||||
const result = await server.testExecuteTool('search_templates', {
|
||||
searchMode: 'by_nodes',
|
||||
nodeTypes: ['nodes-base.httpRequest', 'nodes-base.slack']
|
||||
});
|
||||
expect(result).toEqual({ templates: [] });
|
||||
@@ -320,45 +333,43 @@ describe('Parameter Validation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxResults parameter conversion', () => {
|
||||
it('should convert string numbers to numbers', async () => {
|
||||
describe('maxPropertyResults parameter conversion (v2.26.0 consolidated)', () => {
|
||||
it('should pass numeric maxPropertyResults to searchNodeProperties', async () => {
|
||||
const mockSearchNodeProperties = vi.spyOn(server as any, 'searchNodeProperties');
|
||||
|
||||
await server.testExecuteTool('search_node_properties', {
|
||||
|
||||
// v2.26.0: search_node_properties consolidated into get_node with mode='search_properties'
|
||||
await server.testExecuteTool('get_node', {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
query: 'auth',
|
||||
maxResults: '5'
|
||||
mode: 'search_properties',
|
||||
propertyQuery: 'auth',
|
||||
maxPropertyResults: 5
|
||||
});
|
||||
|
||||
expect(mockSearchNodeProperties).toHaveBeenCalledWith('nodes-base.httpRequest', 'auth', 5);
|
||||
});
|
||||
|
||||
it('should use default when maxResults is invalid', async () => {
|
||||
it('should use default maxPropertyResults when not provided', async () => {
|
||||
const mockSearchNodeProperties = vi.spyOn(server as any, 'searchNodeProperties');
|
||||
|
||||
await server.testExecuteTool('search_node_properties', {
|
||||
|
||||
// v2.26.0: search_node_properties consolidated into get_node with mode='search_properties'
|
||||
await server.testExecuteTool('get_node', {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
query: 'auth',
|
||||
maxResults: 'invalid'
|
||||
mode: 'search_properties',
|
||||
propertyQuery: 'auth'
|
||||
});
|
||||
|
||||
expect(mockSearchNodeProperties).toHaveBeenCalledWith('nodes-base.httpRequest', 'auth', 20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('templateLimit parameter conversion', () => {
|
||||
it('should reject string limit values', async () => {
|
||||
await expect(server.testExecuteTool('list_node_templates', {
|
||||
describe('templateLimit parameter conversion (v2.26.0 consolidated)', () => {
|
||||
it('should handle search_templates with by_nodes mode', async () => {
|
||||
// search_templates now handles list_node_templates functionality via searchMode='by_nodes'
|
||||
await expect(server.testExecuteTool('search_templates', {
|
||||
searchMode: 'by_nodes',
|
||||
nodeTypes: ['nodes-base.httpRequest'],
|
||||
limit: '5'
|
||||
})).rejects.toThrow('list_node_templates: Validation failed:\n • limit: limit must be a number, got string');
|
||||
});
|
||||
|
||||
it('should reject invalid string limit values', async () => {
|
||||
await expect(server.testExecuteTool('list_node_templates', {
|
||||
nodeTypes: ['nodes-base.httpRequest'],
|
||||
limit: 'invalid'
|
||||
})).rejects.toThrow('list_node_templates: Validation failed:\n • limit: limit must be a number, got string');
|
||||
limit: 5
|
||||
})).resolves.toEqual({ templates: [] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -399,24 +410,11 @@ describe('Parameter Validation', () => {
|
||||
expect(result).toEqual({ docs: 'test' });
|
||||
});
|
||||
|
||||
it('should allow list_nodes with no parameters', async () => {
|
||||
const result = await server.testExecuteTool('list_nodes', {});
|
||||
expect(result).toEqual({ nodes: [] });
|
||||
});
|
||||
|
||||
it('should allow list_ai_tools with no parameters', async () => {
|
||||
const result = await server.testExecuteTool('list_ai_tools', {});
|
||||
expect(result).toEqual({ tools: [] });
|
||||
});
|
||||
|
||||
it('should allow get_database_statistics with no parameters', async () => {
|
||||
const result = await server.testExecuteTool('get_database_statistics', {});
|
||||
expect(result).toEqual({ stats: {} });
|
||||
});
|
||||
|
||||
it('should allow list_tasks with no parameters', async () => {
|
||||
const result = await server.testExecuteTool('list_tasks', {});
|
||||
expect(result).toEqual({ tasks: [] });
|
||||
it('should allow tools_documentation with no parameters', async () => {
|
||||
const result = await server.testExecuteTool('tools_documentation', {});
|
||||
expect(result).toBeDefined();
|
||||
// tools_documentation returns an object with documentation content
|
||||
expect(typeof result).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -429,8 +427,8 @@ describe('Parameter Validation', () => {
|
||||
|
||||
it('should list all missing parameters', () => {
|
||||
expect(() => {
|
||||
server.testValidateToolParams('validate_node_operation', { profile: 'strict' }, ['nodeType', 'config']);
|
||||
}).toThrow('validate_node_operation: Validation failed:\n • nodeType: nodeType is required\n • config: config is required');
|
||||
server.testValidateToolParams('validate_node', { profile: 'strict' }, ['nodeType', 'config']);
|
||||
}).toThrow('validate_node: Validation failed:\n • nodeType: nodeType is required\n • config: config is required');
|
||||
});
|
||||
|
||||
it('should include helpful guidance', () => {
|
||||
@@ -455,8 +453,8 @@ describe('Parameter Validation', () => {
|
||||
await expect(server.testExecuteTool('search_nodes', {}))
|
||||
.rejects.toThrow('search_nodes: Validation failed:\n • query: query is required');
|
||||
|
||||
await expect(server.testExecuteTool('validate_node_operation', { nodeType: 'test' }))
|
||||
.rejects.toThrow('validate_node_operation: Validation failed:\n • config: config is required');
|
||||
await expect(server.testExecuteTool('validate_node', { nodeType: 'test' }))
|
||||
.rejects.toThrow('validate_node: Validation failed:\n • config: config is required');
|
||||
});
|
||||
|
||||
it('should handle edge cases in parameter validation gracefully', async () => {
|
||||
@@ -473,11 +471,11 @@ describe('Parameter Validation', () => {
|
||||
// Tools using legacy validation
|
||||
const legacyValidationTools = [
|
||||
{ name: 'get_node', args: {}, expected: 'Missing required parameters for get_node: nodeType' },
|
||||
{ name: 'get_node_documentation', args: {}, expected: 'Missing required parameters for get_node_documentation: nodeType' },
|
||||
{ name: 'search_node_properties', args: {}, expected: 'Missing required parameters for search_node_properties: nodeType, query' },
|
||||
// v2.26.0: get_node_documentation consolidated into get_node with mode='docs'
|
||||
// v2.26.0: search_node_properties consolidated into get_node with mode='search_properties'
|
||||
// Note: get_node_for_task removed in v2.15.0
|
||||
{ name: 'get_property_dependencies', args: {}, expected: 'Missing required parameters for get_property_dependencies: nodeType' },
|
||||
{ name: 'get_node_as_tool_info', args: {}, expected: 'Missing required parameters for get_node_as_tool_info: nodeType' },
|
||||
// Note: get_node_as_tool_info removed in v2.25.0
|
||||
// v2.26.0: get_property_dependencies removed (low usage)
|
||||
{ name: 'get_template', args: {}, expected: 'Missing required parameters for get_template: templateId' },
|
||||
];
|
||||
|
||||
@@ -487,11 +485,11 @@ describe('Parameter Validation', () => {
|
||||
}
|
||||
|
||||
// Tools using new schema validation
|
||||
// Updated for v2.26.0 tool consolidation
|
||||
const schemaValidationTools = [
|
||||
{ name: 'search_nodes', args: {}, expected: 'search_nodes: Validation failed:\n • query: query is required' },
|
||||
{ name: 'validate_node_operation', args: {}, expected: 'validate_node_operation: Validation failed:\n • nodeType: nodeType is required\n • config: config is required' },
|
||||
{ name: 'validate_node_minimal', args: {}, expected: 'validate_node_minimal: Validation failed:\n • nodeType: nodeType is required\n • config: config is required' },
|
||||
{ name: 'list_node_templates', args: {}, expected: 'list_node_templates: Validation failed:\n • nodeTypes: nodeTypes is required' },
|
||||
{ name: 'validate_node', args: {}, expected: 'validate_node: Validation failed:\n • nodeType: nodeType is required\n • config: config is required' },
|
||||
// list_node_templates consolidated into search_templates with searchMode='by_nodes'
|
||||
];
|
||||
|
||||
for (const tool of schemaValidationTools) {
|
||||
@@ -526,17 +524,15 @@ describe('Parameter Validation', () => {
|
||||
handleUpdatePartialWorkflow: vi.fn().mockResolvedValue({ success: true })
|
||||
}));
|
||||
|
||||
// Updated for v2.26.0 tool consolidation:
|
||||
// - n8n_get_workflow now supports mode parameter (full, details, structure, minimal)
|
||||
// - n8n_executions now handles get/list/delete via action parameter
|
||||
const n8nToolsWithRequiredParams = [
|
||||
{ name: 'n8n_create_workflow', args: {}, expected: 'n8n_create_workflow: Validation failed:\n • name: name is required\n • nodes: nodes is required\n • connections: connections is required' },
|
||||
{ name: 'n8n_get_workflow', args: {}, expected: 'n8n_get_workflow: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_get_workflow_details', args: {}, expected: 'n8n_get_workflow_details: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_get_workflow_structure', args: {}, expected: 'n8n_get_workflow_structure: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_get_workflow_minimal', args: {}, expected: 'n8n_get_workflow_minimal: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_update_full_workflow', args: {}, expected: 'n8n_update_full_workflow: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_delete_workflow', args: {}, expected: 'n8n_delete_workflow: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_validate_workflow', args: {}, expected: 'n8n_validate_workflow: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_get_execution', args: {}, expected: 'n8n_get_execution: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_delete_execution', args: {}, expected: 'n8n_delete_execution: Validation failed:\n • id: id is required' },
|
||||
];
|
||||
|
||||
// n8n_update_partial_workflow and n8n_trigger_webhook_workflow use legacy validation
|
||||
|
||||
@@ -49,7 +49,7 @@ vi.mock('@/mcp/tool-docs', () => ({
|
||||
performance: 'Instant - uses in-memory index',
|
||||
bestPractices: ['Start with single words', 'Use FUZZY for uncertain names'],
|
||||
pitfalls: ['Overly specific queries may return no results'],
|
||||
relatedTools: ['list_nodes', 'get_node_info']
|
||||
relatedTools: ['get_node', 'get_node_documentation']
|
||||
}
|
||||
},
|
||||
validate_workflow: {
|
||||
@@ -81,7 +81,7 @@ vi.mock('@/mcp/tool-docs', () => ({
|
||||
performance: 'Depends on workflow complexity',
|
||||
bestPractices: ['Validate before saving', 'Fix errors first'],
|
||||
pitfalls: ['Large workflows may take time'],
|
||||
relatedTools: ['validate_node_operation']
|
||||
relatedTools: ['validate_node']
|
||||
}
|
||||
},
|
||||
get_node_essentials: {
|
||||
@@ -172,7 +172,7 @@ describe('tools-documentation', () => {
|
||||
expect(doc).toContain('## Common Pitfalls');
|
||||
expect(doc).toContain('- Overly specific queries');
|
||||
expect(doc).toContain('## Related Tools');
|
||||
expect(doc).toContain('- list_nodes');
|
||||
expect(doc).toContain('- get_node');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -78,31 +78,6 @@ describe('n8nDocumentationToolsFinal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('list_nodes', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'list_nodes');
|
||||
|
||||
it('should exist', () => {
|
||||
expect(tool).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have correct schema properties', () => {
|
||||
const properties = tool?.inputSchema.properties;
|
||||
expect(properties).toHaveProperty('package');
|
||||
expect(properties).toHaveProperty('category');
|
||||
expect(properties).toHaveProperty('developmentStyle');
|
||||
expect(properties).toHaveProperty('isAITool');
|
||||
expect(properties).toHaveProperty('limit');
|
||||
});
|
||||
|
||||
it('should have correct defaults', () => {
|
||||
expect(tool?.inputSchema.properties.limit.default).toBe(50);
|
||||
});
|
||||
|
||||
it('should have proper enum values', () => {
|
||||
expect(tool?.inputSchema.properties.developmentStyle.enum).toEqual(['declarative', 'programmatic']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_node', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_node');
|
||||
|
||||
@@ -166,18 +141,23 @@ describe('n8nDocumentationToolsFinal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_templates_for_task', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_templates_for_task');
|
||||
describe('search_templates (consolidated)', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_templates');
|
||||
|
||||
it('should exist', () => {
|
||||
expect(tool).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have task as required parameter', () => {
|
||||
expect(tool?.inputSchema.required).toContain('task');
|
||||
it('should have searchMode parameter with correct enum values', () => {
|
||||
const searchModeParam = tool?.inputSchema.properties?.searchMode;
|
||||
expect(searchModeParam).toBeDefined();
|
||||
expect(searchModeParam.enum).toEqual(['keyword', 'by_nodes', 'by_task', 'by_metadata']);
|
||||
expect(searchModeParam.default).toBe('keyword');
|
||||
});
|
||||
|
||||
it('should have correct task enum values', () => {
|
||||
it('should have task parameter for by_task searchMode', () => {
|
||||
const taskParam = tool?.inputSchema.properties?.task;
|
||||
expect(taskParam).toBeDefined();
|
||||
const expectedTasks = [
|
||||
'ai_automation',
|
||||
'data_sync',
|
||||
@@ -190,31 +170,37 @@ describe('n8nDocumentationToolsFinal', () => {
|
||||
'api_integration',
|
||||
'database_operations'
|
||||
];
|
||||
expect(tool?.inputSchema.properties.task.enum).toEqual(expectedTasks);
|
||||
expect(taskParam.enum).toEqual(expectedTasks);
|
||||
});
|
||||
|
||||
it('should have nodeTypes parameter for by_nodes searchMode', () => {
|
||||
const nodeTypesParam = tool?.inputSchema.properties?.nodeTypes;
|
||||
expect(nodeTypesParam).toBeDefined();
|
||||
expect(nodeTypesParam.type).toBe('array');
|
||||
expect(nodeTypesParam.items.type).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tool Description Quality', () => {
|
||||
it('should have concise descriptions that fit in one line', () => {
|
||||
it('should have concise descriptions that fit within reasonable limits', () => {
|
||||
n8nDocumentationToolsFinal.forEach(tool => {
|
||||
// Descriptions should be informative but not overly long
|
||||
expect(tool.description.length).toBeLessThan(300);
|
||||
// Consolidated tools (v2.26.0) may have longer descriptions due to multiple modes
|
||||
// Allow up to 500 chars for tools with mode-based functionality
|
||||
expect(tool.description.length).toBeLessThan(500);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include examples or key information in descriptions', () => {
|
||||
const toolsWithExamples = [
|
||||
'list_nodes',
|
||||
'get_node',
|
||||
'search_nodes',
|
||||
'get_node_documentation'
|
||||
'search_nodes'
|
||||
];
|
||||
|
||||
toolsWithExamples.forEach(toolName => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
|
||||
// Should include either example usage, format information, or "nodes-base"
|
||||
expect(tool?.description).toMatch(/example|Example|format|Format|nodes-base|Common:/i);
|
||||
expect(tool?.description).toMatch(/example|Example|format|Format|nodes-base|Common:|mode/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -249,15 +235,16 @@ describe('n8nDocumentationToolsFinal', () => {
|
||||
|
||||
describe('Tool Categories Coverage', () => {
|
||||
it('should have tools for all major categories', () => {
|
||||
// Updated for v2.26.0 consolidated tools
|
||||
const categories = {
|
||||
discovery: ['list_nodes', 'search_nodes', 'list_ai_tools'],
|
||||
configuration: ['get_node', 'get_node_documentation'],
|
||||
validation: ['validate_node_operation', 'validate_workflow', 'validate_node_minimal'],
|
||||
templates: ['list_tasks', 'search_templates', 'list_templates', 'get_template', 'list_node_templates'], // get_node_for_task removed in v2.15.0
|
||||
discovery: ['search_nodes'],
|
||||
configuration: ['get_node'], // get_node now includes docs mode
|
||||
validation: ['validate_node', 'validate_workflow'], // consolidated validate_node
|
||||
templates: ['search_templates', 'get_template'], // search_templates now handles all search modes
|
||||
documentation: ['tools_documentation']
|
||||
};
|
||||
|
||||
Object.entries(categories).forEach(([category, expectedTools]) => {
|
||||
Object.entries(categories).forEach(([_category, expectedTools]) => {
|
||||
expectedTools.forEach(toolName => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
|
||||
expect(tool).toBeDefined();
|
||||
@@ -294,62 +281,30 @@ describe('n8nDocumentationToolsFinal', () => {
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle tools with no parameters', () => {
|
||||
const toolsWithNoParams = ['list_ai_tools', 'get_database_statistics'];
|
||||
|
||||
toolsWithNoParams.forEach(toolName => {
|
||||
it('should handle tools with optional parameters only', () => {
|
||||
// Tools where all parameters are optional
|
||||
const toolsWithOptionalParams = ['tools_documentation'];
|
||||
|
||||
toolsWithOptionalParams.forEach(toolName => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
|
||||
expect(tool).toBeDefined();
|
||||
expect(Object.keys(tool?.inputSchema.properties || {}).length).toBe(0);
|
||||
// These tools have properties but no required array or empty required array
|
||||
expect(tool?.inputSchema.required === undefined || tool?.inputSchema.required?.length === 0).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have array parameters defined correctly', () => {
|
||||
const toolsWithArrays = ['list_node_templates'];
|
||||
|
||||
toolsWithArrays.forEach(toolName => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
|
||||
const arrayParam = tool?.inputSchema.properties.nodeTypes;
|
||||
expect(arrayParam?.type).toBe('array');
|
||||
expect(arrayParam?.items).toBeDefined();
|
||||
expect(arrayParam?.items.type).toBe('string');
|
||||
});
|
||||
// search_templates now handles nodeTypes for by_nodes mode
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_templates');
|
||||
const arrayParam = tool?.inputSchema.properties?.nodeTypes;
|
||||
expect(arrayParam?.type).toBe('array');
|
||||
expect(arrayParam?.items).toBeDefined();
|
||||
expect(arrayParam?.items.type).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('New Template Tools', () => {
|
||||
describe('list_templates', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'list_templates');
|
||||
|
||||
it('should exist and be properly defined', () => {
|
||||
expect(tool).toBeDefined();
|
||||
expect(tool?.description).toContain('minimal data');
|
||||
});
|
||||
|
||||
it('should have correct parameters', () => {
|
||||
expect(tool?.inputSchema.properties).toHaveProperty('limit');
|
||||
expect(tool?.inputSchema.properties).toHaveProperty('offset');
|
||||
expect(tool?.inputSchema.properties).toHaveProperty('sortBy');
|
||||
|
||||
const limitParam = tool?.inputSchema.properties.limit;
|
||||
expect(limitParam.type).toBe('number');
|
||||
expect(limitParam.minimum).toBe(1);
|
||||
expect(limitParam.maximum).toBe(100);
|
||||
|
||||
const offsetParam = tool?.inputSchema.properties.offset;
|
||||
expect(offsetParam.type).toBe('number');
|
||||
expect(offsetParam.minimum).toBe(0);
|
||||
|
||||
const sortByParam = tool?.inputSchema.properties.sortBy;
|
||||
expect(sortByParam.enum).toEqual(['views', 'created_at', 'name']);
|
||||
});
|
||||
|
||||
it('should have no required parameters', () => {
|
||||
expect(tool?.inputSchema.required).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_template (enhanced)', () => {
|
||||
describe('Consolidated Template Tools (v2.26.0)', () => {
|
||||
describe('get_template', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_template');
|
||||
|
||||
it('should exist and support mode parameter', () => {
|
||||
@@ -370,130 +325,56 @@ describe('n8nDocumentationToolsFinal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('search_templates_by_metadata', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_templates_by_metadata');
|
||||
describe('search_templates (consolidated with searchMode)', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_templates');
|
||||
|
||||
it('should exist in the tools array', () => {
|
||||
it('should exist with searchMode parameter', () => {
|
||||
expect(tool).toBeDefined();
|
||||
expect(tool?.name).toBe('search_templates_by_metadata');
|
||||
expect(tool?.inputSchema.properties).toHaveProperty('searchMode');
|
||||
});
|
||||
|
||||
it('should have proper description', () => {
|
||||
expect(tool?.description).toContain('Search templates by AI-generated metadata');
|
||||
expect(tool?.description).toContain('category');
|
||||
expect(tool?.description).toContain('complexity');
|
||||
});
|
||||
|
||||
it('should have correct input schema structure', () => {
|
||||
expect(tool?.inputSchema.type).toBe('object');
|
||||
expect(tool?.inputSchema.properties).toBeDefined();
|
||||
expect(tool?.inputSchema.required).toBeUndefined(); // All parameters are optional
|
||||
});
|
||||
|
||||
it('should have category parameter with proper schema', () => {
|
||||
const categoryProp = tool?.inputSchema.properties?.category;
|
||||
expect(categoryProp).toBeDefined();
|
||||
expect(categoryProp.type).toBe('string');
|
||||
expect(categoryProp.description).toContain('category');
|
||||
});
|
||||
|
||||
it('should have complexity parameter with enum values', () => {
|
||||
const complexityProp = tool?.inputSchema.properties?.complexity;
|
||||
expect(complexityProp).toBeDefined();
|
||||
expect(complexityProp.enum).toEqual(['simple', 'medium', 'complex']);
|
||||
expect(complexityProp.description).toContain('complexity');
|
||||
});
|
||||
|
||||
it('should have time-based parameters with numeric constraints', () => {
|
||||
const maxTimeProp = tool?.inputSchema.properties?.maxSetupMinutes;
|
||||
const minTimeProp = tool?.inputSchema.properties?.minSetupMinutes;
|
||||
|
||||
expect(maxTimeProp).toBeDefined();
|
||||
expect(maxTimeProp.type).toBe('number');
|
||||
expect(maxTimeProp.maximum).toBe(480);
|
||||
expect(maxTimeProp.minimum).toBe(5);
|
||||
|
||||
expect(minTimeProp).toBeDefined();
|
||||
expect(minTimeProp.type).toBe('number');
|
||||
expect(minTimeProp.maximum).toBe(480);
|
||||
expect(minTimeProp.minimum).toBe(5);
|
||||
});
|
||||
|
||||
it('should have service and audience parameters', () => {
|
||||
const serviceProp = tool?.inputSchema.properties?.requiredService;
|
||||
const audienceProp = tool?.inputSchema.properties?.targetAudience;
|
||||
|
||||
expect(serviceProp).toBeDefined();
|
||||
expect(serviceProp.type).toBe('string');
|
||||
expect(serviceProp.description).toContain('service');
|
||||
|
||||
expect(audienceProp).toBeDefined();
|
||||
expect(audienceProp.type).toBe('string');
|
||||
expect(audienceProp.description).toContain('audience');
|
||||
it('should support metadata filtering via by_metadata searchMode', () => {
|
||||
// These properties are for by_metadata searchMode
|
||||
const props = tool?.inputSchema.properties;
|
||||
expect(props).toHaveProperty('category');
|
||||
expect(props).toHaveProperty('complexity');
|
||||
expect(props?.complexity?.enum).toEqual(['simple', 'medium', 'complex']);
|
||||
});
|
||||
|
||||
it('should have pagination parameters', () => {
|
||||
const limitProp = tool?.inputSchema.properties?.limit;
|
||||
const offsetProp = tool?.inputSchema.properties?.offset;
|
||||
|
||||
|
||||
expect(limitProp).toBeDefined();
|
||||
expect(limitProp.type).toBe('number');
|
||||
expect(limitProp.default).toBe(20);
|
||||
expect(limitProp.maximum).toBe(100);
|
||||
expect(limitProp.minimum).toBe(1);
|
||||
|
||||
|
||||
expect(offsetProp).toBeDefined();
|
||||
expect(offsetProp.type).toBe('number');
|
||||
expect(offsetProp.default).toBe(0);
|
||||
expect(offsetProp.minimum).toBe(0);
|
||||
});
|
||||
|
||||
it('should include all expected properties', () => {
|
||||
it('should include all search mode-specific properties', () => {
|
||||
const properties = Object.keys(tool?.inputSchema.properties || {});
|
||||
// Consolidated tool includes properties from all former tools
|
||||
const expectedProperties = [
|
||||
'category',
|
||||
'complexity',
|
||||
'maxSetupMinutes',
|
||||
'minSetupMinutes',
|
||||
'requiredService',
|
||||
'targetAudience',
|
||||
'searchMode', // New mode selector
|
||||
'query', // For keyword search
|
||||
'nodeTypes', // For by_nodes search (formerly list_node_templates)
|
||||
'task', // For by_task search (formerly get_templates_for_task)
|
||||
'category', // For by_metadata search
|
||||
'complexity',
|
||||
'limit',
|
||||
'offset'
|
||||
];
|
||||
|
||||
|
||||
expectedProperties.forEach(prop => {
|
||||
expect(properties).toContain(prop);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have appropriate additionalProperties setting', () => {
|
||||
expect(tool?.inputSchema.additionalProperties).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enhanced pagination support', () => {
|
||||
const paginatedTools = ['list_node_templates', 'search_templates', 'get_templates_for_task', 'search_templates_by_metadata'];
|
||||
|
||||
paginatedTools.forEach(toolName => {
|
||||
describe(toolName, () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
|
||||
|
||||
it('should support limit parameter', () => {
|
||||
expect(tool?.inputSchema.properties).toHaveProperty('limit');
|
||||
const limitParam = tool?.inputSchema.properties.limit;
|
||||
expect(limitParam.type).toBe('number');
|
||||
expect(limitParam.minimum).toBeGreaterThanOrEqual(1);
|
||||
expect(limitParam.maximum).toBeGreaterThanOrEqual(50);
|
||||
});
|
||||
|
||||
it('should support offset parameter', () => {
|
||||
expect(tool?.inputSchema.properties).toHaveProperty('offset');
|
||||
const offsetParam = tool?.inputSchema.properties.offset;
|
||||
expect(offsetParam.type).toBe('number');
|
||||
expect(offsetParam.minimum).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user