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:
Romuald Członkowski
2025-11-25 18:39:00 +01:00
committed by GitHub
parent 9ee4b9492f
commit ff69e4ccca
28 changed files with 925 additions and 1766 deletions

View File

@@ -1,6 +1,7 @@
--- ---
name: code-reviewer name: code-reviewer
description: Use this agent when you need to review recently written or modified code for quality, security, and maintainability. This agent should be invoked immediately after code changes are made to ensure high standards are maintained. Examples:\n\n<example>\nContext: The user has created a code-review agent that should be called after writing code.\nuser: "Please write a function that validates email addresses"\nassistant: "I'll create an email validation function for you:"\n<function implementation>\nassistant: "Now let me use the code-reviewer agent to review this code for quality and security"\n<commentary>\nSince new code was just written, the code-reviewer agent should be invoked to ensure it meets quality standards.\n</commentary>\n</example>\n\n<example>\nContext: The user has modified existing code and wants it reviewed.\nuser: "I've updated the authentication logic, can you check if it's secure?"\nassistant: "I'll use the code-reviewer agent to analyze your authentication changes for security and best practices"\n<commentary>\nThe user has made changes to security-critical code, so the code-reviewer agent is the appropriate tool to ensure the modifications are secure and well-implemented.\n</commentary>\n</example> description: Use this agent when you need to review recently written or modified code for quality, security, and maintainability. This agent should be invoked immediately after code changes are made to ensure high standards are maintained. Examples:\n\n<example>\nContext: The user has created a code-review agent that should be called after writing code.\nuser: "Please write a function that validates email addresses"\nassistant: "I'll create an email validation function for you:"\n<function implementation>\nassistant: "Now let me use the code-reviewer agent to review this code for quality and security"\n<commentary>\nSince new code was just written, the code-reviewer agent should be invoked to ensure it meets quality standards.\n</commentary>\n</example>\n\n<example>\nContext: The user has modified existing code and wants it reviewed.\nuser: "I've updated the authentication logic, can you check if it's secure?"\nassistant: "I'll use the code-reviewer agent to analyze your authentication changes for security and best practices"\n<commentary>\nThe user has made changes to security-critical code, so the code-reviewer agent is the appropriate tool to ensure the modifications are secure and well-implemented.\n</commentary>\n</example>
model: inherit
--- ---
You are a senior code reviewer with extensive experience in software engineering, security, and best practices. Your role is to ensure code quality, security, and maintainability through thorough and constructive reviews. You are a senior code reviewer with extensive experience in software engineering, security, and best practices. Your role is to ensure code quality, security, and maintainability through thorough and constructive reviews.

View File

