Compare commits

..

1 Commits

Author SHA1 Message Date
czlonkowski
c7da0a2430 fix: resolve YAML syntax error in release.yml workflow
Fixed invalid multi-line string syntax at line 148 by converting to heredoc.
The quoted multi-line string was breaking YAML parsing. Using heredoc (cat <<EOF)
is the proper way to handle multi-line strings in bash within GitHub Actions.

This resolves the CI failure on main branch.

Concieved by Romuald Członkowski - www.aiadvisors.pl/en

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 13:45:53 +02:00
28 changed files with 1581 additions and 2073 deletions

View File

@@ -142,13 +142,19 @@ jobs:
if [ -z "$PREVIOUS_TAG" ]; then
echo " No previous tag found, this might be the first release"
# Generate initial release notes using script
if NOTES=$(node scripts/generate-initial-release-notes.js "$CURRENT_VERSION" 2>/dev/null); then
echo "✅ Successfully generated initial release notes for version $CURRENT_VERSION"
else
echo "⚠️ Could not generate initial release notes for version $CURRENT_VERSION"
NOTES="Initial release v$CURRENT_VERSION"
fi
# Get all commits up to current commit - use heredoc for multiline
NOTES=$(cat <<EOF
### 🎉 Initial Release
This is the initial release of n8n-mcp v$CURRENT_VERSION.
---
**Release Statistics:**
- Commit count: $(git rev-list --count HEAD)
- First release setup
EOF
)
echo "has-notes=true" >> $GITHUB_OUTPUT

View File

