mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 14:32:04 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7dd04b471 | ||
|
|
c7e7bda505 | ||
|
|
bac4936c6d | ||
|
|
25784142fe | ||
|
|
f770043d3d | ||
|
|
1be06c217f | ||
|
|
c974947c84 | ||
|
|
ff69e4ccca | ||
|
|
9ee4b9492f | ||
|
|
4df9558b3e | ||
|
|
05424f66af | ||
|
|
5d2c5df53e | ||
|
|
f5cf1e2934 | ||
|
|
9050967cd6 |
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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>
|
||||
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.
|
||||
|
||||
222
.github/workflows/dependency-check.yml
vendored
Normal file
222
.github/workflows/dependency-check.yml
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
name: Dependency Compatibility Check
|
||||
|
||||
# This workflow verifies that when users install n8n-mcp via npm (without lockfile),
|
||||
# they get compatible dependency versions. This catches issues like #440, #444, #446, #447, #450
|
||||
# where npm resolution gave users incompatible SDK/Zod versions.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- '.github/workflows/dependency-check.yml'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- '.github/workflows/dependency-check.yml'
|
||||
# Allow manual trigger for debugging
|
||||
workflow_dispatch:
|
||||
# Run weekly to catch upstream dependency changes
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Every Monday at 6 AM UTC
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
fresh-install-check:
|
||||
name: Fresh Install Dependency Check
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Pack package for testing
|
||||
run: npm pack
|
||||
|
||||
- name: Create fresh install test directory
|
||||
run: |
|
||||
mkdir -p /tmp/fresh-install-test
|
||||
cp n8n-mcp-*.tgz /tmp/fresh-install-test/
|
||||
|
||||
- name: Install package fresh (simulating user install)
|
||||
working-directory: /tmp/fresh-install-test
|
||||
run: |
|
||||
npm init -y
|
||||
# Install from tarball WITHOUT lockfile (simulates npm install n8n-mcp)
|
||||
npm install ./n8n-mcp-*.tgz
|
||||
|
||||
- name: Verify critical dependency versions
|
||||
working-directory: /tmp/fresh-install-test
|
||||
run: |
|
||||
echo "=== Dependency Version Check ==="
|
||||
echo ""
|
||||
|
||||
# Get actual resolved versions
|
||||
SDK_VERSION=$(npm list @modelcontextprotocol/sdk --json 2>/dev/null | jq -r '.dependencies["n8n-mcp"].dependencies["@modelcontextprotocol/sdk"].version // .dependencies["@modelcontextprotocol/sdk"].version // "not found"')
|
||||
ZOD_VERSION=$(npm list zod --json 2>/dev/null | jq -r '.dependencies["n8n-mcp"].dependencies.zod.version // .dependencies.zod.version // "not found"')
|
||||
|
||||
echo "MCP SDK version: $SDK_VERSION"
|
||||
echo "Zod version: $ZOD_VERSION"
|
||||
echo ""
|
||||
|
||||
# Check MCP SDK version - must be exactly 1.20.1
|
||||
if [[ "$SDK_VERSION" == "not found" ]]; then
|
||||
echo "❌ FAILED: Could not determine MCP SDK version!"
|
||||
echo " The dependency may not have been installed correctly."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$SDK_VERSION" != "1.20.1" ]]; then
|
||||
echo "❌ FAILED: MCP SDK version mismatch!"
|
||||
echo " Expected: 1.20.1"
|
||||
echo " Got: $SDK_VERSION"
|
||||
echo ""
|
||||
echo "This can cause runtime errors. See issues #440, #444, #446, #447, #450"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ MCP SDK version is correct: $SDK_VERSION"
|
||||
|
||||
# Check Zod version - must be 3.x (not 4.x, including pre-releases)
|
||||
if [[ "$ZOD_VERSION" == "not found" ]]; then
|
||||
echo "❌ FAILED: Could not determine Zod version!"
|
||||
echo " The dependency may not have been installed correctly."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$ZOD_VERSION" =~ ^4\. ]]; then
|
||||
echo "❌ FAILED: Zod v4 detected - incompatible with MCP SDK 1.20.1!"
|
||||
echo " Expected: 3.x"
|
||||
echo " Got: $ZOD_VERSION"
|
||||
echo ""
|
||||
echo "Zod v4 causes '_zod' property errors. See issues #440, #444, #446, #447, #450"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Zod version is compatible: $ZOD_VERSION"
|
||||
|
||||
echo ""
|
||||
echo "=== All dependency checks passed ==="
|
||||
|
||||
- name: Test basic functionality
|
||||
working-directory: /tmp/fresh-install-test
|
||||
run: |
|
||||
echo "=== Basic Functionality Test ==="
|
||||
|
||||
# Create a simple test script
|
||||
cat > test-import.mjs << 'EOF'
|
||||
import { spawn } from 'child_process';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Test that the package can be required and basic tools work
|
||||
async function test() {
|
||||
console.log('Testing n8n-mcp package import...');
|
||||
|
||||
// Start the MCP server briefly to verify it initializes
|
||||
const serverPath = path.join(__dirname, 'node_modules/n8n-mcp/dist/mcp/index.js');
|
||||
|
||||
const proc = spawn('node', [serverPath], {
|
||||
env: { ...process.env, MCP_MODE: 'stdio' },
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
// Send initialize request
|
||||
const initRequest = JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'test', version: '1.0.0' }
|
||||
}
|
||||
});
|
||||
|
||||
proc.stdin.write(initRequest + '\n');
|
||||
|
||||
// Wait for response or timeout
|
||||
let output = '';
|
||||
let stderrOutput = '';
|
||||
proc.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (data) => {
|
||||
stderrOutput += data.toString();
|
||||
console.error('stderr:', data.toString());
|
||||
});
|
||||
|
||||
// Give it 5 seconds to respond
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
|
||||
proc.kill();
|
||||
|
||||
// Check for Zod v4 compatibility errors (the bug we're testing for)
|
||||
const allOutput = output + stderrOutput;
|
||||
if (allOutput.includes('_zod') || allOutput.includes('Cannot read properties of undefined')) {
|
||||
console.error('❌ FAILED: Zod compatibility error detected!');
|
||||
console.error('This indicates the SDK/Zod version fix is not working.');
|
||||
console.error('See issues #440, #444, #446, #447, #450');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (output.includes('"result"')) {
|
||||
console.log('✅ MCP server initialized successfully');
|
||||
return true;
|
||||
} else {
|
||||
console.log('Output received:', output.substring(0, 500));
|
||||
// Server might not respond in stdio mode without proper framing
|
||||
// But if we got here without crashing, that's still good
|
||||
console.log('✅ MCP server started without errors');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
test()
|
||||
.then(() => {
|
||||
console.log('=== Basic functionality test passed ===');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('❌ Test failed:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
EOF
|
||||
|
||||
node test-import.mjs
|
||||
|
||||
- name: Generate dependency report
|
||||
if: always()
|
||||
working-directory: /tmp/fresh-install-test
|
||||
run: |
|
||||
echo "=== Full Dependency Tree ===" > dependency-report.txt
|
||||
npm list --all >> dependency-report.txt 2>&1 || true
|
||||
|
||||
echo "" >> dependency-report.txt
|
||||
echo "=== Critical Dependencies ===" >> dependency-report.txt
|
||||
npm list @modelcontextprotocol/sdk zod zod-to-json-schema >> dependency-report.txt 2>&1 || true
|
||||
|
||||
cat dependency-report.txt
|
||||
|
||||
- name: Upload dependency report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dependency-report-${{ github.run_number }}
|
||||
path: /tmp/fresh-install-test/dependency-report.txt
|
||||
retention-days: 30
|
||||
421
CHANGELOG.md
421
CHANGELOG.md
@@ -7,6 +7,427 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.26.5] - 2025-11-27
|
||||
|
||||
### 🔧 Fixed
|
||||
|
||||
- **Tools Documentation: Runtime Token Optimization**
|
||||
- Removed historical migration information from tool descriptions (e.g., "Replaces X, Y, Z...")
|
||||
- Removed version-specific references (v2.21.1, issue #357) that are not needed at runtime
|
||||
- Cleaned up consolidation comments in index.ts
|
||||
- Documentation now starts directly with functional content for better AI agent efficiency
|
||||
- Estimated savings: ~128 tokens per full documentation request
|
||||
- Affected tools: `get_node`, `validate_node`, `search_templates`, `n8n_executions`, `n8n_get_workflow`, `n8n_update_partial_workflow`
|
||||
|
||||
**Conceived by Romuald Członkowski - [AiAdvisors](https://www.aiadvisors.pl/en)**
|
||||
|
||||
## [2.26.4] - 2025-11-26
|
||||
|
||||
### 🔧 Fixed
|
||||
|
||||
- **n8n 1.121 Compatibility**: Added support for new workflow settings introduced in n8n 1.121
|
||||
- Added `availableInMCP` (boolean) to settings whitelist - controls "Available in MCP" toggle
|
||||
- Added `callerPolicy` to settings whitelist - was already in schema but missing from sanitization
|
||||
- Both settings are now preserved during workflow updates instead of being silently stripped
|
||||
- Settings can be toggled via `updateSettings` operation: `{type: "updateSettings", settings: {availableInMCP: true}}`
|
||||
|
||||
**Conceived by Romuald Członkowski - [AiAdvisors](https://www.aiadvisors.pl/en)**
|
||||
|
||||
## [2.26.3] - 2025-11-26
|
||||
|
||||
### 🔧 Fixed
|
||||
|
||||
- **Tools Documentation Gaps**: Addressed remaining documentation issues after v2.26.2 tool consolidation
|
||||
- Added missing `n8n_workflow_versions` documentation with all 6 modes (list, get, rollback, delete, prune, truncate)
|
||||
- Removed non-existent tools (`n8n_diagnostic`, `n8n_list_available_tools`) from documentation exports
|
||||
- Fixed 10+ outdated tool name references:
|
||||
- `get_node_essentials` → `get_node({detail: "standard"})`
|
||||
- `validate_node_operation` → `validate_node()`
|
||||
- `get_minimal` → `n8n_get_workflow({mode: "minimal"})`
|
||||
- Added missing `mode` and `verbose` parameters to `n8n_health_check` documentation
|
||||
- Added missing `mode` parameter to `get_template` documentation (nodes_only, structure, full)
|
||||
- Updated template count from "399+" to "2,700+" in `get_template`
|
||||
- Updated node count from "525" to "500+" in `search_nodes`
|
||||
- Fixed `relatedTools` arrays to remove references to non-existent tools
|
||||
|
||||
**Conceived by Romuald Członkowski - [AiAdvisors](https://www.aiadvisors.pl/en)**
|
||||
|
||||
## [2.26.2] - 2025-11-25
|
||||
|
||||
### 🔧 Fixed
|
||||
|
||||
- **Tool Documentation Cleanup**: Synchronized `tool-docs/` with v2.26.0 tool consolidation
|
||||
- Deleted 23 obsolete documentation files for removed tools (get_node_info, get_node_essentials, validate_node_operation, etc.)
|
||||
- Created consolidated documentation for `get_node` (covers all modes: info, docs, search_properties, versions, compare, breaking, migrations)
|
||||
- Created consolidated documentation for `validate_node` (covers modes: full, minimal; profiles: minimal, runtime, ai-friendly, strict)
|
||||
- Created consolidated documentation for `n8n_executions` (covers actions: get, list, delete)
|
||||
- Updated `search_templates` documentation with all searchModes (keyword, by_nodes, by_task, by_metadata)
|
||||
- Updated `n8n_get_workflow` documentation with all modes (full, details, structure, minimal)
|
||||
- Fixed stale `relatedTools` references pointing to removed tools
|
||||
- Updated `tools-documentation.ts` overview to accurately reflect 19 consolidated tools
|
||||
|
||||
## [2.26.1] - 2025-11-25
|
||||
|
||||
### 🔄 Updated
|
||||
|
||||
- Updated n8n from 1.120.3 to 1.121.2
|
||||
- Updated n8n-core from 1.119.2 to 1.120.1
|
||||
- Updated n8n-workflow from 1.117.0 to 1.118.1
|
||||
- Updated @n8n/n8n-nodes-langchain from 1.119.1 to 1.120.1
|
||||
- Rebuilt node database with 545 nodes (439 from n8n-nodes-base, 106 from @n8n/n8n-nodes-langchain)
|
||||
- Expanded template database from ~2,598 to 2,768 templates (+170 new templates)
|
||||
- Updated README badge with new n8n version
|
||||
|
||||
## [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
|
||||
|
||||
### ✨ Features
|
||||
|
||||
**Session Persistence API**
|
||||
|
||||
Added export/restore functionality for MCP sessions to enable zero-downtime deployments in container environments (Kubernetes, Docker Swarm, etc.).
|
||||
|
||||
#### What's New
|
||||
|
||||
**1. Export Session State**
|
||||
- `exportSessionState()` method in `SingleSessionHTTPServer` and `N8NMCPEngine`
|
||||
- Exports all active sessions with metadata and instance context
|
||||
- Automatically filters expired sessions
|
||||
- Returns serializable `SessionState[]` array
|
||||
|
||||
**2. Restore Session State**
|
||||
- `restoreSessionState(sessions)` method for session recovery
|
||||
- Validates session structure using existing `validateInstanceContext()`
|
||||
- Handles null/invalid sessions gracefully with warnings
|
||||
- Enforces MAX_SESSIONS limit (100 concurrent sessions)
|
||||
- Skips expired sessions during restore
|
||||
|
||||
**3. SessionState Type**
|
||||
- New type definition in `src/types/session-state.ts`
|
||||
- Fully documented with JSDoc comments
|
||||
- Includes metadata (timestamps) and context (credentials)
|
||||
- Exported from main package index
|
||||
|
||||
**4. Dormant Session Behavior**
|
||||
- Restored sessions are "dormant" until first request
|
||||
- Transport and server objects recreated on-demand
|
||||
- Memory-efficient session recovery
|
||||
|
||||
#### Security Considerations
|
||||
|
||||
⚠️ **IMPORTANT:** Exported session data contains plaintext n8n API keys. Downstream applications MUST encrypt session data before persisting to disk using AES-256-GCM or equivalent.
|
||||
|
||||
#### Use Cases
|
||||
- Zero-downtime deployments in container orchestration
|
||||
- Session recovery after crashes or restarts
|
||||
- Multi-tenant platform session management
|
||||
- Rolling updates without user disruption
|
||||
|
||||
#### Testing
|
||||
- 22 comprehensive unit tests (100% passing)
|
||||
- Tests cover export, restore, edge cases, and round-trip cycles
|
||||
- Validation of expired session filtering and error handling
|
||||
|
||||
#### Implementation Details
|
||||
- Only exports sessions with valid `n8nApiUrl` and `n8nApiKey` in context
|
||||
- Respects `sessionTimeout` setting (default 30 minutes)
|
||||
- Session metadata and context persisted; transport/server recreated on-demand
|
||||
- Comprehensive error handling with detailed logging
|
||||
|
||||
**Conceived by Romuald Członkowski - [AiAdvisors](https://www.aiadvisors.pl/en)**
|
||||
|
||||
## [2.24.0] - 2025-01-24
|
||||
|
||||
### ✨ Features
|
||||
|
||||
**Unified Node Information Tool**
|
||||
|
||||
Introduced `get_node` - a unified tool that consolidates and enhances node information retrieval with multiple detail levels, version history, and type structure metadata.
|
||||
|
||||
#### What's New
|
||||
|
||||
**1. Progressive Detail Levels**
|
||||
- `minimal`: Basic metadata only (~200 tokens) - nodeType, displayName, description, category, version summary
|
||||
- `standard`: Essential properties and operations - AI-friendly default (~1000-2000 tokens)
|
||||
- `full`: Complete node information including all properties (~3000-8000 tokens)
|
||||
|
||||
**2. Version History & Management**
|
||||
- `versions` mode: List all versions with breaking changes summary
|
||||
- `compare` mode: Compare two versions with property-level changes
|
||||
- `breaking` mode: Show only breaking changes between versions
|
||||
- `migrations` mode: Show auto-migratable changes
|
||||
- Version summary always included in info mode responses
|
||||
|
||||
**3. Type Structure Metadata**
|
||||
- `includeTypeInfo` parameter exposes type structures from v2.23.0 validation system
|
||||
- Includes: type category, JS type, validation rules, structure hints
|
||||
- Helps AI agents understand complex types (filter, resourceMapper, resourceLocator, etc.)
|
||||
- Adds ~80-120 tokens per property when enabled
|
||||
- Works with all detail levels
|
||||
|
||||
**4. Real-World Examples**
|
||||
- `includeExamples` parameter includes configuration examples from templates
|
||||
- Shows popular workflow patterns
|
||||
- Includes metadata (views, complexity, use cases)
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
```javascript
|
||||
// Standard detail (recommended for AI agents)
|
||||
get_node({nodeType: "nodes-base.httpRequest"})
|
||||
|
||||
// Standard with type info
|
||||
get_node({nodeType: "nodes-base.httpRequest", includeTypeInfo: true})
|
||||
|
||||
// Minimal (quick metadata check)
|
||||
get_node({nodeType: "nodes-base.httpRequest", detail: "minimal"})
|
||||
|
||||
// Full detail with examples
|
||||
get_node({nodeType: "nodes-base.httpRequest", detail: "full", includeExamples: true})
|
||||
|
||||
// Version history
|
||||
get_node({nodeType: "nodes-base.httpRequest", mode: "versions"})
|
||||
|
||||
// Compare versions
|
||||
get_node({
|
||||
nodeType: "nodes-base.httpRequest",
|
||||
mode: "compare",
|
||||
fromVersion: "3.0",
|
||||
toVersion: "4.1"
|
||||
})
|
||||
```
|
||||
|
||||
#### Benefits
|
||||
|
||||
- ✅ **Single Unified API**: One tool for all node information needs
|
||||
- ✅ **Token Efficient**: AI-friendly defaults (standard mode recommended)
|
||||
- ✅ **Progressive Disclosure**: minimal → standard → full as needed
|
||||
- ✅ **Type Aware**: Exposes v2.23.0 type structures for better configuration
|
||||
- ✅ **Version Aware**: Built-in version history and comparison
|
||||
- ✅ **Flexible**: Can combine detail levels with type info and examples
|
||||
- ✅ **Discoverable**: Version summary always visible in info mode
|
||||
|
||||
#### Token Costs
|
||||
|
||||
- `minimal`: ~200 tokens
|
||||
- `standard`: ~1000-2000 tokens (default)
|
||||
- `full`: ~3000-8000 tokens
|
||||
- `includeTypeInfo`: +80-120 tokens per property
|
||||
- `includeExamples`: +200-400 tokens per example
|
||||
- Version modes: ~400-1200 tokens
|
||||
|
||||
### 🗑️ Breaking Changes
|
||||
|
||||
**Removed Deprecated Tools**
|
||||
|
||||
Immediately removed `get_node_info` and `get_node_essentials` in favor of the unified `get_node` tool:
|
||||
- `get_node_info` → Use `get_node` with `detail='full'`
|
||||
- `get_node_essentials` → Use `get_node` with `detail='standard'` (default)
|
||||
|
||||
**Migration:**
|
||||
```javascript
|
||||
// Old
|
||||
get_node_info({nodeType: "nodes-base.httpRequest"})
|
||||
// New
|
||||
get_node({nodeType: "nodes-base.httpRequest", detail: "full"})
|
||||
|
||||
// Old
|
||||
get_node_essentials({nodeType: "nodes-base.httpRequest", includeExamples: true})
|
||||
// New
|
||||
get_node({nodeType: "nodes-base.httpRequest", includeExamples: true})
|
||||
// or
|
||||
get_node({nodeType: "nodes-base.httpRequest", detail: "standard", includeExamples: true})
|
||||
```
|
||||
|
||||
### 📊 Impact
|
||||
|
||||
**Tool Count**: 40 → 39 tools (-2 deprecated, +1 new unified)
|
||||
|
||||
**For AI Agents:**
|
||||
- Better understanding of complex n8n types through type metadata
|
||||
- Version upgrade planning with breaking change detection
|
||||
- Token-efficient defaults reduce costs
|
||||
- Progressive disclosure of information as needed
|
||||
|
||||
**For Users:**
|
||||
- Single tool to learn instead of two separate tools
|
||||
- Clear progression from minimal to full detail
|
||||
- Version history helps with node upgrades
|
||||
- Type-aware configuration assistance
|
||||
|
||||
### 🔧 Technical Details
|
||||
|
||||
**Files Added:**
|
||||
- Enhanced type structure exposure in node information
|
||||
|
||||
**Files Modified:**
|
||||
- `src/mcp/tools.ts` - Removed get_node_info and get_node_essentials, added get_node
|
||||
- `src/mcp/server.ts` - Added unified getNode() implementation with all modes
|
||||
- `package.json` - Version bump to 2.24.0
|
||||
|
||||
**Implementation:**
|
||||
- ~250 lines of new code
|
||||
- 7 new private methods for mode handling
|
||||
- Version repository methods utilized (previously unused)
|
||||
- TypeStructureService integrated for type metadata
|
||||
- 100% backward compatible in behavior (just different API)
|
||||
|
||||
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.23.0] - 2025-11-21
|
||||
|
||||
### ✨ Features
|
||||
|
||||
35
CLAUDE.md
35
CLAUDE.md
@@ -32,7 +32,9 @@ src/
|
||||
│ ├── expression-validator.ts # n8n expression syntax validation (NEW in v2.5.0)
|
||||
│ └── workflow-validator.ts # Complete workflow validation (NEW in v2.5.0)
|
||||
├── types/
|
||||
│ └── type-structures.ts # Type structure definitions (NEW in v2.22.21)
|
||||
│ ├── type-structures.ts # Type structure definitions (NEW in v2.22.21)
|
||||
│ ├── instance-context.ts # Multi-tenant instance configuration
|
||||
│ └── session-state.ts # Session persistence types (NEW in v2.24.1)
|
||||
├── constants/
|
||||
│ └── type-structures.ts # 22 complete type structures (NEW in v2.22.21)
|
||||
├── templates/
|
||||
@@ -64,7 +66,9 @@ src/
|
||||
│ ├── console-manager.ts # Console output isolation (NEW in v2.3.1)
|
||||
│ └── logger.ts # Logging utility with HTTP awareness
|
||||
├── http-server-single-session.ts # Single-session HTTP server (NEW in v2.3.1)
|
||||
│ # Session persistence API (NEW in v2.24.1)
|
||||
├── mcp-engine.ts # Clean API for service integration (NEW in v2.3.1)
|
||||
│ # Session persistence wrappers (NEW in v2.24.1)
|
||||
└── index.ts # Library exports
|
||||
```
|
||||
|
||||
@@ -191,6 +195,35 @@ The MCP server exposes tools in several categories:
|
||||
### Development Best Practices
|
||||
- Run typecheck and lint after every code change
|
||||
|
||||
### Session Persistence Feature (v2.24.1)
|
||||
|
||||
**Location:**
|
||||
- Types: `src/types/session-state.ts`
|
||||
- Implementation: `src/http-server-single-session.ts` (lines 698-702, 1444-1584)
|
||||
- Wrapper: `src/mcp-engine.ts` (lines 123-169)
|
||||
- Tests: `tests/unit/http-server/session-persistence.test.ts`, `tests/unit/mcp-engine/session-persistence.test.ts`
|
||||
|
||||
**Key Features:**
|
||||
- **Export/Restore API**: `exportSessionState()` and `restoreSessionState()` methods
|
||||
- **Multi-tenant support**: Enables zero-downtime deployments for SaaS platforms
|
||||
- **Security-first**: API keys exported as plaintext - downstream MUST encrypt
|
||||
- **Dormant sessions**: Restored sessions recreate transports on first request
|
||||
- **Automatic expiration**: Respects `sessionTimeout` setting (default 30 min)
|
||||
- **MAX_SESSIONS limit**: Caps at 100 concurrent sessions
|
||||
|
||||
**Important Implementation Notes:**
|
||||
- Only exports sessions with valid n8nApiUrl and n8nApiKey in context
|
||||
- Skips expired sessions during both export and restore
|
||||
- Uses `validateInstanceContext()` for data integrity checks
|
||||
- Handles null/invalid session gracefully with warnings
|
||||
- Session metadata (timestamps) and context (credentials) are persisted
|
||||
- Transport and server objects are NOT persisted (recreated on-demand)
|
||||
|
||||
**Testing:**
|
||||
- 22 unit tests covering export, restore, edge cases, and round-trip cycles
|
||||
- Tests use current timestamps to avoid expiration issues
|
||||
- Integration with multi-tenant backends documented in README.md
|
||||
|
||||
# important-instruction-reminders
|
||||
Do what has been asked; nothing more, nothing less.
|
||||
NEVER create files unless they're absolutely necessary for achieving your goal.
|
||||
|
||||
275
README.md
275
README.md
@@ -5,11 +5,11 @@
|
||||
[](https://www.npmjs.com/package/n8n-mcp)
|
||||
[](https://codecov.io/gh/czlonkowski/n8n-mcp)
|
||||
[](https://github.com/czlonkowski/n8n-mcp/actions)
|
||||
[](https://github.com/n8n-io/n8n)
|
||||
[](https://github.com/n8n-io/n8n)
|
||||
[](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp)
|
||||
[](https://railway.com/deploy/n8n-mcp?referralCode=n8n-mcp)
|
||||
|
||||
A Model Context Protocol (MCP) server that provides AI assistants with comprehensive access to n8n node documentation, properties, and operations. Deploy in minutes to give Claude and other AI assistants deep knowledge about n8n's 543 workflow automation nodes.
|
||||
A Model Context Protocol (MCP) server that provides AI assistants with comprehensive access to n8n node documentation, properties, and operations. Deploy in minutes to give Claude and other AI assistants deep knowledge about n8n's 545 workflow automation nodes.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -36,12 +36,31 @@ AI results can be unpredictable. Protect your work!
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
Get n8n-MCP running in 5 minutes:
|
||||
### 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 connect your MCP client.
|
||||
|
||||
---
|
||||
|
||||
## 🏠 Self-Hosting Options
|
||||
|
||||
Prefer to run n8n-MCP yourself? Choose your deployment method:
|
||||
|
||||
### Option A: npx (Quick Local Setup) 🚀
|
||||
|
||||
Get n8n-MCP running in minutes:
|
||||
|
||||
[](https://youtu.be/5CccjiLLyaY?si=Z62SBGlw9G34IQnQ&t=343)
|
||||
|
||||
### Option 1: npx (Fastest - No Installation!) 🚀
|
||||
|
||||
**Prerequisites:** [Node.js](https://nodejs.org/) installed on your system
|
||||
|
||||
```bash
|
||||
@@ -98,7 +117,7 @@ Add to Claude Desktop config:
|
||||
|
||||
**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
|
||||
|
||||
@@ -345,27 +364,6 @@ environment:
|
||||
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
|
||||
|
||||
<div align="center">
|
||||
@@ -386,7 +384,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
|
||||
|
||||
@@ -444,7 +442,7 @@ Add to Claude Desktop config:
|
||||
|
||||
> 💡 Tip: If you’re 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)
|
||||
|
||||
@@ -524,7 +522,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.
|
||||
|
||||
❌ 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
|
||||
When operations are independent, execute them in parallel for maximum performance.
|
||||
@@ -536,7 +534,7 @@ When operations are independent, execute them in parallel for maximum performanc
|
||||
ALWAYS check templates before building from scratch (2,709 available).
|
||||
|
||||
### 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
|
||||
⚠️ CRITICAL: Default parameter values are the #1 source of runtime failures.
|
||||
@@ -547,10 +545,10 @@ ALWAYS explicitly configure ALL parameters that control node behavior.
|
||||
1. **Start**: Call `tools_documentation()` for best practices
|
||||
|
||||
2. **Template Discovery Phase** (FIRST - parallel when searching multiple)
|
||||
- `search_templates_by_metadata({complexity: "simple"})` - Smart filtering
|
||||
- `get_templates_for_task('webhook_processing')` - Curated by task
|
||||
- `search_templates('slack notification')` - Text search
|
||||
- `list_node_templates(['n8n-nodes-base.slack'])` - By node type
|
||||
- `search_templates({searchMode: 'by_metadata', complexity: 'simple'})` - Smart filtering
|
||||
- `search_templates({searchMode: 'by_task', task: 'webhook_processing'})` - Curated by task
|
||||
- `search_templates({query: 'slack notification'})` - Text search (default searchMode='keyword')
|
||||
- `search_templates({searchMode: 'by_nodes', nodeTypes: ['n8n-nodes-base.slack']})` - By node type
|
||||
|
||||
**Filtering strategies**:
|
||||
- Beginners: `complexity: "simple"` + `maxSetupMinutes: 30`
|
||||
@@ -561,18 +559,20 @@ ALWAYS explicitly configure ALL parameters that control node behavior.
|
||||
3. **Node Discovery** (if no suitable template - parallel execution)
|
||||
- Think deeply about requirements. Ask clarifying questions if unclear.
|
||||
- `search_nodes({query: 'keyword', includeExamples: true})` - Parallel for multiple nodes
|
||||
- `list_nodes({category: 'trigger'})` - Browse by category
|
||||
- `list_ai_tools()` - AI-capable nodes
|
||||
- `search_nodes({query: 'trigger'})` - Browse triggers
|
||||
- `search_nodes({query: 'AI agent langchain'})` - AI-capable nodes
|
||||
|
||||
4. **Configuration Phase** (parallel for multiple nodes)
|
||||
- `get_node_essentials(nodeType, {includeExamples: true})` - 10-20 key properties
|
||||
- `search_node_properties(nodeType, 'auth')` - Find specific properties
|
||||
- `get_node_documentation(nodeType)` - Human-readable docs
|
||||
- `get_node({nodeType, detail: 'standard', includeExamples: true})` - Essential properties (default)
|
||||
- `get_node({nodeType, detail: 'minimal'})` - Basic metadata only (~200 tokens)
|
||||
- `get_node({nodeType, detail: 'full'})` - Complete information (~3000-8000 tokens)
|
||||
- `get_node({nodeType, mode: 'search_properties', propertyQuery: 'auth'})` - Find specific properties
|
||||
- `get_node({nodeType, mode: 'docs'})` - Human-readable markdown documentation
|
||||
- Show workflow architecture to user for approval before proceeding
|
||||
|
||||
5. **Validation Phase** (parallel for multiple nodes)
|
||||
- `validate_node_minimal(nodeType, config)` - Quick required fields check
|
||||
- `validate_node_operation(nodeType, config, 'runtime')` - Full validation with fixes
|
||||
- `validate_node({nodeType, config, mode: 'minimal'})` - Quick required fields check
|
||||
- `validate_node({nodeType, config, mode: 'full', profile: 'runtime'})` - Full validation with fixes
|
||||
- Fix ALL errors before proceeding
|
||||
|
||||
6. **Building Phase**
|
||||
@@ -612,15 +612,15 @@ Default values cause runtime failures. Example:
|
||||
### ⚠️ Example Availability
|
||||
`includeExamples: true` returns real configurations from workflow templates.
|
||||
- Coverage varies by node popularity
|
||||
- When no examples available, use `get_node_essentials` + `validate_node_minimal`
|
||||
- When no examples available, use `get_node` + `validate_node({mode: 'minimal'})`
|
||||
|
||||
## Validation Strategy
|
||||
|
||||
### 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)
|
||||
`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)
|
||||
`validate_workflow(workflow)` - Connections, expressions, AI tools
|
||||
@@ -628,7 +628,7 @@ Default values cause runtime failures. Example:
|
||||
### Level 4 - Post-Deployment
|
||||
1. `n8n_validate_workflow({id})` - Validate deployed workflow
|
||||
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
|
||||
|
||||
@@ -774,12 +774,13 @@ Use the same four-parameter format:
|
||||
```
|
||||
// STEP 1: Template Discovery (parallel execution)
|
||||
[Silent execution]
|
||||
search_templates_by_metadata({
|
||||
search_templates({
|
||||
searchMode: 'by_metadata',
|
||||
requiredService: 'slack',
|
||||
complexity: 'simple',
|
||||
targetAudience: 'marketers'
|
||||
})
|
||||
get_templates_for_task('slack_integration')
|
||||
search_templates({searchMode: 'by_task', task: 'slack_integration'})
|
||||
|
||||
// STEP 2: Use template
|
||||
get_template(templateId, {mode: 'full'})
|
||||
@@ -798,17 +799,17 @@ Validation: ✅ All checks passed"
|
||||
// STEP 1: Discovery (parallel execution)
|
||||
[Silent execution]
|
||||
search_nodes({query: 'slack', includeExamples: true})
|
||||
list_nodes({category: 'communication'})
|
||||
search_nodes({query: 'communication trigger'})
|
||||
|
||||
// STEP 2: Configuration (parallel execution)
|
||||
[Silent execution]
|
||||
get_node_essentials('n8n-nodes-base.slack', {includeExamples: true})
|
||||
get_node_essentials('n8n-nodes-base.webhook', {includeExamples: true})
|
||||
get_node({nodeType: 'n8n-nodes-base.slack', detail: 'standard', includeExamples: true})
|
||||
get_node({nodeType: 'n8n-nodes-base.webhook', detail: 'standard', includeExamples: true})
|
||||
|
||||
// STEP 3: Validation (parallel execution)
|
||||
[Silent execution]
|
||||
validate_node_minimal('n8n-nodes-base.slack', config)
|
||||
validate_node_operation('n8n-nodes-base.slack', fullConfig, 'runtime')
|
||||
validate_node({nodeType: 'n8n-nodes-base.slack', config, mode: 'minimal'})
|
||||
validate_node({nodeType: 'n8n-nodes-base.slack', config: fullConfig, mode: 'full', profile: 'runtime'})
|
||||
|
||||
// STEP 4: Build
|
||||
// Construct workflow with validated configs
|
||||
@@ -860,7 +861,7 @@ n8n_update_partial_workflow({
|
||||
- **Only when necessary** - Use code node as last resort
|
||||
- **AI tool capability** - ANY node can be an AI tool (not just marked ones)
|
||||
|
||||
### Most Popular n8n Nodes (for get_node_essentials):
|
||||
### Most Popular n8n Nodes (for get_node):
|
||||
|
||||
1. **n8n-nodes-base.code** - JavaScript/Python scripting
|
||||
2. **n8n-nodes-base.httpRequest** - HTTP API calls
|
||||
@@ -924,7 +925,7 @@ When Claude, Anthropic's AI assistant, tested n8n-MCP, the results were transfor
|
||||
|
||||
**Without MCP:** "I was basically playing a guessing game. 'Is it `scheduleTrigger` or `schedule`? Does it take `interval` or `rule`?' I'd write what seemed logical, but n8n has its own conventions that you can't just intuit. I made six different configuration errors in a simple HackerNews scraper."
|
||||
|
||||
**With MCP:** "Everything just... worked. Instead of guessing, I could ask `get_node_essentials()` and get exactly what I needed - not a 100KB JSON dump, but the actual 5-10 properties that matter. What took 45 minutes now takes 3 minutes."
|
||||
**With MCP:** "Everything just... worked. Instead of guessing, I could ask `get_node()` and get exactly what I needed - not a 100KB JSON dump, but the actual properties that matter. What took 45 minutes now takes 3 minutes."
|
||||
|
||||
**The Real Value:** "It's about confidence. When you're building automation workflows, uncertainty is expensive. One wrong parameter and your workflow fails at 3 AM. With MCP, I could validate my configuration before deployment. That's not just time saved - that's peace of mind."
|
||||
|
||||
@@ -934,94 +935,108 @@ When Claude, Anthropic's AI assistant, tested n8n-MCP, the results were transfor
|
||||
|
||||
Once connected, Claude can use these powerful tools:
|
||||
|
||||
### Core Tools
|
||||
### Core Tools (7 tools)
|
||||
- **`tools_documentation`** - Get documentation for any MCP tool (START HERE!)
|
||||
- **`list_nodes`** - List all n8n nodes with filtering options
|
||||
- **`get_node_info`** - Get comprehensive information about a specific node
|
||||
- **`get_node_essentials`** - Get only essential properties (10-20 instead of 200+). Use `includeExamples: true` to get top 3 real-world configurations from popular templates
|
||||
- **`search_nodes`** - Full-text search across all node documentation. Use `includeExamples: true` to get top 2 real-world configurations per node from templates
|
||||
- **`search_node_properties`** - Find specific properties within nodes
|
||||
- **`list_ai_tools`** - List all AI-capable nodes (ANY node can be used as AI tool!)
|
||||
- **`get_node_as_tool_info`** - Get guidance on using any node as an AI tool
|
||||
- **`search_nodes`** - Full-text search across all nodes. Use `includeExamples: true` for real-world configurations
|
||||
- **`get_node`** - Unified node information tool with multiple modes (v2.26.0):
|
||||
- **Info mode** (default): `detail: 'minimal'|'standard'|'full'`, `includeExamples: true`
|
||||
- **Docs mode**: `mode: 'docs'` - Human-readable markdown documentation
|
||||
- **Property search**: `mode: 'search_properties'`, `propertyQuery: 'auth'`
|
||||
- **Versions**: `mode: 'versions'|'compare'|'breaking'|'migrations'`
|
||||
- **`validate_node`** - Unified node validation (v2.26.0):
|
||||
- `mode: 'minimal'` - Quick required fields check (<100ms)
|
||||
- `mode: 'full'` - Comprehensive validation with profiles (minimal, runtime, ai-friendly, strict)
|
||||
- **`validate_workflow`** - Complete workflow validation including AI Agent validation
|
||||
- **`search_templates`** - Unified template search (v2.26.0):
|
||||
- `searchMode: 'keyword'` (default) - Text search with `query` parameter
|
||||
- `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
|
||||
- **`list_templates`** - Browse all templates with descriptions and optional metadata (2,709 templates)
|
||||
- **`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.
|
||||
### n8n Management Tools (13 tools - Requires API Configuration)
|
||||
These tools require `N8N_API_URL` and `N8N_API_KEY` in your configuration.
|
||||
|
||||
#### Workflow Management
|
||||
- **`n8n_create_workflow`** - Create new workflows with nodes and connections
|
||||
- **`n8n_get_workflow`** - Get complete workflow by ID
|
||||
- **`n8n_get_workflow_details`** - Get workflow with execution statistics
|
||||
- **`n8n_get_workflow_structure`** - Get simplified workflow structure
|
||||
- **`n8n_get_workflow_minimal`** - Get minimal workflow info (ID, name, active status)
|
||||
- **`n8n_get_workflow`** - Unified workflow retrieval (v2.26.0):
|
||||
- `mode: 'full'` (default) - Complete workflow JSON
|
||||
- `mode: 'details'` - Include execution statistics
|
||||
- `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_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_list_workflows`** - List workflows with filtering and pagination
|
||||
- **`n8n_validate_workflow`** - Validate workflows already in n8n by ID (NEW in v2.6.3)
|
||||
- **`n8n_autofix_workflow`** - Automatically fix common workflow errors (NEW in v2.13.0!)
|
||||
- **`n8n_workflow_versions`** - Manage workflow version history and rollback (NEW in v2.22.0!)
|
||||
- **`n8n_validate_workflow`** - Validate workflows in n8n by ID
|
||||
- **`n8n_autofix_workflow`** - Automatically fix common workflow errors
|
||||
- **`n8n_workflow_versions`** - Manage version history and rollback
|
||||
- **`n8n_deploy_template`** - Deploy templates from n8n.io directly to your instance (NEW!)
|
||||
|
||||
#### Execution Management
|
||||
- **`n8n_trigger_webhook_workflow`** - Trigger workflows via webhook URL
|
||||
- **`n8n_get_execution`** - Get execution details by ID
|
||||
- **`n8n_list_executions`** - List executions with status filtering
|
||||
- **`n8n_delete_execution`** - Delete execution records
|
||||
- **`n8n_executions`** - Unified execution management (v2.26.0):
|
||||
- `action: 'list'` - List executions with status filtering
|
||||
- `action: 'get'` - Get execution details by ID
|
||||
- `action: 'delete'` - Delete execution records
|
||||
|
||||
#### System Tools
|
||||
- **`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
|
||||
|
||||
```typescript
|
||||
// Get essentials with real-world examples from templates
|
||||
get_node_essentials({
|
||||
// Get node info with different detail levels
|
||||
get_node({
|
||||
nodeType: "nodes-base.httpRequest",
|
||||
includeExamples: true // Returns top 3 configs from popular templates
|
||||
detail: "standard", // Default: Essential properties
|
||||
includeExamples: true // Include real-world examples from templates
|
||||
})
|
||||
|
||||
// Get documentation
|
||||
get_node({
|
||||
nodeType: "nodes-base.slack",
|
||||
mode: "docs" // Human-readable markdown documentation
|
||||
})
|
||||
|
||||
// Search for specific properties
|
||||
get_node({
|
||||
nodeType: "nodes-base.httpRequest",
|
||||
mode: "search_properties",
|
||||
propertyQuery: "authentication"
|
||||
})
|
||||
|
||||
// Version history and breaking changes
|
||||
get_node({
|
||||
nodeType: "nodes-base.httpRequest",
|
||||
mode: "versions" // View all versions with summary
|
||||
})
|
||||
|
||||
// Search nodes with configuration examples
|
||||
search_nodes({
|
||||
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_operation({
|
||||
// Validate node configuration
|
||||
validate_node({
|
||||
nodeType: "nodes-base.httpRequest",
|
||||
config: { method: "POST", url: "..." },
|
||||
profile: "runtime" // or "minimal", "ai-friendly", "strict"
|
||||
mode: "full",
|
||||
profile: "runtime" // or "minimal", "ai-friendly", "strict"
|
||||
})
|
||||
|
||||
// Quick required field check
|
||||
validate_node_minimal({
|
||||
validate_node({
|
||||
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"
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1114,43 +1129,7 @@ Current database coverage (n8n v1.117.2):
|
||||
|
||||
## 🔄 Recent Updates
|
||||
|
||||
### v2.22.19 - Critical Bug Fix
|
||||
**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
|
||||
```
|
||||
|
||||
See [CHANGELOG.md](./CHANGELOG.md) for complete version history and recent changes.
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
|
||||
BIN
data/nodes.db
BIN
data/nodes.db
Binary file not shown.
757
docs/SESSION_PERSISTENCE.md
Normal file
757
docs/SESSION_PERSISTENCE.md
Normal file
@@ -0,0 +1,757 @@
|
||||
# Session Persistence API - Production Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The Session Persistence API enables zero-downtime container deployments in multi-tenant n8n-mcp environments. It allows you to export active MCP session state before shutdown and restore it after restart, maintaining session continuity across container lifecycle events.
|
||||
|
||||
**Version:** 2.24.1+
|
||||
**Status:** Production-ready
|
||||
**Use Cases:** Multi-tenant SaaS, Kubernetes deployments, container orchestration, rolling updates
|
||||
|
||||
## Architecture
|
||||
|
||||
### Session State Components
|
||||
|
||||
Each persisted session contains:
|
||||
|
||||
1. **Session Metadata**
|
||||
- `sessionId`: Unique session identifier (UUID v4)
|
||||
- `createdAt`: ISO 8601 timestamp of session creation
|
||||
- `lastAccess`: ISO 8601 timestamp of last activity
|
||||
|
||||
2. **Instance Context**
|
||||
- `n8nApiUrl`: n8n instance API endpoint
|
||||
- `n8nApiKey`: n8n API authentication key (plaintext)
|
||||
- `instanceId`: Optional tenant/instance identifier
|
||||
- `sessionId`: Optional session-specific identifier
|
||||
- `metadata`: Optional custom application data
|
||||
|
||||
3. **Dormant Session Pattern**
|
||||
- Transport and MCP server objects are NOT persisted
|
||||
- Recreated automatically on first request after restore
|
||||
- Reduces memory footprint during restore
|
||||
|
||||
## API Reference
|
||||
|
||||
### N8NMCPEngine.exportSessionState()
|
||||
|
||||
Exports all active session state for persistence before shutdown.
|
||||
|
||||
```typescript
|
||||
exportSessionState(): SessionState[]
|
||||
```
|
||||
|
||||
**Returns:** Array of session state objects containing metadata and credentials
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const sessions = engine.exportSessionState();
|
||||
// sessions = [
|
||||
// {
|
||||
// sessionId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
// metadata: {
|
||||
// createdAt: '2025-11-24T10:30:00.000Z',
|
||||
// lastAccess: '2025-11-24T17:15:32.000Z'
|
||||
// },
|
||||
// context: {
|
||||
// n8nApiUrl: 'https://tenant1.n8n.cloud',
|
||||
// n8nApiKey: 'n8n_api_...',
|
||||
// instanceId: 'tenant-123',
|
||||
// metadata: { userId: 'user-456' }
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
```
|
||||
|
||||
**Key Behaviors:**
|
||||
- Exports only non-expired sessions (within sessionTimeout)
|
||||
- Detects and warns about duplicate session IDs
|
||||
- Logs security event with session count
|
||||
- Returns empty array if no active sessions
|
||||
|
||||
### N8NMCPEngine.restoreSessionState()
|
||||
|
||||
Restores sessions from previously exported state after container restart.
|
||||
|
||||
```typescript
|
||||
restoreSessionState(sessions: SessionState[]): number
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `sessions`: Array of session state objects from `exportSessionState()`
|
||||
|
||||
**Returns:** Number of sessions successfully restored
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const sessions = await loadFromEncryptedStorage();
|
||||
const count = engine.restoreSessionState(sessions);
|
||||
console.log(`Restored ${count} sessions`);
|
||||
```
|
||||
|
||||
**Key Behaviors:**
|
||||
- Validates session metadata (timestamps, required fields)
|
||||
- Skips expired sessions (age > sessionTimeout)
|
||||
- Skips duplicate sessions (idempotent)
|
||||
- Respects MAX_SESSIONS limit (100 per container)
|
||||
- Recreates transports/servers lazily on first request
|
||||
- Logs security events for restore success/failure
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Critical: Encrypt Before Storage
|
||||
|
||||
**The exported session state contains plaintext n8n API keys.** You MUST encrypt this data before persisting to disk.
|
||||
|
||||
```typescript
|
||||
// ❌ NEVER DO THIS
|
||||
await fs.writeFile('sessions.json', JSON.stringify(sessions));
|
||||
|
||||
// ✅ ALWAYS ENCRYPT
|
||||
const encrypted = await encryptSessionData(sessions, encryptionKey);
|
||||
await saveToSecureStorage(encrypted);
|
||||
```
|
||||
|
||||
### Recommended Encryption Approach
|
||||
|
||||
```typescript
|
||||
import crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* Encrypt session data using AES-256-GCM
|
||||
*/
|
||||
async function encryptSessionData(
|
||||
sessions: SessionState[],
|
||||
encryptionKey: Buffer
|
||||
): Promise<string> {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey, iv);
|
||||
|
||||
const json = JSON.stringify(sessions);
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(json, 'utf8'),
|
||||
cipher.final()
|
||||
]);
|
||||
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// Return base64: iv:authTag:encrypted
|
||||
return [
|
||||
iv.toString('base64'),
|
||||
authTag.toString('base64'),
|
||||
encrypted.toString('base64')
|
||||
].join(':');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt session data
|
||||
*/
|
||||
async function decryptSessionData(
|
||||
encryptedData: string,
|
||||
encryptionKey: Buffer
|
||||
): Promise<SessionState[]> {
|
||||
const [ivB64, authTagB64, encryptedB64] = encryptedData.split(':');
|
||||
|
||||
const iv = Buffer.from(ivB64, 'base64');
|
||||
const authTag = Buffer.from(authTagB64, 'base64');
|
||||
const encrypted = Buffer.from(encryptedB64, 'base64');
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-256-gcm', encryptionKey, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(encrypted),
|
||||
decipher.final()
|
||||
]);
|
||||
|
||||
return JSON.parse(decrypted.toString('utf8'));
|
||||
}
|
||||
```
|
||||
|
||||
### Key Management
|
||||
|
||||
Store encryption keys securely:
|
||||
- **Kubernetes:** Use Kubernetes Secrets with encryption at rest
|
||||
- **AWS:** Use AWS Secrets Manager or Parameter Store with KMS
|
||||
- **Azure:** Use Azure Key Vault
|
||||
- **GCP:** Use Secret Manager
|
||||
- **Local Dev:** Use environment variables (NEVER commit to git)
|
||||
|
||||
### Security Logging
|
||||
|
||||
All session persistence operations are logged with `[SECURITY]` prefix:
|
||||
|
||||
```
|
||||
[SECURITY] session_export { timestamp, count }
|
||||
[SECURITY] session_restore { timestamp, sessionId, instanceId }
|
||||
[SECURITY] session_restore_failed { timestamp, sessionId, reason }
|
||||
[SECURITY] max_sessions_reached { timestamp, count }
|
||||
```
|
||||
|
||||
Monitor these logs in production for audit trails and security analysis.
|
||||
|
||||
## Implementation Examples
|
||||
|
||||
### 1. Express.js Multi-Tenant Backend
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
import { N8NMCPEngine } from 'n8n-mcp';
|
||||
|
||||
const app = express();
|
||||
const engine = new N8NMCPEngine({
|
||||
sessionTimeout: 1800000, // 30 minutes
|
||||
logLevel: 'info'
|
||||
});
|
||||
|
||||
// Startup: Restore sessions from encrypted storage
|
||||
async function startup() {
|
||||
try {
|
||||
const encrypted = await redis.get('mcp:sessions');
|
||||
if (encrypted) {
|
||||
const sessions = await decryptSessionData(
|
||||
encrypted,
|
||||
process.env.ENCRYPTION_KEY
|
||||
);
|
||||
const count = engine.restoreSessionState(sessions);
|
||||
console.log(`Restored ${count} sessions`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to restore sessions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown: Export sessions to encrypted storage
|
||||
async function shutdown() {
|
||||
try {
|
||||
const sessions = engine.exportSessionState();
|
||||
const encrypted = await encryptSessionData(
|
||||
sessions,
|
||||
process.env.ENCRYPTION_KEY
|
||||
);
|
||||
await redis.set('mcp:sessions', encrypted, 'EX', 3600); // 1 hour TTL
|
||||
console.log(`Exported ${sessions.length} sessions`);
|
||||
} catch (error) {
|
||||
console.error('Failed to export sessions:', error);
|
||||
}
|
||||
|
||||
await engine.shutdown();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('SIGINT', shutdown);
|
||||
|
||||
// Start server
|
||||
await startup();
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
### 2. Kubernetes Deployment with Init Container
|
||||
|
||||
**deployment.yaml:**
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: n8n-mcp
|
||||
spec:
|
||||
replicas: 3
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 1
|
||||
maxSurge: 1
|
||||
template:
|
||||
spec:
|
||||
initContainers:
|
||||
- name: restore-sessions
|
||||
image: your-app:latest
|
||||
command: ['/app/restore-sessions.sh']
|
||||
env:
|
||||
- name: ENCRYPTION_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mcp-secrets
|
||||
key: encryption-key
|
||||
- name: REDIS_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mcp-secrets
|
||||
key: redis-url
|
||||
volumeMounts:
|
||||
- name: sessions
|
||||
mountPath: /sessions
|
||||
|
||||
containers:
|
||||
- name: mcp-server
|
||||
image: your-app:latest
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command: ['/app/export-sessions.sh']
|
||||
env:
|
||||
- name: ENCRYPTION_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mcp-secrets
|
||||
key: encryption-key
|
||||
- name: SESSION_TIMEOUT
|
||||
value: "1800000"
|
||||
volumeMounts:
|
||||
- name: sessions
|
||||
mountPath: /sessions
|
||||
|
||||
# Graceful shutdown configuration
|
||||
terminationGracePeriodSeconds: 30
|
||||
|
||||
volumes:
|
||||
- name: sessions
|
||||
emptyDir: {}
|
||||
```
|
||||
|
||||
**restore-sessions.sh:**
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Restoring sessions from Redis..."
|
||||
|
||||
# Fetch encrypted sessions from Redis
|
||||
ENCRYPTED=$(redis-cli -u "$REDIS_URL" GET "mcp:sessions:${HOSTNAME}")
|
||||
|
||||
if [ -n "$ENCRYPTED" ]; then
|
||||
echo "$ENCRYPTED" > /sessions/encrypted.txt
|
||||
echo "Sessions fetched, will be restored on startup"
|
||||
else
|
||||
echo "No sessions to restore"
|
||||
fi
|
||||
```
|
||||
|
||||
**export-sessions.sh:**
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Exporting sessions to Redis..."
|
||||
|
||||
# Trigger session export via HTTP endpoint
|
||||
curl -X POST http://localhost:3000/internal/export-sessions
|
||||
|
||||
echo "Sessions exported successfully"
|
||||
```
|
||||
|
||||
### 3. Docker Compose with Redis
|
||||
|
||||
**docker-compose.yml:**
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
n8n-mcp:
|
||||
build: .
|
||||
environment:
|
||||
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- SESSION_TIMEOUT=1800000
|
||||
depends_on:
|
||||
- redis
|
||||
volumes:
|
||||
- ./data:/data
|
||||
deploy:
|
||||
replicas: 2
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
order: start-first
|
||||
stop_grace_period: 30s
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
command: redis-server --appendonly yes
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
```
|
||||
|
||||
**Application code:**
|
||||
```typescript
|
||||
import { N8NMCPEngine } from 'n8n-mcp';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
const redis = new Redis(process.env.REDIS_URL);
|
||||
const engine = new N8NMCPEngine();
|
||||
|
||||
// Export endpoint (called by preStop hook)
|
||||
app.post('/internal/export-sessions', async (req, res) => {
|
||||
try {
|
||||
const sessions = engine.exportSessionState();
|
||||
const encrypted = await encryptSessionData(
|
||||
sessions,
|
||||
Buffer.from(process.env.ENCRYPTION_KEY, 'hex')
|
||||
);
|
||||
|
||||
// Store with hostname as key for per-container tracking
|
||||
await redis.set(
|
||||
`mcp:sessions:${os.hostname()}`,
|
||||
encrypted,
|
||||
'EX',
|
||||
3600
|
||||
);
|
||||
|
||||
res.json({ exported: sessions.length });
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error);
|
||||
res.status(500).json({ error: 'Export failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Restore on startup
|
||||
async function startup() {
|
||||
const encrypted = await redis.get(`mcp:sessions:${os.hostname()}`);
|
||||
if (encrypted) {
|
||||
const sessions = await decryptSessionData(
|
||||
encrypted,
|
||||
Buffer.from(process.env.ENCRYPTION_KEY, 'hex')
|
||||
);
|
||||
const count = engine.restoreSessionState(sessions);
|
||||
console.log(`Restored ${count} sessions`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Session Timeout Configuration
|
||||
|
||||
Choose appropriate timeout based on use case:
|
||||
|
||||
```typescript
|
||||
const engine = new N8NMCPEngine({
|
||||
sessionTimeout: 1800000 // 30 minutes (recommended default)
|
||||
});
|
||||
|
||||
// Development: 5 minutes
|
||||
sessionTimeout: 300000
|
||||
|
||||
// Production SaaS: 30-60 minutes
|
||||
sessionTimeout: 1800000 - 3600000
|
||||
|
||||
// Long-running workflows: 2-4 hours
|
||||
sessionTimeout: 7200000 - 14400000
|
||||
```
|
||||
|
||||
### 2. Storage Backend Selection
|
||||
|
||||
**Redis (Recommended for Production)**
|
||||
- Fast read/write for session data
|
||||
- TTL support for automatic cleanup
|
||||
- Pub/sub for distributed coordination
|
||||
- Atomic operations for consistency
|
||||
|
||||
**Database (PostgreSQL/MySQL)**
|
||||
- JSONB column for session state
|
||||
- Good for audit requirements
|
||||
- Slower than Redis
|
||||
- Requires periodic cleanup
|
||||
|
||||
**S3/Cloud Storage**
|
||||
- Good for disaster recovery backups
|
||||
- Not suitable for hot session restore
|
||||
- High latency
|
||||
- Good for long-term session archival
|
||||
|
||||
### 3. Monitoring and Alerting
|
||||
|
||||
Monitor these metrics:
|
||||
|
||||
```typescript
|
||||
// Session export metrics
|
||||
const sessions = engine.exportSessionState();
|
||||
metrics.gauge('mcp.sessions.exported', sessions.length);
|
||||
metrics.gauge('mcp.sessions.export_size_kb',
|
||||
JSON.stringify(sessions).length / 1024
|
||||
);
|
||||
|
||||
// Session restore metrics
|
||||
const restored = engine.restoreSessionState(sessions);
|
||||
metrics.gauge('mcp.sessions.restored', restored);
|
||||
metrics.gauge('mcp.sessions.restore_success_rate',
|
||||
restored / sessions.length
|
||||
);
|
||||
|
||||
// Runtime metrics
|
||||
const info = engine.getSessionInfo();
|
||||
metrics.gauge('mcp.sessions.active', info.active ? 1 : 0);
|
||||
metrics.gauge('mcp.sessions.age_seconds', info.age || 0);
|
||||
```
|
||||
|
||||
Alert on:
|
||||
- Export failures (should be rare)
|
||||
- Low restore success rate (<95%)
|
||||
- MAX_SESSIONS limit reached
|
||||
- High session age (potential leaks)
|
||||
|
||||
### 4. Graceful Shutdown Timing
|
||||
|
||||
Ensure sufficient time for session export:
|
||||
|
||||
```typescript
|
||||
// Kubernetes terminationGracePeriodSeconds
|
||||
terminationGracePeriodSeconds: 30 // 30 seconds minimum
|
||||
|
||||
// Docker stop timeout
|
||||
docker run --stop-timeout 30 your-image
|
||||
|
||||
// Process signal handling
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('SIGTERM received, starting graceful shutdown...');
|
||||
|
||||
// 1. Stop accepting new requests (5s)
|
||||
await server.close();
|
||||
|
||||
// 2. Wait for in-flight requests (10s)
|
||||
await waitForInFlightRequests(10000);
|
||||
|
||||
// 3. Export sessions (5s)
|
||||
const sessions = engine.exportSessionState();
|
||||
await saveEncryptedSessions(sessions);
|
||||
|
||||
// 4. Cleanup (5s)
|
||||
await engine.shutdown();
|
||||
|
||||
// 5. Exit (5s buffer)
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Idempotency Handling
|
||||
|
||||
Sessions can be restored multiple times safely:
|
||||
|
||||
```typescript
|
||||
// First restore
|
||||
const count1 = engine.restoreSessionState(sessions);
|
||||
// count1 = 5
|
||||
|
||||
// Second restore (same sessions)
|
||||
const count2 = engine.restoreSessionState(sessions);
|
||||
// count2 = 0 (all already exist)
|
||||
```
|
||||
|
||||
This is safe for:
|
||||
- Init container retries
|
||||
- Manual recovery operations
|
||||
- Disaster recovery scenarios
|
||||
|
||||
### 6. Multi-Instance Coordination
|
||||
|
||||
For multiple container instances:
|
||||
|
||||
```typescript
|
||||
// Option 1: Per-instance storage (simple)
|
||||
const key = `mcp:sessions:${instance.hostname}`;
|
||||
|
||||
// Option 2: Centralized with distributed lock (advanced)
|
||||
const lock = await acquireLock('mcp:session-export');
|
||||
try {
|
||||
const allSessions = await getAllInstanceSessions();
|
||||
await saveToBackup(allSessions);
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Memory Usage
|
||||
|
||||
```typescript
|
||||
// Each session: ~1-2 KB in memory
|
||||
// 100 sessions: ~100-200 KB
|
||||
// 1000 sessions: ~1-2 MB
|
||||
|
||||
// Export serialized size
|
||||
const sessions = engine.exportSessionState();
|
||||
const sizeKB = JSON.stringify(sessions).length / 1024;
|
||||
console.log(`Export size: ${sizeKB.toFixed(2)} KB`);
|
||||
```
|
||||
|
||||
### Export/Restore Speed
|
||||
|
||||
```typescript
|
||||
// Export: O(n) where n = active sessions
|
||||
// Typical: 50-100 sessions in <10ms
|
||||
|
||||
// Restore: O(n) with validation
|
||||
// Typical: 50-100 sessions in 20-50ms
|
||||
|
||||
// Factor in encryption:
|
||||
// AES-256-GCM: ~1ms per 100 sessions
|
||||
```
|
||||
|
||||
### MAX_SESSIONS Limit
|
||||
|
||||
Hard limit: 100 sessions per container
|
||||
|
||||
```typescript
|
||||
// Restore respects limit
|
||||
const sessions = createSessions(150); // 150 sessions
|
||||
const restored = engine.restoreSessionState(sessions);
|
||||
// restored = 100 (only first 100 restored)
|
||||
```
|
||||
|
||||
For >100 sessions per tenant:
|
||||
- Deploy multiple containers
|
||||
- Use session routing/sharding
|
||||
- Implement session affinity
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: No sessions restored
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Restored 0 sessions
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
1. All sessions expired (age > sessionTimeout)
|
||||
2. Invalid date format in metadata
|
||||
3. Missing required context fields
|
||||
|
||||
**Debug:**
|
||||
```typescript
|
||||
const sessions = await loadFromEncryptedStorage();
|
||||
console.log('Loaded sessions:', sessions.length);
|
||||
|
||||
// Check individual sessions
|
||||
sessions.forEach((s, i) => {
|
||||
const age = Date.now() - new Date(s.metadata.lastAccess).getTime();
|
||||
console.log(`Session ${i}: age=${age}ms, expired=${age > sessionTimeout}`);
|
||||
});
|
||||
```
|
||||
|
||||
### Issue: Restore fails with "invalid context"
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
[SECURITY] session_restore_failed { sessionId: '...', reason: 'invalid context: ...' }
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
1. Missing n8nApiUrl or n8nApiKey
|
||||
2. Invalid URL format
|
||||
3. Corrupted session data
|
||||
|
||||
**Fix:**
|
||||
```typescript
|
||||
// Validate before restore
|
||||
const valid = sessions.filter(s => {
|
||||
if (!s.context?.n8nApiUrl || !s.context?.n8nApiKey) {
|
||||
console.warn(`Invalid session ${s.sessionId}: missing credentials`);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
new URL(s.context.n8nApiUrl); // Validate URL
|
||||
return true;
|
||||
} catch {
|
||||
console.warn(`Invalid session ${s.sessionId}: malformed URL`);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const count = engine.restoreSessionState(valid);
|
||||
```
|
||||
|
||||
### Issue: MAX_SESSIONS limit hit
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Reached MAX_SESSIONS limit (100), skipping remaining sessions
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Scale horizontally (more containers)
|
||||
2. Implement session sharding
|
||||
3. Reduce sessionTimeout
|
||||
4. Clean up inactive sessions
|
||||
|
||||
```typescript
|
||||
// Pre-filter by activity
|
||||
const recentSessions = sessions.filter(s => {
|
||||
const age = Date.now() - new Date(s.metadata.lastAccess).getTime();
|
||||
return age < 600000; // Only restore sessions active in last 10 min
|
||||
});
|
||||
|
||||
const count = engine.restoreSessionState(recentSessions);
|
||||
```
|
||||
|
||||
### Issue: Duplicate session IDs
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Duplicate sessionId detected during export: 550e8400-...
|
||||
```
|
||||
|
||||
**Cause:** Bug in session management logic
|
||||
|
||||
**Fix:** This is a warning, not an error. The duplicate is automatically skipped. If persistent, investigate session creation logic.
|
||||
|
||||
### Issue: High memory usage after restore
|
||||
|
||||
**Symptoms:** Container OOM after restoring many sessions
|
||||
|
||||
**Cause:** Too many sessions for container resources
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
// Restore in batches
|
||||
async function restoreInBatches(sessions: SessionState[], batchSize = 25) {
|
||||
let totalRestored = 0;
|
||||
|
||||
for (let i = 0; i < sessions.length; i += batchSize) {
|
||||
const batch = sessions.slice(i, i + batchSize);
|
||||
const count = engine.restoreSessionState(batch);
|
||||
totalRestored += count;
|
||||
|
||||
// Wait for GC between batches
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
return totalRestored;
|
||||
}
|
||||
```
|
||||
|
||||
## Version Compatibility
|
||||
|
||||
| Feature | Version | Status |
|
||||
|---------|---------|--------|
|
||||
| exportSessionState() | 2.3.0+ | Stable |
|
||||
| restoreSessionState() | 2.3.0+ | Stable |
|
||||
| Security logging | 2.24.1+ | Stable |
|
||||
| Duplicate detection | 2.24.1+ | Stable |
|
||||
| Race condition fix | 2.24.1+ | Stable |
|
||||
| Date validation | 2.24.1+ | Stable |
|
||||
| Optional instanceId | 2.24.1+ | Stable |
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [HTTP Deployment Guide](./HTTP_DEPLOYMENT.md) - Multi-tenant HTTP server setup
|
||||
- [Library Usage Guide](./LIBRARY_USAGE.md) - Embedding n8n-mcp in your app
|
||||
- [Docker Guide](./DOCKER_README.md) - Container deployment
|
||||
- [Flexible Instance Configuration](./FLEXIBLE_INSTANCE_CONFIGURATION.md) - Multi-tenant patterns
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- GitHub Issues: https://github.com/czlonkowski/n8n-mcp/issues
|
||||
- Documentation: https://github.com/czlonkowski/n8n-mcp#readme
|
||||
|
||||
---
|
||||
|
||||
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
|
||||
1054
package-lock.json
generated
1054
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.23.0",
|
||||
"version": "2.27.0",
|
||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -140,21 +140,21 @@
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@n8n/n8n-nodes-langchain": "^1.119.1",
|
||||
"@modelcontextprotocol/sdk": "1.20.1",
|
||||
"@n8n/n8n-nodes-langchain": "^1.120.1",
|
||||
"@supabase/supabase-js": "^2.57.4",
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"lru-cache": "^11.2.1",
|
||||
"n8n": "^1.120.3",
|
||||
"n8n-core": "^1.119.2",
|
||||
"n8n-workflow": "^1.117.0",
|
||||
"n8n": "^1.121.2",
|
||||
"n8n-core": "^1.120.1",
|
||||
"n8n-workflow": "^1.118.1",
|
||||
"openai": "^4.77.0",
|
||||
"sql.js": "^1.13.0",
|
||||
"tslib": "^2.6.2",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-darwin-arm64": "^4.50.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp-runtime",
|
||||
"version": "2.23.0",
|
||||
"version": "2.26.4",
|
||||
"description": "n8n MCP Server Runtime Dependencies Only",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
STANDARD_PROTOCOL_VERSION
|
||||
} from './utils/protocol-version';
|
||||
import { InstanceContext, validateInstanceContext } from './types/instance-context';
|
||||
import { SessionState } from './types/session-state';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -71,6 +72,30 @@ function extractMultiTenantHeaders(req: express.Request): MultiTenantHeaders {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Security logging helper for audit trails
|
||||
* Provides structured logging for security-relevant events
|
||||
*/
|
||||
function logSecurityEvent(
|
||||
event: 'session_export' | 'session_restore' | 'session_restore_failed' | 'max_sessions_reached',
|
||||
details: {
|
||||
sessionId?: string;
|
||||
reason?: string;
|
||||
count?: number;
|
||||
instanceId?: string;
|
||||
}
|
||||
): void {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logEntry = {
|
||||
timestamp,
|
||||
event,
|
||||
...details
|
||||
};
|
||||
|
||||
// Log to standard logger with [SECURITY] prefix for easy filtering
|
||||
logger.info(`[SECURITY] ${event}`, logEntry);
|
||||
}
|
||||
|
||||
export class SingleSessionHTTPServer {
|
||||
// Map to store transports by session ID (following SDK pattern)
|
||||
private transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
|
||||
@@ -687,7 +712,20 @@ export class SingleSessionHTTPServer {
|
||||
if (!this.session) return true;
|
||||
return Date.now() - this.session.lastAccess.getTime() > this.sessionTimeout;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if a specific session is expired based on sessionId
|
||||
* Used for multi-session expiration checks during export/restore
|
||||
*
|
||||
* @param sessionId - The session ID to check
|
||||
* @returns true if session is expired or doesn't exist
|
||||
*/
|
||||
private isSessionExpired(sessionId: string): boolean {
|
||||
const metadata = this.sessionMetadata[sessionId];
|
||||
if (!metadata) return true;
|
||||
return Date.now() - metadata.lastAccess.getTime() > this.sessionTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the HTTP server
|
||||
*/
|
||||
@@ -1406,6 +1444,197 @@ export class SingleSessionHTTPServer {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all active session state for persistence
|
||||
*
|
||||
* Used by multi-tenant backends to dump sessions before container restart.
|
||||
* This method exports the minimal state needed to restore sessions after
|
||||
* a restart: session metadata (timing) and instance context (credentials).
|
||||
*
|
||||
* Transport and server objects are NOT persisted - they will be recreated
|
||||
* on the first request after restore.
|
||||
*
|
||||
* SECURITY WARNING: The exported data contains plaintext n8n API keys.
|
||||
* The downstream application MUST encrypt this data before persisting to disk.
|
||||
*
|
||||
* @returns Array of session state objects, excluding expired sessions
|
||||
*
|
||||
* @example
|
||||
* // Before shutdown
|
||||
* const sessions = server.exportSessionState();
|
||||
* await saveToEncryptedStorage(sessions);
|
||||
*/
|
||||
public exportSessionState(): SessionState[] {
|
||||
const sessions: SessionState[] = [];
|
||||
const seenSessionIds = new Set<string>();
|
||||
|
||||
// Iterate over all sessions with metadata (source of truth for active sessions)
|
||||
for (const sessionId of Object.keys(this.sessionMetadata)) {
|
||||
// Check for duplicates (defensive programming)
|
||||
if (seenSessionIds.has(sessionId)) {
|
||||
logger.warn(`Duplicate sessionId detected during export: ${sessionId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip expired sessions - they're not worth persisting
|
||||
if (this.isSessionExpired(sessionId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const metadata = this.sessionMetadata[sessionId];
|
||||
const context = this.sessionContexts[sessionId];
|
||||
|
||||
// Skip sessions without context - these can't be restored meaningfully
|
||||
// (Context is required to reconnect to the correct n8n instance)
|
||||
if (!context || !context.n8nApiUrl || !context.n8nApiKey) {
|
||||
logger.debug(`Skipping session ${sessionId} - missing required context`);
|
||||
continue;
|
||||
}
|
||||
|
||||
seenSessionIds.add(sessionId);
|
||||
sessions.push({
|
||||
sessionId,
|
||||
metadata: {
|
||||
createdAt: metadata.createdAt.toISOString(),
|
||||
lastAccess: metadata.lastAccess.toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: context.n8nApiUrl,
|
||||
n8nApiKey: context.n8nApiKey,
|
||||
instanceId: context.instanceId || sessionId, // Use sessionId as fallback
|
||||
sessionId: context.sessionId,
|
||||
metadata: context.metadata
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`Exported ${sessions.length} session(s) for persistence`);
|
||||
logSecurityEvent('session_export', { count: sessions.length });
|
||||
return sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore session state from previously exported data
|
||||
*
|
||||
* Used by multi-tenant backends to restore sessions after container restart.
|
||||
* This method restores only the session metadata and instance context.
|
||||
* Transport and server objects will be recreated on the first request.
|
||||
*
|
||||
* Restored sessions are "dormant" until a client makes a request, at which
|
||||
* point the transport and server will be initialized normally.
|
||||
*
|
||||
* @param sessions - Array of session state objects from exportSessionState()
|
||||
* @returns Number of sessions successfully restored
|
||||
*
|
||||
* @example
|
||||
* // After startup
|
||||
* const sessions = await loadFromEncryptedStorage();
|
||||
* const count = server.restoreSessionState(sessions);
|
||||
* console.log(`Restored ${count} sessions`);
|
||||
*/
|
||||
public restoreSessionState(sessions: SessionState[]): number {
|
||||
let restoredCount = 0;
|
||||
|
||||
for (const sessionState of sessions) {
|
||||
try {
|
||||
// Skip null or invalid session objects
|
||||
if (!sessionState || typeof sessionState !== 'object' || !sessionState.sessionId) {
|
||||
logger.warn('Skipping invalid session state object');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we've hit the MAX_SESSIONS limit (check real-time count)
|
||||
if (Object.keys(this.sessionMetadata).length >= MAX_SESSIONS) {
|
||||
logger.warn(
|
||||
`Reached MAX_SESSIONS limit (${MAX_SESSIONS}), skipping remaining sessions`
|
||||
);
|
||||
logSecurityEvent('max_sessions_reached', { count: MAX_SESSIONS });
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip if session already exists (duplicate sessionId)
|
||||
if (this.sessionMetadata[sessionState.sessionId]) {
|
||||
logger.debug(`Skipping session ${sessionState.sessionId} - already exists`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse and validate dates first
|
||||
const createdAt = new Date(sessionState.metadata.createdAt);
|
||||
const lastAccess = new Date(sessionState.metadata.lastAccess);
|
||||
|
||||
if (isNaN(createdAt.getTime()) || isNaN(lastAccess.getTime())) {
|
||||
logger.warn(
|
||||
`Skipping session ${sessionState.sessionId} - invalid date format`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate session isn't expired
|
||||
const age = Date.now() - lastAccess.getTime();
|
||||
if (age > this.sessionTimeout) {
|
||||
logger.debug(
|
||||
`Skipping session ${sessionState.sessionId} - expired (age: ${Math.round(age / 1000)}s)`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate context exists (TypeScript null narrowing)
|
||||
if (!sessionState.context) {
|
||||
logger.warn(`Skipping session ${sessionState.sessionId} - missing context`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate context structure using existing validation
|
||||
const validation = validateInstanceContext(sessionState.context);
|
||||
if (!validation.valid) {
|
||||
const reason = validation.errors?.join(', ') || 'invalid context';
|
||||
logger.warn(
|
||||
`Skipping session ${sessionState.sessionId} - invalid context: ${reason}`
|
||||
);
|
||||
logSecurityEvent('session_restore_failed', {
|
||||
sessionId: sessionState.sessionId,
|
||||
reason
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Restore session metadata
|
||||
this.sessionMetadata[sessionState.sessionId] = {
|
||||
createdAt,
|
||||
lastAccess
|
||||
};
|
||||
|
||||
// Restore session context
|
||||
this.sessionContexts[sessionState.sessionId] = {
|
||||
n8nApiUrl: sessionState.context.n8nApiUrl,
|
||||
n8nApiKey: sessionState.context.n8nApiKey,
|
||||
instanceId: sessionState.context.instanceId,
|
||||
sessionId: sessionState.context.sessionId,
|
||||
metadata: sessionState.context.metadata
|
||||
};
|
||||
|
||||
logger.debug(`Restored session ${sessionState.sessionId}`);
|
||||
logSecurityEvent('session_restore', {
|
||||
sessionId: sessionState.sessionId,
|
||||
instanceId: sessionState.context.instanceId
|
||||
});
|
||||
restoredCount++;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to restore session ${sessionState.sessionId}:`, error);
|
||||
logSecurityEvent('session_restore_failed', {
|
||||
sessionId: sessionState.sessionId,
|
||||
reason: error instanceof Error ? error.message : 'unknown error'
|
||||
});
|
||||
// Continue with next session - don't let one failure break the entire restore
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Restored ${restoredCount}/${sessions.length} session(s) from persistence`
|
||||
);
|
||||
return restoredCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Start if called directly
|
||||
|
||||
@@ -18,6 +18,9 @@ export {
|
||||
validateInstanceContext,
|
||||
isInstanceContext
|
||||
} from './types/instance-context';
|
||||
export type {
|
||||
SessionState
|
||||
} from './types/session-state';
|
||||
|
||||
// Re-export MCP SDK types for convenience
|
||||
export type {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Request, Response } from 'express';
|
||||
import { SingleSessionHTTPServer } from './http-server-single-session';
|
||||
import { logger } from './utils/logger';
|
||||
import { InstanceContext } from './types/instance-context';
|
||||
import { SessionState } from './types/session-state';
|
||||
|
||||
export interface EngineHealth {
|
||||
status: 'healthy' | 'unhealthy';
|
||||
@@ -97,7 +98,7 @@ export class N8NMCPEngine {
|
||||
total: Math.round(memoryUsage.heapTotal / 1024 / 1024),
|
||||
unit: 'MB'
|
||||
},
|
||||
version: '2.3.2'
|
||||
version: '2.24.1'
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Health check failed:', error);
|
||||
@@ -106,7 +107,7 @@ export class N8NMCPEngine {
|
||||
uptime: 0,
|
||||
sessionActive: false,
|
||||
memoryUsage: { used: 0, total: 0, unit: 'MB' },
|
||||
version: '2.3.2'
|
||||
version: '2.24.1'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -118,10 +119,58 @@ export class N8NMCPEngine {
|
||||
getSessionInfo(): { active: boolean; sessionId?: string; age?: number } {
|
||||
return this.server.getSessionInfo();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export all active session state for persistence
|
||||
*
|
||||
* Used by multi-tenant backends to dump sessions before container restart.
|
||||
* Returns an array of session state objects containing metadata and credentials.
|
||||
*
|
||||
* SECURITY WARNING: Exported data contains plaintext n8n API keys.
|
||||
* Encrypt before persisting to disk.
|
||||
*
|
||||
* @returns Array of session state objects
|
||||
*
|
||||
* @example
|
||||
* // Before shutdown
|
||||
* const sessions = engine.exportSessionState();
|
||||
* await saveToEncryptedStorage(sessions);
|
||||
*/
|
||||
exportSessionState(): SessionState[] {
|
||||
if (!this.server) {
|
||||
logger.warn('Cannot export sessions: server not initialized');
|
||||
return [];
|
||||
}
|
||||
return this.server.exportSessionState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore session state from previously exported data
|
||||
*
|
||||
* Used by multi-tenant backends to restore sessions after container restart.
|
||||
* Restores session metadata and instance context. Transports/servers are
|
||||
* recreated on first request.
|
||||
*
|
||||
* @param sessions - Array of session state objects from exportSessionState()
|
||||
* @returns Number of sessions successfully restored
|
||||
*
|
||||
* @example
|
||||
* // After startup
|
||||
* const sessions = await loadFromEncryptedStorage();
|
||||
* const count = engine.restoreSessionState(sessions);
|
||||
* console.log(`Restored ${count} sessions`);
|
||||
*/
|
||||
restoreSessionState(sessions: SessionState[]): number {
|
||||
if (!this.server) {
|
||||
logger.warn('Cannot restore sessions: server not initialized');
|
||||
return 0;
|
||||
}
|
||||
return this.server.restoreSessionState(sessions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful shutdown for service lifecycle
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* process.on('SIGTERM', async () => {
|
||||
* await engine.shutdown();
|
||||
|
||||
@@ -34,6 +34,7 @@ import { ExpressionFormatValidator, ExpressionFormatIssue } from '../services/ex
|
||||
import { WorkflowVersioningService } from '../services/workflow-versioning-service';
|
||||
import { handleUpdatePartialWorkflow } from './handlers-workflow-diff';
|
||||
import { telemetry } from '../telemetry';
|
||||
import { TemplateService } from '../templates/template-service';
|
||||
import {
|
||||
createCacheKey,
|
||||
createInstanceCache,
|
||||
@@ -1553,7 +1554,7 @@ export async function handleHealthCheck(context?: InstanceContext): Promise<McpT
|
||||
'1. Verify n8n instance is running',
|
||||
'2. Check N8N_API_URL is correct',
|
||||
'3. Verify N8N_API_KEY has proper permissions',
|
||||
'4. Run n8n_diagnostic for detailed analysis'
|
||||
'4. Run n8n_health_check with mode="diagnostic" for detailed analysis'
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -1566,63 +1567,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
|
||||
|
||||
/**
|
||||
@@ -1844,8 +1788,8 @@ export async function handleDiagnostic(request: any, context?: InstanceContext):
|
||||
}
|
||||
|
||||
// Check which tools are available
|
||||
const documentationTools = 22; // Base documentation tools
|
||||
const managementTools = apiConfigured ? 16 : 0;
|
||||
const documentationTools = 7; // Base documentation tools (after v2.26.0 consolidation)
|
||||
const managementTools = apiConfigured ? 13 : 0; // Management tools requiring API (includes n8n_deploy_template)
|
||||
const totalTools = documentationTools + managementTools;
|
||||
|
||||
// Check npm version
|
||||
@@ -1981,7 +1925,7 @@ export async function handleDiagnostic(request: any, context?: InstanceContext):
|
||||
example: 'validate_workflow({workflow: {...}})'
|
||||
}
|
||||
],
|
||||
note: '22 documentation tools available without API configuration'
|
||||
note: '14 documentation tools available without API configuration'
|
||||
},
|
||||
whatYouCannotDo: [
|
||||
'✗ Create/update workflows in n8n instance',
|
||||
@@ -1996,8 +1940,8 @@ export async function handleDiagnostic(request: any, context?: InstanceContext):
|
||||
' N8N_API_URL=https://your-n8n-instance.com',
|
||||
' N8N_API_KEY=your_api_key_here',
|
||||
'3. Restart the MCP server',
|
||||
'4. Run n8n_diagnostic again to verify',
|
||||
'5. All 38 tools will be available!'
|
||||
'4. Run n8n_health_check with mode="diagnostic" to verify',
|
||||
'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'
|
||||
}
|
||||
@@ -2246,3 +2190,220 @@ export async function handleWorkflowVersions(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Template Deployment Handler
|
||||
// ========================================================================
|
||||
|
||||
const deployTemplateSchema = z.object({
|
||||
templateId: z.number().positive().int(),
|
||||
name: z.string().optional(),
|
||||
autoUpgradeVersions: z.boolean().default(true),
|
||||
validate: z.boolean().default(true),
|
||||
stripCredentials: z.boolean().default(true)
|
||||
});
|
||||
|
||||
interface RequiredCredential {
|
||||
nodeType: string;
|
||||
nodeName: string;
|
||||
credentialType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy a workflow template from n8n.io directly to the user's n8n instance.
|
||||
*
|
||||
* This handler:
|
||||
* 1. Fetches the template from the local template database
|
||||
* 2. Extracts credential requirements for user guidance
|
||||
* 3. Optionally strips credentials (for user to configure in n8n UI)
|
||||
* 4. Optionally upgrades node typeVersions to latest supported
|
||||
* 5. Optionally validates the workflow structure
|
||||
* 6. Creates the workflow in the n8n instance
|
||||
*/
|
||||
export async function handleDeployTemplate(
|
||||
args: unknown,
|
||||
templateService: TemplateService,
|
||||
repository: NodeRepository,
|
||||
context?: InstanceContext
|
||||
): Promise<McpToolResponse> {
|
||||
try {
|
||||
const client = ensureApiConfigured(context);
|
||||
const input = deployTemplateSchema.parse(args);
|
||||
|
||||
// Fetch template
|
||||
const template = await templateService.getTemplate(input.templateId, 'full');
|
||||
if (!template) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Template ${input.templateId} not found`,
|
||||
details: {
|
||||
hint: 'Use search_templates to find available templates',
|
||||
templateUrl: `https://n8n.io/workflows/${input.templateId}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Extract workflow from template (deep copy to avoid mutation)
|
||||
const workflow = JSON.parse(JSON.stringify(template.workflow));
|
||||
if (!workflow || !workflow.nodes) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Template has invalid workflow structure',
|
||||
details: { templateId: input.templateId }
|
||||
};
|
||||
}
|
||||
|
||||
// Set workflow name
|
||||
const workflowName = input.name || template.name;
|
||||
|
||||
// Collect required credentials before stripping
|
||||
const requiredCredentials: RequiredCredential[] = [];
|
||||
for (const node of workflow.nodes) {
|
||||
if (node.credentials && typeof node.credentials === 'object') {
|
||||
for (const [credType] of Object.entries(node.credentials)) {
|
||||
requiredCredentials.push({
|
||||
nodeType: node.type,
|
||||
nodeName: node.name,
|
||||
credentialType: credType
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip credentials if requested
|
||||
if (input.stripCredentials) {
|
||||
workflow.nodes = workflow.nodes.map((node: any) => {
|
||||
const { credentials, ...rest } = node;
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-upgrade typeVersions if requested
|
||||
if (input.autoUpgradeVersions) {
|
||||
const autoFixer = new WorkflowAutoFixer(repository);
|
||||
|
||||
// Run validation to get issues to fix
|
||||
const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
|
||||
const validationResult = await validator.validateWorkflow(workflow, {
|
||||
validateNodes: true,
|
||||
validateConnections: false,
|
||||
validateExpressions: false,
|
||||
profile: 'runtime'
|
||||
});
|
||||
|
||||
// Generate fixes focused on typeVersion upgrades
|
||||
const fixResult = await autoFixer.generateFixes(
|
||||
workflow,
|
||||
validationResult,
|
||||
[],
|
||||
{ fixTypes: ['typeversion-upgrade', 'typeversion-correction'] }
|
||||
);
|
||||
|
||||
// Apply fixes to workflow
|
||||
if (fixResult.operations.length > 0) {
|
||||
for (const op of fixResult.operations) {
|
||||
if (op.type === 'updateNode' && op.updates) {
|
||||
const node = workflow.nodes.find((n: any) =>
|
||||
n.id === op.nodeId || n.name === op.nodeName
|
||||
);
|
||||
if (node) {
|
||||
for (const [path, value] of Object.entries(op.updates)) {
|
||||
if (path === 'typeVersion') {
|
||||
node.typeVersion = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate workflow if requested
|
||||
if (input.validate) {
|
||||
const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
|
||||
const validationResult = await validator.validateWorkflow(workflow, {
|
||||
validateNodes: true,
|
||||
validateConnections: true,
|
||||
validateExpressions: true,
|
||||
profile: 'runtime'
|
||||
});
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Workflow validation failed',
|
||||
details: {
|
||||
errors: validationResult.errors.map(e => ({
|
||||
node: e.nodeName,
|
||||
message: e.message
|
||||
})),
|
||||
warnings: validationResult.warnings.length,
|
||||
hint: 'Use validate=false to skip validation, or fix the template issues'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Identify trigger type
|
||||
const triggerNode = workflow.nodes.find((n: any) =>
|
||||
n.type?.includes('Trigger') ||
|
||||
n.type?.includes('webhook') ||
|
||||
n.type === 'n8n-nodes-base.webhook'
|
||||
);
|
||||
const triggerType = triggerNode?.type?.split('.').pop() || 'manual';
|
||||
|
||||
// Create workflow via API (always creates inactive)
|
||||
const createdWorkflow = await client.createWorkflow({
|
||||
name: workflowName,
|
||||
nodes: workflow.nodes,
|
||||
connections: workflow.connections,
|
||||
settings: workflow.settings || { executionOrder: 'v1' }
|
||||
});
|
||||
|
||||
// Get base URL for workflow link
|
||||
const apiConfig = context ? getN8nApiConfigFromContext(context) : getN8nApiConfig();
|
||||
const baseUrl = apiConfig?.baseUrl?.replace('/api/v1', '') || '';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
workflowId: createdWorkflow.id,
|
||||
name: createdWorkflow.name,
|
||||
active: false,
|
||||
nodeCount: workflow.nodes.length,
|
||||
triggerType,
|
||||
requiredCredentials: requiredCredentials.length > 0 ? requiredCredentials : undefined,
|
||||
url: baseUrl ? `${baseUrl}/workflow/${createdWorkflow.id}` : undefined,
|
||||
templateId: input.templateId,
|
||||
templateUrl: template.url || `https://n8n.io/workflows/${input.templateId}`
|
||||
},
|
||||
message: `Workflow "${createdWorkflow.name}" deployed successfully from template ${input.templateId}. ${
|
||||
requiredCredentials.length > 0
|
||||
? `Configure ${requiredCredentials.length} credential(s) in n8n to activate.`
|
||||
: ''
|
||||
}`
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid input',
|
||||
details: { errors: error.errors }
|
||||
};
|
||||
}
|
||||
|
||||
if (error instanceof N8nApiError) {
|
||||
return {
|
||||
success: false,
|
||||
error: getUserFriendlyErrorMessage(error),
|
||||
code: error.code,
|
||||
details: error.details as Record<string, unknown> | undefined
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TaskTemplates } from '../services/task-templates';
|
||||
import { ConfigValidator } from '../services/config-validator';
|
||||
import { EnhancedConfigValidator, ValidationMode, ValidationProfile } from '../services/enhanced-config-validator';
|
||||
import { PropertyDependencies } from '../services/property-dependencies';
|
||||
import { TypeStructureService } from '../services/type-structure-service';
|
||||
import { SimpleCache } from '../utils/simple-cache';
|
||||
import { TemplateService } from '../templates/template-service';
|
||||
import { WorkflowValidator } from '../services/workflow-validator';
|
||||
@@ -58,6 +59,67 @@ interface NodeRow {
|
||||
credentials_required?: string;
|
||||
}
|
||||
|
||||
interface VersionSummary {
|
||||
currentVersion: string;
|
||||
totalVersions: number;
|
||||
hasVersionHistory: boolean;
|
||||
}
|
||||
|
||||
interface NodeMinimalInfo {
|
||||
nodeType: string;
|
||||
workflowNodeType: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
category: string;
|
||||
package: string;
|
||||
isAITool: boolean;
|
||||
isTrigger: boolean;
|
||||
isWebhook: boolean;
|
||||
}
|
||||
|
||||
interface NodeStandardInfo {
|
||||
nodeType: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
category: string;
|
||||
requiredProperties: any[];
|
||||
commonProperties: any[];
|
||||
operations?: any[];
|
||||
credentials?: any;
|
||||
examples?: any[];
|
||||
versionInfo: VersionSummary;
|
||||
}
|
||||
|
||||
interface NodeFullInfo {
|
||||
nodeType: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
category: string;
|
||||
properties: any[];
|
||||
operations?: any[];
|
||||
credentials?: any;
|
||||
documentation?: string;
|
||||
versionInfo: VersionSummary;
|
||||
}
|
||||
|
||||
interface VersionHistoryInfo {
|
||||
nodeType: string;
|
||||
versions: any[];
|
||||
latestVersion: string;
|
||||
hasBreakingChanges: boolean;
|
||||
}
|
||||
|
||||
interface VersionComparisonInfo {
|
||||
nodeType: string;
|
||||
fromVersion: string;
|
||||
toVersion: string;
|
||||
changes: any[];
|
||||
breakingChanges?: any[];
|
||||
migrations?: any[];
|
||||
}
|
||||
|
||||
type NodeInfoResponse = NodeMinimalInfo | NodeStandardInfo | NodeFullInfo | VersionHistoryInfo | VersionComparisonInfo;
|
||||
|
||||
export class N8NDocumentationMCPServer {
|
||||
private server: Server;
|
||||
private db: DatabaseAdapter | null = null;
|
||||
@@ -768,38 +830,38 @@ export class N8NDocumentationMCPServer {
|
||||
let validationResult;
|
||||
|
||||
switch (toolName) {
|
||||
case 'validate_node_operation':
|
||||
case 'validate_node':
|
||||
// Consolidated tool handles both modes - validate as operation for now
|
||||
validationResult = ToolValidation.validateNodeOperation(args);
|
||||
break;
|
||||
case 'validate_node_minimal':
|
||||
validationResult = ToolValidation.validateNodeMinimal(args);
|
||||
break;
|
||||
case 'validate_workflow':
|
||||
case 'validate_workflow_connections':
|
||||
case 'validate_workflow_expressions':
|
||||
validationResult = ToolValidation.validateWorkflow(args);
|
||||
break;
|
||||
case 'search_nodes':
|
||||
validationResult = ToolValidation.validateSearchNodes(args);
|
||||
break;
|
||||
case 'list_node_templates':
|
||||
validationResult = ToolValidation.validateListNodeTemplates(args);
|
||||
break;
|
||||
case 'n8n_create_workflow':
|
||||
validationResult = ToolValidation.validateCreateWorkflow(args);
|
||||
break;
|
||||
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_delete_workflow':
|
||||
case 'n8n_validate_workflow':
|
||||
case 'n8n_autofix_workflow':
|
||||
case 'n8n_get_execution':
|
||||
case 'n8n_delete_execution':
|
||||
validationResult = ToolValidation.validateWorkflowId(args);
|
||||
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;
|
||||
case 'n8n_deploy_template':
|
||||
// Requires templateId parameter
|
||||
validationResult = args.templateId !== undefined
|
||||
? { valid: true, errors: [] }
|
||||
: { valid: false, errors: [{ field: 'templateId', message: 'templateId is required' }] };
|
||||
break;
|
||||
default:
|
||||
// For tools not yet migrated to schema validation, use basic validation
|
||||
return this.validateToolParamsBasic(toolName, args, legacyRequiredParams || []);
|
||||
@@ -953,41 +1015,50 @@ export class N8NDocumentationMCPServer {
|
||||
case 'tools_documentation':
|
||||
// No required parameters
|
||||
return this.getToolsDocumentation(args.topic, args.depth);
|
||||
case 'list_nodes':
|
||||
// No required parameters
|
||||
return this.listNodes(args);
|
||||
case 'get_node_info':
|
||||
this.validateToolParams(name, args, ['nodeType']);
|
||||
return this.getNodeInfo(args.nodeType);
|
||||
case 'search_nodes':
|
||||
this.validateToolParams(name, args, ['query']);
|
||||
// Convert limit to number if provided, otherwise use default
|
||||
const limit = args.limit !== undefined ? Number(args.limit) || 20 : 20;
|
||||
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':
|
||||
case 'get_node':
|
||||
this.validateToolParams(name, args, ['nodeType']);
|
||||
return this.getNodeDocumentation(args.nodeType);
|
||||
case 'get_database_statistics':
|
||||
// No required parameters
|
||||
return this.getDatabaseStatistics();
|
||||
case 'get_node_essentials':
|
||||
this.validateToolParams(name, args, ['nodeType']);
|
||||
return this.getNodeEssentials(args.nodeType, args.includeExamples);
|
||||
case 'search_node_properties':
|
||||
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':
|
||||
// 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(
|
||||
args.nodeType,
|
||||
args.detail,
|
||||
args.mode,
|
||||
args.includeTypeInfo,
|
||||
args.includeExamples,
|
||||
args.fromVersion,
|
||||
args.toVersion
|
||||
);
|
||||
case 'validate_node':
|
||||
this.validateToolParams(name, args, ['nodeType', 'config']);
|
||||
// Ensure config is an object
|
||||
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 {
|
||||
nodeType: args.nodeType || 'unknown',
|
||||
workflowNodeType: args.nodeType || 'unknown',
|
||||
@@ -1003,7 +1074,7 @@ export class N8NDocumentationMCPServer {
|
||||
suggestions: [
|
||||
'🔧 RECOVERY: Invalid config detected. Fix with:',
|
||||
' • 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'
|
||||
],
|
||||
summary: {
|
||||
@@ -1014,95 +1085,75 @@ export class N8NDocumentationMCPServer {
|
||||
}
|
||||
};
|
||||
}
|
||||
return this.validateNodeConfig(args.nodeType, args.config, 'operation', args.profile);
|
||||
case 'validate_node_minimal':
|
||||
this.validateToolParams(name, args, ['nodeType', 'config']);
|
||||
// Ensure config is an object
|
||||
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'
|
||||
]
|
||||
};
|
||||
// Handle mode parameter
|
||||
const validationMode = args.mode || 'full';
|
||||
if (validationMode === 'minimal') {
|
||||
return this.validateNodeMinimal(args.nodeType, args.config);
|
||||
}
|
||||
return this.validateNodeMinimal(args.nodeType, args.config);
|
||||
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);
|
||||
return this.validateNodeConfig(args.nodeType, args.config, 'operation', args.profile);
|
||||
case 'get_template':
|
||||
this.validateToolParams(name, args, ['templateId']);
|
||||
const templateId = Number(args.templateId);
|
||||
const mode = args.mode || 'full';
|
||||
return this.getTemplate(templateId, mode);
|
||||
case 'search_templates':
|
||||
this.validateToolParams(name, args, ['query']);
|
||||
const templateMode = args.mode || 'full';
|
||||
return this.getTemplate(templateId, templateMode);
|
||||
case 'search_templates': {
|
||||
// Consolidated tool with searchMode parameter
|
||||
const searchMode = args.searchMode || 'keyword';
|
||||
const searchLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
|
||||
const searchOffset = Math.max(Number(args.offset) || 0, 0);
|
||||
const searchFields = args.fields as string[] | undefined;
|
||||
return this.searchTemplates(args.query, searchLimit, searchOffset, searchFields);
|
||||
case 'get_templates_for_task':
|
||||
this.validateToolParams(name, args, ['task']);
|
||||
const taskLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
|
||||
const taskOffset = Math.max(Number(args.offset) || 0, 0);
|
||||
return this.getTemplatesForTask(args.task, taskLimit, taskOffset);
|
||||
case 'search_templates_by_metadata':
|
||||
// No required params - all filters are optional
|
||||
const metadataLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
|
||||
const metadataOffset = Math.max(Number(args.offset) || 0, 0);
|
||||
return this.searchTemplatesByMetadata({
|
||||
category: args.category,
|
||||
complexity: args.complexity,
|
||||
maxSetupMinutes: args.maxSetupMinutes ? Number(args.maxSetupMinutes) : undefined,
|
||||
minSetupMinutes: args.minSetupMinutes ? Number(args.minSetupMinutes) : undefined,
|
||||
requiredService: args.requiredService,
|
||||
targetAudience: args.targetAudience
|
||||
}, metadataLimit, metadataOffset);
|
||||
|
||||
switch (searchMode) {
|
||||
case 'by_nodes':
|
||||
if (!args.nodeTypes || !Array.isArray(args.nodeTypes) || args.nodeTypes.length === 0) {
|
||||
throw new Error('nodeTypes array is required for searchMode=by_nodes');
|
||||
}
|
||||
return this.listNodeTemplates(args.nodeTypes, searchLimit, searchOffset);
|
||||
case 'by_task':
|
||||
if (!args.task) {
|
||||
throw new Error('task is required for searchMode=by_task');
|
||||
}
|
||||
return this.getTemplatesForTask(args.task, searchLimit, searchOffset);
|
||||
case 'by_metadata':
|
||||
return this.searchTemplatesByMetadata({
|
||||
category: args.category,
|
||||
complexity: args.complexity,
|
||||
maxSetupMinutes: args.maxSetupMinutes ? Number(args.maxSetupMinutes) : undefined,
|
||||
minSetupMinutes: args.minSetupMinutes ? Number(args.minSetupMinutes) : undefined,
|
||||
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':
|
||||
this.validateToolParams(name, args, ['workflow']);
|
||||
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)
|
||||
case 'n8n_create_workflow':
|
||||
this.validateToolParams(name, args, ['name', 'nodes', 'connections']);
|
||||
return n8nHandlers.handleCreateWorkflow(args, this.instanceContext);
|
||||
case 'n8n_get_workflow':
|
||||
case 'n8n_get_workflow': {
|
||||
this.validateToolParams(name, args, ['id']);
|
||||
return n8nHandlers.handleGetWorkflow(args, this.instanceContext);
|
||||
case 'n8n_get_workflow_details':
|
||||
this.validateToolParams(name, args, ['id']);
|
||||
return n8nHandlers.handleGetWorkflowDetails(args, this.instanceContext);
|
||||
case 'n8n_get_workflow_structure':
|
||||
this.validateToolParams(name, args, ['id']);
|
||||
return n8nHandlers.handleGetWorkflowStructure(args, this.instanceContext);
|
||||
case 'n8n_get_workflow_minimal':
|
||||
this.validateToolParams(name, args, ['id']);
|
||||
return n8nHandlers.handleGetWorkflowMinimal(args, this.instanceContext);
|
||||
const workflowMode = args.mode || 'full';
|
||||
switch (workflowMode) {
|
||||
case 'details':
|
||||
return n8nHandlers.handleGetWorkflowDetails(args, this.instanceContext);
|
||||
case 'structure':
|
||||
return n8nHandlers.handleGetWorkflowStructure(args, this.instanceContext);
|
||||
case 'minimal':
|
||||
return n8nHandlers.handleGetWorkflowMinimal(args, this.instanceContext);
|
||||
case 'full':
|
||||
default:
|
||||
return n8nHandlers.handleGetWorkflow(args, this.instanceContext);
|
||||
}
|
||||
}
|
||||
case 'n8n_update_full_workflow':
|
||||
this.validateToolParams(name, args, ['id']);
|
||||
return n8nHandlers.handleUpdateWorkflow(args, this.repository!, this.instanceContext);
|
||||
@@ -1128,28 +1179,43 @@ export class N8NDocumentationMCPServer {
|
||||
case 'n8n_trigger_webhook_workflow':
|
||||
this.validateToolParams(name, args, ['webhookUrl']);
|
||||
return n8nHandlers.handleTriggerWebhookWorkflow(args, this.instanceContext);
|
||||
case 'n8n_get_execution':
|
||||
this.validateToolParams(name, args, ['id']);
|
||||
return n8nHandlers.handleGetExecution(args, this.instanceContext);
|
||||
case 'n8n_list_executions':
|
||||
// No required parameters
|
||||
return n8nHandlers.handleListExecutions(args, this.instanceContext);
|
||||
case 'n8n_delete_execution':
|
||||
this.validateToolParams(name, args, ['id']);
|
||||
return n8nHandlers.handleDeleteExecution(args, this.instanceContext);
|
||||
case 'n8n_executions': {
|
||||
this.validateToolParams(name, args, ['action']);
|
||||
const execAction = args.action;
|
||||
switch (execAction) {
|
||||
case 'get':
|
||||
if (!args.id) {
|
||||
throw new Error('id is required for action=get');
|
||||
}
|
||||
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':
|
||||
// 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);
|
||||
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':
|
||||
this.validateToolParams(name, args, ['mode']);
|
||||
return n8nHandlers.handleWorkflowVersions(args, this.repository!, this.instanceContext);
|
||||
|
||||
case 'n8n_deploy_template':
|
||||
this.validateToolParams(name, args, ['templateId']);
|
||||
await this.ensureInitialized();
|
||||
if (!this.templateService) throw new Error('Template service not initialized');
|
||||
if (!this.repository) throw new Error('Repository not initialized');
|
||||
return n8nHandlers.handleDeployTemplate(args, this.templateService, this.repository, this.instanceContext);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
@@ -2218,6 +2284,393 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified node information retrieval with multiple detail levels and modes.
|
||||
*
|
||||
* @param nodeType - Full node type identifier (e.g., "nodes-base.httpRequest" or "nodes-langchain.agent")
|
||||
* @param detail - Information detail level (minimal, standard, full). Only applies when mode='info'.
|
||||
* - minimal: ~200 tokens, basic metadata only (no version info)
|
||||
* - standard: ~1-2K tokens, essential properties and operations (includes version info, AI-friendly default)
|
||||
* - full: ~3-8K tokens, complete node information with all properties (includes version info)
|
||||
* @param mode - Operation mode determining the type of information returned:
|
||||
* - info: Node configuration details (respects detail level)
|
||||
* - versions: Complete version history with breaking changes summary
|
||||
* - compare: Property-level comparison between two versions (requires fromVersion)
|
||||
* - breaking: Breaking changes only between versions (requires fromVersion)
|
||||
* - migrations: Auto-migratable changes between versions (requires both fromVersion and toVersion)
|
||||
* @param includeTypeInfo - Include type structure metadata for properties (only applies to mode='info').
|
||||
* Adds ~80-120 tokens per property with type category, JS type, and validation rules.
|
||||
* @param includeExamples - Include real-world configuration examples from templates (only applies to mode='info' with detail='standard').
|
||||
* Adds ~200-400 tokens per example.
|
||||
* @param fromVersion - Source version for comparison modes (required for compare, breaking, migrations).
|
||||
* Format: "1.0" or "2.1"
|
||||
* @param toVersion - Target version for comparison modes (optional for compare/breaking, required for migrations).
|
||||
* Defaults to latest version if omitted.
|
||||
* @returns NodeInfoResponse - Union type containing different response structures based on mode and detail parameters
|
||||
*/
|
||||
private async getNode(
|
||||
nodeType: string,
|
||||
detail: string = 'standard',
|
||||
mode: string = 'info',
|
||||
includeTypeInfo?: boolean,
|
||||
includeExamples?: boolean,
|
||||
fromVersion?: string,
|
||||
toVersion?: string
|
||||
): Promise<NodeInfoResponse> {
|
||||
await this.ensureInitialized();
|
||||
if (!this.repository) throw new Error('Repository not initialized');
|
||||
|
||||
// Validate parameters
|
||||
const validDetailLevels = ['minimal', 'standard', 'full'];
|
||||
const validModes = ['info', 'versions', 'compare', 'breaking', 'migrations'];
|
||||
|
||||
if (!validDetailLevels.includes(detail)) {
|
||||
throw new Error(`get_node: Invalid detail level "${detail}". Valid options: ${validDetailLevels.join(', ')}`);
|
||||
}
|
||||
|
||||
if (!validModes.includes(mode)) {
|
||||
throw new Error(`get_node: Invalid mode "${mode}". Valid options: ${validModes.join(', ')}`);
|
||||
}
|
||||
|
||||
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
||||
|
||||
// Version modes - detail level ignored
|
||||
if (mode !== 'info') {
|
||||
return this.handleVersionMode(
|
||||
normalizedType,
|
||||
mode,
|
||||
fromVersion,
|
||||
toVersion
|
||||
);
|
||||
}
|
||||
|
||||
// Info mode - respect detail level
|
||||
return this.handleInfoMode(
|
||||
normalizedType,
|
||||
detail,
|
||||
includeTypeInfo,
|
||||
includeExamples
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle info mode - returns node information at specified detail level
|
||||
*/
|
||||
private async handleInfoMode(
|
||||
nodeType: string,
|
||||
detail: string,
|
||||
includeTypeInfo?: boolean,
|
||||
includeExamples?: boolean
|
||||
): Promise<NodeMinimalInfo | NodeStandardInfo | NodeFullInfo> {
|
||||
switch (detail) {
|
||||
case 'minimal': {
|
||||
// Get basic node metadata only (no version info for minimal mode)
|
||||
let node = this.repository!.getNode(nodeType);
|
||||
|
||||
if (!node) {
|
||||
const alternatives = getNodeTypeAlternatives(nodeType);
|
||||
for (const alt of alternatives) {
|
||||
const found = this.repository!.getNode(alt);
|
||||
if (found) {
|
||||
node = found;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`Node ${nodeType} not found`);
|
||||
}
|
||||
|
||||
return {
|
||||
nodeType: node.nodeType,
|
||||
workflowNodeType: getWorkflowNodeType(node.package ?? 'n8n-nodes-base', node.nodeType),
|
||||
displayName: node.displayName,
|
||||
description: node.description,
|
||||
category: node.category,
|
||||
package: node.package,
|
||||
isAITool: node.isAITool,
|
||||
isTrigger: node.isTrigger,
|
||||
isWebhook: node.isWebhook
|
||||
};
|
||||
}
|
||||
|
||||
case 'standard': {
|
||||
// Use existing getNodeEssentials logic
|
||||
const essentials = await this.getNodeEssentials(nodeType, includeExamples);
|
||||
const versionSummary = this.getVersionSummary(nodeType);
|
||||
|
||||
// Apply type info enrichment if requested
|
||||
if (includeTypeInfo) {
|
||||
essentials.requiredProperties = this.enrichPropertiesWithTypeInfo(essentials.requiredProperties);
|
||||
essentials.commonProperties = this.enrichPropertiesWithTypeInfo(essentials.commonProperties);
|
||||
}
|
||||
|
||||
return {
|
||||
...essentials,
|
||||
versionInfo: versionSummary
|
||||
};
|
||||
}
|
||||
|
||||
case 'full': {
|
||||
// Use existing getNodeInfo logic
|
||||
const fullInfo = await this.getNodeInfo(nodeType);
|
||||
const versionSummary = this.getVersionSummary(nodeType);
|
||||
|
||||
// Apply type info enrichment if requested
|
||||
if (includeTypeInfo && fullInfo.properties) {
|
||||
fullInfo.properties = this.enrichPropertiesWithTypeInfo(fullInfo.properties);
|
||||
}
|
||||
|
||||
return {
|
||||
...fullInfo,
|
||||
versionInfo: versionSummary
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown detail level: ${detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle version modes - returns version history and comparison data
|
||||
*/
|
||||
private async handleVersionMode(
|
||||
nodeType: string,
|
||||
mode: string,
|
||||
fromVersion?: string,
|
||||
toVersion?: string
|
||||
): Promise<VersionHistoryInfo | VersionComparisonInfo> {
|
||||
switch (mode) {
|
||||
case 'versions':
|
||||
return this.getVersionHistory(nodeType);
|
||||
|
||||
case 'compare':
|
||||
if (!fromVersion) {
|
||||
throw new Error(`get_node: fromVersion is required for compare mode (nodeType: ${nodeType})`);
|
||||
}
|
||||
return this.compareVersions(nodeType, fromVersion, toVersion);
|
||||
|
||||
case 'breaking':
|
||||
if (!fromVersion) {
|
||||
throw new Error(`get_node: fromVersion is required for breaking mode (nodeType: ${nodeType})`);
|
||||
}
|
||||
return this.getBreakingChanges(nodeType, fromVersion, toVersion);
|
||||
|
||||
case 'migrations':
|
||||
if (!fromVersion || !toVersion) {
|
||||
throw new Error(`get_node: Both fromVersion and toVersion are required for migrations mode (nodeType: ${nodeType})`);
|
||||
}
|
||||
return this.getMigrations(nodeType, fromVersion, toVersion);
|
||||
|
||||
default:
|
||||
throw new Error(`get_node: Unknown mode: ${mode} (nodeType: ${nodeType})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get version summary (always included in info mode responses)
|
||||
* Cached for 24 hours to improve performance
|
||||
*/
|
||||
private getVersionSummary(nodeType: string): VersionSummary {
|
||||
const cacheKey = `version-summary:${nodeType}`;
|
||||
const cached = this.cache.get(cacheKey) as VersionSummary | null;
|
||||
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const versions = this.repository!.getNodeVersions(nodeType);
|
||||
const latest = this.repository!.getLatestNodeVersion(nodeType);
|
||||
|
||||
const summary: VersionSummary = {
|
||||
currentVersion: latest?.version || 'unknown',
|
||||
totalVersions: versions.length,
|
||||
hasVersionHistory: versions.length > 0
|
||||
};
|
||||
|
||||
// Cache for 24 hours (86400000 ms)
|
||||
this.cache.set(cacheKey, summary, 86400000);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complete version history for a node
|
||||
*/
|
||||
private getVersionHistory(nodeType: string): any {
|
||||
const versions = this.repository!.getNodeVersions(nodeType);
|
||||
|
||||
return {
|
||||
nodeType,
|
||||
totalVersions: versions.length,
|
||||
versions: versions.map(v => ({
|
||||
version: v.version,
|
||||
isCurrent: v.isCurrentMax,
|
||||
minimumN8nVersion: v.minimumN8nVersion,
|
||||
releasedAt: v.releasedAt,
|
||||
hasBreakingChanges: (v.breakingChanges || []).length > 0,
|
||||
breakingChangesCount: (v.breakingChanges || []).length,
|
||||
deprecatedProperties: v.deprecatedProperties || [],
|
||||
addedProperties: v.addedProperties || []
|
||||
})),
|
||||
available: versions.length > 0,
|
||||
message: versions.length === 0 ?
|
||||
'No version history available. Version tracking may not be enabled for this node.' :
|
||||
undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two versions of a node
|
||||
*/
|
||||
private compareVersions(
|
||||
nodeType: string,
|
||||
fromVersion: string,
|
||||
toVersion?: string
|
||||
): any {
|
||||
const latest = this.repository!.getLatestNodeVersion(nodeType);
|
||||
const targetVersion = toVersion || latest?.version;
|
||||
|
||||
if (!targetVersion) {
|
||||
throw new Error('No target version available');
|
||||
}
|
||||
|
||||
const changes = this.repository!.getPropertyChanges(
|
||||
nodeType,
|
||||
fromVersion,
|
||||
targetVersion
|
||||
);
|
||||
|
||||
return {
|
||||
nodeType,
|
||||
fromVersion,
|
||||
toVersion: targetVersion,
|
||||
totalChanges: changes.length,
|
||||
breakingChanges: changes.filter(c => c.isBreaking).length,
|
||||
changes: changes.map(c => ({
|
||||
property: c.propertyName,
|
||||
changeType: c.changeType,
|
||||
isBreaking: c.isBreaking,
|
||||
severity: c.severity,
|
||||
oldValue: c.oldValue,
|
||||
newValue: c.newValue,
|
||||
migrationHint: c.migrationHint,
|
||||
autoMigratable: c.autoMigratable
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get breaking changes between versions
|
||||
*/
|
||||
private getBreakingChanges(
|
||||
nodeType: string,
|
||||
fromVersion: string,
|
||||
toVersion?: string
|
||||
): any {
|
||||
const breakingChanges = this.repository!.getBreakingChanges(
|
||||
nodeType,
|
||||
fromVersion,
|
||||
toVersion
|
||||
);
|
||||
|
||||
return {
|
||||
nodeType,
|
||||
fromVersion,
|
||||
toVersion: toVersion || 'latest',
|
||||
totalBreakingChanges: breakingChanges.length,
|
||||
changes: breakingChanges.map(c => ({
|
||||
fromVersion: c.fromVersion,
|
||||
toVersion: c.toVersion,
|
||||
property: c.propertyName,
|
||||
changeType: c.changeType,
|
||||
severity: c.severity,
|
||||
migrationHint: c.migrationHint,
|
||||
oldValue: c.oldValue,
|
||||
newValue: c.newValue
|
||||
})),
|
||||
upgradeSafe: breakingChanges.length === 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get auto-migratable changes between versions
|
||||
*/
|
||||
private getMigrations(
|
||||
nodeType: string,
|
||||
fromVersion: string,
|
||||
toVersion: string
|
||||
): any {
|
||||
const migrations = this.repository!.getAutoMigratableChanges(
|
||||
nodeType,
|
||||
fromVersion,
|
||||
toVersion
|
||||
);
|
||||
|
||||
const allChanges = this.repository!.getPropertyChanges(
|
||||
nodeType,
|
||||
fromVersion,
|
||||
toVersion
|
||||
);
|
||||
|
||||
return {
|
||||
nodeType,
|
||||
fromVersion,
|
||||
toVersion,
|
||||
autoMigratableChanges: migrations.length,
|
||||
totalChanges: allChanges.length,
|
||||
migrations: migrations.map(m => ({
|
||||
property: m.propertyName,
|
||||
changeType: m.changeType,
|
||||
migrationStrategy: m.migrationStrategy,
|
||||
severity: m.severity
|
||||
})),
|
||||
requiresManualMigration: migrations.length < allChanges.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich property with type structure metadata
|
||||
*/
|
||||
private enrichPropertyWithTypeInfo(property: any): any {
|
||||
if (!property || !property.type) return property;
|
||||
|
||||
const structure = TypeStructureService.getStructure(property.type);
|
||||
if (!structure) return property;
|
||||
|
||||
return {
|
||||
...property,
|
||||
typeInfo: {
|
||||
category: structure.type,
|
||||
jsType: structure.jsType,
|
||||
description: structure.description,
|
||||
isComplex: TypeStructureService.isComplexType(property.type),
|
||||
isPrimitive: TypeStructureService.isPrimitiveType(property.type),
|
||||
allowsExpressions: structure.validation?.allowExpressions ?? true,
|
||||
allowsEmpty: structure.validation?.allowEmpty ?? false,
|
||||
...(structure.structure && {
|
||||
structureHints: {
|
||||
hasProperties: !!structure.structure.properties,
|
||||
hasItems: !!structure.structure.items,
|
||||
isFlexible: structure.structure.flexible ?? false,
|
||||
requiredFields: structure.structure.required ?? []
|
||||
}
|
||||
}),
|
||||
...(structure.notes && { notes: structure.notes })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich an array of properties with type structure metadata
|
||||
*/
|
||||
private enrichPropertiesWithTypeInfo(properties: any[]): any[] {
|
||||
if (!properties || !Array.isArray(properties)) return properties;
|
||||
return properties.map((prop: any) => this.enrichPropertyWithTypeInfo(prop));
|
||||
}
|
||||
|
||||
private async searchNodeProperties(nodeType: string, query: string, maxResults: number = 20): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
if (!this.repository) throw new Error('Repository not initialized');
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const getNodeAsToolInfoDoc: ToolDocumentation = {
|
||||
name: 'get_node_as_tool_info',
|
||||
category: 'configuration',
|
||||
essentials: {
|
||||
description: 'Explains how to use ANY node as an AI tool with requirements and examples.',
|
||||
keyParameters: ['nodeType'],
|
||||
example: 'get_node_as_tool_info({nodeType: "nodes-base.slack"})',
|
||||
performance: 'Fast - returns guidance and examples',
|
||||
tips: [
|
||||
'ANY node can be used as AI tool, not just AI-marked ones',
|
||||
'Community nodes need N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true',
|
||||
'Provides specific use cases and connection requirements'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Shows how to use any n8n node as an AI tool in AI Agent workflows. In n8n, ANY node can be connected to an AI Agent's tool port, allowing the AI to use that node's functionality. This tool provides specific guidance, requirements, and examples for using a node as an AI tool.`,
|
||||
parameters: {
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Full node type WITH prefix: "nodes-base.slack", "nodes-base.googleSheets", etc.',
|
||||
examples: [
|
||||
'nodes-base.slack',
|
||||
'nodes-base.httpRequest',
|
||||
'nodes-base.googleSheets',
|
||||
'nodes-langchain.documentLoader'
|
||||
]
|
||||
}
|
||||
},
|
||||
returns: `Object containing:
|
||||
- nodeType: The node's full type identifier
|
||||
- displayName: Human-readable name
|
||||
- isMarkedAsAITool: Whether node has usableAsTool property
|
||||
- aiToolCapabilities: Detailed AI tool usage information including:
|
||||
- canBeUsedAsTool: Always true in n8n
|
||||
- requiresEnvironmentVariable: For community nodes
|
||||
- commonUseCases: Specific AI tool use cases
|
||||
- requirements: Connection and environment setup
|
||||
- examples: Code examples for common scenarios
|
||||
- tips: Best practices for AI tool usage`,
|
||||
examples: [
|
||||
'get_node_as_tool_info({nodeType: "nodes-base.slack"}) - Get AI tool guidance for Slack',
|
||||
'get_node_as_tool_info({nodeType: "nodes-base.httpRequest"}) - Learn to use HTTP Request as AI tool',
|
||||
'get_node_as_tool_info({nodeType: "nodes-base.postgres"}) - Database queries as AI tools'
|
||||
],
|
||||
useCases: [
|
||||
'Understanding how to connect any node to AI Agent',
|
||||
'Learning environment requirements for community nodes',
|
||||
'Getting specific use case examples for AI tool usage',
|
||||
'Checking if a node is optimized for AI usage',
|
||||
'Understanding credential requirements for AI tools'
|
||||
],
|
||||
performance: 'Very fast - returns pre-computed guidance and examples',
|
||||
bestPractices: [
|
||||
'Use this before configuring nodes as AI tools',
|
||||
'Check environment requirements for community nodes',
|
||||
'Review common use cases to understand best applications',
|
||||
'Test nodes independently before connecting to AI Agent',
|
||||
'Give tools descriptive names in AI Agent configuration'
|
||||
],
|
||||
pitfalls: [
|
||||
'Community nodes require environment variable to be used as tools',
|
||||
'Not all nodes make sense as AI tools (e.g., triggers)',
|
||||
'Some nodes require specific credentials configuration',
|
||||
'Tool descriptions in AI Agent must be clear and detailed'
|
||||
],
|
||||
relatedTools: ['list_ai_tools', 'get_node_essentials', 'validate_node_operation']
|
||||
}
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const getNodeDocumentationDoc: ToolDocumentation = {
|
||||
name: 'get_node_documentation',
|
||||
category: 'configuration',
|
||||
essentials: {
|
||||
description: 'Get readable docs with examples/auth/patterns. Better than raw schema! 87% coverage. Format: "nodes-base.slack"',
|
||||
keyParameters: ['nodeType'],
|
||||
example: 'get_node_documentation({nodeType: "nodes-base.slack"})',
|
||||
performance: 'Fast - pre-parsed',
|
||||
tips: [
|
||||
'87% coverage',
|
||||
'Includes auth examples',
|
||||
'Human-readable format'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Returns human-readable documentation parsed from n8n-docs including examples, authentication setup, and common patterns. More useful than raw schema for understanding node usage.',
|
||||
parameters: {
|
||||
nodeType: { type: 'string', required: true, description: 'Full node type with prefix (e.g., "nodes-base.slack")' }
|
||||
},
|
||||
returns: 'Parsed markdown documentation with examples, authentication guides, common patterns',
|
||||
examples: [
|
||||
'get_node_documentation({nodeType: "nodes-base.slack"}) - Slack usage guide',
|
||||
'get_node_documentation({nodeType: "nodes-base.googleSheets"}) - Sheets examples'
|
||||
],
|
||||
useCases: [
|
||||
'Understanding authentication setup',
|
||||
'Finding usage examples',
|
||||
'Learning common patterns'
|
||||
],
|
||||
performance: 'Fast - Pre-parsed documentation stored in database',
|
||||
bestPractices: [
|
||||
'Use for learning node usage',
|
||||
'Check coverage with get_database_statistics',
|
||||
'Combine with get_node_essentials'
|
||||
],
|
||||
pitfalls: [
|
||||
'Not all nodes have docs (87% coverage)',
|
||||
'May be outdated for new features',
|
||||
'Requires full node type prefix'
|
||||
],
|
||||
relatedTools: ['get_node_info', 'get_node_essentials', 'search_nodes']
|
||||
}
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const getNodeEssentialsDoc: ToolDocumentation = {
|
||||
name: 'get_node_essentials',
|
||||
category: 'configuration',
|
||||
essentials: {
|
||||
description: 'Returns only the most commonly-used properties for a node (10-20 fields). Response is 95% smaller than get_node_info (5KB vs 100KB+). Essential properties include required fields, common options, and authentication settings. Use validate_node_operation for working configurations.',
|
||||
keyParameters: ['nodeType'],
|
||||
example: 'get_node_essentials({nodeType: "nodes-base.slack"})',
|
||||
performance: '<10ms, ~5KB response',
|
||||
tips: [
|
||||
'Always use this before get_node_info',
|
||||
'Use validate_node_operation for examples',
|
||||
'Perfect for understanding node structure'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Returns a curated subset of node properties focusing on the most commonly-used fields. Essential properties are hand-picked for each node type and include: required fields, primary operations, authentication options, and the most frequent configuration patterns. NOTE: Examples have been removed to avoid confusion - use validate_node_operation to get working configurations with proper validation.',
|
||||
parameters: {
|
||||
nodeType: { type: 'string', description: 'Full node type with prefix, e.g., "nodes-base.slack", "nodes-base.httpRequest"', required: true }
|
||||
},
|
||||
returns: `Object containing:
|
||||
{
|
||||
"nodeType": "nodes-base.slack",
|
||||
"displayName": "Slack",
|
||||
"description": "Consume Slack API",
|
||||
"category": "output",
|
||||
"version": "2.3",
|
||||
"requiredProperties": [], // Most nodes have no strictly required fields
|
||||
"commonProperties": [
|
||||
{
|
||||
"name": "resource",
|
||||
"displayName": "Resource",
|
||||
"type": "options",
|
||||
"options": ["channel", "message", "user"],
|
||||
"default": "message"
|
||||
},
|
||||
{
|
||||
"name": "operation",
|
||||
"displayName": "Operation",
|
||||
"type": "options",
|
||||
"options": ["post", "update", "delete"],
|
||||
"default": "post"
|
||||
},
|
||||
// ... 10-20 most common properties
|
||||
],
|
||||
"operations": [
|
||||
{"name": "Post", "description": "Post a message"},
|
||||
{"name": "Update", "description": "Update a message"}
|
||||
],
|
||||
"metadata": {
|
||||
"totalProperties": 121,
|
||||
"isAITool": false,
|
||||
"hasCredentials": true
|
||||
}
|
||||
}`,
|
||||
examples: [
|
||||
'get_node_essentials({nodeType: "nodes-base.httpRequest"}) - HTTP configuration basics',
|
||||
'get_node_essentials({nodeType: "nodes-base.slack"}) - Slack messaging essentials',
|
||||
'get_node_essentials({nodeType: "nodes-base.googleSheets"}) - Sheets operations',
|
||||
'// Workflow: search → essentials → validate',
|
||||
'const nodes = search_nodes({query: "database"});',
|
||||
'const mysql = get_node_essentials({nodeType: "nodes-base.mySql"});',
|
||||
'validate_node_operation("nodes-base.mySql", {operation: "select"}, "minimal");'
|
||||
],
|
||||
useCases: [
|
||||
'Quickly understand node structure without information overload',
|
||||
'Identify which properties are most important',
|
||||
'Learn node basics before diving into advanced features',
|
||||
'Build workflows faster with curated property sets'
|
||||
],
|
||||
performance: '<10ms response time, ~5KB payload (vs 100KB+ for full schema)',
|
||||
bestPractices: [
|
||||
'Always start with essentials, only use get_node_info if needed',
|
||||
'Use validate_node_operation to get working configurations',
|
||||
'Check authentication requirements first',
|
||||
'Use search_node_properties if specific property not in essentials'
|
||||
],
|
||||
pitfalls: [
|
||||
'Advanced properties not included - use get_node_info for complete schema',
|
||||
'Node-specific validators may require additional fields',
|
||||
'Some nodes have 50+ properties, essentials shows only top 10-20'
|
||||
],
|
||||
relatedTools: ['get_node_info for complete schema', 'search_node_properties for finding specific fields', 'validate_node_minimal to check configuration']
|
||||
}
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const getNodeInfoDoc: ToolDocumentation = {
|
||||
name: 'get_node_info',
|
||||
category: 'configuration',
|
||||
essentials: {
|
||||
description: 'Returns complete node schema with ALL properties (100KB+ response). Only use when you need advanced properties not in get_node_essentials. Contains 200+ properties for complex nodes like HTTP Request. Requires full prefix like "nodes-base.httpRequest".',
|
||||
keyParameters: ['nodeType'],
|
||||
example: 'get_node_info({nodeType: "nodes-base.slack"})',
|
||||
performance: '100-500ms, 50-500KB response',
|
||||
tips: [
|
||||
'Try get_node_essentials first (95% smaller)',
|
||||
'Use only for advanced configurations',
|
||||
'Response may have 200+ properties'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Returns the complete JSON schema for a node including all properties, operations, authentication methods, version information, and metadata. Response sizes range from 50KB to 500KB. Use this only when get_node_essentials doesn\'t provide the specific property you need.',
|
||||
parameters: {
|
||||
nodeType: { type: 'string', required: true, description: 'Full node type with prefix. Examples: "nodes-base.slack", "nodes-base.httpRequest", "nodes-langchain.openAi"' }
|
||||
},
|
||||
returns: `Complete node object containing:
|
||||
{
|
||||
"displayName": "Slack",
|
||||
"name": "slack",
|
||||
"type": "nodes-base.slack",
|
||||
"typeVersion": 2.2,
|
||||
"description": "Consume Slack API",
|
||||
"defaults": {"name": "Slack"},
|
||||
"inputs": ["main"],
|
||||
"outputs": ["main"],
|
||||
"credentials": [
|
||||
{
|
||||
"name": "slackApi",
|
||||
"required": true,
|
||||
"displayOptions": {...}
|
||||
}
|
||||
],
|
||||
"properties": [
|
||||
// 200+ property definitions including:
|
||||
{
|
||||
"displayName": "Resource",
|
||||
"name": "resource",
|
||||
"type": "options",
|
||||
"options": ["channel", "message", "user", "file", ...],
|
||||
"default": "message"
|
||||
},
|
||||
{
|
||||
"displayName": "Operation",
|
||||
"name": "operation",
|
||||
"type": "options",
|
||||
"displayOptions": {
|
||||
"show": {"resource": ["message"]}
|
||||
},
|
||||
"options": ["post", "update", "delete", "get", ...],
|
||||
"default": "post"
|
||||
},
|
||||
// ... 200+ more properties with complex conditions
|
||||
],
|
||||
"version": 2.2,
|
||||
"subtitle": "={{$parameter[\"operation\"] + \": \" + $parameter[\"resource\"]}}",
|
||||
"codex": {...},
|
||||
"supportedWebhooks": [...]
|
||||
}`,
|
||||
examples: [
|
||||
'get_node_info({nodeType: "nodes-base.httpRequest"}) - 300+ properties for HTTP requests',
|
||||
'get_node_info({nodeType: "nodes-base.googleSheets"}) - Complex operations and auth',
|
||||
'// When to use get_node_info:',
|
||||
'// 1. First try essentials',
|
||||
'const essentials = get_node_essentials({nodeType: "nodes-base.slack"});',
|
||||
'// 2. If property missing, search for it',
|
||||
'const props = search_node_properties({nodeType: "nodes-base.slack", query: "thread"});',
|
||||
'// 3. Only if needed, get full schema',
|
||||
'const full = get_node_info({nodeType: "nodes-base.slack"});'
|
||||
],
|
||||
useCases: [
|
||||
'Analyzing all available operations for a node',
|
||||
'Understanding complex property dependencies',
|
||||
'Discovering all authentication methods',
|
||||
'Building UI that shows all node options',
|
||||
'Debugging property visibility conditions'
|
||||
],
|
||||
performance: '100-500ms depending on node complexity. HTTP Request node: ~300KB, Simple nodes: ~50KB',
|
||||
bestPractices: [
|
||||
'Always try get_node_essentials first - it\'s 95% smaller',
|
||||
'Use search_node_properties to find specific advanced properties',
|
||||
'Cache results locally - schemas rarely change',
|
||||
'Parse incrementally - don\'t load entire response into memory at once'
|
||||
],
|
||||
pitfalls: [
|
||||
'Response can exceed 500KB for complex nodes',
|
||||
'Contains many rarely-used properties that add noise',
|
||||
'Property conditions can be deeply nested and complex',
|
||||
'Must use full node type with prefix (nodes-base.X not just X)'
|
||||
],
|
||||
relatedTools: ['get_node_essentials for common properties', 'search_node_properties to find specific fields', 'get_property_dependencies to understand conditions']
|
||||
}
|
||||
};
|
||||
88
src/mcp/tool-docs/configuration/get-node.ts
Normal file
88
src/mcp/tool-docs/configuration/get-node.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const getNodeDoc: ToolDocumentation = {
|
||||
name: 'get_node',
|
||||
category: 'configuration',
|
||||
essentials: {
|
||||
description: 'Unified node information tool with progressive detail levels and multiple modes. Get node schema, docs, search properties, or version info.',
|
||||
keyParameters: ['nodeType', 'detail', 'mode', 'includeTypeInfo', 'includeExamples'],
|
||||
example: 'get_node({nodeType: "nodes-base.httpRequest", detail: "standard"})',
|
||||
performance: 'Instant (<10ms) for minimal/standard, moderate for full',
|
||||
tips: [
|
||||
'Use detail="standard" (default) for most tasks - shows required fields',
|
||||
'Use mode="docs" for readable markdown documentation',
|
||||
'Use mode="search_properties" with propertyQuery to find specific fields',
|
||||
'Use mode="versions" to check version history and breaking changes',
|
||||
'Add includeExamples=true to get real-world configuration examples'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `**Detail Levels (mode="info", default):**
|
||||
- minimal (~200 tokens): Basic metadata only - nodeType, displayName, description, category
|
||||
- standard (~1-2K tokens): Essential properties + operations - recommended for most tasks
|
||||
- full (~3-8K tokens): Complete node schema - use only when standard insufficient
|
||||
|
||||
**Operation Modes:**
|
||||
- info (default): Node schema with configurable detail level
|
||||
- docs: Readable markdown documentation with examples and patterns
|
||||
- search_properties: Find specific properties within a node
|
||||
- versions: List all available versions with breaking changes summary
|
||||
- compare: Compare two versions with property-level changes
|
||||
- breaking: Show only breaking changes between versions
|
||||
- migrations: Show auto-migratable changes between versions`,
|
||||
parameters: {
|
||||
nodeType: { type: 'string', required: true, description: 'Full node type with prefix: "nodes-base.httpRequest" or "nodes-langchain.agent"' },
|
||||
detail: { type: 'string', required: false, description: 'Detail level for mode=info: "minimal", "standard" (default), "full"' },
|
||||
mode: { type: 'string', required: false, description: 'Operation mode: "info" (default), "docs", "search_properties", "versions", "compare", "breaking", "migrations"' },
|
||||
includeTypeInfo: { type: 'boolean', required: false, description: 'Include type structure metadata (validation rules, JS types). Adds ~80-120 tokens per property' },
|
||||
includeExamples: { type: 'boolean', required: false, description: 'Include real-world configuration examples from templates. Adds ~200-400 tokens per example' },
|
||||
propertyQuery: { type: 'string', required: false, description: 'For mode=search_properties: search term to find properties (e.g., "auth", "header", "body")' },
|
||||
maxPropertyResults: { type: 'number', required: false, description: 'For mode=search_properties: max results (default 20)' },
|
||||
fromVersion: { type: 'string', required: false, description: 'For compare/breaking/migrations modes: source version (e.g., "1.0")' },
|
||||
toVersion: { type: 'string', required: false, description: 'For compare mode: target version (e.g., "2.0"). Defaults to latest' }
|
||||
},
|
||||
returns: `Depends on mode:
|
||||
- info: Node schema with properties based on detail level
|
||||
- docs: Markdown documentation string
|
||||
- search_properties: Array of matching property paths with descriptions
|
||||
- versions: Version history with breaking changes flags
|
||||
- compare/breaking/migrations: Version comparison details`,
|
||||
examples: [
|
||||
'// Standard detail (recommended for AI agents)\nget_node({nodeType: "nodes-base.httpRequest"})',
|
||||
'// Minimal for quick metadata check\nget_node({nodeType: "nodes-base.slack", detail: "minimal"})',
|
||||
'// Full detail with examples\nget_node({nodeType: "nodes-base.googleSheets", detail: "full", includeExamples: true})',
|
||||
'// Get readable documentation\nget_node({nodeType: "nodes-base.webhook", mode: "docs"})',
|
||||
'// Search for authentication properties\nget_node({nodeType: "nodes-base.httpRequest", mode: "search_properties", propertyQuery: "auth"})',
|
||||
'// Check version history\nget_node({nodeType: "nodes-base.executeWorkflow", mode: "versions"})',
|
||||
'// Compare specific versions\nget_node({nodeType: "nodes-base.httpRequest", mode: "compare", fromVersion: "3.0", toVersion: "4.1"})'
|
||||
],
|
||||
useCases: [
|
||||
'Configure nodes for workflow building (use detail=standard)',
|
||||
'Find specific configuration options (use mode=search_properties)',
|
||||
'Get human-readable node documentation (use mode=docs)',
|
||||
'Check for breaking changes before version upgrades (use mode=breaking)',
|
||||
'Understand complex types with includeTypeInfo=true'
|
||||
],
|
||||
performance: `Token costs by detail level:
|
||||
- minimal: ~200 tokens
|
||||
- standard: ~1000-2000 tokens (default)
|
||||
- full: ~3000-8000 tokens
|
||||
- includeTypeInfo: +80-120 tokens per property
|
||||
- includeExamples: +200-400 tokens per example
|
||||
- Version modes: ~400-1200 tokens`,
|
||||
bestPractices: [
|
||||
'Start with detail="standard" - it covers 95% of use cases',
|
||||
'Only use detail="full" if standard is missing required properties',
|
||||
'Use mode="docs" when explaining nodes to users',
|
||||
'Combine includeTypeInfo=true for complex nodes (filter, resourceMapper)',
|
||||
'Check version history before configuring versioned nodes'
|
||||
],
|
||||
pitfalls: [
|
||||
'detail="full" returns large responses (~100KB) - use sparingly',
|
||||
'Node type must include prefix (nodes-base. or nodes-langchain.)',
|
||||
'includeExamples only works with mode=info and detail=standard',
|
||||
'Version modes require nodes with multiple versions in database'
|
||||
],
|
||||
relatedTools: ['search_nodes', 'validate_node', 'validate_workflow']
|
||||
}
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const getPropertyDependenciesDoc: ToolDocumentation = {
|
||||
name: 'get_property_dependencies',
|
||||
category: 'configuration',
|
||||
essentials: {
|
||||
description: 'Shows property dependencies and visibility rules - which fields appear when.',
|
||||
keyParameters: ['nodeType', 'config?'],
|
||||
example: 'get_property_dependencies({nodeType: "nodes-base.httpRequest"})',
|
||||
performance: 'Fast - analyzes property conditions',
|
||||
tips: [
|
||||
'Shows which properties depend on other property values',
|
||||
'Test visibility impact with optional config parameter',
|
||||
'Helps understand complex conditional property displays'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Analyzes property dependencies and visibility conditions for a node. Shows which properties control the visibility of other properties (e.g., sendBody=true reveals body-related fields). Optionally test how a specific configuration affects property visibility.`,
|
||||
parameters: {
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The node type to analyze (e.g., "nodes-base.httpRequest")',
|
||||
examples: [
|
||||
'nodes-base.httpRequest',
|
||||
'nodes-base.slack',
|
||||
'nodes-base.if',
|
||||
'nodes-base.switch'
|
||||
]
|
||||
},
|
||||
config: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
description: 'Optional partial configuration to check visibility impact',
|
||||
examples: [
|
||||
'{ method: "POST", sendBody: true }',
|
||||
'{ operation: "create", resource: "contact" }',
|
||||
'{ mode: "rules" }'
|
||||
]
|
||||
}
|
||||
},
|
||||
returns: `Object containing:
|
||||
- nodeType: The analyzed node type
|
||||
- displayName: Human-readable node name
|
||||
- controllingProperties: Properties that control visibility of others
|
||||
- dependentProperties: Properties whose visibility depends on others
|
||||
- complexDependencies: Multi-condition dependencies
|
||||
- currentConfig: If config provided, shows:
|
||||
- providedValues: The configuration you passed
|
||||
- visibilityImpact: Which properties are visible/hidden`,
|
||||
examples: [
|
||||
'get_property_dependencies({nodeType: "nodes-base.httpRequest"}) - Analyze HTTP Request dependencies',
|
||||
'get_property_dependencies({nodeType: "nodes-base.httpRequest", config: {sendBody: true}}) - Test visibility with sendBody enabled',
|
||||
'get_property_dependencies({nodeType: "nodes-base.if", config: {mode: "rules"}}) - Check If node in rules mode'
|
||||
],
|
||||
useCases: [
|
||||
'Understanding which properties control others',
|
||||
'Debugging why certain fields are not visible',
|
||||
'Building dynamic UIs that match n8n behavior',
|
||||
'Testing configurations before applying them',
|
||||
'Understanding complex node property relationships'
|
||||
],
|
||||
performance: 'Fast - analyzes property metadata without database queries',
|
||||
bestPractices: [
|
||||
'Use before configuring complex nodes with many conditional fields',
|
||||
'Test different config values to understand visibility rules',
|
||||
'Check dependencies when properties seem to be missing',
|
||||
'Use for nodes with multiple operation modes (Slack, Google Sheets)',
|
||||
'Combine with search_node_properties to find specific fields'
|
||||
],
|
||||
pitfalls: [
|
||||
'Some properties have complex multi-condition dependencies',
|
||||
'Visibility rules can be nested (property A controls B which controls C)',
|
||||
'Not all hidden properties are due to dependencies (some are deprecated)',
|
||||
'Config parameter only tests visibility, does not validate values'
|
||||
],
|
||||
relatedTools: ['search_node_properties', 'get_node_essentials', 'validate_node_operation']
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1 @@
|
||||
export { getNodeInfoDoc } from './get-node-info';
|
||||
export { getNodeEssentialsDoc } from './get-node-essentials';
|
||||
export { getNodeDocumentationDoc } from './get-node-documentation';
|
||||
export { searchNodePropertiesDoc } from './search-node-properties';
|
||||
export { getNodeAsToolInfoDoc } from './get-node-as-tool-info';
|
||||
export { getPropertyDependenciesDoc } from './get-property-dependencies';
|
||||
export { getNodeDoc } from './get-node';
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const searchNodePropertiesDoc: ToolDocumentation = {
|
||||
name: 'search_node_properties',
|
||||
category: 'configuration',
|
||||
essentials: {
|
||||
description: 'Find specific properties in a node without downloading all 200+ properties.',
|
||||
keyParameters: ['nodeType', 'query'],
|
||||
example: 'search_node_properties({nodeType: "nodes-base.httpRequest", query: "auth"})',
|
||||
performance: 'Fast - searches indexed properties',
|
||||
tips: [
|
||||
'Search for "auth", "header", "body", "json", "credential"',
|
||||
'Returns property paths and descriptions',
|
||||
'Much faster than get_node_info for finding specific fields'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Searches for specific properties within a node's configuration schema. Essential for finding authentication fields, headers, body parameters, or any specific property without downloading the entire node schema (which can be 100KB+). Returns matching properties with their paths, types, and descriptions.`,
|
||||
parameters: {
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Full type with prefix',
|
||||
examples: [
|
||||
'nodes-base.httpRequest',
|
||||
'nodes-base.slack',
|
||||
'nodes-base.postgres',
|
||||
'nodes-base.googleSheets'
|
||||
]
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Property to find: "auth", "header", "body", "json"',
|
||||
examples: [
|
||||
'auth',
|
||||
'header',
|
||||
'body',
|
||||
'json',
|
||||
'credential',
|
||||
'timeout',
|
||||
'retry',
|
||||
'pagination'
|
||||
]
|
||||
},
|
||||
maxResults: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Max results (default 20)',
|
||||
default: 20
|
||||
}
|
||||
},
|
||||
returns: `Object containing:
|
||||
- nodeType: The searched node type
|
||||
- query: Your search term
|
||||
- matches: Array of matching properties with:
|
||||
- name: Property identifier
|
||||
- displayName: Human-readable name
|
||||
- type: Property type (string, number, options, etc.)
|
||||
- description: Property description
|
||||
- path: Full path to property (for nested properties)
|
||||
- required: Whether property is required
|
||||
- default: Default value if any
|
||||
- options: Available options for selection properties
|
||||
- showWhen: Visibility conditions
|
||||
- totalMatches: Number of matches found
|
||||
- searchedIn: Total properties searched`,
|
||||
examples: [
|
||||
'search_node_properties({nodeType: "nodes-base.httpRequest", query: "auth"}) - Find authentication fields',
|
||||
'search_node_properties({nodeType: "nodes-base.slack", query: "channel"}) - Find channel-related properties',
|
||||
'search_node_properties({nodeType: "nodes-base.postgres", query: "query"}) - Find query fields',
|
||||
'search_node_properties({nodeType: "nodes-base.webhook", query: "response"}) - Find response options'
|
||||
],
|
||||
useCases: [
|
||||
'Finding authentication/credential fields quickly',
|
||||
'Locating specific parameters without full node info',
|
||||
'Discovering header or body configuration options',
|
||||
'Finding nested properties in complex nodes',
|
||||
'Checking if a node supports specific features (retry, pagination, etc.)'
|
||||
],
|
||||
performance: 'Very fast - searches pre-indexed property metadata',
|
||||
bestPractices: [
|
||||
'Use before get_node_info to find specific properties',
|
||||
'Search for common terms: auth, header, body, credential',
|
||||
'Check showWhen conditions to understand visibility',
|
||||
'Use with get_property_dependencies for complete understanding',
|
||||
'Limit results if you only need to check existence'
|
||||
],
|
||||
pitfalls: [
|
||||
'Some properties may be hidden due to visibility conditions',
|
||||
'Property names may differ from display names',
|
||||
'Nested properties show full path (e.g., "options.retry.limit")',
|
||||
'Search is case-sensitive for property names'
|
||||
],
|
||||
relatedTools: ['get_node_essentials', 'get_property_dependencies', 'get_node_info']
|
||||
}
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const getDatabaseStatisticsDoc: ToolDocumentation = {
|
||||
name: 'get_database_statistics',
|
||||
category: 'discovery',
|
||||
essentials: {
|
||||
description: 'Returns database health metrics and node inventory. Shows 525 total nodes, 263 AI-capable nodes, 104 triggers, with 87% documentation coverage. Primary use: verify MCP connection is working correctly.',
|
||||
keyParameters: [],
|
||||
example: 'get_database_statistics()',
|
||||
performance: 'Instant',
|
||||
tips: [
|
||||
'First tool to call when testing MCP connection',
|
||||
'Shows exact counts for all node categories',
|
||||
'Documentation coverage indicates data quality'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Returns comprehensive database statistics showing the complete inventory of n8n nodes, their categories, documentation coverage, and package distribution. Essential for verifying MCP connectivity and understanding available resources.',
|
||||
parameters: {},
|
||||
returns: `Object containing:
|
||||
{
|
||||
"total_nodes": 525, // All nodes in database
|
||||
"nodes_with_properties": 520, // Nodes with extracted properties (99%)
|
||||
"nodes_with_operations": 334, // Nodes with multiple operations (64%)
|
||||
"ai_tools": 263, // AI-capable nodes
|
||||
"triggers": 104, // Workflow trigger nodes
|
||||
"documentation_coverage": "87%", // Nodes with official docs
|
||||
"packages": {
|
||||
"n8n-nodes-base": 456, // Core n8n nodes
|
||||
"@n8n/n8n-nodes-langchain": 69 // AI/LangChain nodes
|
||||
},
|
||||
"categories": {
|
||||
"trigger": 104,
|
||||
"transform": 250,
|
||||
"output": 45,
|
||||
"input": 38,
|
||||
"AI": 88
|
||||
}
|
||||
}`,
|
||||
examples: [
|
||||
'get_database_statistics() - Returns complete statistics object',
|
||||
'// Common check:',
|
||||
'const stats = get_database_statistics();',
|
||||
'if (stats.total_nodes < 500) console.error("Database incomplete!");'
|
||||
],
|
||||
useCases: [
|
||||
'Verify MCP server is connected and responding',
|
||||
'Check if database rebuild is needed (low node count)',
|
||||
'Monitor documentation coverage improvements',
|
||||
'Validate AI tools availability for workflows',
|
||||
'Audit node distribution across packages'
|
||||
],
|
||||
performance: 'Instant (<1ms) - Statistics are pre-calculated and cached',
|
||||
bestPractices: [
|
||||
'Call this first to verify MCP connection before other operations',
|
||||
'Check total_nodes >= 500 to ensure complete database',
|
||||
'Monitor documentation_coverage for data quality',
|
||||
'Use ai_tools count to verify AI capabilities'
|
||||
],
|
||||
pitfalls: [
|
||||
'Statistics are cached at database build time, not real-time',
|
||||
'Won\'t reflect changes until database is rebuilt',
|
||||
'Package counts may vary with n8n version updates'
|
||||
],
|
||||
relatedTools: ['list_nodes for detailed node listing', 'list_ai_tools for AI nodes', 'n8n_health_check for API connectivity']
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1 @@
|
||||
export { searchNodesDoc } from './search-nodes';
|
||||
export { listNodesDoc } from './list-nodes';
|
||||
export { listAiToolsDoc } from './list-ai-tools';
|
||||
export { getDatabaseStatisticsDoc } from './get-database-statistics';
|
||||
@@ -1,51 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const listAiToolsDoc: ToolDocumentation = {
|
||||
name: 'list_ai_tools',
|
||||
category: 'discovery',
|
||||
essentials: {
|
||||
description: 'DEPRECATED: Basic list of 263 AI nodes. For comprehensive AI Agent guidance, use tools_documentation({topic: "ai_agents_guide"}). That guide covers architecture, connections, tools, validation, and best practices. Use search_nodes({query: "AI", includeExamples: true}) for AI nodes with working examples.',
|
||||
keyParameters: [],
|
||||
example: 'tools_documentation({topic: "ai_agents_guide"}) // Recommended alternative',
|
||||
performance: 'Instant (cached)',
|
||||
tips: [
|
||||
'NEW: Use ai_agents_guide for comprehensive AI workflow documentation',
|
||||
'Use search_nodes({includeExamples: true}) for AI nodes with real-world examples',
|
||||
'ANY node can be an AI tool - not limited to AI-specific nodes',
|
||||
'Use get_node_as_tool_info for guidance on any node'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: '**DEPRECATED in favor of ai_agents_guide**. Lists 263 nodes with built-in AI capabilities. For comprehensive documentation on building AI Agent workflows, use tools_documentation({topic: "ai_agents_guide"}) which covers architecture, the 8 AI connection types, validation, and best practices with real examples. IMPORTANT: This basic list is NOT a complete guide - use the full AI Agents guide instead.',
|
||||
parameters: {},
|
||||
returns: 'Array of 263 AI-optimized nodes. RECOMMENDED: Use ai_agents_guide for comprehensive guidance, or search_nodes({query: "AI", includeExamples: true}) for AI nodes with working configuration examples.',
|
||||
examples: [
|
||||
'// RECOMMENDED: Use the comprehensive AI Agents guide',
|
||||
'tools_documentation({topic: "ai_agents_guide"})',
|
||||
'',
|
||||
'// Or search for AI nodes with real-world examples',
|
||||
'search_nodes({query: "AI Agent", includeExamples: true})',
|
||||
'',
|
||||
'// Basic list (deprecated)',
|
||||
'list_ai_tools() - Returns 263 AI-optimized nodes'
|
||||
],
|
||||
useCases: [
|
||||
'Discover AI model integrations (OpenAI, Anthropic, Google AI)',
|
||||
'Find vector databases for RAG applications',
|
||||
'Locate embedding generators and processors',
|
||||
'Build AI agent tool chains with ANY n8n node'
|
||||
],
|
||||
performance: 'Instant - results are pre-cached in memory',
|
||||
bestPractices: [
|
||||
'Remember: ANY node works as an AI tool when connected to AI Agent',
|
||||
'Common non-AI nodes used as tools: Slack (messaging), Google Sheets (data), HTTP Request (APIs), Code (custom logic)',
|
||||
'For community nodes: set N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true'
|
||||
],
|
||||
pitfalls: [
|
||||
'This list is NOT exhaustive - it only shows nodes with AI-specific features',
|
||||
'Don\'t limit yourself to this list when building AI workflows',
|
||||
'Community nodes require environment variable to work as tools'
|
||||
],
|
||||
relatedTools: ['get_node_as_tool_info for any node usage', 'search_nodes to find specific nodes', 'get_node_essentials to configure nodes']
|
||||
}
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const listNodesDoc: ToolDocumentation = {
|
||||
name: 'list_nodes',
|
||||
category: 'discovery',
|
||||
essentials: {
|
||||
description: 'Lists n8n nodes with filtering options. Returns up to 525 total nodes. Default limit is 50, use limit:200 to get all nodes. Filter by category to find specific node types like triggers (104 nodes) or AI nodes (263 nodes).',
|
||||
keyParameters: ['category', 'package', 'limit', 'isAITool'],
|
||||
example: 'list_nodes({limit:200})',
|
||||
performance: '<10ms for any query size',
|
||||
tips: [
|
||||
'Use limit:200 to get all 525 nodes',
|
||||
'Categories: trigger (104), transform (250+), output/input (50+)',
|
||||
'Use search_nodes for keyword search'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Lists n8n nodes with comprehensive filtering options. Returns an array of node metadata including type, name, description, and category. Database contains 525 total nodes: 456 from n8n-nodes-base package and 69 from @n8n/n8n-nodes-langchain package.',
|
||||
parameters: {
|
||||
category: { type: 'string', description: 'Filter by category: "trigger" (104 nodes), "transform" (250+ nodes), "output", "input", or "AI"', required: false },
|
||||
package: { type: 'string', description: 'Filter by package: "n8n-nodes-base" (456 core nodes) or "@n8n/n8n-nodes-langchain" (69 AI nodes)', required: false },
|
||||
limit: { type: 'number', description: 'Maximum results to return. Default: 50. Use 200+ to get all 525 nodes', required: false },
|
||||
isAITool: { type: 'boolean', description: 'Filter to show only AI-capable nodes (263 nodes)', required: false },
|
||||
developmentStyle: { type: 'string', description: 'Filter by style: "programmatic" or "declarative". Most nodes are programmatic', required: false }
|
||||
},
|
||||
returns: 'Array of node objects, each containing: nodeType (e.g., "nodes-base.webhook"), displayName (e.g., "Webhook"), description, category, package, isAITool flag',
|
||||
examples: [
|
||||
'list_nodes({limit:200}) - Returns all 525 nodes',
|
||||
'list_nodes({category:"trigger"}) - Returns 104 trigger nodes (Webhook, Schedule, Email Trigger, etc.)',
|
||||
'list_nodes({package:"@n8n/n8n-nodes-langchain"}) - Returns 69 AI/LangChain nodes',
|
||||
'list_nodes({isAITool:true}) - Returns 263 AI-capable nodes',
|
||||
'list_nodes({category:"trigger", isAITool:true}) - Combines filters for AI-capable triggers'
|
||||
],
|
||||
useCases: [
|
||||
'Browse all available nodes when building workflows',
|
||||
'Find all trigger nodes to start workflows',
|
||||
'Discover AI/ML nodes for intelligent automation',
|
||||
'Check available nodes in specific packages'
|
||||
],
|
||||
performance: '<10ms for any query size. Results are cached in memory',
|
||||
bestPractices: [
|
||||
'Use limit:200 when you need the complete node inventory',
|
||||
'Filter by category for focused discovery',
|
||||
'Combine with get_node_essentials to configure selected nodes'
|
||||
],
|
||||
pitfalls: [
|
||||
'No text search capability - use search_nodes for keyword search',
|
||||
'developmentStyle filter rarely useful - most nodes are "programmatic"'
|
||||
],
|
||||
relatedTools: ['search_nodes for keyword search', 'list_ai_tools for AI-specific discovery', 'get_node_essentials to configure nodes']
|
||||
}
|
||||
};
|
||||
@@ -4,7 +4,7 @@ export const searchNodesDoc: ToolDocumentation = {
|
||||
name: 'search_nodes',
|
||||
category: 'discovery',
|
||||
essentials: {
|
||||
description: 'Text search across node names and descriptions. Returns most relevant nodes first, with frequently-used nodes (HTTP Request, Webhook, Set, Code, Slack) prioritized in results. Searches all 525 nodes in the database.',
|
||||
description: 'Text search across node names and descriptions. Returns most relevant nodes first, with frequently-used nodes (HTTP Request, Webhook, Set, Code, Slack) prioritized in results. Searches all 500+ nodes in the database.',
|
||||
keyParameters: ['query', 'mode', 'limit'],
|
||||
example: 'search_nodes({query: "webhook"})',
|
||||
performance: '<20ms even for complex queries',
|
||||
@@ -42,13 +42,13 @@ export const searchNodesDoc: ToolDocumentation = {
|
||||
'Start with single keywords for broadest results',
|
||||
'Use FUZZY mode when users might misspell node names',
|
||||
'AND mode works best for 2-3 word searches',
|
||||
'Combine with get_node_essentials after finding the right node'
|
||||
'Combine with get_node after finding the right node'
|
||||
],
|
||||
pitfalls: [
|
||||
'AND mode searches all fields (name, description) not just node names',
|
||||
'FUZZY mode with very short queries (1-2 chars) may return unexpected results',
|
||||
'Exact matches in quotes are case-sensitive'
|
||||
],
|
||||
relatedTools: ['list_nodes for browsing by category', 'get_node_essentials to configure found nodes', 'list_ai_tools for AI-specific search']
|
||||
relatedTools: ['get_node to configure found nodes', 'search_templates to find workflow examples', 'validate_node to check configurations']
|
||||
}
|
||||
};
|
||||
@@ -690,7 +690,7 @@ n8n_validate_workflow({id: "workflow_id"})
|
||||
- **FINAL_AI_VALIDATION_SPEC.md**: Complete validation rules
|
||||
- **n8n_update_partial_workflow**: Workflow modification tool
|
||||
- **search_nodes({query: "AI", includeExamples: true})**: Find AI nodes with examples
|
||||
- **get_node_essentials({nodeType: "...", includeExamples: true})**: Node details with examples
|
||||
- **get_node({nodeType: "...", detail: "standard", includeExamples: true})**: Node details with examples
|
||||
|
||||
---
|
||||
|
||||
@@ -731,8 +731,7 @@ n8n_validate_workflow({id: "workflow_id"})
|
||||
'n8n_update_partial_workflow',
|
||||
'n8n_validate_workflow',
|
||||
'search_nodes',
|
||||
'get_node_essentials',
|
||||
'list_ai_tools'
|
||||
'get_node'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,45 +1,18 @@
|
||||
import { ToolDocumentation } from './types';
|
||||
|
||||
// Import all tool documentations
|
||||
import { searchNodesDoc, listNodesDoc, listAiToolsDoc, getDatabaseStatisticsDoc } from './discovery';
|
||||
import {
|
||||
getNodeEssentialsDoc,
|
||||
getNodeInfoDoc,
|
||||
getNodeDocumentationDoc,
|
||||
searchNodePropertiesDoc,
|
||||
getNodeAsToolInfoDoc,
|
||||
getPropertyDependenciesDoc
|
||||
} from './configuration';
|
||||
import {
|
||||
validateNodeMinimalDoc,
|
||||
validateNodeOperationDoc,
|
||||
validateWorkflowDoc,
|
||||
validateWorkflowConnectionsDoc,
|
||||
validateWorkflowExpressionsDoc
|
||||
} from './validation';
|
||||
import {
|
||||
listTasksDoc,
|
||||
listNodeTemplatesDoc,
|
||||
getTemplateDoc,
|
||||
searchTemplatesDoc,
|
||||
searchTemplatesByMetadataDoc,
|
||||
getTemplatesForTaskDoc
|
||||
} from './templates';
|
||||
import { searchNodesDoc } from './discovery';
|
||||
import { getNodeDoc } from './configuration';
|
||||
import { validateNodeDoc, validateWorkflowDoc } from './validation';
|
||||
import { getTemplateDoc, searchTemplatesDoc } from './templates';
|
||||
import {
|
||||
toolsDocumentationDoc,
|
||||
n8nDiagnosticDoc,
|
||||
n8nHealthCheckDoc,
|
||||
n8nListAvailableToolsDoc
|
||||
n8nHealthCheckDoc
|
||||
} from './system';
|
||||
import {
|
||||
aiAgentsGuide
|
||||
} from './guides';
|
||||
import { aiAgentsGuide } from './guides';
|
||||
import {
|
||||
n8nCreateWorkflowDoc,
|
||||
n8nGetWorkflowDoc,
|
||||
n8nGetWorkflowDetailsDoc,
|
||||
n8nGetWorkflowStructureDoc,
|
||||
n8nGetWorkflowMinimalDoc,
|
||||
n8nUpdateFullWorkflowDoc,
|
||||
n8nUpdatePartialWorkflowDoc,
|
||||
n8nDeleteWorkflowDoc,
|
||||
@@ -47,57 +20,37 @@ import {
|
||||
n8nValidateWorkflowDoc,
|
||||
n8nAutofixWorkflowDoc,
|
||||
n8nTriggerWebhookWorkflowDoc,
|
||||
n8nGetExecutionDoc,
|
||||
n8nListExecutionsDoc,
|
||||
n8nDeleteExecutionDoc
|
||||
n8nExecutionsDoc,
|
||||
n8nWorkflowVersionsDoc,
|
||||
n8nDeployTemplateDoc
|
||||
} from './workflow_management';
|
||||
|
||||
// Combine all tool documentations into a single object
|
||||
export const toolsDocumentation: Record<string, ToolDocumentation> = {
|
||||
// System tools
|
||||
tools_documentation: toolsDocumentationDoc,
|
||||
n8n_diagnostic: n8nDiagnosticDoc,
|
||||
n8n_health_check: n8nHealthCheckDoc,
|
||||
n8n_list_available_tools: n8nListAvailableToolsDoc,
|
||||
|
||||
// Guides
|
||||
ai_agents_guide: aiAgentsGuide,
|
||||
|
||||
// Discovery tools
|
||||
search_nodes: searchNodesDoc,
|
||||
list_nodes: listNodesDoc,
|
||||
list_ai_tools: listAiToolsDoc,
|
||||
get_database_statistics: getDatabaseStatisticsDoc,
|
||||
|
||||
|
||||
// Configuration tools
|
||||
get_node_essentials: getNodeEssentialsDoc,
|
||||
get_node_info: getNodeInfoDoc,
|
||||
get_node_documentation: getNodeDocumentationDoc,
|
||||
search_node_properties: searchNodePropertiesDoc,
|
||||
get_node_as_tool_info: getNodeAsToolInfoDoc,
|
||||
get_property_dependencies: getPropertyDependenciesDoc,
|
||||
|
||||
get_node: getNodeDoc,
|
||||
|
||||
// Validation tools
|
||||
validate_node_minimal: validateNodeMinimalDoc,
|
||||
validate_node_operation: validateNodeOperationDoc,
|
||||
validate_node: validateNodeDoc,
|
||||
validate_workflow: validateWorkflowDoc,
|
||||
validate_workflow_connections: validateWorkflowConnectionsDoc,
|
||||
validate_workflow_expressions: validateWorkflowExpressionsDoc,
|
||||
|
||||
|
||||
// Template tools
|
||||
list_tasks: listTasksDoc,
|
||||
list_node_templates: listNodeTemplatesDoc,
|
||||
get_template: getTemplateDoc,
|
||||
search_templates: searchTemplatesDoc,
|
||||
search_templates_by_metadata: searchTemplatesByMetadataDoc,
|
||||
get_templates_for_task: getTemplatesForTaskDoc,
|
||||
|
||||
|
||||
// Workflow Management tools (n8n API)
|
||||
n8n_create_workflow: n8nCreateWorkflowDoc,
|
||||
n8n_get_workflow: n8nGetWorkflowDoc,
|
||||
n8n_get_workflow_details: n8nGetWorkflowDetailsDoc,
|
||||
n8n_get_workflow_structure: n8nGetWorkflowStructureDoc,
|
||||
n8n_get_workflow_minimal: n8nGetWorkflowMinimalDoc,
|
||||
n8n_update_full_workflow: n8nUpdateFullWorkflowDoc,
|
||||
n8n_update_partial_workflow: n8nUpdatePartialWorkflowDoc,
|
||||
n8n_delete_workflow: n8nDeleteWorkflowDoc,
|
||||
@@ -105,9 +58,9 @@ export const toolsDocumentation: Record<string, ToolDocumentation> = {
|
||||
n8n_validate_workflow: n8nValidateWorkflowDoc,
|
||||
n8n_autofix_workflow: n8nAutofixWorkflowDoc,
|
||||
n8n_trigger_webhook_workflow: n8nTriggerWebhookWorkflowDoc,
|
||||
n8n_get_execution: n8nGetExecutionDoc,
|
||||
n8n_list_executions: n8nListExecutionsDoc,
|
||||
n8n_delete_execution: n8nDeleteExecutionDoc
|
||||
n8n_executions: n8nExecutionsDoc,
|
||||
n8n_workflow_versions: n8nWorkflowVersionsDoc,
|
||||
n8n_deploy_template: n8nDeployTemplateDoc
|
||||
};
|
||||
|
||||
// Re-export types
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
export { toolsDocumentationDoc } from './tools-documentation';
|
||||
export { n8nDiagnosticDoc } from './n8n-diagnostic';
|
||||
export { n8nHealthCheckDoc } from './n8n-health-check';
|
||||
export { n8nListAvailableToolsDoc } from './n8n-list-available-tools';
|
||||
export { n8nHealthCheckDoc } from './n8n-health-check';
|
||||
@@ -5,8 +5,8 @@ export const n8nHealthCheckDoc: ToolDocumentation = {
|
||||
category: 'system',
|
||||
essentials: {
|
||||
description: 'Check n8n instance health, API connectivity, version status, and performance metrics',
|
||||
keyParameters: [],
|
||||
example: 'n8n_health_check({})',
|
||||
keyParameters: ['mode', 'verbose'],
|
||||
example: 'n8n_health_check({mode: "status"})',
|
||||
performance: 'Fast - single API call (~150-200ms median)',
|
||||
tips: [
|
||||
'Use before starting workflow operations to ensure n8n is responsive',
|
||||
@@ -31,7 +31,21 @@ Health checks are crucial for:
|
||||
- Detecting performance degradation
|
||||
- Verifying API compatibility before operations
|
||||
- Ensuring authentication is working correctly`,
|
||||
parameters: {},
|
||||
parameters: {
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Operation mode: "status" (default) for quick health check, "diagnostic" for detailed debug info including env vars and tool status',
|
||||
default: 'status',
|
||||
enum: ['status', 'diagnostic']
|
||||
},
|
||||
verbose: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
description: 'Include extra details in diagnostic mode',
|
||||
default: false
|
||||
}
|
||||
},
|
||||
returns: `Health status object containing:
|
||||
- status: Overall health status ('healthy', 'degraded', 'error')
|
||||
- n8nVersion: n8n instance version information
|
||||
@@ -81,6 +95,6 @@ Health checks are crucial for:
|
||||
'Does not check individual workflow health',
|
||||
'Health endpoint might be cached - not real-time for all metrics'
|
||||
],
|
||||
relatedTools: ['n8n_diagnostic', 'n8n_list_available_tools', 'n8n_list_workflows']
|
||||
relatedTools: ['n8n_list_workflows', 'n8n_validate_workflow', 'n8n_workflow_versions']
|
||||
}
|
||||
};
|
||||
@@ -58,6 +58,6 @@ export const toolsDocumentationDoc: ToolDocumentation = {
|
||||
'Not all internal functions are documented',
|
||||
'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', 'search_templates for workflow examples', 'search_nodes for finding nodes']
|
||||
}
|
||||
};
|
||||
@@ -4,23 +4,30 @@ export const getTemplateDoc: ToolDocumentation = {
|
||||
name: 'get_template',
|
||||
category: 'templates',
|
||||
essentials: {
|
||||
description: 'Get complete workflow JSON by ID. Ready to import. IDs from list_node_templates or search_templates.',
|
||||
keyParameters: ['templateId'],
|
||||
example: 'get_template({templateId: 1234})',
|
||||
description: 'Get workflow template by ID with configurable detail level. Ready to import. IDs from search_templates.',
|
||||
keyParameters: ['templateId', 'mode'],
|
||||
example: 'get_template({templateId: 1234, mode: "full"})',
|
||||
performance: 'Fast (<100ms) - single database lookup',
|
||||
tips: [
|
||||
'Get template IDs from list_node_templates or search_templates first',
|
||||
'Returns complete workflow JSON ready for import into n8n',
|
||||
'Includes all nodes, connections, and settings'
|
||||
'Get template IDs from search_templates first',
|
||||
'Use mode="nodes_only" for quick overview, "structure" for topology, "full" for import',
|
||||
'Returns complete workflow JSON ready for import into n8n'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Retrieves the complete workflow JSON for a specific template by its ID. The returned workflow can be directly imported into n8n through the UI or API. This tool fetches pre-built workflows from the community template library containing 399+ curated workflows.`,
|
||||
description: `Retrieves the complete workflow JSON for a specific template by its ID. The returned workflow can be directly imported into n8n through the UI or API. This tool fetches pre-built workflows from the community template library containing 2,700+ curated workflows.`,
|
||||
parameters: {
|
||||
templateId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
description: 'The numeric ID of the template to retrieve. Get IDs from list_node_templates or search_templates'
|
||||
description: 'The numeric ID of the template to retrieve. Get IDs from search_templates'
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Response detail level: "nodes_only" (minimal - just node list), "structure" (nodes + connections), "full" (complete workflow JSON, default)',
|
||||
default: 'full',
|
||||
enum: ['nodes_only', 'structure', 'full']
|
||||
}
|
||||
},
|
||||
returns: `Returns an object containing:
|
||||
@@ -39,9 +46,10 @@ export const getTemplateDoc: ToolDocumentation = {
|
||||
- settings: Workflow configuration (timezone, error handling, etc.)
|
||||
- usage: Instructions for using the workflow`,
|
||||
examples: [
|
||||
'get_template({templateId: 1234}) - Get Slack notification workflow',
|
||||
'get_template({templateId: 5678}) - Get data sync workflow',
|
||||
'get_template({templateId: 9012}) - Get AI chatbot workflow'
|
||||
'get_template({templateId: 1234}) - Get complete workflow (default mode="full")',
|
||||
'get_template({templateId: 1234, mode: "nodes_only"}) - Get just the node list',
|
||||
'get_template({templateId: 1234, mode: "structure"}) - Get nodes and connections',
|
||||
'get_template({templateId: 5678, mode: "full"}) - Get complete workflow JSON for import'
|
||||
],
|
||||
useCases: [
|
||||
'Download workflows for direct import into n8n',
|
||||
@@ -69,6 +77,6 @@ export const getTemplateDoc: ToolDocumentation = {
|
||||
'Not all templates work with all n8n versions',
|
||||
'Template may reference external services you don\'t have access to'
|
||||
],
|
||||
relatedTools: ['list_node_templates', 'search_templates', 'get_templates_for_task', 'n8n_create_workflow']
|
||||
relatedTools: ['search_templates', 'n8n_create_workflow']
|
||||
}
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const getTemplatesForTaskDoc: ToolDocumentation = {
|
||||
name: 'get_templates_for_task',
|
||||
category: 'templates',
|
||||
essentials: {
|
||||
description: 'Curated templates by task: ai_automation, data_sync, webhooks, email, slack, data_transform, files, scheduling, api, database.',
|
||||
keyParameters: ['task'],
|
||||
example: 'get_templates_for_task({task: "slack_integration"})',
|
||||
performance: 'Fast (<100ms) - pre-categorized results',
|
||||
tips: [
|
||||
'Returns hand-picked templates for specific automation tasks',
|
||||
'Use list_tasks to see all available task categories',
|
||||
'Templates are curated for quality and relevance'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Retrieves curated workflow templates for specific automation tasks. This tool provides hand-picked templates organized by common use cases, making it easy to find the right workflow for your needs. Each task category contains the most popular and effective templates for that particular automation scenario.`,
|
||||
parameters: {
|
||||
task: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The type of task to get templates for. Options: ai_automation, data_sync, webhook_processing, email_automation, slack_integration, data_transformation, file_processing, scheduling, api_integration, database_operations'
|
||||
}
|
||||
},
|
||||
returns: `Returns an object containing:
|
||||
- task: The requested task type
|
||||
- templates: Array of curated templates
|
||||
- id: Template ID
|
||||
- name: Template name
|
||||
- description: What the workflow does
|
||||
- author: Creator information
|
||||
- nodes: Array of node types used
|
||||
- views: Popularity metric
|
||||
- created: Creation date
|
||||
- url: Link to template
|
||||
- totalFound: Number of templates in this category
|
||||
- availableTasks: List of all task categories (if no templates found)`,
|
||||
examples: [
|
||||
'get_templates_for_task({task: "slack_integration"}) - Get Slack automation workflows',
|
||||
'get_templates_for_task({task: "ai_automation"}) - Get AI-powered workflows',
|
||||
'get_templates_for_task({task: "data_sync"}) - Get data synchronization workflows',
|
||||
'get_templates_for_task({task: "webhook_processing"}) - Get webhook handler workflows',
|
||||
'get_templates_for_task({task: "email_automation"}) - Get email automation workflows'
|
||||
],
|
||||
useCases: [
|
||||
'Find workflows for specific business needs',
|
||||
'Discover best practices for common automations',
|
||||
'Get started quickly with pre-built solutions',
|
||||
'Learn patterns for specific integration types',
|
||||
'Browse curated collections of quality workflows'
|
||||
],
|
||||
performance: `Excellent performance with pre-categorized templates:
|
||||
- Query time: <10ms (indexed by task)
|
||||
- No filtering needed (pre-curated)
|
||||
- Returns 5-20 templates per category
|
||||
- Total response time: <100ms`,
|
||||
bestPractices: [
|
||||
'Start with task-based search for faster results',
|
||||
'Review multiple templates to find best patterns',
|
||||
'Check template age for most current approaches',
|
||||
'Combine templates from same category for complex workflows',
|
||||
'Use returned node lists to understand requirements'
|
||||
],
|
||||
pitfalls: [
|
||||
'Not all tasks have many templates available',
|
||||
'Task categories are predefined - no custom categories',
|
||||
'Some templates may overlap between categories',
|
||||
'Curation is subjective - browse all results',
|
||||
'Templates may need updates for latest n8n features'
|
||||
],
|
||||
relatedTools: ['search_templates', 'list_node_templates', 'get_template', 'list_tasks']
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,2 @@
|
||||
export { listTasksDoc } from './list-tasks';
|
||||
export { listNodeTemplatesDoc } from './list-node-templates';
|
||||
export { getTemplateDoc } from './get-template';
|
||||
export { searchTemplatesDoc } from './search-templates';
|
||||
export { searchTemplatesByMetadataDoc } from './search-templates-by-metadata';
|
||||
export { getTemplatesForTaskDoc } from './get-templates-for-task';
|
||||
@@ -1,78 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const listNodeTemplatesDoc: ToolDocumentation = {
|
||||
name: 'list_node_templates',
|
||||
category: 'templates',
|
||||
essentials: {
|
||||
description: 'Find templates using specific nodes. 399 community workflows. Use FULL types: "n8n-nodes-base.httpRequest".',
|
||||
keyParameters: ['nodeTypes', 'limit'],
|
||||
example: 'list_node_templates({nodeTypes: ["n8n-nodes-base.slack"]})',
|
||||
performance: 'Fast (<100ms) - indexed node search',
|
||||
tips: [
|
||||
'Must use FULL node type with package prefix: "n8n-nodes-base.slack"',
|
||||
'Can search for multiple nodes to find workflows using all of them',
|
||||
'Returns templates sorted by popularity (view count)'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Finds workflow templates that use specific n8n nodes. This is the best way to discover how particular nodes are used in real workflows. Search the community library of 399+ templates by specifying which nodes you want to see in action. Templates are sorted by popularity to show the most useful examples first.`,
|
||||
parameters: {
|
||||
nodeTypes: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
description: 'Array of node types to search for. Must use full type names with package prefix (e.g., ["n8n-nodes-base.httpRequest", "n8n-nodes-base.openAi"])'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Maximum number of templates to return. Default 10, max 100'
|
||||
}
|
||||
},
|
||||
returns: `Returns an object containing:
|
||||
- templates: Array of matching templates
|
||||
- id: Template ID for retrieval
|
||||
- name: Template name
|
||||
- description: What the workflow does
|
||||
- author: Creator details (name, username, verified)
|
||||
- nodes: Complete list of nodes used
|
||||
- views: View count (popularity metric)
|
||||
- created: Creation date
|
||||
- url: Link to template on n8n.io
|
||||
- totalFound: Total number of matching templates
|
||||
- tip: Usage hints if no results`,
|
||||
examples: [
|
||||
'list_node_templates({nodeTypes: ["n8n-nodes-base.slack"]}) - Find all Slack workflows',
|
||||
'list_node_templates({nodeTypes: ["n8n-nodes-base.httpRequest", "n8n-nodes-base.postgres"]}) - Find workflows using both HTTP and Postgres',
|
||||
'list_node_templates({nodeTypes: ["@n8n/n8n-nodes-langchain.openAi"], limit: 20}) - Find AI workflows with OpenAI',
|
||||
'list_node_templates({nodeTypes: ["n8n-nodes-base.webhook", "n8n-nodes-base.respondToWebhook"]}) - Find webhook examples'
|
||||
],
|
||||
useCases: [
|
||||
'Learn how to use specific nodes through examples',
|
||||
'Find workflows combining particular integrations',
|
||||
'Discover patterns for node combinations',
|
||||
'See real-world usage of complex nodes',
|
||||
'Find templates for your exact tech stack'
|
||||
],
|
||||
performance: `Optimized for node-based searches:
|
||||
- Indexed by node type for fast lookups
|
||||
- Query time: <50ms for single node
|
||||
- Multiple nodes: <100ms (uses AND logic)
|
||||
- Returns pre-sorted by popularity
|
||||
- No full-text search needed`,
|
||||
bestPractices: [
|
||||
'Always use full node type with package prefix',
|
||||
'Search for core nodes that define the workflow purpose',
|
||||
'Start with single node searches, then refine',
|
||||
'Check node types with list_nodes if unsure of names',
|
||||
'Review multiple templates to learn different approaches'
|
||||
],
|
||||
pitfalls: [
|
||||
'Node types must match exactly - no partial matches',
|
||||
'Package prefix required: "slack" won\'t work, use "n8n-nodes-base.slack"',
|
||||
'Some nodes have version numbers: "n8n-nodes-base.httpRequestV3"',
|
||||
'Templates may use old node versions not in current n8n',
|
||||
'AND logic means all specified nodes must be present'
|
||||
],
|
||||
relatedTools: ['get_template', 'search_templates', 'get_templates_for_task', 'list_nodes']
|
||||
}
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const listTasksDoc: ToolDocumentation = {
|
||||
name: 'list_tasks',
|
||||
category: 'templates',
|
||||
essentials: {
|
||||
description: 'List task templates by category: HTTP/API, Webhooks, Database, AI, Data Processing, Communication.',
|
||||
keyParameters: ['category'],
|
||||
example: 'list_tasks({category: "HTTP/API"})',
|
||||
performance: 'Instant',
|
||||
tips: [
|
||||
'Categories: HTTP/API, Webhooks, Database, AI',
|
||||
'Shows pre-configured node settings',
|
||||
'Use get_node_for_task for details'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Lists available task templates organized by category. Each task represents a common automation pattern with pre-configured node settings. Categories include HTTP/API, Webhooks, Database, AI, Data Processing, and Communication.',
|
||||
parameters: {
|
||||
category: { type: 'string', description: 'Filter by category (optional)' }
|
||||
},
|
||||
returns: 'Array of tasks with name, category, description, nodeType',
|
||||
examples: [
|
||||
'list_tasks() - Get all task templates',
|
||||
'list_tasks({category: "Database"}) - Database-related tasks',
|
||||
'list_tasks({category: "AI"}) - AI automation tasks'
|
||||
],
|
||||
useCases: [
|
||||
'Discover common automation patterns',
|
||||
'Find pre-configured solutions',
|
||||
'Learn node usage patterns',
|
||||
'Quick workflow setup'
|
||||
],
|
||||
performance: 'Instant - Static task list',
|
||||
bestPractices: [
|
||||
'Browse all categories first',
|
||||
'Use get_node_for_task for config',
|
||||
'Combine multiple tasks in workflows'
|
||||
],
|
||||
pitfalls: [
|
||||
'Tasks are templates, customize as needed',
|
||||
'Not all nodes have task templates'
|
||||
],
|
||||
relatedTools: ['get_node_for_task', 'search_templates', 'get_templates_for_task']
|
||||
}
|
||||
};
|
||||
@@ -1,118 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const searchTemplatesByMetadataDoc: ToolDocumentation = {
|
||||
name: 'search_templates_by_metadata',
|
||||
category: 'templates',
|
||||
essentials: {
|
||||
description: 'Search templates using AI-generated metadata filters. Find templates by complexity, setup time, required services, or target audience. Enables smart template discovery beyond simple text search.',
|
||||
keyParameters: ['category', 'complexity', 'maxSetupMinutes', 'targetAudience'],
|
||||
example: 'search_templates_by_metadata({complexity: "simple", maxSetupMinutes: 30})',
|
||||
performance: 'Fast (<100ms) - JSON extraction queries',
|
||||
tips: [
|
||||
'All filters are optional - combine them for precise results',
|
||||
'Use getAvailableCategories() to see valid category values',
|
||||
'Complexity levels: simple, medium, complex',
|
||||
'Setup time is in minutes (5-480 range)'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Advanced template search using AI-generated metadata. Each template has been analyzed by GPT-4 to extract structured information about its purpose, complexity, setup requirements, and target users. This enables intelligent filtering beyond simple keyword matching, helping you find templates that match your specific needs, skill level, and available time.`,
|
||||
parameters: {
|
||||
category: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Filter by category like "automation", "integration", "data processing", "communication". Use template service getAvailableCategories() for full list.'
|
||||
},
|
||||
complexity: {
|
||||
type: 'string (enum)',
|
||||
required: false,
|
||||
description: 'Filter by implementation complexity: "simple" (beginner-friendly), "medium" (some experience needed), or "complex" (advanced features)'
|
||||
},
|
||||
maxSetupMinutes: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Maximum acceptable setup time in minutes (5-480). Find templates you can implement within your time budget.'
|
||||
},
|
||||
minSetupMinutes: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Minimum setup time in minutes (5-480). Find more substantial templates that offer comprehensive solutions.'
|
||||
},
|
||||
requiredService: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Filter by required external service like "openai", "slack", "google", "shopify". Ensures you have necessary accounts/APIs.'
|
||||
},
|
||||
targetAudience: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Filter by intended users: "developers", "marketers", "analysts", "operations", "sales". Find templates for your role.'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Maximum results to return. Default 20, max 100.'
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Pagination offset for results. Default 0.'
|
||||
}
|
||||
},
|
||||
returns: `Returns an object containing:
|
||||
- items: Array of matching templates with full metadata
|
||||
- id: Template ID
|
||||
- name: Template name
|
||||
- description: Purpose and functionality
|
||||
- author: Creator details
|
||||
- nodes: Array of nodes used
|
||||
- views: Popularity count
|
||||
- metadata: AI-generated structured data
|
||||
- categories: Primary use categories
|
||||
- complexity: Difficulty level
|
||||
- use_cases: Specific applications
|
||||
- estimated_setup_minutes: Time to implement
|
||||
- required_services: External dependencies
|
||||
- key_features: Main capabilities
|
||||
- target_audience: Intended users
|
||||
- total: Total matching templates
|
||||
- filters: Applied filter criteria
|
||||
- filterSummary: Human-readable filter description
|
||||
- availableCategories: Suggested categories if no results
|
||||
- availableAudiences: Suggested audiences if no results
|
||||
- tip: Contextual guidance`,
|
||||
examples: [
|
||||
'search_templates_by_metadata({complexity: "simple"}) - Find beginner-friendly templates',
|
||||
'search_templates_by_metadata({category: "automation", maxSetupMinutes: 30}) - Quick automation templates',
|
||||
'search_templates_by_metadata({targetAudience: "marketers"}) - Marketing-focused workflows',
|
||||
'search_templates_by_metadata({requiredService: "openai", complexity: "medium"}) - AI templates with moderate complexity',
|
||||
'search_templates_by_metadata({minSetupMinutes: 60, category: "integration"}) - Comprehensive integration solutions'
|
||||
],
|
||||
useCases: [
|
||||
'Finding beginner-friendly templates by setting complexity:"simple"',
|
||||
'Discovering templates you can implement quickly with maxSetupMinutes:30',
|
||||
'Finding role-specific workflows with targetAudience filter',
|
||||
'Identifying templates that need specific APIs with requiredService filter',
|
||||
'Combining multiple filters for precise template discovery'
|
||||
],
|
||||
performance: 'Fast (<100ms) - Uses SQLite JSON extraction on pre-generated metadata. 97.5% coverage (2,534/2,598 templates).',
|
||||
bestPractices: [
|
||||
'Start with broad filters and narrow down based on results',
|
||||
'Use getAvailableCategories() to discover valid category values',
|
||||
'Combine complexity and setup time for skill-appropriate templates',
|
||||
'Check required services before selecting templates to ensure you have necessary accounts'
|
||||
],
|
||||
pitfalls: [
|
||||
'Not all templates have metadata (97.5% coverage)',
|
||||
'Setup time estimates assume basic n8n familiarity',
|
||||
'Categories/audiences use partial matching - be specific',
|
||||
'Metadata is AI-generated and may occasionally be imprecise'
|
||||
],
|
||||
relatedTools: [
|
||||
'list_templates',
|
||||
'search_templates',
|
||||
'list_node_templates',
|
||||
'get_templates_for_task'
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -4,86 +4,138 @@ export const searchTemplatesDoc: ToolDocumentation = {
|
||||
name: 'search_templates',
|
||||
category: 'templates',
|
||||
essentials: {
|
||||
description: 'Search templates by name/description keywords. NOT for node types! For nodes use list_node_templates. Example: "chatbot".',
|
||||
keyParameters: ['query', 'limit', 'fields'],
|
||||
example: 'search_templates({query: "chatbot", fields: ["id", "name"]})',
|
||||
description: 'Unified template search with multiple modes: keyword search, by node types, by task type, or by metadata. 2,700+ templates available.',
|
||||
keyParameters: ['searchMode', 'query', 'nodeTypes', 'task', 'limit'],
|
||||
example: 'search_templates({searchMode: "by_task", task: "webhook_processing"})',
|
||||
performance: 'Fast (<100ms) - FTS5 full-text search',
|
||||
tips: [
|
||||
'Searches template names and descriptions, NOT node types',
|
||||
'Use keywords like "automation", "sync", "notification"',
|
||||
'For node-specific search, use list_node_templates instead',
|
||||
'Use fields parameter to get only specific data (reduces response by 70-90%)'
|
||||
'searchMode="keyword" (default): Search by name/description',
|
||||
'searchMode="by_nodes": Find templates using specific nodes',
|
||||
'searchMode="by_task": Get curated templates for common tasks',
|
||||
'searchMode="by_metadata": Filter by complexity, services, audience'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Performs full-text search across workflow template names and descriptions. This tool is ideal for finding workflows based on their purpose or functionality rather than specific nodes used. It searches through the community library of 399+ templates using SQLite FTS5 for fast, fuzzy matching.`,
|
||||
description: `**Search Modes:**
|
||||
- keyword (default): Full-text search across template names and descriptions
|
||||
- by_nodes: Find templates that use specific node types
|
||||
- by_task: Get curated templates for predefined task categories
|
||||
- by_metadata: Filter by complexity, setup time, required services, or target audience
|
||||
|
||||
**Available Task Types (for searchMode="by_task"):**
|
||||
ai_automation, data_sync, webhook_processing, email_automation, slack_integration, data_transformation, file_processing, scheduling, api_integration, database_operations`,
|
||||
parameters: {
|
||||
searchMode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Search mode: "keyword" (default), "by_nodes", "by_task", "by_metadata"'
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Search query for template names/descriptions. NOT for node types! Examples: "chatbot", "automation", "social media", "webhook". For node-based search use list_node_templates instead.'
|
||||
required: false,
|
||||
description: 'For searchMode=keyword: Search keywords (e.g., "chatbot", "automation")'
|
||||
},
|
||||
nodeTypes: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
description: 'For searchMode=by_nodes: Array of node types (e.g., ["n8n-nodes-base.httpRequest", "n8n-nodes-base.slack"])'
|
||||
},
|
||||
task: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'For searchMode=by_task: Task type (ai_automation, data_sync, webhook_processing, email_automation, slack_integration, data_transformation, file_processing, scheduling, api_integration, database_operations)'
|
||||
},
|
||||
complexity: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'For searchMode=by_metadata: Filter by complexity ("simple", "medium", "complex")'
|
||||
},
|
||||
maxSetupMinutes: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'For searchMode=by_metadata: Maximum setup time in minutes (5-480)'
|
||||
},
|
||||
minSetupMinutes: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'For searchMode=by_metadata: Minimum setup time in minutes (5-480)'
|
||||
},
|
||||
requiredService: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'For searchMode=by_metadata: Filter by required service (e.g., "openai", "slack", "google")'
|
||||
},
|
||||
targetAudience: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'For searchMode=by_metadata: Filter by target audience (e.g., "developers", "marketers")'
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'For searchMode=by_metadata: Filter by category (e.g., "automation", "integration")'
|
||||
},
|
||||
fields: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
description: 'Fields to include in response. Options: "id", "name", "description", "author", "nodes", "views", "created", "url", "metadata". Default: all fields. Example: ["id", "name"] for minimal response.'
|
||||
description: 'For searchMode=keyword: Fields to include (id, name, description, author, nodes, views, created, url, metadata)'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Maximum number of results. Default 20, max 100'
|
||||
description: 'Maximum results (default 20, max 100)'
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Pagination offset (default 0)'
|
||||
}
|
||||
},
|
||||
returns: `Returns an object containing:
|
||||
- templates: Array of matching templates sorted by relevance
|
||||
- id: Template ID for retrieval
|
||||
- name: Template name (with match highlights)
|
||||
- templates: Array of matching templates
|
||||
- id: Template ID for get_template()
|
||||
- name: Template name
|
||||
- description: What the workflow does
|
||||
- author: Creator information
|
||||
- nodes: Array of all nodes used
|
||||
- nodes: Array of node types used
|
||||
- views: Popularity metric
|
||||
- created: Creation date
|
||||
- url: Link to template
|
||||
- relevanceScore: Search match score
|
||||
- metadata: AI-generated metadata (complexity, services, etc.)
|
||||
- totalFound: Total matching templates
|
||||
- searchQuery: The processed search query
|
||||
- tip: Helpful hints if no results`,
|
||||
- searchMode: The mode used`,
|
||||
examples: [
|
||||
'search_templates({query: "chatbot"}) - Find chatbot and conversational AI workflows',
|
||||
'search_templates({query: "email notification"}) - Find email alert workflows',
|
||||
'search_templates({query: "data sync"}) - Find data synchronization workflows',
|
||||
'search_templates({query: "webhook automation", limit: 30}) - Find webhook-based automations',
|
||||
'search_templates({query: "social media scheduler"}) - Find social posting workflows',
|
||||
'search_templates({query: "slack", fields: ["id", "name"]}) - Get only IDs and names of Slack templates',
|
||||
'search_templates({query: "automation", fields: ["id", "name", "description"]}) - Get minimal info for automation templates'
|
||||
'// Keyword search (default)\nsearch_templates({query: "chatbot"})',
|
||||
'// Find templates using specific nodes\nsearch_templates({searchMode: "by_nodes", nodeTypes: ["n8n-nodes-base.httpRequest", "n8n-nodes-base.slack"]})',
|
||||
'// Get templates for a task type\nsearch_templates({searchMode: "by_task", task: "webhook_processing"})',
|
||||
'// Filter by metadata\nsearch_templates({searchMode: "by_metadata", complexity: "simple", requiredService: "openai"})',
|
||||
'// Combine metadata filters\nsearch_templates({searchMode: "by_metadata", maxSetupMinutes: 30, targetAudience: "developers"})'
|
||||
],
|
||||
useCases: [
|
||||
'Find workflows by business purpose',
|
||||
'Discover automations for specific use cases',
|
||||
'Search by workflow functionality',
|
||||
'Find templates by problem they solve',
|
||||
'Explore workflows by industry or domain'
|
||||
'Find workflows by business purpose (keyword search)',
|
||||
'Find templates using specific integrations (by_nodes)',
|
||||
'Get pre-built solutions for common tasks (by_task)',
|
||||
'Filter by complexity for team skill level (by_metadata)',
|
||||
'Find templates requiring specific services (by_metadata)'
|
||||
],
|
||||
performance: `Excellent performance with FTS5 indexing:
|
||||
- Full-text search: <50ms for most queries
|
||||
- Fuzzy matching enabled for typos
|
||||
- Relevance-based sorting included
|
||||
- Searches both title and description
|
||||
- Returns highlighted matches`,
|
||||
performance: `Fast performance across all modes:
|
||||
- keyword: <50ms with FTS5 indexing
|
||||
- by_nodes: <100ms with indexed lookups
|
||||
- by_task: <50ms from curated cache
|
||||
- by_metadata: <100ms with filtered queries`,
|
||||
bestPractices: [
|
||||
'Use descriptive keywords about the workflow purpose',
|
||||
'Try multiple related terms if first search has few results',
|
||||
'Combine terms for more specific results',
|
||||
'Check both name and description in results',
|
||||
'Use quotes for exact phrase matching'
|
||||
'Use searchMode="by_task" for common automation patterns',
|
||||
'Use searchMode="by_nodes" when you know which integrations you need',
|
||||
'Use searchMode="keyword" for general discovery',
|
||||
'Combine by_metadata filters for precise matching',
|
||||
'Use get_template(id) to get the full workflow JSON'
|
||||
],
|
||||
pitfalls: [
|
||||
'Does NOT search by node types - use list_node_templates',
|
||||
'Search is case-insensitive but not semantic',
|
||||
'Very specific terms may return no results',
|
||||
'Descriptions may be brief - check full template',
|
||||
'Relevance scoring may not match your expectations'
|
||||
'searchMode="keyword" searches names/descriptions, not node types',
|
||||
'by_nodes requires full node type with prefix (n8n-nodes-base.xxx)',
|
||||
'by_metadata filters may return fewer results',
|
||||
'Not all templates have complete metadata'
|
||||
],
|
||||
relatedTools: ['list_node_templates', 'get_templates_for_task', 'get_template', 'search_nodes']
|
||||
relatedTools: ['get_template', 'search_nodes', 'validate_workflow']
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,2 @@
|
||||
export { validateNodeMinimalDoc } from './validate-node-minimal';
|
||||
export { validateNodeOperationDoc } from './validate-node-operation';
|
||||
export { validateNodeDoc } from './validate-node';
|
||||
export { validateWorkflowDoc } from './validate-workflow';
|
||||
export { validateWorkflowConnectionsDoc } from './validate-workflow-connections';
|
||||
export { validateWorkflowExpressionsDoc } from './validate-workflow-expressions';
|
||||
@@ -1,47 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const validateNodeMinimalDoc: ToolDocumentation = {
|
||||
name: 'validate_node_minimal',
|
||||
category: 'validation',
|
||||
essentials: {
|
||||
description: 'Fast check for missing required fields only. No warnings/suggestions. Returns: list of missing fields.',
|
||||
keyParameters: ['nodeType', 'config'],
|
||||
example: 'validate_node_minimal("nodes-base.slack", {resource: "message"})',
|
||||
performance: 'Instant',
|
||||
tips: [
|
||||
'Returns only missing required fields',
|
||||
'No warnings or suggestions',
|
||||
'Perfect for real-time validation'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Minimal validation that only checks for missing required fields. Returns array of missing field names without any warnings or suggestions. Ideal for quick validation during node configuration.',
|
||||
parameters: {
|
||||
nodeType: { type: 'string', required: true, description: 'Node type with prefix (e.g., "nodes-base.slack")' },
|
||||
config: { type: 'object', required: true, description: 'Node configuration to validate' }
|
||||
},
|
||||
returns: 'Array of missing required field names (empty if valid)',
|
||||
examples: [
|
||||
'validate_node_minimal("nodes-base.slack", {resource: "message", operation: "post"}) - Check Slack config',
|
||||
'validate_node_minimal("nodes-base.httpRequest", {method: "GET"}) - Check HTTP config'
|
||||
],
|
||||
useCases: [
|
||||
'Real-time form validation',
|
||||
'Quick configuration checks',
|
||||
'Pre-deployment validation',
|
||||
'Interactive configuration builders'
|
||||
],
|
||||
performance: 'Instant - Simple field checking without complex validation',
|
||||
bestPractices: [
|
||||
'Use for quick feedback loops',
|
||||
'Follow with validate_node_operation for thorough check',
|
||||
'Check return array length for validity'
|
||||
],
|
||||
pitfalls: [
|
||||
'Only checks required fields',
|
||||
'No type validation',
|
||||
'No operation-specific validation'
|
||||
],
|
||||
relatedTools: ['validate_node_operation', 'get_node_essentials', 'get_property_dependencies']
|
||||
}
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const validateNodeOperationDoc: ToolDocumentation = {
|
||||
name: 'validate_node_operation',
|
||||
category: 'validation',
|
||||
essentials: {
|
||||
description: 'Validates node configuration with operation awareness. Checks required fields, data types, and operation-specific rules. Returns specific errors with automated fix suggestions. Different profiles for different validation needs.',
|
||||
keyParameters: ['nodeType', 'config', 'profile'],
|
||||
example: 'validate_node_operation({nodeType: "nodes-base.slack", config: {resource: "message", operation: "post", text: "Hi"}})',
|
||||
performance: '<100ms',
|
||||
tips: [
|
||||
'Profile choices: minimal (editing), runtime (execution), ai-friendly (balanced), strict (deployment)',
|
||||
'Returns fixes you can apply directly',
|
||||
'Operation-aware - knows Slack post needs text',
|
||||
'Validates operator structures for IF and Switch nodes with conditions'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Comprehensive node configuration validation that understands operation context. For example, it knows Slack message posting requires text field, while channel listing doesn\'t. Provides different validation profiles for different stages of workflow development.',
|
||||
parameters: {
|
||||
nodeType: { type: 'string', required: true, description: 'Full node type with prefix: "nodes-base.slack", "nodes-base.httpRequest"' },
|
||||
config: { type: 'object', required: true, description: 'Node configuration. Must include operation fields (resource/operation/action) if the node has multiple operations' },
|
||||
profile: { type: 'string', required: false, description: 'Validation profile - controls what\'s checked. Default: "ai-friendly"' }
|
||||
},
|
||||
returns: `Object containing:
|
||||
{
|
||||
"isValid": false,
|
||||
"errors": [
|
||||
{
|
||||
"field": "channel",
|
||||
"message": "Required field 'channel' is missing",
|
||||
"severity": "error",
|
||||
"fix": "#general"
|
||||
}
|
||||
],
|
||||
"warnings": [
|
||||
{
|
||||
"field": "retryOnFail",
|
||||
"message": "Consider enabling retry for reliability",
|
||||
"severity": "warning",
|
||||
"fix": true
|
||||
}
|
||||
],
|
||||
"suggestions": [
|
||||
{
|
||||
"field": "timeout",
|
||||
"message": "Set timeout to prevent hanging",
|
||||
"fix": 30000
|
||||
}
|
||||
],
|
||||
"fixes": {
|
||||
"channel": "#general",
|
||||
"retryOnFail": true,
|
||||
"timeout": 30000
|
||||
}
|
||||
}`,
|
||||
examples: [
|
||||
'// Missing required field',
|
||||
'validate_node_operation({nodeType: "nodes-base.slack", config: {resource: "message", operation: "post"}})',
|
||||
'// Returns: {isValid: false, errors: [{field: "text", message: "Required field missing"}], fixes: {text: "Message text"}}',
|
||||
'',
|
||||
'// Validate with strict profile for production',
|
||||
'validate_node_operation({nodeType: "nodes-base.httpRequest", config: {method: "POST", url: "https://api.example.com"}, profile: "strict"})',
|
||||
'',
|
||||
'// Apply fixes automatically',
|
||||
'const result = validate_node_operation({nodeType: "nodes-base.slack", config: myConfig});',
|
||||
'if (!result.isValid) {',
|
||||
' myConfig = {...myConfig, ...result.fixes};',
|
||||
'}'
|
||||
],
|
||||
useCases: [
|
||||
'Validate configuration before workflow execution',
|
||||
'Debug why a node isn\'t working as expected',
|
||||
'Generate configuration fixes automatically',
|
||||
'Different validation for editing vs production',
|
||||
'Check IF/Switch operator structures (binary vs unary operators)',
|
||||
'Validate conditions.options metadata for filter-based nodes'
|
||||
],
|
||||
performance: '<100ms for most nodes, <200ms for complex nodes with many conditions',
|
||||
bestPractices: [
|
||||
'Use "minimal" profile during user editing for fast feedback',
|
||||
'Use "runtime" profile (default) before execution',
|
||||
'Use "ai-friendly" when AI configures nodes',
|
||||
'Use "strict" profile before production deployment',
|
||||
'Always include operation fields (resource/operation) in config',
|
||||
'Apply suggested fixes to resolve issues quickly'
|
||||
],
|
||||
pitfalls: [
|
||||
'Must include operation fields for multi-operation nodes',
|
||||
'Fixes are suggestions - review before applying',
|
||||
'Profile affects what\'s validated - minimal skips many checks',
|
||||
'**Binary vs Unary operators**: Binary operators (equals, contains, greaterThan) must NOT have singleValue:true. Unary operators (isEmpty, isNotEmpty, true, false) REQUIRE singleValue:true',
|
||||
'**IF and Switch nodes with conditions**: Must have complete conditions.options structure: {version: 2, leftValue: "", caseSensitive: true/false, typeValidation: "strict"}',
|
||||
'**Operator type field**: Must be data type (string/number/boolean/dateTime/array/object), NOT operation name (e.g., use type:"string" operation:"equals", not type:"equals")'
|
||||
],
|
||||
relatedTools: ['validate_node_minimal for quick checks', 'get_node_essentials for valid examples', 'validate_workflow for complete workflow validation']
|
||||
}
|
||||
};
|
||||
80
src/mcp/tool-docs/validation/validate-node.ts
Normal file
80
src/mcp/tool-docs/validation/validate-node.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const validateNodeDoc: ToolDocumentation = {
|
||||
name: 'validate_node',
|
||||
category: 'validation',
|
||||
essentials: {
|
||||
description: 'Validate n8n node configuration. Use mode="full" for comprehensive validation with errors/warnings/suggestions, mode="minimal" for quick required fields check.',
|
||||
keyParameters: ['nodeType', 'config', 'mode', 'profile'],
|
||||
example: 'validate_node({nodeType: "nodes-base.slack", config: {resource: "channel", operation: "create"}})',
|
||||
performance: 'Fast (<100ms)',
|
||||
tips: [
|
||||
'Always call get_node({detail:"standard"}) first to see required fields',
|
||||
'Use mode="minimal" for quick checks during development',
|
||||
'Use mode="full" with profile="strict" before production deployment',
|
||||
'Includes automatic structure validation for filter, resourceMapper, etc.'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `**Validation Modes:**
|
||||
- full (default): Comprehensive validation with errors, warnings, suggestions, and automatic structure validation
|
||||
- minimal: Quick check for required fields only - fast but less thorough
|
||||
|
||||
**Validation Profiles (for mode="full"):**
|
||||
- minimal: Very lenient, basic checks only
|
||||
- runtime: Standard validation (default)
|
||||
- ai-friendly: Balanced for AI agent workflows
|
||||
- strict: Most thorough, recommended for production
|
||||
|
||||
**Automatic Structure Validation:**
|
||||
Validates complex n8n types automatically:
|
||||
- filter (FilterValue): 40+ operations (equals, contains, regex, etc.)
|
||||
- resourceMapper (ResourceMapperValue): Data mapping configuration
|
||||
- assignmentCollection (AssignmentCollectionValue): Variable assignments
|
||||
- resourceLocator (INodeParameterResourceLocator): Resource selection modes`,
|
||||
parameters: {
|
||||
nodeType: { type: 'string', required: true, description: 'Node type with prefix: "nodes-base.slack"' },
|
||||
config: { type: 'object', required: true, description: 'Configuration object to validate. Use {} for empty config' },
|
||||
mode: { type: 'string', required: false, description: 'Validation mode: "full" (default) or "minimal"' },
|
||||
profile: { type: 'string', required: false, description: 'Validation profile for mode=full: "minimal", "runtime" (default), "ai-friendly", "strict"' }
|
||||
},
|
||||
returns: `Object containing:
|
||||
- nodeType: The validated node type
|
||||
- workflowNodeType: Type to use in workflow JSON
|
||||
- displayName: Human-readable node name
|
||||
- valid: Boolean indicating if configuration is valid
|
||||
- errors: Array of error objects with type, property, message, fix
|
||||
- warnings: Array of warning objects with suggestions
|
||||
- suggestions: Array of improvement suggestions
|
||||
- missingRequiredFields: (mode=minimal only) Array of missing required field names
|
||||
- summary: Object with hasErrors, errorCount, warningCount, suggestionCount`,
|
||||
examples: [
|
||||
'// Full validation with default profile\nvalidate_node({nodeType: "nodes-base.slack", config: {resource: "channel", operation: "create"}})',
|
||||
'// Quick required fields check\nvalidate_node({nodeType: "nodes-base.webhook", config: {}, mode: "minimal"})',
|
||||
'// Strict validation for production\nvalidate_node({nodeType: "nodes-base.httpRequest", config: {...}, mode: "full", profile: "strict"})',
|
||||
'// Validate IF node with filter\nvalidate_node({nodeType: "nodes-base.if", config: {conditions: {combinator: "and", conditions: [...]}}})'
|
||||
],
|
||||
useCases: [
|
||||
'Validate node configuration before adding to workflow',
|
||||
'Quick check for required fields during development',
|
||||
'Pre-production validation with strict profile',
|
||||
'Validate complex structures (filters, resource mappers)',
|
||||
'Get suggestions for improving node configuration'
|
||||
],
|
||||
performance: 'Fast validation: <50ms for minimal mode, <100ms for full mode. Structure validation adds minimal overhead.',
|
||||
bestPractices: [
|
||||
'Always call get_node() first to understand required fields',
|
||||
'Use mode="minimal" for rapid iteration during development',
|
||||
'Use profile="strict" before deploying to production',
|
||||
'Pay attention to warnings - they often prevent runtime issues',
|
||||
'Validate after any configuration changes'
|
||||
],
|
||||
pitfalls: [
|
||||
'Empty config {} is valid for some nodes (e.g., manual trigger)',
|
||||
'mode="minimal" only checks required fields, not value validity',
|
||||
'Some warnings may be acceptable for specific use cases',
|
||||
'Credential validation requires runtime context'
|
||||
],
|
||||
relatedTools: ['get_node', 'validate_workflow', 'n8n_autofix_workflow']
|
||||
}
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const validateWorkflowConnectionsDoc: ToolDocumentation = {
|
||||
name: 'validate_workflow_connections',
|
||||
category: 'validation',
|
||||
essentials: {
|
||||
description: 'Check workflow connections only: valid nodes, no cycles, proper triggers, AI tool links. Fast structure validation.',
|
||||
keyParameters: ['workflow'],
|
||||
example: 'validate_workflow_connections({workflow: {nodes: [...], connections: {...}}})',
|
||||
performance: 'Fast (<100ms)',
|
||||
tips: [
|
||||
'Use for quick structure checks when editing connections',
|
||||
'Detects orphaned nodes and circular dependencies',
|
||||
'Validates AI Agent tool connections to ensure proper node references'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Validates only the connection structure of a workflow without checking node configurations or expressions. This focused validation checks that all referenced nodes exist, detects circular dependencies, ensures proper trigger node placement, validates AI tool connections, and identifies orphaned or unreachable nodes.',
|
||||
parameters: {
|
||||
workflow: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
description: 'The workflow JSON with nodes array and connections object.'
|
||||
}
|
||||
},
|
||||
returns: 'Object with valid (boolean), errors (array), warnings (array), and statistics about connections',
|
||||
examples: [
|
||||
'validate_workflow_connections({workflow: myWorkflow}) - Check all connections',
|
||||
'validate_workflow_connections({workflow: {nodes: [...], connections: {...}}}) - Validate structure only'
|
||||
],
|
||||
useCases: [
|
||||
'Quick validation when modifying workflow connections',
|
||||
'Ensure all node references in connections are valid',
|
||||
'Detect circular dependencies that would cause infinite loops',
|
||||
'Validate AI Agent nodes have proper tool connections',
|
||||
'Check workflow has at least one trigger node',
|
||||
'Find orphaned nodes not connected to any flow'
|
||||
],
|
||||
performance: 'Fast (<100ms). Only validates structure, not node content. Scales linearly with connection count.',
|
||||
bestPractices: [
|
||||
'Run after adding or removing connections',
|
||||
'Use before validate_workflow for quick structural checks',
|
||||
'Check for warnings about orphaned nodes',
|
||||
'Ensure trigger nodes are properly positioned',
|
||||
'Validate after using n8n_update_partial_workflow with connection operations'
|
||||
],
|
||||
pitfalls: [
|
||||
'Does not validate node configurations - use validate_workflow for full validation',
|
||||
'Cannot detect logical errors in connection flow',
|
||||
'Some valid workflows may have intentionally disconnected nodes',
|
||||
'Circular dependency detection only catches direct loops',
|
||||
'Does not validate connection types match node capabilities'
|
||||
],
|
||||
relatedTools: ['validate_workflow', 'validate_workflow_expressions', 'n8n_update_partial_workflow']
|
||||
}
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const validateWorkflowExpressionsDoc: ToolDocumentation = {
|
||||
name: 'validate_workflow_expressions',
|
||||
category: 'validation',
|
||||
essentials: {
|
||||
description: 'Validate n8n expressions: syntax {{}}, variables ($json/$node), references. Returns errors with locations.',
|
||||
keyParameters: ['workflow'],
|
||||
example: 'validate_workflow_expressions({workflow: {nodes: [...], connections: {...}}})',
|
||||
performance: 'Fast (<100ms)',
|
||||
tips: [
|
||||
'Catches syntax errors in {{}} expressions before runtime',
|
||||
'Validates $json, $node, and other n8n variables',
|
||||
'Shows exact location of expression errors in node parameters'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Validates all n8n expressions within a workflow for syntax correctness and reference validity. This tool scans all node parameters for n8n expressions (enclosed in {{}}), checks expression syntax, validates variable references like $json and $node("NodeName"), ensures referenced nodes exist in the workflow, and provides detailed error locations for debugging.',
|
||||
parameters: {
|
||||
workflow: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
description: 'The workflow JSON to check for expression errors.'
|
||||
}
|
||||
},
|
||||
returns: 'Object with valid (boolean), errors (array with node ID, parameter path, and error details), and expression count',
|
||||
examples: [
|
||||
'validate_workflow_expressions({workflow: myWorkflow}) - Check all expressions',
|
||||
'validate_workflow_expressions({workflow: {nodes: [...], connections: {...}}}) - Validate expression syntax'
|
||||
],
|
||||
useCases: [
|
||||
'Catch expression syntax errors before workflow execution',
|
||||
'Validate node references in $node() expressions exist',
|
||||
'Find typos in variable names like $json or $input',
|
||||
'Ensure complex expressions are properly formatted',
|
||||
'Debug expression errors with exact parameter locations',
|
||||
'Validate expressions after workflow modifications'
|
||||
],
|
||||
performance: 'Fast (<100ms). Scans all string parameters in all nodes. Performance scales with workflow size and expression count.',
|
||||
bestPractices: [
|
||||
'Run after modifying any expressions in node parameters',
|
||||
'Check all $node() references when renaming nodes',
|
||||
'Validate expressions before workflow deployment',
|
||||
'Pay attention to nested object paths in expressions',
|
||||
'Use with validate_workflow for comprehensive validation'
|
||||
],
|
||||
pitfalls: [
|
||||
'Cannot validate expression logic, only syntax',
|
||||
'Runtime data availability not checked (e.g., if $json.field exists)',
|
||||
'Complex JavaScript in expressions may need runtime testing',
|
||||
'Does not validate expression return types',
|
||||
'Some valid expressions may use advanced features not fully parsed'
|
||||
],
|
||||
relatedTools: ['validate_workflow', 'validate_workflow_connections', 'validate_node_operation']
|
||||
}
|
||||
};
|
||||
@@ -79,6 +79,6 @@ export const validateWorkflowDoc: ToolDocumentation = {
|
||||
'Validation cannot catch all runtime errors (e.g., API failures)',
|
||||
'Profile setting only affects node validation, not connection/expression checks'
|
||||
],
|
||||
relatedTools: ['validate_workflow_connections', 'validate_workflow_expressions', 'validate_node_operation', 'n8n_create_workflow', 'n8n_update_partial_workflow', 'n8n_autofix_workflow']
|
||||
relatedTools: ['validate_node', 'n8n_create_workflow', 'n8n_update_partial_workflow', 'n8n_autofix_workflow']
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,5 @@
|
||||
export { n8nCreateWorkflowDoc } from './n8n-create-workflow';
|
||||
export { n8nGetWorkflowDoc } from './n8n-get-workflow';
|
||||
export { n8nGetWorkflowDetailsDoc } from './n8n-get-workflow-details';
|
||||
export { n8nGetWorkflowStructureDoc } from './n8n-get-workflow-structure';
|
||||
export { n8nGetWorkflowMinimalDoc } from './n8n-get-workflow-minimal';
|
||||
export { n8nUpdateFullWorkflowDoc } from './n8n-update-full-workflow';
|
||||
export { n8nUpdatePartialWorkflowDoc } from './n8n-update-partial-workflow';
|
||||
export { n8nDeleteWorkflowDoc } from './n8n-delete-workflow';
|
||||
@@ -10,6 +7,6 @@ export { n8nListWorkflowsDoc } from './n8n-list-workflows';
|
||||
export { n8nValidateWorkflowDoc } from './n8n-validate-workflow';
|
||||
export { n8nAutofixWorkflowDoc } from './n8n-autofix-workflow';
|
||||
export { n8nTriggerWebhookWorkflowDoc } from './n8n-trigger-webhook-workflow';
|
||||
export { n8nGetExecutionDoc } from './n8n-get-execution';
|
||||
export { n8nListExecutionsDoc } from './n8n-list-executions';
|
||||
export { n8nDeleteExecutionDoc } from './n8n-delete-execution';
|
||||
export { n8nExecutionsDoc } from './n8n-executions';
|
||||
export { n8nWorkflowVersionsDoc } from './n8n-workflow-versions';
|
||||
export { n8nDeployTemplateDoc } from './n8n-deploy-template';
|
||||
|
||||
@@ -153,9 +153,8 @@ Requires N8N_API_URL and N8N_API_KEY environment variables to be configured.`,
|
||||
relatedTools: [
|
||||
'n8n_validate_workflow',
|
||||
'validate_workflow',
|
||||
'n8n_update_partial_workflow',
|
||||
'validate_workflow_expressions',
|
||||
'validate_node_operation'
|
||||
'validate_node',
|
||||
'n8n_update_partial_workflow'
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const n8nDeleteExecutionDoc: ToolDocumentation = {
|
||||
name: 'n8n_delete_execution',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Delete an execution record. This only removes the execution history, not any data processed.',
|
||||
keyParameters: ['id'],
|
||||
example: 'n8n_delete_execution({id: "12345"})',
|
||||
performance: 'Immediate deletion, no undo available',
|
||||
tips: [
|
||||
'Deletion is permanent - execution cannot be recovered',
|
||||
'Only removes execution history, not external data changes',
|
||||
'Use for cleanup of test executions or sensitive data'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Permanently deletes a workflow execution record from n8n's history. This removes the execution metadata, logs, and any stored input/output data. However, it does NOT undo any actions the workflow performed (API calls, database changes, file operations, etc.). Use this for cleaning up test executions, removing sensitive data, or managing storage.`,
|
||||
parameters: {
|
||||
id: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The execution ID to delete. This action cannot be undone'
|
||||
}
|
||||
},
|
||||
returns: `Confirmation of deletion or error if execution not found. No data is returned about the deleted execution.`,
|
||||
examples: [
|
||||
'n8n_delete_execution({id: "12345"}) - Delete a specific execution',
|
||||
'n8n_delete_execution({id: "test-run-567"}) - Clean up test execution',
|
||||
'n8n_delete_execution({id: "sensitive-data-890"}) - Remove execution with sensitive data',
|
||||
'n8n_delete_execution({id: "failed-execution-123"}) - Delete failed execution after debugging'
|
||||
],
|
||||
useCases: [
|
||||
'Clean up test or development execution history',
|
||||
'Remove executions containing sensitive or personal data',
|
||||
'Manage storage by deleting old execution records',
|
||||
'Clean up after debugging failed workflows',
|
||||
'Comply with data retention policies'
|
||||
],
|
||||
performance: `Deletion is immediate and permanent. The operation is fast (< 100ms) as it only removes database records. No external systems or data are affected.`,
|
||||
bestPractices: [
|
||||
'Verify execution ID before deletion - action cannot be undone',
|
||||
'Consider exporting execution data before deletion if needed',
|
||||
'Use list_executions to find executions to delete',
|
||||
'Document why executions were deleted for audit trails',
|
||||
'Remember deletion only affects n8n records, not external changes'
|
||||
],
|
||||
pitfalls: [
|
||||
'Deletion is PERMANENT - no undo or recovery possible',
|
||||
'Does NOT reverse workflow actions (API calls, DB changes, etc.)',
|
||||
'Deleting executions breaks audit trails and debugging history',
|
||||
'Cannot delete currently running executions (waiting status)',
|
||||
'Bulk deletion not supported - must delete one at a time'
|
||||
],
|
||||
relatedTools: ['n8n_list_executions', 'n8n_get_execution', 'n8n_trigger_webhook_workflow']
|
||||
}
|
||||
};
|
||||
@@ -11,7 +11,7 @@ export const n8nDeleteWorkflowDoc: ToolDocumentation = {
|
||||
tips: [
|
||||
'Action is irreversible',
|
||||
'Deletes all execution history',
|
||||
'Check workflow first with get_minimal'
|
||||
'Check workflow first with n8n_get_workflow({mode: "minimal"})'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
@@ -34,7 +34,7 @@ export const n8nDeleteWorkflowDoc: ToolDocumentation = {
|
||||
performance: 'Fast operation - typically 50-150ms. May take longer if workflow has extensive execution history.',
|
||||
bestPractices: [
|
||||
'Always confirm before deletion',
|
||||
'Check workflow with get_minimal first',
|
||||
'Check workflow with n8n_get_workflow({mode: "minimal"}) first',
|
||||
'Consider deactivating instead of deleting',
|
||||
'Export workflow before deletion for backup'
|
||||
],
|
||||
@@ -45,6 +45,6 @@ export const n8nDeleteWorkflowDoc: ToolDocumentation = {
|
||||
'Active workflows can be deleted',
|
||||
'No built-in confirmation'
|
||||
],
|
||||
relatedTools: ['n8n_get_workflow_minimal', 'n8n_list_workflows', 'n8n_update_partial_workflow', 'n8n_delete_execution']
|
||||
relatedTools: ['n8n_get_workflow', 'n8n_list_workflows', 'n8n_update_partial_workflow', 'n8n_executions']
|
||||
}
|
||||
};
|
||||
69
src/mcp/tool-docs/workflow_management/n8n-deploy-template.ts
Normal file
69
src/mcp/tool-docs/workflow_management/n8n-deploy-template.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const n8nDeployTemplateDoc: ToolDocumentation = {
|
||||
name: 'n8n_deploy_template',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Deploy a workflow template from n8n.io directly to your n8n instance. Fetches template, optionally upgrades node versions and validates, then creates workflow.',
|
||||
keyParameters: ['templateId', 'name', 'autoUpgradeVersions', 'validate', 'stripCredentials'],
|
||||
example: 'n8n_deploy_template({templateId: 2776, name: "My Deployed Template"})',
|
||||
performance: 'Network-dependent',
|
||||
tips: [
|
||||
'Workflow created inactive - configure credentials in n8n UI first',
|
||||
'Returns list of required credentials',
|
||||
'Use search_templates to find template IDs',
|
||||
'Templates are upgraded to latest node versions by default'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Deploys a workflow template from n8n.io directly to your n8n instance. This tool combines fetching a template and creating a workflow in a single operation. Templates are stored locally and fetched from the database. The workflow is always created in an inactive state, allowing you to configure credentials before activation.',
|
||||
parameters: {
|
||||
templateId: { type: 'number', required: true, description: 'Template ID from n8n.io (find via search_templates)' },
|
||||
name: { type: 'string', description: 'Custom workflow name (default: template name)' },
|
||||
autoUpgradeVersions: { type: 'boolean', description: 'Upgrade node typeVersions to latest supported (default: true)' },
|
||||
validate: { type: 'boolean', description: 'Validate workflow before deployment (default: true)' },
|
||||
stripCredentials: { type: 'boolean', description: 'Remove credential references - user configures in n8n UI (default: true)' }
|
||||
},
|
||||
returns: 'Object with workflowId, name, nodeCount, triggerType, requiredCredentials array, url, templateId, templateUrl',
|
||||
examples: [
|
||||
`// Deploy template with default settings
|
||||
n8n_deploy_template({templateId: 2776})`,
|
||||
`// Deploy with custom name
|
||||
n8n_deploy_template({
|
||||
templateId: 2776,
|
||||
name: "My Google Drive to Airtable Sync"
|
||||
})`,
|
||||
`// Deploy without validation (faster, use for trusted templates)
|
||||
n8n_deploy_template({
|
||||
templateId: 2776,
|
||||
validate: false
|
||||
})`,
|
||||
`// Keep original node versions (useful for compatibility)
|
||||
n8n_deploy_template({
|
||||
templateId: 2776,
|
||||
autoUpgradeVersions: false
|
||||
})`
|
||||
],
|
||||
useCases: [
|
||||
'Quickly deploy pre-built workflow templates',
|
||||
'Set up common automation patterns',
|
||||
'Bootstrap new projects with proven workflows',
|
||||
'Deploy templates found via search_templates'
|
||||
],
|
||||
performance: 'Network-dependent - Typically 200-500ms (template fetch + workflow creation)',
|
||||
bestPractices: [
|
||||
'Use search_templates to find templates by use case',
|
||||
'Review required credentials in the response',
|
||||
'Configure credentials in n8n UI before activating',
|
||||
'Test workflow before connecting to production systems'
|
||||
],
|
||||
pitfalls: [
|
||||
'**REQUIRES N8N_API_URL and N8N_API_KEY environment variables** - tool unavailable without n8n API access',
|
||||
'Workflows created in INACTIVE state - must configure credentials and activate in n8n',
|
||||
'Templates may reference services you do not have (Slack, Google, etc.)',
|
||||
'Template database must be populated - run npm run fetch:templates if templates not found',
|
||||
'Validation may fail for templates with outdated node configurations'
|
||||
],
|
||||
relatedTools: ['search_templates', 'get_template', 'n8n_create_workflow', 'n8n_validate_workflow']
|
||||
}
|
||||
};
|
||||
82
src/mcp/tool-docs/workflow_management/n8n-executions.ts
Normal file
82
src/mcp/tool-docs/workflow_management/n8n-executions.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const n8nExecutionsDoc: ToolDocumentation = {
|
||||
name: 'n8n_executions',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Manage workflow executions: get details, list, or delete. Unified tool for all execution operations.',
|
||||
keyParameters: ['action', 'id', 'workflowId', 'status'],
|
||||
example: 'n8n_executions({action: "list", workflowId: "abc123", status: "error"})',
|
||||
performance: 'Fast (50-200ms)',
|
||||
tips: [
|
||||
'action="get": Get execution details by ID',
|
||||
'action="list": List executions with filters',
|
||||
'action="delete": Delete execution record',
|
||||
'Use mode parameter for action=get to control detail level'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `**Actions:**
|
||||
- get: Retrieve execution details by ID with configurable detail level
|
||||
- list: List executions with filtering and pagination
|
||||
- delete: Remove an execution record from history
|
||||
|
||||
**Detail Modes for action="get":**
|
||||
- preview: Structure only, no data
|
||||
- summary: 2 items per node (default)
|
||||
- filtered: Custom items limit, optionally filter by node names
|
||||
- full: All execution data (can be very large)`,
|
||||
parameters: {
|
||||
action: { type: 'string', required: true, description: 'Operation: "get", "list", or "delete"' },
|
||||
id: { type: 'string', required: false, description: 'Execution ID (required for action=get or action=delete)' },
|
||||
mode: { type: 'string', required: false, description: 'For action=get: "preview", "summary" (default), "filtered", "full"' },
|
||||
nodeNames: { type: 'array', required: false, description: 'For action=get with mode=filtered: Filter to specific nodes by name' },
|
||||
itemsLimit: { type: 'number', required: false, description: 'For action=get with mode=filtered: Items per node (0=structure, 2=default, -1=unlimited)' },
|
||||
includeInputData: { type: 'boolean', required: false, description: 'For action=get: Include input data in addition to output (default: false)' },
|
||||
workflowId: { type: 'string', required: false, description: 'For action=list: Filter by workflow ID' },
|
||||
status: { type: 'string', required: false, description: 'For action=list: Filter by status ("success", "error", "waiting")' },
|
||||
limit: { type: 'number', required: false, description: 'For action=list: Number of results (1-100, default: 100)' },
|
||||
cursor: { type: 'string', required: false, description: 'For action=list: Pagination cursor from previous response' },
|
||||
projectId: { type: 'string', required: false, description: 'For action=list: Filter by project ID (enterprise)' },
|
||||
includeData: { type: 'boolean', required: false, description: 'For action=list: Include execution data (default: false)' }
|
||||
},
|
||||
returns: `Depends on action:
|
||||
- get: Execution object with data based on mode
|
||||
- list: { data: [...executions], nextCursor?: string }
|
||||
- delete: { success: boolean, message: string }`,
|
||||
examples: [
|
||||
'// List recent executions for a workflow\nn8n_executions({action: "list", workflowId: "abc123", limit: 10})',
|
||||
'// List failed executions\nn8n_executions({action: "list", status: "error"})',
|
||||
'// Get execution summary\nn8n_executions({action: "get", id: "exec_456"})',
|
||||
'// Get full execution data\nn8n_executions({action: "get", id: "exec_456", mode: "full"})',
|
||||
'// Get specific nodes from execution\nn8n_executions({action: "get", id: "exec_456", mode: "filtered", nodeNames: ["HTTP Request", "Slack"]})',
|
||||
'// Delete an execution\nn8n_executions({action: "delete", id: "exec_456"})'
|
||||
],
|
||||
useCases: [
|
||||
'Debug workflow failures (get with mode=full)',
|
||||
'Monitor workflow health (list with status filter)',
|
||||
'Audit execution history',
|
||||
'Clean up old execution records',
|
||||
'Analyze specific node outputs'
|
||||
],
|
||||
performance: `Response times:
|
||||
- list: 50-150ms depending on filters
|
||||
- get (preview/summary): 30-100ms
|
||||
- get (full): 100-500ms+ depending on data size
|
||||
- delete: 30-80ms`,
|
||||
bestPractices: [
|
||||
'Use mode="summary" (default) for debugging - shows enough data',
|
||||
'Use mode="filtered" with nodeNames for large workflows',
|
||||
'Filter by workflowId when listing to reduce results',
|
||||
'Use cursor for pagination through large result sets',
|
||||
'Delete old executions to save storage'
|
||||
],
|
||||
pitfalls: [
|
||||
'Requires N8N_API_URL and N8N_API_KEY configured',
|
||||
'mode="full" can return very large responses for complex workflows',
|
||||
'Execution must exist or returns 404',
|
||||
'Delete is permanent - cannot undo'
|
||||
],
|
||||
relatedTools: ['n8n_get_workflow', 'n8n_trigger_webhook_workflow', 'n8n_validate_workflow']
|
||||
}
|
||||
};
|
||||
@@ -1,283 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const n8nGetExecutionDoc: ToolDocumentation = {
|
||||
name: 'n8n_get_execution',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Get execution details with smart filtering to avoid token limits. Use preview mode first to assess data size, then fetch appropriately.',
|
||||
keyParameters: ['id', 'mode', 'itemsLimit', 'nodeNames'],
|
||||
example: `
|
||||
// RECOMMENDED WORKFLOW:
|
||||
// 1. Preview first
|
||||
n8n_get_execution({id: "12345", mode: "preview"})
|
||||
// Returns: structure, counts, size estimate, recommendation
|
||||
|
||||
// 2. Based on recommendation, fetch data:
|
||||
n8n_get_execution({id: "12345", mode: "summary"}) // 2 items per node
|
||||
n8n_get_execution({id: "12345", mode: "filtered", itemsLimit: 5}) // 5 items
|
||||
n8n_get_execution({id: "12345", nodeNames: ["HTTP Request"]}) // Specific node
|
||||
`,
|
||||
performance: 'Preview: <50ms, Summary: <200ms, Full: depends on data size',
|
||||
tips: [
|
||||
'ALWAYS use preview mode first for large datasets',
|
||||
'Preview shows structure + counts without consuming tokens for data',
|
||||
'Summary mode (2 items per node) is safe default',
|
||||
'Use nodeNames to focus on specific nodes only',
|
||||
'itemsLimit: 0 = structure only, -1 = unlimited',
|
||||
'Check recommendation.suggestedMode from preview'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Retrieves and intelligently filters execution data to enable inspection without exceeding token limits. This tool provides multiple modes for different use cases, from quick previews to complete data retrieval.
|
||||
|
||||
**The Problem**: Workflows processing large datasets (50+ database records) generate execution data that exceeds token/response limits, making traditional full-data fetching impossible.
|
||||
|
||||
**The Solution**: Four retrieval modes with smart filtering:
|
||||
1. **Preview**: Structure + counts only (no actual data)
|
||||
2. **Summary**: 2 sample items per node (safe default)
|
||||
3. **Filtered**: Custom limits and node selection
|
||||
4. **Full**: Complete data (use with caution)
|
||||
|
||||
**Recommended Workflow**:
|
||||
1. Start with preview mode to assess size
|
||||
2. Use recommendation to choose appropriate mode
|
||||
3. Fetch filtered data as needed`,
|
||||
|
||||
parameters: {
|
||||
id: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The execution ID to retrieve. Obtained from list_executions or webhook trigger responses'
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: `Retrieval mode (default: auto-detect from other params):
|
||||
- 'preview': Structure, counts, size estimates - NO actual data (fastest)
|
||||
- 'summary': Metadata + 2 sample items per node (safe default)
|
||||
- 'filtered': Custom filtering with itemsLimit/nodeNames
|
||||
- 'full': Complete execution data (use with caution)`
|
||||
},
|
||||
nodeNames: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
description: 'Filter to specific nodes by name. Example: ["HTTP Request", "Filter"]. Useful when you only need to inspect specific nodes.'
|
||||
},
|
||||
itemsLimit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: `Items to return per node (default: 2):
|
||||
- 0: Structure only (see data shape without values)
|
||||
- 1-N: Return N items per node
|
||||
- -1: Unlimited (return all items)
|
||||
|
||||
Note: Structure-only mode (0) shows JSON schema without actual values.`
|
||||
},
|
||||
includeInputData: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
description: 'Include input data in addition to output data (default: false). Useful for debugging data transformations.'
|
||||
},
|
||||
includeData: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
description: 'DEPRECATED: Legacy parameter. Use mode instead. If true, maps to mode="summary" for backward compatibility.'
|
||||
}
|
||||
},
|
||||
|
||||
returns: `**Preview Mode Response**:
|
||||
{
|
||||
mode: 'preview',
|
||||
preview: {
|
||||
totalNodes: number,
|
||||
executedNodes: number,
|
||||
estimatedSizeKB: number,
|
||||
nodes: {
|
||||
[nodeName]: {
|
||||
status: 'success' | 'error',
|
||||
itemCounts: { input: number, output: number },
|
||||
dataStructure: {...}, // JSON schema
|
||||
estimatedSizeKB: number
|
||||
}
|
||||
}
|
||||
},
|
||||
recommendation: {
|
||||
canFetchFull: boolean,
|
||||
suggestedMode: 'preview'|'summary'|'filtered'|'full',
|
||||
suggestedItemsLimit?: number,
|
||||
reason: string
|
||||
}
|
||||
}
|
||||
|
||||
**Summary/Filtered/Full Mode Response**:
|
||||
{
|
||||
mode: 'summary' | 'filtered' | 'full',
|
||||
summary: {
|
||||
totalNodes: number,
|
||||
executedNodes: number,
|
||||
totalItems: number,
|
||||
hasMoreData: boolean // true if truncated
|
||||
},
|
||||
nodes: {
|
||||
[nodeName]: {
|
||||
executionTime: number,
|
||||
itemsInput: number,
|
||||
itemsOutput: number,
|
||||
status: 'success' | 'error',
|
||||
error?: string,
|
||||
data: {
|
||||
output: [...], // Actual data items
|
||||
metadata: {
|
||||
totalItems: number,
|
||||
itemsShown: number,
|
||||
truncated: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
|
||||
examples: [
|
||||
`// Example 1: Preview workflow (RECOMMENDED FIRST STEP)
|
||||
n8n_get_execution({id: "exec_123", mode: "preview"})
|
||||
// Returns structure, counts, size, recommendation
|
||||
// Use this to decide how to fetch data`,
|
||||
|
||||
`// Example 2: Follow recommendation
|
||||
const preview = n8n_get_execution({id: "exec_123", mode: "preview"});
|
||||
if (preview.recommendation.canFetchFull) {
|
||||
n8n_get_execution({id: "exec_123", mode: "full"});
|
||||
} else {
|
||||
n8n_get_execution({
|
||||
id: "exec_123",
|
||||
mode: "filtered",
|
||||
itemsLimit: preview.recommendation.suggestedItemsLimit
|
||||
});
|
||||
}`,
|
||||
|
||||
`// Example 3: Summary mode (safe default for unknown datasets)
|
||||
n8n_get_execution({id: "exec_123", mode: "summary"})
|
||||
// Gets 2 items per node - safe for most cases`,
|
||||
|
||||
`// Example 4: Filter to specific node
|
||||
n8n_get_execution({
|
||||
id: "exec_123",
|
||||
mode: "filtered",
|
||||
nodeNames: ["HTTP Request"],
|
||||
itemsLimit: 5
|
||||
})
|
||||
// Gets only HTTP Request node, 5 items`,
|
||||
|
||||
`// Example 5: Structure only (see data shape)
|
||||
n8n_get_execution({
|
||||
id: "exec_123",
|
||||
mode: "filtered",
|
||||
itemsLimit: 0
|
||||
})
|
||||
// Returns JSON schema without actual values`,
|
||||
|
||||
`// Example 6: Debug with input data
|
||||
n8n_get_execution({
|
||||
id: "exec_123",
|
||||
mode: "filtered",
|
||||
nodeNames: ["Transform"],
|
||||
itemsLimit: 2,
|
||||
includeInputData: true
|
||||
})
|
||||
// See both input and output for debugging`,
|
||||
|
||||
`// Example 7: Backward compatibility (legacy)
|
||||
n8n_get_execution({id: "exec_123"}) // Minimal data
|
||||
n8n_get_execution({id: "exec_123", includeData: true}) // Maps to summary mode`
|
||||
],
|
||||
|
||||
useCases: [
|
||||
'Monitor status of triggered workflows',
|
||||
'Debug failed workflows by examining error messages and partial data',
|
||||
'Inspect large datasets without exceeding token limits',
|
||||
'Validate data transformations between nodes',
|
||||
'Understand execution flow and timing',
|
||||
'Track workflow performance metrics',
|
||||
'Verify successful completion before proceeding',
|
||||
'Extract specific data from execution results'
|
||||
],
|
||||
|
||||
performance: `**Response Times** (approximate):
|
||||
- Preview mode: <50ms (no data, just structure)
|
||||
- Summary mode: <200ms (2 items per node)
|
||||
- Filtered mode: 50-500ms (depends on filters)
|
||||
- Full mode: 200ms-5s (depends on data size)
|
||||
|
||||
**Token Consumption**:
|
||||
- Preview: ~500 tokens (no data values)
|
||||
- Summary (2 items): ~2-5K tokens
|
||||
- Filtered (5 items): ~5-15K tokens
|
||||
- Full (50+ items): 50K+ tokens (may exceed limits)
|
||||
|
||||
**Optimization Tips**:
|
||||
- Use preview for all large datasets
|
||||
- Use nodeNames to focus on relevant nodes only
|
||||
- Start with small itemsLimit and increase if needed
|
||||
- Use itemsLimit: 0 to see structure without data`,
|
||||
|
||||
bestPractices: [
|
||||
'ALWAYS use preview mode first for unknown datasets',
|
||||
'Trust the recommendation.suggestedMode from preview',
|
||||
'Use nodeNames to filter to relevant nodes only',
|
||||
'Start with summary mode if preview indicates moderate size',
|
||||
'Use itemsLimit: 0 to understand data structure',
|
||||
'Check hasMoreData to know if results are truncated',
|
||||
'Store execution IDs from triggers for later inspection',
|
||||
'Use mode="filtered" with custom limits for large datasets',
|
||||
'Include input data only when debugging transformations',
|
||||
'Monitor summary.totalItems to understand dataset size'
|
||||
],
|
||||
|
||||
pitfalls: [
|
||||
'DON\'T fetch full mode without previewing first - may timeout',
|
||||
'DON\'T assume all data fits - always check hasMoreData',
|
||||
'DON\'T ignore the recommendation from preview mode',
|
||||
'Execution data is retained based on n8n settings - old executions may be purged',
|
||||
'Binary data (files, images) is not fully included - only metadata',
|
||||
'Status "waiting" indicates execution is still running',
|
||||
'Error executions may have partial data from successful nodes',
|
||||
'Very large individual items (>1MB) may be truncated',
|
||||
'Preview mode estimates may be off by 10-20% for complex structures',
|
||||
'Node names are case-sensitive in nodeNames filter'
|
||||
],
|
||||
|
||||
modeComparison: `**When to use each mode**:
|
||||
|
||||
**Preview**:
|
||||
- ALWAYS use first for unknown datasets
|
||||
- When you need to know if data is safe to fetch
|
||||
- To see data structure without consuming tokens
|
||||
- To get size estimates and recommendations
|
||||
|
||||
**Summary** (default):
|
||||
- Safe default for most cases
|
||||
- When you need representative samples
|
||||
- When preview recommends it
|
||||
- For quick data inspection
|
||||
|
||||
**Filtered**:
|
||||
- When you need specific nodes only
|
||||
- When you need more than 2 items but not all
|
||||
- When preview recommends it with itemsLimit
|
||||
- For targeted data extraction
|
||||
|
||||
**Full**:
|
||||
- ONLY when preview says canFetchFull: true
|
||||
- For small executions (< 20 items total)
|
||||
- When you genuinely need all data
|
||||
- When you're certain data fits in token limit`,
|
||||
|
||||
relatedTools: [
|
||||
'n8n_list_executions - Find execution IDs',
|
||||
'n8n_trigger_webhook_workflow - Trigger and get execution ID',
|
||||
'n8n_delete_execution - Clean up old executions',
|
||||
'n8n_get_workflow - Get workflow structure',
|
||||
'validate_workflow - Validate before executing'
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const n8nGetWorkflowDetailsDoc: ToolDocumentation = {
|
||||
name: 'n8n_get_workflow_details',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Get workflow details with metadata, version, execution stats. More info than get_workflow.',
|
||||
keyParameters: ['id'],
|
||||
example: 'n8n_get_workflow_details({id: "workflow_123"})',
|
||||
performance: 'Fast (100-300ms)',
|
||||
tips: [
|
||||
'Includes execution statistics',
|
||||
'Shows version history info',
|
||||
'Contains metadata like tags'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Retrieves comprehensive workflow details including metadata, execution statistics, version information, and usage analytics. Provides more information than get_workflow, including data not typically needed for editing but useful for monitoring and analysis.',
|
||||
parameters: {
|
||||
id: { type: 'string', required: true, description: 'Workflow ID to retrieve details for' }
|
||||
},
|
||||
returns: 'Extended workflow object with: id, name, nodes, connections, settings, plus metadata (tags, owner, shared users), execution stats (success/error counts, average runtime), version info, created/updated timestamps',
|
||||
examples: [
|
||||
'n8n_get_workflow_details({id: "abc123"}) - Get workflow with stats',
|
||||
'const details = n8n_get_workflow_details({id: "xyz789"}); // Analyze performance'
|
||||
],
|
||||
useCases: [
|
||||
'Monitor workflow performance',
|
||||
'Analyze execution patterns',
|
||||
'View workflow metadata',
|
||||
'Check version information',
|
||||
'Audit workflow usage'
|
||||
],
|
||||
performance: 'Slightly slower than get_workflow due to additional metadata - typically 100-300ms. Stats may be cached.',
|
||||
bestPractices: [
|
||||
'Use for monitoring and analysis',
|
||||
'Check execution stats before optimization',
|
||||
'Review error counts for debugging',
|
||||
'Monitor average execution times'
|
||||
],
|
||||
pitfalls: [
|
||||
'Requires N8N_API_URL and N8N_API_KEY configured',
|
||||
'More data than needed for simple edits',
|
||||
'Stats may have slight delay',
|
||||
'Not all n8n versions support all fields'
|
||||
],
|
||||
relatedTools: ['n8n_get_workflow', 'n8n_list_executions', 'n8n_get_execution', 'n8n_list_workflows']
|
||||
}
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const n8nGetWorkflowMinimalDoc: ToolDocumentation = {
|
||||
name: 'n8n_get_workflow_minimal',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Get minimal info: ID, name, active status, tags. Fast for listings.',
|
||||
keyParameters: ['id'],
|
||||
example: 'n8n_get_workflow_minimal({id: "workflow_123"})',
|
||||
performance: 'Very fast (<50ms)',
|
||||
tips: [
|
||||
'Fastest way to check workflow exists',
|
||||
'Perfect for status checks',
|
||||
'Use in list displays'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Retrieves only essential workflow information without nodes or connections. Returns minimal data needed for listings, status checks, and quick lookups. Optimized for performance when full workflow data is not needed.',
|
||||
parameters: {
|
||||
id: { type: 'string', required: true, description: 'Workflow ID to retrieve minimal info for' }
|
||||
},
|
||||
returns: 'Minimal workflow object with: id, name, active status, tags array, createdAt, updatedAt. No nodes, connections, or settings included.',
|
||||
examples: [
|
||||
'n8n_get_workflow_minimal({id: "abc123"}) - Quick existence check',
|
||||
'const info = n8n_get_workflow_minimal({id: "xyz789"}); // Check if active'
|
||||
],
|
||||
useCases: [
|
||||
'Quick workflow existence checks',
|
||||
'Display workflow lists',
|
||||
'Check active/inactive status',
|
||||
'Get workflow tags',
|
||||
'Performance-critical operations'
|
||||
],
|
||||
performance: 'Extremely fast - typically under 50ms. Returns only database metadata without loading workflow definition.',
|
||||
bestPractices: [
|
||||
'Use for list displays and dashboards',
|
||||
'Ideal for existence checks before operations',
|
||||
'Cache results for UI responsiveness',
|
||||
'Combine with list_workflows for bulk checks'
|
||||
],
|
||||
pitfalls: [
|
||||
'Requires N8N_API_URL and N8N_API_KEY configured',
|
||||
'No workflow content - cannot edit or validate',
|
||||
'Tags may be empty array',
|
||||
'Must use get_workflow for actual workflow data'
|
||||
],
|
||||
relatedTools: ['n8n_list_workflows', 'n8n_get_workflow', 'n8n_get_workflow_structure', 'n8n_update_partial_workflow']
|
||||
}
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const n8nGetWorkflowStructureDoc: ToolDocumentation = {
|
||||
name: 'n8n_get_workflow_structure',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Get workflow structure: nodes and connections only. No parameter details.',
|
||||
keyParameters: ['id'],
|
||||
example: 'n8n_get_workflow_structure({id: "workflow_123"})',
|
||||
performance: 'Fast (75-150ms)',
|
||||
tips: [
|
||||
'Shows workflow topology',
|
||||
'Node types without parameters',
|
||||
'Perfect for visualization'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Retrieves workflow structural information including node types, positions, and connections, but without detailed node parameters. Ideal for understanding workflow topology, creating visualizations, or analyzing workflow complexity without the overhead of full parameter data.',
|
||||
parameters: {
|
||||
id: { type: 'string', required: true, description: 'Workflow ID to retrieve structure for' }
|
||||
},
|
||||
returns: 'Workflow structure with: id, name, nodes array (id, name, type, position only), connections object. No node parameters, credentials, or settings included.',
|
||||
examples: [
|
||||
'n8n_get_workflow_structure({id: "abc123"}) - Visualize workflow',
|
||||
'const structure = n8n_get_workflow_structure({id: "xyz789"}); // Analyze complexity'
|
||||
],
|
||||
useCases: [
|
||||
'Generate workflow visualizations',
|
||||
'Analyze workflow complexity',
|
||||
'Understand node relationships',
|
||||
'Create workflow diagrams',
|
||||
'Quick topology validation'
|
||||
],
|
||||
performance: 'Fast retrieval - typically 75-150ms. Faster than get_workflow as parameters are stripped.',
|
||||
bestPractices: [
|
||||
'Use for visualization tools',
|
||||
'Ideal for workflow analysis',
|
||||
'Good for connection validation',
|
||||
'Cache for UI diagram rendering'
|
||||
],
|
||||
pitfalls: [
|
||||
'Requires N8N_API_URL and N8N_API_KEY configured',
|
||||
'No parameter data for configuration',
|
||||
'Cannot validate node settings',
|
||||
'Must use get_workflow for editing'
|
||||
],
|
||||
relatedTools: ['n8n_get_workflow', 'n8n_validate_workflow_connections', 'n8n_get_workflow_minimal', 'validate_workflow_connections']
|
||||
}
|
||||
};
|
||||
@@ -4,46 +4,63 @@ export const n8nGetWorkflowDoc: ToolDocumentation = {
|
||||
name: 'n8n_get_workflow',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Get a workflow by ID. Returns the complete workflow including nodes, connections, and settings.',
|
||||
keyParameters: ['id'],
|
||||
example: 'n8n_get_workflow({id: "workflow_123"})',
|
||||
description: 'Get workflow by ID with different detail levels. Use mode to control response size and content.',
|
||||
keyParameters: ['id', 'mode'],
|
||||
example: 'n8n_get_workflow({id: "workflow_123", mode: "structure"})',
|
||||
performance: 'Fast (50-200ms)',
|
||||
tips: [
|
||||
'Returns complete workflow JSON',
|
||||
'Includes all node parameters',
|
||||
'Use get_workflow_minimal for faster listings'
|
||||
'mode="full" (default): Complete workflow with all data',
|
||||
'mode="details": Full workflow + execution stats',
|
||||
'mode="structure": Just nodes and connections (topology)',
|
||||
'mode="minimal": Only id, name, active status, tags'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Retrieves a complete workflow from n8n by its ID. Returns full workflow definition including all nodes with their parameters, connections between nodes, and workflow settings. This is the primary tool for fetching workflows for viewing, editing, or cloning.',
|
||||
description: `**Modes:**
|
||||
- full (default): Complete workflow including all nodes with parameters, connections, and settings
|
||||
- details: Full workflow plus execution statistics (success/error counts, last execution time)
|
||||
- structure: Nodes and connections only - useful for topology analysis
|
||||
- minimal: Just id, name, active status, and tags - fastest response`,
|
||||
parameters: {
|
||||
id: { type: 'string', required: true, description: 'Workflow ID to retrieve' }
|
||||
id: { type: 'string', required: true, description: 'Workflow ID to retrieve' },
|
||||
mode: { type: 'string', required: false, description: 'Detail level: "full" (default), "details", "structure", "minimal"' }
|
||||
},
|
||||
returns: 'Complete workflow object containing: id, name, active status, nodes array (with full parameters), connections object, settings, createdAt, updatedAt',
|
||||
returns: `Depends on mode:
|
||||
- full: Complete workflow object (id, name, active, nodes[], connections{}, settings, createdAt, updatedAt)
|
||||
- details: Full workflow + executionStats (successCount, errorCount, lastExecution, etc.)
|
||||
- structure: { nodes: [...], connections: {...} } - topology only
|
||||
- minimal: { id, name, active, tags, createdAt, updatedAt }`,
|
||||
examples: [
|
||||
'n8n_get_workflow({id: "abc123"}) - Get workflow for editing',
|
||||
'const wf = n8n_get_workflow({id: "xyz789"}); // Clone workflow structure'
|
||||
'// Get complete workflow (default)\nn8n_get_workflow({id: "abc123"})',
|
||||
'// Get workflow with execution stats\nn8n_get_workflow({id: "abc123", mode: "details"})',
|
||||
'// Get just the topology\nn8n_get_workflow({id: "abc123", mode: "structure"})',
|
||||
'// Quick metadata check\nn8n_get_workflow({id: "abc123", mode: "minimal"})'
|
||||
],
|
||||
useCases: [
|
||||
'View workflow configuration',
|
||||
'Export workflow for backup',
|
||||
'Clone workflow structure',
|
||||
'Debug workflow issues',
|
||||
'Prepare for updates'
|
||||
'View and edit workflow (mode=full)',
|
||||
'Analyze workflow performance (mode=details)',
|
||||
'Clone or compare workflow structure (mode=structure)',
|
||||
'List workflows with status (mode=minimal)',
|
||||
'Debug workflow issues'
|
||||
],
|
||||
performance: 'Fast retrieval - typically 50-200ms depending on workflow size. Cached by n8n for performance.',
|
||||
performance: `Response times vary by mode:
|
||||
- minimal: ~20-50ms (smallest response)
|
||||
- structure: ~30-80ms (nodes + connections only)
|
||||
- full: ~50-200ms (complete workflow)
|
||||
- details: ~100-300ms (includes execution queries)`,
|
||||
bestPractices: [
|
||||
'Check workflow exists before updating',
|
||||
'Use for complete workflow data needs',
|
||||
'Cache results when making multiple operations',
|
||||
'Validate after retrieving if modifying'
|
||||
'Use mode="minimal" when listing or checking status',
|
||||
'Use mode="structure" for workflow analysis or cloning',
|
||||
'Use mode="full" (default) when editing',
|
||||
'Use mode="details" for debugging execution issues',
|
||||
'Validate workflow after retrieval if planning modifications'
|
||||
],
|
||||
pitfalls: [
|
||||
'Requires N8N_API_URL and N8N_API_KEY configured',
|
||||
'Returns all data - use minimal/structure for performance',
|
||||
'Workflow must exist or returns 404',
|
||||
'Credentials are referenced but not included'
|
||||
'mode="details" adds database queries for execution stats',
|
||||
'Workflow must exist or returns 404 error',
|
||||
'Credentials are referenced by ID but values not included'
|
||||
],
|
||||
relatedTools: ['n8n_get_workflow_minimal', 'n8n_get_workflow_structure', 'n8n_update_full_workflow', 'n8n_validate_workflow']
|
||||
relatedTools: ['n8n_list_workflows', 'n8n_update_full_workflow', 'n8n_update_partial_workflow', 'n8n_validate_workflow']
|
||||
}
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const n8nListExecutionsDoc: ToolDocumentation = {
|
||||
name: 'n8n_list_executions',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'List workflow executions with optional filters. Supports pagination for large result sets.',
|
||||
keyParameters: ['workflowId', 'status', 'limit'],
|
||||
example: 'n8n_list_executions({workflowId: "abc123", status: "error"})',
|
||||
performance: 'Fast metadata retrieval, use pagination for large datasets',
|
||||
tips: [
|
||||
'Filter by status (success/error/waiting) to find specific execution types',
|
||||
'Use workflowId to see all executions for a specific workflow',
|
||||
'Pagination via cursor allows retrieving large execution histories'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Lists workflow executions with powerful filtering options. This tool is essential for monitoring workflow performance, finding failed executions, and tracking workflow activity. Supports pagination for retrieving large execution histories and filtering by workflow, status, and project.`,
|
||||
parameters: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Number of executions to return (1-100, default: 100). Use with cursor for pagination'
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Pagination cursor from previous response. Used to retrieve next page of results'
|
||||
},
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Filter executions by specific workflow ID. Shows all executions for that workflow'
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Filter by project ID (enterprise feature). Groups executions by project'
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
enum: ['success', 'error', 'waiting'],
|
||||
description: 'Filter by execution status. Success = completed, Error = failed, Waiting = running'
|
||||
},
|
||||
includeData: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
description: 'Include execution data in results (default: false). Significantly increases response size'
|
||||
}
|
||||
},
|
||||
returns: `Array of execution objects with metadata, pagination cursor for next page, and optionally execution data. Each execution includes ID, status, start/end times, and workflow reference.`,
|
||||
examples: [
|
||||
'n8n_list_executions({limit: 10}) - Get 10 most recent executions',
|
||||
'n8n_list_executions({workflowId: "abc123"}) - All executions for specific workflow',
|
||||
'n8n_list_executions({status: "error", limit: 50}) - Find failed executions',
|
||||
'n8n_list_executions({status: "waiting"}) - Monitor currently running workflows',
|
||||
'n8n_list_executions({cursor: "next-page-token"}) - Get next page of results'
|
||||
],
|
||||
useCases: [
|
||||
'Monitor workflow execution history and patterns',
|
||||
'Find and debug failed workflow executions',
|
||||
'Track currently running workflows (waiting status)',
|
||||
'Analyze workflow performance and execution frequency',
|
||||
'Generate execution reports for specific workflows'
|
||||
],
|
||||
performance: `Listing executions is fast for metadata only. Including data (includeData: true) significantly impacts performance. Use pagination (limit + cursor) for large result sets. Default limit of 100 balances performance with usability.`,
|
||||
bestPractices: [
|
||||
'Use status filters to focus on specific execution types',
|
||||
'Implement pagination for large execution histories',
|
||||
'Avoid includeData unless you need execution details',
|
||||
'Filter by workflowId when monitoring specific workflows',
|
||||
'Check for cursor in response to detect more pages'
|
||||
],
|
||||
pitfalls: [
|
||||
'Large limits with includeData can cause timeouts',
|
||||
'Execution retention depends on n8n configuration',
|
||||
'Cursor tokens expire - use them promptly',
|
||||
'Status "waiting" includes both running and queued executions',
|
||||
'Deleted workflows still show in execution history'
|
||||
],
|
||||
relatedTools: ['n8n_get_execution', 'n8n_trigger_webhook_workflow', 'n8n_delete_execution', 'n8n_list_workflows']
|
||||
}
|
||||
};
|
||||
@@ -50,6 +50,6 @@ export const n8nListWorkflowsDoc: ToolDocumentation = {
|
||||
'Server may return fewer than requested limit',
|
||||
'returned field is count of current page only, not system total'
|
||||
],
|
||||
relatedTools: ['n8n_get_workflow_minimal', 'n8n_get_workflow', 'n8n_update_partial_workflow', 'n8n_list_executions']
|
||||
relatedTools: ['n8n_get_workflow', 'n8n_update_partial_workflow', 'n8n_executions']
|
||||
}
|
||||
};
|
||||
@@ -64,13 +64,13 @@ export const n8nTriggerWebhookWorkflowDoc: ToolDocumentation = {
|
||||
When a webhook trigger fails, the error response now includes specific guidance to help debug the issue:
|
||||
|
||||
**Error with Execution ID** (workflow started but failed):
|
||||
- Format: "Workflow {workflowId} execution {executionId} failed. Use n8n_get_execution({id: '{executionId}', mode: 'preview'}) to investigate the error."
|
||||
- Format: "Workflow {workflowId} execution {executionId} failed. Use n8n_executions({action: 'get', id: '{executionId}', mode: 'preview'}) to investigate the error."
|
||||
- Response includes: executionId and workflowId fields for direct access
|
||||
- Recommended action: Use n8n_get_execution with mode='preview' for fast, efficient error inspection
|
||||
- Recommended action: Use n8n_executions with action='get' and mode='preview' for fast, efficient error inspection
|
||||
|
||||
**Error without Execution ID** (workflow didn't start):
|
||||
- Format: "Workflow failed to execute. Use n8n_list_executions to find recent executions, then n8n_get_execution with mode='preview' to investigate."
|
||||
- Recommended action: Check recent executions with n8n_list_executions
|
||||
- Format: "Workflow failed to execute. Use n8n_executions({action: 'list'}) to find recent executions, then n8n_executions({action: 'get', mode: 'preview'}) to investigate."
|
||||
- Recommended action: Check recent executions with n8n_executions({action: 'list'})
|
||||
|
||||
**Why mode='preview'?**
|
||||
- Fast: <50ms response time
|
||||
@@ -92,7 +92,7 @@ When a webhook trigger fails, the error response now includes specific guidance
|
||||
|
||||
**Investigation Workflow**:
|
||||
1. Trigger returns error with execution ID
|
||||
2. Call n8n_get_execution({id: executionId, mode: 'preview'}) to see structure and error
|
||||
2. Call n8n_executions({action: 'get', id: executionId, mode: 'preview'}) to see structure and error
|
||||
3. Based on preview recommendation, fetch more data if needed
|
||||
4. Fix issues in workflow and retry`,
|
||||
bestPractices: [
|
||||
@@ -101,7 +101,7 @@ When a webhook trigger fails, the error response now includes specific guidance
|
||||
'Use async mode (waitForResponse: false) for long-running workflows',
|
||||
'Include authentication headers when webhook requires them',
|
||||
'Test webhook URL manually first to ensure it works',
|
||||
'When errors occur, use n8n_get_execution with mode="preview" first for efficient debugging',
|
||||
'When errors occur, use n8n_executions with action="get" and mode="preview" first for efficient debugging',
|
||||
'Store execution IDs from error responses for later investigation'
|
||||
],
|
||||
pitfalls: [
|
||||
@@ -110,9 +110,9 @@ When a webhook trigger fails, the error response now includes specific guidance
|
||||
'Webhook node must be the trigger node in the workflow',
|
||||
'Timeout errors occur with long workflows in sync mode',
|
||||
'Data format must match webhook node expectations',
|
||||
'Error messages always include n8n_get_execution guidance - follow the suggested steps for efficient debugging',
|
||||
'Error messages always include n8n_executions guidance - follow the suggested steps for efficient debugging',
|
||||
'Execution IDs in error responses are crucial for debugging - always check for and use them'
|
||||
],
|
||||
relatedTools: ['n8n_get_execution', 'n8n_list_executions', 'n8n_get_workflow', 'n8n_create_workflow']
|
||||
relatedTools: ['n8n_executions', 'n8n_get_workflow', 'n8n_create_workflow']
|
||||
}
|
||||
};
|
||||
@@ -90,7 +90,6 @@ Full support for all 8 AI connection types used in n8n AI workflows:
|
||||
|
||||
**Important Notes**:
|
||||
- **AI nodes do NOT require main connections**: Nodes like OpenAI Chat Model, Postgres Chat Memory, Embeddings OpenAI, and Supabase Vector Store use AI-specific connection types exclusively. They should ONLY have connections like \`ai_languageModel\`, \`ai_memory\`, \`ai_embedding\`, or \`ai_tool\` - NOT \`main\` connections.
|
||||
- **Fixed in v2.21.1**: Validation now correctly recognizes AI nodes that only have AI-specific connections without requiring \`main\` connections (resolves issue #357).
|
||||
|
||||
**Best Practices**:
|
||||
- Always specify \`sourceOutput\` for AI connections (defaults to "main" if omitted)
|
||||
|
||||
@@ -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',
|
||||
'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']
|
||||
}
|
||||
};
|
||||
168
src/mcp/tool-docs/workflow_management/n8n-workflow-versions.ts
Normal file
168
src/mcp/tool-docs/workflow_management/n8n-workflow-versions.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const n8nWorkflowVersionsDoc: ToolDocumentation = {
|
||||
name: 'n8n_workflow_versions',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Manage workflow version history, rollback to previous versions, and cleanup old versions',
|
||||
keyParameters: ['mode', 'workflowId', 'versionId'],
|
||||
example: 'n8n_workflow_versions({mode: "list", workflowId: "abc123"})',
|
||||
performance: 'Fast for list/get (~100ms), moderate for rollback (~200-500ms)',
|
||||
tips: [
|
||||
'Use mode="list" to see all saved versions before rollback',
|
||||
'Rollback creates a backup version automatically',
|
||||
'Use prune to clean up old versions and save storage',
|
||||
'truncate requires explicit confirmTruncate: true'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Comprehensive workflow version management system. Supports six operations:
|
||||
|
||||
**list** - Show version history for a workflow
|
||||
- Returns all saved versions with timestamps, snapshot sizes, and metadata
|
||||
- Use limit parameter to control how many versions to return
|
||||
|
||||
**get** - Get details of a specific version
|
||||
- Returns the complete workflow snapshot from that version
|
||||
- Use to compare versions or extract old configurations
|
||||
|
||||
**rollback** - Restore workflow to a previous version
|
||||
- Creates a backup of the current workflow before rollback
|
||||
- Optionally validates the workflow structure before applying
|
||||
- Returns the restored workflow and backup version ID
|
||||
|
||||
**delete** - Delete specific version(s)
|
||||
- Delete a single version by versionId
|
||||
- Delete all versions for a workflow with deleteAll: true
|
||||
|
||||
**prune** - Clean up old versions
|
||||
- Keeps only the N most recent versions (default: 10)
|
||||
- Useful for managing storage and keeping history manageable
|
||||
|
||||
**truncate** - Delete ALL versions for ALL workflows
|
||||
- Dangerous operation requiring explicit confirmation
|
||||
- Use for complete version history cleanup`,
|
||||
parameters: {
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Operation mode: "list", "get", "rollback", "delete", "prune", or "truncate"',
|
||||
enum: ['list', 'get', 'rollback', 'delete', 'prune', 'truncate']
|
||||
},
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Workflow ID (required for list, rollback, delete, prune modes)'
|
||||
},
|
||||
versionId: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Version ID (required for get mode, optional for rollback to specific version, required for single delete)'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
default: 10,
|
||||
description: 'Maximum versions to return in list mode'
|
||||
},
|
||||
validateBefore: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
default: true,
|
||||
description: 'Validate workflow structure before rollback (rollback mode only)'
|
||||
},
|
||||
deleteAll: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
default: false,
|
||||
description: 'Delete all versions for workflow (delete mode only)'
|
||||
},
|
||||
maxVersions: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
default: 10,
|
||||
description: 'Keep N most recent versions (prune mode only)'
|
||||
},
|
||||
confirmTruncate: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
default: false,
|
||||
description: 'REQUIRED: Must be true to truncate all versions (truncate mode only)'
|
||||
}
|
||||
},
|
||||
returns: `Response varies by mode:
|
||||
|
||||
**list mode:**
|
||||
- versions: Array of version objects with id, workflowId, snapshotSize, createdAt
|
||||
- totalCount: Total number of versions
|
||||
|
||||
**get mode:**
|
||||
- version: Complete version object including workflow snapshot
|
||||
|
||||
**rollback mode:**
|
||||
- success: Boolean indicating success
|
||||
- restoredVersion: The version that was restored
|
||||
- backupVersionId: ID of the backup created before rollback
|
||||
|
||||
**delete mode:**
|
||||
- deletedCount: Number of versions deleted
|
||||
|
||||
**prune mode:**
|
||||
- prunedCount: Number of old versions removed
|
||||
- remainingCount: Number of versions kept
|
||||
|
||||
**truncate mode:**
|
||||
- deletedCount: Total versions deleted across all workflows`,
|
||||
examples: [
|
||||
'// List version history\nn8n_workflow_versions({mode: "list", workflowId: "abc123", limit: 5})',
|
||||
'// Get specific version details\nn8n_workflow_versions({mode: "get", versionId: 42})',
|
||||
'// Rollback to latest saved version\nn8n_workflow_versions({mode: "rollback", workflowId: "abc123"})',
|
||||
'// Rollback to specific version\nn8n_workflow_versions({mode: "rollback", workflowId: "abc123", versionId: 42})',
|
||||
'// Delete specific version\nn8n_workflow_versions({mode: "delete", workflowId: "abc123", versionId: 42})',
|
||||
'// Delete all versions for workflow\nn8n_workflow_versions({mode: "delete", workflowId: "abc123", deleteAll: true})',
|
||||
'// Prune to keep only 5 most recent\nn8n_workflow_versions({mode: "prune", workflowId: "abc123", maxVersions: 5})',
|
||||
'// Truncate all versions (dangerous!)\nn8n_workflow_versions({mode: "truncate", confirmTruncate: true})'
|
||||
],
|
||||
useCases: [
|
||||
'Recover from accidental workflow changes',
|
||||
'Compare workflow versions to understand changes',
|
||||
'Maintain audit trail of workflow modifications',
|
||||
'Clean up old versions to save database storage',
|
||||
'Roll back failed workflow deployments'
|
||||
],
|
||||
performance: `Performance varies by operation:
|
||||
- list: Fast (~100ms) - simple database query
|
||||
- get: Fast (~100ms) - single row retrieval
|
||||
- rollback: Moderate (~200-500ms) - includes backup creation and workflow update
|
||||
- delete: Fast (~50-100ms) - database delete operation
|
||||
- prune: Moderate (~100-300ms) - depends on number of versions to delete
|
||||
- truncate: Slow (1-5s) - deletes all records across all workflows`,
|
||||
modeComparison: `| Mode | Required Params | Optional Params | Risk Level |
|
||||
|------|-----------------|-----------------|------------|
|
||||
| list | workflowId | limit | Low |
|
||||
| get | versionId | - | Low |
|
||||
| rollback | workflowId | versionId, validateBefore | Medium |
|
||||
| delete | workflowId | versionId, deleteAll | High |
|
||||
| prune | workflowId | maxVersions | Medium |
|
||||
| truncate | confirmTruncate=true | - | Critical |`,
|
||||
bestPractices: [
|
||||
'Always list versions before rollback to pick the right one',
|
||||
'Enable validateBefore for rollback to catch structural issues',
|
||||
'Use prune regularly to keep version history manageable',
|
||||
'Never use truncate in production without explicit need',
|
||||
'Document why you are rolling back for audit purposes'
|
||||
],
|
||||
pitfalls: [
|
||||
'Rollback overwrites current workflow - backup is created automatically',
|
||||
'Deleted versions cannot be recovered',
|
||||
'Truncate affects ALL workflows - use with extreme caution',
|
||||
'Version IDs are sequential but may have gaps after deletes',
|
||||
'Large workflows may have significant version storage overhead'
|
||||
],
|
||||
relatedTools: [
|
||||
'n8n_get_workflow - View current workflow state',
|
||||
'n8n_update_partial_workflow - Make incremental changes',
|
||||
'n8n_validate_workflow - Validate before deployment'
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -84,57 +84,67 @@ When working with Code nodes, always start by calling the relevant guide:
|
||||
|
||||
## Standard Workflow Pattern
|
||||
|
||||
⚠️ **CRITICAL**: Always call get_node_essentials() FIRST before configuring any node!
|
||||
⚠️ **CRITICAL**: Always call get_node() with detail='standard' FIRST before configuring any node!
|
||||
|
||||
1. **Find** the node you need:
|
||||
- search_nodes({query: "slack"}) - Search by keyword
|
||||
- list_nodes({category: "communication"}) - List by category
|
||||
- list_ai_tools() - List AI-capable nodes
|
||||
- search_nodes({query: "communication"}) - Search by category name
|
||||
- search_nodes({query: "AI langchain"}) - Search for AI-capable nodes
|
||||
|
||||
2. **Configure** the node (ALWAYS START WITH ESSENTIALS):
|
||||
- ✅ get_node_essentials("nodes-base.slack") - Get essential properties FIRST (5KB, shows required fields)
|
||||
- get_node_info("nodes-base.slack") - Get complete schema only if essentials insufficient (100KB+)
|
||||
- search_node_properties("nodes-base.slack", "auth") - Find specific properties
|
||||
2. **Configure** the node (ALWAYS START WITH STANDARD DETAIL):
|
||||
- ✅ get_node({nodeType: "nodes-base.slack", detail: "standard"}) - Get essential properties FIRST (~1-2KB, shows required fields)
|
||||
- get_node({nodeType: "nodes-base.slack", detail: "full"}) - Get complete schema only if standard insufficient (~100KB+)
|
||||
- get_node({nodeType: "nodes-base.slack", mode: "docs"}) - Get readable markdown documentation
|
||||
- get_node({nodeType: "nodes-base.slack", mode: "search_properties", propertyQuery: "auth"}) - Find specific properties
|
||||
|
||||
3. **Validate** before deployment:
|
||||
- validate_node_minimal("nodes-base.slack", config) - Check required fields (includes automatic structure validation)
|
||||
- validate_node_operation("nodes-base.slack", config) - Full validation with fixes (includes automatic structure validation)
|
||||
- validate_workflow(workflow) - Validate entire workflow
|
||||
- validate_node({nodeType: "nodes-base.slack", config: {...}, mode: "minimal"}) - Quick required fields check
|
||||
- validate_node({nodeType: "nodes-base.slack", config: {...}}) - Full validation with errors/warnings/suggestions
|
||||
- validate_workflow({workflow: {...}}) - Validate entire workflow
|
||||
|
||||
## Tool Categories
|
||||
## Tool Categories (19 Tools Total)
|
||||
|
||||
**Discovery Tools**
|
||||
- search_nodes - Full-text search across all nodes
|
||||
- list_nodes - List nodes with filtering by category, package, or type
|
||||
- list_ai_tools - List all AI-capable nodes with usage guidance
|
||||
**Discovery Tools** (1 tool)
|
||||
- search_nodes - Full-text search across all nodes (supports OR, AND, FUZZY modes)
|
||||
|
||||
**Configuration Tools**
|
||||
- get_node_essentials - ✅ CALL THIS FIRST! Returns 10-20 key properties with examples and required fields
|
||||
- get_node_info - Returns complete node schema (only use if essentials is insufficient)
|
||||
- search_node_properties - Search for specific properties within a node
|
||||
- get_property_dependencies - Analyze property visibility dependencies
|
||||
**Configuration Tools** (1 consolidated tool)
|
||||
- get_node - Unified node information tool:
|
||||
- detail='minimal'/'standard'/'full': Progressive detail levels
|
||||
- mode='docs': Readable markdown documentation
|
||||
- mode='search_properties': Find specific properties
|
||||
- mode='versions'/'compare'/'breaking'/'migrations': Version management
|
||||
|
||||
**Validation Tools**
|
||||
- validate_node_minimal - Quick validation of required fields (includes structure validation)
|
||||
- validate_node_operation - Full validation with operation awareness (includes structure validation)
|
||||
- validate_workflow - Complete workflow validation including connections
|
||||
**Validation Tools** (2 tools)
|
||||
- validate_node - Unified validation with mode='full' or mode='minimal'
|
||||
- validate_workflow - Complete workflow validation (nodes, connections, expressions)
|
||||
|
||||
**Template Tools**
|
||||
- list_tasks - List common task templates
|
||||
- get_node_for_task - Get pre-configured node for specific tasks
|
||||
- search_templates - Search workflow templates by keyword
|
||||
**Template Tools** (2 tools)
|
||||
- get_template - Get complete workflow JSON by ID
|
||||
- search_templates - Unified template search:
|
||||
- searchMode='keyword': Text search (default)
|
||||
- searchMode='by_nodes': Find templates using specific nodes
|
||||
- searchMode='by_task': Curated task-based templates
|
||||
- searchMode='by_metadata': Filter by complexity/services
|
||||
|
||||
**n8n API Tools** (requires N8N_API_URL configuration)
|
||||
**n8n API Tools** (13 tools, requires N8N_API_URL configuration)
|
||||
- n8n_create_workflow - Create new workflows
|
||||
- n8n_update_partial_workflow - Update workflows using diff operations
|
||||
- n8n_validate_workflow - Validate workflow from n8n instance
|
||||
- n8n_trigger_webhook_workflow - Trigger workflow execution
|
||||
- n8n_get_workflow - Get workflow with mode='full'/'details'/'structure'/'minimal'
|
||||
- n8n_update_full_workflow - Full workflow replacement
|
||||
- n8n_update_partial_workflow - Incremental diff-based updates
|
||||
- n8n_delete_workflow - Delete workflow
|
||||
- n8n_list_workflows - List workflows with filters
|
||||
- n8n_validate_workflow - Validate workflow by ID
|
||||
- n8n_autofix_workflow - Auto-fix common issues
|
||||
- n8n_trigger_webhook_workflow - Trigger via webhook
|
||||
- n8n_executions - Unified execution management (action='get'/'list'/'delete')
|
||||
- n8n_health_check - Check n8n API connectivity
|
||||
- n8n_workflow_versions - Version history and rollback
|
||||
- n8n_deploy_template - Deploy templates directly to n8n instance
|
||||
|
||||
## Performance Characteristics
|
||||
- Instant (<10ms): search_nodes, list_nodes, get_node_essentials
|
||||
- Fast (<100ms): validate_node_minimal, get_node_for_task
|
||||
- Moderate (100-500ms): validate_workflow, get_node_info
|
||||
- Instant (<10ms): search_nodes, get_node (minimal/standard)
|
||||
- Fast (<100ms): validate_node, get_template
|
||||
- Moderate (100-500ms): validate_workflow, get_node (full detail)
|
||||
- Network-dependent: All n8n_* tools
|
||||
|
||||
For comprehensive documentation on any tool:
|
||||
@@ -167,7 +177,7 @@ ${tools.map(toolName => {
|
||||
|
||||
## Usage Notes
|
||||
- All node types require the "nodes-base." or "nodes-langchain." prefix
|
||||
- Use get_node_essentials() first for most tasks (95% smaller than get_node_info)
|
||||
- Use get_node() with detail='standard' first for most tasks (~95% smaller than detail='full')
|
||||
- Validation profiles: minimal (editing), runtime (default), strict (deployment)
|
||||
- n8n API tools only available when N8N_API_URL and N8N_API_KEY are configured
|
||||
|
||||
@@ -413,8 +423,8 @@ try {
|
||||
5. Use descriptive variable names
|
||||
|
||||
## Related Tools
|
||||
- get_node_essentials("nodes-base.code")
|
||||
- validate_node_operation()
|
||||
- get_node({nodeType: "nodes-base.code"}) - Get Code node configuration details
|
||||
- validate_node({nodeType: "nodes-base.code", config: {...}}) - Validate Code node setup
|
||||
- python_code_node_guide (for Python syntax)`;
|
||||
}
|
||||
|
||||
@@ -682,7 +692,7 @@ except json.JSONDecodeError:
|
||||
\`\`\`
|
||||
|
||||
## Related Tools
|
||||
- get_node_essentials("nodes-base.code")
|
||||
- validate_node_operation()
|
||||
- get_node({nodeType: "nodes-base.code"}) - Get Code node configuration details
|
||||
- validate_node({nodeType: "nodes-base.code", config: {...}}) - Validate Code node setup
|
||||
- javascript_code_node_guide (for JavaScript syntax)`;
|
||||
}
|
||||
@@ -13,25 +13,18 @@ export const n8nFriendlyDescriptions: Record<string, {
|
||||
description: string;
|
||||
params: Record<string, string>;
|
||||
}> = {
|
||||
// Validation tools - most prone to errors
|
||||
validate_node_operation: {
|
||||
description: 'Validate n8n node. ALWAYS pass two parameters: nodeType (string) and config (object). Example call: {"nodeType": "nodes-base.slack", "config": {"resource": "channel", "operation": "create"}}',
|
||||
// Consolidated validation tool (replaces validate_node_operation and validate_node_minimal)
|
||||
validate_node: {
|
||||
description: 'Validate n8n node config. Pass nodeType (string) and config (object). Use mode="full" for comprehensive validation, mode="minimal" for quick check. Example: {"nodeType": "nodes-base.slack", "config": {"resource": "channel", "operation": "create"}}',
|
||||
params: {
|
||||
nodeType: 'String value like "nodes-base.slack"',
|
||||
config: 'Object value like {"resource": "channel", "operation": "create"} or empty object {}',
|
||||
mode: 'Optional string: "full" (default) or "minimal"',
|
||||
profile: 'Optional string: "minimal" or "runtime" or "ai-friendly" or "strict"'
|
||||
}
|
||||
},
|
||||
|
||||
validate_node_minimal: {
|
||||
description: 'Check required fields. MUST pass: nodeType (string) and config (object). Example: {"nodeType": "nodes-base.webhook", "config": {}}',
|
||||
params: {
|
||||
nodeType: 'String like "nodes-base.webhook"',
|
||||
config: 'Object, use {} for empty'
|
||||
}
|
||||
},
|
||||
|
||||
// Search and info tools
|
||||
|
||||
// Search tool
|
||||
search_nodes: {
|
||||
description: 'Search nodes. Pass query (string). Example: {"query": "webhook"}',
|
||||
params: {
|
||||
@@ -39,98 +32,53 @@ export const n8nFriendlyDescriptions: Record<string, {
|
||||
limit: 'Optional number, default 20'
|
||||
}
|
||||
},
|
||||
|
||||
get_node_info: {
|
||||
description: 'Get node details. Pass nodeType (string). Example: {"nodeType": "nodes-base.httpRequest"}',
|
||||
|
||||
// Consolidated node info tool (replaces get_node_info, get_node_essentials, get_node_documentation, search_node_properties)
|
||||
get_node: {
|
||||
description: 'Get node info with multiple modes. Pass nodeType (string). Use mode="info" for config, mode="docs" for documentation, mode="search_properties" with propertyQuery for finding fields. Example: {"nodeType": "nodes-base.httpRequest", "detail": "standard"}',
|
||||
params: {
|
||||
nodeType: 'String with prefix like "nodes-base.httpRequest"'
|
||||
nodeType: 'String with prefix like "nodes-base.httpRequest"',
|
||||
mode: 'Optional string: "info" (default), "docs", "search_properties", "versions", "compare", "breaking", "migrations"',
|
||||
detail: 'Optional string: "minimal", "standard" (default), "full"',
|
||||
propertyQuery: 'For mode="search_properties": search term like "auth"'
|
||||
}
|
||||
},
|
||||
|
||||
get_node_essentials: {
|
||||
description: 'Get node basics. Pass nodeType (string). Example: {"nodeType": "nodes-base.slack"}',
|
||||
params: {
|
||||
nodeType: 'String with prefix like "nodes-base.slack"'
|
||||
}
|
||||
},
|
||||
|
||||
// Task tools
|
||||
get_node_for_task: {
|
||||
description: 'Find node for task. Pass task (string). Example: {"task": "send_http_request"}',
|
||||
params: {
|
||||
task: 'String task name like "send_http_request"'
|
||||
}
|
||||
},
|
||||
|
||||
list_tasks: {
|
||||
description: 'List tasks by category. Pass category (string). Example: {"category": "HTTP/API"}',
|
||||
params: {
|
||||
category: 'String: "HTTP/API" or "Webhooks" or "Database" or "AI/LangChain" or "Data Processing" or "Communication"'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Workflow validation
|
||||
validate_workflow: {
|
||||
description: 'Validate workflow. Pass workflow object. MUST have: {"workflow": {"nodes": [array of node objects], "connections": {object with node connections}}}. Each node needs: name, type, typeVersion, position.',
|
||||
description: 'Validate workflow structure, connections, and expressions. Pass workflow object. MUST have: {"workflow": {"nodes": [array of node objects], "connections": {object with node connections}}}. Each node needs: name, type, typeVersion, position.',
|
||||
params: {
|
||||
workflow: 'Object with two required fields: nodes (array) and connections (object). Example: {"nodes": [{"name": "Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [250, 300], "parameters": {}}], "connections": {}}',
|
||||
options: 'Optional object. Example: {"validateNodes": true, "profile": "runtime"}'
|
||||
options: 'Optional object. Example: {"validateNodes": true, "validateConnections": true, "validateExpressions": true, "profile": "runtime"}'
|
||||
}
|
||||
},
|
||||
|
||||
validate_workflow_connections: {
|
||||
description: 'Validate workflow connections only. Pass workflow object. Example: {"workflow": {"nodes": [...], "connections": {}}}',
|
||||
params: {
|
||||
workflow: 'Object with nodes array and connections object. Minimal example: {"nodes": [{"name": "Webhook"}], "connections": {}}'
|
||||
}
|
||||
},
|
||||
|
||||
validate_workflow_expressions: {
|
||||
description: 'Validate n8n expressions in workflow. Pass workflow object. Example: {"workflow": {"nodes": [...], "connections": {}}}',
|
||||
params: {
|
||||
workflow: 'Object with nodes array and connections object containing n8n expressions like {{ $json.data }}'
|
||||
}
|
||||
},
|
||||
|
||||
// Property tools
|
||||
get_property_dependencies: {
|
||||
description: 'Get field dependencies. Pass nodeType (string) and optional config (object). Example: {"nodeType": "nodes-base.httpRequest", "config": {}}',
|
||||
params: {
|
||||
nodeType: 'String like "nodes-base.httpRequest"',
|
||||
config: 'Optional object, use {} for empty'
|
||||
}
|
||||
},
|
||||
|
||||
// AI tool info
|
||||
get_node_as_tool_info: {
|
||||
description: 'Get AI tool usage. Pass nodeType (string). Example: {"nodeType": "nodes-base.slack"}',
|
||||
params: {
|
||||
nodeType: 'String with prefix like "nodes-base.slack"'
|
||||
}
|
||||
},
|
||||
|
||||
// Template tools
|
||||
|
||||
// Consolidated template search (replaces search_templates, list_node_templates, search_templates_by_metadata, get_templates_for_task)
|
||||
search_templates: {
|
||||
description: 'Search workflow templates. Pass query (string). Example: {"query": "chatbot"}',
|
||||
description: 'Search workflow templates with multiple modes. Use searchMode="keyword" for text search, searchMode="by_nodes" to find by node types, searchMode="by_task" for task-based templates, searchMode="by_metadata" for filtering. Example: {"query": "chatbot"} or {"searchMode": "by_task", "task": "webhook_processing"}',
|
||||
params: {
|
||||
query: 'String keyword like "chatbot" or "webhook"',
|
||||
query: 'For searchMode="keyword": string keyword like "chatbot"',
|
||||
searchMode: 'Optional: "keyword" (default), "by_nodes", "by_task", "by_metadata"',
|
||||
nodeTypes: 'For searchMode="by_nodes": array like ["n8n-nodes-base.httpRequest"]',
|
||||
task: 'For searchMode="by_task": task like "webhook_processing", "ai_automation"',
|
||||
limit: 'Optional number, default 20'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
get_template: {
|
||||
description: 'Get template by ID. Pass templateId (number). Example: {"templateId": 1234}',
|
||||
params: {
|
||||
templateId: 'Number ID like 1234'
|
||||
templateId: 'Number ID like 1234',
|
||||
mode: 'Optional: "full" (default), "nodes_only", "structure"'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Documentation tool
|
||||
tools_documentation: {
|
||||
description: 'Get tool docs. Pass optional depth (string). Example: {"depth": "essentials"} or {}',
|
||||
params: {
|
||||
depth: 'Optional string: "essentials" or "overview" or "detailed"',
|
||||
topic: 'Optional string topic name'
|
||||
depth: 'Optional string: "essentials" (default) or "full"',
|
||||
topic: 'Optional string tool name like "search_nodes"'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,55 +70,19 @@ export const n8nManagementTools: ToolDefinition[] = [
|
||||
},
|
||||
{
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Workflow ID'
|
||||
}
|
||||
},
|
||||
required: ['id']
|
||||
}
|
||||
},
|
||||
{
|
||||
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'
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Workflow ID'
|
||||
},
|
||||
mode: {
|
||||
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'
|
||||
}
|
||||
},
|
||||
required: ['id']
|
||||
@@ -343,122 +307,87 @@ export const n8nManagementTools: ToolDefinition[] = [
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'n8n_get_execution',
|
||||
description: `Get execution details with smart filtering. RECOMMENDED: Use mode='preview' first to assess data size.
|
||||
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)`,
|
||||
name: 'n8n_executions',
|
||||
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.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
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: {
|
||||
type: 'string',
|
||||
description: 'Execution ID'
|
||||
description: 'Execution ID (required for action=get or action=delete)'
|
||||
},
|
||||
// For action='get' - detail level
|
||||
mode: {
|
||||
type: 'string',
|
||||
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: {
|
||||
type: 'array',
|
||||
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: {
|
||||
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: {
|
||||
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: {
|
||||
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']
|
||||
}
|
||||
},
|
||||
{
|
||||
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']
|
||||
required: ['action']
|
||||
}
|
||||
},
|
||||
|
||||
// System Tools
|
||||
{
|
||||
name: 'n8n_health_check',
|
||||
description: `Check n8n instance health and API connectivity. Returns status and available features.`,
|
||||
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.`,
|
||||
description: `Check n8n instance health and API connectivity. Use mode='diagnostic' for detailed troubleshooting with env vars and tool status.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
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: {
|
||||
type: 'boolean',
|
||||
description: 'Include detailed debug information (default: false)'
|
||||
description: 'Include extra details in diagnostic mode (default: false)'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -516,5 +445,40 @@ Examples:
|
||||
},
|
||||
required: ['mode']
|
||||
}
|
||||
},
|
||||
|
||||
// Template Deployment Tool
|
||||
{
|
||||
name: 'n8n_deploy_template',
|
||||
description: `Deploy a workflow template from n8n.io directly to your n8n instance. Fetches template, optionally upgrades node versions and validates, then creates workflow. Returns workflow ID and required credentials list.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
templateId: {
|
||||
type: 'number',
|
||||
description: 'Template ID from n8n.io (required)'
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Custom workflow name (default: template name)'
|
||||
},
|
||||
autoUpgradeVersions: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Automatically upgrade node typeVersions to latest supported (default: true)'
|
||||
},
|
||||
validate: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Validate workflow before deployment (default: true)'
|
||||
},
|
||||
stripCredentials: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Remove credential references from nodes - user configures in n8n UI (default: true)'
|
||||
}
|
||||
},
|
||||
required: ['templateId']
|
||||
}
|
||||
}
|
||||
];
|
||||
514
src/mcp/tools.ts
514
src/mcp/tools.ts
@@ -26,51 +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: 'get_node_info',
|
||||
description: `Get full node documentation. Pass nodeType as string with prefix. Example: nodeType="nodes-base.webhook"`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
description: 'Full type: "nodes-base.{name}" or "nodes-langchain.{name}". Examples: nodes-base.httpRequest, nodes-base.webhook, nodes-base.slack',
|
||||
},
|
||||
},
|
||||
required: ['nodeType'],
|
||||
},
|
||||
},
|
||||
{
|
||||
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.`,
|
||||
@@ -102,93 +57,61 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
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"`,
|
||||
name: 'get_node',
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeType: {
|
||||
type: 'string',
|
||||
description: 'Full type with prefix: "nodes-base.slack"',
|
||||
description: 'Full node type: "nodes-base.httpRequest" or "nodes-langchain.agent"',
|
||||
},
|
||||
},
|
||||
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_essentials',
|
||||
description: `Get node essential info with optional real-world examples from templates. Pass nodeType as string with prefix. Example: nodeType="nodes-base.slack". Use includeExamples=true to get top 3 template configs.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nodeType: {
|
||||
detail: {
|
||||
type: 'string',
|
||||
description: 'Full type: "nodes-base.httpRequest"',
|
||||
enum: ['minimal', 'standard', 'full'],
|
||||
default: 'standard',
|
||||
description: 'Information detail level. standard=essential properties (recommended), full=everything',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
enum: ['info', 'docs', 'search_properties', 'versions', 'compare', 'breaking', 'migrations'],
|
||||
default: 'info',
|
||||
description: 'Operation mode. info=node schema, docs=readable markdown documentation, search_properties=find specific properties, versions/compare/breaking/migrations=version info',
|
||||
},
|
||||
includeTypeInfo: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Include type structure metadata (type category, JS type, validation rules). Only applies to mode=info. Adds ~80-120 tokens per property.',
|
||||
},
|
||||
includeExamples: {
|
||||
type: 'boolean',
|
||||
description: 'Include top 3 real-world configuration examples from popular templates (default: false)',
|
||||
default: false,
|
||||
description: 'Include real-world configuration examples from templates. Only applies to mode=info with detail=standard. Adds ~200-400 tokens per example.',
|
||||
},
|
||||
fromVersion: {
|
||||
type: 'string',
|
||||
description: 'Source version for compare/breaking/migrations modes (e.g., "1.0")',
|
||||
},
|
||||
toVersion: {
|
||||
type: 'string',
|
||||
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'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search_node_properties',
|
||||
description: `Find specific properties in a node (auth, headers, body, etc). Returns paths and descriptions.`,
|
||||
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"}`,
|
||||
name: 'validate_node',
|
||||
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: {
|
||||
@@ -200,10 +123,16 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
type: 'object',
|
||||
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: {
|
||||
type: 'string',
|
||||
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',
|
||||
},
|
||||
},
|
||||
@@ -242,6 +171,11 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
}
|
||||
},
|
||||
suggestions: { type: 'array', items: { type: 'string' } },
|
||||
missingRequiredFields: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Only present in mode=minimal'
|
||||
},
|
||||
summary: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -252,132 +186,7 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['nodeType', 'displayName', 'valid', 'errors', 'warnings', 'suggestions', 'summary']
|
||||
},
|
||||
},
|
||||
{
|
||||
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'],
|
||||
required: ['nodeType', 'displayName', 'valid']
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -402,13 +211,20 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
},
|
||||
{
|
||||
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: {
|
||||
type: 'object',
|
||||
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: {
|
||||
type: 'string',
|
||||
description: 'Search keyword as string. Example: "chatbot"',
|
||||
description: 'For searchMode=keyword: search keyword (e.g., "chatbot")',
|
||||
},
|
||||
fields: {
|
||||
type: 'array',
|
||||
@@ -416,36 +232,20 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
type: 'string',
|
||||
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: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results. Default 20.',
|
||||
default: 20,
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
// For searchMode='by_nodes'
|
||||
nodeTypes: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'For searchMode=by_nodes: array of node types (e.g., ["n8n-nodes-base.httpRequest", "n8n-nodes-base.slack"])',
|
||||
},
|
||||
offset: {
|
||||
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: {
|
||||
// For searchMode='by_task'
|
||||
task: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'ai_automation',
|
||||
'data_sync',
|
||||
'data_sync',
|
||||
'webhook_processing',
|
||||
'email_automation',
|
||||
'slack_integration',
|
||||
@@ -455,60 +255,39 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
'api_integration',
|
||||
'database_operations'
|
||||
],
|
||||
description: 'The type of task to get templates for',
|
||||
description: 'For searchMode=by_task: the type of task',
|
||||
},
|
||||
limit: {
|
||||
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: {
|
||||
// For searchMode='by_metadata'
|
||||
category: {
|
||||
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: {
|
||||
type: 'string',
|
||||
enum: ['simple', 'medium', 'complex'],
|
||||
description: 'Filter by complexity level',
|
||||
description: 'For searchMode=by_metadata: filter by complexity level',
|
||||
},
|
||||
maxSetupMinutes: {
|
||||
type: 'number',
|
||||
description: 'Maximum setup time in minutes',
|
||||
description: 'For searchMode=by_metadata: maximum setup time in minutes',
|
||||
minimum: 5,
|
||||
maximum: 480,
|
||||
},
|
||||
minSetupMinutes: {
|
||||
type: 'number',
|
||||
description: 'Minimum setup time in minutes',
|
||||
description: 'For searchMode=by_metadata: minimum setup time in minutes',
|
||||
minimum: 5,
|
||||
maximum: 480,
|
||||
},
|
||||
requiredService: {
|
||||
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: {
|
||||
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: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results. Default 20.',
|
||||
@@ -523,7 +302,6 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -611,143 +389,43 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
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:
|
||||
*
|
||||
*
|
||||
* 1. RECOMMENDED WORKFLOW:
|
||||
* - Start: search_nodes → get_node_essentials → get_node_for_task → validate_node_operation
|
||||
* - Discovery: list_nodes({category:"trigger"}) for browsing categories
|
||||
* - Quick Config: get_node_essentials("nodes-base.httpRequest") - only essential properties
|
||||
* - Full Details: get_node_info only when essentials aren't enough
|
||||
* - Validation: Use validate_node_operation for complex nodes (Slack, Google Sheets, etc.)
|
||||
*
|
||||
* - Start: search_nodes → get_node → validate_node
|
||||
* - Discovery: search_nodes({query:"trigger"}) for finding nodes
|
||||
* - Quick Config: get_node("nodes-base.httpRequest", {detail:"standard"}) - only essential properties
|
||||
* - Documentation: get_node("nodes-base.httpRequest", {mode:"docs"}) - readable markdown docs
|
||||
* - 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:
|
||||
* Triggers: webhook, schedule, emailReadImap, slackTrigger
|
||||
* Core: httpRequest, code, set, if, merge, splitInBatches
|
||||
* Integrations: slack, gmail, googleSheets, postgres, mongodb
|
||||
* AI: agent, openAi, chainLlm, documentLoader
|
||||
*
|
||||
*
|
||||
* 3. SEARCH TIPS:
|
||||
* - search_nodes returns ANY word match (OR logic)
|
||||
* - 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:
|
||||
* - search_templates("slack") searches template names/descriptions, NOT node types!
|
||||
* - To find templates using Slack node: list_node_templates(["n8n-nodes-base.slack"])
|
||||
* - For task-based templates: get_templates_for_task("slack_integration")
|
||||
* - 399 templates available from the last year
|
||||
*
|
||||
* - To find templates using Slack node: search_templates({searchMode:"by_nodes", nodeTypes:["n8n-nodes-base.slack"]})
|
||||
* - For task-based templates: search_templates({searchMode:"by_task", task:"slack_integration"})
|
||||
*
|
||||
* 5. KNOWN ISSUES:
|
||||
* - Some nodes have duplicate properties with different conditions
|
||||
* - Package names: use 'n8n-nodes-base' not '@n8n/n8n-nodes-base'
|
||||
* - Check showWhen/hideWhen to identify the right property variant
|
||||
*
|
||||
*
|
||||
* 6. PERFORMANCE:
|
||||
* - get_node_essentials: Fast (<5KB)
|
||||
* - get_node_info: Slow (100KB+) - use sparingly
|
||||
* - search_nodes/list_nodes: Fast, cached
|
||||
* - get_node (detail=standard): Fast (<5KB)
|
||||
* - get_node (detail=full): Slow (100KB+) - use sparingly
|
||||
* - search_nodes: Fast, cached
|
||||
*/
|
||||
@@ -62,6 +62,7 @@ export const workflowSettingsSchema = z.object({
|
||||
executionTimeout: z.number().optional(),
|
||||
errorWorkflow: z.string().optional(),
|
||||
callerPolicy: z.enum(['any', 'workflowsFromSameOwner', 'workflowsFromAList']).optional(),
|
||||
availableInMCP: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// Default settings for workflow creation
|
||||
@@ -181,7 +182,9 @@ export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> {
|
||||
'executionTimeout',
|
||||
'errorWorkflow',
|
||||
'timezone',
|
||||
'executionOrder'
|
||||
'executionOrder',
|
||||
'callerPolicy',
|
||||
'availableInMCP',
|
||||
];
|
||||
|
||||
if (cleanedWorkflow.settings && typeof cleanedWorkflow.settings === 'object') {
|
||||
|
||||
@@ -383,14 +383,11 @@ export class WorkflowValidator {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Normalize node type FIRST to ensure consistent lookup
|
||||
// Normalize node type for database lookup (DO NOT mutate the original workflow)
|
||||
// The normalizer converts to short form (nodes-base.*) for database queries,
|
||||
// but n8n API requires full form (n8n-nodes-base.*). Never modify the input workflow.
|
||||
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(node.type);
|
||||
|
||||
// Update node type in place if it was normalized
|
||||
if (normalizedType !== node.type) {
|
||||
node.type = normalizedType;
|
||||
}
|
||||
|
||||
// Get node definition using normalized type (needed for typeVersion validation)
|
||||
const nodeInfo = this.nodeRepository.getNode(normalizedType);
|
||||
|
||||
@@ -684,7 +681,12 @@ export class WorkflowValidator {
|
||||
}
|
||||
|
||||
// Special validation for SplitInBatches node
|
||||
if (sourceNode && sourceNode.type === 'nodes-base.splitInBatches') {
|
||||
// Check both full form (n8n-nodes-base.*) and short form (nodes-base.*)
|
||||
const isSplitInBatches = sourceNode && (
|
||||
sourceNode.type === 'n8n-nodes-base.splitInBatches' ||
|
||||
sourceNode.type === 'nodes-base.splitInBatches'
|
||||
);
|
||||
if (isSplitInBatches) {
|
||||
this.validateSplitInBatchesConnection(
|
||||
sourceNode,
|
||||
outputIndex,
|
||||
@@ -696,8 +698,8 @@ export class WorkflowValidator {
|
||||
|
||||
// Check for self-referencing connections
|
||||
if (connection.node === sourceName) {
|
||||
// This is only a warning for non-loop nodes
|
||||
if (sourceNode && sourceNode.type !== 'nodes-base.splitInBatches') {
|
||||
// This is only a warning for non-loop nodes (not SplitInBatches)
|
||||
if (sourceNode && !isSplitInBatches) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
message: `Node "${sourceName}" has a self-referencing connection. This can cause infinite loops.`
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Export n8n node type definitions and utilities
|
||||
export * from './node-types';
|
||||
export * from './type-structures';
|
||||
export * from './instance-context';
|
||||
export * from './session-state';
|
||||
|
||||
export interface MCPServerConfig {
|
||||
port: number;
|
||||
|
||||
92
src/types/session-state.ts
Normal file
92
src/types/session-state.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Session persistence types for multi-tenant deployments
|
||||
*
|
||||
* These types support exporting and restoring MCP session state across
|
||||
* container restarts, enabling seamless session persistence in production.
|
||||
*/
|
||||
|
||||
import { InstanceContext } from './instance-context.js';
|
||||
|
||||
/**
|
||||
* Serializable session state for persistence across restarts
|
||||
*
|
||||
* This interface represents the minimal state needed to restore an MCP session
|
||||
* after a container restart. Only the session metadata and instance context are
|
||||
* persisted - transport and server objects are recreated on the first request.
|
||||
*
|
||||
* @example
|
||||
* // Export sessions before shutdown
|
||||
* const sessions = server.exportSessionState();
|
||||
* await saveToEncryptedStorage(sessions);
|
||||
*
|
||||
* @example
|
||||
* // Restore sessions on startup
|
||||
* const sessions = await loadFromEncryptedStorage();
|
||||
* const count = server.restoreSessionState(sessions);
|
||||
* console.log(`Restored ${count} sessions`);
|
||||
*/
|
||||
export interface SessionState {
|
||||
/**
|
||||
* Unique session identifier
|
||||
* Format: UUID v4 or custom format from MCP proxy
|
||||
*/
|
||||
sessionId: string;
|
||||
|
||||
/**
|
||||
* Session timing metadata for expiration tracking
|
||||
*/
|
||||
metadata: {
|
||||
/**
|
||||
* When the session was created (ISO 8601 timestamp)
|
||||
* Used to track total session age
|
||||
*/
|
||||
createdAt: string;
|
||||
|
||||
/**
|
||||
* When the session was last accessed (ISO 8601 timestamp)
|
||||
* Used to determine if session has expired based on timeout
|
||||
*/
|
||||
lastAccess: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* n8n instance context (credentials and configuration)
|
||||
*
|
||||
* Contains the n8n API credentials and instance-specific settings.
|
||||
* This is the critical data needed to reconnect to the correct n8n instance.
|
||||
*
|
||||
* Note: API keys are stored in plaintext. The downstream application
|
||||
* MUST encrypt this data before persisting to disk.
|
||||
*/
|
||||
context: {
|
||||
/**
|
||||
* n8n instance API URL
|
||||
* Example: "https://n8n.example.com"
|
||||
*/
|
||||
n8nApiUrl: string;
|
||||
|
||||
/**
|
||||
* n8n instance API key (plaintext - encrypt before storage!)
|
||||
* Example: "n8n_api_1234567890abcdef"
|
||||
*/
|
||||
n8nApiKey: string;
|
||||
|
||||
/**
|
||||
* Instance identifier (optional)
|
||||
* Custom identifier for tracking which n8n instance this session belongs to
|
||||
*/
|
||||
instanceId?: string;
|
||||
|
||||
/**
|
||||
* Session-specific ID (optional)
|
||||
* May differ from top-level sessionId in some proxy configurations
|
||||
*/
|
||||
sessionId?: string;
|
||||
|
||||
/**
|
||||
* Additional metadata (optional)
|
||||
* Extensible field for custom application data
|
||||
*/
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
}
|
||||
@@ -4,72 +4,71 @@ import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
|
||||
describe('Basic MCP Connection', () => {
|
||||
it('should initialize MCP server', async () => {
|
||||
const server = new N8NDocumentationMCPServer();
|
||||
|
||||
// Test executeTool directly - it returns raw data
|
||||
const result = await server.executeTool('get_database_statistics', {});
|
||||
|
||||
// Test executeTool directly - tools_documentation returns a string
|
||||
const result = await server.executeTool('tools_documentation', {});
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('object');
|
||||
expect(result.totalNodes).toBeDefined();
|
||||
expect(result.statistics).toBeDefined();
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toContain('n8n MCP');
|
||||
|
||||
await server.shutdown();
|
||||
});
|
||||
|
||||
it('should execute list_nodes tool', async () => {
|
||||
|
||||
it('should execute search_nodes tool', async () => {
|
||||
const server = new N8NDocumentationMCPServer();
|
||||
|
||||
// First check if we have any nodes in the database
|
||||
const stats = await server.executeTool('get_database_statistics', {});
|
||||
const hasNodes = stats.totalNodes > 0;
|
||||
|
||||
const result = await server.executeTool('list_nodes', { limit: 5 });
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('object');
|
||||
expect(result.nodes).toBeDefined();
|
||||
expect(Array.isArray(result.nodes)).toBe(true);
|
||||
|
||||
if (hasNodes) {
|
||||
// If database has nodes, we should get up to 5
|
||||
expect(result.nodes.length).toBeLessThanOrEqual(5);
|
||||
expect(result.nodes.length).toBeGreaterThan(0);
|
||||
expect(result.nodes[0]).toHaveProperty('nodeType');
|
||||
expect(result.nodes[0]).toHaveProperty('displayName');
|
||||
} else {
|
||||
// In test environment with empty database, we expect empty results
|
||||
expect(result.nodes).toHaveLength(0);
|
||||
|
||||
try {
|
||||
// Search for a common node to verify database has content
|
||||
const result = await server.executeTool('search_nodes', { query: 'http', limit: 5 });
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('object');
|
||||
expect(result.results).toBeDefined();
|
||||
expect(Array.isArray(result.results)).toBe(true);
|
||||
|
||||
if (result.totalCount > 0) {
|
||||
// If database has nodes, we should get results
|
||||
expect(result.results.length).toBeLessThanOrEqual(5);
|
||||
expect(result.results.length).toBeGreaterThan(0);
|
||||
expect(result.results[0]).toHaveProperty('nodeType');
|
||||
expect(result.results[0]).toHaveProperty('displayName');
|
||||
}
|
||||
} catch (error: any) {
|
||||
// In test environment with empty database, expect appropriate error
|
||||
expect(error.message).toContain('Database is empty');
|
||||
}
|
||||
|
||||
|
||||
await server.shutdown();
|
||||
});
|
||||
|
||||
it('should search nodes', async () => {
|
||||
|
||||
it('should search nodes by keyword', async () => {
|
||||
const server = new N8NDocumentationMCPServer();
|
||||
|
||||
// First check if we have any nodes in the database
|
||||
const stats = await server.executeTool('get_database_statistics', {});
|
||||
const hasNodes = stats.totalNodes > 0;
|
||||
|
||||
const result = await server.executeTool('search_nodes', { query: 'webhook' });
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('object');
|
||||
expect(result.results).toBeDefined();
|
||||
expect(Array.isArray(result.results)).toBe(true);
|
||||
|
||||
// Only expect results if the database has nodes
|
||||
if (hasNodes) {
|
||||
expect(result.results.length).toBeGreaterThan(0);
|
||||
expect(result.totalCount).toBeGreaterThan(0);
|
||||
|
||||
// Should find webhook node
|
||||
const webhookNode = result.results.find((n: any) => n.nodeType === 'nodes-base.webhook');
|
||||
expect(webhookNode).toBeDefined();
|
||||
expect(webhookNode.displayName).toContain('Webhook');
|
||||
} else {
|
||||
// In test environment with empty database, we expect empty results
|
||||
expect(result.results).toHaveLength(0);
|
||||
expect(result.totalCount).toBe(0);
|
||||
|
||||
try {
|
||||
// Search to check if database has nodes
|
||||
const searchResult = await server.executeTool('search_nodes', { query: 'set', limit: 1 });
|
||||
const hasNodes = searchResult.totalCount > 0;
|
||||
|
||||
const result = await server.executeTool('search_nodes', { query: 'webhook' });
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('object');
|
||||
expect(result.results).toBeDefined();
|
||||
expect(Array.isArray(result.results)).toBe(true);
|
||||
|
||||
// Only expect results if the database has nodes
|
||||
if (hasNodes) {
|
||||
expect(result.results.length).toBeGreaterThan(0);
|
||||
expect(result.totalCount).toBeGreaterThan(0);
|
||||
|
||||
// Should find webhook node
|
||||
const webhookNode = result.results.find((n: any) => n.nodeType === 'nodes-base.webhook');
|
||||
expect(webhookNode).toBeDefined();
|
||||
expect(webhookNode.displayName).toContain('Webhook');
|
||||
}
|
||||
} catch (error: any) {
|
||||
// In test environment with empty database, expect appropriate error
|
||||
expect(error.message).toContain('Database is empty');
|
||||
}
|
||||
|
||||
|
||||
await server.shutdown();
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,7 @@ describe('MCP Error Handling', () => {
|
||||
it('should handle invalid params', async () => {
|
||||
try {
|
||||
// Missing required parameter
|
||||
await client.callTool({ name: 'get_node_info', arguments: {} });
|
||||
await client.callTool({ name: 'get_node', arguments: {} });
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeDefined();
|
||||
@@ -71,7 +71,7 @@ describe('MCP Error Handling', () => {
|
||||
it('should handle internal errors gracefully', async () => {
|
||||
try {
|
||||
// Invalid node type format should cause internal processing error
|
||||
await client.callTool({ name: 'get_node_info', arguments: {
|
||||
await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'completely-invalid-format-$$$$'
|
||||
} });
|
||||
expect.fail('Should have thrown an error');
|
||||
@@ -84,16 +84,16 @@ describe('MCP Error Handling', () => {
|
||||
|
||||
describe('Tool-Specific Errors', () => {
|
||||
describe('Node Discovery Errors', () => {
|
||||
it('should handle invalid category filter', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
category: 'invalid_category'
|
||||
it('should handle search with no matching results', async () => {
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: 'xyznonexistentnode123'
|
||||
} });
|
||||
|
||||
// Should return empty array, not error
|
||||
const result = JSON.parse((response as any).content[0].text);
|
||||
expect(result).toHaveProperty('nodes');
|
||||
expect(Array.isArray(result.nodes)).toBe(true);
|
||||
expect(result.nodes).toHaveLength(0);
|
||||
expect(result).toHaveProperty('results');
|
||||
expect(Array.isArray(result.results)).toBe(true);
|
||||
expect(result.results).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle invalid search mode', async () => {
|
||||
@@ -123,7 +123,7 @@ describe('MCP Error Handling', () => {
|
||||
|
||||
it('should handle non-existent node types', async () => {
|
||||
try {
|
||||
await client.callTool({ name: 'get_node_info', arguments: {
|
||||
await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'nodes-base.thisDoesNotExist'
|
||||
} });
|
||||
expect.fail('Should have thrown an error');
|
||||
@@ -135,11 +135,13 @@ describe('MCP Error Handling', () => {
|
||||
});
|
||||
|
||||
describe('Validation Errors', () => {
|
||||
// v2.26.0: validate_node_operation consolidated into validate_node
|
||||
it('should handle invalid validation profile', async () => {
|
||||
try {
|
||||
await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: { method: 'GET', url: 'https://api.example.com' },
|
||||
mode: 'full',
|
||||
profile: 'invalid_profile' as any
|
||||
} });
|
||||
expect.fail('Should have thrown an error');
|
||||
@@ -228,15 +230,17 @@ describe('MCP Error Handling', () => {
|
||||
describe('Large Payload Handling', () => {
|
||||
it('should handle large node info requests', async () => {
|
||||
// HTTP Request node has extensive properties
|
||||
const response = await client.callTool({ name: 'get_node_info', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest'
|
||||
const response = await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
detail: 'full'
|
||||
} });
|
||||
|
||||
expect((response as any).content[0].text.length).toBeGreaterThan(10000);
|
||||
|
||||
|
||||
// Should be valid JSON
|
||||
const nodeInfo = JSON.parse((response as any).content[0].text);
|
||||
expect(nodeInfo).toHaveProperty('properties');
|
||||
expect(nodeInfo).toHaveProperty('nodeType');
|
||||
expect(nodeInfo).toHaveProperty('displayName');
|
||||
});
|
||||
|
||||
it('should handle large workflow validation', async () => {
|
||||
@@ -277,9 +281,9 @@ describe('MCP Error Handling', () => {
|
||||
|
||||
for (let i = 0; i < requestCount; i++) {
|
||||
promises.push(
|
||||
client.callTool({ name: 'list_nodes', arguments: {
|
||||
limit: 1,
|
||||
category: i % 2 === 0 ? 'trigger' : 'transform'
|
||||
client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: i % 2 === 0 ? 'webhook' : 'http',
|
||||
limit: 1
|
||||
} })
|
||||
);
|
||||
}
|
||||
@@ -290,12 +294,14 @@ describe('MCP Error Handling', () => {
|
||||
});
|
||||
|
||||
describe('Invalid JSON Handling', () => {
|
||||
// v2.26.0: validate_node_operation consolidated into validate_node
|
||||
it('should handle invalid JSON in tool parameters', async () => {
|
||||
try {
|
||||
// Config should be an object, not a string
|
||||
await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: 'invalid json string' as any
|
||||
config: 'invalid json string' as any,
|
||||
mode: 'full'
|
||||
} });
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
@@ -318,13 +324,13 @@ describe('MCP Error Handling', () => {
|
||||
describe('Timeout Scenarios', () => {
|
||||
it('should handle rapid sequential requests', async () => {
|
||||
const start = Date.now();
|
||||
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
}
|
||||
|
||||
const duration = Date.now() - start;
|
||||
|
||||
|
||||
// Should complete reasonably quickly (under 5 seconds)
|
||||
expect(duration).toBeLessThan(5000);
|
||||
});
|
||||
@@ -355,7 +361,7 @@ describe('MCP Error Handling', () => {
|
||||
|
||||
for (const nodeType of largeNodes) {
|
||||
promises.push(
|
||||
client.callTool({ name: 'get_node_info', arguments: { nodeType } })
|
||||
client.callTool({ name: 'get_node', arguments: { nodeType } })
|
||||
.catch(() => null) // Some might not exist
|
||||
);
|
||||
}
|
||||
@@ -400,7 +406,7 @@ describe('MCP Error Handling', () => {
|
||||
it('should continue working after errors', async () => {
|
||||
// Cause an error
|
||||
try {
|
||||
await client.callTool({ name: 'get_node_info', arguments: {
|
||||
await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'invalid'
|
||||
} });
|
||||
} catch (error) {
|
||||
@@ -408,25 +414,25 @@ describe('MCP Error Handling', () => {
|
||||
}
|
||||
|
||||
// Should still work
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: { limit: 1 } });
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: { query: 'webhook', limit: 1 } });
|
||||
expect(response).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle mixed success and failure', async () => {
|
||||
const promises = [
|
||||
client.callTool({ name: 'list_nodes', arguments: { limit: 5 } }),
|
||||
client.callTool({ name: 'get_node_info', arguments: { nodeType: 'invalid' } }).catch(e => ({ error: e })),
|
||||
client.callTool({ name: 'get_database_statistics', arguments: {} }),
|
||||
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: 'tools_documentation', arguments: {} }),
|
||||
client.callTool({ name: 'search_nodes', arguments: { query: '' } }).catch(e => ({ error: e })),
|
||||
client.callTool({ name: 'list_ai_tools', arguments: {} })
|
||||
client.callTool({ name: 'get_node', arguments: { nodeType: 'nodes-base.httpRequest' } })
|
||||
];
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
|
||||
// Some should succeed, some should fail
|
||||
const successes = results.filter(r => !('error' in r));
|
||||
const failures = results.filter(r => 'error' in r);
|
||||
|
||||
|
||||
expect(successes.length).toBeGreaterThan(0);
|
||||
expect(failures.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -434,14 +440,14 @@ describe('MCP Error Handling', () => {
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty responses gracefully', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
category: 'nonexistent_category'
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: 'xyznonexistentnode12345'
|
||||
} });
|
||||
|
||||
const result = JSON.parse((response as any).content[0].text);
|
||||
expect(result).toHaveProperty('nodes');
|
||||
expect(Array.isArray(result.nodes)).toBe(true);
|
||||
expect(result.nodes).toHaveLength(0);
|
||||
expect(result).toHaveProperty('results');
|
||||
expect(Array.isArray(result.results)).toBe(true);
|
||||
expect(result.results).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle special characters in parameters', async () => {
|
||||
@@ -467,14 +473,15 @@ describe('MCP Error Handling', () => {
|
||||
|
||||
it('should handle null and undefined gracefully', async () => {
|
||||
// Most tools should handle missing optional params
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: 'webhook',
|
||||
limit: undefined as any,
|
||||
category: null as any
|
||||
mode: null as any
|
||||
} });
|
||||
|
||||
const result = JSON.parse((response as any).content[0].text);
|
||||
expect(result).toHaveProperty('nodes');
|
||||
expect(Array.isArray(result.nodes)).toBe(true);
|
||||
expect(result).toHaveProperty('results');
|
||||
expect(Array.isArray(result.results)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -482,7 +489,7 @@ describe('MCP Error Handling', () => {
|
||||
it('should provide helpful error messages', async () => {
|
||||
try {
|
||||
// Use a truly invalid node type
|
||||
await client.callTool({ name: 'get_node_info', arguments: {
|
||||
await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'invalid-node-type-that-does-not-exist'
|
||||
} });
|
||||
expect.fail('Should have thrown an error');
|
||||
@@ -506,13 +513,15 @@ describe('MCP Error Handling', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// v2.26.0: validate_node_operation consolidated into validate_node
|
||||
it('should provide context for validation errors', async () => {
|
||||
const response = await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
const response = await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: {
|
||||
// Missing required fields
|
||||
method: 'INVALID_METHOD'
|
||||
}
|
||||
},
|
||||
mode: 'full'
|
||||
} });
|
||||
|
||||
const validation = JSON.parse((response as any).content[0].text);
|
||||
|
||||
@@ -23,13 +23,13 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// Verify database is populated by checking statistics
|
||||
const statsResponse = await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
if ((statsResponse as any).content && (statsResponse as any).content[0]) {
|
||||
const stats = JSON.parse((statsResponse as any).content[0].text);
|
||||
// Verify database is populated by searching for a common node
|
||||
const searchResponse = await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 1 } });
|
||||
if ((searchResponse as any).content && (searchResponse as any).content[0]) {
|
||||
const searchResult = JSON.parse((searchResponse as any).content[0].text);
|
||||
// Ensure database has nodes for testing
|
||||
if (!stats.totalNodes || stats.totalNodes === 0) {
|
||||
console.error('Database stats:', stats);
|
||||
if (!searchResult.totalCount || searchResult.totalCount === 0) {
|
||||
console.error('Search result:', searchResult);
|
||||
throw new Error('Test database not properly populated');
|
||||
}
|
||||
}
|
||||
@@ -46,13 +46,13 @@ describe('MCP Performance Tests', () => {
|
||||
const start = performance.now();
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
}
|
||||
|
||||
const duration = performance.now() - start;
|
||||
const avgTime = duration / iterations;
|
||||
|
||||
console.log(`Average response time for get_database_statistics: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(`Average response time for tools_documentation: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
|
||||
|
||||
// Environment-aware threshold (relaxed +20% for type safety overhead)
|
||||
@@ -60,20 +60,20 @@ describe('MCP Performance Tests', () => {
|
||||
expect(avgTime).toBeLessThan(threshold);
|
||||
});
|
||||
|
||||
it('should handle list operations efficiently', async () => {
|
||||
it('should handle search operations efficiently', async () => {
|
||||
const iterations = 50;
|
||||
const start = performance.now();
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await client.callTool({ name: 'list_nodes', arguments: { limit: 10 } });
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 10 } });
|
||||
}
|
||||
|
||||
const duration = performance.now() - start;
|
||||
const avgTime = duration / iterations;
|
||||
|
||||
console.log(`Average response time for list_nodes: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(`Average response time for search_nodes: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
|
||||
|
||||
|
||||
// Environment-aware threshold
|
||||
const threshold = process.env.CI ? 40 : 20;
|
||||
expect(avgTime).toBeLessThan(threshold);
|
||||
@@ -114,13 +114,13 @@ describe('MCP Performance Tests', () => {
|
||||
const start = performance.now();
|
||||
|
||||
for (const nodeType of nodeTypes) {
|
||||
await client.callTool({ name: 'get_node_info', arguments: { nodeType } });
|
||||
await client.callTool({ name: 'get_node', arguments: { nodeType } });
|
||||
}
|
||||
|
||||
const duration = performance.now() - start;
|
||||
const avgTime = duration / nodeTypes.length;
|
||||
|
||||
console.log(`Average response time for get_node_info: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(`Average response time for get_node: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
|
||||
|
||||
// Environment-aware threshold (these are large responses)
|
||||
@@ -137,7 +137,7 @@ describe('MCP Performance Tests', () => {
|
||||
const promises = [];
|
||||
for (let i = 0; i < concurrentRequests; i++) {
|
||||
promises.push(
|
||||
client.callTool({ name: 'list_nodes', arguments: { limit: 5 } })
|
||||
client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 5 } })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
console.log(`Average time for ${concurrentRequests} concurrent requests: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
|
||||
|
||||
|
||||
// Concurrent requests should be more efficient than sequential
|
||||
const threshold = process.env.CI ? 25 : 10;
|
||||
expect(avgTime).toBeLessThan(threshold);
|
||||
@@ -156,11 +156,11 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
it('should handle mixed concurrent operations', async () => {
|
||||
const operations = [
|
||||
{ tool: 'list_nodes', params: { limit: 10 } },
|
||||
{ tool: 'search_nodes', params: { query: 'http' } },
|
||||
{ tool: 'get_database_statistics', params: {} },
|
||||
{ tool: 'list_ai_tools', params: {} },
|
||||
{ tool: 'list_tasks', params: {} }
|
||||
{ tool: 'search_nodes', params: { query: 'http', limit: 10 } },
|
||||
{ tool: 'search_nodes', params: { query: 'webhook' } },
|
||||
{ tool: 'tools_documentation', params: {} },
|
||||
{ tool: 'get_node', params: { nodeType: 'nodes-base.httpRequest' } },
|
||||
{ tool: 'get_node', params: { nodeType: 'nodes-base.webhook' } }
|
||||
];
|
||||
|
||||
const rounds = 10;
|
||||
@@ -186,34 +186,35 @@ describe('MCP Performance Tests', () => {
|
||||
});
|
||||
|
||||
describe('Large Data Performance', () => {
|
||||
it('should handle large node lists efficiently', async () => {
|
||||
it('should handle large search results efficiently', async () => {
|
||||
const start = performance.now();
|
||||
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
limit: 200 // Get many nodes
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: 'n8n', // Broad query to get many results
|
||||
limit: 200
|
||||
} });
|
||||
|
||||
const duration = performance.now() - start;
|
||||
|
||||
console.log(`Time to list 200 nodes: ${duration.toFixed(2)}ms`);
|
||||
|
||||
console.log(`Time to search 200 nodes: ${duration.toFixed(2)}ms`);
|
||||
|
||||
// Environment-aware threshold
|
||||
const threshold = process.env.CI ? 200 : 100;
|
||||
expect(duration).toBeLessThan(threshold);
|
||||
|
||||
// Check the response content
|
||||
expect(response).toBeDefined();
|
||||
|
||||
let nodes;
|
||||
|
||||
let results;
|
||||
if (response.content && Array.isArray(response.content) && response.content[0]) {
|
||||
// MCP standard response format
|
||||
expect(response.content[0].type).toBe('text');
|
||||
expect(response.content[0].text).toBeDefined();
|
||||
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(response.content[0].text);
|
||||
// list_nodes returns an object with nodes property
|
||||
nodes = parsed.nodes || parsed;
|
||||
// search_nodes returns an object with results property
|
||||
results = parsed.results || parsed;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse JSON:', e);
|
||||
console.error('Response text was:', response.content[0].text);
|
||||
@@ -221,18 +222,18 @@ describe('MCP Performance Tests', () => {
|
||||
}
|
||||
} else if (Array.isArray(response)) {
|
||||
// Direct array response
|
||||
nodes = response;
|
||||
} else if (response.nodes) {
|
||||
// Object with nodes property
|
||||
nodes = response.nodes;
|
||||
results = response;
|
||||
} else if (response.results) {
|
||||
// Object with results property
|
||||
results = response.results;
|
||||
} else {
|
||||
console.error('Unexpected response format:', response);
|
||||
throw new Error('Unexpected response format');
|
||||
}
|
||||
|
||||
expect(nodes).toBeDefined();
|
||||
expect(Array.isArray(nodes)).toBe(true);
|
||||
expect(nodes.length).toBeGreaterThan(100);
|
||||
|
||||
expect(results).toBeDefined();
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(results.length).toBeGreaterThan(50);
|
||||
});
|
||||
|
||||
it('should handle large workflow validation efficiently', async () => {
|
||||
@@ -301,10 +302,10 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
for (let i = 0; i < iterations; i += batchSize) {
|
||||
const promises = [];
|
||||
|
||||
|
||||
for (let j = 0; j < batchSize; j++) {
|
||||
promises.push(
|
||||
client.callTool({ name: 'get_database_statistics', arguments: {} })
|
||||
client.callTool({ name: 'tools_documentation', arguments: {} })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -330,9 +331,9 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
// Perform large operations
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await client.callTool({ name: 'list_nodes', arguments: { limit: 200 } });
|
||||
await client.callTool({ name: 'get_node_info', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest'
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'n8n', limit: 200 } });
|
||||
await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest'
|
||||
} });
|
||||
}
|
||||
|
||||
@@ -359,16 +360,16 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
for (const load of loadLevels) {
|
||||
const start = performance.now();
|
||||
|
||||
|
||||
const promises = [];
|
||||
for (let i = 0; i < load; i++) {
|
||||
promises.push(
|
||||
client.callTool({ name: 'list_nodes', arguments: { limit: 1 } })
|
||||
client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 1 } })
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
|
||||
const duration = performance.now() - start;
|
||||
const avgTime = duration / load;
|
||||
|
||||
@@ -384,10 +385,10 @@ describe('MCP Performance Tests', () => {
|
||||
// Average time should not increase dramatically with load
|
||||
const firstAvg = results[0].avgTime;
|
||||
const lastAvg = results[results.length - 1].avgTime;
|
||||
|
||||
|
||||
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
|
||||
console.log(`Performance scaling - First avg: ${firstAvg.toFixed(2)}ms, Last avg: ${lastAvg.toFixed(2)}ms`);
|
||||
|
||||
|
||||
// Environment-aware scaling factor
|
||||
const scalingFactor = process.env.CI ? 3 : 2;
|
||||
expect(lastAvg).toBeLessThan(firstAvg * scalingFactor);
|
||||
@@ -403,16 +404,16 @@ describe('MCP Performance Tests', () => {
|
||||
const operation = i % 4;
|
||||
switch (operation) {
|
||||
case 0:
|
||||
promises.push(client.callTool({ name: 'list_nodes', arguments: { limit: 5 } }));
|
||||
promises.push(client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 5 } }));
|
||||
break;
|
||||
case 1:
|
||||
promises.push(client.callTool({ name: 'search_nodes', arguments: { query: 'test' } }));
|
||||
break;
|
||||
case 2:
|
||||
promises.push(client.callTool({ name: 'get_database_statistics', arguments: {} }));
|
||||
promises.push(client.callTool({ name: 'tools_documentation', arguments: {} }));
|
||||
break;
|
||||
case 3:
|
||||
promises.push(client.callTool({ name: 'list_ai_tools', arguments: {} }));
|
||||
promises.push(client.callTool({ name: 'get_node', arguments: { nodeType: 'nodes-base.set' } }));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -431,10 +432,10 @@ describe('MCP Performance Tests', () => {
|
||||
});
|
||||
|
||||
describe('Critical Path Optimization', () => {
|
||||
it('should optimize tool listing performance', async () => {
|
||||
it('should optimize search performance', async () => {
|
||||
// Warm up with multiple calls to ensure everything is initialized
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await client.callTool({ name: 'list_nodes', arguments: { limit: 1 } });
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 1 } });
|
||||
}
|
||||
|
||||
const iterations = 100;
|
||||
@@ -442,32 +443,32 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const start = performance.now();
|
||||
await client.callTool({ name: 'list_nodes', arguments: { limit: 20 } });
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 20 } });
|
||||
times.push(performance.now() - start);
|
||||
}
|
||||
|
||||
// Remove outliers (first few runs might be slower)
|
||||
times.sort((a, b) => a - b);
|
||||
const trimmedTimes = times.slice(10, -10); // Remove top and bottom 10%
|
||||
|
||||
|
||||
const avgTime = trimmedTimes.reduce((a, b) => a + b, 0) / trimmedTimes.length;
|
||||
const minTime = Math.min(...trimmedTimes);
|
||||
const maxTime = Math.max(...trimmedTimes);
|
||||
|
||||
console.log(`list_nodes performance - Avg: ${avgTime.toFixed(2)}ms, Min: ${minTime.toFixed(2)}ms, Max: ${maxTime.toFixed(2)}ms`);
|
||||
console.log(`search_nodes performance - Avg: ${avgTime.toFixed(2)}ms, Min: ${minTime.toFixed(2)}ms, Max: ${maxTime.toFixed(2)}ms`);
|
||||
console.log(`Environment: ${process.env.CI ? 'CI' : 'Local'}`);
|
||||
|
||||
// Environment-aware thresholds
|
||||
const threshold = process.env.CI ? 25 : 10;
|
||||
expect(avgTime).toBeLessThan(threshold);
|
||||
|
||||
|
||||
// Max should not be too much higher than average (no outliers)
|
||||
// More lenient in CI due to resource contention
|
||||
const maxMultiplier = process.env.CI ? 5 : 3;
|
||||
expect(maxTime).toBeLessThan(avgTime * maxMultiplier);
|
||||
});
|
||||
|
||||
it('should optimize search performance', async () => {
|
||||
it('should handle varied search queries efficiently', async () => {
|
||||
// Warm up with multiple calls
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'test' } });
|
||||
@@ -487,7 +488,7 @@ describe('MCP Performance Tests', () => {
|
||||
// Remove outliers
|
||||
times.sort((a, b) => a - b);
|
||||
const trimmedTimes = times.slice(10, -10); // Remove top and bottom 10%
|
||||
|
||||
|
||||
const avgTime = trimmedTimes.reduce((a, b) => a + b, 0) / trimmedTimes.length;
|
||||
|
||||
console.log(`search_nodes average performance: ${avgTime.toFixed(2)}ms`);
|
||||
@@ -503,7 +504,7 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
// First call (cold)
|
||||
const coldStart = performance.now();
|
||||
await client.callTool({ name: 'get_node_info', arguments: { nodeType } });
|
||||
await client.callTool({ name: 'get_node', arguments: { nodeType } });
|
||||
const coldTime = performance.now() - coldStart;
|
||||
|
||||
// Give cache time to settle
|
||||
@@ -513,7 +514,7 @@ describe('MCP Performance Tests', () => {
|
||||
const warmTimes: number[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const start = performance.now();
|
||||
await client.callTool({ name: 'get_node_info', arguments: { nodeType } });
|
||||
await client.callTool({ name: 'get_node', arguments: { nodeType } });
|
||||
warmTimes.push(performance.now() - start);
|
||||
}
|
||||
|
||||
@@ -542,7 +543,7 @@ describe('MCP Performance Tests', () => {
|
||||
|
||||
while (performance.now() - start < duration) {
|
||||
try {
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
requestCount++;
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
@@ -559,7 +560,7 @@ describe('MCP Performance Tests', () => {
|
||||
// Relaxed to 75 RPS locally to account for parallel test execution overhead
|
||||
const rpsThreshold = process.env.CI ? 50 : 75;
|
||||
expect(requestsPerSecond).toBeGreaterThan(rpsThreshold);
|
||||
|
||||
|
||||
// Error rate should be very low
|
||||
expect(errorCount).toBe(0);
|
||||
});
|
||||
@@ -591,7 +592,7 @@ describe('MCP Performance Tests', () => {
|
||||
const recoveryTimes: number[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const start = performance.now();
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
recoveryTimes.push(performance.now() - start);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('MCP Protocol Compliance', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
expectedOrder.push(i);
|
||||
requests.push(
|
||||
client.callTool({ name: 'get_database_statistics', arguments: {} })
|
||||
client.callTool({ name: 'tools_documentation', arguments: {} })
|
||||
.then(() => i)
|
||||
);
|
||||
}
|
||||
@@ -125,14 +125,14 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
it('should handle missing params gracefully', async () => {
|
||||
// Most tools should work without params
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {} });
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: { query: 'webhook' } });
|
||||
expect(response).toBeDefined();
|
||||
});
|
||||
|
||||
it('should validate params schema', async () => {
|
||||
try {
|
||||
// Invalid nodeType format (missing prefix)
|
||||
const response = await client.callTool({ name: 'get_node_info', arguments: {
|
||||
const response = await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'httpRequest' // Should be 'nodes-base.httpRequest'
|
||||
} });
|
||||
// Check if the response indicates an error
|
||||
@@ -147,8 +147,8 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
describe('Content Types', () => {
|
||||
it('should handle text content in tool responses', async () => {
|
||||
const response = await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
|
||||
const response = await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
|
||||
expect((response as any).content).toHaveLength(1);
|
||||
expect((response as any).content[0]).toHaveProperty('type', 'text');
|
||||
expect((response as any).content[0]).toHaveProperty('text');
|
||||
@@ -157,7 +157,7 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
it('should handle large text responses', async () => {
|
||||
// Get a large node info response
|
||||
const response = await client.callTool({ name: 'get_node_info', arguments: {
|
||||
const response = await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest'
|
||||
} });
|
||||
|
||||
@@ -167,23 +167,24 @@ describe('MCP Protocol Compliance', () => {
|
||||
});
|
||||
|
||||
it('should handle JSON content properly', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: 'webhook',
|
||||
limit: 5
|
||||
} });
|
||||
|
||||
expect((response as any).content).toHaveLength(1);
|
||||
const content = JSON.parse((response as any).content[0].text);
|
||||
expect(content).toHaveProperty('nodes');
|
||||
expect(Array.isArray(content.nodes)).toBe(true);
|
||||
expect(content).toHaveProperty('results');
|
||||
expect(Array.isArray(content.results)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request/Response Correlation', () => {
|
||||
it('should correlate concurrent requests correctly', async () => {
|
||||
const requests = [
|
||||
client.callTool({ name: 'get_node_essentials', arguments: { nodeType: 'nodes-base.httpRequest' } }),
|
||||
client.callTool({ name: 'get_node_essentials', arguments: { nodeType: 'nodes-base.webhook' } }),
|
||||
client.callTool({ name: 'get_node_essentials', arguments: { nodeType: 'nodes-base.slack' } })
|
||||
client.callTool({ name: 'get_node', arguments: { nodeType: 'nodes-base.httpRequest' } }),
|
||||
client.callTool({ name: 'get_node', arguments: { nodeType: 'nodes-base.webhook' } }),
|
||||
client.callTool({ name: 'get_node', arguments: { nodeType: 'nodes-base.slack' } })
|
||||
];
|
||||
|
||||
const responses = await Promise.all(requests);
|
||||
@@ -197,10 +198,10 @@ describe('MCP Protocol Compliance', () => {
|
||||
const results: string[] = [];
|
||||
|
||||
// Start multiple requests with different delays
|
||||
const p1 = client.callTool({ name: 'get_database_statistics', arguments: {} })
|
||||
.then(() => { results.push('stats'); return 'stats'; });
|
||||
const p1 = client.callTool({ name: 'tools_documentation', arguments: {} })
|
||||
.then(() => { results.push('docs'); return 'docs'; });
|
||||
|
||||
const p2 = client.callTool({ name: 'list_nodes', arguments: { limit: 1 } })
|
||||
const p2 = client.callTool({ name: 'search_nodes', arguments: { query: 'webhook', limit: 1 } })
|
||||
.then(() => { results.push('nodes'); return 'nodes'; });
|
||||
|
||||
const p3 = client.callTool({ name: 'search_nodes', arguments: { query: 'http' } })
|
||||
@@ -216,13 +217,14 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
describe('Protocol Extensions', () => {
|
||||
it('should handle tool-specific extensions', async () => {
|
||||
// Test tool with complex params
|
||||
const response = await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
// Test tool with complex params (using consolidated validate_node from v2.26.0)
|
||||
const response = await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com'
|
||||
},
|
||||
mode: 'full',
|
||||
profile: 'runtime'
|
||||
} });
|
||||
|
||||
@@ -232,13 +234,13 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
it('should support optional parameters', async () => {
|
||||
// Call with minimal params
|
||||
const response1 = await client.callTool({ name: 'list_nodes', arguments: {} });
|
||||
|
||||
const response1 = await client.callTool({ name: 'search_nodes', arguments: { query: 'webhook' } });
|
||||
|
||||
// Call with all params
|
||||
const response2 = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
const response2 = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
query: 'webhook',
|
||||
limit: 10,
|
||||
category: 'trigger',
|
||||
package: 'n8n-nodes-base'
|
||||
mode: 'OR'
|
||||
} });
|
||||
|
||||
expect(response1).toBeDefined();
|
||||
@@ -255,7 +257,7 @@ describe('MCP Protocol Compliance', () => {
|
||||
await testClient.connect(clientTransport);
|
||||
|
||||
// Make a request
|
||||
const response = await testClient.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response = await testClient.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(response).toBeDefined();
|
||||
|
||||
// Close client
|
||||
@@ -263,7 +265,7 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
// Further requests should fail
|
||||
try {
|
||||
await testClient.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await testClient.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
@@ -286,7 +288,7 @@ describe('MCP Protocol Compliance', () => {
|
||||
const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
|
||||
await testClient.connect(clientTransport);
|
||||
|
||||
const response = await testClient.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response = await testClient.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(response).toBeDefined();
|
||||
|
||||
await testClient.close();
|
||||
|
||||
@@ -100,8 +100,8 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// Make some requests
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'list_nodes', arguments: { limit: 5 } });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 5 } });
|
||||
|
||||
// Clean termination
|
||||
await client.close();
|
||||
@@ -109,7 +109,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
|
||||
// Client should be closed
|
||||
try {
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect.fail('Should not be able to make requests after close');
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
@@ -133,7 +133,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// Make a request to ensure connection is active
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
|
||||
// Simulate abrupt disconnection by closing transport
|
||||
await clientTransport.close();
|
||||
@@ -141,7 +141,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
|
||||
// Further operations should fail
|
||||
try {
|
||||
await client.callTool({ name: 'list_nodes', arguments: {} });
|
||||
await client.callTool({ name: 'search_nodes', arguments: { query: 'http' } });
|
||||
expect.fail('Should not be able to make requests after transport close');
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
@@ -179,14 +179,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client1.connect(ct1);
|
||||
|
||||
// First session operations
|
||||
const response1 = await client1.callTool({ name: 'list_nodes', arguments: { limit: 3 } });
|
||||
const response1 = await client1.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 3 } });
|
||||
expect(response1).toBeDefined();
|
||||
expect((response1 as any).content).toBeDefined();
|
||||
expect((response1 as any).content[0]).toHaveProperty('type', 'text');
|
||||
const data1 = JSON.parse(((response1 as any).content[0] as any).text);
|
||||
// Handle both array response and object with nodes property
|
||||
const nodes1 = Array.isArray(data1) ? data1 : data1.nodes;
|
||||
expect(nodes1).toHaveLength(3);
|
||||
// Handle both array response and object with results property
|
||||
const results1 = Array.isArray(data1) ? data1 : data1.results;
|
||||
expect(results1.length).toBeLessThanOrEqual(3);
|
||||
|
||||
// Close first session completely
|
||||
await client1.close();
|
||||
@@ -204,14 +204,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client2.connect(ct2);
|
||||
|
||||
// Second session operations
|
||||
const response2 = await client2.callTool({ name: 'list_nodes', arguments: { limit: 5 } });
|
||||
const response2 = await client2.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 5 } });
|
||||
expect(response2).toBeDefined();
|
||||
expect((response2 as any).content).toBeDefined();
|
||||
expect((response2 as any).content[0]).toHaveProperty('type', 'text');
|
||||
const data2 = JSON.parse(((response2 as any).content[0] as any).text);
|
||||
// Handle both array response and object with nodes property
|
||||
const nodes2 = Array.isArray(data2) ? data2 : data2.nodes;
|
||||
expect(nodes2).toHaveLength(5);
|
||||
// Handle both array response and object with results property
|
||||
const results2 = Array.isArray(data2) ? data2 : data2.results;
|
||||
expect(results2.length).toBeLessThanOrEqual(5);
|
||||
|
||||
// Clean up
|
||||
await client2.close();
|
||||
@@ -228,9 +228,9 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
const client1 = new Client({ name: 'multi-seq-1', version: '1.0.0' }, {});
|
||||
await client1.connect(ct1);
|
||||
|
||||
const resp1 = await client1.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const resp1 = await client1.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(resp1).toBeDefined();
|
||||
|
||||
|
||||
await client1.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
@@ -239,8 +239,8 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await mcpServer.connectToTransport(st2);
|
||||
const client2 = new Client({ name: 'multi-seq-2', version: '1.0.0' }, {});
|
||||
await client2.connect(ct2);
|
||||
|
||||
const resp2 = await client2.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
|
||||
const resp2 = await client2.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(resp2).toBeDefined();
|
||||
|
||||
await client2.close();
|
||||
@@ -261,14 +261,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client1.connect(ct1);
|
||||
|
||||
// Make some requests
|
||||
await client1.callTool({ name: 'list_nodes', arguments: { limit: 10 } });
|
||||
await client1.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 10 } });
|
||||
await client1.close();
|
||||
await mcpServer1.close();
|
||||
|
||||
// Second session - should be fresh
|
||||
const mcpServer2 = new TestableN8NMCPServer();
|
||||
await mcpServer2.initialize();
|
||||
|
||||
|
||||
const [st2, ct2] = InMemoryTransport.createLinkedPair();
|
||||
await mcpServer2.connectToTransport(st2);
|
||||
|
||||
@@ -276,7 +276,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client2.connect(ct2);
|
||||
|
||||
// Should work normally
|
||||
const response = await client2.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response = await client2.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(response).toBeDefined();
|
||||
|
||||
await client2.close();
|
||||
@@ -299,7 +299,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// Quick operation
|
||||
const response = await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response = await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(response).toBeDefined();
|
||||
|
||||
// Explicit cleanup for each iteration
|
||||
@@ -392,7 +392,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
|
||||
// Light operation
|
||||
if (i % 10 === 0) {
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
}
|
||||
|
||||
// Explicit cleanup
|
||||
@@ -420,8 +420,8 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < requestCount; i++) {
|
||||
const toolName = i % 2 === 0 ? 'list_nodes' : 'get_database_statistics';
|
||||
const params = toolName === 'list_nodes' ? { limit: 1 } : {};
|
||||
const toolName = i % 2 === 0 ? 'search_nodes' : 'tools_documentation';
|
||||
const params = toolName === 'search_nodes' ? { query: 'http', limit: 1 } : {};
|
||||
promises.push(client.callTool({ name: toolName as any, arguments: params }));
|
||||
}
|
||||
|
||||
@@ -451,7 +451,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
|
||||
// Make an error-inducing request
|
||||
try {
|
||||
await client.callTool({ name: 'get_node_info', arguments: {
|
||||
await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'invalid-node-type'
|
||||
} });
|
||||
expect.fail('Should have thrown an error');
|
||||
@@ -460,9 +460,9 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
}
|
||||
|
||||
// Session should still be active
|
||||
const response = await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response = await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(response).toBeDefined();
|
||||
|
||||
|
||||
await client.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
|
||||
await mcpServer.close();
|
||||
@@ -485,8 +485,8 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
// Multiple error-inducing requests
|
||||
// Note: get_node_for_task was removed in v2.15.0
|
||||
const errorPromises = [
|
||||
client.callTool({ name: 'get_node_info', arguments: { nodeType: 'invalid1' } }).catch(e => e),
|
||||
client.callTool({ name: 'get_node_info', arguments: { nodeType: 'invalid2' } }).catch(e => e),
|
||||
client.callTool({ name: 'get_node', arguments: { nodeType: 'invalid1' } }).catch(e => e),
|
||||
client.callTool({ name: 'get_node', arguments: { nodeType: 'invalid2' } }).catch(e => e),
|
||||
client.callTool({ name: 'search_nodes', arguments: { query: '' } }).catch(e => e) // Empty query should error
|
||||
];
|
||||
|
||||
@@ -496,9 +496,9 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
});
|
||||
|
||||
// Session should still work
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: { limit: 1 } });
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: { query: 'http', limit: 1 } });
|
||||
expect(response).toBeDefined();
|
||||
|
||||
|
||||
await client.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
|
||||
await mcpServer.close();
|
||||
@@ -539,7 +539,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
resources.clients.push(client);
|
||||
|
||||
// Make a request to ensure connection is active
|
||||
await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
}
|
||||
|
||||
// Verify all resources are active
|
||||
@@ -586,7 +586,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
// 3. Verify cleanup by attempting operations (should fail)
|
||||
for (let i = 0; i < resources.clients.length; i++) {
|
||||
try {
|
||||
await resources.clients[i].callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
await resources.clients[i].callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect.fail('Client should be closed');
|
||||
} catch (error) {
|
||||
// Expected - client is closed
|
||||
@@ -643,9 +643,9 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
}, {});
|
||||
|
||||
await client.connect(ct1);
|
||||
|
||||
|
||||
// Initial request
|
||||
const response1 = await client.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response1 = await client.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
expect(response1).toBeDefined();
|
||||
|
||||
// Close first client
|
||||
@@ -654,7 +654,7 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
|
||||
// New connection with same server
|
||||
const [st2, ct2] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
|
||||
const connectTimeout = setTimeout(() => {
|
||||
throw new Error('Second connection timeout');
|
||||
}, 3000);
|
||||
@@ -673,14 +673,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
}, {});
|
||||
|
||||
await newClient.connect(ct2);
|
||||
|
||||
|
||||
// Should work normally
|
||||
const callTimeout = setTimeout(() => {
|
||||
throw new Error('Second call timeout');
|
||||
}, 3000);
|
||||
|
||||
try {
|
||||
const response2 = await newClient.callTool({ name: 'get_database_statistics', arguments: {} });
|
||||
const response2 = await newClient.callTool({ name: 'tools_documentation', arguments: {} });
|
||||
clearTimeout(callTimeout);
|
||||
expect(response2).toBeDefined();
|
||||
} catch (error) {
|
||||
|
||||
@@ -114,7 +114,7 @@ export class TestableN8NMCPServer {
|
||||
// The MCP server initializes its database lazily
|
||||
// We can trigger initialization by calling executeTool
|
||||
try {
|
||||
await this.mcpServer.executeTool('get_database_statistics', {});
|
||||
await this.mcpServer.executeTool('tools_documentation', {});
|
||||
} catch (error) {
|
||||
// Ignore errors, we just want to trigger initialization
|
||||
}
|
||||
|
||||
@@ -30,66 +30,6 @@ describe('MCP Tool Invocation', () => {
|
||||
});
|
||||
|
||||
describe('Node Discovery Tools', () => {
|
||||
describe('list_nodes', () => {
|
||||
it('should list nodes with default parameters', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {} });
|
||||
|
||||
expect((response as any).content).toHaveLength(1);
|
||||
expect((response as any).content[0].type).toBe('text');
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
// The result is an object with nodes array and totalCount
|
||||
expect(result).toHaveProperty('nodes');
|
||||
expect(result).toHaveProperty('totalCount');
|
||||
|
||||
const nodes = result.nodes;
|
||||
expect(Array.isArray(nodes)).toBe(true);
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
|
||||
// Check node structure
|
||||
const firstNode = nodes[0];
|
||||
expect(firstNode).toHaveProperty('nodeType');
|
||||
expect(firstNode).toHaveProperty('displayName');
|
||||
expect(firstNode).toHaveProperty('category');
|
||||
});
|
||||
|
||||
it('should filter nodes by category', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
category: 'trigger'
|
||||
}});
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
const nodes = result.nodes;
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
nodes.forEach((node: any) => {
|
||||
expect(node.category).toBe('trigger');
|
||||
});
|
||||
});
|
||||
|
||||
it('should limit results', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
limit: 5
|
||||
}});
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
const nodes = result.nodes;
|
||||
expect(nodes).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should filter by package', async () => {
|
||||
const response = await client.callTool({ name: 'list_nodes', arguments: {
|
||||
package: 'n8n-nodes-base'
|
||||
}});
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
const nodes = result.nodes;
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
nodes.forEach((node: any) => {
|
||||
expect(node.package).toBe('n8n-nodes-base');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('search_nodes', () => {
|
||||
it('should search nodes by keyword', async () => {
|
||||
const response = await client.callTool({ name: 'search_nodes', arguments: {
|
||||
@@ -146,24 +86,25 @@ describe('MCP Tool Invocation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_node_info', () => {
|
||||
describe('get_node', () => {
|
||||
it('should get complete node information', async () => {
|
||||
const response = await client.callTool({ name: 'get_node_info', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest'
|
||||
const response = await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
detail: 'full'
|
||||
}});
|
||||
|
||||
expect(((response as any).content[0]).type).toBe('text');
|
||||
const nodeInfo = JSON.parse(((response as any).content[0]).text);
|
||||
|
||||
|
||||
expect(nodeInfo).toHaveProperty('nodeType', 'nodes-base.httpRequest');
|
||||
expect(nodeInfo).toHaveProperty('displayName');
|
||||
expect(nodeInfo).toHaveProperty('properties');
|
||||
expect(Array.isArray(nodeInfo.properties)).toBe(true);
|
||||
expect(nodeInfo).toHaveProperty('description');
|
||||
expect(nodeInfo).toHaveProperty('version');
|
||||
});
|
||||
|
||||
it('should handle non-existent nodes', async () => {
|
||||
try {
|
||||
await client.callTool({ name: 'get_node_info', arguments: {
|
||||
await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'nodes-base.nonExistent'
|
||||
}});
|
||||
expect.fail('Should have thrown an error');
|
||||
@@ -174,7 +115,7 @@ describe('MCP Tool Invocation', () => {
|
||||
|
||||
it('should handle invalid node type format', async () => {
|
||||
try {
|
||||
await client.callTool({ name: 'get_node_info', arguments: {
|
||||
await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'invalidFormat'
|
||||
}});
|
||||
expect.fail('Should have thrown an error');
|
||||
@@ -184,38 +125,42 @@ describe('MCP Tool Invocation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_node_essentials', () => {
|
||||
it('should return condensed node information', async () => {
|
||||
const response = await client.callTool({ name: 'get_node_essentials', arguments: {
|
||||
describe('get_node with different detail levels', () => {
|
||||
it('should return standard detail by default', async () => {
|
||||
const response = await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest'
|
||||
}});
|
||||
|
||||
const essentials = JSON.parse(((response as any).content[0]).text);
|
||||
|
||||
expect(essentials).toHaveProperty('nodeType');
|
||||
expect(essentials).toHaveProperty('displayName');
|
||||
expect(essentials).toHaveProperty('commonProperties');
|
||||
expect(essentials).toHaveProperty('requiredProperties');
|
||||
|
||||
// Should be smaller than full info
|
||||
const fullResponse = await client.callTool({ name: 'get_node_info', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest'
|
||||
const nodeInfo = JSON.parse(((response as any).content[0]).text);
|
||||
|
||||
expect(nodeInfo).toHaveProperty('nodeType');
|
||||
expect(nodeInfo).toHaveProperty('displayName');
|
||||
expect(nodeInfo).toHaveProperty('description');
|
||||
expect(nodeInfo).toHaveProperty('requiredProperties');
|
||||
expect(nodeInfo).toHaveProperty('commonProperties');
|
||||
|
||||
// Should be smaller than full detail
|
||||
const fullResponse = await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
detail: 'full'
|
||||
}});
|
||||
|
||||
|
||||
expect(((response as any).content[0]).text.length).toBeLessThan(((fullResponse as any).content[0]).text.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation Tools', () => {
|
||||
describe('validate_node_operation', () => {
|
||||
// v2.26.0: validate_node_operation consolidated into validate_node with mode parameter
|
||||
describe('validate_node', () => {
|
||||
it('should validate valid node configuration', async () => {
|
||||
const response = await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
const response = await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/data'
|
||||
}
|
||||
},
|
||||
mode: 'full'
|
||||
}});
|
||||
|
||||
const validation = JSON.parse(((response as any).content[0]).text);
|
||||
@@ -225,12 +170,13 @@ describe('MCP Tool Invocation', () => {
|
||||
});
|
||||
|
||||
it('should detect missing required fields', async () => {
|
||||
const response = await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
const response = await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: {
|
||||
method: 'GET'
|
||||
// Missing required 'url' field
|
||||
}
|
||||
},
|
||||
mode: 'full'
|
||||
}});
|
||||
|
||||
const validation = JSON.parse(((response as any).content[0]).text);
|
||||
@@ -241,11 +187,12 @@ describe('MCP Tool Invocation', () => {
|
||||
|
||||
it('should support different validation profiles', async () => {
|
||||
const profiles = ['minimal', 'runtime', 'ai-friendly', 'strict'];
|
||||
|
||||
|
||||
for (const profile of profiles) {
|
||||
const response = await client.callTool({ name: 'validate_node_operation', arguments: {
|
||||
const response = await client.callTool({ name: 'validate_node', arguments: {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: { method: 'GET', url: 'https://api.example.com' },
|
||||
mode: 'full',
|
||||
profile
|
||||
}});
|
||||
|
||||
@@ -410,7 +357,8 @@ describe('MCP Tool Invocation', () => {
|
||||
depth: 'full'
|
||||
}});
|
||||
|
||||
expect(((response as any).content[0]).text.length).toBeGreaterThan(5000);
|
||||
// Reduced from 5000 after v2.26.0 tool consolidation (31→19 tools)
|
||||
expect(((response as any).content[0]).text.length).toBeGreaterThan(4000);
|
||||
expect(((response as any).content[0]).text).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -424,85 +372,8 @@ describe('MCP Tool Invocation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI Tools', () => {
|
||||
describe('list_ai_tools', () => {
|
||||
it('should list AI-capable nodes', async () => {
|
||||
const response = await client.callTool({ name: 'list_ai_tools', arguments: {} });
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
expect(result).toHaveProperty('tools');
|
||||
const aiTools = result.tools;
|
||||
expect(Array.isArray(aiTools)).toBe(true);
|
||||
expect(aiTools.length).toBeGreaterThan(0);
|
||||
|
||||
// All should have nodeType and displayName
|
||||
aiTools.forEach((tool: any) => {
|
||||
expect(tool).toHaveProperty('nodeType');
|
||||
expect(tool).toHaveProperty('displayName');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_node_as_tool_info', () => {
|
||||
it('should provide AI tool usage information', async () => {
|
||||
const response = await client.callTool({ name: 'get_node_as_tool_info', arguments: {
|
||||
nodeType: 'nodes-base.slack'
|
||||
}});
|
||||
|
||||
const info = JSON.parse(((response as any).content[0]).text);
|
||||
expect(info).toHaveProperty('nodeType');
|
||||
expect(info).toHaveProperty('isMarkedAsAITool');
|
||||
expect(info).toHaveProperty('aiToolCapabilities');
|
||||
expect(info.aiToolCapabilities).toHaveProperty('commonUseCases');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Task Templates', () => {
|
||||
// get_node_for_task was removed in v2.15.0
|
||||
// Use search_nodes({ includeExamples: true }) instead for real-world examples
|
||||
|
||||
describe('list_tasks', () => {
|
||||
it('should list all available tasks', async () => {
|
||||
const response = await client.callTool({ name: 'list_tasks', arguments: {} });
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
expect(result).toHaveProperty('totalTasks');
|
||||
expect(result).toHaveProperty('categories');
|
||||
expect(result.totalTasks).toBeGreaterThan(0);
|
||||
|
||||
// Check categories structure
|
||||
const categories = result.categories;
|
||||
expect(typeof categories).toBe('object');
|
||||
|
||||
// Check at least one category has tasks
|
||||
const hasTasksInCategories = Object.values(categories).some((tasks: any) =>
|
||||
Array.isArray(tasks) && tasks.length > 0
|
||||
);
|
||||
expect(hasTasksInCategories).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter by category', async () => {
|
||||
const response = await client.callTool({ name: 'list_tasks', arguments: {
|
||||
category: 'HTTP/API'
|
||||
}});
|
||||
|
||||
const result = JSON.parse(((response as any).content[0]).text);
|
||||
expect(result).toHaveProperty('category', 'HTTP/API');
|
||||
expect(result).toHaveProperty('tasks');
|
||||
|
||||
const httpTasks = result.tasks;
|
||||
expect(Array.isArray(httpTasks)).toBe(true);
|
||||
expect(httpTasks.length).toBeGreaterThan(0);
|
||||
|
||||
httpTasks.forEach((task: any) => {
|
||||
expect(task).toHaveProperty('task');
|
||||
expect(task).toHaveProperty('description');
|
||||
expect(task).toHaveProperty('nodeType');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
// AI Tools section removed - list_ai_tools and get_node_as_tool_info were removed in v2.25.0
|
||||
// Use search_nodes with query for finding AI-capable nodes
|
||||
|
||||
describe('Complex Tool Interactions', () => {
|
||||
it('should handle tool chaining', async () => {
|
||||
@@ -515,7 +386,7 @@ describe('MCP Tool Invocation', () => {
|
||||
|
||||
// Get info for first result
|
||||
const firstNode = nodes[0];
|
||||
const infoResponse = await client.callTool({ name: 'get_node_info', arguments: {
|
||||
const infoResponse = await client.callTool({ name: 'get_node', arguments: {
|
||||
nodeType: firstNode.nodeType
|
||||
}});
|
||||
|
||||
@@ -523,20 +394,20 @@ describe('MCP Tool Invocation', () => {
|
||||
});
|
||||
|
||||
it('should handle parallel tool calls', async () => {
|
||||
const tools = [
|
||||
'list_nodes',
|
||||
'get_database_statistics',
|
||||
'list_ai_tools',
|
||||
'list_tasks'
|
||||
const toolCalls = [
|
||||
{ name: 'search_nodes', arguments: { query: 'http' } },
|
||||
{ name: 'tools_documentation', arguments: {} },
|
||||
{ name: 'get_node', arguments: { nodeType: 'nodes-base.httpRequest' } },
|
||||
{ name: 'search_nodes', arguments: { query: 'webhook' } }
|
||||
];
|
||||
|
||||
const promises = tools.map(tool =>
|
||||
client.callTool({ name: tool as any, arguments: {} })
|
||||
const promises = toolCalls.map(call =>
|
||||
client.callTool(call)
|
||||
);
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
expect(responses).toHaveLength(tools.length);
|
||||
|
||||
expect(responses).toHaveLength(toolCalls.length);
|
||||
responses.forEach(response => {
|
||||
expect(response.content).toHaveLength(1);
|
||||
expect(((response as any).content[0]).type).toBe('text');
|
||||
@@ -548,8 +419,8 @@ describe('MCP Tool Invocation', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
|
||||
const [fullInfo, essentials, searchResult] = await Promise.all([
|
||||
client.callTool({ name: 'get_node_info', arguments: { nodeType } }),
|
||||
client.callTool({ name: 'get_node_essentials', arguments: { nodeType } }),
|
||||
client.callTool({ name: 'get_node', arguments: { nodeType } }),
|
||||
client.callTool({ name: 'get_node', arguments: { nodeType } }),
|
||||
client.callTool({ name: 'search_nodes', arguments: { query: 'httpRequest' } })
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
/**
|
||||
* Integration Tests: handleListAvailableTools
|
||||
*
|
||||
* Tests tool listing functionality.
|
||||
* Covers tool discovery and configuration status.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createMcpContext } from '../utils/mcp-context';
|
||||
import { InstanceContext } from '../../../../src/types/instance-context';
|
||||
import { handleListAvailableTools } from '../../../../src/mcp/handlers-n8n-manager';
|
||||
import { ListToolsResponse } from '../utils/response-types';
|
||||
|
||||
describe('Integration: handleListAvailableTools', () => {
|
||||
let mcpContext: InstanceContext;
|
||||
|
||||
beforeEach(() => {
|
||||
mcpContext = createMcpContext();
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// List All Tools
|
||||
// ======================================================================
|
||||
|
||||
describe('Tool Listing', () => {
|
||||
it('should list all available tools organized by category', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data).toBeDefined();
|
||||
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
// Verify tools array exists
|
||||
expect(data).toHaveProperty('tools');
|
||||
expect(Array.isArray(data.tools)).toBe(true);
|
||||
expect(data.tools.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify tool categories
|
||||
const categories = data.tools.map((cat: any) => cat.category);
|
||||
expect(categories).toContain('Workflow Management');
|
||||
expect(categories).toContain('Execution Management');
|
||||
expect(categories).toContain('System');
|
||||
|
||||
// Verify each category has tools
|
||||
data.tools.forEach(category => {
|
||||
expect(category).toHaveProperty('category');
|
||||
expect(category).toHaveProperty('tools');
|
||||
expect(Array.isArray(category.tools)).toBe(true);
|
||||
expect(category.tools.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify each tool has required fields
|
||||
category.tools.forEach(tool => {
|
||||
expect(tool).toHaveProperty('name');
|
||||
expect(tool).toHaveProperty('description');
|
||||
expect(typeof tool.name).toBe('string');
|
||||
expect(typeof tool.description).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should include API configuration status', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
// Verify configuration status
|
||||
expect(data).toHaveProperty('apiConfigured');
|
||||
expect(typeof data.apiConfigured).toBe('boolean');
|
||||
|
||||
// Since tests run with API configured, should be true
|
||||
expect(data.apiConfigured).toBe(true);
|
||||
|
||||
// Verify configuration details are present when configured
|
||||
if (data.apiConfigured) {
|
||||
expect(data).toHaveProperty('configuration');
|
||||
expect(data.configuration).toBeDefined();
|
||||
expect(data.configuration).toHaveProperty('apiUrl');
|
||||
expect(data.configuration).toHaveProperty('timeout');
|
||||
expect(data.configuration).toHaveProperty('maxRetries');
|
||||
}
|
||||
});
|
||||
|
||||
it('should include API limitations information', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
// Verify limitations are documented
|
||||
expect(data).toHaveProperty('limitations');
|
||||
expect(Array.isArray(data.limitations)).toBe(true);
|
||||
expect(data.limitations.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify limitations are informative strings
|
||||
data.limitations.forEach(limitation => {
|
||||
expect(typeof limitation).toBe('string');
|
||||
expect(limitation.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Common known limitations
|
||||
const limitationsText = data.limitations.join(' ');
|
||||
expect(limitationsText).toContain('Cannot execute workflows directly');
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// Workflow Management Tools
|
||||
// ======================================================================
|
||||
|
||||
describe('Workflow Management Tools', () => {
|
||||
it('should include all workflow management tools', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
const workflowCategory = data.tools.find(cat => cat.category === 'Workflow Management');
|
||||
expect(workflowCategory).toBeDefined();
|
||||
|
||||
const toolNames = workflowCategory!.tools.map(t => t.name);
|
||||
|
||||
// Core workflow tools
|
||||
expect(toolNames).toContain('n8n_create_workflow');
|
||||
expect(toolNames).toContain('n8n_get_workflow');
|
||||
expect(toolNames).toContain('n8n_update_workflow');
|
||||
expect(toolNames).toContain('n8n_delete_workflow');
|
||||
expect(toolNames).toContain('n8n_list_workflows');
|
||||
|
||||
// Enhanced workflow tools
|
||||
expect(toolNames).toContain('n8n_get_workflow_details');
|
||||
expect(toolNames).toContain('n8n_get_workflow_structure');
|
||||
expect(toolNames).toContain('n8n_get_workflow_minimal');
|
||||
expect(toolNames).toContain('n8n_validate_workflow');
|
||||
expect(toolNames).toContain('n8n_autofix_workflow');
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// Execution Management Tools
|
||||
// ======================================================================
|
||||
|
||||
describe('Execution Management Tools', () => {
|
||||
it('should include all execution management tools', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
const executionCategory = data.tools.find(cat => cat.category === 'Execution Management');
|
||||
expect(executionCategory).toBeDefined();
|
||||
|
||||
const toolNames = executionCategory!.tools.map(t => t.name);
|
||||
|
||||
expect(toolNames).toContain('n8n_trigger_webhook_workflow');
|
||||
expect(toolNames).toContain('n8n_get_execution');
|
||||
expect(toolNames).toContain('n8n_list_executions');
|
||||
expect(toolNames).toContain('n8n_delete_execution');
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// System Tools
|
||||
// ======================================================================
|
||||
|
||||
describe('System Tools', () => {
|
||||
it('should include system tools', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
const systemCategory = data.tools.find(cat => cat.category === 'System');
|
||||
expect(systemCategory).toBeDefined();
|
||||
|
||||
const toolNames = systemCategory!.tools.map(t => t.name);
|
||||
|
||||
expect(toolNames).toContain('n8n_health_check');
|
||||
expect(toolNames).toContain('n8n_list_available_tools');
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// Response Format Verification
|
||||
// ======================================================================
|
||||
|
||||
describe('Response Format', () => {
|
||||
it('should return complete tool list response structure', async () => {
|
||||
const response = await handleListAvailableTools(mcpContext);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data).toBeDefined();
|
||||
|
||||
const data = response.data as ListToolsResponse;
|
||||
|
||||
// Verify all required fields
|
||||
expect(data).toHaveProperty('tools');
|
||||
expect(data).toHaveProperty('apiConfigured');
|
||||
expect(data).toHaveProperty('limitations');
|
||||
|
||||
// Verify optional configuration field
|
||||
if (data.apiConfigured) {
|
||||
expect(data).toHaveProperty('configuration');
|
||||
}
|
||||
|
||||
// Verify data types
|
||||
expect(Array.isArray(data.tools)).toBe(true);
|
||||
expect(typeof data.apiConfigured).toBe('boolean');
|
||||
expect(Array.isArray(data.limitations)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,29 +19,6 @@ export interface HealthCheckResponse {
|
||||
[key: string]: any; // Allow dynamic property access for optional field checks
|
||||
}
|
||||
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ToolCategory {
|
||||
category: string;
|
||||
tools: ToolDefinition[];
|
||||
}
|
||||
|
||||
export interface ApiConfiguration {
|
||||
apiUrl: string;
|
||||
timeout: number;
|
||||
maxRetries: number;
|
||||
}
|
||||
|
||||
export interface ListToolsResponse {
|
||||
tools: ToolCategory[];
|
||||
apiConfigured: boolean;
|
||||
configuration?: ApiConfiguration | null;
|
||||
limitations: string[];
|
||||
}
|
||||
|
||||
export interface ApiStatus {
|
||||
configured: boolean;
|
||||
connected: boolean;
|
||||
|
||||
@@ -227,7 +227,7 @@ describe.skip('MCP Telemetry Integration', () => {
|
||||
const callToolRequest: CallToolRequest = {
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'get_node_info',
|
||||
name: 'get_node',
|
||||
arguments: { nodeType: 'invalid-node' }
|
||||
}
|
||||
};
|
||||
@@ -247,11 +247,11 @@ describe.skip('MCP Telemetry Integration', () => {
|
||||
}
|
||||
}
|
||||
|
||||
expect(telemetry.trackToolUsage).toHaveBeenCalledWith('get_node_info', false);
|
||||
expect(telemetry.trackToolUsage).toHaveBeenCalledWith('get_node', false);
|
||||
expect(telemetry.trackError).toHaveBeenCalledWith(
|
||||
'Error',
|
||||
'Node not found',
|
||||
'get_node_info'
|
||||
'get_node'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -263,7 +263,7 @@ describe.skip('MCP Telemetry Integration', () => {
|
||||
const callToolRequest: CallToolRequest = {
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'get_node_info',
|
||||
name: 'get_node',
|
||||
arguments: { nodeType: 'nodes-base.webhook' }
|
||||
}
|
||||
};
|
||||
@@ -282,7 +282,7 @@ describe.skip('MCP Telemetry Integration', () => {
|
||||
|
||||
expect(telemetry.trackToolSequence).toHaveBeenCalledWith(
|
||||
'search_nodes',
|
||||
'get_node_info',
|
||||
'get_node',
|
||||
expect.any(Number)
|
||||
);
|
||||
});
|
||||
@@ -500,15 +500,15 @@ describe.skip('MCP Telemetry Integration', () => {
|
||||
const slowToolRequest: CallToolRequest = {
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'list_nodes',
|
||||
arguments: { limit: 1000 }
|
||||
name: 'search_nodes',
|
||||
arguments: { query: 'http', limit: 1000 }
|
||||
}
|
||||
};
|
||||
|
||||
// Mock a slow operation
|
||||
vi.spyOn(mcpServer as any, 'executeTool').mockImplementation(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second delay
|
||||
return { nodes: [], totalCount: 0 };
|
||||
return { results: [], totalCount: 0 };
|
||||
});
|
||||
|
||||
const server = (mcpServer as any).server;
|
||||
@@ -519,7 +519,7 @@ describe.skip('MCP Telemetry Integration', () => {
|
||||
}
|
||||
|
||||
expect(telemetry.trackToolUsage).toHaveBeenCalledWith(
|
||||
'list_nodes',
|
||||
'search_nodes',
|
||||
true,
|
||||
expect.any(Number)
|
||||
);
|
||||
|
||||
546
tests/unit/http-server/session-persistence.test.ts
Normal file
546
tests/unit/http-server/session-persistence.test.ts
Normal file
@@ -0,0 +1,546 @@
|
||||
/**
|
||||
* Unit tests for session persistence API
|
||||
* Tests export and restore functionality for multi-tenant session management
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { SingleSessionHTTPServer } from '../../../src/http-server-single-session';
|
||||
import { SessionState } from '../../../src/types/session-state';
|
||||
|
||||
describe('SingleSessionHTTPServer - Session Persistence', () => {
|
||||
let server: SingleSessionHTTPServer;
|
||||
|
||||
beforeEach(() => {
|
||||
server = new SingleSessionHTTPServer();
|
||||
});
|
||||
|
||||
describe('exportSessionState()', () => {
|
||||
it('should return empty array when no sessions exist', () => {
|
||||
const exported = server.exportSessionState();
|
||||
expect(exported).toEqual([]);
|
||||
});
|
||||
|
||||
it('should export active sessions with all required fields', () => {
|
||||
// Create mock sessions by directly manipulating internal state
|
||||
const sessionId1 = 'test-session-1';
|
||||
const sessionId2 = 'test-session-2';
|
||||
|
||||
// Use current timestamps to avoid expiration
|
||||
const now = new Date();
|
||||
const createdAt1 = new Date(now.getTime() - 10 * 60 * 1000); // 10 minutes ago
|
||||
const lastAccess1 = new Date(now.getTime() - 5 * 60 * 1000); // 5 minutes ago
|
||||
const createdAt2 = new Date(now.getTime() - 15 * 60 * 1000); // 15 minutes ago
|
||||
const lastAccess2 = new Date(now.getTime() - 3 * 60 * 1000); // 3 minutes ago
|
||||
|
||||
// Access private properties for testing
|
||||
const serverAny = server as any;
|
||||
|
||||
serverAny.sessionMetadata[sessionId1] = {
|
||||
createdAt: createdAt1,
|
||||
lastAccess: lastAccess1
|
||||
};
|
||||
|
||||
serverAny.sessionContexts[sessionId1] = {
|
||||
n8nApiUrl: 'https://n8n1.example.com',
|
||||
n8nApiKey: 'key1',
|
||||
instanceId: 'instance1',
|
||||
sessionId: sessionId1,
|
||||
metadata: { userId: 'user1' }
|
||||
};
|
||||
|
||||
serverAny.sessionMetadata[sessionId2] = {
|
||||
createdAt: createdAt2,
|
||||
lastAccess: lastAccess2
|
||||
};
|
||||
|
||||
serverAny.sessionContexts[sessionId2] = {
|
||||
n8nApiUrl: 'https://n8n2.example.com',
|
||||
n8nApiKey: 'key2',
|
||||
instanceId: 'instance2'
|
||||
};
|
||||
|
||||
const exported = server.exportSessionState();
|
||||
|
||||
expect(exported).toHaveLength(2);
|
||||
|
||||
// Verify first session
|
||||
expect(exported[0]).toMatchObject({
|
||||
sessionId: sessionId1,
|
||||
metadata: {
|
||||
createdAt: createdAt1.toISOString(),
|
||||
lastAccess: lastAccess1.toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://n8n1.example.com',
|
||||
n8nApiKey: 'key1',
|
||||
instanceId: 'instance1',
|
||||
sessionId: sessionId1,
|
||||
metadata: { userId: 'user1' }
|
||||
}
|
||||
});
|
||||
|
||||
// Verify second session
|
||||
expect(exported[1]).toMatchObject({
|
||||
sessionId: sessionId2,
|
||||
metadata: {
|
||||
createdAt: createdAt2.toISOString(),
|
||||
lastAccess: lastAccess2.toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://n8n2.example.com',
|
||||
n8nApiKey: 'key2',
|
||||
instanceId: 'instance2'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip expired sessions during export', () => {
|
||||
const serverAny = server as any;
|
||||
const now = Date.now();
|
||||
const sessionTimeout = 30 * 60 * 1000; // 30 minutes (default)
|
||||
|
||||
// Create an active session (accessed recently)
|
||||
serverAny.sessionMetadata['active-session'] = {
|
||||
createdAt: new Date(now - 10 * 60 * 1000), // 10 minutes ago
|
||||
lastAccess: new Date(now - 5 * 60 * 1000) // 5 minutes ago
|
||||
};
|
||||
serverAny.sessionContexts['active-session'] = {
|
||||
n8nApiUrl: 'https://active.example.com',
|
||||
n8nApiKey: 'active-key',
|
||||
instanceId: 'active-instance'
|
||||
};
|
||||
|
||||
// Create an expired session (last accessed > 30 minutes ago)
|
||||
serverAny.sessionMetadata['expired-session'] = {
|
||||
createdAt: new Date(now - 60 * 60 * 1000), // 60 minutes ago
|
||||
lastAccess: new Date(now - 45 * 60 * 1000) // 45 minutes ago (expired)
|
||||
};
|
||||
serverAny.sessionContexts['expired-session'] = {
|
||||
n8nApiUrl: 'https://expired.example.com',
|
||||
n8nApiKey: 'expired-key',
|
||||
instanceId: 'expired-instance'
|
||||
};
|
||||
|
||||
const exported = server.exportSessionState();
|
||||
|
||||
expect(exported).toHaveLength(1);
|
||||
expect(exported[0].sessionId).toBe('active-session');
|
||||
});
|
||||
|
||||
it('should skip sessions without required context fields', () => {
|
||||
const serverAny = server as any;
|
||||
|
||||
// Session with complete context
|
||||
serverAny.sessionMetadata['complete-session'] = {
|
||||
createdAt: new Date(),
|
||||
lastAccess: new Date()
|
||||
};
|
||||
serverAny.sessionContexts['complete-session'] = {
|
||||
n8nApiUrl: 'https://complete.example.com',
|
||||
n8nApiKey: 'complete-key',
|
||||
instanceId: 'complete-instance'
|
||||
};
|
||||
|
||||
// Session with missing n8nApiUrl
|
||||
serverAny.sessionMetadata['missing-url'] = {
|
||||
createdAt: new Date(),
|
||||
lastAccess: new Date()
|
||||
};
|
||||
serverAny.sessionContexts['missing-url'] = {
|
||||
n8nApiKey: 'key',
|
||||
instanceId: 'instance'
|
||||
};
|
||||
|
||||
// Session with missing n8nApiKey
|
||||
serverAny.sessionMetadata['missing-key'] = {
|
||||
createdAt: new Date(),
|
||||
lastAccess: new Date()
|
||||
};
|
||||
serverAny.sessionContexts['missing-key'] = {
|
||||
n8nApiUrl: 'https://example.com',
|
||||
instanceId: 'instance'
|
||||
};
|
||||
|
||||
// Session with no context at all
|
||||
serverAny.sessionMetadata['no-context'] = {
|
||||
createdAt: new Date(),
|
||||
lastAccess: new Date()
|
||||
};
|
||||
|
||||
const exported = server.exportSessionState();
|
||||
|
||||
expect(exported).toHaveLength(1);
|
||||
expect(exported[0].sessionId).toBe('complete-session');
|
||||
});
|
||||
|
||||
it('should use sessionId as fallback for instanceId', () => {
|
||||
const serverAny = server as any;
|
||||
const sessionId = 'test-session';
|
||||
|
||||
serverAny.sessionMetadata[sessionId] = {
|
||||
createdAt: new Date(),
|
||||
lastAccess: new Date()
|
||||
};
|
||||
serverAny.sessionContexts[sessionId] = {
|
||||
n8nApiUrl: 'https://example.com',
|
||||
n8nApiKey: 'key'
|
||||
// No instanceId provided
|
||||
};
|
||||
|
||||
const exported = server.exportSessionState();
|
||||
|
||||
expect(exported).toHaveLength(1);
|
||||
expect(exported[0].context.instanceId).toBe(sessionId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreSessionState()', () => {
|
||||
it('should restore valid sessions correctly', () => {
|
||||
const sessions: SessionState[] = [
|
||||
{
|
||||
sessionId: 'restored-session-1',
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://restored1.example.com',
|
||||
n8nApiKey: 'restored-key-1',
|
||||
instanceId: 'restored-instance-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
sessionId: 'restored-session-2',
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://restored2.example.com',
|
||||
n8nApiKey: 'restored-key-2',
|
||||
instanceId: 'restored-instance-2',
|
||||
sessionId: 'custom-session-id',
|
||||
metadata: { custom: 'data' }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const count = server.restoreSessionState(sessions);
|
||||
|
||||
expect(count).toBe(2);
|
||||
|
||||
// Verify sessions were restored by checking internal state
|
||||
const serverAny = server as any;
|
||||
|
||||
expect(serverAny.sessionMetadata['restored-session-1']).toBeDefined();
|
||||
expect(serverAny.sessionContexts['restored-session-1']).toMatchObject({
|
||||
n8nApiUrl: 'https://restored1.example.com',
|
||||
n8nApiKey: 'restored-key-1',
|
||||
instanceId: 'restored-instance-1'
|
||||
});
|
||||
|
||||
expect(serverAny.sessionMetadata['restored-session-2']).toBeDefined();
|
||||
expect(serverAny.sessionContexts['restored-session-2']).toMatchObject({
|
||||
n8nApiUrl: 'https://restored2.example.com',
|
||||
n8nApiKey: 'restored-key-2',
|
||||
instanceId: 'restored-instance-2',
|
||||
sessionId: 'custom-session-id',
|
||||
metadata: { custom: 'data' }
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip expired sessions during restore', () => {
|
||||
const now = Date.now();
|
||||
const sessionTimeout = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
const sessions: SessionState[] = [
|
||||
{
|
||||
sessionId: 'active-session',
|
||||
metadata: {
|
||||
createdAt: new Date(now - 10 * 60 * 1000).toISOString(),
|
||||
lastAccess: new Date(now - 5 * 60 * 1000).toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://active.example.com',
|
||||
n8nApiKey: 'active-key',
|
||||
instanceId: 'active-instance'
|
||||
}
|
||||
},
|
||||
{
|
||||
sessionId: 'expired-session',
|
||||
metadata: {
|
||||
createdAt: new Date(now - 60 * 60 * 1000).toISOString(),
|
||||
lastAccess: new Date(now - 45 * 60 * 1000).toISOString() // Expired
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://expired.example.com',
|
||||
n8nApiKey: 'expired-key',
|
||||
instanceId: 'expired-instance'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const count = server.restoreSessionState(sessions);
|
||||
|
||||
expect(count).toBe(1);
|
||||
|
||||
const serverAny = server as any;
|
||||
expect(serverAny.sessionMetadata['active-session']).toBeDefined();
|
||||
expect(serverAny.sessionMetadata['expired-session']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should skip sessions with missing required context fields', () => {
|
||||
const sessions: SessionState[] = [
|
||||
{
|
||||
sessionId: 'valid-session',
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://valid.example.com',
|
||||
n8nApiKey: 'valid-key',
|
||||
instanceId: 'valid-instance'
|
||||
}
|
||||
},
|
||||
{
|
||||
sessionId: 'missing-url',
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: '', // Empty URL
|
||||
n8nApiKey: 'key',
|
||||
instanceId: 'instance'
|
||||
}
|
||||
},
|
||||
{
|
||||
sessionId: 'missing-key',
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://example.com',
|
||||
n8nApiKey: '', // Empty key
|
||||
instanceId: 'instance'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const count = server.restoreSessionState(sessions);
|
||||
|
||||
expect(count).toBe(1);
|
||||
|
||||
const serverAny = server as any;
|
||||
expect(serverAny.sessionMetadata['valid-session']).toBeDefined();
|
||||
expect(serverAny.sessionMetadata['missing-url']).toBeUndefined();
|
||||
expect(serverAny.sessionMetadata['missing-key']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should skip duplicate sessionIds', () => {
|
||||
const serverAny = server as any;
|
||||
|
||||
// Create an existing session
|
||||
serverAny.sessionMetadata['existing-session'] = {
|
||||
createdAt: new Date(),
|
||||
lastAccess: new Date()
|
||||
};
|
||||
|
||||
const sessions: SessionState[] = [
|
||||
{
|
||||
sessionId: 'new-session',
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://new.example.com',
|
||||
n8nApiKey: 'new-key',
|
||||
instanceId: 'new-instance'
|
||||
}
|
||||
},
|
||||
{
|
||||
sessionId: 'existing-session', // Duplicate
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://duplicate.example.com',
|
||||
n8nApiKey: 'duplicate-key',
|
||||
instanceId: 'duplicate-instance'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const count = server.restoreSessionState(sessions);
|
||||
|
||||
expect(count).toBe(1);
|
||||
expect(serverAny.sessionMetadata['new-session']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle restore failures gracefully', () => {
|
||||
const sessions: any[] = [
|
||||
{
|
||||
sessionId: 'valid-session',
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://valid.example.com',
|
||||
n8nApiKey: 'valid-key',
|
||||
instanceId: 'valid-instance'
|
||||
}
|
||||
},
|
||||
{
|
||||
sessionId: 'bad-session',
|
||||
metadata: {}, // Missing required fields
|
||||
context: null // Invalid context
|
||||
},
|
||||
null, // Invalid session
|
||||
{
|
||||
// Missing sessionId
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://example.com',
|
||||
n8nApiKey: 'key',
|
||||
instanceId: 'instance'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Should not throw and should restore only the valid session
|
||||
expect(() => {
|
||||
const count = server.restoreSessionState(sessions);
|
||||
expect(count).toBe(1); // Only valid-session should be restored
|
||||
}).not.toThrow();
|
||||
|
||||
// Verify the valid session was restored
|
||||
const serverAny = server as any;
|
||||
expect(serverAny.sessionMetadata['valid-session']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should respect MAX_SESSIONS limit during restore', () => {
|
||||
// Create 99 existing sessions (MAX_SESSIONS is 100)
|
||||
const serverAny = server as any;
|
||||
const now = new Date();
|
||||
for (let i = 0; i < 99; i++) {
|
||||
serverAny.sessionMetadata[`existing-${i}`] = {
|
||||
createdAt: now,
|
||||
lastAccess: now
|
||||
};
|
||||
}
|
||||
|
||||
// Try to restore 3 sessions (should only restore 1 due to limit)
|
||||
const sessions: SessionState[] = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
sessions.push({
|
||||
sessionId: `new-session-${i}`,
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: `https://new${i}.example.com`,
|
||||
n8nApiKey: `new-key-${i}`,
|
||||
instanceId: `new-instance-${i}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const count = server.restoreSessionState(sessions);
|
||||
|
||||
expect(count).toBe(1);
|
||||
expect(serverAny.sessionMetadata['new-session-0']).toBeDefined();
|
||||
expect(serverAny.sessionMetadata['new-session-1']).toBeUndefined();
|
||||
expect(serverAny.sessionMetadata['new-session-2']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should parse ISO 8601 timestamps correctly', () => {
|
||||
// Use current timestamps to avoid expiration
|
||||
const now = new Date();
|
||||
const createdAtDate = new Date(now.getTime() - 10 * 60 * 1000); // 10 minutes ago
|
||||
const lastAccessDate = new Date(now.getTime() - 5 * 60 * 1000); // 5 minutes ago
|
||||
const createdAt = createdAtDate.toISOString();
|
||||
const lastAccess = lastAccessDate.toISOString();
|
||||
|
||||
const sessions: SessionState[] = [
|
||||
{
|
||||
sessionId: 'timestamp-session',
|
||||
metadata: { createdAt, lastAccess },
|
||||
context: {
|
||||
n8nApiUrl: 'https://example.com',
|
||||
n8nApiKey: 'key',
|
||||
instanceId: 'instance'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const count = server.restoreSessionState(sessions);
|
||||
expect(count).toBe(1);
|
||||
|
||||
const serverAny = server as any;
|
||||
const metadata = serverAny.sessionMetadata['timestamp-session'];
|
||||
|
||||
expect(metadata.createdAt).toBeInstanceOf(Date);
|
||||
expect(metadata.lastAccess).toBeInstanceOf(Date);
|
||||
expect(metadata.createdAt.toISOString()).toBe(createdAt);
|
||||
expect(metadata.lastAccess.toISOString()).toBe(lastAccess);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Round-trip export and restore', () => {
|
||||
it('should preserve data through export → restore cycle', () => {
|
||||
// Create sessions with current timestamps
|
||||
const serverAny = server as any;
|
||||
const now = new Date();
|
||||
const createdAt = new Date(now.getTime() - 10 * 60 * 1000); // 10 minutes ago
|
||||
const lastAccess = new Date(now.getTime() - 5 * 60 * 1000); // 5 minutes ago
|
||||
|
||||
serverAny.sessionMetadata['session-1'] = {
|
||||
createdAt,
|
||||
lastAccess
|
||||
};
|
||||
serverAny.sessionContexts['session-1'] = {
|
||||
n8nApiUrl: 'https://n8n1.example.com',
|
||||
n8nApiKey: 'key1',
|
||||
instanceId: 'instance1',
|
||||
sessionId: 'custom-id-1',
|
||||
metadata: { userId: 'user1', role: 'admin' }
|
||||
};
|
||||
|
||||
// Export sessions
|
||||
const exported = server.exportSessionState();
|
||||
expect(exported).toHaveLength(1);
|
||||
|
||||
// Clear sessions
|
||||
delete serverAny.sessionMetadata['session-1'];
|
||||
delete serverAny.sessionContexts['session-1'];
|
||||
|
||||
// Restore sessions
|
||||
const count = server.restoreSessionState(exported);
|
||||
expect(count).toBe(1);
|
||||
|
||||
// Verify data integrity
|
||||
const metadata = serverAny.sessionMetadata['session-1'];
|
||||
const context = serverAny.sessionContexts['session-1'];
|
||||
|
||||
expect(metadata.createdAt.toISOString()).toBe(createdAt.toISOString());
|
||||
expect(metadata.lastAccess.toISOString()).toBe(lastAccess.toISOString());
|
||||
|
||||
expect(context).toMatchObject({
|
||||
n8nApiUrl: 'https://n8n1.example.com',
|
||||
n8nApiKey: 'key1',
|
||||
instanceId: 'instance1',
|
||||
sessionId: 'custom-id-1',
|
||||
metadata: { userId: 'user1', role: 'admin' }
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
255
tests/unit/mcp-engine/session-persistence.test.ts
Normal file
255
tests/unit/mcp-engine/session-persistence.test.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* Unit tests for N8NMCPEngine session persistence wrapper methods
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { N8NMCPEngine } from '../../../src/mcp-engine';
|
||||
import { SessionState } from '../../../src/types/session-state';
|
||||
|
||||
describe('N8NMCPEngine - Session Persistence', () => {
|
||||
let engine: N8NMCPEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
engine = new N8NMCPEngine({
|
||||
sessionTimeout: 30 * 60 * 1000,
|
||||
logLevel: 'error' // Quiet during tests
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportSessionState()', () => {
|
||||
it('should return empty array when no sessions exist', () => {
|
||||
const exported = engine.exportSessionState();
|
||||
expect(exported).toEqual([]);
|
||||
});
|
||||
|
||||
it('should delegate to underlying server', () => {
|
||||
// Access private server to create test sessions
|
||||
const engineAny = engine as any;
|
||||
const server = engineAny.server;
|
||||
const serverAny = server as any;
|
||||
|
||||
// Create a mock session
|
||||
serverAny.sessionMetadata['test-session'] = {
|
||||
createdAt: new Date(),
|
||||
lastAccess: new Date()
|
||||
};
|
||||
serverAny.sessionContexts['test-session'] = {
|
||||
n8nApiUrl: 'https://test.example.com',
|
||||
n8nApiKey: 'test-key',
|
||||
instanceId: 'test-instance'
|
||||
};
|
||||
|
||||
const exported = engine.exportSessionState();
|
||||
|
||||
expect(exported).toHaveLength(1);
|
||||
expect(exported[0].sessionId).toBe('test-session');
|
||||
expect(exported[0].context.n8nApiUrl).toBe('https://test.example.com');
|
||||
});
|
||||
|
||||
it('should handle server not initialized', () => {
|
||||
// Create engine without server
|
||||
const engineAny = {} as N8NMCPEngine;
|
||||
const exportMethod = N8NMCPEngine.prototype.exportSessionState.bind(engineAny);
|
||||
|
||||
// Should not throw, should return empty array
|
||||
expect(() => exportMethod()).not.toThrow();
|
||||
const result = exportMethod();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreSessionState()', () => {
|
||||
it('should restore sessions via underlying server', () => {
|
||||
const sessions: SessionState[] = [
|
||||
{
|
||||
sessionId: 'restored-session',
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://restored.example.com',
|
||||
n8nApiKey: 'restored-key',
|
||||
instanceId: 'restored-instance'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const count = engine.restoreSessionState(sessions);
|
||||
|
||||
expect(count).toBe(1);
|
||||
|
||||
// Verify session was restored
|
||||
const engineAny = engine as any;
|
||||
const server = engineAny.server;
|
||||
const serverAny = server as any;
|
||||
|
||||
expect(serverAny.sessionMetadata['restored-session']).toBeDefined();
|
||||
expect(serverAny.sessionContexts['restored-session']).toMatchObject({
|
||||
n8nApiUrl: 'https://restored.example.com',
|
||||
n8nApiKey: 'restored-key',
|
||||
instanceId: 'restored-instance'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 0 when restoring empty array', () => {
|
||||
const count = engine.restoreSessionState([]);
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle server not initialized', () => {
|
||||
const engineAny = {} as N8NMCPEngine;
|
||||
const restoreMethod = N8NMCPEngine.prototype.restoreSessionState.bind(engineAny);
|
||||
|
||||
const sessions: SessionState[] = [
|
||||
{
|
||||
sessionId: 'test',
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://test.example.com',
|
||||
n8nApiKey: 'test-key',
|
||||
instanceId: 'test-instance'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Should not throw, should return 0
|
||||
expect(() => restoreMethod(sessions)).not.toThrow();
|
||||
const result = restoreMethod(sessions);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should return count of successfully restored sessions', () => {
|
||||
const now = Date.now();
|
||||
const sessions: SessionState[] = [
|
||||
{
|
||||
sessionId: 'valid-1',
|
||||
metadata: {
|
||||
createdAt: new Date(now - 10 * 60 * 1000).toISOString(),
|
||||
lastAccess: new Date(now - 5 * 60 * 1000).toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://valid1.example.com',
|
||||
n8nApiKey: 'key1',
|
||||
instanceId: 'instance1'
|
||||
}
|
||||
},
|
||||
{
|
||||
sessionId: 'valid-2',
|
||||
metadata: {
|
||||
createdAt: new Date(now - 10 * 60 * 1000).toISOString(),
|
||||
lastAccess: new Date(now - 5 * 60 * 1000).toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://valid2.example.com',
|
||||
n8nApiKey: 'key2',
|
||||
instanceId: 'instance2'
|
||||
}
|
||||
},
|
||||
{
|
||||
sessionId: 'expired',
|
||||
metadata: {
|
||||
createdAt: new Date(now - 60 * 60 * 1000).toISOString(),
|
||||
lastAccess: new Date(now - 45 * 60 * 1000).toISOString() // Expired
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://expired.example.com',
|
||||
n8nApiKey: 'expired-key',
|
||||
instanceId: 'expired-instance'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const count = engine.restoreSessionState(sessions);
|
||||
|
||||
expect(count).toBe(2); // Only 2 valid sessions
|
||||
});
|
||||
});
|
||||
|
||||
describe('Round-trip through engine', () => {
|
||||
it('should preserve sessions through export → restore cycle', () => {
|
||||
// Create mock sessions with current timestamps
|
||||
const engineAny = engine as any;
|
||||
const server = engineAny.server;
|
||||
const serverAny = server as any;
|
||||
|
||||
const now = new Date();
|
||||
const createdAt = new Date(now.getTime() - 10 * 60 * 1000); // 10 minutes ago
|
||||
const lastAccess = new Date(now.getTime() - 5 * 60 * 1000); // 5 minutes ago
|
||||
|
||||
serverAny.sessionMetadata['engine-session'] = {
|
||||
createdAt,
|
||||
lastAccess
|
||||
};
|
||||
serverAny.sessionContexts['engine-session'] = {
|
||||
n8nApiUrl: 'https://engine-test.example.com',
|
||||
n8nApiKey: 'engine-key',
|
||||
instanceId: 'engine-instance',
|
||||
metadata: { env: 'production' }
|
||||
};
|
||||
|
||||
// Export via engine
|
||||
const exported = engine.exportSessionState();
|
||||
expect(exported).toHaveLength(1);
|
||||
|
||||
// Clear sessions
|
||||
delete serverAny.sessionMetadata['engine-session'];
|
||||
delete serverAny.sessionContexts['engine-session'];
|
||||
|
||||
// Restore via engine
|
||||
const count = engine.restoreSessionState(exported);
|
||||
expect(count).toBe(1);
|
||||
|
||||
// Verify data
|
||||
expect(serverAny.sessionMetadata['engine-session']).toBeDefined();
|
||||
expect(serverAny.sessionContexts['engine-session']).toMatchObject({
|
||||
n8nApiUrl: 'https://engine-test.example.com',
|
||||
n8nApiKey: 'engine-key',
|
||||
instanceId: 'engine-instance',
|
||||
metadata: { env: 'production' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration with getSessionInfo()', () => {
|
||||
it('should reflect restored sessions in session info', () => {
|
||||
const sessions: SessionState[] = [
|
||||
{
|
||||
sessionId: 'info-session-1',
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://info1.example.com',
|
||||
n8nApiKey: 'info-key-1',
|
||||
instanceId: 'info-instance-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
sessionId: 'info-session-2',
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccess: new Date().toISOString()
|
||||
},
|
||||
context: {
|
||||
n8nApiUrl: 'https://info2.example.com',
|
||||
n8nApiKey: 'info-key-2',
|
||||
instanceId: 'info-instance-2'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
engine.restoreSessionState(sessions);
|
||||
|
||||
const info = engine.getSessionInfo();
|
||||
|
||||
// Note: getSessionInfo() reflects metadata, not transports
|
||||
// Restored sessions won't have transports until first request
|
||||
expect(info).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -73,14 +73,14 @@ describe('Disabled Tools Feature (Issue #410)', () => {
|
||||
});
|
||||
|
||||
it('should parse multiple disabled tools correctly', () => {
|
||||
process.env.DISABLED_TOOLS = 'n8n_diagnostic,n8n_health_check,list_nodes';
|
||||
process.env.DISABLED_TOOLS = 'n8n_diagnostic,n8n_health_check,search_nodes';
|
||||
server = new TestableN8NMCPServer();
|
||||
const disabledTools = server.testGetDisabledTools();
|
||||
|
||||
expect(disabledTools.size).toBe(3);
|
||||
expect(disabledTools.has('n8n_diagnostic')).toBe(true);
|
||||
expect(disabledTools.has('n8n_health_check')).toBe(true);
|
||||
expect(disabledTools.has('list_nodes')).toBe(true);
|
||||
expect(disabledTools.has('search_nodes')).toBe(true);
|
||||
});
|
||||
|
||||
it('should trim whitespace from tool names', () => {
|
||||
@@ -94,14 +94,14 @@ describe('Disabled Tools Feature (Issue #410)', () => {
|
||||
});
|
||||
|
||||
it('should filter out empty entries from comma-separated list', () => {
|
||||
process.env.DISABLED_TOOLS = 'n8n_diagnostic,,n8n_health_check,,,list_nodes';
|
||||
process.env.DISABLED_TOOLS = 'n8n_diagnostic,,n8n_health_check,,,search_nodes';
|
||||
server = new TestableN8NMCPServer();
|
||||
const disabledTools = server.testGetDisabledTools();
|
||||
|
||||
expect(disabledTools.size).toBe(3);
|
||||
expect(disabledTools.has('n8n_diagnostic')).toBe(true);
|
||||
expect(disabledTools.has('n8n_health_check')).toBe(true);
|
||||
expect(disabledTools.has('list_nodes')).toBe(true);
|
||||
expect(disabledTools.has('search_nodes')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle single comma correctly', () => {
|
||||
|
||||
1163
tests/unit/mcp/get-node-unified.test.ts
Normal file
1163
tests/unit/mcp/get-node-unified.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
265
tests/unit/mcp/handlers-deploy-template.test.ts
Normal file
265
tests/unit/mcp/handlers-deploy-template.test.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Unit tests for handleDeployTemplate handler - input validation and schema tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Test the schema directly without needing full API mocking
|
||||
const deployTemplateSchema = z.object({
|
||||
templateId: z.number().positive().int(),
|
||||
name: z.string().optional(),
|
||||
autoUpgradeVersions: z.boolean().default(true),
|
||||
validate: z.boolean().default(true),
|
||||
stripCredentials: z.boolean().default(true)
|
||||
});
|
||||
|
||||
describe('handleDeployTemplate Schema Validation', () => {
|
||||
describe('Input Schema', () => {
|
||||
it('should require templateId as a positive integer', () => {
|
||||
// Valid input
|
||||
const validResult = deployTemplateSchema.safeParse({ templateId: 123 });
|
||||
expect(validResult.success).toBe(true);
|
||||
|
||||
// Invalid: missing templateId
|
||||
const missingResult = deployTemplateSchema.safeParse({});
|
||||
expect(missingResult.success).toBe(false);
|
||||
|
||||
// Invalid: templateId as string
|
||||
const stringResult = deployTemplateSchema.safeParse({ templateId: '123' });
|
||||
expect(stringResult.success).toBe(false);
|
||||
|
||||
// Invalid: negative templateId
|
||||
const negativeResult = deployTemplateSchema.safeParse({ templateId: -1 });
|
||||
expect(negativeResult.success).toBe(false);
|
||||
|
||||
// Invalid: zero templateId
|
||||
const zeroResult = deployTemplateSchema.safeParse({ templateId: 0 });
|
||||
expect(zeroResult.success).toBe(false);
|
||||
|
||||
// Invalid: decimal templateId
|
||||
const decimalResult = deployTemplateSchema.safeParse({ templateId: 123.5 });
|
||||
expect(decimalResult.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept optional name parameter', () => {
|
||||
const withName = deployTemplateSchema.safeParse({
|
||||
templateId: 123,
|
||||
name: 'Custom Name'
|
||||
});
|
||||
expect(withName.success).toBe(true);
|
||||
if (withName.success) {
|
||||
expect(withName.data.name).toBe('Custom Name');
|
||||
}
|
||||
|
||||
const withoutName = deployTemplateSchema.safeParse({ templateId: 123 });
|
||||
expect(withoutName.success).toBe(true);
|
||||
if (withoutName.success) {
|
||||
expect(withoutName.data.name).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should default autoUpgradeVersions to true', () => {
|
||||
const result = deployTemplateSchema.safeParse({ templateId: 123 });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.autoUpgradeVersions).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow setting autoUpgradeVersions to false', () => {
|
||||
const result = deployTemplateSchema.safeParse({
|
||||
templateId: 123,
|
||||
autoUpgradeVersions: false
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.autoUpgradeVersions).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should default validate to true', () => {
|
||||
const result = deployTemplateSchema.safeParse({ templateId: 123 });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.validate).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should default stripCredentials to true', () => {
|
||||
const result = deployTemplateSchema.safeParse({ templateId: 123 });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.stripCredentials).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept all parameters together', () => {
|
||||
const result = deployTemplateSchema.safeParse({
|
||||
templateId: 2776,
|
||||
name: 'My Deployed Workflow',
|
||||
autoUpgradeVersions: false,
|
||||
validate: false,
|
||||
stripCredentials: false
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.templateId).toBe(2776);
|
||||
expect(result.data.name).toBe('My Deployed Workflow');
|
||||
expect(result.data.autoUpgradeVersions).toBe(false);
|
||||
expect(result.data.validate).toBe(false);
|
||||
expect(result.data.stripCredentials).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDeployTemplate Helper Functions', () => {
|
||||
describe('Credential Extraction Logic', () => {
|
||||
it('should extract credential types from node credentials object', () => {
|
||||
const nodes = [
|
||||
{
|
||||
id: 'node-1',
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
credentials: {
|
||||
slackApi: { id: 'cred-1', name: 'My Slack' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'node-2',
|
||||
name: 'Google Sheets',
|
||||
type: 'n8n-nodes-base.googleSheets',
|
||||
credentials: {
|
||||
googleSheetsOAuth2Api: { id: 'cred-2', name: 'My Google' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'node-3',
|
||||
name: 'Set',
|
||||
type: 'n8n-nodes-base.set'
|
||||
// No credentials
|
||||
}
|
||||
];
|
||||
|
||||
// Simulate the credential extraction logic from the handler
|
||||
const requiredCredentials: Array<{
|
||||
nodeType: string;
|
||||
nodeName: string;
|
||||
credentialType: string;
|
||||
}> = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.credentials && typeof node.credentials === 'object') {
|
||||
for (const [credType] of Object.entries(node.credentials)) {
|
||||
requiredCredentials.push({
|
||||
nodeType: node.type,
|
||||
nodeName: node.name,
|
||||
credentialType: credType
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(requiredCredentials).toHaveLength(2);
|
||||
expect(requiredCredentials[0]).toEqual({
|
||||
nodeType: 'n8n-nodes-base.slack',
|
||||
nodeName: 'Slack',
|
||||
credentialType: 'slackApi'
|
||||
});
|
||||
expect(requiredCredentials[1]).toEqual({
|
||||
nodeType: 'n8n-nodes-base.googleSheets',
|
||||
nodeName: 'Google Sheets',
|
||||
credentialType: 'googleSheetsOAuth2Api'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Credential Stripping Logic', () => {
|
||||
it('should remove credentials property from nodes', () => {
|
||||
const nodes = [
|
||||
{
|
||||
id: 'node-1',
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
typeVersion: 2,
|
||||
position: [250, 300],
|
||||
parameters: { channel: '#general' },
|
||||
credentials: {
|
||||
slackApi: { id: 'cred-1', name: 'My Slack' }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Simulate the credential stripping logic from the handler
|
||||
const strippedNodes = nodes.map((node: any) => {
|
||||
const { credentials, ...rest } = node;
|
||||
return rest;
|
||||
});
|
||||
|
||||
expect(strippedNodes[0].credentials).toBeUndefined();
|
||||
expect(strippedNodes[0].id).toBe('node-1');
|
||||
expect(strippedNodes[0].name).toBe('Slack');
|
||||
expect(strippedNodes[0].parameters).toEqual({ channel: '#general' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Trigger Type Detection Logic', () => {
|
||||
it('should identify trigger nodes', () => {
|
||||
const testCases = [
|
||||
{ type: 'n8n-nodes-base.scheduleTrigger', expected: 'scheduleTrigger' },
|
||||
{ type: 'n8n-nodes-base.webhook', expected: 'webhook' },
|
||||
{ type: 'n8n-nodes-base.emailReadImapTrigger', expected: 'emailReadImapTrigger' },
|
||||
{ type: 'n8n-nodes-base.googleDriveTrigger', expected: 'googleDriveTrigger' }
|
||||
];
|
||||
|
||||
for (const { type, expected } of testCases) {
|
||||
const nodes = [{ type, name: 'Trigger' }];
|
||||
|
||||
// Simulate the trigger detection logic from the handler
|
||||
const triggerNode = nodes.find((n: any) =>
|
||||
n.type?.includes('Trigger') ||
|
||||
n.type?.includes('webhook') ||
|
||||
n.type === 'n8n-nodes-base.webhook'
|
||||
);
|
||||
const triggerType = triggerNode?.type?.split('.').pop() || 'manual';
|
||||
|
||||
expect(triggerType).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return manual for workflows without trigger', () => {
|
||||
const nodes = [
|
||||
{ type: 'n8n-nodes-base.set', name: 'Set' },
|
||||
{ type: 'n8n-nodes-base.httpRequest', name: 'HTTP Request' }
|
||||
];
|
||||
|
||||
const triggerNode = nodes.find((n: any) =>
|
||||
n.type?.includes('Trigger') ||
|
||||
n.type?.includes('webhook') ||
|
||||
n.type === 'n8n-nodes-base.webhook'
|
||||
);
|
||||
const triggerType = triggerNode?.type?.split('.').pop() || 'manual';
|
||||
|
||||
expect(triggerType).toBe('manual');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tool Definition Validation', () => {
|
||||
it('should have correct tool name', () => {
|
||||
// This tests that the tool is properly exported
|
||||
const toolName = 'n8n_deploy_template';
|
||||
expect(toolName).toBe('n8n_deploy_template');
|
||||
});
|
||||
|
||||
it('should have required parameter templateId in schema', () => {
|
||||
// Validate that the schema correctly requires templateId
|
||||
const validResult = deployTemplateSchema.safeParse({ templateId: 123 });
|
||||
const invalidResult = deployTemplateSchema.safeParse({});
|
||||
|
||||
expect(validResult.success).toBe(true);
|
||||
expect(invalidResult.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1031,7 +1031,7 @@ describe('handlers-n8n-manager', () => {
|
||||
'1. Verify n8n instance is running',
|
||||
'2. Check N8N_API_URL is correct',
|
||||
'3. Verify N8N_API_KEY has proper permissions',
|
||||
'4. Run n8n_diagnostic for detailed analysis',
|
||||
'4. Run n8n_health_check with mode="diagnostic" for detailed analysis',
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -1068,14 +1068,14 @@ describe('handlers-n8n-manager', () => {
|
||||
},
|
||||
toolsAvailability: {
|
||||
documentationTools: {
|
||||
count: 22,
|
||||
count: 7,
|
||||
enabled: true,
|
||||
},
|
||||
managementTools: {
|
||||
count: 16,
|
||||
count: 13,
|
||||
enabled: true,
|
||||
},
|
||||
totalAvailable: 38,
|
||||
totalAvailable: 20,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -140,10 +140,9 @@ describe('Parameter Validation', () => {
|
||||
// Mock the actual tool methods to avoid database calls
|
||||
beforeEach(() => {
|
||||
// Mock all the tool methods that would be called
|
||||
vi.spyOn(server as any, 'getNodeInfo').mockResolvedValue({ mockResult: true });
|
||||
vi.spyOn(server as any, 'getNode').mockResolvedValue({ mockResult: true });
|
||||
vi.spyOn(server as any, 'searchNodes').mockResolvedValue({ results: [] });
|
||||
vi.spyOn(server as any, 'getNodeDocumentation').mockResolvedValue({ docs: 'test' });
|
||||
vi.spyOn(server as any, 'getNodeEssentials').mockResolvedValue({ essentials: true });
|
||||
vi.spyOn(server as any, 'searchNodeProperties').mockResolvedValue({ properties: [] });
|
||||
// Note: getNodeForTask removed in v2.15.0
|
||||
vi.spyOn(server as any, 'validateNodeConfig').mockResolvedValue({ valid: true });
|
||||
@@ -159,15 +158,15 @@ describe('Parameter Validation', () => {
|
||||
vi.spyOn(server as any, 'validateWorkflowExpressions').mockResolvedValue({ valid: true });
|
||||
});
|
||||
|
||||
describe('get_node_info', () => {
|
||||
describe('get_node', () => {
|
||||
it('should require nodeType parameter', async () => {
|
||||
await expect(server.testExecuteTool('get_node_info', {}))
|
||||
.rejects.toThrow('Missing required parameters for get_node_info: nodeType');
|
||||
await expect(server.testExecuteTool('get_node', {}))
|
||||
.rejects.toThrow('Missing required parameters for get_node: nodeType');
|
||||
});
|
||||
|
||||
it('should succeed with valid nodeType', async () => {
|
||||
const result = await server.testExecuteTool('get_node_info', {
|
||||
nodeType: 'nodes-base.httpRequest'
|
||||
const result = await server.testExecuteTool('get_node', {
|
||||
nodeType: 'nodes-base.httpRequest'
|
||||
});
|
||||
expect(result).toEqual({ mockResult: true });
|
||||
});
|
||||
@@ -202,63 +201,76 @@ describe('Parameter Validation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate_node_operation', () => {
|
||||
describe('validate_node (consolidated)', () => {
|
||||
it('should require nodeType and config parameters', async () => {
|
||||
await expect(server.testExecuteTool('validate_node_operation', {}))
|
||||
.rejects.toThrow('validate_node_operation: Validation failed:\n • nodeType: nodeType is required\n • config: config is required');
|
||||
await expect(server.testExecuteTool('validate_node', {}))
|
||||
.rejects.toThrow('validate_node: Validation failed:\n • nodeType: nodeType is required\n • config: config is required');
|
||||
});
|
||||
|
||||
it('should require nodeType parameter when config is provided', async () => {
|
||||
await expect(server.testExecuteTool('validate_node_operation', { config: {} }))
|
||||
.rejects.toThrow('validate_node_operation: Validation failed:\n • nodeType: nodeType is required');
|
||||
await expect(server.testExecuteTool('validate_node', { config: {} }))
|
||||
.rejects.toThrow('validate_node: Validation failed:\n • nodeType: nodeType is required');
|
||||
});
|
||||
|
||||
it('should require config parameter when nodeType is provided', async () => {
|
||||
await expect(server.testExecuteTool('validate_node_operation', { nodeType: 'nodes-base.httpRequest' }))
|
||||
.rejects.toThrow('validate_node_operation: Validation failed:\n • config: config is required');
|
||||
await expect(server.testExecuteTool('validate_node', { nodeType: 'nodes-base.httpRequest' }))
|
||||
.rejects.toThrow('validate_node: Validation failed:\n • config: config is required');
|
||||
});
|
||||
|
||||
it('should succeed with valid parameters', async () => {
|
||||
const result = await server.testExecuteTool('validate_node_operation', {
|
||||
it('should succeed with valid parameters (full mode)', async () => {
|
||||
const result = await server.testExecuteTool('validate_node', {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: { method: 'GET', url: 'https://api.example.com' }
|
||||
config: { method: 'GET', url: 'https://api.example.com' },
|
||||
mode: 'full'
|
||||
});
|
||||
expect(result).toEqual({ valid: true });
|
||||
});
|
||||
|
||||
it('should succeed with valid parameters (minimal mode)', async () => {
|
||||
const result = await server.testExecuteTool('validate_node', {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: {},
|
||||
mode: 'minimal'
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('search_node_properties', () => {
|
||||
it('should require nodeType and query parameters', async () => {
|
||||
await expect(server.testExecuteTool('search_node_properties', {}))
|
||||
.rejects.toThrow('Missing required parameters for search_node_properties: nodeType, query');
|
||||
describe('get_node mode=search_properties (consolidated)', () => {
|
||||
it('should require nodeType and propertyQuery parameters', async () => {
|
||||
await expect(server.testExecuteTool('get_node', { mode: 'search_properties' }))
|
||||
.rejects.toThrow('Missing required parameters for get_node: nodeType');
|
||||
});
|
||||
|
||||
it('should succeed with valid parameters', async () => {
|
||||
const result = await server.testExecuteTool('search_node_properties', {
|
||||
const result = await server.testExecuteTool('get_node', {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
query: 'auth'
|
||||
mode: 'search_properties',
|
||||
propertyQuery: 'auth'
|
||||
});
|
||||
expect(result).toEqual({ properties: [] });
|
||||
});
|
||||
|
||||
it('should handle optional maxResults parameter', async () => {
|
||||
const result = await server.testExecuteTool('search_node_properties', {
|
||||
it('should handle optional maxPropertyResults parameter', async () => {
|
||||
const result = await server.testExecuteTool('get_node', {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
query: 'auth',
|
||||
maxResults: 5
|
||||
mode: 'search_properties',
|
||||
propertyQuery: 'auth',
|
||||
maxPropertyResults: 5
|
||||
});
|
||||
expect(result).toEqual({ properties: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('list_node_templates', () => {
|
||||
it('should require nodeTypes parameter', async () => {
|
||||
await expect(server.testExecuteTool('list_node_templates', {}))
|
||||
.rejects.toThrow('list_node_templates: Validation failed:\n • nodeTypes: nodeTypes is required');
|
||||
describe('search_templates searchMode=by_nodes (consolidated)', () => {
|
||||
it('should require nodeTypes parameter for by_nodes searchMode', async () => {
|
||||
await expect(server.testExecuteTool('search_templates', { searchMode: 'by_nodes' }))
|
||||
.rejects.toThrow('nodeTypes array is required for searchMode=by_nodes');
|
||||
});
|
||||
|
||||
it('should succeed with valid nodeTypes array', async () => {
|
||||
const result = await server.testExecuteTool('list_node_templates', {
|
||||
const result = await server.testExecuteTool('search_templates', {
|
||||
searchMode: 'by_nodes',
|
||||
nodeTypes: ['nodes-base.httpRequest', 'nodes-base.slack']
|
||||
});
|
||||
expect(result).toEqual({ templates: [] });
|
||||
@@ -321,45 +333,43 @@ describe('Parameter Validation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxResults parameter conversion', () => {
|
||||
it('should convert string numbers to numbers', async () => {
|
||||
describe('maxPropertyResults parameter conversion (v2.26.0 consolidated)', () => {
|
||||
it('should pass numeric maxPropertyResults to searchNodeProperties', async () => {
|
||||
const mockSearchNodeProperties = vi.spyOn(server as any, 'searchNodeProperties');
|
||||
|
||||
await server.testExecuteTool('search_node_properties', {
|
||||
|
||||
// v2.26.0: search_node_properties consolidated into get_node with mode='search_properties'
|
||||
await server.testExecuteTool('get_node', {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
query: 'auth',
|
||||
maxResults: '5'
|
||||
mode: 'search_properties',
|
||||
propertyQuery: 'auth',
|
||||
maxPropertyResults: 5
|
||||
});
|
||||
|
||||
expect(mockSearchNodeProperties).toHaveBeenCalledWith('nodes-base.httpRequest', 'auth', 5);
|
||||
});
|
||||
|
||||
it('should use default when maxResults is invalid', async () => {
|
||||
it('should use default maxPropertyResults when not provided', async () => {
|
||||
const mockSearchNodeProperties = vi.spyOn(server as any, 'searchNodeProperties');
|
||||
|
||||
await server.testExecuteTool('search_node_properties', {
|
||||
|
||||
// v2.26.0: search_node_properties consolidated into get_node with mode='search_properties'
|
||||
await server.testExecuteTool('get_node', {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
query: 'auth',
|
||||
maxResults: 'invalid'
|
||||
mode: 'search_properties',
|
||||
propertyQuery: 'auth'
|
||||
});
|
||||
|
||||
expect(mockSearchNodeProperties).toHaveBeenCalledWith('nodes-base.httpRequest', 'auth', 20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('templateLimit parameter conversion', () => {
|
||||
it('should reject string limit values', async () => {
|
||||
await expect(server.testExecuteTool('list_node_templates', {
|
||||
describe('templateLimit parameter conversion (v2.26.0 consolidated)', () => {
|
||||
it('should handle search_templates with by_nodes mode', async () => {
|
||||
// search_templates now handles list_node_templates functionality via searchMode='by_nodes'
|
||||
await expect(server.testExecuteTool('search_templates', {
|
||||
searchMode: 'by_nodes',
|
||||
nodeTypes: ['nodes-base.httpRequest'],
|
||||
limit: '5'
|
||||
})).rejects.toThrow('list_node_templates: Validation failed:\n • limit: limit must be a number, got string');
|
||||
});
|
||||
|
||||
it('should reject invalid string limit values', async () => {
|
||||
await expect(server.testExecuteTool('list_node_templates', {
|
||||
nodeTypes: ['nodes-base.httpRequest'],
|
||||
limit: 'invalid'
|
||||
})).rejects.toThrow('list_node_templates: Validation failed:\n • limit: limit must be a number, got string');
|
||||
limit: 5
|
||||
})).resolves.toEqual({ templates: [] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -400,38 +410,25 @@ describe('Parameter Validation', () => {
|
||||
expect(result).toEqual({ docs: 'test' });
|
||||
});
|
||||
|
||||
it('should allow list_nodes with no parameters', async () => {
|
||||
const result = await server.testExecuteTool('list_nodes', {});
|
||||
expect(result).toEqual({ nodes: [] });
|
||||
});
|
||||
|
||||
it('should allow list_ai_tools with no parameters', async () => {
|
||||
const result = await server.testExecuteTool('list_ai_tools', {});
|
||||
expect(result).toEqual({ tools: [] });
|
||||
});
|
||||
|
||||
it('should allow get_database_statistics with no parameters', async () => {
|
||||
const result = await server.testExecuteTool('get_database_statistics', {});
|
||||
expect(result).toEqual({ stats: {} });
|
||||
});
|
||||
|
||||
it('should allow list_tasks with no parameters', async () => {
|
||||
const result = await server.testExecuteTool('list_tasks', {});
|
||||
expect(result).toEqual({ tasks: [] });
|
||||
it('should allow tools_documentation with no parameters', async () => {
|
||||
const result = await server.testExecuteTool('tools_documentation', {});
|
||||
expect(result).toBeDefined();
|
||||
// tools_documentation returns an object with documentation content
|
||||
expect(typeof result).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Message Quality', () => {
|
||||
it('should provide clear error messages with tool name', () => {
|
||||
expect(() => {
|
||||
server.testValidateToolParams('get_node_info', {}, ['nodeType']);
|
||||
}).toThrow('Missing required parameters for get_node_info: nodeType. Please provide the required parameters to use this tool.');
|
||||
server.testValidateToolParams('get_node', {}, ['nodeType']);
|
||||
}).toThrow('Missing required parameters for get_node: nodeType. Please provide the required parameters to use this tool.');
|
||||
});
|
||||
|
||||
it('should list all missing parameters', () => {
|
||||
expect(() => {
|
||||
server.testValidateToolParams('validate_node_operation', { profile: 'strict' }, ['nodeType', 'config']);
|
||||
}).toThrow('validate_node_operation: Validation failed:\n • nodeType: nodeType is required\n • config: config is required');
|
||||
server.testValidateToolParams('validate_node', { profile: 'strict' }, ['nodeType', 'config']);
|
||||
}).toThrow('validate_node: Validation failed:\n • nodeType: nodeType is required\n • config: config is required');
|
||||
});
|
||||
|
||||
it('should include helpful guidance', () => {
|
||||
@@ -447,39 +444,38 @@ describe('Parameter Validation', () => {
|
||||
it('should convert validation errors to MCP error responses rather than throwing exceptions', async () => {
|
||||
// This test simulates what happens at the MCP level when a tool validation fails
|
||||
// The server should catch the validation error and return it as an MCP error response
|
||||
|
||||
|
||||
// Directly test the executeTool method to ensure it throws appropriately
|
||||
// The MCP server's request handler should catch these and convert to error responses
|
||||
await expect(server.testExecuteTool('get_node_info', {}))
|
||||
.rejects.toThrow('Missing required parameters for get_node_info: nodeType');
|
||||
await expect(server.testExecuteTool('get_node', {}))
|
||||
.rejects.toThrow('Missing required parameters for get_node: nodeType');
|
||||
|
||||
await expect(server.testExecuteTool('search_nodes', {}))
|
||||
.rejects.toThrow('search_nodes: Validation failed:\n • query: query is required');
|
||||
|
||||
await expect(server.testExecuteTool('validate_node_operation', { nodeType: 'test' }))
|
||||
.rejects.toThrow('validate_node_operation: Validation failed:\n • config: config is required');
|
||||
await expect(server.testExecuteTool('validate_node', { nodeType: 'test' }))
|
||||
.rejects.toThrow('validate_node: Validation failed:\n • config: config is required');
|
||||
});
|
||||
|
||||
it('should handle edge cases in parameter validation gracefully', async () => {
|
||||
// Test with null args (should be handled by args = args || {})
|
||||
await expect(server.testExecuteTool('get_node_info', null))
|
||||
await expect(server.testExecuteTool('get_node', null))
|
||||
.rejects.toThrow('Missing required parameters');
|
||||
|
||||
|
||||
// Test with undefined args
|
||||
await expect(server.testExecuteTool('get_node_info', undefined))
|
||||
await expect(server.testExecuteTool('get_node', undefined))
|
||||
.rejects.toThrow('Missing required parameters');
|
||||
});
|
||||
|
||||
it('should provide consistent error format across all tools', async () => {
|
||||
// Tools using legacy validation
|
||||
const legacyValidationTools = [
|
||||
{ name: 'get_node_info', args: {}, expected: 'Missing required parameters for get_node_info: nodeType' },
|
||||
{ name: 'get_node_documentation', args: {}, expected: 'Missing required parameters for get_node_documentation: nodeType' },
|
||||
{ name: 'get_node_essentials', args: {}, expected: 'Missing required parameters for get_node_essentials: nodeType' },
|
||||
{ name: 'search_node_properties', args: {}, expected: 'Missing required parameters for search_node_properties: nodeType, query' },
|
||||
{ name: 'get_node', args: {}, expected: 'Missing required parameters for get_node: nodeType' },
|
||||
// v2.26.0: get_node_documentation consolidated into get_node with mode='docs'
|
||||
// v2.26.0: search_node_properties consolidated into get_node with mode='search_properties'
|
||||
// Note: get_node_for_task removed in v2.15.0
|
||||
{ name: 'get_property_dependencies', args: {}, expected: 'Missing required parameters for get_property_dependencies: nodeType' },
|
||||
{ name: 'get_node_as_tool_info', args: {}, expected: 'Missing required parameters for get_node_as_tool_info: nodeType' },
|
||||
// Note: get_node_as_tool_info removed in v2.25.0
|
||||
// v2.26.0: get_property_dependencies removed (low usage)
|
||||
{ name: 'get_template', args: {}, expected: 'Missing required parameters for get_template: templateId' },
|
||||
];
|
||||
|
||||
@@ -489,11 +485,11 @@ describe('Parameter Validation', () => {
|
||||
}
|
||||
|
||||
// Tools using new schema validation
|
||||
// Updated for v2.26.0 tool consolidation
|
||||
const schemaValidationTools = [
|
||||
{ name: 'search_nodes', args: {}, expected: 'search_nodes: Validation failed:\n • query: query is required' },
|
||||
{ name: 'validate_node_operation', args: {}, expected: 'validate_node_operation: Validation failed:\n • nodeType: nodeType is required\n • config: config is required' },
|
||||
{ name: 'validate_node_minimal', args: {}, expected: 'validate_node_minimal: Validation failed:\n • nodeType: nodeType is required\n • config: config is required' },
|
||||
{ name: 'list_node_templates', args: {}, expected: 'list_node_templates: Validation failed:\n • nodeTypes: nodeTypes is required' },
|
||||
{ name: 'validate_node', args: {}, expected: 'validate_node: Validation failed:\n • nodeType: nodeType is required\n • config: config is required' },
|
||||
// list_node_templates consolidated into search_templates with searchMode='by_nodes'
|
||||
];
|
||||
|
||||
for (const tool of schemaValidationTools) {
|
||||
@@ -528,17 +524,15 @@ describe('Parameter Validation', () => {
|
||||
handleUpdatePartialWorkflow: vi.fn().mockResolvedValue({ success: true })
|
||||
}));
|
||||
|
||||
// Updated for v2.26.0 tool consolidation:
|
||||
// - n8n_get_workflow now supports mode parameter (full, details, structure, minimal)
|
||||
// - n8n_executions now handles get/list/delete via action parameter
|
||||
const n8nToolsWithRequiredParams = [
|
||||
{ name: 'n8n_create_workflow', args: {}, expected: 'n8n_create_workflow: Validation failed:\n • name: name is required\n • nodes: nodes is required\n • connections: connections is required' },
|
||||
{ name: 'n8n_get_workflow', args: {}, expected: 'n8n_get_workflow: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_get_workflow_details', args: {}, expected: 'n8n_get_workflow_details: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_get_workflow_structure', args: {}, expected: 'n8n_get_workflow_structure: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_get_workflow_minimal', args: {}, expected: 'n8n_get_workflow_minimal: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_update_full_workflow', args: {}, expected: 'n8n_update_full_workflow: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_delete_workflow', args: {}, expected: 'n8n_delete_workflow: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_validate_workflow', args: {}, expected: 'n8n_validate_workflow: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_get_execution', args: {}, expected: 'n8n_get_execution: Validation failed:\n • id: id is required' },
|
||||
{ name: 'n8n_delete_execution', args: {}, expected: 'n8n_delete_execution: Validation failed:\n • id: id is required' },
|
||||
];
|
||||
|
||||
// n8n_update_partial_workflow and n8n_trigger_webhook_workflow use legacy validation
|
||||
|
||||
@@ -49,7 +49,7 @@ vi.mock('@/mcp/tool-docs', () => ({
|
||||
performance: 'Instant - uses in-memory index',
|
||||
bestPractices: ['Start with single words', 'Use FUZZY for uncertain names'],
|
||||
pitfalls: ['Overly specific queries may return no results'],
|
||||
relatedTools: ['list_nodes', 'get_node_info']
|
||||
relatedTools: ['get_node', 'get_node_documentation']
|
||||
}
|
||||
},
|
||||
validate_workflow: {
|
||||
@@ -81,7 +81,7 @@ vi.mock('@/mcp/tool-docs', () => ({
|
||||
performance: 'Depends on workflow complexity',
|
||||
bestPractices: ['Validate before saving', 'Fix errors first'],
|
||||
pitfalls: ['Large workflows may take time'],
|
||||
relatedTools: ['validate_node_operation']
|
||||
relatedTools: ['validate_node']
|
||||
}
|
||||
},
|
||||
get_node_essentials: {
|
||||
@@ -172,7 +172,7 @@ describe('tools-documentation', () => {
|
||||
expect(doc).toContain('## Common Pitfalls');
|
||||
expect(doc).toContain('- Overly specific queries');
|
||||
expect(doc).toContain('## Related Tools');
|
||||
expect(doc).toContain('- list_nodes');
|
||||
expect(doc).toContain('- get_node');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -78,33 +78,8 @@ 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_info', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_node_info');
|
||||
describe('get_node', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_node');
|
||||
|
||||
it('should exist', () => {
|
||||
expect(tool).toBeDefined();
|
||||
@@ -114,8 +89,8 @@ describe('n8nDocumentationToolsFinal', () => {
|
||||
expect(tool?.inputSchema.required).toContain('nodeType');
|
||||
});
|
||||
|
||||
it('should mention performance implications in description', () => {
|
||||
expect(tool?.description).toMatch(/100KB\+|large|full/i);
|
||||
it('should mention detail levels in description', () => {
|
||||
expect(tool?.description).toMatch(/minimal|standard|full/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -166,18 +141,23 @@ describe('n8nDocumentationToolsFinal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_templates_for_task', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_templates_for_task');
|
||||
describe('search_templates (consolidated)', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_templates');
|
||||
|
||||
it('should exist', () => {
|
||||
expect(tool).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have task as required parameter', () => {
|
||||
expect(tool?.inputSchema.required).toContain('task');
|
||||
it('should have searchMode parameter with correct enum values', () => {
|
||||
const searchModeParam = tool?.inputSchema.properties?.searchMode;
|
||||
expect(searchModeParam).toBeDefined();
|
||||
expect(searchModeParam.enum).toEqual(['keyword', 'by_nodes', 'by_task', 'by_metadata']);
|
||||
expect(searchModeParam.default).toBe('keyword');
|
||||
});
|
||||
|
||||
it('should have correct task enum values', () => {
|
||||
it('should have task parameter for by_task searchMode', () => {
|
||||
const taskParam = tool?.inputSchema.properties?.task;
|
||||
expect(taskParam).toBeDefined();
|
||||
const expectedTasks = [
|
||||
'ai_automation',
|
||||
'data_sync',
|
||||
@@ -190,32 +170,37 @@ describe('n8nDocumentationToolsFinal', () => {
|
||||
'api_integration',
|
||||
'database_operations'
|
||||
];
|
||||
expect(tool?.inputSchema.properties.task.enum).toEqual(expectedTasks);
|
||||
expect(taskParam.enum).toEqual(expectedTasks);
|
||||
});
|
||||
|
||||
it('should have nodeTypes parameter for by_nodes searchMode', () => {
|
||||
const nodeTypesParam = tool?.inputSchema.properties?.nodeTypes;
|
||||
expect(nodeTypesParam).toBeDefined();
|
||||
expect(nodeTypesParam.type).toBe('array');
|
||||
expect(nodeTypesParam.items.type).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tool Description Quality', () => {
|
||||
it('should have concise descriptions that fit in one line', () => {
|
||||
it('should have concise descriptions that fit within reasonable limits', () => {
|
||||
n8nDocumentationToolsFinal.forEach(tool => {
|
||||
// Descriptions should be informative but not overly long
|
||||
expect(tool.description.length).toBeLessThan(300);
|
||||
// Consolidated tools (v2.26.0) may have longer descriptions due to multiple modes
|
||||
// Allow up to 500 chars for tools with mode-based functionality
|
||||
expect(tool.description.length).toBeLessThan(500);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include examples or key information in descriptions', () => {
|
||||
const toolsWithExamples = [
|
||||
'list_nodes',
|
||||
'get_node_info',
|
||||
'search_nodes',
|
||||
'get_node_essentials',
|
||||
'get_node_documentation'
|
||||
'get_node',
|
||||
'search_nodes'
|
||||
];
|
||||
|
||||
toolsWithExamples.forEach(toolName => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
|
||||
// Should include either example usage, format information, or "nodes-base"
|
||||
expect(tool?.description).toMatch(/example|Example|format|Format|nodes-base|Common:/i);
|
||||
expect(tool?.description).toMatch(/example|Example|format|Format|nodes-base|Common:|mode/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -250,15 +235,16 @@ describe('n8nDocumentationToolsFinal', () => {
|
||||
|
||||
describe('Tool Categories Coverage', () => {
|
||||
it('should have tools for all major categories', () => {
|
||||
// Updated for v2.26.0 consolidated tools
|
||||
const categories = {
|
||||
discovery: ['list_nodes', 'search_nodes', 'list_ai_tools'],
|
||||
configuration: ['get_node_info', 'get_node_essentials', 'get_node_documentation'],
|
||||
validation: ['validate_node_operation', 'validate_workflow', 'validate_node_minimal'],
|
||||
templates: ['list_tasks', 'search_templates', 'list_templates', 'get_template', 'list_node_templates'], // get_node_for_task removed in v2.15.0
|
||||
discovery: ['search_nodes'],
|
||||
configuration: ['get_node'], // get_node now includes docs mode
|
||||
validation: ['validate_node', 'validate_workflow'], // consolidated validate_node
|
||||
templates: ['search_templates', 'get_template'], // search_templates now handles all search modes
|
||||
documentation: ['tools_documentation']
|
||||
};
|
||||
|
||||
Object.entries(categories).forEach(([category, expectedTools]) => {
|
||||
Object.entries(categories).forEach(([_category, expectedTools]) => {
|
||||
expectedTools.forEach(toolName => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
|
||||
expect(tool).toBeDefined();
|
||||
@@ -295,62 +281,30 @@ describe('n8nDocumentationToolsFinal', () => {
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle tools with no parameters', () => {
|
||||
const toolsWithNoParams = ['list_ai_tools', 'get_database_statistics'];
|
||||
|
||||
toolsWithNoParams.forEach(toolName => {
|
||||
it('should handle tools with optional parameters only', () => {
|
||||
// Tools where all parameters are optional
|
||||
const toolsWithOptionalParams = ['tools_documentation'];
|
||||
|
||||
toolsWithOptionalParams.forEach(toolName => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
|
||||
expect(tool).toBeDefined();
|
||||
expect(Object.keys(tool?.inputSchema.properties || {}).length).toBe(0);
|
||||
// These tools have properties but no required array or empty required array
|
||||
expect(tool?.inputSchema.required === undefined || tool?.inputSchema.required?.length === 0).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have array parameters defined correctly', () => {
|
||||
const toolsWithArrays = ['list_node_templates'];
|
||||
|
||||
toolsWithArrays.forEach(toolName => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
|
||||
const arrayParam = tool?.inputSchema.properties.nodeTypes;
|
||||
expect(arrayParam?.type).toBe('array');
|
||||
expect(arrayParam?.items).toBeDefined();
|
||||
expect(arrayParam?.items.type).toBe('string');
|
||||
});
|
||||
// search_templates now handles nodeTypes for by_nodes mode
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_templates');
|
||||
const arrayParam = tool?.inputSchema.properties?.nodeTypes;
|
||||
expect(arrayParam?.type).toBe('array');
|
||||
expect(arrayParam?.items).toBeDefined();
|
||||
expect(arrayParam?.items.type).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('New Template Tools', () => {
|
||||
describe('list_templates', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'list_templates');
|
||||
|
||||
it('should exist and be properly defined', () => {
|
||||
expect(tool).toBeDefined();
|
||||
expect(tool?.description).toContain('minimal data');
|
||||
});
|
||||
|
||||
it('should have correct parameters', () => {
|
||||
expect(tool?.inputSchema.properties).toHaveProperty('limit');
|
||||
expect(tool?.inputSchema.properties).toHaveProperty('offset');
|
||||
expect(tool?.inputSchema.properties).toHaveProperty('sortBy');
|
||||
|
||||
const limitParam = tool?.inputSchema.properties.limit;
|
||||
expect(limitParam.type).toBe('number');
|
||||
expect(limitParam.minimum).toBe(1);
|
||||
expect(limitParam.maximum).toBe(100);
|
||||
|
||||
const offsetParam = tool?.inputSchema.properties.offset;
|
||||
expect(offsetParam.type).toBe('number');
|
||||
expect(offsetParam.minimum).toBe(0);
|
||||
|
||||
const sortByParam = tool?.inputSchema.properties.sortBy;
|
||||
expect(sortByParam.enum).toEqual(['views', 'created_at', 'name']);
|
||||
});
|
||||
|
||||
it('should have no required parameters', () => {
|
||||
expect(tool?.inputSchema.required).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_template (enhanced)', () => {
|
||||
describe('Consolidated Template Tools (v2.26.0)', () => {
|
||||
describe('get_template', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_template');
|
||||
|
||||
it('should exist and support mode parameter', () => {
|
||||
@@ -371,130 +325,56 @@ describe('n8nDocumentationToolsFinal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('search_templates_by_metadata', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_templates_by_metadata');
|
||||
describe('search_templates (consolidated with searchMode)', () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_templates');
|
||||
|
||||
it('should exist in the tools array', () => {
|
||||
it('should exist with searchMode parameter', () => {
|
||||
expect(tool).toBeDefined();
|
||||
expect(tool?.name).toBe('search_templates_by_metadata');
|
||||
expect(tool?.inputSchema.properties).toHaveProperty('searchMode');
|
||||
});
|
||||
|
||||
it('should have proper description', () => {
|
||||
expect(tool?.description).toContain('Search templates by AI-generated metadata');
|
||||
expect(tool?.description).toContain('category');
|
||||
expect(tool?.description).toContain('complexity');
|
||||
});
|
||||
|
||||
it('should have correct input schema structure', () => {
|
||||
expect(tool?.inputSchema.type).toBe('object');
|
||||
expect(tool?.inputSchema.properties).toBeDefined();
|
||||
expect(tool?.inputSchema.required).toBeUndefined(); // All parameters are optional
|
||||
});
|
||||
|
||||
it('should have category parameter with proper schema', () => {
|
||||
const categoryProp = tool?.inputSchema.properties?.category;
|
||||
expect(categoryProp).toBeDefined();
|
||||
expect(categoryProp.type).toBe('string');
|
||||
expect(categoryProp.description).toContain('category');
|
||||
});
|
||||
|
||||
it('should have complexity parameter with enum values', () => {
|
||||
const complexityProp = tool?.inputSchema.properties?.complexity;
|
||||
expect(complexityProp).toBeDefined();
|
||||
expect(complexityProp.enum).toEqual(['simple', 'medium', 'complex']);
|
||||
expect(complexityProp.description).toContain('complexity');
|
||||
});
|
||||
|
||||
it('should have time-based parameters with numeric constraints', () => {
|
||||
const maxTimeProp = tool?.inputSchema.properties?.maxSetupMinutes;
|
||||
const minTimeProp = tool?.inputSchema.properties?.minSetupMinutes;
|
||||
|
||||
expect(maxTimeProp).toBeDefined();
|
||||
expect(maxTimeProp.type).toBe('number');
|
||||
expect(maxTimeProp.maximum).toBe(480);
|
||||
expect(maxTimeProp.minimum).toBe(5);
|
||||
|
||||
expect(minTimeProp).toBeDefined();
|
||||
expect(minTimeProp.type).toBe('number');
|
||||
expect(minTimeProp.maximum).toBe(480);
|
||||
expect(minTimeProp.minimum).toBe(5);
|
||||
});
|
||||
|
||||
it('should have service and audience parameters', () => {
|
||||
const serviceProp = tool?.inputSchema.properties?.requiredService;
|
||||
const audienceProp = tool?.inputSchema.properties?.targetAudience;
|
||||
|
||||
expect(serviceProp).toBeDefined();
|
||||
expect(serviceProp.type).toBe('string');
|
||||
expect(serviceProp.description).toContain('service');
|
||||
|
||||
expect(audienceProp).toBeDefined();
|
||||
expect(audienceProp.type).toBe('string');
|
||||
expect(audienceProp.description).toContain('audience');
|
||||
it('should support metadata filtering via by_metadata searchMode', () => {
|
||||
// These properties are for by_metadata searchMode
|
||||
const props = tool?.inputSchema.properties;
|
||||
expect(props).toHaveProperty('category');
|
||||
expect(props).toHaveProperty('complexity');
|
||||
expect(props?.complexity?.enum).toEqual(['simple', 'medium', 'complex']);
|
||||
});
|
||||
|
||||
it('should have pagination parameters', () => {
|
||||
const limitProp = tool?.inputSchema.properties?.limit;
|
||||
const offsetProp = tool?.inputSchema.properties?.offset;
|
||||
|
||||
|
||||
expect(limitProp).toBeDefined();
|
||||
expect(limitProp.type).toBe('number');
|
||||
expect(limitProp.default).toBe(20);
|
||||
expect(limitProp.maximum).toBe(100);
|
||||
expect(limitProp.minimum).toBe(1);
|
||||
|
||||
|
||||
expect(offsetProp).toBeDefined();
|
||||
expect(offsetProp.type).toBe('number');
|
||||
expect(offsetProp.default).toBe(0);
|
||||
expect(offsetProp.minimum).toBe(0);
|
||||
});
|
||||
|
||||
it('should include all expected properties', () => {
|
||||
it('should include all search mode-specific properties', () => {
|
||||
const properties = Object.keys(tool?.inputSchema.properties || {});
|
||||
// Consolidated tool includes properties from all former tools
|
||||
const expectedProperties = [
|
||||
'category',
|
||||
'complexity',
|
||||
'maxSetupMinutes',
|
||||
'minSetupMinutes',
|
||||
'requiredService',
|
||||
'targetAudience',
|
||||
'searchMode', // New mode selector
|
||||
'query', // For keyword search
|
||||
'nodeTypes', // For by_nodes search (formerly list_node_templates)
|
||||
'task', // For by_task search (formerly get_templates_for_task)
|
||||
'category', // For by_metadata search
|
||||
'complexity',
|
||||
'limit',
|
||||
'offset'
|
||||
];
|
||||
|
||||
|
||||
expectedProperties.forEach(prop => {
|
||||
expect(properties).toContain(prop);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have appropriate additionalProperties setting', () => {
|
||||
expect(tool?.inputSchema.additionalProperties).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enhanced pagination support', () => {
|
||||
const paginatedTools = ['list_node_templates', 'search_templates', 'get_templates_for_task', 'search_templates_by_metadata'];
|
||||
|
||||
paginatedTools.forEach(toolName => {
|
||||
describe(toolName, () => {
|
||||
const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
|
||||
|
||||
it('should support limit parameter', () => {
|
||||
expect(tool?.inputSchema.properties).toHaveProperty('limit');
|
||||
const limitParam = tool?.inputSchema.properties.limit;
|
||||
expect(limitParam.type).toBe('number');
|
||||
expect(limitParam.minimum).toBeGreaterThanOrEqual(1);
|
||||
expect(limitParam.maximum).toBeGreaterThanOrEqual(50);
|
||||
});
|
||||
|
||||
it('should support offset parameter', () => {
|
||||
expect(tool?.inputSchema.properties).toHaveProperty('offset');
|
||||
const offsetParam = tool?.inputSchema.properties.offset;
|
||||
expect(offsetParam.type).toBe('number');
|
||||
expect(offsetParam.minimum).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -403,45 +403,47 @@ describe('n8n-validation', () => {
|
||||
settings: {
|
||||
executionOrder: 'v1' as const,
|
||||
saveDataSuccessExecution: 'none' as const,
|
||||
callerPolicy: 'workflowsFromSameOwner' as const, // Filtered out (not in OpenAPI spec)
|
||||
callerPolicy: 'workflowsFromSameOwner' as const, // Now whitelisted (n8n 1.121+)
|
||||
timeSavedPerExecution: 5, // Filtered out (UI-only property)
|
||||
},
|
||||
} as any;
|
||||
|
||||
const cleaned = cleanWorkflowForUpdate(workflow);
|
||||
|
||||
// Unsafe properties filtered out, safe properties kept
|
||||
// Unsafe properties filtered out, safe properties kept (callerPolicy now whitelisted)
|
||||
expect(cleaned.settings).toEqual({
|
||||
executionOrder: 'v1',
|
||||
saveDataSuccessExecution: 'none'
|
||||
saveDataSuccessExecution: 'none',
|
||||
callerPolicy: 'workflowsFromSameOwner'
|
||||
});
|
||||
expect(cleaned.settings).not.toHaveProperty('callerPolicy');
|
||||
expect(cleaned.settings).not.toHaveProperty('timeSavedPerExecution');
|
||||
});
|
||||
|
||||
it('should filter out callerPolicy (Issue #248 - API limitation)', () => {
|
||||
it('should preserve callerPolicy and availableInMCP (n8n 1.121+ settings)', () => {
|
||||
const workflow = {
|
||||
name: 'Test Workflow',
|
||||
nodes: [],
|
||||
connections: {},
|
||||
settings: {
|
||||
executionOrder: 'v1' as const,
|
||||
callerPolicy: 'workflowsFromSameOwner' as const, // Filtered out
|
||||
callerPolicy: 'workflowsFromSameOwner' as const, // Now whitelisted
|
||||
availableInMCP: true, // New in n8n 1.121
|
||||
errorWorkflow: 'N2O2nZy3aUiBRGFN',
|
||||
},
|
||||
} as any;
|
||||
|
||||
const cleaned = cleanWorkflowForUpdate(workflow);
|
||||
|
||||
// callerPolicy filtered out (causes API errors), safe properties kept
|
||||
// callerPolicy and availableInMCP now whitelisted (n8n 1.121+)
|
||||
expect(cleaned.settings).toEqual({
|
||||
executionOrder: 'v1',
|
||||
callerPolicy: 'workflowsFromSameOwner',
|
||||
availableInMCP: true,
|
||||
errorWorkflow: 'N2O2nZy3aUiBRGFN'
|
||||
});
|
||||
expect(cleaned.settings).not.toHaveProperty('callerPolicy');
|
||||
});
|
||||
|
||||
it('should filter all settings properties correctly (Issue #248 - API design)', () => {
|
||||
it('should preserve all whitelisted settings properties including callerPolicy (Issue #248 - updated for n8n 1.121)', () => {
|
||||
const workflow = {
|
||||
name: 'Test Workflow',
|
||||
nodes: [],
|
||||
@@ -455,14 +457,14 @@ describe('n8n-validation', () => {
|
||||
saveExecutionProgress: false,
|
||||
executionTimeout: 300,
|
||||
errorWorkflow: 'error-workflow-id',
|
||||
callerPolicy: 'workflowsFromAList' as const, // Filtered out (not in OpenAPI spec)
|
||||
callerPolicy: 'workflowsFromAList' as const, // Now whitelisted (n8n 1.121+)
|
||||
availableInMCP: false, // New in n8n 1.121
|
||||
},
|
||||
} as any;
|
||||
|
||||
const cleaned = cleanWorkflowForUpdate(workflow);
|
||||
|
||||
// Safe properties kept, unsafe properties filtered out
|
||||
// See: https://community.n8n.io/t/api-workflow-update-endpoint-doesnt-support-setting-callerpolicy/161916
|
||||
// All whitelisted properties kept including callerPolicy and availableInMCP
|
||||
expect(cleaned.settings).toEqual({
|
||||
executionOrder: 'v0',
|
||||
timezone: 'UTC',
|
||||
@@ -471,9 +473,10 @@ describe('n8n-validation', () => {
|
||||
saveManualExecutions: false,
|
||||
saveExecutionProgress: false,
|
||||
executionTimeout: 300,
|
||||
errorWorkflow: 'error-workflow-id'
|
||||
errorWorkflow: 'error-workflow-id',
|
||||
callerPolicy: 'workflowsFromAList',
|
||||
availableInMCP: false
|
||||
});
|
||||
expect(cleaned.settings).not.toHaveProperty('callerPolicy');
|
||||
});
|
||||
|
||||
it('should handle workflows without settings gracefully', () => {
|
||||
@@ -494,7 +497,6 @@ describe('n8n-validation', () => {
|
||||
nodes: [],
|
||||
connections: {},
|
||||
settings: {
|
||||
callerPolicy: 'workflowsFromSameOwner' as const, // Filtered out
|
||||
timeSavedPerExecution: 5, // Filtered out (UI-only)
|
||||
someOtherProperty: 'value', // Filtered out
|
||||
},
|
||||
@@ -514,19 +516,19 @@ describe('n8n-validation', () => {
|
||||
connections: {},
|
||||
settings: {
|
||||
executionOrder: 'v1' as const, // Whitelisted
|
||||
callerPolicy: 'workflowsFromSameOwner' as const, // Filtered out
|
||||
callerPolicy: 'workflowsFromSameOwner' as const, // Now whitelisted (n8n 1.121+)
|
||||
timezone: 'America/New_York', // Whitelisted
|
||||
someOtherProperty: 'value', // Filtered out
|
||||
},
|
||||
} as any;
|
||||
|
||||
const cleaned = cleanWorkflowForUpdate(workflow);
|
||||
// Should keep only whitelisted properties
|
||||
// Should keep only whitelisted properties (callerPolicy now whitelisted)
|
||||
expect(cleaned.settings).toEqual({
|
||||
executionOrder: 'v1',
|
||||
callerPolicy: 'workflowsFromSameOwner',
|
||||
timezone: 'America/New_York'
|
||||
});
|
||||
expect(cleaned.settings).not.toHaveProperty('callerPolicy');
|
||||
expect(cleaned.settings).not.toHaveProperty('someOtherProperty');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user