@@ -7,6 +7,161 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [2.26.0] - 2025-01-25
### ✨ Features
**Tool Consolidation - Reduced Tool Count by 38%**
Major consolidation of MCP tools from 31 tools to 19 tools, using mode-based parameters for better AI agent ergonomics. This reduces cognitive load for AI agents while maintaining full functionality.
#### Consolidated Tools
**1. Node Tools - `get_node` Enhanced**
The `get_node` tool now supports additional modes:
- `mode='docs'`: Replaces `get_node_documentation` - returns readable docs with examples
- `mode='search_properties'`: Replaces `search_node_properties` - search within node properties
```javascript
// Old: get_node_documentation
get_node_documentation({nodeType: "nodes-base.slack"})
// New: mode='docs'
get_node({nodeType: "nodes-base.slack", mode: "docs"})
// Old: search_node_properties
search_node_properties({nodeType: "nodes-base.httpRequest", query: "auth"})
// New: mode='search_properties'
get_node({nodeType: "nodes-base.httpRequest", mode: "search_properties", propertyQuery: "auth"})
```
**2. Validation Tools - `validate_node` Unified**
Consolidated `validate_node_operation` and `validate_node_minimal` into single `validate_node`:
- `mode='full'`: Full validation (replaces `validate_node_operation`)
- `mode='minimal'`: Quick required fields check (replaces `validate_node_minimal`)
```javascript
// Old: validate_node_operation
validate_node_operation({nodeType: "nodes-base.slack", config: {...}})
// New: mode='full' (default)
validate_node({nodeType: "nodes-base.slack", config: {...}, mode: "full"})
// Old: validate_node_minimal
validate_node_minimal({nodeType: "nodes-base.slack", config: {}})
// New: mode='minimal'
validate_node({nodeType: "nodes-base.slack", config: {}, mode: "minimal"})
```
**3. Template Tools - `search_templates` Enhanced**
Consolidated `list_node_templates`, `search_templates_by_metadata`, and `get_templates_for_task`:
- `searchMode='keyword'`: Search by keywords (default, was `search_templates`)
- `searchMode='by_nodes'`: Search by node types (was `list_node_templates`)
- `searchMode='by_metadata'`: Search by AI metadata (was `search_templates_by_metadata`)
- `searchMode='by_task'`: Search by task type (was `get_templates_for_task`)
```javascript
// Old: list_node_templates
list_node_templates({nodeTypes: ["n8n-nodes-base.httpRequest"]})
// New: searchMode='by_nodes'
search_templates({searchMode: "by_nodes", nodeTypes: ["n8n-nodes-base.httpRequest"]})
// Old: get_templates_for_task
get_templates_for_task({task: "webhook_processing"})
// New: searchMode='by_task'
search_templates({searchMode: "by_task", task: "webhook_processing"})
```
**4. Workflow Getters - `n8n_get_workflow` Enhanced**
Consolidated `n8n_get_workflow_details`, `n8n_get_workflow_structure`, `n8n_get_workflow_minimal`:
- `mode='full'`: Complete workflow data (default)
- `mode='details'`: Workflow with metadata (was `n8n_get_workflow_details`)
- `mode='structure'`: Nodes and connections only (was `n8n_get_workflow_structure`)
- `mode='minimal'`: ID, name, active status (was `n8n_get_workflow_minimal`)
```javascript
// Old: n8n_get_workflow_details
n8n_get_workflow_details({id: "123"})
// New: mode='details'
n8n_get_workflow({id: "123", mode: "details"})
// Old: n8n_get_workflow_minimal
n8n_get_workflow_minimal({id: "123"})
// New: mode='minimal'
n8n_get_workflow({id: "123", mode: "minimal"})
```
**5. Execution Tools - `n8n_executions` Unified**
Consolidated `n8n_list_executions`, `n8n_get_execution`, `n8n_delete_execution`:
- `action='list'`: List executions with filters
- `action='get'`: Get single execution details
- `action='delete'`: Delete an execution
```javascript
// Old: n8n_list_executions
n8n_list_executions({workflowId: "123", status: "success"})
// New: action='list'
n8n_executions({action: "list", workflowId: "123", status: "success"})
// Old: n8n_get_execution
n8n_get_execution({id: "456"})
// New: action='get'
n8n_executions({action: "get", id: "456"})
// Old: n8n_delete_execution
n8n_delete_execution({id: "456"})
// New: action='delete'
n8n_executions({action: "delete", id: "456"})
```
### 🗑️ Removed Tools
The following tools have been removed (use consolidated equivalents):
- `get_node_documentation``get_node` with `mode='docs'`
- `search_node_properties``get_node` with `mode='search_properties'`
- `get_property_dependencies` → Removed (use `validate_node` for dependency info)
- `validate_node_operation``validate_node` with `mode='full'`
- `validate_node_minimal``validate_node` with `mode='minimal'`
- `list_node_templates``search_templates` with `searchMode='by_nodes'`
- `search_templates_by_metadata``search_templates` with `searchMode='by_metadata'`
- `get_templates_for_task``search_templates` with `searchMode='by_task'`
- `n8n_get_workflow_details``n8n_get_workflow` with `mode='details'`
- `n8n_get_workflow_structure``n8n_get_workflow` with `mode='structure'`
- `n8n_get_workflow_minimal``n8n_get_workflow` with `mode='minimal'`
- `n8n_list_executions``n8n_executions` with `action='list'`
- `n8n_get_execution``n8n_executions` with `action='get'`
- `n8n_delete_execution``n8n_executions` with `action='delete'`
### 📊 Impact
**Tool Count**: 31 → 19 tools (38% reduction)
**For AI Agents:**
- Fewer tools to choose from reduces decision complexity
- Mode-based parameters provide clear action disambiguation
- Consistent patterns across tool categories
- Backward-compatible parameter handling
**For Users:**
- Simpler tool discovery and documentation
- Consistent API design patterns
- Reduced token usage in tool descriptions
### 🔧 Technical Details
**Files Modified:**
- `src/mcp/tools.ts` - Consolidated tool definitions
- `src/mcp/tools-n8n-manager.ts` - n8n manager tool consolidation
- `src/mcp/server.ts` - Handler consolidation and mode routing
- `tests/unit/mcp/parameter-validation.test.ts` - Updated for new tool names
- `tests/integration/mcp-protocol/tool-invocation.test.ts` - Updated test cases
- `tests/integration/mcp-protocol/error-handling.test.ts` - Updated error handling tests
**Conceived by Romuald Członkowski - [AiAdvisors](https://www.aiadvisors.pl/en)**
## [2.24.1] - 2025-01-24 ## [2.24.1] - 2025-01-24
### ✨ Features ### ✨ Features

276
README.md
View File

@@ -36,11 +36,44 @@ AI results can be unpredictable. Protect your work!
## 🚀 Quick Start ## 🚀 Quick Start
Get n8n-MCP running in 5 minutes: Get n8n-MCP running in minutes:
[![n8n-mcp Video Quickstart Guide](./thumbnail.png)](https://youtu.be/5CccjiLLyaY?si=Z62SBGlw9G34IQnQ&t=343) [![n8n-mcp Video Quickstart Guide](./thumbnail.png)](https://youtu.be/5CccjiLLyaY?si=Z62SBGlw9G34IQnQ&t=343)
### Option 1: npx (Fastest - No Installation!) 🚀 ### Option 1: Hosted Service (Easiest - No Setup!) ☁️
**The fastest way to try n8n-MCP** - no installation, no configuration:
👉 **[dashboard.n8n-mcp.com](https://dashboard.n8n-mcp.com)**
-**Free tier**: 100 tool calls/day
-**Instant access**: Start building workflows immediately
-**Always up-to-date**: Latest n8n nodes and templates
-**No infrastructure**: We handle everything
Just sign up, get your API key, and add to Claude Desktop:
```json
{
"mcpServers": {
"n8n-mcp": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-remote@latest", "https://mcp.n8n-mcp.com/sse"],
"env": {
"API_KEY": "your-api-key-from-dashboard"
}
}
}
}
```
---
## 🏠 Self-Hosting Options
Prefer to run n8n-MCP yourself? Choose your deployment method:
### Option A: npx (Quick Local Setup) 🚀
**Prerequisites:** [Node.js](https://nodejs.org/) installed on your system **Prerequisites:** [Node.js](https://nodejs.org/) installed on your system
@@ -98,7 +131,7 @@ Add to Claude Desktop config:
**Restart Claude Desktop after updating configuration** - That's it! 🎉 **Restart Claude Desktop after updating configuration** - That's it! 🎉
### Option 2: Docker (Easy & Isolated) 🐳 ### Option B: Docker (Isolated & Reproducible) 🐳
**Prerequisites:** Docker installed on your system **Prerequisites:** Docker installed on your system
@@ -345,27 +378,6 @@ environment:
SQLJS_SAVE_INTERVAL_MS: "10000" SQLJS_SAVE_INTERVAL_MS: "10000"
``` ```
### Memory Leak Fix (v2.20.2)
**Issue #330** identified a critical memory leak in long-running Docker/Kubernetes deployments:
- **Before:** 100 MB → 2.2 GB over 72 hours (OOM kills)
- **After:** Stable at 100-200 MB indefinitely
**Fixes Applied:**
- ✅ Docker images now use better-sqlite3 by default (eliminates leak entirely)
- ✅ sql.js fallback optimized (98% reduction in save frequency)
- ✅ Removed unnecessary memory allocations (50% reduction per save)
- ✅ Configurable save interval via `SQLJS_SAVE_INTERVAL_MS`
For Kubernetes deployments with memory limits:
```yaml
resources:
requests:
memory: 256Mi
limits:
memory: 512Mi
```
## 💖 Support This Project ## 💖 Support This Project
<div align="center"> <div align="center">
@@ -386,7 +398,7 @@ Every sponsorship directly translates to hours invested in making n8n-mcp better
--- ---
### Option 3: Local Installation (For Development) ### Option C: Local Installation (For Development)
**Prerequisites:** [Node.js](https://nodejs.org/) installed on your system **Prerequisites:** [Node.js](https://nodejs.org/) installed on your system
@@ -444,7 +456,7 @@ Add to Claude Desktop config:
> 💡 Tip: If youre running n8n locally on the same machine (e.g., via Docker), use http://host.docker.internal:5678 as the N8N_API_URL. > 💡 Tip: If youre running n8n locally on the same machine (e.g., via Docker), use http://host.docker.internal:5678 as the N8N_API_URL.
### Option 4: Railway Cloud Deployment (One-Click Deploy) ☁️ ### Option D: Railway Cloud Deployment (One-Click Deploy) ☁️
**Prerequisites:** Railway account (free tier available) **Prerequisites:** Railway account (free tier available)
@@ -524,7 +536,7 @@ You are an expert in n8n automation software using n8n-MCP tools. Your role is t
CRITICAL: Execute tools without commentary. Only respond AFTER all tools complete. CRITICAL: Execute tools without commentary. Only respond AFTER all tools complete.
❌ BAD: "Let me search for Slack nodes... Great! Now let me get details..." ❌ BAD: "Let me search for Slack nodes... Great! Now let me get details..."
✅ GOOD: [Execute search_nodes and get_node_essentials in parallel, then respond] ✅ GOOD: [Execute search_nodes and get_node in parallel, then respond]
### 2. Parallel Execution ### 2. Parallel Execution
When operations are independent, execute them in parallel for maximum performance. When operations are independent, execute them in parallel for maximum performance.
@@ -536,7 +548,7 @@ When operations are independent, execute them in parallel for maximum performanc
ALWAYS check templates before building from scratch (2,709 available). ALWAYS check templates before building from scratch (2,709 available).
### 4. Multi-Level Validation ### 4. Multi-Level Validation
Use validate_node_minimal → validate_node_operation → validate_workflow pattern. Use validate_node(mode='minimal') → validate_node(mode='full') → validate_workflow pattern.
### 5. Never Trust Defaults ### 5. Never Trust Defaults
⚠️ CRITICAL: Default parameter values are the #1 source of runtime failures. ⚠️ CRITICAL: Default parameter values are the #1 source of runtime failures.
@@ -547,10 +559,10 @@ ALWAYS explicitly configure ALL parameters that control node behavior.
1. **Start**: Call `tools_documentation()` for best practices 1. **Start**: Call `tools_documentation()` for best practices
2. **Template Discovery Phase** (FIRST - parallel when searching multiple) 2. **Template Discovery Phase** (FIRST - parallel when searching multiple)
- `search_templates_by_metadata({complexity: "simple"})` - Smart filtering - `search_templates({searchMode: 'by_metadata', complexity: 'simple'})` - Smart filtering
- `get_templates_for_task('webhook_processing')` - Curated by task - `search_templates({searchMode: 'by_task', task: 'webhook_processing'})` - Curated by task
- `search_templates('slack notification')` - Text search - `search_templates({query: 'slack notification'})` - Text search (default searchMode='keyword')
- `list_node_templates(['n8n-nodes-base.slack'])` - By node type - `search_templates({searchMode: 'by_nodes', nodeTypes: ['n8n-nodes-base.slack']})` - By node type
**Filtering strategies**: **Filtering strategies**:
- Beginners: `complexity: "simple"` + `maxSetupMinutes: 30` - Beginners: `complexity: "simple"` + `maxSetupMinutes: 30`
@@ -561,20 +573,20 @@ ALWAYS explicitly configure ALL parameters that control node behavior.
3. **Node Discovery** (if no suitable template - parallel execution) 3. **Node Discovery** (if no suitable template - parallel execution)
- Think deeply about requirements. Ask clarifying questions if unclear. - Think deeply about requirements. Ask clarifying questions if unclear.
- `search_nodes({query: 'keyword', includeExamples: true})` - Parallel for multiple nodes - `search_nodes({query: 'keyword', includeExamples: true})` - Parallel for multiple nodes
- `list_nodes({category: 'trigger'})` - Browse by category - `search_nodes({query: 'trigger'})` - Browse triggers
- `list_ai_tools()` - AI-capable nodes - `search_nodes({query: 'AI agent langchain'})` - AI-capable nodes
4. **Configuration Phase** (parallel for multiple nodes) 4. **Configuration Phase** (parallel for multiple nodes)
- `get_node(nodeType, {detail: 'standard', includeExamples: true})` - Essential properties (default) - `get_node({nodeType, detail: 'standard', includeExamples: true})` - Essential properties (default)
- `get_node(nodeType, {detail: 'minimal'})` - Basic metadata only (~200 tokens) - `get_node({nodeType, detail: 'minimal'})` - Basic metadata only (~200 tokens)
- `get_node(nodeType, {detail: 'full'})` - Complete information (~3000-8000 tokens) - `get_node({nodeType, detail: 'full'})` - Complete information (~3000-8000 tokens)
- `search_node_properties(nodeType, 'auth')` - Find specific properties - `get_node({nodeType, mode: 'search_properties', propertyQuery: 'auth'})` - Find specific properties
- `get_node_documentation(nodeType)` - Human-readable docs - `get_node({nodeType, mode: 'docs'})` - Human-readable markdown documentation
- Show workflow architecture to user for approval before proceeding - Show workflow architecture to user for approval before proceeding
5. **Validation Phase** (parallel for multiple nodes) 5. **Validation Phase** (parallel for multiple nodes)
- `validate_node_minimal(nodeType, config)` - Quick required fields check - `validate_node({nodeType, config, mode: 'minimal'})` - Quick required fields check
- `validate_node_operation(nodeType, config, 'runtime')` - Full validation with fixes - `validate_node({nodeType, config, mode: 'full', profile: 'runtime'})` - Full validation with fixes
- Fix ALL errors before proceeding - Fix ALL errors before proceeding
6. **Building Phase** 6. **Building Phase**
@@ -614,15 +626,15 @@ Default values cause runtime failures. Example:
### ⚠️ Example Availability ### ⚠️ Example Availability
`includeExamples: true` returns real configurations from workflow templates. `includeExamples: true` returns real configurations from workflow templates.
- Coverage varies by node popularity - Coverage varies by node popularity
- When no examples available, use `get_node` + `validate_node_minimal` - When no examples available, use `get_node` + `validate_node({mode: 'minimal'})`
## Validation Strategy ## Validation Strategy
### Level 1 - Quick Check (before building) ### Level 1 - Quick Check (before building)
`validate_node_minimal(nodeType, config)` - Required fields only (<100ms) `validate_node({nodeType, config, mode: 'minimal'})` - Required fields only (<100ms)
### Level 2 - Comprehensive (before building) ### Level 2 - Comprehensive (before building)
`validate_node_operation(nodeType, config, 'runtime')` - Full validation with fixes `validate_node({nodeType, config, mode: 'full', profile: 'runtime'})` - Full validation with fixes
### Level 3 - Complete (after building) ### Level 3 - Complete (after building)
`validate_workflow(workflow)` - Connections, expressions, AI tools `validate_workflow(workflow)` - Connections, expressions, AI tools
@@ -630,7 +642,7 @@ Default values cause runtime failures. Example:
### Level 4 - Post-Deployment ### Level 4 - Post-Deployment
1. `n8n_validate_workflow({id})` - Validate deployed workflow 1. `n8n_validate_workflow({id})` - Validate deployed workflow
2. `n8n_autofix_workflow({id})` - Auto-fix common errors 2. `n8n_autofix_workflow({id})` - Auto-fix common errors
3. `n8n_list_executions()` - Monitor execution status 3. `n8n_executions({action: 'list'})` - Monitor execution status
## Response Format ## Response Format
@@ -776,12 +788,13 @@ Use the same four-parameter format:
``` ```
// STEP 1: Template Discovery (parallel execution) // STEP 1: Template Discovery (parallel execution)
[Silent execution] [Silent execution]
search_templates_by_metadata({ search_templates({
searchMode: 'by_metadata',
requiredService: 'slack', requiredService: 'slack',
complexity: 'simple', complexity: 'simple',
targetAudience: 'marketers' targetAudience: 'marketers'
}) })
get_templates_for_task('slack_integration') search_templates({searchMode: 'by_task', task: 'slack_integration'})
// STEP 2: Use template // STEP 2: Use template
get_template(templateId, {mode: 'full'}) get_template(templateId, {mode: 'full'})
@@ -800,17 +813,17 @@ Validation: ✅ All checks passed"
// STEP 1: Discovery (parallel execution) // STEP 1: Discovery (parallel execution)
[Silent execution] [Silent execution]
search_nodes({query: 'slack', includeExamples: true}) search_nodes({query: 'slack', includeExamples: true})
list_nodes({category: 'communication'}) search_nodes({query: 'communication trigger'})
// STEP 2: Configuration (parallel execution) // STEP 2: Configuration (parallel execution)
[Silent execution] [Silent execution]
get_node('n8n-nodes-base.slack', {detail: 'standard', includeExamples: true}) get_node({nodeType: 'n8n-nodes-base.slack', detail: 'standard', includeExamples: true})
get_node('n8n-nodes-base.webhook', {detail: 'standard', includeExamples: true}) get_node({nodeType: 'n8n-nodes-base.webhook', detail: 'standard', includeExamples: true})
// STEP 3: Validation (parallel execution) // STEP 3: Validation (parallel execution)
[Silent execution] [Silent execution]
validate_node_minimal('n8n-nodes-base.slack', config) validate_node({nodeType: 'n8n-nodes-base.slack', config, mode: 'minimal'})
validate_node_operation('n8n-nodes-base.slack', fullConfig, 'runtime') validate_node({nodeType: 'n8n-nodes-base.slack', config: fullConfig, mode: 'full', profile: 'runtime'})
// STEP 4: Build // STEP 4: Build
// Construct workflow with validated configs // Construct workflow with validated configs
@@ -936,73 +949,52 @@ When Claude, Anthropic's AI assistant, tested n8n-MCP, the results were transfor
Once connected, Claude can use these powerful tools: Once connected, Claude can use these powerful tools:
### Core Tools ### Core Tools (7 tools)
- **`tools_documentation`** - Get documentation for any MCP tool (START HERE!) - **`tools_documentation`** - Get documentation for any MCP tool (START HERE!)
- **`list_nodes`** - List all n8n nodes with filtering options - **`search_nodes`** - Full-text search across all nodes. Use `includeExamples: true` for real-world configurations
- **`get_node`** - Unified node information tool with multiple detail levels: - **`get_node`** - Unified node information tool with multiple modes (v2.26.0):
- `detail: 'minimal'` - Basic metadata only (~200 tokens) - **Info mode** (default): `detail: 'minimal'|'standard'|'full'`, `includeExamples: true`
- `detail: 'standard'` - Essential properties (default, ~1000-2000 tokens) - **Docs mode**: `mode: 'docs'` - Human-readable markdown documentation
- `detail: 'full'` - Complete information (~3000-8000 tokens) - **Property search**: `mode: 'search_properties'`, `propertyQuery: 'auth'`
- `includeExamples: true` - Include real-world configurations from popular templates - **Versions**: `mode: 'versions'|'compare'|'breaking'|'migrations'`
- `mode: 'versions'` - View version history and breaking changes - **`validate_node`** - Unified node validation (v2.26.0):
- `mode: 'compare'` - Compare two versions with property-level changes - `mode: 'minimal'` - Quick required fields check (<100ms)
- `includeTypeInfo: true` - Add type structure metadata (NEW!) - `mode: 'full'` - Comprehensive validation with profiles (minimal, runtime, ai-friendly, strict)
- **`search_nodes`** - Full-text search across all node documentation. Use `includeExamples: true` to get top 2 real-world configurations per node from templates - **`validate_workflow`** - Complete workflow validation including AI Agent validation
- **`search_node_properties`** - Find specific properties within nodes - **`search_templates`** - Unified template search (v2.26.0):
- **`list_ai_tools`** - List all AI-capable nodes (ANY node can be used as AI tool!) - `searchMode: 'keyword'` (default) - Text search with `query` parameter
- **`get_node_as_tool_info`** - Get guidance on using any node as an AI tool - `searchMode: 'by_nodes'` - Find templates using specific `nodeTypes`
- `searchMode: 'by_task'` - Curated templates for common `task` types
- `searchMode: 'by_metadata'` - Filter by `complexity`, `requiredService`, `targetAudience`
- **`get_template`** - Get complete workflow JSON (modes: nodes_only, structure, full)
### Template Tools ### n8n Management Tools (12 tools - Requires API Configuration)
- **`list_templates`** - Browse all templates with descriptions and optional metadata (2,709 templates) These tools require `N8N_API_URL` and `N8N_API_KEY` in your configuration.
- **`search_templates`** - Text search across template names and descriptions
- **`search_templates_by_metadata`** - Advanced filtering by complexity, setup time, services, audience
- **`list_node_templates`** - Find templates using specific nodes
- **`get_template`** - Get complete workflow JSON for import
- **`get_templates_for_task`** - Curated templates for common automation tasks
### Validation Tools
- **`validate_workflow`** - Complete workflow validation including **AI Agent validation** (NEW in v2.17.0!)
- Detects missing language model connections
- Validates AI tool connections (no false warnings)
- Enforces streaming mode constraints
- Checks memory and output parser configurations
- **`validate_workflow_connections`** - Check workflow structure and AI tool connections
- **`validate_workflow_expressions`** - Validate n8n expressions including $fromAI()
- **`validate_node_operation`** - Validate node configurations (operation-aware, profiles support)
- **`validate_node_minimal`** - Quick validation for just required fields
### Advanced Tools
- **`get_property_dependencies`** - Analyze property visibility conditions
- **`get_node_documentation`** - Get parsed documentation from n8n-docs
- **`get_database_statistics`** - View database metrics and coverage
### n8n Management Tools (Optional - Requires API Configuration)
These powerful tools allow you to manage n8n workflows directly from Claude. They're only available when you provide `N8N_API_URL` and `N8N_API_KEY` in your configuration.
#### Workflow Management #### Workflow Management
- **`n8n_create_workflow`** - Create new workflows with nodes and connections - **`n8n_create_workflow`** - Create new workflows with nodes and connections
- **`n8n_get_workflow`** - Get complete workflow by ID - **`n8n_get_workflow`** - Unified workflow retrieval (v2.26.0):
- **`n8n_get_workflow_details`** - Get workflow with execution statistics - `mode: 'full'` (default) - Complete workflow JSON
- **`n8n_get_workflow_structure`** - Get simplified workflow structure - `mode: 'details'` - Include execution statistics
- **`n8n_get_workflow_minimal`** - Get minimal workflow info (ID, name, active status) - `mode: 'structure'` - Nodes and connections topology only
- `mode: 'minimal'` - Just ID, name, active status
- **`n8n_update_full_workflow`** - Update entire workflow (complete replacement) - **`n8n_update_full_workflow`** - Update entire workflow (complete replacement)
- **`n8n_update_partial_workflow`** - Update workflow using diff operations (NEW in v2.7.0!) - **`n8n_update_partial_workflow`** - Update workflow using diff operations
- **`n8n_delete_workflow`** - Delete workflows permanently - **`n8n_delete_workflow`** - Delete workflows permanently
- **`n8n_list_workflows`** - List workflows with filtering and pagination - **`n8n_list_workflows`** - List workflows with filtering and pagination
- **`n8n_validate_workflow`** - Validate workflows already in n8n by ID (NEW in v2.6.3) - **`n8n_validate_workflow`** - Validate workflows in n8n by ID
- **`n8n_autofix_workflow`** - Automatically fix common workflow errors (NEW in v2.13.0!) - **`n8n_autofix_workflow`** - Automatically fix common workflow errors
- **`n8n_workflow_versions`** - Manage workflow version history and rollback (NEW in v2.22.0!) - **`n8n_workflow_versions`** - Manage version history and rollback
#### Execution Management #### Execution Management
- **`n8n_trigger_webhook_workflow`** - Trigger workflows via webhook URL - **`n8n_trigger_webhook_workflow`** - Trigger workflows via webhook URL
- **`n8n_get_execution`** - Get execution details by ID - **`n8n_executions`** - Unified execution management (v2.26.0):
- **`n8n_list_executions`** - List executions with status filtering - `action: 'list'` - List executions with status filtering
- **`n8n_delete_execution`** - Delete execution records - `action: 'get'` - Get execution details by ID
- `action: 'delete'` - Delete execution records
#### System Tools #### System Tools
- **`n8n_health_check`** - Check n8n API connectivity and features - **`n8n_health_check`** - Check n8n API connectivity and features
- **`n8n_diagnostic`** - Troubleshoot management tools visibility and configuration issues
- **`n8n_list_available_tools`** - List all available management tools
### Example Usage ### Example Usage
@@ -1014,17 +1006,17 @@ get_node({
includeExamples: true // Include real-world examples from templates includeExamples: true // Include real-world examples from templates
}) })
// Minimal info for quick reference // Get documentation
get_node({ get_node({
nodeType: "nodes-base.slack", nodeType: "nodes-base.slack",
detail: "minimal" // ~200 tokens: just basic metadata mode: "docs" // Human-readable markdown documentation
}) })
// Full documentation // Search for specific properties
get_node({ get_node({
nodeType: "nodes-base.webhook", nodeType: "nodes-base.httpRequest",
detail: "full", // Complete information mode: "search_properties",
includeTypeInfo: true // Include type structure metadata propertyQuery: "authentication"
}) })
// Version history and breaking changes // Version history and breaking changes
@@ -1033,31 +1025,31 @@ get_node({
mode: "versions" // View all versions with summary mode: "versions" // View all versions with summary
}) })
// Compare versions
get_node({
nodeType: "nodes-base.slack",
mode: "compare",
fromVersion: "2.1",
toVersion: "2.2"
})
// Search nodes with configuration examples // Search nodes with configuration examples
search_nodes({ search_nodes({
query: "send email gmail", query: "send email gmail",
includeExamples: true // Returns top 2 configs per node includeExamples: true // Returns top 2 configs per node
}) })
// Validate before deployment // Validate node configuration
validate_node_operation({ validate_node({
nodeType: "nodes-base.httpRequest", nodeType: "nodes-base.httpRequest",
config: { method: "POST", url: "..." }, config: { method: "POST", url: "..." },
mode: "full",
profile: "runtime" // or "minimal", "ai-friendly", "strict" profile: "runtime" // or "minimal", "ai-friendly", "strict"
}) })
// Quick required field check // Quick required field check
validate_node_minimal({ validate_node({
nodeType: "nodes-base.slack", nodeType: "nodes-base.slack",
config: { resource: "message", operation: "send" } config: { resource: "message", operation: "send" },
mode: "minimal"
})
// Search templates by task
search_templates({
searchMode: "by_task",
task: "webhook_processing"
}) })
``` ```
@@ -1150,43 +1142,7 @@ Current database coverage (n8n v1.117.2):
## 🔄 Recent Updates ## 🔄 Recent Updates
### v2.22.19 - Critical Bug Fix See [CHANGELOG.md](./CHANGELOG.md) for complete version history and recent changes.
**Fixed:** Stack overflow in session removal (Issue #427)
- Eliminated infinite recursion in HTTP server session cleanup
- Transport resources now deleted before closing to prevent circular event handler chain
- Production logs no longer show "RangeError: Maximum call stack size exceeded"
- All session cleanup operations now complete successfully without crashes
See [CHANGELOG.md](./docs/CHANGELOG.md) for full version history and recent changes.
## ⚠️ Known Issues
### Claude Desktop Container Management
#### Container Accumulation (Fixed in v2.7.20+)
Previous versions had an issue where containers would not properly clean up when Claude Desktop sessions ended. This has been fixed in v2.7.20+ with proper signal handling.
**For best container lifecycle management:**
1. **Use the --init flag** (recommended) - Docker's init system ensures proper signal handling:
```json
{
"mcpServers": {
"n8n-mcp": {
"command": "docker",
"args": [
"run", "-i", "--rm", "--init",
"ghcr.io/czlonkowski/n8n-mcp:latest"
]
}
}
}
```
2. **Ensure you're using v2.7.20 or later** - Check your version:
```bash
docker run --rm ghcr.io/czlonkowski/n8n-mcp:latest --version
```
## 🧪 Testing ## 🧪 Testing

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.24.1", "version": "2.26.0",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -1553,7 +1553,7 @@ export async function handleHealthCheck(context?: InstanceContext): Promise<McpT
'1. Verify n8n instance is running', '1. Verify n8n instance is running',
'2. Check N8N_API_URL is correct', '2. Check N8N_API_URL is correct',
'3. Verify N8N_API_KEY has proper permissions', '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'
] ]
} }
}; };
@@ -1566,63 +1566,6 @@ export async function handleHealthCheck(context?: InstanceContext): Promise<McpT
} }
} }
export async function handleListAvailableTools(context?: InstanceContext): Promise<McpToolResponse> {
const tools = [
{
category: 'Workflow Management',
tools: [
{ name: 'n8n_create_workflow', description: 'Create new workflows' },
{ name: 'n8n_get_workflow', description: 'Get workflow by ID' },
{ name: 'n8n_get_workflow_details', description: 'Get detailed workflow info with stats' },
{ name: 'n8n_get_workflow_structure', description: 'Get simplified workflow structure' },
{ name: 'n8n_get_workflow_minimal', description: 'Get minimal workflow info' },
{ name: 'n8n_update_workflow', description: 'Update existing workflows' },
{ name: 'n8n_delete_workflow', description: 'Delete workflows' },
{ name: 'n8n_list_workflows', description: 'List workflows with filters' },
{ name: 'n8n_validate_workflow', description: 'Validate workflow from n8n instance' },
{ name: 'n8n_autofix_workflow', description: 'Automatically fix common workflow errors' }
]
},
{
category: 'Execution Management',
tools: [
{ name: 'n8n_trigger_webhook_workflow', description: 'Trigger workflows via webhook' },
{ name: 'n8n_get_execution', description: 'Get execution details' },
{ name: 'n8n_list_executions', description: 'List executions with filters' },
{ name: 'n8n_delete_execution', description: 'Delete execution records' }
]
},
{
category: 'System',
tools: [
{ name: 'n8n_health_check', description: 'Check API connectivity' },
{ name: 'n8n_list_available_tools', description: 'List all available tools' }
]
}
];
const config = getN8nApiConfig();
const apiConfigured = config !== null;
return {
success: true,
data: {
tools,
apiConfigured,
configuration: config ? {
apiUrl: config.baseUrl,
timeout: config.timeout,
maxRetries: config.maxRetries
} : null,
limitations: [
'Cannot execute workflows directly (must use webhooks)',
'Cannot stop running executions',
'Tags and credentials have limited API support'
]
}
};
}
// Environment-aware debugging helpers // Environment-aware debugging helpers
/** /**
@@ -1844,8 +1787,8 @@ export async function handleDiagnostic(request: any, context?: InstanceContext):
} }
// Check which tools are available // Check which tools are available
const documentationTools = 22; // Base documentation tools const documentationTools = 7; // Base documentation tools (after v2.26.0 consolidation)
const managementTools = apiConfigured ? 16 : 0; const managementTools = apiConfigured ? 12 : 0; // Management tools requiring API (after v2.26.0 consolidation)
const totalTools = documentationTools + managementTools; const totalTools = documentationTools + managementTools;
// Check npm version // Check npm version
@@ -1981,7 +1924,7 @@ export async function handleDiagnostic(request: any, context?: InstanceContext):
example: 'validate_workflow({workflow: {...}})' example: 'validate_workflow({workflow: {...}})'
} }
], ],
note: '22 documentation tools available without API configuration' note: '14 documentation tools available without API configuration'
}, },
whatYouCannotDo: [ whatYouCannotDo: [
'✗ Create/update workflows in n8n instance', '✗ Create/update workflows in n8n instance',
@@ -1996,8 +1939,8 @@ export async function handleDiagnostic(request: any, context?: InstanceContext):
' N8N_API_URL=https://your-n8n-instance.com', ' N8N_API_URL=https://your-n8n-instance.com',
' N8N_API_KEY=your_api_key_here', ' N8N_API_KEY=your_api_key_here',
'3. Restart the MCP server', '3. Restart the MCP server',
'4. Run n8n_diagnostic again to verify', '4. Run n8n_health_check with mode="diagnostic" to verify',
'5. All 38 tools will be available!' '5. All 19 tools will be available!'
], ],
documentation: 'https://github.com/czlonkowski/n8n-mcp?tab=readme-ov-file#n8n-management-tools-optional---requires-api-configuration' documentation: 'https://github.com/czlonkowski/n8n-mcp?tab=readme-ov-file#n8n-management-tools-optional---requires-api-configuration'
} }

View File

@@ -830,38 +830,32 @@ export class N8NDocumentationMCPServer {
let validationResult; let validationResult;
switch (toolName) { switch (toolName) {
case 'validate_node_operation': case 'validate_node':
// Consolidated tool handles both modes - validate as operation for now
validationResult = ToolValidation.validateNodeOperation(args); validationResult = ToolValidation.validateNodeOperation(args);
break; break;
case 'validate_node_minimal':
validationResult = ToolValidation.validateNodeMinimal(args);
break;
case 'validate_workflow': case 'validate_workflow':
case 'validate_workflow_connections':
case 'validate_workflow_expressions':
validationResult = ToolValidation.validateWorkflow(args); validationResult = ToolValidation.validateWorkflow(args);
break; break;
case 'search_nodes': case 'search_nodes':
validationResult = ToolValidation.validateSearchNodes(args); validationResult = ToolValidation.validateSearchNodes(args);
break; break;
case 'list_node_templates':
validationResult = ToolValidation.validateListNodeTemplates(args);
break;
case 'n8n_create_workflow': case 'n8n_create_workflow':
validationResult = ToolValidation.validateCreateWorkflow(args); validationResult = ToolValidation.validateCreateWorkflow(args);
break; break;
case 'n8n_get_workflow': case 'n8n_get_workflow':
case 'n8n_get_workflow_details':
case 'n8n_get_workflow_structure':
case 'n8n_get_workflow_minimal':
case 'n8n_update_full_workflow': case 'n8n_update_full_workflow':
case 'n8n_delete_workflow': case 'n8n_delete_workflow':
case 'n8n_validate_workflow': case 'n8n_validate_workflow':
case 'n8n_autofix_workflow': case 'n8n_autofix_workflow':
case 'n8n_get_execution':
case 'n8n_delete_execution':
validationResult = ToolValidation.validateWorkflowId(args); validationResult = ToolValidation.validateWorkflowId(args);
break; break;
case 'n8n_executions':
// Requires action parameter, id validation done in handler based on action
validationResult = args.action
? { valid: true, errors: [] }
: { valid: false, errors: [{ field: 'action', message: 'action is required' }] };
break;
default: default:
// For tools not yet migrated to schema validation, use basic validation // For tools not yet migrated to schema validation, use basic validation
return this.validateToolParamsBasic(toolName, args, legacyRequiredParams || []); return this.validateToolParamsBasic(toolName, args, legacyRequiredParams || []);
@@ -1015,25 +1009,24 @@ export class N8NDocumentationMCPServer {
case 'tools_documentation': case 'tools_documentation':
// No required parameters // No required parameters
return this.getToolsDocumentation(args.topic, args.depth); return this.getToolsDocumentation(args.topic, args.depth);
case 'list_nodes':
// No required parameters
return this.listNodes(args);
case 'search_nodes': case 'search_nodes':
this.validateToolParams(name, args, ['query']); this.validateToolParams(name, args, ['query']);
// Convert limit to number if provided, otherwise use default // Convert limit to number if provided, otherwise use default
const limit = args.limit !== undefined ? Number(args.limit) || 20 : 20; const limit = args.limit !== undefined ? Number(args.limit) || 20 : 20;
return this.searchNodes(args.query, limit, { mode: args.mode, includeExamples: args.includeExamples }); return this.searchNodes(args.query, limit, { mode: args.mode, includeExamples: args.includeExamples });
case 'list_ai_tools':
// No required parameters
return this.listAITools();
case 'get_node_documentation':
this.validateToolParams(name, args, ['nodeType']);
return this.getNodeDocumentation(args.nodeType);
case 'get_database_statistics':
// No required parameters
return this.getDatabaseStatistics();
case 'get_node': case 'get_node':
this.validateToolParams(name, args, ['nodeType']); this.validateToolParams(name, args, ['nodeType']);
// Handle consolidated modes: docs, search_properties
if (args.mode === 'docs') {
return this.getNodeDocumentation(args.nodeType);
}
if (args.mode === 'search_properties') {
if (!args.propertyQuery) {
throw new Error('propertyQuery is required for mode=search_properties');
}
const maxResults = args.maxPropertyResults !== undefined ? Number(args.maxPropertyResults) || 20 : 20;
return this.searchNodeProperties(args.nodeType, args.propertyQuery, maxResults);
}
return this.getNode( return this.getNode(
args.nodeType, args.nodeType,
args.detail, args.detail,
@@ -1043,18 +1036,23 @@ export class N8NDocumentationMCPServer {
args.fromVersion, args.fromVersion,
args.toVersion args.toVersion
); );
case 'search_node_properties': case 'validate_node':
this.validateToolParams(name, args, ['nodeType', 'query']);
const maxResults = args.maxResults !== undefined ? Number(args.maxResults) || 20 : 20;
return this.searchNodeProperties(args.nodeType, args.query, maxResults);
case 'list_tasks':
// No required parameters
return this.listTasks(args.category);
case 'validate_node_operation':
this.validateToolParams(name, args, ['nodeType', 'config']); this.validateToolParams(name, args, ['nodeType', 'config']);
// Ensure config is an object // Ensure config is an object
if (typeof args.config !== 'object' || args.config === null) { if (typeof args.config !== 'object' || args.config === null) {
logger.warn(`validate_node_operation called with invalid config type: ${typeof args.config}`); logger.warn(`validate_node called with invalid config type: ${typeof args.config}`);
const validationMode = args.mode || 'full';
if (validationMode === 'minimal') {
return {
nodeType: args.nodeType || 'unknown',
displayName: 'Unknown Node',
valid: false,
missingRequiredFields: [
'Invalid config format - expected object',
'🔧 RECOVERY: Use format { "resource": "...", "operation": "..." } or {} for empty config'
]
};
}
return { return {
nodeType: args.nodeType || 'unknown', nodeType: args.nodeType || 'unknown',
workflowNodeType: args.nodeType || 'unknown', workflowNodeType: args.nodeType || 'unknown',
@@ -1070,7 +1068,7 @@ export class N8NDocumentationMCPServer {
suggestions: [ suggestions: [
'🔧 RECOVERY: Invalid config detected. Fix with:', '🔧 RECOVERY: Invalid config detected. Fix with:',
' • Ensure config is an object: { "resource": "...", "operation": "..." }', ' • Ensure config is an object: { "resource": "...", "operation": "..." }',
' • Use get_node_essentials to see required fields for this node type', ' • Use get_node to see required fields for this node type',
' • Check if the node type is correct before configuring it' ' • Check if the node type is correct before configuring it'
], ],
summary: { summary: {
@@ -1081,95 +1079,75 @@ export class N8NDocumentationMCPServer {
} }
}; };
} }
return this.validateNodeConfig(args.nodeType, args.config, 'operation', args.profile); // Handle mode parameter
case 'validate_node_minimal': const validationMode = args.mode || 'full';
this.validateToolParams(name, args, ['nodeType', 'config']); if (validationMode === 'minimal') {
// Ensure config is an object return this.validateNodeMinimal(args.nodeType, args.config);
if (typeof args.config !== 'object' || args.config === null) {
logger.warn(`validate_node_minimal called with invalid config type: ${typeof args.config}`);
return {
nodeType: args.nodeType || 'unknown',
displayName: 'Unknown Node',
valid: false,
missingRequiredFields: [
'Invalid config format - expected object',
'🔧 RECOVERY: Use format { "resource": "...", "operation": "..." } or {} for empty config'
]
};
} }
return this.validateNodeMinimal(args.nodeType, args.config); return this.validateNodeConfig(args.nodeType, args.config, 'operation', args.profile);
case 'get_property_dependencies':
this.validateToolParams(name, args, ['nodeType']);
return this.getPropertyDependencies(args.nodeType, args.config);
case 'get_node_as_tool_info':
this.validateToolParams(name, args, ['nodeType']);
return this.getNodeAsToolInfo(args.nodeType);
case 'list_templates':
// No required params
const listLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
const listOffset = Math.max(Number(args.offset) || 0, 0);
const sortBy = args.sortBy || 'views';
const includeMetadata = Boolean(args.includeMetadata);
return this.listTemplates(listLimit, listOffset, sortBy, includeMetadata);
case 'list_node_templates':
this.validateToolParams(name, args, ['nodeTypes']);
const templateLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
const templateOffset = Math.max(Number(args.offset) || 0, 0);
return this.listNodeTemplates(args.nodeTypes, templateLimit, templateOffset);
case 'get_template': case 'get_template':
this.validateToolParams(name, args, ['templateId']); this.validateToolParams(name, args, ['templateId']);
const templateId = Number(args.templateId); const templateId = Number(args.templateId);
const mode = args.mode || 'full'; const templateMode = args.mode || 'full';
return this.getTemplate(templateId, mode); return this.getTemplate(templateId, templateMode);
case 'search_templates': case 'search_templates': {
this.validateToolParams(name, args, ['query']); // Consolidated tool with searchMode parameter
const searchMode = args.searchMode || 'keyword';
const searchLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100); const searchLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
const searchOffset = Math.max(Number(args.offset) || 0, 0); const searchOffset = Math.max(Number(args.offset) || 0, 0);
const searchFields = args.fields as string[] | undefined;
return this.searchTemplates(args.query, searchLimit, searchOffset, searchFields); switch (searchMode) {
case 'get_templates_for_task': case 'by_nodes':
this.validateToolParams(name, args, ['task']); if (!args.nodeTypes || !Array.isArray(args.nodeTypes) || args.nodeTypes.length === 0) {
const taskLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100); throw new Error('nodeTypes array is required for searchMode=by_nodes');
const taskOffset = Math.max(Number(args.offset) || 0, 0); }
return this.getTemplatesForTask(args.task, taskLimit, taskOffset); return this.listNodeTemplates(args.nodeTypes, searchLimit, searchOffset);
case 'search_templates_by_metadata': case 'by_task':
// No required params - all filters are optional if (!args.task) {
const metadataLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100); throw new Error('task is required for searchMode=by_task');
const metadataOffset = Math.max(Number(args.offset) || 0, 0); }
return this.searchTemplatesByMetadata({ return this.getTemplatesForTask(args.task, searchLimit, searchOffset);
category: args.category, case 'by_metadata':
complexity: args.complexity, return this.searchTemplatesByMetadata({
maxSetupMinutes: args.maxSetupMinutes ? Number(args.maxSetupMinutes) : undefined, category: args.category,
minSetupMinutes: args.minSetupMinutes ? Number(args.minSetupMinutes) : undefined, complexity: args.complexity,
requiredService: args.requiredService, maxSetupMinutes: args.maxSetupMinutes ? Number(args.maxSetupMinutes) : undefined,
targetAudience: args.targetAudience minSetupMinutes: args.minSetupMinutes ? Number(args.minSetupMinutes) : undefined,
}, metadataLimit, metadataOffset); requiredService: args.requiredService,
targetAudience: args.targetAudience
}, searchLimit, searchOffset);
case 'keyword':
default:
if (!args.query) {
throw new Error('query is required for searchMode=keyword');
}
const searchFields = args.fields as string[] | undefined;
return this.searchTemplates(args.query, searchLimit, searchOffset, searchFields);
}
}
case 'validate_workflow': case 'validate_workflow':
this.validateToolParams(name, args, ['workflow']); this.validateToolParams(name, args, ['workflow']);
return this.validateWorkflow(args.workflow, args.options); return this.validateWorkflow(args.workflow, args.options);
case 'validate_workflow_connections':
this.validateToolParams(name, args, ['workflow']);
return this.validateWorkflowConnections(args.workflow);
case 'validate_workflow_expressions':
this.validateToolParams(name, args, ['workflow']);
return this.validateWorkflowExpressions(args.workflow);
// n8n Management Tools (if API is configured) // n8n Management Tools (if API is configured)
case 'n8n_create_workflow': case 'n8n_create_workflow':
this.validateToolParams(name, args, ['name', 'nodes', 'connections']); this.validateToolParams(name, args, ['name', 'nodes', 'connections']);
return n8nHandlers.handleCreateWorkflow(args, this.instanceContext); return n8nHandlers.handleCreateWorkflow(args, this.instanceContext);
case 'n8n_get_workflow': case 'n8n_get_workflow': {
this.validateToolParams(name, args, ['id']); this.validateToolParams(name, args, ['id']);
return n8nHandlers.handleGetWorkflow(args, this.instanceContext); const workflowMode = args.mode || 'full';
case 'n8n_get_workflow_details': switch (workflowMode) {
this.validateToolParams(name, args, ['id']); case 'details':
return n8nHandlers.handleGetWorkflowDetails(args, this.instanceContext); return n8nHandlers.handleGetWorkflowDetails(args, this.instanceContext);
case 'n8n_get_workflow_structure': case 'structure':
this.validateToolParams(name, args, ['id']); return n8nHandlers.handleGetWorkflowStructure(args, this.instanceContext);
return n8nHandlers.handleGetWorkflowStructure(args, this.instanceContext); case 'minimal':
case 'n8n_get_workflow_minimal': return n8nHandlers.handleGetWorkflowMinimal(args, this.instanceContext);
this.validateToolParams(name, args, ['id']); case 'full':
return n8nHandlers.handleGetWorkflowMinimal(args, this.instanceContext); default:
return n8nHandlers.handleGetWorkflow(args, this.instanceContext);
}
}
case 'n8n_update_full_workflow': case 'n8n_update_full_workflow':
this.validateToolParams(name, args, ['id']); this.validateToolParams(name, args, ['id']);
return n8nHandlers.handleUpdateWorkflow(args, this.repository!, this.instanceContext); return n8nHandlers.handleUpdateWorkflow(args, this.repository!, this.instanceContext);
@@ -1195,24 +1173,32 @@ export class N8NDocumentationMCPServer {
case 'n8n_trigger_webhook_workflow': case 'n8n_trigger_webhook_workflow':
this.validateToolParams(name, args, ['webhookUrl']); this.validateToolParams(name, args, ['webhookUrl']);
return n8nHandlers.handleTriggerWebhookWorkflow(args, this.instanceContext); return n8nHandlers.handleTriggerWebhookWorkflow(args, this.instanceContext);
case 'n8n_get_execution': case 'n8n_executions': {
this.validateToolParams(name, args, ['id']); this.validateToolParams(name, args, ['action']);
return n8nHandlers.handleGetExecution(args, this.instanceContext); const execAction = args.action;
case 'n8n_list_executions': switch (execAction) {
// No required parameters case 'get':
return n8nHandlers.handleListExecutions(args, this.instanceContext); if (!args.id) {
case 'n8n_delete_execution': throw new Error('id is required for action=get');
this.validateToolParams(name, args, ['id']); }
return n8nHandlers.handleDeleteExecution(args, this.instanceContext); return n8nHandlers.handleGetExecution(args, this.instanceContext);
case 'list':
return n8nHandlers.handleListExecutions(args, this.instanceContext);
case 'delete':
if (!args.id) {
throw new Error('id is required for action=delete');
}
return n8nHandlers.handleDeleteExecution(args, this.instanceContext);
default:
throw new Error(`Unknown action: ${execAction}. Valid actions: get, list, delete`);
}
}
case 'n8n_health_check': case 'n8n_health_check':
// No required parameters // No required parameters - supports mode='status' (default) or mode='diagnostic'
if (args.mode === 'diagnostic') {
return n8nHandlers.handleDiagnostic({ params: { arguments: args } }, this.instanceContext);
}
return n8nHandlers.handleHealthCheck(this.instanceContext); return n8nHandlers.handleHealthCheck(this.instanceContext);
case 'n8n_list_available_tools':
// No required parameters
return n8nHandlers.handleListAvailableTools(this.instanceContext);
case 'n8n_diagnostic':
// No required parameters
return n8nHandlers.handleDiagnostic({ params: { arguments: args } }, this.instanceContext);
case 'n8n_workflow_versions': case 'n8n_workflow_versions':
this.validateToolParams(name, args, ['mode']); this.validateToolParams(name, args, ['mode']);
return n8nHandlers.handleWorkflowVersions(args, this.repository!, this.instanceContext); return n8nHandlers.handleWorkflowVersions(args, this.repository!, this.instanceContext);

View File

@@ -58,6 +58,6 @@ export const toolsDocumentationDoc: ToolDocumentation = {
'Not all internal functions are documented', 'Not all internal functions are documented',
'Special topics (code guides) require exact names' 'Special topics (code guides) require exact names'
], ],
relatedTools: ['n8n_list_available_tools for dynamic tool discovery', 'list_tasks for common configurations', 'get_database_statistics to verify MCP connection'] relatedTools: ['n8n_health_check for verifying API connection', 'get_node_for_task for common configurations', 'search_nodes for finding nodes']
} }
}; };

View File

@@ -10,7 +10,7 @@ export const getTemplatesForTaskDoc: ToolDocumentation = {
performance: 'Fast (<100ms) - pre-categorized results', performance: 'Fast (<100ms) - pre-categorized results',
tips: [ tips: [
'Returns hand-picked templates for specific automation tasks', 'Returns hand-picked templates for specific automation tasks',
'Use list_tasks to see all available task categories', 'Available tasks: ai_automation, data_sync, webhook_processing, email_automation, slack_integration, etc.',
'Templates are curated for quality and relevance' 'Templates are curated for quality and relevance'
] ]
}, },

View File

@@ -66,6 +66,6 @@ Requires N8N_API_URL and N8N_API_KEY environment variables to be configured.`,
'Profile affects validation time - strict is slower but more thorough', 'Profile affects validation time - strict is slower but more thorough',
'Expression validation may flag working but non-standard syntax' 'Expression validation may flag working but non-standard syntax'
], ],
relatedTools: ['validate_workflow', 'n8n_get_workflow', 'validate_workflow_expressions', 'n8n_health_check', 'n8n_autofix_workflow'] relatedTools: ['validate_workflow', 'n8n_get_workflow', 'n8n_health_check', 'n8n_autofix_workflow']
} }
}; };

View File

@@ -88,8 +88,8 @@ When working with Code nodes, always start by calling the relevant guide:
1. **Find** the node you need: 1. **Find** the node you need:
- search_nodes({query: "slack"}) - Search by keyword - search_nodes({query: "slack"}) - Search by keyword
- list_nodes({category: "communication"}) - List by category - search_nodes({query: "communication"}) - Search by category name
- list_ai_tools() - List AI-capable nodes - search_nodes({query: "AI langchain"}) - Search for AI-capable nodes
2. **Configure** the node (ALWAYS START WITH STANDARD DETAIL): 2. **Configure** the node (ALWAYS START WITH STANDARD DETAIL):
- ✅ get_node("nodes-base.slack", {detail: 'standard'}) - Get essential properties FIRST (~1-2KB, shows required fields) - ✅ get_node("nodes-base.slack", {detail: 'standard'}) - Get essential properties FIRST (~1-2KB, shows required fields)
@@ -105,9 +105,7 @@ When working with Code nodes, always start by calling the relevant guide:
## Tool Categories ## Tool Categories
**Discovery Tools** **Discovery Tools**
- search_nodes - Full-text search across all nodes - search_nodes - Full-text search across all nodes (supports OR, AND, FUZZY modes)
- list_nodes - List nodes with filtering by category, package, or type
- list_ai_tools - List all AI-capable nodes with usage guidance
**Configuration Tools** **Configuration Tools**
- get_node - ✅ Unified node information tool with progressive detail levels: - get_node - ✅ Unified node information tool with progressive detail levels:
@@ -125,10 +123,11 @@ When working with Code nodes, always start by calling the relevant guide:
- validate_workflow - Complete workflow validation including connections - validate_workflow - Complete workflow validation including connections
**Template Tools** **Template Tools**
- list_tasks - List common task templates
- get_node_for_task - Get pre-configured node for specific tasks - get_node_for_task - Get pre-configured node for specific tasks
- search_templates - Search workflow templates by keyword - search_templates - Search workflow templates by keyword
- get_template - Get complete workflow JSON by ID - get_template - Get complete workflow JSON by ID
- list_node_templates - Find templates using specific nodes
- get_templates_for_task - Get curated templates by task type
**n8n API Tools** (requires N8N_API_URL configuration) **n8n API Tools** (requires N8N_API_URL configuration)
- n8n_create_workflow - Create new workflows - n8n_create_workflow - Create new workflows
@@ -137,7 +136,7 @@ When working with Code nodes, always start by calling the relevant guide:
- n8n_trigger_webhook_workflow - Trigger workflow execution - n8n_trigger_webhook_workflow - Trigger workflow execution
## Performance Characteristics ## Performance Characteristics
- Instant (<10ms): search_nodes, list_nodes, get_node (minimal/standard) - Instant (<10ms): search_nodes, get_node (minimal/standard)
- Fast (<100ms): validate_node_minimal, get_node_for_task - Fast (<100ms): validate_node_minimal, get_node_for_task
- Moderate (100-500ms): validate_workflow, get_node (full detail) - Moderate (100-500ms): validate_workflow, get_node (full detail)
- Network-dependent: All n8n_* tools - Network-dependent: All n8n_* tools

View File

@@ -70,55 +70,19 @@ export const n8nManagementTools: ToolDefinition[] = [
}, },
{ {
name: 'n8n_get_workflow', name: 'n8n_get_workflow',
description: `Get a workflow by ID. Returns the complete workflow including nodes, connections, and settings.`, description: `Get workflow by ID with different detail levels. Use mode='full' for complete workflow, 'details' for metadata+stats, 'structure' for nodes/connections only, 'minimal' for id/name/active/tags.`,
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
id: { id: {
type: 'string', type: 'string',
description: 'Workflow ID' description: 'Workflow ID'
} },
}, mode: {
required: ['id'] type: 'string',
} enum: ['full', 'details', 'structure', 'minimal'],
}, default: 'full',
{ description: 'Detail level: full=complete workflow, details=full+execution stats, structure=nodes/connections topology, minimal=metadata only'
name: 'n8n_get_workflow_details',
description: `Get workflow details with metadata, version, execution stats. More info than get_workflow.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Workflow ID'
}
},
required: ['id']
}
},
{
name: 'n8n_get_workflow_structure',
description: `Get workflow structure: nodes and connections only. No parameter details.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Workflow ID'
}
},
required: ['id']
}
},
{
name: 'n8n_get_workflow_minimal',
description: `Get minimal info: ID, name, active status, tags. Fast for listings.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Workflow ID'
} }
}, },
required: ['id'] required: ['id']
@@ -343,122 +307,87 @@ export const n8nManagementTools: ToolDefinition[] = [
} }
}, },
{ {
name: 'n8n_get_execution', name: 'n8n_executions',
description: `Get execution details with smart filtering. RECOMMENDED: Use mode='preview' first to assess data size. description: `Manage workflow executions: get details, list, or delete. Use action='get' with id for execution details, action='list' for listing executions, action='delete' to remove execution record.`,
Examples:
- {id, mode:'preview'} - Structure & counts (fast, no data)
- {id, mode:'summary'} - 2 samples per node (default)
- {id, mode:'filtered', itemsLimit:5} - 5 items per node
- {id, nodeNames:['HTTP Request']} - Specific node only
- {id, mode:'full'} - Complete data (use with caution)`,
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
action: {
type: 'string',
enum: ['get', 'list', 'delete'],
description: 'Operation: get=get execution details, list=list executions, delete=delete execution'
},
// For action='get' and action='delete'
id: { id: {
type: 'string', type: 'string',
description: 'Execution ID' description: 'Execution ID (required for action=get or action=delete)'
}, },
// For action='get' - detail level
mode: { mode: {
type: 'string', type: 'string',
enum: ['preview', 'summary', 'filtered', 'full'], enum: ['preview', 'summary', 'filtered', 'full'],
description: 'Data retrieval mode: preview=structure only, summary=2 items, filtered=custom, full=all data' description: 'For action=get: preview=structure only, summary=2 items (default), filtered=custom, full=all data'
}, },
nodeNames: { nodeNames: {
type: 'array', type: 'array',
items: { type: 'string' }, items: { type: 'string' },
description: 'Filter to specific nodes by name (for filtered mode)' description: 'For action=get with mode=filtered: filter to specific nodes by name'
}, },
itemsLimit: { itemsLimit: {
type: 'number', type: 'number',
description: 'Items per node: 0=structure only, 2=default, -1=unlimited (for filtered mode)' description: 'For action=get with mode=filtered: items per node (0=structure, 2=default, -1=unlimited)'
}, },
includeInputData: { includeInputData: {
type: 'boolean', type: 'boolean',
description: 'Include input data in addition to output (default: false)' description: 'For action=get: include input data in addition to output (default: false)'
},
// For action='list'
limit: {
type: 'number',
description: 'For action=list: number of executions to return (1-100, default: 100)'
},
cursor: {
type: 'string',
description: 'For action=list: pagination cursor from previous response'
},
workflowId: {
type: 'string',
description: 'For action=list: filter by workflow ID'
},
projectId: {
type: 'string',
description: 'For action=list: filter by project ID (enterprise feature)'
},
status: {
type: 'string',
enum: ['success', 'error', 'waiting'],
description: 'For action=list: filter by execution status'
}, },
includeData: { includeData: {
type: 'boolean', type: 'boolean',
description: 'Legacy: Include execution data. Maps to mode=summary if true (deprecated, use mode instead)' description: 'For action=list: include execution data (default: false)'
} }
}, },
required: ['id'] required: ['action']
}
},
{
name: 'n8n_list_executions',
description: `List workflow executions (returns up to limit). Check hasMore/nextCursor for pagination.`,
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of executions to return (1-100, default: 100)'
},
cursor: {
type: 'string',
description: 'Pagination cursor from previous response'
},
workflowId: {
type: 'string',
description: 'Filter by workflow ID'
},
projectId: {
type: 'string',
description: 'Filter by project ID (enterprise feature)'
},
status: {
type: 'string',
enum: ['success', 'error', 'waiting'],
description: 'Filter by execution status'
},
includeData: {
type: 'boolean',
description: 'Include execution data (default: false)'
}
}
}
},
{
name: 'n8n_delete_execution',
description: `Delete an execution record. This only removes the execution history, not any data processed.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Execution ID to delete'
}
},
required: ['id']
} }
}, },
// System Tools // System Tools
{ {
name: 'n8n_health_check', name: 'n8n_health_check',
description: `Check n8n instance health and API connectivity. Returns status and available features.`, description: `Check n8n instance health and API connectivity. Use mode='diagnostic' for detailed troubleshooting with env vars and tool status.`,
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'n8n_list_available_tools',
description: `List available n8n tools and capabilities.`,
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'n8n_diagnostic',
description: `Diagnose n8n API config. Shows tool status, API connectivity, env vars. Helps troubleshoot missing tools.`,
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
mode: {
type: 'string',
enum: ['status', 'diagnostic'],
description: 'Mode: "status" (default) for quick health check, "diagnostic" for detailed debug info including env vars and tool status',
default: 'status'
},
verbose: { verbose: {
type: 'boolean', type: 'boolean',
description: 'Include detailed debug information (default: false)' description: 'Include extra details in diagnostic mode (default: false)'
} }
} }
} }

View File

@@ -26,37 +26,6 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
}, },
}, },
}, },
{
name: 'list_nodes',
description: `List n8n nodes. Common: list_nodes({limit:200}) for all, list_nodes({category:'trigger'}) for triggers. Package: "n8n-nodes-base" or "@n8n/n8n-nodes-langchain". Categories: trigger/transform/output/input.`,
inputSchema: {
type: 'object',
properties: {
package: {
type: 'string',
description: '"n8n-nodes-base" (core) or "@n8n/n8n-nodes-langchain" (AI)',
},
category: {
type: 'string',
description: 'trigger|transform|output|input|AI',
},
developmentStyle: {
type: 'string',
enum: ['declarative', 'programmatic'],
description: 'Usually "programmatic"',
},
isAITool: {
type: 'boolean',
description: 'Filter AI-capable nodes',
},
limit: {
type: 'number',
description: 'Max results (default 50, use 200+ for all)',
default: 50,
},
},
},
},
{ {
name: 'search_nodes', name: 'search_nodes',
description: `Search n8n nodes by keyword with optional real-world examples. Pass query as string. Example: query="webhook" or query="database". Returns max 20 results. Use includeExamples=true to get top 2 template configs per node.`, description: `Search n8n nodes by keyword with optional real-world examples. Pass query as string. Example: query="webhook" or query="database". Returns max 20 results. Use includeExamples=true to get top 2 template configs per node.`,
@@ -87,39 +56,9 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
required: ['query'], required: ['query'],
}, },
}, },
{
name: 'list_ai_tools',
description: `List 263 AI-optimized nodes. Note: ANY node can be AI tool! Connect any node to AI Agent's tool port. Community nodes need N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true.`,
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_node_documentation',
description: `Get readable docs with examples/auth/patterns. Better than raw schema! 87% coverage. Format: "nodes-base.slack"`,
inputSchema: {
type: 'object',
properties: {
nodeType: {
type: 'string',
description: 'Full type with prefix: "nodes-base.slack"',
},
},
required: ['nodeType'],
},
},
{
name: 'get_database_statistics',
description: `Node stats: 525 total, 263 AI tools, 104 triggers, 87% docs coverage. Verifies MCP working.`,
inputSchema: {
type: 'object',
properties: {},
},
},
{ {
name: 'get_node', name: 'get_node',
description: `Get node info with progressive detail levels. Detail: minimal (~200 tokens), standard (~1-2K, default), full (~3-8K). Version modes: versions (history), compare (diff), breaking (changes), migrations (auto-migrate). Supports includeTypeInfo and includeExamples. Use standard for most tasks.`, description: `Get node info with progressive detail levels and multiple modes. Detail: minimal (~200 tokens), standard (~1-2K, default), full (~3-8K). Modes: info (default), docs (markdown documentation), search_properties (find properties), versions/compare/breaking/migrations (version info). Use format='docs' for readable documentation, mode='search_properties' with propertyQuery for finding specific fields.`,
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@@ -135,9 +74,9 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
}, },
mode: { mode: {
type: 'string', type: 'string',
enum: ['info', 'versions', 'compare', 'breaking', 'migrations'], enum: ['info', 'docs', 'search_properties', 'versions', 'compare', 'breaking', 'migrations'],
default: 'info', default: 'info',
description: 'Operation mode. info=node information, versions=version history, compare/breaking/migrations=version comparison', description: 'Operation mode. info=node schema, docs=readable markdown documentation, search_properties=find specific properties, versions/compare/breaking/migrations=version info',
}, },
includeTypeInfo: { includeTypeInfo: {
type: 'boolean', type: 'boolean',
@@ -157,49 +96,22 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
type: 'string', type: 'string',
description: 'Target version for compare mode (e.g., "2.0"). Defaults to latest if omitted.', description: 'Target version for compare mode (e.g., "2.0"). Defaults to latest if omitted.',
}, },
propertyQuery: {
type: 'string',
description: 'For mode=search_properties: search term to find properties (e.g., "auth", "header", "body")',
},
maxPropertyResults: {
type: 'number',
description: 'For mode=search_properties: max results (default 20)',
default: 20,
},
}, },
required: ['nodeType'], required: ['nodeType'],
}, },
}, },
{ {
name: 'search_node_properties', name: 'validate_node',
description: `Find specific properties in a node (auth, headers, body, etc). Returns paths and descriptions.`, description: `Validate n8n node configuration. Use mode='full' for comprehensive validation with errors/warnings/suggestions, mode='minimal' for quick required fields check. Example: nodeType="nodes-base.slack", config={resource:"channel",operation:"create"}`,
inputSchema: {
type: 'object',
properties: {
nodeType: {
type: 'string',
description: 'Full type with prefix',
},
query: {
type: 'string',
description: 'Property to find: "auth", "header", "body", "json"',
},
maxResults: {
type: 'number',
description: 'Max results (default 20)',
default: 20,
},
},
required: ['nodeType', 'query'],
},
},
{
name: 'list_tasks',
description: `List task templates by category: HTTP/API, Webhooks, Database, AI, Data Processing, Communication.`,
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Filter by category (optional)',
},
},
},
},
{
name: 'validate_node_operation',
description: `Validate n8n node configuration. Pass nodeType as string and config as object. Example: nodeType="nodes-base.slack", config={resource:"channel",operation:"create"}`,
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@@ -211,10 +123,16 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
type: 'object', type: 'object',
description: 'Configuration as object. For simple nodes use {}. For complex nodes include fields like {resource:"channel",operation:"create"}', description: 'Configuration as object. For simple nodes use {}. For complex nodes include fields like {resource:"channel",operation:"create"}',
}, },
mode: {
type: 'string',
enum: ['full', 'minimal'],
description: 'Validation mode. full=comprehensive validation with errors/warnings/suggestions, minimal=quick required fields check only. Default is "full"',
default: 'full',
},
profile: { profile: {
type: 'string', type: 'string',
enum: ['strict', 'runtime', 'ai-friendly', 'minimal'], enum: ['strict', 'runtime', 'ai-friendly', 'minimal'],
description: 'Profile string: "minimal", "runtime", "ai-friendly", or "strict". Default is "ai-friendly"', description: 'Profile for mode=full: "minimal", "runtime", "ai-friendly", or "strict". Default is "ai-friendly"',
default: 'ai-friendly', default: 'ai-friendly',
}, },
}, },
@@ -253,6 +171,11 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
} }
}, },
suggestions: { type: 'array', items: { type: 'string' } }, suggestions: { type: 'array', items: { type: 'string' } },
missingRequiredFields: {
type: 'array',
items: { type: 'string' },
description: 'Only present in mode=minimal'
},
summary: { summary: {
type: 'object', type: 'object',
properties: { properties: {
@@ -263,132 +186,7 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
} }
} }
}, },
required: ['nodeType', 'displayName', 'valid', 'errors', 'warnings', 'suggestions', 'summary'] required: ['nodeType', 'displayName', 'valid']
},
},
{
name: 'validate_node_minimal',
description: `Check n8n node required fields. Pass nodeType as string and config as empty object {}. Example: nodeType="nodes-base.webhook", config={}`,
inputSchema: {
type: 'object',
properties: {
nodeType: {
type: 'string',
description: 'Node type as string. Example: "nodes-base.slack"',
},
config: {
type: 'object',
description: 'Configuration object. Always pass {} for empty config',
},
},
required: ['nodeType', 'config'],
additionalProperties: false,
},
outputSchema: {
type: 'object',
properties: {
nodeType: { type: 'string' },
displayName: { type: 'string' },
valid: { type: 'boolean' },
missingRequiredFields: {
type: 'array',
items: { type: 'string' }
}
},
required: ['nodeType', 'displayName', 'valid', 'missingRequiredFields']
},
},
{
name: 'get_property_dependencies',
description: `Shows property dependencies and visibility rules. Example: sendBody=true reveals body fields. Test visibility with optional config.`,
inputSchema: {
type: 'object',
properties: {
nodeType: {
type: 'string',
description: 'The node type to analyze (e.g., "nodes-base.httpRequest")',
},
config: {
type: 'object',
description: 'Optional partial configuration to check visibility impact',
},
},
required: ['nodeType'],
},
},
{
name: 'get_node_as_tool_info',
description: `How to use ANY node as AI tool. Shows requirements, use cases, examples. Works for all nodes, not just AI-marked ones.`,
inputSchema: {
type: 'object',
properties: {
nodeType: {
type: 'string',
description: 'Full node type WITH prefix: "nodes-base.slack", "nodes-base.googleSheets", etc.',
},
},
required: ['nodeType'],
},
},
{
name: 'list_templates',
description: `List all templates with minimal data (id, name, description, views, node count). Optionally include AI-generated metadata for smart filtering.`,
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of results (1-100). Default 10.',
default: 10,
minimum: 1,
maximum: 100,
},
offset: {
type: 'number',
description: 'Pagination offset. Default 0.',
default: 0,
minimum: 0,
},
sortBy: {
type: 'string',
enum: ['views', 'created_at', 'name'],
description: 'Sort field. Default: views (popularity).',
default: 'views',
},
includeMetadata: {
type: 'boolean',
description: 'Include AI-generated metadata (categories, complexity, setup time, etc.). Default false.',
default: false,
},
},
},
},
{
name: 'list_node_templates',
description: `Find templates using specific nodes. Returns paginated results. Use FULL types: "n8n-nodes-base.httpRequest".`,
inputSchema: {
type: 'object',
properties: {
nodeTypes: {
type: 'array',
items: { type: 'string' },
description: 'Array of node types to search for (e.g., ["n8n-nodes-base.httpRequest", "n8n-nodes-base.openAi"])',
},
limit: {
type: 'number',
description: 'Maximum number of templates to return. Default 10.',
default: 10,
minimum: 1,
maximum: 100,
},
offset: {
type: 'number',
description: 'Pagination offset. Default 0.',
default: 0,
minimum: 0,
},
},
required: ['nodeTypes'],
}, },
}, },
{ {
@@ -413,13 +211,20 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
}, },
{ {
name: 'search_templates', name: 'search_templates',
description: `Search templates by name/description keywords. Returns paginated results. NOT for node types! For nodes use list_node_templates.`, description: `Search templates with multiple modes. Use searchMode='keyword' for text search, 'by_nodes' to find templates using specific nodes, 'by_task' for curated task-based templates, 'by_metadata' for filtering by complexity/setup time/services.`,
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
searchMode: {
type: 'string',
enum: ['keyword', 'by_nodes', 'by_task', 'by_metadata'],
description: 'Search mode. keyword=text search (default), by_nodes=find by node types, by_task=curated task templates, by_metadata=filter by complexity/services',
default: 'keyword',
},
// For searchMode='keyword'
query: { query: {
type: 'string', type: 'string',
description: 'Search keyword as string. Example: "chatbot"', description: 'For searchMode=keyword: search keyword (e.g., "chatbot")',
}, },
fields: { fields: {
type: 'array', type: 'array',
@@ -427,36 +232,20 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
type: 'string', type: 'string',
enum: ['id', 'name', 'description', 'author', 'nodes', 'views', 'created', 'url', 'metadata'], enum: ['id', 'name', 'description', 'author', 'nodes', 'views', 'created', 'url', 'metadata'],
}, },
description: 'Fields to include in response. Default: all fields. Example: ["id", "name"] for minimal response.', description: 'For searchMode=keyword: fields to include in response. Default: all fields.',
}, },
limit: { // For searchMode='by_nodes'
type: 'number', nodeTypes: {
description: 'Maximum number of results. Default 20.', type: 'array',
default: 20, items: { type: 'string' },
minimum: 1, description: 'For searchMode=by_nodes: array of node types (e.g., ["n8n-nodes-base.httpRequest", "n8n-nodes-base.slack"])',
maximum: 100,
}, },
offset: { // For searchMode='by_task'
type: 'number',
description: 'Pagination offset. Default 0.',
default: 0,
minimum: 0,
},
},
required: ['query'],
},
},
{
name: 'get_templates_for_task',
description: `Curated templates by task. Returns paginated results sorted by popularity.`,
inputSchema: {
type: 'object',
properties: {
task: { task: {
type: 'string', type: 'string',
enum: [ enum: [
'ai_automation', 'ai_automation',
'data_sync', 'data_sync',
'webhook_processing', 'webhook_processing',
'email_automation', 'email_automation',
'slack_integration', 'slack_integration',
@@ -466,60 +255,39 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
'api_integration', 'api_integration',
'database_operations' 'database_operations'
], ],
description: 'The type of task to get templates for', description: 'For searchMode=by_task: the type of task',
}, },
limit: { // For searchMode='by_metadata'
type: 'number',
description: 'Maximum number of results. Default 10.',
default: 10,
minimum: 1,
maximum: 100,
},
offset: {
type: 'number',
description: 'Pagination offset. Default 0.',
default: 0,
minimum: 0,
},
},
required: ['task'],
},
},
{
name: 'search_templates_by_metadata',
description: `Search templates by AI-generated metadata. Filter by category, complexity, setup time, services, or audience. Returns rich metadata for smart template discovery.`,
inputSchema: {
type: 'object',
properties: {
category: { category: {
type: 'string', type: 'string',
description: 'Filter by category (e.g., "automation", "integration", "data processing")', description: 'For searchMode=by_metadata: filter by category (e.g., "automation", "integration")',
}, },
complexity: { complexity: {
type: 'string', type: 'string',
enum: ['simple', 'medium', 'complex'], enum: ['simple', 'medium', 'complex'],
description: 'Filter by complexity level', description: 'For searchMode=by_metadata: filter by complexity level',
}, },
maxSetupMinutes: { maxSetupMinutes: {
type: 'number', type: 'number',
description: 'Maximum setup time in minutes', description: 'For searchMode=by_metadata: maximum setup time in minutes',
minimum: 5, minimum: 5,
maximum: 480, maximum: 480,
}, },
minSetupMinutes: { minSetupMinutes: {
type: 'number', type: 'number',
description: 'Minimum setup time in minutes', description: 'For searchMode=by_metadata: minimum setup time in minutes',
minimum: 5, minimum: 5,
maximum: 480, maximum: 480,
}, },
requiredService: { requiredService: {
type: 'string', type: 'string',
description: 'Filter by required service (e.g., "openai", "slack", "google")', description: 'For searchMode=by_metadata: filter by required service (e.g., "openai", "slack")',
}, },
targetAudience: { targetAudience: {
type: 'string', type: 'string',
description: 'Filter by target audience (e.g., "developers", "marketers", "analysts")', description: 'For searchMode=by_metadata: filter by target audience (e.g., "developers", "marketers")',
}, },
// Common pagination
limit: { limit: {
type: 'number', type: 'number',
description: 'Maximum number of results. Default 20.', description: 'Maximum number of results. Default 20.',
@@ -534,7 +302,6 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
minimum: 0, minimum: 0,
}, },
}, },
additionalProperties: false,
}, },
}, },
{ {
@@ -622,143 +389,43 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
required: ['valid', 'summary'] required: ['valid', 'summary']
}, },
}, },
{
name: 'validate_workflow_connections',
description: `Check workflow connections only: valid nodes, no cycles, proper triggers, AI tool links. Fast structure validation.`,
inputSchema: {
type: 'object',
properties: {
workflow: {
type: 'object',
description: 'The workflow JSON with nodes array and connections object.',
},
},
required: ['workflow'],
additionalProperties: false,
},
outputSchema: {
type: 'object',
properties: {
valid: { type: 'boolean' },
statistics: {
type: 'object',
properties: {
totalNodes: { type: 'number' },
triggerNodes: { type: 'number' },
validConnections: { type: 'number' },
invalidConnections: { type: 'number' }
}
},
errors: {
type: 'array',
items: {
type: 'object',
properties: {
node: { type: 'string' },
message: { type: 'string' }
}
}
},
warnings: {
type: 'array',
items: {
type: 'object',
properties: {
node: { type: 'string' },
message: { type: 'string' }
}
}
}
},
required: ['valid', 'statistics']
},
},
{
name: 'validate_workflow_expressions',
description: `Validate n8n expressions: syntax {{}}, variables ($json/$node), references. Returns errors with locations.`,
inputSchema: {
type: 'object',
properties: {
workflow: {
type: 'object',
description: 'The workflow JSON to check for expression errors.',
},
},
required: ['workflow'],
additionalProperties: false,
},
outputSchema: {
type: 'object',
properties: {
valid: { type: 'boolean' },
statistics: {
type: 'object',
properties: {
totalNodes: { type: 'number' },
expressionsValidated: { type: 'number' }
}
},
errors: {
type: 'array',
items: {
type: 'object',
properties: {
node: { type: 'string' },
message: { type: 'string' }
}
}
},
warnings: {
type: 'array',
items: {
type: 'object',
properties: {
node: { type: 'string' },
message: { type: 'string' }
}
}
},
tips: { type: 'array', items: { type: 'string' } }
},
required: ['valid', 'statistics']
},
},
]; ];
/** /**
* QUICK REFERENCE for AI Agents: * QUICK REFERENCE for AI Agents:
* *
* 1. RECOMMENDED WORKFLOW: * 1. RECOMMENDED WORKFLOW:
* - Start: search_nodes → get_node_essentials → get_node_for_task → validate_node_operation * - Start: search_nodes → get_node → validate_node
* - Discovery: list_nodes({category:"trigger"}) for browsing categories * - Discovery: search_nodes({query:"trigger"}) for finding nodes
* - Quick Config: get_node_essentials("nodes-base.httpRequest") - only essential properties * - Quick Config: get_node("nodes-base.httpRequest", {detail:"standard"}) - only essential properties
* - Full Details: get_node_info only when essentials aren't enough * - Documentation: get_node("nodes-base.httpRequest", {mode:"docs"}) - readable markdown docs
* - Validation: Use validate_node_operation for complex nodes (Slack, Google Sheets, etc.) * - Find Properties: get_node("nodes-base.httpRequest", {mode:"search_properties", propertyQuery:"auth"})
* * - Full Details: get_node with detail="full" only when standard isn't enough
* - Validation: Use validate_node for complex nodes (Slack, Google Sheets, etc.)
*
* 2. COMMON NODE TYPES: * 2. COMMON NODE TYPES:
* Triggers: webhook, schedule, emailReadImap, slackTrigger * Triggers: webhook, schedule, emailReadImap, slackTrigger
* Core: httpRequest, code, set, if, merge, splitInBatches * Core: httpRequest, code, set, if, merge, splitInBatches
* Integrations: slack, gmail, googleSheets, postgres, mongodb * Integrations: slack, gmail, googleSheets, postgres, mongodb
* AI: agent, openAi, chainLlm, documentLoader * AI: agent, openAi, chainLlm, documentLoader
* *
* 3. SEARCH TIPS: * 3. SEARCH TIPS:
* - search_nodes returns ANY word match (OR logic) * - search_nodes returns ANY word match (OR logic)
* - Single words more precise, multiple words broader * - Single words more precise, multiple words broader
* - If no results: use list_nodes with category filter * - If no results: try different keywords or partial names
* *
* 4. TEMPLATE SEARCHING: * 4. TEMPLATE SEARCHING:
* - search_templates("slack") searches template names/descriptions, NOT node types! * - search_templates("slack") searches template names/descriptions, NOT node types!
* - To find templates using Slack node: list_node_templates(["n8n-nodes-base.slack"]) * - To find templates using Slack node: search_templates({searchMode:"by_nodes", nodeTypes:["n8n-nodes-base.slack"]})
* - For task-based templates: get_templates_for_task("slack_integration") * - For task-based templates: search_templates({searchMode:"by_task", task:"slack_integration"})
* - 399 templates available from the last year *
*
* 5. KNOWN ISSUES: * 5. KNOWN ISSUES:
* - Some nodes have duplicate properties with different conditions * - Some nodes have duplicate properties with different conditions
* - Package names: use 'n8n-nodes-base' not '@n8n/n8n-nodes-base' * - Package names: use 'n8n-nodes-base' not '@n8n/n8n-nodes-base'
* - Check showWhen/hideWhen to identify the right property variant * - Check showWhen/hideWhen to identify the right property variant
* *
* 6. PERFORMANCE: * 6. PERFORMANCE:
* - get_node_essentials: Fast (<5KB) * - get_node (detail=standard): Fast (<5KB)
* - get_node_info: Slow (100KB+) - use sparingly * - get_node (detail=full): Slow (100KB+) - use sparingly
* - search_nodes/list_nodes: Fast, cached * - search_nodes: Fast, cached
*/ */

