Compare commits

..

16 Commits

Author SHA1 Message Date
Romuald Członkowski
eee52a7f53 Merge pull request #406 from czlonkowski/fix/helpful-error-changes-vs-updates
fix: Add helpful error messages for 'changes' vs 'updates' parameter (Issue #392)
2025-11-08 13:39:26 +01:00
czlonkowski
a66cb18cce fix: Add helpful error messages for 'changes' vs 'updates' parameter (Issue #392)
Fixed cryptic "Cannot read properties of undefined (reading 'name')" error when
users mistakenly use 'changes' instead of 'updates' in updateNode operations.

Changes:
- Added early validation in validateUpdateNode() to detect common parameter mistake
- Provides clear, educational error messages with examples
- Fixed outdated documentation example in VS_CODE_PROJECT_SETUP.md
- Added comprehensive test coverage (2 test cases)

Error Messages:
- Before: "Diff engine error: Cannot read properties of undefined (reading 'name')"
- After: "Invalid parameter 'changes'. The updateNode operation requires 'updates'
  (not 'changes'). Example: {type: "updateNode", nodeId: "abc", updates: {...}}"

Testing:
- Test coverage: 85% confidence (production ready)
- n8n-mcp-tester: All 3 test cases passed
- Code review: Approved with minor optional suggestions

Impact:
- AI agents now receive actionable error messages
- Self-correction enabled through clear examples
- Zero breaking changes (backward compatible)
- Follows existing patterns from Issue #249

Files Modified:
- src/services/workflow-diff-engine.ts (10 lines added)
- docs/VS_CODE_PROJECT_SETUP.md (1 line fixed)
- tests/unit/services/workflow-diff-engine.test.ts (2 tests added)
- CHANGELOG.md (comprehensive entry)
- package.json (version bump to 2.22.12)

Fixes #392

Conceived by Romuald Członkowski - www.aiadvisors.pl/en
2025-11-08 13:29:22 +01:00
Romuald Członkowski
0e0f0998af Merge pull request #403 from czlonkowski/feat/workflow-activation-operations 2025-11-07 07:54:33 +01:00
czlonkowski
08a4be8370 fix: Add missing typeVersion to workflow activation test nodes
Fixed TypeScript linting errors in workflow-diff-engine.test.ts by adding
typeVersion: 1 to all test nodes that were missing it.

Fixes CI linting failures in Test Suite workflow.

Conceived by Romuald Członkowski - www.aiadvisors.pl/en
2025-11-07 00:12:36 +01:00
czlonkowski
3578f2cc31 test: Add comprehensive test coverage for workflow activation/deactivation
Added 25 new tests to improve coverage for workflow activation/deactivation feature:
- 7 tests for handlers-workflow-diff.test.ts (activation/deactivation handler logic)
- 8 tests for workflow-diff-engine.test.ts (validate/apply activate/deactivate operations)
- 10 tests for n8n-api-client.test.ts (API client activation/deactivation methods)

Coverage improvements:
- Branch coverage increased from 77% to 85.58%
- All 3512 tests passing

Tests cover:
- Successful workflow activation/deactivation after updates
- Error handling for activation/deactivation failures
- Validation of activatable trigger nodes (webhook, schedule, etc.)
- Rejection of workflows without activatable triggers
- API client error cases (not found, already active/inactive, server errors)

Conceived by Romuald Członkowski - www.aiadvisors.pl/en
2025-11-06 23:58:34 +01:00
czlonkowski
4d3b8fbc91 fix: Remove outdated "Cannot activate" limitation from test expectations
After implementing workflow activation/deactivation operations, the
"Cannot activate" limitation no longer applies. Updated the test to
match the current API capabilities.

Related to #399

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

Co-Authored-By: Claude <noreply@anthropic.com>

Conceived by Romuald Członkowski - www.aiadvisors.pl/en
2025-11-06 23:27:13 +01:00
czlonkowski
5688384113 fix: Update test expectations for workflow activation response format
The workflow activation/deactivation implementation added two new fields
to the response details object (active and warnings). Updated test
expectations to match the new response format.

Fixes CI test failures in handlers-workflow-diff.test.ts

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

Co-Authored-By: Claude <noreply@anthropic.com>

Conceived by Romuald Członkowski - www.aiadvisors.pl/en
2025-11-06 23:14:11 +01:00
czlonkowski
346fa3c8d2 feat: Add workflow activation/deactivation via diff operations
Implements workflow activation and deactivation as diff operations in
n8n_update_partial_workflow tool, following the pattern of other
configuration operations.

Changes:
- Add activateWorkflow/deactivateWorkflow API methods
- Add operation types to diff engine
- Update tool documentation
- Remove activation limitation

Resolves #399
Credits: ArtemisAI, cmj-hub for investigation and initial implementation
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
2025-11-06 22:49:46 +01:00
czlonkowski
3d5ceae43f updated date 2025-11-06 00:21:41 +01:00
czlonkowski
1834d474a5 update privacy policy 2025-11-06 00:20:36 +01:00
Romuald Członkowski
a4ef1efaf8 fix: Gracefully handle FTS5 unavailability in sql.js fallback (#398)
Fixed critical startup crash when server falls back to sql.js adapter
due to Node.js version mismatches.

Problem:
- better-sqlite3 fails to load when Node runtime version differs from build version
- Server falls back to sql.js (pure JS, no native dependencies)
- Database health check crashed with "no such module: fts5"
- Server exits immediately, preventing Claude Desktop connection

Solution:
- Wrapped FTS5 health check in try-catch block
- Logs warning when FTS5 not available
- Server continues with fallback search (LIKE queries)
- Graceful degradation: works with any Node.js version

Impact:
- Server now starts successfully with sql.js fallback
- Works with Node v20 (Claude Desktop) even when built with Node v22
- Clear warnings about FTS5 unavailability
- Users can choose: sql.js (slower, works everywhere) or rebuild better-sqlite3 (faster)

Files Changed:
- src/mcp/server.ts: Added try-catch around FTS5 health check (lines 299-317)

Testing:
-  Tested with Node v20.17.0 (Claude Desktop)
-  Tested with Node v22.17.0 (build version)
-  All 6 startup checkpoints pass
-  Database health check passes with warning

Fixes: Claude Desktop connection failures with Node.js version mismatches

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
2025-11-04 16:14:16 +01:00
Romuald Członkowski
65f51ad8b5 chore: bump version to 2.22.9 (#395)
* chore: bump version to 2.22.9

Updated version number to trigger release workflow after n8n 1.118.1 update.
Previous version 2.22.8 was already released on 2025-10-28, so the release
workflow did not trigger when PR #393 was merged.

Changes:
- Bump package.json version from 2.22.8 to 2.22.9
- Update CHANGELOG.md with correct version and date

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

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

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: update n8n update workflow with lessons learned

Added new fast workflow section based on 2025-11-04 update experience:
- CRITICAL: Check existing releases first to avoid version conflicts
- Skip local tests - CI runs them anyway (saves 2-3 min)
- Integration test failures with 'unauthorized' are infrastructure issues
- Release workflow only triggers on version CHANGE
- Updated time estimates for fast vs full workflow

This will make future n8n updates smoother and faster.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: exclude versionCounter from workflow updates for n8n 1.118.1

n8n 1.118.1 returns versionCounter in GET /workflows/{id} responses but
rejects it in PUT /workflows/{id} updates with the error:
'request/body must NOT have additional properties'

This was causing all integration tests to fail in CI with n8n 1.118.1.

Changes:
- Added versionCounter to excluded properties in cleanWorkflowForUpdate()
- Tested and verified fix works with n8n 1.118.1 test instance

Fixes CI failures in PR #395

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: improve versionCounter fix with types and tests

- Add versionCounter type definition to Workflow and WorkflowExport interfaces
- Add comprehensive test coverage for versionCounter exclusion
- Update CHANGELOG with detailed bug fix documentation

Addresses code review feedback from PR #395

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-04 11:33:54 +01:00
Romuald Członkowski
af6efe9e88 chore: update n8n to 1.118.1 and bump version to 2.22.8 (#393)
- Updated n8n from 1.117.2 to 1.118.1
- Updated n8n-core from 1.116.0 to 1.117.0
- Updated n8n-workflow from 1.114.0 to 1.115.0
- Updated @n8n/n8n-nodes-langchain from 1.116.2 to 1.117.0
- Rebuilt node database with 542 nodes (439 from n8n-nodes-base, 103 from @n8n/n8n-nodes-langchain)
- Updated README badge with new n8n version
- Updated CHANGELOG with dependency changes

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-03 22:27:56 +01:00
Romuald Członkowski
3f427f9528 Update n8n to 1.117.2 (#379) 2025-10-28 08:55:20 +01:00
Liz
18b8747005 Update CLAUDE_CODE_SETUP.md (#276)
* Update CLAUDE_CODE_SETUP.md

docs: Improve CLI setup for PowerShell and scope management

This commit introduces two improvements to the CLAUDE_CODE_SETUP.md documentation to enhance user experience, particularly for Windows users and those managing configuration scopes.

1.  Add PowerShell-Compatible Commands:
    The original `claude mcp add` commands use a syntax that fails in native Windows PowerShell due to its parameter parsing. This change adds dedicated code blocks for PowerShell, which correctly wrap the `-e` arguments in single quotes.

2.  Clarify Configuration Scope Management:
    The documentation previously lacked guidance on the default configuration scope and how to switch to a `project` scope. A new "Tips" section has been added to:
    - Explain the default scope and the purpose of `--scope project`.
    - Provide a clear, recommended CLI method for switching scopes.
    - Offer an advanced, manual method by editing the `.claude.json` file.

* Update CLAUDE_CODE_SETUP.md  again
2025-10-27 22:43:48 +01:00
Daniel Ishi
749f1c53eb docs: Emphasize MCP_MODE=stdio requirement for Claude Desktop (#377)
Fixes #376

Without this environment variable, Claude Desktop shows JSON parsing errors
because debug logs contaminate the JSON-RPC stdout channel.

Added prominent warning to Quick Start section explaining:
- Why MCP_MODE=stdio is required
- What happens without it (JSON parse errors)
- How it prevents the issue (suppresses console output)

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-authored-by: Claude Code Assistant <noreply@anthropic.com>
2025-10-27 22:40:44 +01:00
28 changed files with 2002 additions and 1794 deletions

View File

@@ -7,6 +7,366 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [2.22.12] - 2025-01-08
### 🐛 Bug Fixes
**Issue #392: Helpful Error Messages for "changes" vs "updates" Parameter**
Fixed cryptic error message when users mistakenly use `changes` instead of `updates` in updateNode operations. AI agents now receive clear, educational error messages that help them self-correct immediately.
#### Problem
Users who mistakenly used `changes` instead of `updates` in `n8n_update_partial_workflow` updateNode operations encountered a cryptic error:
```
Diff engine error: Cannot read properties of undefined (reading 'name')
```
This error occurred because:
1. The code tried to read `operation.updates.name` at line 406 of `workflow-diff-engine.ts`
2. When users sent `changes` instead of `updates`, `operation.updates` was `undefined`
3. Reading `.name` from `undefined` → unhelpful error message
4. AI agents had no guidance on what went wrong or how to fix it
**Root Cause**: No early validation to detect this common parameter mistake before attempting to access properties.
#### Solution
Added early validation in `validateUpdateNode()` method to detect and provide helpful guidance:
**1. Parameter Validation** (`src/services/workflow-diff-engine.ts` lines 400-409):
```typescript
// Check for common parameter mistake: "changes" instead of "updates" (Issue #392)
const operationAny = operation as any;
if (operationAny.changes && !operation.updates) {
return `Invalid parameter 'changes'. The updateNode operation requires 'updates' (not 'changes'). Example: {type: "updateNode", nodeId: "abc", updates: {name: "New Name", "parameters.url": "https://example.com"}}`;
}
// Check for missing required parameter
if (!operation.updates) {
return `Missing required parameter 'updates'. The updateNode operation requires an 'updates' object containing properties to modify. Example: {type: "updateNode", nodeId: "abc", updates: {name: "New Name"}}`;
}
```
**2. Documentation Fix** (`docs/VS_CODE_PROJECT_SETUP.md` line 165):
- Fixed outdated example that showed incorrect parameter name
- Changed from: `{type: 'updateNode', nodeId: 'slack1', changes: {position: [100, 200]}}`
- Changed to: `{type: 'updateNode', nodeId: 'slack1', updates: {position: [100, 200]}}`
- Prevents AI agents from learning the wrong syntax
**3. Comprehensive Test Coverage** (`tests/unit/services/workflow-diff-engine.test.ts` lines 388-428):
- Test for using `changes` instead of `updates` (validates helpful error message)
- Test for missing `updates` parameter entirely
- Both tests verify error message content includes examples
#### Error Messages
**Before Fix:**
```
Diff engine error: Cannot read properties of undefined (reading 'name')
```
**After Fix:**
```
Missing required parameter 'updates'. The updateNode operation requires an 'updates'
object containing properties to modify. Example: {type: "updateNode", nodeId: "abc",
updates: {name: "New Name"}}
```
#### Impact
**For AI Agents:**
-**Clear Error Messages**: Explicitly states what's wrong ("Invalid parameter 'changes'")
-**Educational**: Explains the correct parameter name ("requires 'updates'")
-**Actionable**: Includes working example showing correct syntax
-**Self-Correction**: AI agents can immediately fix their code based on the error
**Testing Results:**
- Test Coverage: 85% confidence (production ready)
- n8n-mcp-tester validation: All 3 test cases passed
- Code Review: Approved with minor optional suggestions
- Consistency: Follows existing patterns from Issue #249
**Production Impact:**
- **Risk Level**: Very Low (only adds validation, no logic changes)
- **Breaking Changes**: None (backward compatible)
- **False Positive Rate**: 0% (validation is specific to the exact mistake)
#### Technical Details
**Files Modified (3 files):**
- `src/services/workflow-diff-engine.ts` - Added early validation (10 lines)
- `docs/VS_CODE_PROJECT_SETUP.md` - Fixed incorrect example (1 line)
- `tests/unit/services/workflow-diff-engine.test.ts` - Added 2 comprehensive test cases (40 lines)
**Configuration (1 file):**
- `package.json` - Version bump to 2.22.12
**Validation Flow:**
1. Check if operation has `changes` property but no `updates` → Error with helpful message
2. Check if operation is missing `updates` entirely → Error with example
3. Continue with normal validation if `updates` is present
**Consistency:**
- Pattern matches existing parameter validation in `validateAddConnection()` (lines 444-451)
- Error message format consistent with existing errors (lines 461, 466, 469)
- Uses same `as any` approach for detecting invalid properties
#### References
- **Issue**: #392 - "Diff engine error: Cannot read properties of undefined (reading 'name')"
- **Reporter**: User Aldekein (via cmj-hub investigation)
- **Test Coverage Assessment**: 85% confidence - SUFFICIENT for production
- **Code Review**: APPROVE WITH COMMENTS - Well-implemented and ready to merge
- **Related Issues**: None (this is a new validation feature)
Conceived by Romuald Członkowski - [www.aiadvisors.pl/en](https://www.aiadvisors.pl/en)
## [2.22.11] - 2025-01-06
### ✨ New Features
**Issue #399: Workflow Activation via Diff Operations**
Added workflow activation and deactivation as diff operations in `n8n_update_partial_workflow`, using n8n's dedicated API endpoints.
#### Problem
The n8n API provides dedicated `POST /workflows/{id}/activate` and `POST /workflows/{id}/deactivate` endpoints, but these were not accessible through n8n-mcp. Users could not programmatically control workflow activation status, forcing manual activation through the n8n UI.
#### Solution
Implemented activation/deactivation as diff operations, following the established pattern of metadata operations like `updateSettings` and `updateName`. This keeps the tool count manageable (40 tools, not 42) and provides a consistent interface.
#### Changes
**API Client** (`src/services/n8n-api-client.ts`):
- Added `activateWorkflow(id: string): Promise<Workflow>` method
- Added `deactivateWorkflow(id: string): Promise<Workflow>` method
- Both use POST requests to dedicated n8n API endpoints
**Diff Engine Types** (`src/types/workflow-diff.ts`):
- Added `ActivateWorkflowOperation` interface
- Added `DeactivateWorkflowOperation` interface
- Added `shouldActivate` and `shouldDeactivate` flags to `WorkflowDiffResult`
- Increased supported operations from 15 to 17
**Diff Engine** (`src/services/workflow-diff-engine.ts`):
- Added validation for activation (requires activatable triggers)
- Added operation application logic
- Transfers activation intent from workflow object to result
- Validates workflow has activatable triggers (webhook, schedule, etc.)
- Rejects workflows with only `executeWorkflowTrigger` (cannot activate)
**Handler** (`src/mcp/handlers-workflow-diff.ts`):
- Checks `shouldActivate` and `shouldDeactivate` flags after workflow update
- Calls appropriate API methods
- Includes activation status in response message and details
- Handles activation/deactivation errors gracefully
**Documentation** (`src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts`):
- Updated operation count from 15 to 17
- Added "Workflow Activation Operations" section
- Added activation tip to essentials
**Tool Registration** (`src/mcp/handlers-n8n-manager.ts`):
- Removed "Cannot activate/deactivate workflows via API" from limitations
#### Usage
```javascript
// Activate workflow
n8n_update_partial_workflow({
id: "workflow_id",
operations: [{
type: "activateWorkflow"
}]
})
// Deactivate workflow
n8n_update_partial_workflow({
id: "workflow_id",
operations: [{
type: "deactivateWorkflow"
}]
})
// Combine with other operations
n8n_update_partial_workflow({
id: "workflow_id",
operations: [
{type: "updateNode", nodeId: "abc", updates: {name: "Updated"}},
{type: "activateWorkflow"}
]
})
```
#### Validation
- **Activation**: Requires at least one enabled activatable trigger node
- **Deactivation**: Always valid
- **Error Handling**: Clear messages when activation fails due to missing triggers
- **Trigger Detection**: Uses `isActivatableTrigger()` utility (Issue #351 compliance)
#### Benefits
- ✅ Consistent with existing architecture (metadata operations pattern)
- ✅ Keeps tool count at 40 (not 42)
- ✅ Atomic operations - activation happens after workflow update
- ✅ Proper validation - prevents activation without triggers
- ✅ Clear error messages - guides users on trigger requirements
- ✅ Works with other operations - can update and activate in one call
#### Credits
- **@ArtemisAI** - Original investigation and API endpoint discovery
- **@cmj-hub** - Implementation attempt and PR contribution
- Architectural guidance from project maintainer
Resolves #399
Conceived by Romuald Członkowski - [www.aiadvisors.pl/en](https://www.aiadvisors.pl/en)
## [2.22.10] - 2025-11-04
### 🐛 Bug Fixes
**sql.js Fallback: Fixed Database Health Check Crash**
Fixed critical startup crash when the server falls back to sql.js adapter (used when better-sqlite3 fails to load, such as Node.js version mismatches between build and runtime).
#### Problem
When Claude Desktop was configured to use a different Node.js version than the one used to build the project:
- better-sqlite3 fails to load due to NODE_MODULE_VERSION mismatch (e.g., built with Node v22, running with Node v20)
- System gracefully falls back to sql.js adapter (pure JavaScript, no native dependencies)
- **BUT** the database health check crashed with "no such module: fts5" error
- Server exits immediately after startup, preventing connection
**Error Details:**
```
[ERROR] Database health check failed: Error: no such module: fts5
at e.handleError (sql-wasm.js:90:371)
at e.prepare (sql-wasm.js:89:104)
at SQLJSAdapter.prepare (database-adapter.js:202:30)
at N8NDocumentationMCPServer.validateDatabaseHealth (server.js:251:42)
```
**Root Cause:** The health check attempted to query the FTS5 (Full-Text Search) table, which is not available in sql.js. The error was not caught, causing the server to exit.
#### Solution
Wrapped the FTS5 health check in a try-catch block to handle sql.js gracefully:
```typescript
// Check if FTS5 table exists (wrap in try-catch for sql.js compatibility)
try {
const ftsExists = this.db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name='nodes_fts'
`).get();
if (!ftsExists) {
logger.warn('FTS5 table missing - search performance will be degraded...');
} else {
const ftsCount = this.db.prepare('SELECT COUNT(*) as count FROM nodes_fts').get();
if (ftsCount.count === 0) {
logger.warn('FTS5 index is empty - search will not work properly...');
}
}
} catch (ftsError) {
// FTS5 not supported (e.g., sql.js fallback) - this is OK, just warn
logger.warn('FTS5 not available - using fallback search. For better performance, ensure better-sqlite3 is properly installed.');
}
```
#### Impact
**Before Fix:**
- ❌ Server crashed immediately when using sql.js fallback
- ❌ Claude Desktop connection failed with Node.js version mismatches
- ❌ No way to use the MCP server without matching Node.js versions exactly
**After Fix:**
- ✅ Server starts successfully with sql.js fallback
- ✅ Works with any Node.js version (graceful degradation)
- ✅ Clear warning about FTS5 unavailability in logs
- ✅ Users can choose between sql.js (slower, works everywhere) or rebuilding better-sqlite3 (faster, requires matching Node version)
#### Performance Notes
When using sql.js fallback:
- Full-text search (FTS5) is not available, falls back to LIKE queries
- Slightly slower search performance (~10-30ms vs ~5ms with FTS5)
- All other functionality works identically
- Database operations work correctly
**Recommendation:** For best performance, ensure better-sqlite3 loads successfully by matching Node.js versions or rebuilding:
```bash
# If Node version mismatch, rebuild better-sqlite3
npm rebuild better-sqlite3
```
#### Files Changed
**Modified (1 file):**
- `src/mcp/server.ts` (lines 299-317) - Added try-catch around FTS5 health check
#### Testing
- ✅ Tested with Node v20.17.0 (Claude Desktop version)
- ✅ Tested with Node v22.17.0 (build version)
- ✅ Server starts successfully in both cases
- ✅ sql.js fallback works correctly with graceful FTS5 degradation
- ✅ All 6 startup checkpoints pass
- ✅ Database health check passes with warning
Conceived by Romuald Członkowski - [www.aiadvisors.pl/en](https://www.aiadvisors.pl/en)
## [2.22.9] - 2025-11-04
### 🔄 Dependencies Update
**n8n Platform Update to 1.118.1**
Updated n8n and all related dependencies to the latest versions:
- **n8n**: 1.117.2 → 1.118.1
- **n8n-core**: 1.116.0 → 1.117.0
- **n8n-workflow**: 1.114.0 → 1.115.0
- **@n8n/n8n-nodes-langchain**: 1.116.2 → 1.117.0
### 📊 Database Changes
- Rebuilt node database with **542 nodes**
- 439 nodes from n8n-nodes-base
- 103 nodes from @n8n/n8n-nodes-langchain
- All node metadata synchronized with latest n8n release
### 🐛 Bug Fixes
**n8n 1.118.1+ Compatibility: Fixed versionCounter API Rejection**
Fixed integration test failures caused by n8n 1.118.1 API change where `versionCounter` property is returned in GET responses but rejected in PUT requests.
**Impact**:
- Integration tests were failing with "request/body must NOT have additional properties" error
- Workflow update operations via n8n API were failing
**Solution**:
- Added `versionCounter` to property exclusion list in `cleanWorkflowForUpdate()` (src/services/n8n-validation.ts:136)
- Added `versionCounter?: number` type definition to Workflow and WorkflowExport interfaces
- Added test coverage to prevent regression
### ✅ Verification
- Database rebuild completed successfully
- All node types validated
- Documentation mappings updated
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
## [2.22.7] - 2025-10-26
### 📝 Documentation Fixes

View File

@@ -1,5 +1,87 @@
# n8n Update Process - Quick Reference
## ⚡ Recommended Fast Workflow (2025-11-04)
**CRITICAL FIRST STEP**: Check existing releases to avoid version conflicts!
```bash
# 1. CHECK EXISTING RELEASES FIRST (prevents version conflicts!)
gh release list | head -5
# Look at the latest version - your new version must be higher!
# 2. Switch to main and pull
git checkout main && git pull
# 3. Check for updates (dry run)
npm run update:n8n:check
# 4. Run update and skip tests (we'll test in CI)
yes y | npm run update:n8n
# 5. Create feature branch
git checkout -b update/n8n-X.X.X
# 6. Update version in package.json (must be HIGHER than latest release!)
# Edit: "version": "2.XX.X" (not the version from the release list!)
# 7. Update CHANGELOG.md
# - Change version number to match package.json
# - Update date to today
# - Update dependency versions
# 8. Update README badge
# Edit line 8: Change n8n version badge to new n8n version
# 9. Commit and push
git add -A
git commit -m "chore: update n8n to X.X.X and bump version to 2.XX.X
- Updated n8n from X.X.X to X.X.X
- Updated n8n-core from X.X.X to X.X.X
- Updated n8n-workflow from X.X.X to X.X.X
- Updated @n8n/n8n-nodes-langchain from X.X.X to X.X.X
- Rebuilt node database with XXX nodes (XXX from n8n-nodes-base, XXX from @n8n/n8n-nodes-langchain)
- Updated README badge with new n8n version
- Updated CHANGELOG with dependency changes
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>"
git push -u origin update/n8n-X.X.X
# 10. Create PR
gh pr create --title "chore: update n8n to X.X.X" --body "Updates n8n and all related dependencies to the latest versions..."
# 11. After PR is merged, verify release triggered
gh release list | head -1
# If the new version appears, you're done!
# If not, the version might have already been released - bump version again and create new PR
```
### Why This Workflow?
**Fast**: Skip local tests (2-3 min saved) - CI runs them anyway
**Safe**: Unit tests in CI verify compatibility
**Clean**: All changes in one PR with proper tracking
**Automatic**: Release workflow triggers on merge if version is new
### Common Issues
**Problem**: Release workflow doesn't trigger after merge
**Cause**: Version number was already released (check `gh release list`)
**Solution**: Create new PR bumping version by one patch number
**Problem**: Integration tests fail in CI with "unauthorized"
**Cause**: n8n test instance credentials expired (infrastructure issue)
**Solution**: Ignore if unit tests pass - this is not a code problem
**Problem**: CI takes 8+ minutes
**Reason**: Integration tests need live n8n instance (slow)
**Normal**: Unit tests (~2 min) + integration tests (~6 min) = ~8 min total
## Quick One-Command Update
For a complete update with tests and publish preparation:
@@ -99,12 +181,14 @@ This command:
## Important Notes
1. **Always run on main branch** - Make sure you're on main and it's clean
2. **The update script is smart** - It automatically syncs all n8n dependencies to compatible versions
3. **Tests are required** - The publish script now runs tests automatically
4. **Database rebuild is automatic** - The update script handles this for you
5. **Template sanitization is automatic** - Any API tokens in workflow templates are replaced with placeholders
6. **Docker image builds automatically** - Pushing to GitHub triggers the workflow
1. **ALWAYS check existing releases first** - Use `gh release list` to see what versions are already released. Your new version must be higher!
2. **Release workflow only triggers on version CHANGE** - If you merge a PR with an already-released version (e.g., 2.22.8), the workflow won't run. You'll need to bump to a new version (e.g., 2.22.9) and create another PR.
3. **Integration test failures in CI are usually infrastructure issues** - If unit tests pass but integration tests fail with "unauthorized", this is typically because the test n8n instance credentials need updating. The code itself is fine.
4. **Skip local tests - let CI handle them** - Running tests locally adds 2-3 minutes with no benefit since CI runs them anyway. The fast workflow skips local tests.
5. **The update script is smart** - It automatically syncs all n8n dependencies to compatible versions
6. **Database rebuild is automatic** - The update script handles this for you
7. **Template sanitization is automatic** - Any API tokens in workflow templates are replaced with placeholders
8. **Docker image builds automatically** - Pushing to GitHub triggers the workflow
## GitHub Push Protection
@@ -115,11 +199,27 @@ As of July 2025, GitHub's push protection may block database pushes if they cont
3. If push is still blocked, use the GitHub web interface to review and allow the push
## Time Estimate
### Fast Workflow (Recommended)
- Local work: ~2-3 minutes
- npm install and database rebuild: ~2-3 minutes
- File edits (CHANGELOG, README, package.json): ~30 seconds
- Git operations (commit, push, create PR): ~30 seconds
- CI testing after PR creation: ~8-10 minutes (runs automatically)
- Unit tests: ~2 minutes
- Integration tests: ~6 minutes (may fail with infrastructure issues - ignore if unit tests pass)
- Other checks: ~1 minute
**Total hands-on time: ~3 minutes** (then wait for CI)
### Full Workflow with Local Tests
- Total time: ~5-7 minutes
- Test suite: ~2.5 minutes
- npm install and database rebuild: ~2-3 minutes
- The rest: seconds
**Note**: The fast workflow is recommended since CI runs the same tests anyway.
## Troubleshooting
If tests fail:

View File

@@ -54,6 +54,10 @@ Collected data is used solely to:
- Identify common error patterns
- Improve tool performance and reliability
- Guide development priorities
- Train machine learning models for workflow generation
All ML training uses sanitized, anonymized data only.
Users can opt-out at any time with `npx n8n-mcp telemetry disable`
## Data Retention
- Data is retained for analysis purposes
@@ -66,4 +70,4 @@ We may update this privacy policy from time to time. Updates will be reflected i
For questions about telemetry or privacy, please open an issue on GitHub:
https://github.com/czlonkowski/n8n-mcp/issues
Last updated: 2025-09-25
Last updated: 2025-11-06

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.116.2-orange.svg)](https://github.com/n8n-io/n8n)
[![n8n version](https://img.shields.io/badge/n8n-1.118.1-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 525+ 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 541 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:
- 📚 **536 n8n nodes** from both n8n-nodes-base and @n8n/n8n-nodes-langchain
- 📚 **541 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** - 90% coverage from official n8n docs (including AI nodes)
- 🤖 **AI tools** - 263 AI-capable nodes detected with full documentation
- 📄 **Documentation** - 87% coverage from official n8n docs (including AI nodes)
- 🤖 **AI tools** - 271 AI-capable nodes detected with full documentation
- 💡 **Real-world examples** - 2,646 pre-extracted configurations from popular templates
- 🎯 **Template library** - 2,500+ workflow templates with smart filtering
- 🎯 **Template library** - 2,709 workflow templates with 100% metadata coverage
## ⚠️ Important Safety Warning
@@ -51,6 +51,8 @@ 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
{
@@ -531,7 +533,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,500+ available).
ALWAYS check templates before building from scratch (2,709 available).
### 4. Multi-Level Validation
Use validate_node_minimal → validate_node_operation → validate_workflow pattern.
@@ -840,7 +842,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,500+ available)
3. **Templates first** - Always check before building (2,709 available)
4. **Multi-level validation** - Quick check → Full validation → Workflow validation
5. **Never trust defaults** - Explicitly configure ALL parameters
@@ -943,7 +945,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,500+ templates)
- **`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
@@ -1098,17 +1100,17 @@ npm run dev:http # HTTP dev mode
## 📊 Metrics & Coverage
Current database coverage (n8n v1.113.3):
Current database coverage (n8n v1.117.2):
- ✅ **536/536** nodes loaded (100%)
- ✅ **528** nodes with properties (98.7%)
- ✅ **470** nodes with documentation (88%)
- ✅ **267** AI-capable tools detected
- ✅ **541/541** nodes loaded (100%)
- ✅ **541** nodes with properties (100%)
- ✅ **470** nodes with documentation (87%)
- ✅ **271** AI-capable tools detected
- ✅ **2,646** pre-extracted template configurations
- ✅ **2,500+** workflow templates available
- ✅ **2,709** workflow templates available (100% metadata coverage)
- ✅ **AI Agent & LangChain nodes** fully documented
- ⚡ **Average response time**: ~12ms
- 💾 **Database size**: ~15MB (optimized)
- 💾 **Database size**: ~68MB (includes templates with metadata)
## 🔄 Recent Updates

Binary file not shown.

View File

@@ -4,7 +4,9 @@ Connect n8n-MCP to Claude Code CLI for enhanced n8n workflow development from th
## Quick Setup via CLI
### Basic configuration (documentation tools only):
### Basic configuration (documentation tools only)
**For Linux, macOS, or Windows (WSL/Git Bash):**
```bash
claude mcp add n8n-mcp \
-e MCP_MODE=stdio \
@@ -13,9 +15,21 @@ 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):
### Full configuration (with n8n management tools)
**For Linux, macOS, or Windows (WSL/Git Bash):**
```bash
claude mcp add n8n-mcp \
-e MCP_MODE=stdio \
@@ -26,6 +40,18 @@ 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
@@ -133,9 +159,11 @@ 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 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
- 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.

View File

@@ -162,7 +162,7 @@ n8n_validate_workflow({id: createdWorkflowId})
n8n_update_partial_workflow({
workflowId: id,
operations: [
{type: 'updateNode', nodeId: 'slack1', changes: {position: [100, 200]}}
{type: 'updateNode', nodeId: 'slack1', updates: {position: [100, 200]}}
]
})

1923
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.7",
"version": "2.22.12",
"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.115.1",
"@n8n/n8n-nodes-langchain": "^1.117.0",
"@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.116.2",
"n8n-core": "^1.115.1",
"n8n-workflow": "^1.113.0",
"n8n": "^1.118.1",
"n8n-core": "^1.117.0",
"n8n-workflow": "^1.115.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.7",
"version": "2.22.11",
"description": "n8n MCP Server Runtime Dependencies Only",
"private": true,
"dependencies": {

View File

@@ -0,0 +1,99 @@
#!/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

@@ -1561,7 +1561,6 @@ export async function handleListAvailableTools(context?: InstanceContext): Promi
maxRetries: config.maxRetries
} : null,
limitations: [
'Cannot activate/deactivate workflows via API',
'Cannot execute workflows directly (must use webhooks)',
'Cannot stop running executions',
'Tags and credentials have limited API support'

View File

@@ -245,15 +245,52 @@ export async function handleUpdatePartialWorkflow(
// Update workflow via API
try {
const updatedWorkflow = await client.updateWorkflow(input.id, diffResult.workflow!);
// Handle activation/deactivation if requested
let finalWorkflow = updatedWorkflow;
let activationMessage = '';
if (diffResult.shouldActivate) {
try {
finalWorkflow = await client.activateWorkflow(input.id);
activationMessage = ' Workflow activated.';
} catch (activationError) {
logger.error('Failed to activate workflow after update', activationError);
return {
success: false,
error: 'Workflow updated successfully but activation failed',
details: {
workflowUpdated: true,
activationError: activationError instanceof Error ? activationError.message : 'Unknown error'
}
};
}
} else if (diffResult.shouldDeactivate) {
try {
finalWorkflow = await client.deactivateWorkflow(input.id);
activationMessage = ' Workflow deactivated.';
} catch (deactivationError) {
logger.error('Failed to deactivate workflow after update', deactivationError);
return {
success: false,
error: 'Workflow updated successfully but deactivation failed',
details: {
workflowUpdated: true,
deactivationError: deactivationError instanceof Error ? deactivationError.message : 'Unknown error'
}
};
}
}
return {
success: true,
data: updatedWorkflow,
message: `Workflow "${updatedWorkflow.name}" updated successfully. Applied ${diffResult.operationsApplied} operations.`,
data: finalWorkflow,
message: `Workflow "${finalWorkflow.name}" updated successfully. Applied ${diffResult.operationsApplied} operations.${activationMessage}`,
details: {
operationsApplied: diffResult.operationsApplied,
workflowId: updatedWorkflow.id,
workflowName: updatedWorkflow.name,
workflowId: finalWorkflow.id,
workflowName: finalWorkflow.name,
active: finalWorkflow.active,
applied: diffResult.applied,
failed: diffResult.failed,
errors: diffResult.errors,

View File

@@ -296,19 +296,24 @@ export class N8NDocumentationMCPServer {
throw new Error('Database is empty. Run "npm run rebuild" to populate node data.');
}
// Check if FTS5 table exists
const ftsExists = this.db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name='nodes_fts'
`).get();
// Check if FTS5 table exists (wrap in try-catch for sql.js compatibility)
try {
const ftsExists = this.db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name='nodes_fts'
`).get();
if (!ftsExists) {
logger.warn('FTS5 table missing - search performance will be degraded. Please run: npm run rebuild');
} else {
const ftsCount = this.db.prepare('SELECT COUNT(*) as count FROM nodes_fts').get() as { count: number };
if (ftsCount.count === 0) {
logger.warn('FTS5 index is empty - search will not work properly. Please run: npm run rebuild');
if (!ftsExists) {
logger.warn('FTS5 table missing - search performance will be degraded. Please run: npm run rebuild');
} else {
const ftsCount = this.db.prepare('SELECT COUNT(*) as count FROM nodes_fts').get() as { count: number };
if (ftsCount.count === 0) {
logger.warn('FTS5 index is empty - search will not work properly. Please run: npm run rebuild');
}
}
} catch (ftsError) {
// FTS5 not supported (e.g., sql.js fallback) - this is OK, just warn
logger.warn('FTS5 not available - using fallback search. For better performance, ensure better-sqlite3 is properly installed.');
}
logger.info(`Database health check passed: ${nodeCount.count} nodes loaded`);

View File

@@ -4,7 +4,7 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
name: 'n8n_update_partial_workflow',
category: 'workflow_management',
essentials: {
description: 'Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, rewireConnection, cleanStaleConnections, replaceConnections, updateSettings, updateName, add/removeTag. Supports smart parameters (branch, case) for multi-output nodes. Full support for AI connections (ai_languageModel, ai_tool, ai_memory, ai_embedding, ai_vectorStore, ai_document, ai_textSplitter, ai_outputParser).',
description: 'Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, rewireConnection, cleanStaleConnections, replaceConnections, updateSettings, updateName, add/removeTag, activateWorkflow, deactivateWorkflow. Supports smart parameters (branch, case) for multi-output nodes. Full support for AI connections (ai_languageModel, ai_tool, ai_memory, ai_embedding, ai_vectorStore, ai_document, ai_textSplitter, ai_outputParser).',
keyParameters: ['id', 'operations', 'continueOnError'],
example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "rewireConnection", source: "IF", from: "Old", to: "New", branch: "true"}]})',
performance: 'Fast (50-200ms)',
@@ -19,11 +19,12 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
'For AI connections, specify sourceOutput type (ai_languageModel, ai_tool, etc.)',
'Batch AI component connections for atomic updates',
'Auto-sanitization: ALL nodes auto-fixed during updates (operator structures, missing metadata)',
'Node renames automatically update all connection references - no manual connection operations needed'
'Node renames automatically update all connection references - no manual connection operations needed',
'Activate/deactivate workflows: Use activateWorkflow/deactivateWorkflow operations (requires activatable triggers like webhook/schedule)'
]
},
full: {
description: `Updates workflows using surgical diff operations instead of full replacement. Supports 15 operation types for precise modifications. Operations are validated and applied atomically by default - all succeed or none are applied.
description: `Updates workflows using surgical diff operations instead of full replacement. Supports 17 operation types for precise modifications. Operations are validated and applied atomically by default - all succeed or none are applied.
## Available Operations:
@@ -48,6 +49,10 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
- **addTag**: Add a workflow tag
- **removeTag**: Remove a workflow tag
### Workflow Activation Operations (2 types):
- **activateWorkflow**: Activate the workflow to enable automatic execution via triggers
- **deactivateWorkflow**: Deactivate the workflow to prevent automatic execution
## Smart Parameters for Multi-Output Nodes
For **IF nodes**, use semantic 'branch' parameter instead of technical sourceIndex:

View File

@@ -75,10 +75,15 @@ async function fetchTemplatesRobust() {
// Fetch detail
const detail = await fetcher.fetchTemplateDetail(template.id);
// Save immediately
repository.saveTemplate(template, detail);
saved++;
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`);
}
// Rate limiting
await new Promise(resolve => setTimeout(resolve, 200));

View File

@@ -170,6 +170,24 @@ export class N8nApiClient {
}
}
async activateWorkflow(id: string): Promise<Workflow> {
try {
const response = await this.client.post(`/workflows/${id}/activate`);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async deactivateWorkflow(id: string): Promise<Workflow> {
try {
const response = await this.client.post(`/workflows/${id}/deactivate`);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
/**
* Lists workflows from n8n instance.
*

View File

@@ -133,6 +133,7 @@ export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> {
createdAt,
updatedAt,
versionId,
versionCounter, // Added: n8n 1.118.1+ returns this but rejects it in updates
meta,
staticData,
// Remove fields that cause API errors

View File

@@ -25,6 +25,8 @@ import {
UpdateNameOperation,
AddTagOperation,
RemoveTagOperation,
ActivateWorkflowOperation,
DeactivateWorkflowOperation,
CleanStaleConnectionsOperation,
ReplaceConnectionsOperation
} from '../types/workflow-diff';
@@ -32,6 +34,7 @@ import { Workflow, WorkflowNode, WorkflowConnection } from '../types/n8n-api';
import { Logger } from '../utils/logger';
import { validateWorkflowNode, validateWorkflowConnections } from './n8n-validation';
import { sanitizeNode, sanitizeWorkflowNodes } from './node-sanitizer';
import { isActivatableTrigger } from '../utils/node-type-utils';
const logger = new Logger({ prefix: '[WorkflowDiffEngine]' });
@@ -214,12 +217,23 @@ export class WorkflowDiffEngine {
}
const operationsApplied = request.operations.length;
// Extract activation flags from workflow object
const shouldActivate = (workflowCopy as any)._shouldActivate === true;
const shouldDeactivate = (workflowCopy as any)._shouldDeactivate === true;
// Clean up temporary flags
delete (workflowCopy as any)._shouldActivate;
delete (workflowCopy as any)._shouldDeactivate;
return {
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
warnings: this.warnings.length > 0 ? this.warnings : undefined,
shouldActivate: shouldActivate || undefined,
shouldDeactivate: shouldDeactivate || undefined
};
}
} catch (error) {
@@ -262,6 +276,10 @@ export class WorkflowDiffEngine {
case 'addTag':
case 'removeTag':
return null; // These are always valid
case 'activateWorkflow':
return this.validateActivateWorkflow(workflow, operation);
case 'deactivateWorkflow':
return this.validateDeactivateWorkflow(workflow, operation);
case 'cleanStaleConnections':
return this.validateCleanStaleConnections(workflow, operation);
case 'replaceConnections':
@@ -315,6 +333,12 @@ export class WorkflowDiffEngine {
case 'removeTag':
this.applyRemoveTag(workflow, operation);
break;
case 'activateWorkflow':
this.applyActivateWorkflow(workflow, operation);
break;
case 'deactivateWorkflow':
this.applyDeactivateWorkflow(workflow, operation);
break;
case 'cleanStaleConnections':
this.applyCleanStaleConnections(workflow, operation);
break;
@@ -373,6 +397,17 @@ export class WorkflowDiffEngine {
}
private validateUpdateNode(workflow: Workflow, operation: UpdateNodeOperation): string | null {
// Check for common parameter mistake: "changes" instead of "updates" (Issue #392)
const operationAny = operation as any;
if (operationAny.changes && !operation.updates) {
return `Invalid parameter 'changes'. The updateNode operation requires 'updates' (not 'changes'). Example: {type: "updateNode", nodeId: "abc", updates: {name: "New Name", "parameters.url": "https://example.com"}}`;
}
// Check for missing required parameter
if (!operation.updates) {
return `Missing required parameter 'updates'. The updateNode operation requires an 'updates' object containing properties to modify. Example: {type: "updateNode", nodeId: "abc", updates: {name: "New Name"}}`;
}
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
if (!node) {
return this.formatNodeNotFoundError(workflow, operation.nodeId || operation.nodeName || '', 'updateNode');
@@ -847,13 +882,46 @@ export class WorkflowDiffEngine {
private applyRemoveTag(workflow: Workflow, operation: RemoveTagOperation): void {
if (!workflow.tags) return;
const index = workflow.tags.indexOf(operation.tag);
if (index !== -1) {
workflow.tags.splice(index, 1);
}
}
// Workflow activation operation validators
private validateActivateWorkflow(workflow: Workflow, operation: ActivateWorkflowOperation): string | null {
// Check if workflow has at least one activatable trigger
// Issue #351: executeWorkflowTrigger cannot activate workflows
const activatableTriggers = workflow.nodes.filter(
node => !node.disabled && isActivatableTrigger(node.type)
);
if (activatableTriggers.length === 0) {
return 'Cannot activate workflow: No activatable trigger nodes found. Workflows must have at least one enabled trigger node (webhook, schedule, email, etc.). Note: executeWorkflowTrigger cannot activate workflows as they can only be invoked by other workflows.';
}
return null;
}
private validateDeactivateWorkflow(workflow: Workflow, operation: DeactivateWorkflowOperation): string | null {
// Deactivation is always valid - any workflow can be deactivated
return null;
}
// Workflow activation operation appliers
private applyActivateWorkflow(workflow: Workflow, operation: ActivateWorkflowOperation): void {
// Set flag in workflow object to indicate activation intent
// The handler will call the API method after workflow update
(workflow as any)._shouldActivate = true;
}
private applyDeactivateWorkflow(workflow: Workflow, operation: DeactivateWorkflowOperation): void {
// Set flag in workflow object to indicate deactivation intent
// The handler will call the API method after workflow update
(workflow as any)._shouldDeactivate = true;
}
// Connection cleanup operation validators
private validateCleanStaleConnections(workflow: Workflow, operation: CleanStaleConnectionsOperation): string | null {
// This operation is always valid - it just cleans up what it finds

View File

@@ -40,7 +40,37 @@ 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
@@ -73,93 +103,105 @@ export class TemplateFetcher {
let page = 1;
let hasMore = true;
let totalWorkflows = 0;
logger.info('Starting complete template fetch from n8n.io API');
while (hasMore) {
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;
}
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`);
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)
}
} catch (error) {
logger.error(`Error fetching templates page ${page}:`, error);
throw error;
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)
}
}
logger.info(`Fetched all ${allTemplates.length} templates from n8n.io`);
return allTemplates;
}
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 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 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];
try {
const detail = await this.fetchTemplateDetail(workflow.id);
const detail = await this.fetchTemplateDetail(workflow.id);
if (detail !== null) {
details.set(workflow.id, detail);
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
} 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
}
logger.info(`Successfully fetched ${details.size} template details`);
logger.info(`Successfully fetched ${details.size} template details (${skipped} skipped)`);
return details;
}

View File

@@ -496,10 +496,17 @@ export class TemplateRepository {
// Count node usage
const nodeCount: Record<string, number> = {};
topNodes.forEach(t => {
const nodes = JSON.parse(t.nodes_used);
nodes.forEach((n: string) => {
nodeCount[n] = (nodeCount[n] || 0) + 1;
});
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);
}
});
// Get top 10 most used nodes

View File

@@ -66,6 +66,7 @@ export interface Workflow {
updatedAt?: string;
createdAt?: string;
versionId?: string;
versionCounter?: number; // Added: n8n 1.118.1+ returns this in GET responses
meta?: {
instanceId?: string;
};
@@ -152,6 +153,7 @@ export interface WorkflowExport {
tags?: string[];
pinData?: Record<string, unknown>;
versionId?: string;
versionCounter?: number; // Added: n8n 1.118.1+
meta?: Record<string, unknown>;
}

View File

@@ -114,6 +114,16 @@ export interface RemoveTagOperation extends DiffOperation {
tag: string;
}
export interface ActivateWorkflowOperation extends DiffOperation {
type: 'activateWorkflow';
// No additional properties needed - just activates the workflow
}
export interface DeactivateWorkflowOperation extends DiffOperation {
type: 'deactivateWorkflow';
// No additional properties needed - just deactivates the workflow
}
// Connection Cleanup Operations
export interface CleanStaleConnectionsOperation extends DiffOperation {
type: 'cleanStaleConnections';
@@ -148,6 +158,8 @@ export type WorkflowDiffOperation =
| UpdateNameOperation
| AddTagOperation
| RemoveTagOperation
| ActivateWorkflowOperation
| DeactivateWorkflowOperation
| CleanStaleConnectionsOperation
| ReplaceConnectionsOperation;
@@ -176,6 +188,8 @@ export interface WorkflowDiffResult {
applied?: number[]; // Indices of successfully applied operations (when continueOnError is true)
failed?: number[]; // Indices of failed operations (when continueOnError is true)
staleConnectionsRemoved?: Array<{ from: string; to: string }>; // For cleanStaleConnections operation
shouldActivate?: boolean; // Flag to activate workflow after update (for activateWorkflow operation)
shouldDeactivate?: boolean; // Flag to deactivate workflow after update (for deactivateWorkflow operation)
}
// Helper type for node reference (supports both ID and name)

View File

@@ -101,7 +101,6 @@ describe('Integration: handleListAvailableTools', () => {
// Common known limitations
const limitationsText = data.limitations.join(' ');
expect(limitationsText).toContain('Cannot activate');
expect(limitationsText).toContain('Cannot execute workflows directly');
});
});

View File

@@ -156,9 +156,11 @@ describe('handlers-workflow-diff', () => {
operationsApplied: 1,
workflowId: 'test-workflow-id',
workflowName: 'Test Workflow',
active: true,
applied: [0],
failed: [],
errors: [],
warnings: undefined,
},
});
@@ -633,5 +635,211 @@ describe('handlers-workflow-diff', () => {
},
});
});
describe('Workflow Activation/Deactivation', () => {
it('should activate workflow after successful update', async () => {
const testWorkflow = createTestWorkflow({ active: false });
const updatedWorkflow = { ...testWorkflow, active: false };
const activatedWorkflow = { ...testWorkflow, active: true };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldActivate: true,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.activateWorkflow = vi.fn().mockResolvedValue(activatedWorkflow);
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'activateWorkflow' }],
}, mockRepository);
expect(result.success).toBe(true);
expect(result.data).toEqual(activatedWorkflow);
expect(result.message).toContain('Workflow activated');
expect(result.details?.active).toBe(true);
expect(mockApiClient.activateWorkflow).toHaveBeenCalledWith('test-workflow-id');
});
it('should deactivate workflow after successful update', async () => {
const testWorkflow = createTestWorkflow({ active: true });
const updatedWorkflow = { ...testWorkflow, active: true };
const deactivatedWorkflow = { ...testWorkflow, active: false };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldDeactivate: true,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.deactivateWorkflow = vi.fn().mockResolvedValue(deactivatedWorkflow);
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'deactivateWorkflow' }],
}, mockRepository);
expect(result.success).toBe(true);
expect(result.data).toEqual(deactivatedWorkflow);
expect(result.message).toContain('Workflow deactivated');
expect(result.details?.active).toBe(false);
expect(mockApiClient.deactivateWorkflow).toHaveBeenCalledWith('test-workflow-id');
});
it('should handle activation failure after successful update', async () => {
const testWorkflow = createTestWorkflow({ active: false });
const updatedWorkflow = { ...testWorkflow, active: false };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldActivate: true,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.activateWorkflow = vi.fn().mockRejectedValue(new Error('Activation failed: No trigger nodes'));
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'activateWorkflow' }],
}, mockRepository);
expect(result.success).toBe(false);
expect(result.error).toBe('Workflow updated successfully but activation failed');
expect(result.details).toEqual({
workflowUpdated: true,
activationError: 'Activation failed: No trigger nodes',
});
});
it('should handle deactivation failure after successful update', async () => {
const testWorkflow = createTestWorkflow({ active: true });
const updatedWorkflow = { ...testWorkflow, active: true };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldDeactivate: true,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.deactivateWorkflow = vi.fn().mockRejectedValue(new Error('Deactivation failed'));
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'deactivateWorkflow' }],
}, mockRepository);
expect(result.success).toBe(false);
expect(result.error).toBe('Workflow updated successfully but deactivation failed');
expect(result.details).toEqual({
workflowUpdated: true,
deactivationError: 'Deactivation failed',
});
});
it('should update workflow without activation when shouldActivate is false', async () => {
const testWorkflow = createTestWorkflow({ active: false });
const updatedWorkflow = { ...testWorkflow, active: false };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldActivate: false,
shouldDeactivate: false,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.activateWorkflow = vi.fn();
mockApiClient.deactivateWorkflow = vi.fn();
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'updateName', name: 'Updated' }],
}, mockRepository);
expect(result.success).toBe(true);
expect(result.message).not.toContain('activated');
expect(result.message).not.toContain('deactivated');
expect(mockApiClient.activateWorkflow).not.toHaveBeenCalled();
expect(mockApiClient.deactivateWorkflow).not.toHaveBeenCalled();
});
it('should handle non-Error activation failures', async () => {
const testWorkflow = createTestWorkflow({ active: false });
const updatedWorkflow = { ...testWorkflow, active: false };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldActivate: true,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.activateWorkflow = vi.fn().mockRejectedValue('String error');
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'activateWorkflow' }],
}, mockRepository);
expect(result.success).toBe(false);
expect(result.error).toBe('Workflow updated successfully but activation failed');
expect(result.details).toEqual({
workflowUpdated: true,
activationError: 'Unknown error',
});
});
it('should handle non-Error deactivation failures', async () => {
const testWorkflow = createTestWorkflow({ active: true });
const updatedWorkflow = { ...testWorkflow, active: true };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldDeactivate: true,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.deactivateWorkflow = vi.fn().mockRejectedValue({ code: 'UNKNOWN' });
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'deactivateWorkflow' }],
}, mockRepository);
expect(result.success).toBe(false);
expect(result.error).toBe('Workflow updated successfully but deactivation failed');
expect(result.details).toEqual({
workflowUpdated: true,
deactivationError: 'Unknown error',
});
});
});
});
});