@@ -7,381 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [2.22.7] - 2025-10-26
### 📝 Documentation Fixes
**Issue #292: Corrected Array Property Removal Documentation in n8n_update_partial_workflow**
Fixed critical documentation error in property removal patterns that could have led users to write non-functional code.
#### Problem
The documentation incorrectly showed using array index notation `[0]` for removing array elements:
```javascript
// INCORRECT (doesn't work as shown)
updates: { "parameters.headers[0]": undefined }
```
**Root Cause**: The `setNestedProperty` implementation doesn't parse array index notation like `[0]`. It treats `headers[0]` as a literal object key, not an array index.
**Impact**: Users following the documentation would write code that doesn't behave as expected. The property `headers[0]` would be treated as an object key, not an array element reference.
#### Fixed
**Three documentation corrections in `src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts`:**
1. **Nested Property Removal Section** (lines 236-244):
- Changed comment from `// Remove array element` to `// Remove entire array property`
- Changed code from `"parameters.headers[0]": undefined` to `"parameters.headers": undefined`
2. **Example rm5** (line 340):
- Changed comment from `// Remove array element` to `// Remove entire array property`
- Changed code from `"parameters.headers[0]": undefined` to `"parameters.headers": undefined`
3. **Pitfalls Section** (line 405):
- OLD: `'Array element removal with undefined removes the element at that index, which may shift subsequent indices'`
- NEW: `'Array index notation (e.g., "parameters.headers[0]") is not supported - remove the entire array property instead'`
#### Correct Usage
**To remove an array property:**
```javascript
// Correct: Remove entire array
n8n_update_partial_workflow({
id: "wf_012",
operations: [{
type: "updateNode",
nodeName: "HTTP Request",
updates: { "parameters.headers": undefined } // Remove entire headers array
}]
});
```
**NOT:**
```javascript
// Incorrect: Array index notation doesn't work
updates: { "parameters.headers[0]": undefined } // Treated as object key "headers[0]"
```
#### Impact
- **Prevents User Confusion**: Clear documentation on what works vs. what doesn't
- **Accurate Examples**: All examples now show correct, working patterns
- **Better Error Prevention**: Pitfall warning helps users avoid this mistake
- **No Code Changes**: This is purely a documentation fix - no implementation changes needed
#### Testing
- ✅ Documentation reviewed by code-reviewer agent
- ✅ Tested by n8n-mcp-tester agent
- ✅ All examples verified against actual implementation behavior
- ✅ Pitfall accurately describes technical limitation
#### Files Changed
**Documentation (1 file)**:
- `src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts` - Corrected 3 instances of array property removal documentation
**Configuration (2 files)**:
- `package.json` - Version bump to 2.22.7
- `package.runtime.json` - Version bump to 2.22.7
#### Related
- **Issue**: #292 - Missing documentation on how to remove node properties using `updateNode`
- **PR**: #375 - Resolve GitHub Issue 292 in n8n-mcp
- **Code Review**: Identified critical array index notation documentation error
- **Root Cause**: Implementation doesn't parse array bracket notation `[N]`
Conceived by Romuald Członkowski - [www.aiadvisors.pl/en](https://www.aiadvisors.pl/en)
## [2.22.6] - 2025-10-25
### 🐛 Bug Fixes
**Issue #228: Fix Docker Port Configuration Mismatch**
Fixed critical Docker configuration bug where custom PORT environment variable values were not properly mapped to container ports, causing connection failures in Docker deployments.
#### Problem
- **docker-compose.yml**: Port mapping `"${PORT:-3000}:3000"` hardcoded container port to 3000
- **docker-compose.yml**: Health check hardcoded to port 3000
- **Dockerfile**: Health check hardcoded to port 3000
- Impact: When PORT≠3000 (e.g., PORT=8080), Docker mapped host port to wrong container port
#### Solution
- **docker-compose.yml line 44**: Changed port mapping to `"${PORT:-3000}:${PORT:-3000}"`
- **docker-compose.yml line 56**: Updated health check to use dynamic port `$${PORT:-3000}`
- **Dockerfile line 93**: Updated HEALTHCHECK to use dynamic port `${PORT:-3000}`
- **Dockerfile line 85**: Added clarifying comment about PORT configurability
#### Testing
- Verified with default PORT (3000)
- Verified with custom PORT (8080)
- Health checks work correctly in both scenarios
#### Related Issues
- Fixes #228 (Docker Compose port error)
- Likely fixes #109 (Configuration ignored in HTTP mode)
- Likely fixes #84 (Can't access container)
Conceived by Romuald Członkowski - [www.aiadvisors.pl/en](https://www.aiadvisors.pl/en)
## [2.22.3] - 2025-10-25
### 🔧 Code Quality Improvements
**Issue #349: Refactor n8n API Response Validation (PR #367)**
Improved code maintainability and added comprehensive test coverage for defensive response validation added in PR #367.
#### Refactoring
**1. Eliminated DRY Violation**
- Extracted duplicated validation logic into `validateListResponse<T>()` helper method
- Reduced code duplication from 88 lines to single reusable function
- Impact: 75% reduction in validation code, easier maintenance
**2. Enhanced Error Handling**
- Consistent error message format across all list operations
- Limited error message verbosity (max 5 keys shown to prevent information exposure)
- Added security protection against data structure exposure
- Better error messages: `got object with keys: [data, items, total, hasMore, meta]`
**3. Improved Documentation**
- Added JSDoc comments explaining backwards compatibility
- Documented modern vs legacy response formats
- Referenced issue #349 for context
#### Testing
**Added Comprehensive Unit Tests** (29 new test cases)
- Legacy array format wrapping for all 4 methods
- Null/undefined response handling
- Primitive type rejection (string, number, boolean)
- Invalid structure detection
- Non-array data field validation
- Error message truncation with many keys
- 100% coverage of new validation logic
**Test Coverage Results**:
- Before: 0% coverage of validation scenarios
- After: 100% coverage (29/29 scenarios tested)
- All validation paths exercised and verified
#### Impact
**Code Quality**:
- ✅ DRY principle restored (no duplication)
- ✅ Type safety improved with generics
- ✅ Consistent error handling across all methods
- ✅ Well-documented backwards compatibility
**Maintainability**:
- ✅ Single source of truth for validation logic
- ✅ Future bug fixes apply to all methods automatically
- ✅ Easier to understand and modify
**Security**:
- ✅ Limited information exposure in error messages
- ✅ Protection against verbose error logs
**Testing**:
- ✅ Full test coverage prevents regressions
- ✅ All edge cases validated
- ✅ Backwards compatibility verified
#### Files Modified
**Code (1 file)**:
- `src/services/n8n-api-client.ts`
- Added `validateListResponse<T>()` private helper method (44 lines)
- Refactored listWorkflows, listExecutions, listCredentials, listTags (reduced from ~100 lines to ~20 lines)
- Added JSDoc documentation to all 4 list methods
- Net reduction: ~80 lines of code
**Tests (1 file)**:
- `tests/unit/services/n8n-api-client.test.ts`
- Added 29 comprehensive validation test cases (237 lines)
- Coverage for all 4 list methods
- Tests for legacy format, null responses, invalid structures, key truncation
**Configuration (1 file)**:
- `package.json` - Version bump to 2.22.3
#### Technical Details
**Helper Method Signature**:
```typescript
private validateListResponse<T>(
responseData: any,
resourceType: string
): { data: T[]; nextCursor?: string | null }
```
**Error Message Example**:
```
Invalid response from n8n API for workflows: expected {data: [], nextCursor?: string},
got object with keys: [items, total, hasMore, page, limit]...
```
**Usage Example**:
```typescript
async listWorkflows(params: WorkflowListParams = {}): Promise<WorkflowListResponse> {
try {
const response = await this.client.get('/workflows', { params });
return this.validateListResponse<Workflow>(response.data, 'workflows');
} catch (error) {
throw handleN8nApiError(error);
}
}
```
#### Related
- **Issue**: #349 - Response validation for n8n API list operations
- **PR**: #367 - Add defensive response validation (original implementation)
- **Code Review**: Identified DRY violation and missing test coverage
- **Testing**: Validated by n8n-mcp-tester agent
- **Analysis**: Both agents confirmed functional correctness, recommended refactoring
Conceived by Romuald Członkowski - [www.aiadvisors.pl/en](https://www.aiadvisors.pl/en)
---
### ✨ Enhancements
**Issue #361: Enhanced HTTP Request Node Validation Suggestions**
Added helpful suggestions for HTTP Request node best practices to prevent common production issues discovered through real-world workflow analysis.
#### What's New
1. **alwaysOutputData Suggestion**
- Suggests adding `alwaysOutputData: true` at node level (not in parameters)
- Prevents silent workflow failures when HTTP requests error
- Ensures downstream error handling can process failed requests
- Example suggestion: "Consider adding alwaysOutputData: true at node level for better error handling. This ensures the node produces output even when HTTP requests fail, allowing downstream error handling."
2. **responseFormat Suggestion for API Endpoints**
- Suggests setting `options.response.response.responseFormat` for API endpoints
- Prevents JSON parsing confusion
- Triggered when URL contains `/api`, `/rest`, `supabase`, `firebase`, `googleapis`, or `.com/v` patterns
- Example suggestion: "API endpoints should explicitly set options.response.response.responseFormat to 'json' or 'text' to prevent confusion about response parsing"
3. **Enhanced URL Protocol Validation**
- Detects missing protocol in expression-based URLs
- Warns about patterns like `=www.{{ $json.domain }}.com` (missing http://)
- Warns about expressions without protocol: `={{ $json.domain }}/api/data`
- Example warning: "URL expression appears to be missing http:// or https:// protocol"
#### Investigation Findings
This enhancement was developed after thorough investigation of issue #361:
**Key Discoveries:**
- ✅ Mixed expression syntax `=literal{{ expression }}` **actually works in n8n** - the issue report's primary claim was incorrect
- ✅ Real validation gaps identified: missing `alwaysOutputData` and `responseFormat` checks
- ✅ Workflow analysis showed "?" icon in UI caused by missing required URL (already caught by validation)
- ✅ Compared broken vs fixed workflows to identify actual production issues
**Testing Evidence:**
- Analyzed workflow SwjKJsJhe8OsYfBk with mixed syntax - executions successful
- Compared broken workflow (mBmkyj460i5rYTG4) with fixed workflow (hQI9pby3nSFtk4TV)
- Identified that fixed workflow has `alwaysOutputData: true` and explicit `responseFormat: "json"`
#### Impact
- **Non-Breaking**: All changes are suggestions/warnings, not errors
- **Profile-Aware**: Suggestions shown in all profiles for maximum helpfulness
- **Actionable**: Clear guidance on how to implement best practices
- **Production-Focused**: Addresses real workflow reliability concerns from actual broken workflows
#### Test Coverage
Added 8 new test cases covering:
- alwaysOutputData suggestion for all HTTP Request nodes
- responseFormat suggestion for API endpoint detection (various patterns)
- responseFormat NOT suggested when already configured
- URL protocol validation for expression-based URLs
- Protocol warnings for missing http:// in expressions
- No false positives when protocol is correctly included
#### Technical Details
**Files Modified:**
- `src/services/enhanced-config-validator.ts` - Added `enhanceHttpRequestValidation()` implementation
- `tests/unit/services/enhanced-config-validator.test.ts` - Added 8 comprehensive test cases
**Validation Flow:**
1. Check for alwaysOutputData suggestion (all HTTP Request nodes)
2. Detect API endpoints by URL patterns
3. Check for explicit responseFormat configuration
4. Validate expression-based URLs for protocol issues
#### Related
- **Issue**: #361 - validate_node_operation: Missing critical HTTP Request node configuration checks
- **Analysis**: Deep investigation with @agent-Explore and @agent-n8n-mcp-tester
- **Workflows Analyzed**:
- SwjKJsJhe8OsYfBk (mixed syntax test)
- mBmkyj460i5rYTG4 (broken workflow)
- hQI9pby3nSFtk4TV (fixed workflow)
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
---
### 🐛 Bug Fixes
**Issue #360: Enhanced Warnings for If/Switch Node Connection Parameters**
Fixed issue where users could unintentionally place multiple If node connections on the same branch (TRUE/FALSE) when using `sourceIndex` parameter instead of the recommended `branch` parameter. The system now provides helpful warnings to guide users toward better practices.
#### What Was Fixed
1. **New Warning System**:
- Warns when using `sourceIndex` with If nodes - suggests `branch="true"` or `branch="false"` instead
- Warns when using `sourceIndex` with Switch nodes - suggests `case=N` instead
- Explains the correct branch structure: `main[0]=TRUE branch, main[1]=FALSE branch`
2. **Enhanced Documentation**:
- Added **CRITICAL** pitfalls to `n8n_update_partial_workflow` tool documentation
- Clear guidance that using `sourceIndex=0` for multiple connections puts them ALL on the TRUE branch
- Examples showing correct vs. incorrect usage
3. **Type System Improvements**:
- Added `warnings` field to `WorkflowDiffResult` interface
- Warnings are non-blocking (operations still succeed)
- Differentiated from errors for better UX
#### Behavior
The existing `branch` parameter works correctly and has comprehensive test coverage:
- `branch="true"` → routes to `main[0]` (TRUE path)
- `branch="false"` → routes to `main[1]` (FALSE path)
The issue was that users who didn't know about the `branch` parameter would naturally use `sourceIndex`, which led to incorrect branch routing.
#### Example Warning
```
Connection to If node "Check Condition" uses sourceIndex=0.
Consider using branch="true" or branch="false" for better clarity.
If node outputs: main[0]=TRUE branch, main[1]=FALSE branch.
```
#### Test Coverage
- Added regression tests that reproduce the exact issue from #360
- Verify warnings are generated for If and Switch nodes
- Confirm existing smart parameter tests still pass
**Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en**
---
### ✨ New Features
**Auto-Update Node Versions with Smart Migration**

View File

@@ -82,7 +82,7 @@ ENV IS_DOCKER=true
# To opt-out, uncomment the following line:
# ENV N8N_MCP_TELEMETRY_DISABLED=true
# Expose HTTP port (default 3000, configurable via PORT environment variable at runtime)
# Expose HTTP port
EXPOSE 3000
# Set stop signal to SIGTERM (default, but explicit is better)
@@ -90,7 +90,7 @@ STOPSIGNAL SIGTERM
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD sh -c 'curl -f http://127.0.0.1:${PORT:-3000}/health || exit 1'
CMD curl -f http://127.0.0.1:3000/health || exit 1
# Optimized entrypoint
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

View File

@@ -5,23 +5,23 @@
[![npm version](https://img.shields.io/npm/v/n8n-mcp.svg)](https://www.npmjs.com/package/n8n-mcp)
[![codecov](https://codecov.io/gh/czlonkowski/n8n-mcp/graph/badge.svg?token=YOUR_TOKEN)](https://codecov.io/gh/czlonkowski/n8n-mcp)
[![Tests](https://img.shields.io/badge/tests-3336%20passing-brightgreen.svg)](https://github.com/czlonkowski/n8n-mcp/actions)
[![n8n version](https://img.shields.io/badge/n8n-1.117.2-orange.svg)](https://github.com/n8n-io/n8n)
[![n8n version](https://img.shields.io/badge/n8n-^1.116.2-orange.svg)](https://github.com/n8n-io/n8n)
[![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fczlonkowski%2Fn8n--mcp-green.svg)](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp)
[![Deploy on Railway](https://railway.com/button.svg)](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 541 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 525+ workflow automation nodes.
## Overview
n8n-MCP serves as a bridge between n8n's workflow automation platform and AI models, enabling them to understand and work with n8n nodes effectively. It provides structured access to:
- 📚 **541 n8n nodes** from both n8n-nodes-base and @n8n/n8n-nodes-langchain
- 📚 **536 n8n nodes** from both n8n-nodes-base and @n8n/n8n-nodes-langchain
- 🔧 **Node properties** - 99% coverage with detailed schemas
-**Node operations** - 63.6% coverage of available actions
- 📄 **Documentation** - 87% coverage from official n8n docs (including AI nodes)
- 🤖 **AI tools** - 271 AI-capable nodes detected with full documentation
- 📄 **Documentation** - 90% coverage from official n8n docs (including AI nodes)
- 🤖 **AI tools** - 263 AI-capable nodes detected with full documentation
- 💡 **Real-world examples** - 2,646 pre-extracted configurations from popular templates
- 🎯 **Template library** - 2,709 workflow templates with 100% metadata coverage
- 🎯 **Template library** - 2,500+ workflow templates with smart filtering
## ⚠️ Important Safety Warning
@@ -51,8 +51,6 @@ npx n8n-mcp
Add to Claude Desktop config:
> ⚠️ **Important**: The `MCP_MODE: "stdio"` environment variable is **required** for Claude Desktop. Without it, you will see JSON parsing errors like `"Unexpected token..."` in the UI. This variable ensures that only JSON-RPC messages are sent to stdout, preventing debug logs from interfering with the protocol.
**Basic configuration (documentation tools only):**
```json
{
@@ -533,7 +531,7 @@ When operations are independent, execute them in parallel for maximum performanc
❌ BAD: Sequential tool calls (await each one before the next)
### 3. Templates First
ALWAYS check templates before building from scratch (2,709 available).
ALWAYS check templates before building from scratch (2,500+ available).
### 4. Multi-Level Validation
Use validate_node_minimal → validate_node_operation → validate_workflow pattern.
@@ -842,7 +840,7 @@ n8n_update_partial_workflow({
### Core Behavior
1. **Silent execution** - No commentary between tools
2. **Parallel by default** - Execute independent operations simultaneously
3. **Templates first** - Always check before building (2,709 available)
3. **Templates first** - Always check before building (2,500+ available)
4. **Multi-level validation** - Quick check → Full validation → Workflow validation
5. **Never trust defaults** - Explicitly configure ALL parameters
@@ -945,7 +943,7 @@ Once connected, Claude can use these powerful tools:
- **`get_node_as_tool_info`** - Get guidance on using any node as an AI tool
### Template Tools
- **`list_templates`** - Browse all templates with descriptions and optional metadata (2,709 templates)
- **`list_templates`** - Browse all templates with descriptions and optional metadata (2,500+ 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
@@ -1100,17 +1098,17 @@ npm run dev:http # HTTP dev mode
## 📊 Metrics & Coverage
Current database coverage (n8n v1.117.2):
Current database coverage (n8n v1.113.3):
- ✅ **541/541** nodes loaded (100%)
- ✅ **541** nodes with properties (100%)
- ✅ **470** nodes with documentation (87%)
- ✅ **271** AI-capable tools detected
- ✅ **536/536** nodes loaded (100%)
- ✅ **528** nodes with properties (98.7%)
- ✅ **470** nodes with documentation (88%)
- ✅ **267** AI-capable tools detected
- ✅ **2,646** pre-extracted template configurations
- ✅ **2,709** workflow templates available (100% metadata coverage)
- ✅ **2,500+** workflow templates available
- ✅ **AI Agent & LangChain nodes** fully documented
- ⚡ **Average response time**: ~12ms
- 💾 **Database size**: ~68MB (includes templates with metadata)
- 💾 **Database size**: ~15MB (optimized)
## 🔄 Recent Updates

Binary file not shown.

View File

@@ -20,19 +20,19 @@ services:
image: n8n-mcp:latest
container_name: n8n-mcp
ports:
- "${PORT:-3000}:${PORT:-3000}"
- "3000:3000"
environment:
- MCP_MODE=${MCP_MODE:-http}
- AUTH_TOKEN=${AUTH_TOKEN}
- NODE_ENV=${NODE_ENV:-production}
- LOG_LEVEL=${LOG_LEVEL:-info}
- PORT=${PORT:-3000}
- PORT=3000
volumes:
# Mount data directory for persistence
- ./data:/app/data
restart: unless-stopped
healthcheck:
test: ["CMD", "sh", "-c", "curl -f http://localhost:$${PORT:-3000}/health"]
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -37,12 +37,11 @@ services:
container_name: n8n-mcp
restart: unless-stopped
ports:
- "${MCP_PORT:-3000}:${MCP_PORT:-3000}"
- "${MCP_PORT:-3000}:3000"
environment:
- NODE_ENV=production
- N8N_MODE=true
- MCP_MODE=http
- PORT=${MCP_PORT:-3000}
- N8N_API_URL=http://n8n:5678
- N8N_API_KEY=${N8N_API_KEY}
- MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN}
@@ -57,7 +56,7 @@ services:
n8n:
condition: service_healthy
healthcheck:
test: ["CMD", "sh", "-c", "curl -f http://localhost:$${MCP_PORT:-3000}/health"]
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -41,7 +41,7 @@ services:
# Port mapping
ports:
- "${PORT:-3000}:${PORT:-3000}"
- "${PORT:-3000}:3000"
# Resource limits
deploy:
@@ -53,7 +53,7 @@ services:
# Health check
healthcheck:
test: ["CMD", "sh", "-c", "curl -f http://127.0.0.1:$${PORT:-3000}/health"]
test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -4,9 +4,7 @@ Connect n8n-MCP to Claude Code CLI for enhanced n8n workflow development from th
## Quick Setup via CLI
### Basic configuration (documentation tools only)
**For Linux, macOS, or Windows (WSL/Git Bash):**
### Basic configuration (documentation tools only):
```bash
claude mcp add n8n-mcp \
-e MCP_MODE=stdio \
@@ -15,21 +13,9 @@ claude mcp add n8n-mcp \
-- npx n8n-mcp
```
**For native Windows PowerShell:**
```powershell
# Note: The backtick ` is PowerShell's line continuation character.
claude mcp add n8n-mcp `
'-e MCP_MODE=stdio' `
'-e LOG_LEVEL=error' `
'-e DISABLE_CONSOLE_OUTPUT=true' `
-- npx n8n-mcp
```
![Adding n8n-MCP server in Claude Code](./img/cc_command.png)
### Full configuration (with n8n management tools)
**For Linux, macOS, or Windows (WSL/Git Bash):**
### Full configuration (with n8n management tools):
```bash
claude mcp add n8n-mcp \
-e MCP_MODE=stdio \
@@ -40,18 +26,6 @@ claude mcp add n8n-mcp \
-- npx n8n-mcp
```
**For native Windows PowerShell:**
```powershell
# Note: The backtick ` is PowerShell's line continuation character.
claude mcp add n8n-mcp `
'-e MCP_MODE=stdio' `
'-e LOG_LEVEL=error' `
'-e DISABLE_CONSOLE_OUTPUT=true' `
'-e N8N_API_URL=https://your-n8n-instance.com' `
'-e N8N_API_KEY=your-api-key' `
-- npx n8n-mcp
```
Make sure to replace `https://your-n8n-instance.com` with your actual n8n URL and `your-api-key` with your n8n API key.
## Alternative Setup Methods
@@ -159,11 +133,9 @@ For optimal results, create a `CLAUDE.md` file in your project root with the ins
## Tips
- If you're running n8n locally, use `http://localhost:5678` as the `N8N_API_URL`.
- The n8n API credentials are optional. Without them, you'll only have access to documentation and validation tools. With credentials, you get full workflow management capabilities.
- **Scope Management:**
- By default, `claude mcp add` uses `--scope local` (also called "user scope"), which saves the configuration to your global user settings and keeps API keys private.
- To share the configuration with your team, use `--scope project`. This saves the configuration to a `.mcp.json` file in your project's root directory.
- **Switching Scope:** The cleanest method is to `remove` the server and then `add` it back with the desired scope flag (e.g., `claude mcp remove n8n-mcp` followed by `claude mcp add n8n-mcp --scope project`).
- **Manual Switching (Advanced):** You can manually edit your `.claude.json` file (e.g., `C:\Users\YourName\.claude.json`). To switch, cut the `"n8n-mcp": { ... }` block from the top-level `"mcpServers"` object (user scope) and paste it into the nested `"mcpServers"` object under your project's path key (project scope), or vice versa. **Important:** You may need to restart Claude Code for manual changes to take effect.
- Claude Code will automatically start the MCP server when you begin a conversation.
- If you're running n8n locally, use `http://localhost:5678` as the N8N_API_URL
- The n8n API credentials are optional - without them, you'll have documentation and validation tools only
- With API credentials, you'll get full workflow management capabilities
- Use `--scope local` (default) to keep your API credentials private
- Use `--scope project` to share configuration with your team (put credentials in environment variables)
- Claude Code will automatically start the MCP server when you begin a conversation

View File

@@ -59,10 +59,10 @@ docker compose up -d
- n8n-mcp-data:/app/data
ports:
- "${PORT:-3000}:${PORT:-3000}"
- "${PORT:-3000}:3000"
healthcheck:
test: ["CMD", "sh", "-c", "curl -f http://127.0.0.1:$${PORT:-3000}/health"]
test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 10s
retries: 3

1790
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp",
"version": "2.22.8",
"version": "2.22.0",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -140,15 +140,15 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.20.1",
"@n8n/n8n-nodes-langchain": "^1.116.2",
"@n8n/n8n-nodes-langchain": "^1.115.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.117.2",
"n8n-core": "^1.116.0",
"n8n-workflow": "^1.114.0",
"n8n": "^1.116.2",
"n8n-core": "^1.115.1",
"n8n-workflow": "^1.113.0",
"openai": "^4.77.0",
"sql.js": "^1.13.0",
"tslib": "^2.6.2",

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp-runtime",
"version": "2.22.8",
"version": "2.22.0",
"description": "n8n MCP Server Runtime Dependencies Only",
"private": true,
"dependencies": {

View File

@@ -1,45 +0,0 @@
#!/usr/bin/env node
/**
* Generate release notes for the initial release
* Used by GitHub Actions when no previous tag exists
*/
const { execSync } = require('child_process');
function generateInitialReleaseNotes(version) {
try {
// Get total commit count
const commitCount = execSync('git rev-list --count HEAD', { encoding: 'utf8' }).trim();
// Generate release notes
const releaseNotes = [
'### 🎉 Initial Release',
'',
`This is the initial release of n8n-mcp v${version}.`,
'',
'---',
'',
'**Release Statistics:**',
`- Commit count: ${commitCount}`,
'- First release setup'
];
return releaseNotes.join('\n');
} catch (error) {
console.error(`Error generating initial release notes: ${error.message}`);
return `Failed to generate initial release notes: ${error.message}`;
}
}
// Parse command line arguments
const version = process.argv[2];
if (!version) {
console.error('Usage: generate-initial-release-notes.js <version>');
process.exit(1);
}
const releaseNotes = generateInitialReleaseNotes(version);
console.log(releaseNotes);

View File

@@ -1,99 +0,0 @@
#!/usr/bin/env ts-node
import * as fs from 'fs';
import * as path from 'path';
import { createDatabaseAdapter } from '../src/database/database-adapter';
interface BatchResponse {
id: string;
custom_id: string;
response: {
status_code: number;
body: {
choices: Array<{
message: {
content: string;
};
}>;
};
};
error: any;
}
async function processBatchMetadata(batchFile: string) {
console.log(`📥 Processing batch file: ${batchFile}`);
// Read the JSONL file
const content = fs.readFileSync(batchFile, 'utf-8');
const lines = content.trim().split('\n');
console.log(`📊 Found ${lines.length} batch responses`);
// Initialize database
const db = await createDatabaseAdapter('./data/nodes.db');
let updated = 0;
let skipped = 0;
let errors = 0;
for (const line of lines) {
try {
const response: BatchResponse = JSON.parse(line);
// Extract template ID from custom_id (format: "template-9100")
const templateId = parseInt(response.custom_id.replace('template-', ''));
// Check for errors
if (response.error || response.response.status_code !== 200) {
console.warn(`⚠️ Template ${templateId}: API error`, response.error);
errors++;
continue;
}
// Extract metadata from response
const metadataJson = response.response.body.choices[0].message.content;
// Validate it's valid JSON
JSON.parse(metadataJson); // Will throw if invalid
// Update database
const stmt = db.prepare(`
UPDATE templates
SET metadata_json = ?
WHERE id = ?
`);
stmt.run(metadataJson, templateId);
updated++;
console.log(`✅ Template ${templateId}: Updated metadata`);
} catch (error: any) {
console.error(`❌ Error processing line:`, error.message);
errors++;
}
}
// Close database
if ('close' in db && typeof db.close === 'function') {
db.close();
}
console.log(`\n📈 Summary:`);
console.log(` - Updated: ${updated}`);
console.log(` - Skipped: ${skipped}`);
console.log(` - Errors: ${errors}`);
console.log(` - Total: ${lines.length}`);
}
// Main
const batchFile = process.argv[2] || '/Users/romualdczlonkowski/Pliki/n8n-mcp/n8n-mcp/docs/batch_68fff7242850819091cfed64f10fb6b4_output.jsonl';
processBatchMetadata(batchFile)
.then(() => {
console.log('\n✅ Batch processing complete!');
process.exit(0);
})
.catch((error) => {
console.error('\n❌ Batch processing failed:', error);
process.exit(1);
});

View File

@@ -138,7 +138,6 @@ export async function handleUpdatePartialWorkflow(
error: 'Failed to apply diff operations',
details: {
errors: diffResult.errors,
warnings: diffResult.warnings,
operationsApplied: diffResult.operationsApplied,
applied: diffResult.applied,
failed: diffResult.failed
@@ -155,9 +154,6 @@ export async function handleUpdatePartialWorkflow(
data: {
valid: true,
operationsToApply: input.operations.length
},
details: {
warnings: diffResult.warnings
}
};
}
@@ -256,8 +252,7 @@ export async function handleUpdatePartialWorkflow(
workflowName: updatedWorkflow.name,
applied: diffResult.applied,
failed: diffResult.failed,
errors: diffResult.errors,
warnings: diffResult.warnings
errors: diffResult.errors
}
};
} catch (error) {

View File

@@ -186,115 +186,7 @@ Please choose a different name.
- Simply rename nodes with updateNode - no manual connection operations needed
- Multiple renames in one call work atomically
- Can rename a node and add/remove connections using the new name in the same batch
- Use \`validateOnly: true\` to preview effects before applying
## Removing Properties with undefined
To remove a property from a node, set its value to \`undefined\` in the updates object. This is essential when migrating from deprecated properties or cleaning up optional configuration fields.
### Why Use undefined?
- **Property removal vs. null**: Setting a property to \`undefined\` removes it completely from the node object, while \`null\` sets the property to a null value
- **Validation constraints**: Some properties are mutually exclusive (e.g., \`continueOnFail\` and \`onError\`). Simply setting one without removing the other will fail validation
- **Deprecated property migration**: When n8n deprecates properties, you must remove the old property before the new one will work
### Basic Property Removal
\`\`\`javascript
// Remove error handling configuration
n8n_update_partial_workflow({
id: "wf_123",
operations: [{
type: "updateNode",
nodeName: "HTTP Request",
updates: { onError: undefined }
}]
});
// Remove disabled flag
n8n_update_partial_workflow({
id: "wf_456",
operations: [{
type: "updateNode",
nodeId: "node_abc",
updates: { disabled: undefined }
}]
});
\`\`\`
### Nested Property Removal
Use dot notation to remove nested properties:
\`\`\`javascript
// Remove nested parameter
n8n_update_partial_workflow({
id: "wf_789",
operations: [{
type: "updateNode",
nodeName: "API Request",
updates: { "parameters.authentication": undefined }
}]
});
// Remove entire array property
n8n_update_partial_workflow({
id: "wf_012",
operations: [{
type: "updateNode",
nodeName: "HTTP Request",
updates: { "parameters.headers": undefined }
}]
});
\`\`\`
### Migrating from Deprecated Properties
Common scenario: replacing \`continueOnFail\` with \`onError\`:
\`\`\`javascript
// WRONG: Setting only the new property leaves the old one
n8n_update_partial_workflow({
id: "wf_123",
operations: [{
type: "updateNode",
nodeName: "HTTP Request",
updates: { onError: "continueErrorOutput" }
}]
});
// Error: continueOnFail and onError are mutually exclusive
// CORRECT: Remove the old property first
n8n_update_partial_workflow({
id: "wf_123",
operations: [{
type: "updateNode",
nodeName: "HTTP Request",
updates: {
continueOnFail: undefined,
onError: "continueErrorOutput"
}
}]
});
\`\`\`
### Batch Property Removal
Remove multiple properties in one operation:
\`\`\`javascript
n8n_update_partial_workflow({
id: "wf_345",
operations: [{
type: "updateNode",
nodeName: "Data Processor",
updates: {
continueOnFail: undefined,
alwaysOutputData: undefined,
"parameters.legacy_option": undefined
}
}]
});
\`\`\`
### When to Use undefined
- Removing deprecated properties during migration
- Cleaning up optional configuration flags
- Resolving mutual exclusivity validation errors
- Removing stale or unnecessary node metadata
- Simplifying node configuration`,
- Use \`validateOnly: true\` to preview effects before applying`,
parameters: {
id: { type: 'string', required: true, description: 'Workflow ID to update' },
operations: {
@@ -331,13 +223,7 @@ n8n_update_partial_workflow({
'// Vector Store setup: Connect embeddings and documents\nn8n_update_partial_workflow({id: "ai7", operations: [\n {type: "addConnection", source: "Embeddings OpenAI", target: "Pinecone Vector Store", sourceOutput: "ai_embedding"},\n {type: "addConnection", source: "Default Data Loader", target: "Pinecone Vector Store", sourceOutput: "ai_document"}\n]})',
'// Connect Vector Store Tool to AI Agent (retrieval setup)\nn8n_update_partial_workflow({id: "ai8", operations: [\n {type: "addConnection", source: "Pinecone Vector Store", target: "Vector Store Tool", sourceOutput: "ai_vectorStore"},\n {type: "addConnection", source: "Vector Store Tool", target: "AI Agent", sourceOutput: "ai_tool"}\n]})',
'// Rewire AI Agent to use different language model\nn8n_update_partial_workflow({id: "ai9", operations: [{type: "rewireConnection", source: "AI Agent", from: "OpenAI Chat Model", to: "Anthropic Chat Model", sourceOutput: "ai_languageModel"}]})',
'// Replace all AI tools for an agent\nn8n_update_partial_workflow({id: "ai10", operations: [\n {type: "removeConnection", source: "Old Tool 1", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "removeConnection", source: "Old Tool 2", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "addConnection", source: "New HTTP Tool", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "addConnection", source: "New Code Tool", target: "AI Agent", sourceOutput: "ai_tool"}\n]})',
'\n// ============ REMOVING PROPERTIES EXAMPLES ============',
'// Remove a simple property\nn8n_update_partial_workflow({id: "rm1", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {onError: undefined}}]})',
'// Migrate from deprecated continueOnFail to onError\nn8n_update_partial_workflow({id: "rm2", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {continueOnFail: undefined, onError: "continueErrorOutput"}}]})',
'// Remove nested property\nn8n_update_partial_workflow({id: "rm3", operations: [{type: "updateNode", nodeName: "API Request", updates: {"parameters.authentication": undefined}}]})',
'// Remove multiple properties\nn8n_update_partial_workflow({id: "rm4", operations: [{type: "updateNode", nodeName: "Data Processor", updates: {continueOnFail: undefined, alwaysOutputData: undefined, "parameters.legacy_option": undefined}}]})',
'// Remove entire array property\nn8n_update_partial_workflow({id: "rm5", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {"parameters.headers": undefined}}]})'
'// Replace all AI tools for an agent\nn8n_update_partial_workflow({id: "ai10", operations: [\n {type: "removeConnection", source: "Old Tool 1", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "removeConnection", source: "Old Tool 2", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "addConnection", source: "New HTTP Tool", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "addConnection", source: "New Code Tool", target: "AI Agent", sourceOutput: "ai_tool"}\n]})'
],
useCases: [
'Rewire connections when replacing nodes',
@@ -373,11 +259,7 @@ n8n_update_partial_workflow({
'Connect language model BEFORE adding AI Agent to ensure validation passes',
'Use targetIndex for fallback models (primary=0, fallback=1)',
'Batch AI component connections in a single operation for atomicity',
'Validate AI workflows after connection changes to catch configuration errors',
'To remove properties, set them to undefined (not null) in the updates object',
'When migrating from deprecated properties, remove the old property and add the new one in the same operation',
'Use undefined to resolve mutual exclusivity validation errors between properties',
'Batch multiple property removals in a single updateNode operation for efficiency'
'Validate AI workflows after connection changes to catch configuration errors'
],
pitfalls: [
'**REQUIRES N8N_API_URL and N8N_API_KEY environment variables** - will not work without n8n API access',
@@ -390,19 +272,12 @@ n8n_update_partial_workflow({
'Use "updates" property for updateNode operations: {type: "updateNode", updates: {...}}',
'Smart parameters (branch, case) only work with IF and Switch nodes - ignored for other node types',
'Explicit sourceIndex overrides smart parameters (branch, case) if both provided',
'**CRITICAL**: For If nodes, ALWAYS use branch="true"/"false" instead of sourceIndex. Using sourceIndex=0 for multiple connections will put them ALL on the TRUE branch (main[0]), breaking your workflow logic!',
'**CRITICAL**: For Switch nodes, ALWAYS use case=N instead of sourceIndex. Using same sourceIndex for multiple connections will put them on the same case output.',
'cleanStaleConnections removes ALL broken connections - cannot be selective',
'replaceConnections overwrites entire connections object - all previous connections lost',
'**Auto-sanitization behavior**: Binary operators (equals, contains) automatically have singleValue removed; unary operators (isEmpty, isNotEmpty) automatically get singleValue:true added',
'**Auto-sanitization runs on ALL nodes**: When ANY update is made, ALL nodes in the workflow are sanitized (not just modified ones)',
'**Auto-sanitization cannot fix everything**: It fixes operator structures and missing metadata, but cannot fix broken connections or branch mismatches',
'**Corrupted workflows beyond repair**: Workflows in paradoxical states (API returns corrupt, API rejects updates) cannot be fixed via API - must be recreated',
'Setting a property to null does NOT remove it - use undefined instead',
'When properties are mutually exclusive (e.g., continueOnFail and onError), setting only the new property will fail - you must remove the old one with undefined',
'Removing a required property may cause validation errors - check node documentation first',
'Nested property removal with dot notation only removes the specific nested field, not the entire parent object',
'Array index notation (e.g., "parameters.headers[0]") is not supported - remove the entire array property instead'
'**Corrupted workflows beyond repair**: Workflows in paradoxical states (API returns corrupt, API rejects updates) cannot be fixed via API - must be recreated'
],
relatedTools: ['n8n_update_full_workflow', 'n8n_get_workflow', 'validate_workflow', 'tools_documentation']
}

View File

@@ -75,15 +75,10 @@ async function fetchTemplatesRobust() {
// Fetch detail
const detail = await fetcher.fetchTemplateDetail(template.id);
if (detail !== null) {
// Save immediately
repository.saveTemplate(template, detail);
saved++;
} else {
errors++;
console.error(`\n❌ Failed to fetch template ${template.id} (${template.name}) after retries`);
}
// Save immediately
repository.saveTemplate(template, detail);
saved++;
// Rate limiting
await new Promise(resolve => setTimeout(resolve, 200));

View File

@@ -401,59 +401,7 @@ export class EnhancedConfigValidator extends ConfigValidator {
config: Record<string, any>,
result: EnhancedValidationResult
): void {
const url = String(config.url || '');
const options = config.options || {};
// 1. Suggest alwaysOutputData for better error handling (node-level property)
// Note: We can't check if it exists (it's node-level, not in parameters),
// but we can suggest it as a best practice
if (!result.suggestions.some(s => typeof s === 'string' && s.includes('alwaysOutputData'))) {
result.suggestions.push(
'Consider adding alwaysOutputData: true at node level (not in parameters) for better error handling. ' +
'This ensures the node produces output even when HTTP requests fail, allowing downstream error handling.'
);
}
// 2. Suggest responseFormat for API endpoints
const lowerUrl = url.toLowerCase();
const isApiEndpoint =
// Subdomain patterns (api.example.com)
/^https?:\/\/api\./i.test(url) ||
// Path patterns with word boundaries to prevent false positives like "therapist", "restaurant"
/\/api[\/\?]|\/api$/i.test(url) ||
/\/rest[\/\?]|\/rest$/i.test(url) ||
// Known API service domains
lowerUrl.includes('supabase.co') ||
lowerUrl.includes('firebase') ||
lowerUrl.includes('googleapis.com') ||
// Versioned API paths (e.g., example.com/v1, example.com/v2)
/\.com\/v\d+/i.test(url);
if (isApiEndpoint && !options.response?.response?.responseFormat) {
result.suggestions.push(
'API endpoints should explicitly set options.response.response.responseFormat to "json" or "text" ' +
'to prevent confusion about response parsing. Example: ' +
'{ "options": { "response": { "response": { "responseFormat": "json" } } } }'
);
}
// 3. Enhanced URL protocol validation for expressions
if (url && url.startsWith('=')) {
// Expression-based URL - check for common protocol issues
const expressionContent = url.slice(1); // Remove = prefix
const lowerExpression = expressionContent.toLowerCase();
// Check for missing protocol in expression (case-insensitive)
if (expressionContent.startsWith('www.') ||
(expressionContent.includes('{{') && !lowerExpression.includes('http'))) {
result.warnings.push({
type: 'invalid_value',
property: 'url',
message: 'URL expression appears to be missing http:// or https:// protocol',
suggestion: 'Include protocol in your expression. Example: ={{ "https://" + $json.domain + ".com" }}'
});
}
}
// Examples removed - validation provides error messages and fixes instead
}
/**

View File

@@ -170,23 +170,10 @@ export class N8nApiClient {
}
}
/**
* Lists workflows from n8n instance.
*
* @param params - Query parameters for filtering and pagination
* @returns Paginated list of workflows
*
* @remarks
* This method handles two response formats for backwards compatibility:
* - Modern (n8n v0.200.0+): {data: Workflow[], nextCursor?: string}
* - Legacy (older versions): Workflow[] (wrapped automatically)
*
* @see https://github.com/czlonkowski/n8n-mcp/issues/349
*/
async listWorkflows(params: WorkflowListParams = {}): Promise<WorkflowListResponse> {
try {
const response = await this.client.get('/workflows', { params });
return this.validateListResponse<Workflow>(response.data, 'workflows');
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
@@ -204,23 +191,10 @@ export class N8nApiClient {
}
}
/**
* Lists executions from n8n instance.
*
* @param params - Query parameters for filtering and pagination
* @returns Paginated list of executions
*
* @remarks
* This method handles two response formats for backwards compatibility:
* - Modern (n8n v0.200.0+): {data: Execution[], nextCursor?: string}
* - Legacy (older versions): Execution[] (wrapped automatically)
*
* @see https://github.com/czlonkowski/n8n-mcp/issues/349
*/
async listExecutions(params: ExecutionListParams = {}): Promise<ExecutionListResponse> {
try {
const response = await this.client.get('/executions', { params });
return this.validateListResponse<Execution>(response.data, 'executions');
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
@@ -287,23 +261,10 @@ export class N8nApiClient {
}
// Credential Management
/**
* Lists credentials from n8n instance.
*
* @param params - Query parameters for filtering and pagination
* @returns Paginated list of credentials
*
* @remarks
* This method handles two response formats for backwards compatibility:
* - Modern (n8n v0.200.0+): {data: Credential[], nextCursor?: string}
* - Legacy (older versions): Credential[] (wrapped automatically)
*
* @see https://github.com/czlonkowski/n8n-mcp/issues/349
*/
async listCredentials(params: CredentialListParams = {}): Promise<CredentialListResponse> {
try {
const response = await this.client.get('/credentials', { params });
return this.validateListResponse<Credential>(response.data, 'credentials');
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
@@ -345,23 +306,10 @@ export class N8nApiClient {
}
// Tag Management
/**
* Lists tags from n8n instance.
*
* @param params - Query parameters for filtering and pagination
* @returns Paginated list of tags
*
* @remarks
* This method handles two response formats for backwards compatibility:
* - Modern (n8n v0.200.0+): {data: Tag[], nextCursor?: string}
* - Legacy (older versions): Tag[] (wrapped automatically)
*
* @see https://github.com/czlonkowski/n8n-mcp/issues/349
*/
async listTags(params: TagListParams = {}): Promise<TagListResponse> {
try {
const response = await this.client.get('/tags', { params });
return this.validateListResponse<Tag>(response.data, 'tags');
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
@@ -464,49 +412,4 @@ export class N8nApiClient {
throw handleN8nApiError(error);
}
}
/**
* Validates and normalizes n8n API list responses.
* Handles both modern format {data: [], nextCursor?: string} and legacy array format.
*
* @param responseData - Raw response data from n8n API
* @param resourceType - Resource type for error messages (e.g., 'workflows', 'executions')
* @returns Normalized response in modern format
* @throws Error if response structure is invalid
*/
private validateListResponse<T>(
responseData: any,
resourceType: string
): { data: T[]; nextCursor?: string | null } {
// Validate response structure
if (!responseData || typeof responseData !== 'object') {
throw new Error(`Invalid response from n8n API for ${resourceType}: response is not an object`);
}
// Handle legacy case where API returns array directly (older n8n versions)
if (Array.isArray(responseData)) {
logger.warn(
`n8n API returned array directly instead of {data, nextCursor} object for ${resourceType}. ` +
'Wrapping in expected format for backwards compatibility.'
);
return {
data: responseData,
nextCursor: null
};
}
// Validate expected format {data: [], nextCursor?: string}
if (!Array.isArray(responseData.data)) {
const keys = Object.keys(responseData).slice(0, 5);
const keysPreview = keys.length < Object.keys(responseData).length
? `${keys.join(', ')}...`
: keys.join(', ');
throw new Error(
`Invalid response from n8n API for ${resourceType}: expected {data: [], nextCursor?: string}, ` +
`got object with keys: [${keysPreview}]`
);
}
return responseData;
}
}

View File

@@ -38,8 +38,6 @@ const logger = new Logger({ prefix: '[WorkflowDiffEngine]' });
export class WorkflowDiffEngine {
// Track node name changes during operations for connection reference updates
private renameMap: Map<string, string> = new Map();
// Track warnings during operation processing
private warnings: WorkflowDiffValidationError[] = [];
/**
* Apply diff operations to a workflow
@@ -49,9 +47,8 @@ export class WorkflowDiffEngine {
request: WorkflowDiffRequest
): Promise<WorkflowDiffResult> {
try {
// Reset tracking for this diff operation
// Reset rename tracking for this diff operation
this.renameMap.clear();
this.warnings = [];
// Clone workflow to avoid modifying original
const workflowCopy = JSON.parse(JSON.stringify(workflow));
@@ -117,7 +114,6 @@ export class WorkflowDiffEngine {
? 'Validation successful. All operations are valid.'
: `Validation completed with ${errors.length} errors.`,
errors: errors.length > 0 ? errors : undefined,
warnings: this.warnings.length > 0 ? this.warnings : undefined,
applied: appliedIndices,
failed: failedIndices
};
@@ -130,7 +126,6 @@ export class WorkflowDiffEngine {
operationsApplied: appliedIndices.length,
message: `Applied ${appliedIndices.length} operations, ${failedIndices.length} failed (continueOnError mode)`,
errors: errors.length > 0 ? errors : undefined,
warnings: this.warnings.length > 0 ? this.warnings : undefined,
applied: appliedIndices,
failed: failedIndices
};
@@ -218,8 +213,7 @@ export class WorkflowDiffEngine {
success: true,
workflow: workflowCopy,
operationsApplied,
message: `Successfully applied ${operationsApplied} operations (${nodeOperations.length} node ops, ${otherOperations.length} other ops)`,
warnings: this.warnings.length > 0 ? this.warnings : undefined
message: `Successfully applied ${operationsApplied} operations (${nodeOperations.length} node ops, ${otherOperations.length} other ops)`
};
}
} catch (error) {
@@ -691,24 +685,6 @@ export class WorkflowDiffEngine {
sourceIndex = operation.case;
}
// Validation: Warn if using sourceIndex with If/Switch nodes without smart parameters
if (sourceNode && operation.sourceIndex !== undefined && operation.branch === undefined && operation.case === undefined) {
if (sourceNode.type === 'n8n-nodes-base.if') {
this.warnings.push({
operation: -1, // Not tied to specific operation index in request
message: `Connection to If node "${operation.source}" uses sourceIndex=${operation.sourceIndex}. ` +
`Consider using branch="true" or branch="false" for better clarity. ` +
`If node outputs: main[0]=TRUE branch, main[1]=FALSE branch.`
});
} else if (sourceNode.type === 'n8n-nodes-base.switch') {
this.warnings.push({
operation: -1, // Not tied to specific operation index in request
message: `Connection to Switch node "${operation.source}" uses sourceIndex=${operation.sourceIndex}. ` +
`Consider using case=N for better clarity (case=0 for first output, case=1 for second, etc.).`
});
}
}
return { sourceOutput, sourceIndex };
}

View File

@@ -40,37 +40,7 @@ export interface TemplateDetail {
export class TemplateFetcher {
private readonly baseUrl = 'https://api.n8n.io/api/templates';
private readonly pageSize = 250; // Maximum allowed by API
private readonly maxRetries = 3;
private readonly retryDelay = 1000; // 1 second base delay
/**
* Retry helper for API calls
*/
private async retryWithBackoff<T>(
fn: () => Promise<T>,
context: string,
maxRetries: number = this.maxRetries
): Promise<T | null> {
let lastError: any;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
lastError = error;
if (attempt < maxRetries) {
const delay = this.retryDelay * attempt; // Exponential backoff
logger.warn(`${context} - Attempt ${attempt}/${maxRetries} failed, retrying in ${delay}ms...`);
await this.sleep(delay);
}
}
}
logger.error(`${context} - All ${maxRetries} attempts failed, skipping`, lastError);
return null;
}
/**
* Fetch all templates and filter to last 12 months
* This fetches ALL pages first, then applies date filter locally
@@ -103,105 +73,93 @@ export class TemplateFetcher {
let page = 1;
let hasMore = true;
let totalWorkflows = 0;
logger.info('Starting complete template fetch from n8n.io API');
while (hasMore) {
const result = await this.retryWithBackoff(
async () => {
const response = await axios.get(`${this.baseUrl}/search`, {
params: {
page,
rows: this.pageSize
// Note: sort_by parameter doesn't work, templates come in popularity order
}
});
return response.data;
},
`Fetching templates page ${page}`
);
if (result === null) {
// All retries failed for this page, skip it and continue
logger.warn(`Skipping page ${page} after ${this.maxRetries} failed attempts`);
try {
const response = await axios.get(`${this.baseUrl}/search`, {
params: {
page,
rows: this.pageSize
// Note: sort_by parameter doesn't work, templates come in popularity order
}
});
const { workflows } = response.data;
totalWorkflows = response.data.totalWorkflows || totalWorkflows;
allTemplates.push(...workflows);
// Calculate total pages for better progress reporting
const totalPages = Math.ceil(totalWorkflows / this.pageSize);
if (progressCallback) {
// Enhanced progress with page information
progressCallback(allTemplates.length, totalWorkflows);
}
logger.debug(`Fetched page ${page}/${totalPages}: ${workflows.length} templates (total so far: ${allTemplates.length}/${totalWorkflows})`);
// Check if there are more pages
if (workflows.length < this.pageSize) {
hasMore = false;
}
page++;
continue;
}
const { workflows } = result;
totalWorkflows = result.totalWorkflows || totalWorkflows;
allTemplates.push(...workflows);
// Calculate total pages for better progress reporting
const totalPages = Math.ceil(totalWorkflows / this.pageSize);
if (progressCallback) {
// Enhanced progress with page information
progressCallback(allTemplates.length, totalWorkflows);
}
logger.debug(`Fetched page ${page}/${totalPages}: ${workflows.length} templates (total so far: ${allTemplates.length}/${totalWorkflows})`);
// Check if there are more pages
if (workflows.length < this.pageSize) {
hasMore = false;
}
page++;
// Rate limiting - be nice to the API (slightly faster with 250 rows/page)
if (hasMore) {
await this.sleep(300); // 300ms between requests (was 500ms with 100 rows)
// Rate limiting - be nice to the API (slightly faster with 250 rows/page)
if (hasMore) {
await this.sleep(300); // 300ms between requests (was 500ms with 100 rows)
}
} catch (error) {
logger.error(`Error fetching templates page ${page}:`, error);
throw error;
}
}
logger.info(`Fetched all ${allTemplates.length} templates from n8n.io`);
return allTemplates;
}
async fetchTemplateDetail(workflowId: number): Promise<TemplateDetail | null> {
const result = await this.retryWithBackoff(
async () => {
const response = await axios.get(`${this.baseUrl}/workflows/${workflowId}`);
return response.data.workflow;
},
`Fetching template detail for workflow ${workflowId}`
);
return result;
async fetchTemplateDetail(workflowId: number): Promise<TemplateDetail> {
try {
const response = await axios.get(`${this.baseUrl}/workflows/${workflowId}`);
return response.data.workflow;
} catch (error) {
logger.error(`Error fetching template detail for ${workflowId}:`, error);
throw error;
}
}
async fetchAllTemplateDetails(
workflows: TemplateWorkflow[],
workflows: TemplateWorkflow[],
progressCallback?: (current: number, total: number) => void
): Promise<Map<number, TemplateDetail>> {
const details = new Map<number, TemplateDetail>();
let skipped = 0;
logger.info(`Fetching details for ${workflows.length} templates`);
for (let i = 0; i < workflows.length; i++) {
const workflow = workflows[i];
const detail = await this.fetchTemplateDetail(workflow.id);
if (detail !== null) {
try {
const detail = await this.fetchTemplateDetail(workflow.id);
details.set(workflow.id, detail);
} else {
skipped++;
logger.warn(`Skipped workflow ${workflow.id} after ${this.maxRetries} failed attempts`);
if (progressCallback) {
progressCallback(i + 1, workflows.length);
}
// Rate limiting (conservative to avoid API throttling)
await this.sleep(150); // 150ms between requests
} catch (error) {
logger.error(`Failed to fetch details for workflow ${workflow.id}:`, error);
// Continue with other templates
}
if (progressCallback) {
progressCallback(i + 1, workflows.length);
}
// Rate limiting (conservative to avoid API throttling)
await this.sleep(150); // 150ms between requests
}
logger.info(`Successfully fetched ${details.size} template details (${skipped} skipped)`);
logger.info(`Successfully fetched ${details.size} template details`);
return details;
}

View File

@@ -496,17 +496,10 @@ export class TemplateRepository {
// Count node usage
const nodeCount: Record<string, number> = {};
topNodes.forEach(t => {
if (!t.nodes_used) return;
try {
const nodes = JSON.parse(t.nodes_used);
if (Array.isArray(nodes)) {
nodes.forEach((n: string) => {
nodeCount[n] = (nodeCount[n] || 0) + 1;
});
}
} catch (error) {
logger.warn(`Failed to parse nodes_used for template stats:`, error);
}
const nodes = JSON.parse(t.nodes_used);
nodes.forEach((n: string) => {
nodeCount[n] = (nodeCount[n] || 0) + 1;
});
});
// Get top 10 most used nodes

View File

@@ -170,7 +170,6 @@ export interface WorkflowDiffResult {
success: boolean;
workflow?: any; // Updated workflow if successful
errors?: WorkflowDiffValidationError[];
warnings?: WorkflowDiffValidationError[]; // Non-blocking warnings (e.g., parameter suggestions)
operationsApplied?: number;
message?: string;
applied?: number[]; // Indices of successfully applied operations (when continueOnError is true)

View File

@@ -188,7 +188,6 @@ describe('handlers-workflow-diff', () => {
operationsApplied: 1,
message: 'Validation successful',
errors: [],
warnings: []
});
const result = await handleUpdatePartialWorkflow(diffRequest, mockRepository);
@@ -200,9 +199,6 @@ describe('handlers-workflow-diff', () => {
valid: true,
operationsToApply: 1,
},
details: {
warnings: []
}
});
expect(mockApiClient.updateWorkflow).not.toHaveBeenCalled();

View File

@@ -802,335 +802,4 @@ describe('EnhancedConfigValidator', () => {
expect(result.errors[0].property).toBe('test');
});
});
describe('enhanceHttpRequestValidation', () => {
it('should suggest alwaysOutputData for HTTP Request nodes', () => {
const nodeType = 'nodes-base.httpRequest';
const config = {
url: 'https://api.example.com/data',
method: 'GET'
};
const properties = [
{ name: 'url', type: 'string', required: true },
{ name: 'method', type: 'options', required: false }
];
const result = EnhancedConfigValidator.validateWithMode(
nodeType,
config,
properties,
'operation',
'ai-friendly'
);
expect(result.valid).toBe(true);
expect(result.suggestions).toContainEqual(
expect.stringContaining('alwaysOutputData: true at node level')
);
expect(result.suggestions).toContainEqual(
expect.stringContaining('ensures the node produces output even when HTTP requests fail')
);
});
it('should suggest responseFormat for API endpoint URLs', () => {
const nodeType = 'nodes-base.httpRequest';
const config = {
url: 'https://api.example.com/data',
method: 'GET',
options: {} // Empty options, no responseFormat
};
const properties = [
{ name: 'url', type: 'string', required: true },
{ name: 'method', type: 'options', required: false },
{ name: 'options', type: 'collection', required: false }
];
const result = EnhancedConfigValidator.validateWithMode(
nodeType,
config,
properties,
'operation',
'ai-friendly'
);
expect(result.valid).toBe(true);
expect(result.suggestions).toContainEqual(
expect.stringContaining('responseFormat')
);
expect(result.suggestions).toContainEqual(
expect.stringContaining('options.response.response.responseFormat')
);
});
it('should suggest responseFormat for Supabase URLs', () => {
const nodeType = 'nodes-base.httpRequest';
const config = {
url: 'https://xxciwnthnnywanbplqwg.supabase.co/rest/v1/messages',
method: 'GET',
options: {}
};
const properties = [
{ name: 'url', type: 'string', required: true }
];
const result = EnhancedConfigValidator.validateWithMode(
nodeType,
config,
properties,
'operation',
'ai-friendly'
);
expect(result.suggestions).toContainEqual(
expect.stringContaining('responseFormat')
);
});
it('should NOT suggest responseFormat when already configured', () => {
const nodeType = 'nodes-base.httpRequest';
const config = {
url: 'https://api.example.com/data',
method: 'GET',
options: {
response: {
response: {
responseFormat: 'json'
}
}
}
};
const properties = [
{ name: 'url', type: 'string', required: true },
{ name: 'options', type: 'collection', required: false }
];
const result = EnhancedConfigValidator.validateWithMode(
nodeType,
config,
properties,
'operation',
'ai-friendly'
);
const responseFormatSuggestion = result.suggestions.find(
(s: string) => s.includes('responseFormat')
);
expect(responseFormatSuggestion).toBeUndefined();
});
it('should warn about missing protocol in expression-based URLs', () => {
const nodeType = 'nodes-base.httpRequest';
const config = {
url: '=www.{{ $json.domain }}.com',
method: 'GET'
};
const properties = [
{ name: 'url', type: 'string', required: true }
];
const result = EnhancedConfigValidator.validateWithMode(
nodeType,
config,
properties,
'operation',
'ai-friendly'
);
expect(result.warnings).toContainEqual(
expect.objectContaining({
type: 'invalid_value',
property: 'url',
message: expect.stringContaining('missing http:// or https://')
})
);
});
it('should warn about missing protocol in expressions with template markers', () => {
const nodeType = 'nodes-base.httpRequest';
const config = {
url: '={{ $json.domain }}/api/data',
method: 'GET'
};
const properties = [
{ name: 'url', type: 'string', required: true }
];
const result = EnhancedConfigValidator.validateWithMode(
nodeType,
config,
properties,
'operation',
'ai-friendly'
);
expect(result.warnings).toContainEqual(
expect.objectContaining({
type: 'invalid_value',
property: 'url',
message: expect.stringContaining('missing http:// or https://')
})
);
});
it('should NOT warn when expression includes http protocol', () => {
const nodeType = 'nodes-base.httpRequest';
const config = {
url: '={{ "https://" + $json.domain + ".com" }}',
method: 'GET'
};
const properties = [
{ name: 'url', type: 'string', required: true }
];
const result = EnhancedConfigValidator.validateWithMode(
nodeType,
config,
properties,
'operation',
'ai-friendly'
);
const urlWarning = result.warnings.find(
(w: any) => w.property === 'url' && w.message.includes('protocol')
);
expect(urlWarning).toBeUndefined();
});
it('should NOT suggest responseFormat for non-API URLs', () => {
const nodeType = 'nodes-base.httpRequest';
const config = {
url: 'https://example.com/page.html',
method: 'GET',
options: {}
};
const properties = [
{ name: 'url', type: 'string', required: true }
];
const result = EnhancedConfigValidator.validateWithMode(
nodeType,
config,
properties,
'operation',
'ai-friendly'
);
const responseFormatSuggestion = result.suggestions.find(
(s: string) => s.includes('responseFormat')
);
expect(responseFormatSuggestion).toBeUndefined();
});
it('should detect missing protocol in expressions with uppercase HTTP', () => {
const nodeType = 'nodes-base.httpRequest';
const config = {
url: '={{ "HTTP://" + $json.domain + ".com" }}',
method: 'GET'
};
const properties = [
{ name: 'url', type: 'string', required: true }
];
const result = EnhancedConfigValidator.validateWithMode(
nodeType,
config,
properties,
'operation',
'ai-friendly'
);
// Should NOT warn because HTTP:// is present (case-insensitive)
expect(result.warnings).toHaveLength(0);
});
it('should NOT suggest responseFormat for false positive URLs', () => {
const nodeType = 'nodes-base.httpRequest';
const testUrls = [
'https://example.com/therapist-directory',
'https://restaurant-bookings.com/reserve',
'https://forest-management.org/data'
];
testUrls.forEach(url => {
const config = {
url,
method: 'GET',
options: {}
};
const properties = [
{ name: 'url', type: 'string', required: true }
];
const result = EnhancedConfigValidator.validateWithMode(
nodeType,
config,
properties,
'operation',
'ai-friendly'
);
const responseFormatSuggestion = result.suggestions.find(
(s: string) => s.includes('responseFormat')
);
expect(responseFormatSuggestion).toBeUndefined();
});
});
it('should suggest responseFormat for case-insensitive API paths', () => {
const nodeType = 'nodes-base.httpRequest';
const testUrls = [
'https://example.com/API/users',
'https://example.com/Rest/data',
'https://example.com/REST/v1/items'
];
testUrls.forEach(url => {
const config = {
url,
method: 'GET',
options: {}
};
const properties = [
{ name: 'url', type: 'string', required: true }
];
const result = EnhancedConfigValidator.validateWithMode(
nodeType,
config,
properties,
'operation',
'ai-friendly'
);
expect(result.suggestions).toContainEqual(
expect.stringContaining('responseFormat')
);
});
});
it('should handle null and undefined URLs gracefully', () => {
const nodeType = 'nodes-base.httpRequest';
const testConfigs = [
{ url: null, method: 'GET' },
{ url: undefined, method: 'GET' },
{ url: '', method: 'GET' }
];
testConfigs.forEach(config => {
const properties = [
{ name: 'url', type: 'string', required: true }
];
expect(() => {
EnhancedConfigValidator.validateWithMode(
nodeType,
config,
properties,
'operation',
'ai-friendly'
);
}).not.toThrow();
});
});
});
});

View File

@@ -413,242 +413,6 @@ describe('N8nApiClient', () => {
});
});
describe('Response Format Validation (PR #367)', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
describe('listWorkflows - validation', () => {
it('should handle modern format with data and nextCursor', async () => {
const response = { data: [{ id: '1', name: 'Test' }], nextCursor: 'abc123' };
mockAxiosInstance.get.mockResolvedValue({ data: response });
const result = await client.listWorkflows();
expect(result).toEqual(response);
expect(result.data).toHaveLength(1);
expect(result.nextCursor).toBe('abc123');
});
it('should wrap legacy array format and log warning', async () => {
const workflows = [{ id: '1', name: 'Test' }];
mockAxiosInstance.get.mockResolvedValue({ data: workflows });
const result = await client.listWorkflows();
expect(result).toEqual({ data: workflows, nextCursor: null });
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('n8n API returned array directly')
);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('workflows')
);
});
it('should throw error on null response', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: null });
await expect(client.listWorkflows()).rejects.toThrow(
'Invalid response from n8n API for workflows: response is not an object'
);
});
it('should throw error on undefined response', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: undefined });
await expect(client.listWorkflows()).rejects.toThrow(
'Invalid response from n8n API for workflows: response is not an object'
);
});
it('should throw error on string response', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: 'invalid' });
await expect(client.listWorkflows()).rejects.toThrow(
'Invalid response from n8n API for workflows: response is not an object'
);
});
it('should throw error on number response', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: 42 });
await expect(client.listWorkflows()).rejects.toThrow(
'Invalid response from n8n API for workflows: response is not an object'
);
});
it('should throw error on invalid structure with different keys', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: { items: [], total: 10 } });
await expect(client.listWorkflows()).rejects.toThrow(
'Invalid response from n8n API for workflows: expected {data: [], nextCursor?: string}, got object with keys: [items, total]'
);
});
it('should throw error when data is not an array', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: { data: 'invalid' } });
await expect(client.listWorkflows()).rejects.toThrow(
'Invalid response from n8n API for workflows: expected {data: [], nextCursor?: string}'
);
});
it('should limit exposed keys to first 5 when many keys present', async () => {
const manyKeys = { items: [], total: 10, page: 1, limit: 20, hasMore: true, metadata: {} };
mockAxiosInstance.get.mockResolvedValue({ data: manyKeys });
try {
await client.listWorkflows();
expect.fail('Should have thrown error');
} catch (error: any) {
expect(error.message).toContain('items, total, page, limit, hasMore...');
expect(error.message).not.toContain('metadata');
}
});
});
describe('listExecutions - validation', () => {
it('should handle modern format with data and nextCursor', async () => {
const response = { data: [{ id: '1' }], nextCursor: 'abc123' };
mockAxiosInstance.get.mockResolvedValue({ data: response });
const result = await client.listExecutions();
expect(result).toEqual(response);
});
it('should wrap legacy array format and log warning', async () => {
const executions = [{ id: '1' }];
mockAxiosInstance.get.mockResolvedValue({ data: executions });
const result = await client.listExecutions();
expect(result).toEqual({ data: executions, nextCursor: null });
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('executions')
);
});
it('should throw error on null response', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: null });
await expect(client.listExecutions()).rejects.toThrow(
'Invalid response from n8n API for executions: response is not an object'
);
});
it('should throw error on invalid structure', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: { items: [] } });
await expect(client.listExecutions()).rejects.toThrow(
'Invalid response from n8n API for executions'
);
});
it('should throw error when data is not an array', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: { data: 'invalid' } });
await expect(client.listExecutions()).rejects.toThrow(
'Invalid response from n8n API for executions'
);
});
});
describe('listCredentials - validation', () => {
it('should handle modern format with data and nextCursor', async () => {
const response = { data: [{ id: '1' }], nextCursor: 'abc123' };
mockAxiosInstance.get.mockResolvedValue({ data: response });
const result = await client.listCredentials();
expect(result).toEqual(response);
});
it('should wrap legacy array format and log warning', async () => {
const credentials = [{ id: '1' }];
mockAxiosInstance.get.mockResolvedValue({ data: credentials });
const result = await client.listCredentials();
expect(result).toEqual({ data: credentials, nextCursor: null });
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('credentials')
);
});
it('should throw error on null response', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: null });
await expect(client.listCredentials()).rejects.toThrow(
'Invalid response from n8n API for credentials: response is not an object'
);
});
it('should throw error on invalid structure', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: { items: [] } });
await expect(client.listCredentials()).rejects.toThrow(
'Invalid response from n8n API for credentials'
);
});
it('should throw error when data is not an array', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: { data: 'invalid' } });
await expect(client.listCredentials()).rejects.toThrow(
'Invalid response from n8n API for credentials'
);
});
});
describe('listTags - validation', () => {
it('should handle modern format with data and nextCursor', async () => {
const response = { data: [{ id: '1' }], nextCursor: 'abc123' };
mockAxiosInstance.get.mockResolvedValue({ data: response });
const result = await client.listTags();
expect(result).toEqual(response);
});
it('should wrap legacy array format and log warning', async () => {
const tags = [{ id: '1' }];
mockAxiosInstance.get.mockResolvedValue({ data: tags });
const result = await client.listTags();
expect(result).toEqual({ data: tags, nextCursor: null });
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('tags')
);
});
it('should throw error on null response', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: null });
await expect(client.listTags()).rejects.toThrow(
'Invalid response from n8n API for tags: response is not an object'
);
});
it('should throw error on invalid structure', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: { items: [] } });
await expect(client.listTags()).rejects.toThrow(
'Invalid response from n8n API for tags'
);
});
it('should throw error when data is not an array', async () => {
mockAxiosInstance.get.mockResolvedValue({ data: { data: 'invalid' } });
await expect(client.listTags()).rejects.toThrow(
'Invalid response from n8n API for tags'
);
});
});
});
describe('getExecution', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);

View File

@@ -1418,113 +1418,6 @@ describe('WorkflowDiffEngine', () => {
expect(result.workflow!.connections['Switch']['main'][2][0].node).toBe('Handler');
expect(result.workflow!.connections['Switch']['main'][1]).toEqual([]);
});
it('should warn when using sourceIndex with If node (issue #360)', async () => {
const addIF: any = {
type: 'addNode',
node: {
name: 'Check Condition',
type: 'n8n-nodes-base.if',
position: [400, 300]
}
};
const addSuccess: any = {
type: 'addNode',
node: {
name: 'Success Handler',
type: 'n8n-nodes-base.set',
position: [600, 200]
}
};
const addError: any = {
type: 'addNode',
node: {
name: 'Error Handler',
type: 'n8n-nodes-base.set',
position: [600, 400]
}
};
// BAD: Using sourceIndex with If node (reproduces issue #360)
const connectSuccess: any = {
type: 'addConnection',
source: 'Check Condition',
target: 'Success Handler',
sourceIndex: 0 // Should use branch="true" instead
};
const connectError: any = {
type: 'addConnection',
source: 'Check Condition',
target: 'Error Handler',
sourceIndex: 0 // Should use branch="false" instead - both will end up in main[0]!
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [addIF, addSuccess, addError, connectSuccess, connectError]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
// Should produce warnings
expect(result.warnings).toBeDefined();
expect(result.warnings!.length).toBe(2);
expect(result.warnings![0].message).toContain('Consider using branch="true" or branch="false"');
expect(result.warnings![0].message).toContain('If node outputs: main[0]=TRUE branch, main[1]=FALSE branch');
expect(result.warnings![1].message).toContain('Consider using branch="true" or branch="false"');
// Both connections end up in main[0] (the bug behavior)
expect(result.workflow!.connections['Check Condition']['main'][0].length).toBe(2);
expect(result.workflow!.connections['Check Condition']['main'][0][0].node).toBe('Success Handler');
expect(result.workflow!.connections['Check Condition']['main'][0][1].node).toBe('Error Handler');
});
it('should warn when using sourceIndex with Switch node', async () => {
const addSwitch: any = {
type: 'addNode',
node: {
name: 'Switch',
type: 'n8n-nodes-base.switch',
position: [400, 300]
}
};
const addHandler: any = {
type: 'addNode',
node: {
name: 'Handler',
type: 'n8n-nodes-base.set',
position: [600, 300]
}
};
// BAD: Using sourceIndex with Switch node
const connect: any = {
type: 'addConnection',
source: 'Switch',
target: 'Handler',
sourceIndex: 1 // Should use case=1 instead
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [addSwitch, addHandler, connect]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
// Should produce warning
expect(result.warnings).toBeDefined();
expect(result.warnings!.length).toBe(1);
expect(result.warnings![0].message).toContain('Consider using case=N for better clarity');
});
});
describe('AddConnection with sourceIndex (Phase 0 Fix)', () => {