View File

@@ -4,72 +4,71 @@ import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
describe('Basic MCP Connection', () => { describe('Basic MCP Connection', () => {
it('should initialize MCP server', async () => { it('should initialize MCP server', async () => {
const server = new N8NDocumentationMCPServer(); const server = new N8NDocumentationMCPServer();
// Test executeTool directly - it returns raw data // Test executeTool directly - tools_documentation returns a string
const result = await server.executeTool('get_database_statistics', {}); const result = await server.executeTool('tools_documentation', {});
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(typeof result).toBe('object'); expect(typeof result).toBe('string');
expect(result.totalNodes).toBeDefined(); expect(result).toContain('n8n MCP');
expect(result.statistics).toBeDefined();
await server.shutdown(); await server.shutdown();
}); });
it('should execute list_nodes tool', async () => { it('should execute search_nodes tool', async () => {
const server = new N8NDocumentationMCPServer(); const server = new N8NDocumentationMCPServer();
// First check if we have any nodes in the database try {
const stats = await server.executeTool('get_database_statistics', {}); // Search for a common node to verify database has content
const hasNodes = stats.totalNodes > 0; const result = await server.executeTool('search_nodes', { query: 'http', limit: 5 });
expect(result).toBeDefined();
const result = await server.executeTool('list_nodes', { limit: 5 }); expect(typeof result).toBe('object');
expect(result).toBeDefined(); expect(result.results).toBeDefined();
expect(typeof result).toBe('object'); expect(Array.isArray(result.results)).toBe(true);
expect(result.nodes).toBeDefined();
expect(Array.isArray(result.nodes)).toBe(true); if (result.totalCount > 0) {
// If database has nodes, we should get results
if (hasNodes) { expect(result.results.length).toBeLessThanOrEqual(5);
// If database has nodes, we should get up to 5 expect(result.results.length).toBeGreaterThan(0);
expect(result.nodes.length).toBeLessThanOrEqual(5); expect(result.results[0]).toHaveProperty('nodeType');
expect(result.nodes.length).toBeGreaterThan(0); expect(result.results[0]).toHaveProperty('displayName');
expect(result.nodes[0]).toHaveProperty('nodeType'); }
expect(result.nodes[0]).toHaveProperty('displayName'); } catch (error: any) {
} else { // In test environment with empty database, expect appropriate error
// In test environment with empty database, we expect empty results expect(error.message).toContain('Database is empty');
expect(result.nodes).toHaveLength(0);
} }
await server.shutdown(); await server.shutdown();
}); });
it('should search nodes', async () => { it('should search nodes by keyword', async () => {
const server = new N8NDocumentationMCPServer(); const server = new N8NDocumentationMCPServer();
// First check if we have any nodes in the database try {
const stats = await server.executeTool('get_database_statistics', {}); // Search to check if database has nodes
const hasNodes = stats.totalNodes > 0; 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(); const result = await server.executeTool('search_nodes', { query: 'webhook' });
expect(typeof result).toBe('object'); expect(result).toBeDefined();
expect(result.results).toBeDefined(); expect(typeof result).toBe('object');
expect(Array.isArray(result.results)).toBe(true); expect(result.results).toBeDefined();
expect(Array.isArray(result.results)).toBe(true);
// Only expect results if the database has nodes
if (hasNodes) { // Only expect results if the database has nodes
expect(result.results.length).toBeGreaterThan(0); if (hasNodes) {
expect(result.totalCount).toBeGreaterThan(0); 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'); // Should find webhook node
expect(webhookNode).toBeDefined(); const webhookNode = result.results.find((n: any) => n.nodeType === 'nodes-base.webhook');
expect(webhookNode.displayName).toContain('Webhook'); expect(webhookNode).toBeDefined();
} else { expect(webhookNode.displayName).toContain('Webhook');
// In test environment with empty database, we expect empty results }
expect(result.results).toHaveLength(0); } catch (error: any) {
expect(result.totalCount).toBe(0); // In test environment with empty database, expect appropriate error
expect(error.message).toContain('Database is empty');
} }
await server.shutdown(); await server.shutdown();
}); });
}); });