View File

@@ -362,19 +362,19 @@ describe('N8nApiClient', () => {
it('should delete workflow successfully', async () => {
mockAxiosInstance.delete.mockResolvedValue({ data: {} });
await client.deleteWorkflow('123');
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/workflows/123');
});
it('should handle deletion error', async () => {
const error = {
const error = {
message: 'Request failed',
response: { status: 404, data: { message: 'Not found' } }
response: { status: 404, data: { message: 'Not found' } }
};
await mockAxiosInstance.simulateError('delete', error);
try {
await client.deleteWorkflow('123');
expect.fail('Should have thrown an error');
@@ -386,6 +386,178 @@ describe('N8nApiClient', () => {
});
});
describe('activateWorkflow', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should activate workflow successfully', async () => {
const workflow = { id: '123', name: 'Test', active: false, nodes: [], connections: {} };
const activatedWorkflow = { ...workflow, active: true };
mockAxiosInstance.post.mockResolvedValue({ data: activatedWorkflow });
const result = await client.activateWorkflow('123');
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/workflows/123/activate');
expect(result).toEqual(activatedWorkflow);
expect(result.active).toBe(true);
});
it('should handle activation error - no trigger nodes', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Workflow must have at least one trigger node' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.activateWorkflow('123');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).message).toContain('trigger node');
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
it('should handle activation error - workflow not found', async () => {
const error = {
message: 'Request failed',
response: { status: 404, data: { message: 'Workflow not found' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.activateWorkflow('non-existent');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).message).toContain('not found');
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
it('should handle activation error - workflow already active', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Workflow is already active' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.activateWorkflow('123');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).message).toContain('already active');
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
it('should handle server error during activation', async () => {
const error = {
message: 'Request failed',
response: { status: 500, data: { message: 'Internal server error' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.activateWorkflow('123');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nServerError);
expect((err as N8nServerError).message).toBe('Internal server error');
expect((err as N8nServerError).statusCode).toBe(500);
}
});
});
describe('deactivateWorkflow', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should deactivate workflow successfully', async () => {
const workflow = { id: '123', name: 'Test', active: true, nodes: [], connections: {} };
const deactivatedWorkflow = { ...workflow, active: false };
mockAxiosInstance.post.mockResolvedValue({ data: deactivatedWorkflow });
const result = await client.deactivateWorkflow('123');
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/workflows/123/deactivate');
expect(result).toEqual(deactivatedWorkflow);
expect(result.active).toBe(false);
});
it('should handle deactivation error - workflow not found', async () => {
const error = {
message: 'Request failed',
response: { status: 404, data: { message: 'Workflow not found' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.deactivateWorkflow('non-existent');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).message).toContain('not found');
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
it('should handle deactivation error - workflow already inactive', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Workflow is already inactive' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.deactivateWorkflow('123');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).message).toContain('already inactive');
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
it('should handle server error during deactivation', async () => {
const error = {
message: 'Request failed',
response: { status: 500, data: { message: 'Internal server error' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.deactivateWorkflow('123');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nServerError);
expect((err as N8nServerError).message).toBe('Internal server error');
expect((err as N8nServerError).statusCode).toBe(500);
}
});
it('should handle authentication error during deactivation', async () => {
const error = {
message: 'Request failed',
response: { status: 401, data: { message: 'Invalid API key' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.deactivateWorkflow('123');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nAuthenticationError);
expect((err as N8nAuthenticationError).message).toBe('Invalid API key');
expect((err as N8nAuthenticationError).statusCode).toBe(401);
}
});
});
describe('listWorkflows', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);

View File

@@ -313,6 +313,7 @@ describe('n8n-validation', () => {
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
versionId: 'v123',
versionCounter: 5, // n8n 1.118.1+ field
meta: { test: 'data' },
staticData: { some: 'data' },
pinData: { pin: 'data' },
@@ -333,6 +334,7 @@ describe('n8n-validation', () => {
expect(cleaned).not.toHaveProperty('createdAt');
expect(cleaned).not.toHaveProperty('updatedAt');
expect(cleaned).not.toHaveProperty('versionId');
expect(cleaned).not.toHaveProperty('versionCounter'); // n8n 1.118.1+ compatibility
expect(cleaned).not.toHaveProperty('meta');
expect(cleaned).not.toHaveProperty('staticData');
expect(cleaned).not.toHaveProperty('pinData');
@@ -349,6 +351,22 @@ describe('n8n-validation', () => {
expect(cleaned.settings).toEqual({ executionOrder: 'v1' });
});
it('should exclude versionCounter for n8n 1.118.1+ compatibility', () => {
const workflow = {
name: 'Test Workflow',
nodes: [],
connections: {},
versionId: 'v123',
versionCounter: 5, // n8n 1.118.1 returns this but rejects it in PUT
} as any;
const cleaned = cleanWorkflowForUpdate(workflow);
expect(cleaned).not.toHaveProperty('versionCounter');
expect(cleaned).not.toHaveProperty('versionId');
expect(cleaned.name).toBe('Test Workflow');
});
it('should add empty settings object for cloud API compatibility', () => {
const workflow = {
name: 'Test Workflow',

View File

@@ -380,10 +380,52 @@ describe('WorkflowDiffEngine', () => {
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(false);
expect(result.errors![0].message).toContain('Node not found');
});
it('should provide helpful error when using "changes" instead of "updates" (Issue #392)', async () => {
// Simulate the common mistake of using "changes" instead of "updates"
const operation: any = {
type: 'updateNode',
nodeId: 'http-1',
changes: { // Wrong property name
'parameters.url': 'https://example.com'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(false);
expect(result.errors![0].message).toContain('Invalid parameter \'changes\'');
expect(result.errors![0].message).toContain('requires \'updates\'');
expect(result.errors![0].message).toContain('Example:');
});
it('should provide helpful error when "updates" parameter is missing', async () => {
const operation: any = {
type: 'updateNode',
nodeId: 'http-1'
// Missing "updates" property
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(false);
expect(result.errors![0].message).toContain('Missing required parameter \'updates\'');
expect(result.errors![0].message).toContain('Example:');
});
});
describe('MoveNode Operation', () => {
@@ -4269,4 +4311,358 @@ describe('WorkflowDiffEngine', () => {
expect(result.workflow.connections["When clicking 'Execute workflow'"]).toBeDefined();
});
});
describe('Workflow Activation/Deactivation Operations', () => {
it('should activate workflow with activatable trigger nodes', async () => {
// Create workflow with webhook trigger (activatable)
const workflowWithTrigger = createWorkflow('Test Workflow')
.addWebhookNode({ id: 'webhook-1', name: 'Webhook Trigger' })
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('webhook-1', 'http-1')
.build() as Workflow;
// Fix connections to use node names
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithTrigger.connections)) {
const node = workflowWithTrigger.nodes.find((n: any) => n.id === nodeId);
if (node) {
newConnections[node.name] = {};
for (const [outputName, connections] of Object.entries(outputs)) {
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
conns.map((conn: any) => {
const targetNode = workflowWithTrigger.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithTrigger.connections = newConnections;
const operation: any = {
type: 'activateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithTrigger, request);
expect(result.success).toBe(true);
expect(result.shouldActivate).toBe(true);
expect((result.workflow as any)._shouldActivate).toBeUndefined(); // Flag should be cleaned up
});
it('should reject activation if no activatable trigger nodes', async () => {
// Create workflow with no trigger nodes at all
const workflowWithoutActivatableTrigger = createWorkflow('Test Workflow')
.addNode({
id: 'set-1',
name: 'Set Node',
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [100, 100],
parameters: {}
})
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('set-1', 'http-1')
.build() as Workflow;
// Fix connections to use node names
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithoutActivatableTrigger.connections)) {
const node = workflowWithoutActivatableTrigger.nodes.find((n: any) => n.id === nodeId);
if (node) {
newConnections[node.name] = {};
for (const [outputName, connections] of Object.entries(outputs)) {
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
conns.map((conn: any) => {
const targetNode = workflowWithoutActivatableTrigger.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithoutActivatableTrigger.connections = newConnections;
const operation: any = {
type: 'activateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithoutActivatableTrigger, request);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors![0].message).toContain('No activatable trigger nodes found');
expect(result.errors![0].message).toContain('executeWorkflowTrigger cannot activate workflows');
});
it('should reject activation if all trigger nodes are disabled', async () => {
// Create workflow with disabled webhook trigger
const workflowWithDisabledTrigger = createWorkflow('Test Workflow')
.addWebhookNode({ id: 'webhook-1', name: 'Webhook Trigger', disabled: true })
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('webhook-1', 'http-1')
.build() as Workflow;
// Fix connections to use node names
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithDisabledTrigger.connections)) {
const node = workflowWithDisabledTrigger.nodes.find((n: any) => n.id === nodeId);
if (node) {
newConnections[node.name] = {};
for (const [outputName, connections] of Object.entries(outputs)) {
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
conns.map((conn: any) => {
const targetNode = workflowWithDisabledTrigger.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithDisabledTrigger.connections = newConnections;
const operation: any = {
type: 'activateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithDisabledTrigger, request);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors![0].message).toContain('No activatable trigger nodes found');
});
it('should activate workflow with schedule trigger', async () => {
// Create workflow with schedule trigger (activatable)
const workflowWithSchedule = createWorkflow('Test Workflow')
.addNode({
id: 'schedule-1',
name: 'Schedule',
type: 'n8n-nodes-base.scheduleTrigger',
typeVersion: 1,
position: [100, 100],
parameters: { rule: { interval: [{ field: 'hours', hoursInterval: 1 }] } }
})
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('schedule-1', 'http-1')
.build() as Workflow;
// Fix connections
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithSchedule.connections)) {
const node = workflowWithSchedule.nodes.find((n: any) => n.id === nodeId);
if (node) {
newConnections[node.name] = {};
for (const [outputName, connections] of Object.entries(outputs)) {
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
conns.map((conn: any) => {
const targetNode = workflowWithSchedule.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithSchedule.connections = newConnections;
const operation: any = {
type: 'activateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithSchedule, request);
expect(result.success).toBe(true);
expect(result.shouldActivate).toBe(true);
});
it('should deactivate workflow successfully', async () => {
// Any workflow can be deactivated
const operation: any = {
type: 'deactivateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.shouldDeactivate).toBe(true);
expect((result.workflow as any)._shouldDeactivate).toBeUndefined(); // Flag should be cleaned up
});
it('should deactivate workflow without trigger nodes', async () => {
// Create workflow without any trigger nodes
const workflowWithoutTrigger = createWorkflow('Test Workflow')
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.addNode({
id: 'set-1',
name: 'Set',
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [300, 100],
parameters: {}
})
.connect('http-1', 'set-1')
.build() as Workflow;
// Fix connections
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithoutTrigger.connections)) {
const node = workflowWithoutTrigger.nodes.find((n: any) => n.id === nodeId);
if (node) {
newConnections[node.name] = {};
for (const [outputName, connections] of Object.entries(outputs)) {
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
conns.map((conn: any) => {
const targetNode = workflowWithoutTrigger.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithoutTrigger.connections = newConnections;
const operation: any = {
type: 'deactivateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithoutTrigger, request);
expect(result.success).toBe(true);
expect(result.shouldDeactivate).toBe(true);
});
it('should combine activation with other operations', async () => {
// Create workflow with webhook trigger
const workflowWithTrigger = createWorkflow('Test Workflow')
.addWebhookNode({ id: 'webhook-1', name: 'Webhook Trigger' })
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('webhook-1', 'http-1')
.build() as Workflow;
// Fix connections
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithTrigger.connections)) {
const node = workflowWithTrigger.nodes.find((n: any) => n.id === nodeId);
if (node) {
newConnections[node.name] = {};
for (const [outputName, connections] of Object.entries(outputs)) {
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
conns.map((conn: any) => {
const targetNode = workflowWithTrigger.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithTrigger.connections = newConnections;
const operations: any[] = [
{
type: 'updateName',
name: 'Updated Workflow Name'
},
{
type: 'addTag',
tag: 'production'
},
{
type: 'activateWorkflow'
}
];
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations
};
const result = await diffEngine.applyDiff(workflowWithTrigger, request);
expect(result.success).toBe(true);
expect(result.operationsApplied).toBe(3);
expect(result.workflow!.name).toBe('Updated Workflow Name');
expect(result.workflow!.tags).toContain('production');
expect(result.shouldActivate).toBe(true);
});
it('should reject activation if workflow has executeWorkflowTrigger only', async () => {
// Create workflow with executeWorkflowTrigger (not activatable - Issue #351)
const workflowWithExecuteTrigger = createWorkflow('Test Workflow')
.addNode({
id: 'execute-1',
name: 'Execute Workflow Trigger',
type: 'n8n-nodes-base.executeWorkflowTrigger',
typeVersion: 1,
position: [100, 100],
parameters: {}
})
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('execute-1', 'http-1')
.build() as Workflow;
// Fix connections
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithExecuteTrigger.connections)) {
const node = workflowWithExecuteTrigger.nodes.find((n: any) => n.id === nodeId);
if (node) {
newConnections[node.name] = {};
for (const [outputName, connections] of Object.entries(outputs)) {
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
conns.map((conn: any) => {
const targetNode = workflowWithExecuteTrigger.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithExecuteTrigger.connections = newConnections;
const operation: any = {
type: 'activateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithExecuteTrigger, request);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors![0].message).toContain('No activatable trigger nodes found');
expect(result.errors![0].message).toContain('executeWorkflowTrigger cannot activate workflows');
});
});
});