View File

@@ -84,16 +84,16 @@ describe('MCP Error Handling', () => {
describe('Tool-Specific Errors', () => { describe('Tool-Specific Errors', () => {
describe('Node Discovery Errors', () => { describe('Node Discovery Errors', () => {
it('should handle invalid category filter', async () => { it('should handle search with no matching results', async () => {
const response = await client.callTool({ name: 'list_nodes', arguments: { const response = await client.callTool({ name: 'search_nodes', arguments: {
category: 'invalid_category' query: 'xyznonexistentnode123'
} }); } });
// Should return empty array, not error // Should return empty array, not error
const result = JSON.parse((response as any).content[0].text); const result = JSON.parse((response as any).content[0].text);
expect(result).toHaveProperty('nodes'); expect(result).toHaveProperty('results');
expect(Array.isArray(result.nodes)).toBe(true); expect(Array.isArray(result.results)).toBe(true);
expect(result.nodes).toHaveLength(0); expect(result.results).toHaveLength(0);
}); });
it('should handle invalid search mode', async () => { it('should handle invalid search mode', async () => {
@@ -135,11 +135,13 @@ describe('MCP Error Handling', () => {
}); });
describe('Validation Errors', () => { describe('Validation Errors', () => {
// v2.26.0: validate_node_operation consolidated into validate_node
it('should handle invalid validation profile', async () => { it('should handle invalid validation profile', async () => {
try { try {
await client.callTool({ name: 'validate_node_operation', arguments: { await client.callTool({ name: 'validate_node', arguments: {
nodeType: 'nodes-base.httpRequest', nodeType: 'nodes-base.httpRequest',
config: { method: 'GET', url: 'https://api.example.com' }, config: { method: 'GET', url: 'https://api.example.com' },
mode: 'full',
profile: 'invalid_profile' as any profile: 'invalid_profile' as any
} }); } });
expect.fail('Should have thrown an error'); expect.fail('Should have thrown an error');
@@ -279,9 +281,9 @@ describe('MCP Error Handling', () => {
for (let i = 0; i < requestCount; i++) { for (let i = 0; i < requestCount; i++) {
promises.push( promises.push(
client.callTool({ name: 'list_nodes', arguments: { client.callTool({ name: 'search_nodes', arguments: {
limit: 1, query: i % 2 === 0 ? 'webhook' : 'http',
category: i % 2 === 0 ? 'trigger' : 'transform' limit: 1
} }) } })
); );
} }
@@ -292,12 +294,14 @@ describe('MCP Error Handling', () => {
}); });
describe('Invalid JSON Handling', () => { describe('Invalid JSON Handling', () => {
// v2.26.0: validate_node_operation consolidated into validate_node
it('should handle invalid JSON in tool parameters', async () => { it('should handle invalid JSON in tool parameters', async () => {
try { try {
// Config should be an object, not a string // 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', 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'); expect.fail('Should have thrown an error');
} catch (error: any) { } catch (error: any) {
@@ -320,13 +324,13 @@ describe('MCP Error Handling', () => {
describe('Timeout Scenarios', () => { describe('Timeout Scenarios', () => {
it('should handle rapid sequential requests', async () => { it('should handle rapid sequential requests', async () => {
const start = Date.now(); const start = Date.now();
for (let i = 0; i < 20; i++) { 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; const duration = Date.now() - start;
// Should complete reasonably quickly (under 5 seconds) // Should complete reasonably quickly (under 5 seconds)
expect(duration).toBeLessThan(5000); expect(duration).toBeLessThan(5000);
}); });
@@ -410,25 +414,25 @@ describe('MCP Error Handling', () => {
} }
// Should still work // 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(); expect(response).toBeDefined();
}); });
it('should handle mixed success and failure', async () => { it('should handle mixed success and failure', async () => {
const promises = [ 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_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: '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); const results = await Promise.all(promises);
// Some should succeed, some should fail // Some should succeed, some should fail
const successes = results.filter(r => !('error' in r)); const successes = results.filter(r => !('error' in r));
const failures = results.filter(r => 'error' in r); const failures = results.filter(r => 'error' in r);
expect(successes.length).toBeGreaterThan(0); expect(successes.length).toBeGreaterThan(0);
expect(failures.length).toBeGreaterThan(0); expect(failures.length).toBeGreaterThan(0);
}); });
@@ -436,14 +440,14 @@ describe('MCP Error Handling', () => {
describe('Edge Cases', () => { describe('Edge Cases', () => {
it('should handle empty responses gracefully', async () => { it('should handle empty responses gracefully', async () => {
const response = await client.callTool({ name: 'list_nodes', arguments: { const response = await client.callTool({ name: 'search_nodes', arguments: {
category: 'nonexistent_category' query: 'xyznonexistentnode12345'
} }); } });
const result = JSON.parse((response as any).content[0].text); const result = JSON.parse((response as any).content[0].text);
expect(result).toHaveProperty('nodes'); expect(result).toHaveProperty('results');
expect(Array.isArray(result.nodes)).toBe(true); expect(Array.isArray(result.results)).toBe(true);
expect(result.nodes).toHaveLength(0); expect(result.results).toHaveLength(0);
}); });
it('should handle special characters in parameters', async () => { it('should handle special characters in parameters', async () => {
@@ -469,14 +473,15 @@ describe('MCP Error Handling', () => {
it('should handle null and undefined gracefully', async () => { it('should handle null and undefined gracefully', async () => {
// Most tools should handle missing optional params // 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, limit: undefined as any,
category: null as any mode: null as any
} }); } });
const result = JSON.parse((response as any).content[0].text); const result = JSON.parse((response as any).content[0].text);
expect(result).toHaveProperty('nodes'); expect(result).toHaveProperty('results');
expect(Array.isArray(result.nodes)).toBe(true); 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 () => { 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', nodeType: 'nodes-base.httpRequest',
config: { config: {
// Missing required fields // Missing required fields
method: 'INVALID_METHOD' method: 'INVALID_METHOD'
} },
mode: 'full'
} }); } });
const validation = JSON.parse((response as any).content[0].text); const validation = JSON.parse((response as any).content[0].text);

View File

@@ -23,13 +23,13 @@ describe('MCP Performance Tests', () => {
await client.connect(clientTransport); await client.connect(clientTransport);
// Verify database is populated by checking statistics // Verify database is populated by searching for a common node
const statsResponse = await client.callTool({ name: 'get_database_statistics', arguments: {} }); const searchResponse = await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 1 } });
if ((statsResponse as any).content && (statsResponse as any).content[0]) { if ((searchResponse as any).content && (searchResponse as any).content[0]) {
const stats = JSON.parse((statsResponse as any).content[0].text); const searchResult = JSON.parse((searchResponse as any).content[0].text);
// Ensure database has nodes for testing // Ensure database has nodes for testing
if (!stats.totalNodes || stats.totalNodes === 0) { if (!searchResult.totalCount || searchResult.totalCount === 0) {
console.error('Database stats:', stats); console.error('Search result:', searchResult);
throw new Error('Test database not properly populated'); throw new Error('Test database not properly populated');
} }
} }
@@ -46,13 +46,13 @@ describe('MCP Performance Tests', () => {
const start = performance.now(); const start = performance.now();
for (let i = 0; i < iterations; i++) { 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 duration = performance.now() - start;
const avgTime = duration / iterations; 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'}`); console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
// Environment-aware threshold (relaxed +20% for type safety overhead) // Environment-aware threshold (relaxed +20% for type safety overhead)
@@ -60,20 +60,20 @@ describe('MCP Performance Tests', () => {
expect(avgTime).toBeLessThan(threshold); expect(avgTime).toBeLessThan(threshold);
}); });
it('should handle list operations efficiently', async () => { it('should handle search operations efficiently', async () => {
const iterations = 50; const iterations = 50;
const start = performance.now(); const start = performance.now();
for (let i = 0; i < iterations; i++) { 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 duration = performance.now() - start;
const avgTime = duration / iterations; 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'}`); console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
// Environment-aware threshold // Environment-aware threshold
const threshold = process.env.CI ? 40 : 20; const threshold = process.env.CI ? 40 : 20;
expect(avgTime).toBeLessThan(threshold); expect(avgTime).toBeLessThan(threshold);
@@ -137,7 +137,7 @@ describe('MCP Performance Tests', () => {
const promises = []; const promises = [];
for (let i = 0; i < concurrentRequests; i++) { for (let i = 0; i < concurrentRequests; i++) {
promises.push( 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(`Average time for ${concurrentRequests} concurrent requests: ${avgTime.toFixed(2)}ms`);
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`); console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
// Concurrent requests should be more efficient than sequential // Concurrent requests should be more efficient than sequential
const threshold = process.env.CI ? 25 : 10; const threshold = process.env.CI ? 25 : 10;
expect(avgTime).toBeLessThan(threshold); expect(avgTime).toBeLessThan(threshold);
@@ -156,11 +156,11 @@ describe('MCP Performance Tests', () => {
it('should handle mixed concurrent operations', async () => { it('should handle mixed concurrent operations', async () => {
const operations = [ const operations = [
{ tool: 'list_nodes', params: { limit: 10 } }, { tool: 'search_nodes', params: { query: 'http', limit: 10 } },
{ tool: 'search_nodes', params: { query: 'http' } }, { tool: 'search_nodes', params: { query: 'webhook' } },
{ tool: 'get_database_statistics', params: {} }, { tool: 'tools_documentation', params: {} },
{ tool: 'list_ai_tools', params: {} }, { tool: 'get_node', params: { nodeType: 'nodes-base.httpRequest' } },
{ tool: 'list_tasks', params: {} } { tool: 'get_node', params: { nodeType: 'nodes-base.webhook' } }
]; ];
const rounds = 10; const rounds = 10;
@@ -186,34 +186,35 @@ describe('MCP Performance Tests', () => {
}); });
describe('Large Data Performance', () => { 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 start = performance.now();
const response = await client.callTool({ name: 'list_nodes', arguments: { const response = await client.callTool({ name: 'search_nodes', arguments: {
limit: 200 // Get many nodes query: 'n8n', // Broad query to get many results
limit: 200
} }); } });
const duration = performance.now() - start; 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 // Environment-aware threshold
const threshold = process.env.CI ? 200 : 100; const threshold = process.env.CI ? 200 : 100;
expect(duration).toBeLessThan(threshold); expect(duration).toBeLessThan(threshold);
// Check the response content // Check the response content
expect(response).toBeDefined(); expect(response).toBeDefined();
let nodes; let results;
if (response.content && Array.isArray(response.content) && response.content[0]) { if (response.content && Array.isArray(response.content) && response.content[0]) {
// MCP standard response format // MCP standard response format
expect(response.content[0].type).toBe('text'); expect(response.content[0].type).toBe('text');
expect(response.content[0].text).toBeDefined(); expect(response.content[0].text).toBeDefined();
try { try {
const parsed = JSON.parse(response.content[0].text); const parsed = JSON.parse(response.content[0].text);
// list_nodes returns an object with nodes property // search_nodes returns an object with results property
nodes = parsed.nodes || parsed; results = parsed.results || parsed;
} catch (e) { } catch (e) {
console.error('Failed to parse JSON:', e); console.error('Failed to parse JSON:', e);
console.error('Response text was:', response.content[0].text); console.error('Response text was:', response.content[0].text);
@@ -221,18 +222,18 @@ describe('MCP Performance Tests', () => {
} }
} else if (Array.isArray(response)) { } else if (Array.isArray(response)) {
// Direct array response // Direct array response
nodes = response; results = response;
} else if (response.nodes) { } else if (response.results) {
// Object with nodes property // Object with results property
nodes = response.nodes; results = response.results;
} else { } else {
console.error('Unexpected response format:', response); console.error('Unexpected response format:', response);
throw new Error('Unexpected response format'); throw new Error('Unexpected response format');
} }
expect(nodes).toBeDefined(); expect(results).toBeDefined();
expect(Array.isArray(nodes)).toBe(true); expect(Array.isArray(results)).toBe(true);
expect(nodes.length).toBeGreaterThan(100); expect(results.length).toBeGreaterThan(50);
}); });
it('should handle large workflow validation efficiently', async () => { it('should handle large workflow validation efficiently', async () => {
@@ -301,10 +302,10 @@ describe('MCP Performance Tests', () => {
for (let i = 0; i < iterations; i += batchSize) { for (let i = 0; i < iterations; i += batchSize) {
const promises = []; const promises = [];
for (let j = 0; j < batchSize; j++) { for (let j = 0; j < batchSize; j++) {
promises.push( 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 // Perform large operations
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
await client.callTool({ name: 'list_nodes', arguments: { limit: 200 } }); await client.callTool({ name: 'search_nodes', arguments: { query: 'n8n', limit: 200 } });
await client.callTool({ name: 'get_node', arguments: { await client.callTool({ name: 'get_node', arguments: {
nodeType: 'nodes-base.httpRequest' nodeType: 'nodes-base.httpRequest'
} }); } });
} }
@@ -359,16 +360,16 @@ describe('MCP Performance Tests', () => {
for (const load of loadLevels) { for (const load of loadLevels) {
const start = performance.now(); const start = performance.now();
const promises = []; const promises = [];
for (let i = 0; i < load; i++) { for (let i = 0; i < load; i++) {
promises.push( promises.push(
client.callTool({ name: 'list_nodes', arguments: { limit: 1 } }) client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 1 } })
); );
} }
await Promise.all(promises); await Promise.all(promises);
const duration = performance.now() - start; const duration = performance.now() - start;
const avgTime = duration / load; const avgTime = duration / load;
@@ -384,10 +385,10 @@ describe('MCP Performance Tests', () => {
// Average time should not increase dramatically with load // Average time should not increase dramatically with load
const firstAvg = results[0].avgTime; const firstAvg = results[0].avgTime;
const lastAvg = results[results.length - 1].avgTime; const lastAvg = results[results.length - 1].avgTime;
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`); console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
console.log(`Performance scaling - First avg: ${firstAvg.toFixed(2)}ms, Last avg: ${lastAvg.toFixed(2)}ms`); console.log(`Performance scaling - First avg: ${firstAvg.toFixed(2)}ms, Last avg: ${lastAvg.toFixed(2)}ms`);
// Environment-aware scaling factor // Environment-aware scaling factor
const scalingFactor = process.env.CI ? 3 : 2; const scalingFactor = process.env.CI ? 3 : 2;
expect(lastAvg).toBeLessThan(firstAvg * scalingFactor); expect(lastAvg).toBeLessThan(firstAvg * scalingFactor);
@@ -403,16 +404,16 @@ describe('MCP Performance Tests', () => {
const operation = i % 4; const operation = i % 4;
switch (operation) { switch (operation) {
case 0: 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; break;
case 1: case 1:
promises.push(client.callTool({ name: 'search_nodes', arguments: { query: 'test' } })); promises.push(client.callTool({ name: 'search_nodes', arguments: { query: 'test' } }));
break; break;
case 2: case 2:
promises.push(client.callTool({ name: 'get_database_statistics', arguments: {} })); promises.push(client.callTool({ name: 'tools_documentation', arguments: {} }));
break; break;
case 3: case 3:
promises.push(client.callTool({ name: 'list_ai_tools', arguments: {} })); promises.push(client.callTool({ name: 'get_node', arguments: { nodeType: 'nodes-base.set' } }));
break; break;
} }
} }
@@ -431,10 +432,10 @@ describe('MCP Performance Tests', () => {
}); });
describe('Critical Path Optimization', () => { 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 // Warm up with multiple calls to ensure everything is initialized
for (let i = 0; i < 5; i++) { 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; const iterations = 100;
@@ -442,32 +443,32 @@ describe('MCP Performance Tests', () => {
for (let i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
const start = performance.now(); 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); times.push(performance.now() - start);
} }
// Remove outliers (first few runs might be slower) // Remove outliers (first few runs might be slower)
times.sort((a, b) => a - b); times.sort((a, b) => a - b);
const trimmedTimes = times.slice(10, -10); // Remove top and bottom 10% const trimmedTimes = times.slice(10, -10); // Remove top and bottom 10%
const avgTime = trimmedTimes.reduce((a, b) => a + b, 0) / trimmedTimes.length; const avgTime = trimmedTimes.reduce((a, b) => a + b, 0) / trimmedTimes.length;
const minTime = Math.min(...trimmedTimes); const minTime = Math.min(...trimmedTimes);
const maxTime = Math.max(...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'}`); console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
// Environment-aware thresholds // Environment-aware thresholds
const threshold = process.env.CI ? 25 : 10; const threshold = process.env.CI ? 25 : 10;
expect(avgTime).toBeLessThan(threshold); expect(avgTime).toBeLessThan(threshold);
// Max should not be too much higher than average (no outliers) // Max should not be too much higher than average (no outliers)
// More lenient in CI due to resource contention // More lenient in CI due to resource contention
const maxMultiplier = process.env.CI ? 5 : 3; const maxMultiplier = process.env.CI ? 5 : 3;
expect(maxTime).toBeLessThan(avgTime * maxMultiplier); expect(maxTime).toBeLessThan(avgTime * maxMultiplier);
}); });
it('should optimize search performance', async () => { it('should handle varied search queries efficiently', async () => {
// Warm up with multiple calls // Warm up with multiple calls
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
await client.callTool({ name: 'search_nodes', arguments: { query: 'test' } }); await client.callTool({ name: 'search_nodes', arguments: { query: 'test' } });
@@ -487,7 +488,7 @@ describe('MCP Performance Tests', () => {
// Remove outliers // Remove outliers
times.sort((a, b) => a - b); times.sort((a, b) => a - b);
const trimmedTimes = times.slice(10, -10); // Remove top and bottom 10% const trimmedTimes = times.slice(10, -10); // Remove top and bottom 10%
const avgTime = trimmedTimes.reduce((a, b) => a + b, 0) / trimmedTimes.length; const avgTime = trimmedTimes.reduce((a, b) => a + b, 0) / trimmedTimes.length;
console.log(`search_nodes average performance: ${avgTime.toFixed(2)}ms`); console.log(`search_nodes average performance: ${avgTime.toFixed(2)}ms`);
@@ -542,7 +543,7 @@ describe('MCP Performance Tests', () => {
while (performance.now() - start < duration) { while (performance.now() - start < duration) {
try { try {
await client.callTool({ name: 'get_database_statistics', arguments: {} }); await client.callTool({ name: 'tools_documentation', arguments: {} });
requestCount++; requestCount++;
} catch (error) { } catch (error) {
errorCount++; errorCount++;
@@ -559,7 +560,7 @@ describe('MCP Performance Tests', () => {
// Relaxed to 75 RPS locally to account for parallel test execution overhead // Relaxed to 75 RPS locally to account for parallel test execution overhead
const rpsThreshold = process.env.CI ? 50 : 75; const rpsThreshold = process.env.CI ? 50 : 75;
expect(requestsPerSecond).toBeGreaterThan(rpsThreshold); expect(requestsPerSecond).toBeGreaterThan(rpsThreshold);
// Error rate should be very low // Error rate should be very low
expect(errorCount).toBe(0); expect(errorCount).toBe(0);
}); });
@@ -591,7 +592,7 @@ describe('MCP Performance Tests', () => {
const recoveryTimes: number[] = []; const recoveryTimes: number[] = [];
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const start = performance.now(); const start = performance.now();
await client.callTool({ name: 'get_database_statistics', arguments: {} }); await client.callTool({ name: 'tools_documentation', arguments: {} });
recoveryTimes.push(performance.now() - start); recoveryTimes.push(performance.now() - start);
} }

View File

@@ -74,7 +74,7 @@ describe('MCP Protocol Compliance', () => {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
expectedOrder.push(i); expectedOrder.push(i);
requests.push( requests.push(
client.callTool({ name: 'get_database_statistics', arguments: {} }) client.callTool({ name: 'tools_documentation', arguments: {} })
.then(() => i) .then(() => i)
); );
} }
@@ -125,7 +125,7 @@ describe('MCP Protocol Compliance', () => {
it('should handle missing params gracefully', async () => { it('should handle missing params gracefully', async () => {
// Most tools should work without params // 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(); expect(response).toBeDefined();
}); });
@@ -147,8 +147,8 @@ describe('MCP Protocol Compliance', () => {
describe('Content Types', () => { describe('Content Types', () => {
it('should handle text content in tool responses', async () => { 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).toHaveLength(1);
expect((response as any).content[0]).toHaveProperty('type', 'text'); expect((response as any).content[0]).toHaveProperty('type', 'text');
expect((response as any).content[0]).toHaveProperty('text'); expect((response as any).content[0]).toHaveProperty('text');
@@ -167,14 +167,15 @@ describe('MCP Protocol Compliance', () => {
}); });
it('should handle JSON content properly', async () => { 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 limit: 5
} }); } });
expect((response as any).content).toHaveLength(1); expect((response as any).content).toHaveLength(1);
const content = JSON.parse((response as any).content[0].text); const content = JSON.parse((response as any).content[0].text);
expect(content).toHaveProperty('nodes'); expect(content).toHaveProperty('results');
expect(Array.isArray(content.nodes)).toBe(true); expect(Array.isArray(content.results)).toBe(true);
}); });
}); });
@@ -197,10 +198,10 @@ describe('MCP Protocol Compliance', () => {
const results: string[] = []; const results: string[] = [];
// Start multiple requests with different delays // Start multiple requests with different delays
const p1 = client.callTool({ name: 'get_database_statistics', arguments: {} }) const p1 = client.callTool({ name: 'tools_documentation', arguments: {} })
.then(() => { results.push('stats'); return 'stats'; }); .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'; }); .then(() => { results.push('nodes'); return 'nodes'; });
const p3 = client.callTool({ name: 'search_nodes', arguments: { query: 'http' } }) const p3 = client.callTool({ name: 'search_nodes', arguments: { query: 'http' } })
@@ -216,13 +217,14 @@ describe('MCP Protocol Compliance', () => {
describe('Protocol Extensions', () => { describe('Protocol Extensions', () => {
it('should handle tool-specific extensions', async () => { it('should handle tool-specific extensions', async () => {
// Test tool with complex params // Test tool with complex params (using consolidated validate_node from v2.26.0)
const response = await client.callTool({ name: 'validate_node_operation', arguments: { const response = await client.callTool({ name: 'validate_node', arguments: {
nodeType: 'nodes-base.httpRequest', nodeType: 'nodes-base.httpRequest',
config: { config: {
method: 'GET', method: 'GET',
url: 'https://api.example.com' url: 'https://api.example.com'
}, },
mode: 'full',
profile: 'runtime' profile: 'runtime'
} }); } });
@@ -232,13 +234,13 @@ describe('MCP Protocol Compliance', () => {
it('should support optional parameters', async () => { it('should support optional parameters', async () => {
// Call with minimal params // 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 // 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, limit: 10,
category: 'trigger', mode: 'OR'
package: 'n8n-nodes-base'
} }); } });
expect(response1).toBeDefined(); expect(response1).toBeDefined();
@@ -255,7 +257,7 @@ describe('MCP Protocol Compliance', () => {
await testClient.connect(clientTransport); await testClient.connect(clientTransport);
// Make a request // 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(); expect(response).toBeDefined();
// Close client // Close client
@@ -263,7 +265,7 @@ describe('MCP Protocol Compliance', () => {
// Further requests should fail // Further requests should fail
try { try {
await testClient.callTool({ name: 'get_database_statistics', arguments: {} }); await testClient.callTool({ name: 'tools_documentation', arguments: {} });
expect.fail('Should have thrown an error'); expect.fail('Should have thrown an error');
} catch (error) { } catch (error) {
expect(error).toBeDefined(); expect(error).toBeDefined();
@@ -286,7 +288,7 @@ describe('MCP Protocol Compliance', () => {
const testClient = new Client({ name: 'test', version: '1.0.0' }, {}); const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
await testClient.connect(clientTransport); 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(); expect(response).toBeDefined();
await testClient.close(); await testClient.close();

View File

@@ -100,8 +100,8 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
await client.connect(clientTransport); await client.connect(clientTransport);
// Make some requests // Make some requests
await client.callTool({ name: 'get_database_statistics', arguments: {} }); await client.callTool({ name: 'tools_documentation', arguments: {} });
await client.callTool({ name: 'list_nodes', arguments: { limit: 5 } }); await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 5 } });
// Clean termination // Clean termination
await client.close(); await client.close();
@@ -109,7 +109,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
// Client should be closed // Client should be closed
try { 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'); expect.fail('Should not be able to make requests after close');
} catch (error) { } catch (error) {
expect(error).toBeDefined(); expect(error).toBeDefined();
@@ -133,7 +133,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
await client.connect(clientTransport); await client.connect(clientTransport);
// Make a request to ensure connection is active // 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 // Simulate abrupt disconnection by closing transport
await clientTransport.close(); await clientTransport.close();
@@ -141,7 +141,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
// Further operations should fail // Further operations should fail
try { 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'); expect.fail('Should not be able to make requests after transport close');
} catch (error) { } catch (error) {
expect(error).toBeDefined(); expect(error).toBeDefined();
@@ -179,14 +179,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
await client1.connect(ct1); await client1.connect(ct1);
// First session operations // 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).toBeDefined();
expect((response1 as any).content).toBeDefined(); expect((response1 as any).content).toBeDefined();
expect((response1 as any).content[0]).toHaveProperty('type', 'text'); expect((response1 as any).content[0]).toHaveProperty('type', 'text');
const data1 = JSON.parse(((response1 as any).content[0] as any).text); const data1 = JSON.parse(((response1 as any).content[0] as any).text);
// Handle both array response and object with nodes property // Handle both array response and object with results property
const nodes1 = Array.isArray(data1) ? data1 : data1.nodes; const results1 = Array.isArray(data1) ? data1 : data1.results;
expect(nodes1).toHaveLength(3); expect(results1.length).toBeLessThanOrEqual(3);
// Close first session completely // Close first session completely
await client1.close(); await client1.close();
@@ -204,14 +204,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
await client2.connect(ct2); await client2.connect(ct2);
// Second session operations // 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).toBeDefined();
expect((response2 as any).content).toBeDefined(); expect((response2 as any).content).toBeDefined();
expect((response2 as any).content[0]).toHaveProperty('type', 'text'); expect((response2 as any).content[0]).toHaveProperty('type', 'text');
const data2 = JSON.parse(((response2 as any).content[0] as any).text); const data2 = JSON.parse(((response2 as any).content[0] as any).text);
// Handle both array response and object with nodes property // Handle both array response and object with results property
const nodes2 = Array.isArray(data2) ? data2 : data2.nodes; const results2 = Array.isArray(data2) ? data2 : data2.results;
expect(nodes2).toHaveLength(5); expect(results2.length).toBeLessThanOrEqual(5);
// Clean up // Clean up
await client2.close(); 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' }, {}); const client1 = new Client({ name: 'multi-seq-1', version: '1.0.0' }, {});
await client1.connect(ct1); 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(); expect(resp1).toBeDefined();
await client1.close(); await client1.close();
await new Promise(resolve => setTimeout(resolve, 50)); await new Promise(resolve => setTimeout(resolve, 50));
@@ -239,8 +239,8 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
await mcpServer.connectToTransport(st2); await mcpServer.connectToTransport(st2);
const client2 = new Client({ name: 'multi-seq-2', version: '1.0.0' }, {}); const client2 = new Client({ name: 'multi-seq-2', version: '1.0.0' }, {});
await client2.connect(ct2); 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(); expect(resp2).toBeDefined();
await client2.close(); await client2.close();
@@ -261,14 +261,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
await client1.connect(ct1); await client1.connect(ct1);
// Make some requests // 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 client1.close();
await mcpServer1.close(); await mcpServer1.close();
// Second session - should be fresh // Second session - should be fresh
const mcpServer2 = new TestableN8NMCPServer(); const mcpServer2 = new TestableN8NMCPServer();
await mcpServer2.initialize(); await mcpServer2.initialize();
const [st2, ct2] = InMemoryTransport.createLinkedPair(); const [st2, ct2] = InMemoryTransport.createLinkedPair();
await mcpServer2.connectToTransport(st2); await mcpServer2.connectToTransport(st2);
@@ -276,7 +276,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
await client2.connect(ct2); await client2.connect(ct2);
// Should work normally // 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(); expect(response).toBeDefined();
await client2.close(); await client2.close();
@@ -299,7 +299,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
await client.connect(clientTransport); await client.connect(clientTransport);
// Quick operation // Quick operation
const response = await client.callTool({ name: 'get_database_statistics', arguments: {} }); const response = await client.callTool({ name: 'tools_documentation', arguments: {} });
expect(response).toBeDefined(); expect(response).toBeDefined();
// Explicit cleanup for each iteration // Explicit cleanup for each iteration
@@ -392,7 +392,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
// Light operation // Light operation
if (i % 10 === 0) { if (i % 10 === 0) {
await client.callTool({ name: 'get_database_statistics', arguments: {} }); await client.callTool({ name: 'tools_documentation', arguments: {} });
} }
// Explicit cleanup // Explicit cleanup
@@ -420,8 +420,8 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
const promises = []; const promises = [];
for (let i = 0; i < requestCount; i++) { for (let i = 0; i < requestCount; i++) {
const toolName = i % 2 === 0 ? 'list_nodes' : 'get_database_statistics'; const toolName = i % 2 === 0 ? 'search_nodes' : 'tools_documentation';
const params = toolName === 'list_nodes' ? { limit: 1 } : {}; const params = toolName === 'search_nodes' ? { query: 'http', limit: 1 } : {};
promises.push(client.callTool({ name: toolName as any, arguments: params })); 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 // 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(); expect(response).toBeDefined();
await client.close(); await client.close();
await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
await mcpServer.close(); await mcpServer.close();
@@ -496,9 +496,9 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
}); });
// Session should still work // 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(); expect(response).toBeDefined();
await client.close(); await client.close();
await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
await mcpServer.close(); await mcpServer.close();
@@ -539,7 +539,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
resources.clients.push(client); resources.clients.push(client);
// Make a request to ensure connection is active // 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 // Verify all resources are active
@@ -586,7 +586,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
// 3. Verify cleanup by attempting operations (should fail) // 3. Verify cleanup by attempting operations (should fail)
for (let i = 0; i < resources.clients.length; i++) { for (let i = 0; i < resources.clients.length; i++) {
try { 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'); expect.fail('Client should be closed');
} catch (error) { } catch (error) {
// Expected - client is closed // Expected - client is closed
@@ -643,9 +643,9 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
}, {}); }, {});
await client.connect(ct1); await client.connect(ct1);
// Initial request // Initial request
const response1 = await client.callTool({ name: 'get_database_statistics', arguments: {} }); const response1 = await client.callTool({ name: 'tools_documentation', arguments: {} });
expect(response1).toBeDefined(); expect(response1).toBeDefined();
// Close first client // Close first client
@@ -654,7 +654,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
// New connection with same server // New connection with same server
const [st2, ct2] = InMemoryTransport.createLinkedPair(); const [st2, ct2] = InMemoryTransport.createLinkedPair();
const connectTimeout = setTimeout(() => { const connectTimeout = setTimeout(() => {
throw new Error('Second connection timeout'); throw new Error('Second connection timeout');
}, 3000); }, 3000);
@@ -673,14 +673,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
}, {}); }, {});
await newClient.connect(ct2); await newClient.connect(ct2);
// Should work normally // Should work normally
const callTimeout = setTimeout(() => { const callTimeout = setTimeout(() => {
throw new Error('Second call timeout'); throw new Error('Second call timeout');
}, 3000); }, 3000);
try { try {
const response2 = await newClient.callTool({ name: 'get_database_statistics', arguments: {} }); const response2 = await newClient.callTool({ name: 'tools_documentation', arguments: {} });
clearTimeout(callTimeout); clearTimeout(callTimeout);
expect(response2).toBeDefined(); expect(response2).toBeDefined();
} catch (error) { } catch (error) {

View File

@@ -114,7 +114,7 @@ export class TestableN8NMCPServer {
// The MCP server initializes its database lazily // The MCP server initializes its database lazily
// We can trigger initialization by calling executeTool // We can trigger initialization by calling executeTool
try { try {
await this.mcpServer.executeTool('get_database_statistics', {}); await this.mcpServer.executeTool('tools_documentation', {});
} catch (error) { } catch (error) {
// Ignore errors, we just want to trigger initialization // Ignore errors, we just want to trigger initialization
} }

View File

@@ -30,66 +30,6 @@ describe('MCP Tool Invocation', () => {
}); });
describe('Node Discovery Tools', () => { 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', () => { describe('search_nodes', () => {
it('should search nodes by keyword', async () => { it('should search nodes by keyword', async () => {
const response = await client.callTool({ name: 'search_nodes', arguments: { const response = await client.callTool({ name: 'search_nodes', arguments: {
@@ -211,14 +151,16 @@ describe('MCP Tool Invocation', () => {
}); });
describe('Validation Tools', () => { 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 () => { 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', nodeType: 'nodes-base.httpRequest',
config: { config: {
method: 'GET', method: 'GET',
url: 'https://api.example.com/data' url: 'https://api.example.com/data'
} },
mode: 'full'
}}); }});
const validation = JSON.parse(((response as any).content[0]).text); 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 () => { 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', nodeType: 'nodes-base.httpRequest',
config: { config: {
method: 'GET' method: 'GET'
// Missing required 'url' field // Missing required 'url' field
} },
mode: 'full'
}}); }});
const validation = JSON.parse(((response as any).content[0]).text); 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 () => { it('should support different validation profiles', async () => {
const profiles = ['minimal', 'runtime', 'ai-friendly', 'strict']; const profiles = ['minimal', 'runtime', 'ai-friendly', 'strict'];
for (const profile of profiles) { 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', nodeType: 'nodes-base.httpRequest',
config: { method: 'GET', url: 'https://api.example.com' }, config: { method: 'GET', url: 'https://api.example.com' },
mode: 'full',
profile profile
}}); }});
@@ -427,85 +371,8 @@ describe('MCP Tool Invocation', () => {
}); });
}); });
describe('AI Tools', () => { // AI Tools section removed - list_ai_tools and get_node_as_tool_info were removed in v2.25.0
describe('list_ai_tools', () => { // Use search_nodes with query for finding AI-capable nodes
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');
});
});
});
});
describe('Complex Tool Interactions', () => { describe('Complex Tool Interactions', () => {
it('should handle tool chaining', async () => { it('should handle tool chaining', async () => {
@@ -526,20 +393,20 @@ describe('MCP Tool Invocation', () => {
}); });
it('should handle parallel tool calls', async () => { it('should handle parallel tool calls', async () => {
const tools = [ const toolCalls = [
'list_nodes', { name: 'search_nodes', arguments: { query: 'http' } },
'get_database_statistics', { name: 'tools_documentation', arguments: {} },
'list_ai_tools', { name: 'get_node', arguments: { nodeType: 'nodes-base.httpRequest' } },
'list_tasks' { name: 'search_nodes', arguments: { query: 'webhook' } }
]; ];
const promises = tools.map(tool => const promises = toolCalls.map(call =>
client.callTool({ name: tool as any, arguments: {} }) client.callTool(call)
); );
const responses = await Promise.all(promises); const responses = await Promise.all(promises);
expect(responses).toHaveLength(tools.length); expect(responses).toHaveLength(toolCalls.length);
responses.forEach(response => { responses.forEach(response => {
expect(response.content).toHaveLength(1); expect(response.content).toHaveLength(1);
expect(((response as any).content[0]).type).toBe('text'); expect(((response as any).content[0]).type).toBe('text');

View File

@@ -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);
});
});
});

View File

@@ -19,29 +19,6 @@ export interface HealthCheckResponse {
[key: string]: any; // Allow dynamic property access for optional field checks [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 { export interface ApiStatus {
configured: boolean; configured: boolean;
connected: boolean; connected: boolean;

View File

@@ -500,15 +500,15 @@ describe.skip('MCP Telemetry Integration', () => {
const slowToolRequest: CallToolRequest = { const slowToolRequest: CallToolRequest = {
method: 'tools/call', method: 'tools/call',
params: { params: {
name: 'list_nodes', name: 'search_nodes',
arguments: { limit: 1000 } arguments: { query: 'http', limit: 1000 }
} }
}; };
// Mock a slow operation // Mock a slow operation
vi.spyOn(mcpServer as any, 'executeTool').mockImplementation(async () => { vi.spyOn(mcpServer as any, 'executeTool').mockImplementation(async () => {
await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second delay await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second delay
return { nodes: [], totalCount: 0 }; return { results: [], totalCount: 0 };
}); });
const server = (mcpServer as any).server; const server = (mcpServer as any).server;
@@ -519,7 +519,7 @@ describe.skip('MCP Telemetry Integration', () => {
} }
expect(telemetry.trackToolUsage).toHaveBeenCalledWith( expect(telemetry.trackToolUsage).toHaveBeenCalledWith(
'list_nodes', 'search_nodes',
true, true,
expect.any(Number) expect.any(Number)
); );

View File

@@ -73,14 +73,14 @@ describe('Disabled Tools Feature (Issue #410)', () => {
}); });
it('should parse multiple disabled tools correctly', () => { 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(); server = new TestableN8NMCPServer();
const disabledTools = server.testGetDisabledTools(); const disabledTools = server.testGetDisabledTools();
expect(disabledTools.size).toBe(3); expect(disabledTools.size).toBe(3);
expect(disabledTools.has('n8n_diagnostic')).toBe(true); expect(disabledTools.has('n8n_diagnostic')).toBe(true);
expect(disabledTools.has('n8n_health_check')).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', () => { 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', () => { 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(); server = new TestableN8NMCPServer();
const disabledTools = server.testGetDisabledTools(); const disabledTools = server.testGetDisabledTools();
expect(disabledTools.size).toBe(3); expect(disabledTools.size).toBe(3);
expect(disabledTools.has('n8n_diagnostic')).toBe(true); expect(disabledTools.has('n8n_diagnostic')).toBe(true);
expect(disabledTools.has('n8n_health_check')).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', () => { it('should handle single comma correctly', () => {

View File

@@ -1031,7 +1031,7 @@ describe('handlers-n8n-manager', () => {
'1. Verify n8n instance is running', '1. Verify n8n instance is running',
'2. Check N8N_API_URL is correct', '2. Check N8N_API_URL is correct',
'3. Verify N8N_API_KEY has proper permissions', '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: { toolsAvailability: {
documentationTools: { documentationTools: {
count: 22, count: 7,
enabled: true, enabled: true,
}, },
managementTools: { managementTools: {
count: 16, count: 12,
enabled: true, enabled: true,
}, },
totalAvailable: 38, totalAvailable: 19,
}, },
}); });

View File

@@ -201,63 +201,76 @@ describe('Parameter Validation', () => {
}); });
}); });
describe('validate_node_operation', () => { describe('validate_node (consolidated)', () => {
it('should require nodeType and config parameters', async () => { it('should require nodeType and config parameters', async () => {
await expect(server.testExecuteTool('validate_node_operation', {})) await expect(server.testExecuteTool('validate_node', {}))
.rejects.toThrow('validate_node_operation: Validation failed:\n • nodeType: nodeType is required\n • config: config is required'); .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 () => { it('should require nodeType parameter when config is provided', async () => {
await expect(server.testExecuteTool('validate_node_operation', { config: {} })) await expect(server.testExecuteTool('validate_node', { config: {} }))
.rejects.toThrow('validate_node_operation: Validation failed:\n • nodeType: nodeType is required'); .rejects.toThrow('validate_node: Validation failed:\n • nodeType: nodeType is required');
}); });
it('should require config parameter when nodeType is provided', async () => { it('should require config parameter when nodeType is provided', async () => {
await expect(server.testExecuteTool('validate_node_operation', { nodeType: 'nodes-base.httpRequest' })) await expect(server.testExecuteTool('validate_node', { nodeType: 'nodes-base.httpRequest' }))
.rejects.toThrow('validate_node_operation: Validation failed:\n • config: config is required'); .rejects.toThrow('validate_node: Validation failed:\n • config: config is required');
}); });
it('should succeed with valid parameters', async () => { it('should succeed with valid parameters (full mode)', async () => {
const result = await server.testExecuteTool('validate_node_operation', { const result = await server.testExecuteTool('validate_node', {
nodeType: 'nodes-base.httpRequest', 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 }); 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', () => { describe('get_node mode=search_properties (consolidated)', () => {
it('should require nodeType and query parameters', async () => { it('should require nodeType and propertyQuery parameters', async () => {
await expect(server.testExecuteTool('search_node_properties', {})) await expect(server.testExecuteTool('get_node', { mode: 'search_properties' }))
.rejects.toThrow('Missing required parameters for search_node_properties: nodeType, query'); .rejects.toThrow('Missing required parameters for get_node: nodeType');
}); });
it('should succeed with valid parameters', async () => { 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', nodeType: 'nodes-base.httpRequest',
query: 'auth' mode: 'search_properties',
propertyQuery: 'auth'
}); });
expect(result).toEqual({ properties: [] }); expect(result).toEqual({ properties: [] });
}); });
it('should handle optional maxResults parameter', async () => { it('should handle optional maxPropertyResults parameter', async () => {
const result = await server.testExecuteTool('search_node_properties', { const result = await server.testExecuteTool('get_node', {
nodeType: 'nodes-base.httpRequest', nodeType: 'nodes-base.httpRequest',
query: 'auth', mode: 'search_properties',
maxResults: 5 propertyQuery: 'auth',
maxPropertyResults: 5
}); });
expect(result).toEqual({ properties: [] }); expect(result).toEqual({ properties: [] });
}); });
}); });
describe('list_node_templates', () => { describe('search_templates searchMode=by_nodes (consolidated)', () => {
it('should require nodeTypes parameter', async () => { it('should require nodeTypes parameter for by_nodes searchMode', async () => {
await expect(server.testExecuteTool('list_node_templates', {})) await expect(server.testExecuteTool('search_templates', { searchMode: 'by_nodes' }))
.rejects.toThrow('list_node_templates: Validation failed:\n • nodeTypes: nodeTypes is required'); .rejects.toThrow('nodeTypes array is required for searchMode=by_nodes');
}); });
it('should succeed with valid nodeTypes array', async () => { 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'] nodeTypes: ['nodes-base.httpRequest', 'nodes-base.slack']
}); });
expect(result).toEqual({ templates: [] }); expect(result).toEqual({ templates: [] });
@@ -320,45 +333,43 @@ describe('Parameter Validation', () => {
}); });
}); });
describe('maxResults parameter conversion', () => { describe('maxPropertyResults parameter conversion (v2.26.0 consolidated)', () => {
it('should convert string numbers to numbers', async () => { it('should pass numeric maxPropertyResults to searchNodeProperties', async () => {
const mockSearchNodeProperties = vi.spyOn(server as any, 'searchNodeProperties'); 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', nodeType: 'nodes-base.httpRequest',
query: 'auth', mode: 'search_properties',
maxResults: '5' propertyQuery: 'auth',
maxPropertyResults: 5
}); });
expect(mockSearchNodeProperties).toHaveBeenCalledWith('nodes-base.httpRequest', 'auth', 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'); 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', nodeType: 'nodes-base.httpRequest',
query: 'auth', mode: 'search_properties',
maxResults: 'invalid' propertyQuery: 'auth'
}); });
expect(mockSearchNodeProperties).toHaveBeenCalledWith('nodes-base.httpRequest', 'auth', 20); expect(mockSearchNodeProperties).toHaveBeenCalledWith('nodes-base.httpRequest', 'auth', 20);
}); });
}); });
describe('templateLimit parameter conversion', () => { describe('templateLimit parameter conversion (v2.26.0 consolidated)', () => {
it('should reject string limit values', async () => { it('should handle search_templates with by_nodes mode', async () => {
await expect(server.testExecuteTool('list_node_templates', { // 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'], nodeTypes: ['nodes-base.httpRequest'],
limit: '5' limit: 5
})).rejects.toThrow('list_node_templates: Validation failed:\n • limit: limit must be a number, got string'); })).resolves.toEqual({ templates: [] });
});
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');
}); });
}); });
@@ -399,24 +410,11 @@ describe('Parameter Validation', () => {
expect(result).toEqual({ docs: 'test' }); expect(result).toEqual({ docs: 'test' });
}); });
it('should allow list_nodes with no parameters', async () => { it('should allow tools_documentation with no parameters', async () => {
const result = await server.testExecuteTool('list_nodes', {}); const result = await server.testExecuteTool('tools_documentation', {});
expect(result).toEqual({ nodes: [] }); expect(result).toBeDefined();
}); // tools_documentation returns an object with documentation content
expect(typeof result).toBe('object');
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: [] });
}); });
}); });
@@ -429,8 +427,8 @@ describe('Parameter Validation', () => {
it('should list all missing parameters', () => { it('should list all missing parameters', () => {
expect(() => { expect(() => {
server.testValidateToolParams('validate_node_operation', { profile: 'strict' }, ['nodeType', 'config']); server.testValidateToolParams('validate_node', { profile: 'strict' }, ['nodeType', 'config']);
}).toThrow('validate_node_operation: Validation failed:\n • nodeType: nodeType is required\n • config: config is required'); }).toThrow('validate_node: Validation failed:\n • nodeType: nodeType is required\n • config: config is required');
}); });
it('should include helpful guidance', () => { it('should include helpful guidance', () => {
@@ -455,8 +453,8 @@ describe('Parameter Validation', () => {
await expect(server.testExecuteTool('search_nodes', {})) await expect(server.testExecuteTool('search_nodes', {}))
.rejects.toThrow('search_nodes: Validation failed:\n • query: query is required'); .rejects.toThrow('search_nodes: Validation failed:\n • query: query is required');
await expect(server.testExecuteTool('validate_node_operation', { nodeType: 'test' })) await expect(server.testExecuteTool('validate_node', { nodeType: 'test' }))
.rejects.toThrow('validate_node_operation: Validation failed:\n • config: config is required'); .rejects.toThrow('validate_node: Validation failed:\n • config: config is required');
}); });
it('should handle edge cases in parameter validation gracefully', async () => { it('should handle edge cases in parameter validation gracefully', async () => {
@@ -473,11 +471,11 @@ describe('Parameter Validation', () => {
// Tools using legacy validation // Tools using legacy validation
const legacyValidationTools = [ const legacyValidationTools = [
{ name: 'get_node', args: {}, expected: 'Missing required parameters for get_node: nodeType' }, { 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' }, // v2.26.0: get_node_documentation consolidated into get_node with mode='docs'
{ name: 'search_node_properties', args: {}, expected: 'Missing required parameters for search_node_properties: nodeType, query' }, // v2.26.0: search_node_properties consolidated into get_node with mode='search_properties'
// Note: get_node_for_task removed in v2.15.0 // Note: get_node_for_task removed in v2.15.0
{ name: 'get_property_dependencies', args: {}, expected: 'Missing required parameters for get_property_dependencies: nodeType' }, // Note: get_node_as_tool_info removed in v2.25.0
{ name: 'get_node_as_tool_info', args: {}, expected: 'Missing required parameters for get_node_as_tool_info: nodeType' }, // v2.26.0: get_property_dependencies removed (low usage)
{ name: 'get_template', args: {}, expected: 'Missing required parameters for get_template: templateId' }, { name: 'get_template', args: {}, expected: 'Missing required parameters for get_template: templateId' },
]; ];
@@ -487,11 +485,11 @@ describe('Parameter Validation', () => {
} }
// Tools using new schema validation // Tools using new schema validation
// Updated for v2.26.0 tool consolidation
const schemaValidationTools = [ const schemaValidationTools = [
{ name: 'search_nodes', args: {}, expected: 'search_nodes: Validation failed:\n • query: query is required' }, { 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', args: {}, expected: 'validate_node: 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' }, // list_node_templates consolidated into search_templates with searchMode='by_nodes'
{ name: 'list_node_templates', args: {}, expected: 'list_node_templates: Validation failed:\n • nodeTypes: nodeTypes is required' },
]; ];
for (const tool of schemaValidationTools) { for (const tool of schemaValidationTools) {
@@ -526,17 +524,15 @@ describe('Parameter Validation', () => {
handleUpdatePartialWorkflow: vi.fn().mockResolvedValue({ success: true }) 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 = [ 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_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', 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_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_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_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 // n8n_update_partial_workflow and n8n_trigger_webhook_workflow use legacy validation

View File

@@ -49,7 +49,7 @@ vi.mock('@/mcp/tool-docs', () => ({
performance: 'Instant - uses in-memory index', performance: 'Instant - uses in-memory index',
bestPractices: ['Start with single words', 'Use FUZZY for uncertain names'], bestPractices: ['Start with single words', 'Use FUZZY for uncertain names'],
pitfalls: ['Overly specific queries may return no results'], pitfalls: ['Overly specific queries may return no results'],
relatedTools: ['list_nodes', 'get_node_info'] relatedTools: ['get_node', 'get_node_documentation']
} }
}, },
validate_workflow: { validate_workflow: {
@@ -81,7 +81,7 @@ vi.mock('@/mcp/tool-docs', () => ({
performance: 'Depends on workflow complexity', performance: 'Depends on workflow complexity',
bestPractices: ['Validate before saving', 'Fix errors first'], bestPractices: ['Validate before saving', 'Fix errors first'],
pitfalls: ['Large workflows may take time'], pitfalls: ['Large workflows may take time'],
relatedTools: ['validate_node_operation'] relatedTools: ['validate_node']
} }
}, },
get_node_essentials: { get_node_essentials: {
@@ -172,7 +172,7 @@ describe('tools-documentation', () => {
expect(doc).toContain('## Common Pitfalls'); expect(doc).toContain('## Common Pitfalls');
expect(doc).toContain('- Overly specific queries'); expect(doc).toContain('- Overly specific queries');
expect(doc).toContain('## Related Tools'); expect(doc).toContain('## Related Tools');
expect(doc).toContain('- list_nodes'); expect(doc).toContain('- get_node');
}); });
}); });

View File

@@ -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', () => { describe('get_node', () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_node'); const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_node');
@@ -166,18 +141,23 @@ describe('n8nDocumentationToolsFinal', () => {
}); });
}); });
describe('get_templates_for_task', () => { describe('search_templates (consolidated)', () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_templates_for_task'); const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_templates');
it('should exist', () => { it('should exist', () => {
expect(tool).toBeDefined(); expect(tool).toBeDefined();
}); });
it('should have task as required parameter', () => { it('should have searchMode parameter with correct enum values', () => {
expect(tool?.inputSchema.required).toContain('task'); 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 = [ const expectedTasks = [
'ai_automation', 'ai_automation',
'data_sync', 'data_sync',
@@ -190,31 +170,37 @@ describe('n8nDocumentationToolsFinal', () => {
'api_integration', 'api_integration',
'database_operations' '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', () => { 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 => { n8nDocumentationToolsFinal.forEach(tool => {
// Descriptions should be informative but not overly long // Consolidated tools (v2.26.0) may have longer descriptions due to multiple modes
expect(tool.description.length).toBeLessThan(300); // 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', () => { it('should include examples or key information in descriptions', () => {
const toolsWithExamples = [ const toolsWithExamples = [
'list_nodes',
'get_node', 'get_node',
'search_nodes', 'search_nodes'
'get_node_documentation'
]; ];
toolsWithExamples.forEach(toolName => { toolsWithExamples.forEach(toolName => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName); const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
// Should include either example usage, format information, or "nodes-base" // 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', () => { describe('Tool Categories Coverage', () => {
it('should have tools for all major categories', () => { it('should have tools for all major categories', () => {
// Updated for v2.26.0 consolidated tools
const categories = { const categories = {
discovery: ['list_nodes', 'search_nodes', 'list_ai_tools'], discovery: ['search_nodes'],
configuration: ['get_node', 'get_node_documentation'], configuration: ['get_node'], // get_node now includes docs mode
validation: ['validate_node_operation', 'validate_workflow', 'validate_node_minimal'], validation: ['validate_node', 'validate_workflow'], // consolidated validate_node
templates: ['list_tasks', 'search_templates', 'list_templates', 'get_template', 'list_node_templates'], // get_node_for_task removed in v2.15.0 templates: ['search_templates', 'get_template'], // search_templates now handles all search modes
documentation: ['tools_documentation'] documentation: ['tools_documentation']
}; };
Object.entries(categories).forEach(([category, expectedTools]) => { Object.entries(categories).forEach(([_category, expectedTools]) => {
expectedTools.forEach(toolName => { expectedTools.forEach(toolName => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName); const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
expect(tool).toBeDefined(); expect(tool).toBeDefined();
@@ -294,62 +281,30 @@ describe('n8nDocumentationToolsFinal', () => {
}); });
describe('Edge Cases', () => { describe('Edge Cases', () => {
it('should handle tools with no parameters', () => { it('should handle tools with optional parameters only', () => {
const toolsWithNoParams = ['list_ai_tools', 'get_database_statistics']; // Tools where all parameters are optional
const toolsWithOptionalParams = ['tools_documentation'];
toolsWithNoParams.forEach(toolName => {
toolsWithOptionalParams.forEach(toolName => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName); const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
expect(tool).toBeDefined(); 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', () => { it('should have array parameters defined correctly', () => {
const toolsWithArrays = ['list_node_templates']; // search_templates now handles nodeTypes for by_nodes mode
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_templates');
toolsWithArrays.forEach(toolName => { const arrayParam = tool?.inputSchema.properties?.nodeTypes;
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName); expect(arrayParam?.type).toBe('array');
const arrayParam = tool?.inputSchema.properties.nodeTypes; expect(arrayParam?.items).toBeDefined();
expect(arrayParam?.type).toBe('array'); expect(arrayParam?.items.type).toBe('string');
expect(arrayParam?.items).toBeDefined();
expect(arrayParam?.items.type).toBe('string');
});
}); });
}); });
describe('New Template Tools', () => { describe('Consolidated Template Tools (v2.26.0)', () => {
describe('list_templates', () => { describe('get_template', () => {
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)', () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_template'); const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_template');
it('should exist and support mode parameter', () => { it('should exist and support mode parameter', () => {
@@ -370,130 +325,56 @@ describe('n8nDocumentationToolsFinal', () => {
}); });
}); });
describe('search_templates_by_metadata', () => { describe('search_templates (consolidated with searchMode)', () => {
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_templates_by_metadata'); 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).toBeDefined();
expect(tool?.name).toBe('search_templates_by_metadata'); expect(tool?.inputSchema.properties).toHaveProperty('searchMode');
}); });
it('should have proper description', () => { it('should support metadata filtering via by_metadata searchMode', () => {
expect(tool?.description).toContain('Search templates by AI-generated metadata'); // These properties are for by_metadata searchMode
expect(tool?.description).toContain('category'); const props = tool?.inputSchema.properties;
expect(tool?.description).toContain('complexity'); expect(props).toHaveProperty('category');
}); expect(props).toHaveProperty('complexity');
expect(props?.complexity?.enum).toEqual(['simple', 'medium', 'complex']);
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 have pagination parameters', () => { it('should have pagination parameters', () => {
const limitProp = tool?.inputSchema.properties?.limit; const limitProp = tool?.inputSchema.properties?.limit;
const offsetProp = tool?.inputSchema.properties?.offset; const offsetProp = tool?.inputSchema.properties?.offset;
expect(limitProp).toBeDefined(); expect(limitProp).toBeDefined();
expect(limitProp.type).toBe('number'); expect(limitProp.type).toBe('number');
expect(limitProp.default).toBe(20); expect(limitProp.default).toBe(20);
expect(limitProp.maximum).toBe(100); expect(limitProp.maximum).toBe(100);
expect(limitProp.minimum).toBe(1); expect(limitProp.minimum).toBe(1);
expect(offsetProp).toBeDefined(); expect(offsetProp).toBeDefined();
expect(offsetProp.type).toBe('number'); expect(offsetProp.type).toBe('number');
expect(offsetProp.default).toBe(0); expect(offsetProp.default).toBe(0);
expect(offsetProp.minimum).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 || {}); const properties = Object.keys(tool?.inputSchema.properties || {});
// Consolidated tool includes properties from all former tools
const expectedProperties = [ const expectedProperties = [
'category', 'searchMode', // New mode selector
'complexity', 'query', // For keyword search
'maxSetupMinutes', 'nodeTypes', // For by_nodes search (formerly list_node_templates)
'minSetupMinutes', 'task', // For by_task search (formerly get_templates_for_task)
'requiredService', 'category', // For by_metadata search
'targetAudience', 'complexity',
'limit', 'limit',
'offset' 'offset'
]; ];
expectedProperties.forEach(prop => { expectedProperties.forEach(prop => {
expect(properties).toContain(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);
});
});
});
}); });
}); });
}); });