mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 05:23:08 +00:00
Compare commits
14 Commits
claude/ver
...
fix/sql-js
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11307a9ed2 | ||
|
|
a6ebd1df41 | ||
|
|
65f51ad8b5 | ||
|
|
af6efe9e88 | ||
|
|
3f427f9528 | ||
|
|
18b8747005 | ||
|
|
749f1c53eb | ||
|
|
892c4ed70a | ||
|
|
590dc087ac | ||
|
|
ee7229b4db | ||
|
|
b6683b8381 | ||
|
|
b2300429fd | ||
|
|
b87f638e52 | ||
|
|
9f291154f2 |
28
.github/workflows/release.yml
vendored
28
.github/workflows/release.yml
vendored
@@ -142,19 +142,13 @@ jobs:
|
|||||||
if [ -z "$PREVIOUS_TAG" ]; then
|
if [ -z "$PREVIOUS_TAG" ]; then
|
||||||
echo "ℹ️ No previous tag found, this might be the first release"
|
echo "ℹ️ No previous tag found, this might be the first release"
|
||||||
|
|
||||||
# Get all commits up to current commit - use heredoc for multiline
|
# Generate initial release notes using script
|
||||||
NOTES=$(cat <<EOF
|
if NOTES=$(node scripts/generate-initial-release-notes.js "$CURRENT_VERSION" 2>/dev/null); then
|
||||||
### Initial Release
|
echo "✅ Successfully generated initial release notes for version $CURRENT_VERSION"
|
||||||
|
else
|
||||||
This is the initial release of n8n-mcp v$CURRENT_VERSION.
|
echo "⚠️ Could not generate initial release notes for version $CURRENT_VERSION"
|
||||||
|
NOTES="Initial release v$CURRENT_VERSION"
|
||||||
___
|
fi
|
||||||
|
|
||||||
**Release Statistics:**
|
|
||||||
- Commit count: $(git rev-list --count HEAD)
|
|
||||||
- First release setup
|
|
||||||
EOF
|
|
||||||
)
|
|
||||||
|
|
||||||
echo "has-notes=true" >> $GITHUB_OUTPUT
|
echo "has-notes=true" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
@@ -220,11 +214,11 @@ EOF
|
|||||||
# Create release body
|
# Create release body
|
||||||
cat > release_body.md << 'EOF'
|
cat > release_body.md << 'EOF'
|
||||||
# Release v${{ needs.detect-version-change.outputs.new-version }}
|
# Release v${{ needs.detect-version-change.outputs.new-version }}
|
||||||
|
|
||||||
${{ needs.generate-release-notes.outputs.release-notes }}
|
${{ needs.generate-release-notes.outputs.release-notes }}
|
||||||
|
|
||||||
___
|
---
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### NPM Package
|
### NPM Package
|
||||||
|
|||||||
260
CHANGELOG.md
260
CHANGELOG.md
@@ -7,6 +7,266 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
**Issue #292: Corrected Array Property Removal Documentation in n8n_update_partial_workflow**
|
||||||
|
|
||||||
|
Fixed critical documentation error in property removal patterns that could have led users to write non-functional code.
|
||||||
|
|
||||||
|
#### Problem
|
||||||
|
|
||||||
|
The documentation incorrectly showed using array index notation `[0]` for removing array elements:
|
||||||
|
```javascript
|
||||||
|
// INCORRECT (doesn't work as shown)
|
||||||
|
updates: { "parameters.headers[0]": undefined }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Root Cause**: The `setNestedProperty` implementation doesn't parse array index notation like `[0]`. It treats `headers[0]` as a literal object key, not an array index.
|
||||||
|
|
||||||
|
**Impact**: Users following the documentation would write code that doesn't behave as expected. The property `headers[0]` would be treated as an object key, not an array element reference.
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
**Three documentation corrections in `src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts`:**
|
||||||
|
|
||||||
|
1. **Nested Property Removal Section** (lines 236-244):
|
||||||
|
- Changed comment from `// Remove array element` to `// Remove entire array property`
|
||||||
|
- Changed code from `"parameters.headers[0]": undefined` to `"parameters.headers": undefined`
|
||||||
|
|
||||||
|
2. **Example rm5** (line 340):
|
||||||
|
- Changed comment from `// Remove array element` to `// Remove entire array property`
|
||||||
|
- Changed code from `"parameters.headers[0]": undefined` to `"parameters.headers": undefined`
|
||||||
|
|
||||||
|
3. **Pitfalls Section** (line 405):
|
||||||
|
- OLD: `'Array element removal with undefined removes the element at that index, which may shift subsequent indices'`
|
||||||
|
- NEW: `'Array index notation (e.g., "parameters.headers[0]") is not supported - remove the entire array property instead'`
|
||||||
|
|
||||||
|
#### Correct Usage
|
||||||
|
|
||||||
|
**To remove an array property:**
|
||||||
|
```javascript
|
||||||
|
// Correct: Remove entire array
|
||||||
|
n8n_update_partial_workflow({
|
||||||
|
id: "wf_012",
|
||||||
|
operations: [{
|
||||||
|
type: "updateNode",
|
||||||
|
nodeName: "HTTP Request",
|
||||||
|
updates: { "parameters.headers": undefined } // Remove entire headers array
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOT:**
|
||||||
|
```javascript
|
||||||
|
// Incorrect: Array index notation doesn't work
|
||||||
|
updates: { "parameters.headers[0]": undefined } // Treated as object key "headers[0]"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Impact
|
||||||
|
|
||||||
|
- **Prevents User Confusion**: Clear documentation on what works vs. what doesn't
|
||||||
|
- **Accurate Examples**: All examples now show correct, working patterns
|
||||||
|
- **Better Error Prevention**: Pitfall warning helps users avoid this mistake
|
||||||
|
- **No Code Changes**: This is purely a documentation fix - no implementation changes needed
|
||||||
|
|
||||||
|
#### Testing
|
||||||
|
|
||||||
|
- ✅ Documentation reviewed by code-reviewer agent
|
||||||
|
- ✅ Tested by n8n-mcp-tester agent
|
||||||
|
- ✅ All examples verified against actual implementation behavior
|
||||||
|
- ✅ Pitfall accurately describes technical limitation
|
||||||
|
|
||||||
|
#### Files Changed
|
||||||
|
|
||||||
|
**Documentation (1 file)**:
|
||||||
|
- `src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts` - Corrected 3 instances of array property removal documentation
|
||||||
|
|
||||||
|
**Configuration (2 files)**:
|
||||||
|
- `package.json` - Version bump to 2.22.7
|
||||||
|
- `package.runtime.json` - Version bump to 2.22.7
|
||||||
|
|
||||||
|
#### Related
|
||||||
|
|
||||||
|
- **Issue**: #292 - Missing documentation on how to remove node properties using `updateNode`
|
||||||
|
- **PR**: #375 - Resolve GitHub Issue 292 in n8n-mcp
|
||||||
|
- **Code Review**: Identified critical array index notation documentation error
|
||||||
|
- **Root Cause**: Implementation doesn't parse array bracket notation `[N]`
|
||||||
|
|
||||||
|
Conceived by Romuald Członkowski - [www.aiadvisors.pl/en](https://www.aiadvisors.pl/en)
|
||||||
|
|
||||||
|
## [2.22.6] - 2025-10-25
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
**Issue #228: Fix Docker Port Configuration Mismatch**
|
||||||
|
|
||||||
|
Fixed critical Docker configuration bug where custom PORT environment variable values were not properly mapped to container ports, causing connection failures in Docker deployments.
|
||||||
|
|
||||||
|
#### Problem
|
||||||
|
- **docker-compose.yml**: Port mapping `"${PORT:-3000}:3000"` hardcoded container port to 3000
|
||||||
|
- **docker-compose.yml**: Health check hardcoded to port 3000
|
||||||
|
- **Dockerfile**: Health check hardcoded to port 3000
|
||||||
|
- Impact: When PORT≠3000 (e.g., PORT=8080), Docker mapped host port to wrong container port
|
||||||
|
|
||||||
|
#### Solution
|
||||||
|
- **docker-compose.yml line 44**: Changed port mapping to `"${PORT:-3000}:${PORT:-3000}"`
|
||||||
|
- **docker-compose.yml line 56**: Updated health check to use dynamic port `$${PORT:-3000}`
|
||||||
|
- **Dockerfile line 93**: Updated HEALTHCHECK to use dynamic port `${PORT:-3000}`
|
||||||
|
- **Dockerfile line 85**: Added clarifying comment about PORT configurability
|
||||||
|
|
||||||
|
#### Testing
|
||||||
|
- Verified with default PORT (3000)
|
||||||
|
- Verified with custom PORT (8080)
|
||||||
|
- Health checks work correctly in both scenarios
|
||||||
|
|
||||||
|
#### Related Issues
|
||||||
|
- Fixes #228 (Docker Compose port error)
|
||||||
|
- Likely fixes #109 (Configuration ignored in HTTP mode)
|
||||||
|
- Likely fixes #84 (Can't access container)
|
||||||
|
|
||||||
|
Conceived by Romuald Członkowski - [www.aiadvisors.pl/en](https://www.aiadvisors.pl/en)
|
||||||
|
|
||||||
## [2.22.3] - 2025-10-25
|
## [2.22.3] - 2025-10-25
|
||||||
|
|
||||||
### 🔧 Code Quality Improvements
|
### 🔧 Code Quality Improvements
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ ENV IS_DOCKER=true
|
|||||||
# To opt-out, uncomment the following line:
|
# To opt-out, uncomment the following line:
|
||||||
# ENV N8N_MCP_TELEMETRY_DISABLED=true
|
# ENV N8N_MCP_TELEMETRY_DISABLED=true
|
||||||
|
|
||||||
# Expose HTTP port
|
# Expose HTTP port (default 3000, configurable via PORT environment variable at runtime)
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Set stop signal to SIGTERM (default, but explicit is better)
|
# Set stop signal to SIGTERM (default, but explicit is better)
|
||||||
@@ -90,7 +90,7 @@ STOPSIGNAL SIGTERM
|
|||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD curl -f http://127.0.0.1:3000/health || exit 1
|
CMD sh -c 'curl -f http://127.0.0.1:${PORT:-3000}/health || exit 1'
|
||||||
|
|
||||||
# Optimized entrypoint
|
# Optimized entrypoint
|
||||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,5 +1,87 @@
|
|||||||
# n8n Update Process - Quick Reference
|
# 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
|
## Quick One-Command Update
|
||||||
|
|
||||||
For a complete update with tests and publish preparation:
|
For a complete update with tests and publish preparation:
|
||||||
@@ -99,12 +181,14 @@ This command:
|
|||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
|
|
||||||
1. **Always run on main branch** - Make sure you're on main and it's clean
|
1. **ALWAYS check existing releases first** - Use `gh release list` to see what versions are already released. Your new version must be higher!
|
||||||
2. **The update script is smart** - It automatically syncs all n8n dependencies to compatible versions
|
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. **Tests are required** - The publish script now runs tests automatically
|
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. **Database rebuild is automatic** - The update script handles this for you
|
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. **Template sanitization is automatic** - Any API tokens in workflow templates are replaced with placeholders
|
5. **The update script is smart** - It automatically syncs all n8n dependencies to compatible versions
|
||||||
6. **Docker image builds automatically** - Pushing to GitHub triggers the workflow
|
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
|
## 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
|
3. If push is still blocked, use the GitHub web interface to review and allow the push
|
||||||
|
|
||||||
## Time Estimate
|
## 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
|
- Total time: ~5-7 minutes
|
||||||
- Test suite: ~2.5 minutes
|
- Test suite: ~2.5 minutes
|
||||||
- npm install and database rebuild: ~2-3 minutes
|
- npm install and database rebuild: ~2-3 minutes
|
||||||
- The rest: seconds
|
- The rest: seconds
|
||||||
|
|
||||||
|
**Note**: The fast workflow is recommended since CI runs the same tests anyway.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
If tests fail:
|
If tests fail:
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ Collected data is used solely to:
|
|||||||
- Identify common error patterns
|
- Identify common error patterns
|
||||||
- Improve tool performance and reliability
|
- Improve tool performance and reliability
|
||||||
- Guide development priorities
|
- 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 Retention
|
||||||
- Data is retained for analysis purposes
|
- Data is retained for analysis purposes
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -5,23 +5,23 @@
|
|||||||
[](https://www.npmjs.com/package/n8n-mcp)
|
[](https://www.npmjs.com/package/n8n-mcp)
|
||||||
[](https://codecov.io/gh/czlonkowski/n8n-mcp)
|
[](https://codecov.io/gh/czlonkowski/n8n-mcp)
|
||||||
[](https://github.com/czlonkowski/n8n-mcp/actions)
|
[](https://github.com/czlonkowski/n8n-mcp/actions)
|
||||||
[](https://github.com/n8n-io/n8n)
|
[](https://github.com/n8n-io/n8n)
|
||||||
[](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp)
|
[](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp)
|
||||||
[](https://railway.com/deploy/n8n-mcp?referralCode=n8n-mcp)
|
[](https://railway.com/deploy/n8n-mcp?referralCode=n8n-mcp)
|
||||||
|
|
||||||
A Model Context Protocol (MCP) server that provides AI assistants with comprehensive access to n8n node documentation, properties, and operations. Deploy in minutes to give Claude and other AI assistants deep knowledge about n8n's 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
|
## 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:
|
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 properties** - 99% coverage with detailed schemas
|
||||||
- ⚡ **Node operations** - 63.6% coverage of available actions
|
- ⚡ **Node operations** - 63.6% coverage of available actions
|
||||||
- 📄 **Documentation** - 90% coverage from official n8n docs (including AI nodes)
|
- 📄 **Documentation** - 87% coverage from official n8n docs (including AI nodes)
|
||||||
- 🤖 **AI tools** - 263 AI-capable nodes detected with full documentation
|
- 🤖 **AI tools** - 271 AI-capable nodes detected with full documentation
|
||||||
- 💡 **Real-world examples** - 2,646 pre-extracted configurations from popular templates
|
- 💡 **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
|
## ⚠️ Important Safety Warning
|
||||||
@@ -51,6 +51,8 @@ npx n8n-mcp
|
|||||||
|
|
||||||
Add to Claude Desktop config:
|
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):**
|
**Basic configuration (documentation tools only):**
|
||||||
```json
|
```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)
|
❌ BAD: Sequential tool calls (await each one before the next)
|
||||||
|
|
||||||
### 3. Templates First
|
### 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
|
### 4. Multi-Level Validation
|
||||||
Use validate_node_minimal → validate_node_operation → validate_workflow pattern.
|
Use validate_node_minimal → validate_node_operation → validate_workflow pattern.
|
||||||
@@ -840,7 +842,7 @@ n8n_update_partial_workflow({
|
|||||||
### Core Behavior
|
### Core Behavior
|
||||||
1. **Silent execution** - No commentary between tools
|
1. **Silent execution** - No commentary between tools
|
||||||
2. **Parallel by default** - Execute independent operations simultaneously
|
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
|
4. **Multi-level validation** - Quick check → Full validation → Workflow validation
|
||||||
5. **Never trust defaults** - Explicitly configure ALL parameters
|
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
|
- **`get_node_as_tool_info`** - Get guidance on using any node as an AI tool
|
||||||
|
|
||||||
### Template Tools
|
### 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`** - Text search across template names and descriptions
|
||||||
- **`search_templates_by_metadata`** - Advanced filtering by complexity, setup time, services, audience
|
- **`search_templates_by_metadata`** - Advanced filtering by complexity, setup time, services, audience
|
||||||
- **`list_node_templates`** - Find templates using specific nodes
|
- **`list_node_templates`** - Find templates using specific nodes
|
||||||
@@ -1098,17 +1100,17 @@ npm run dev:http # HTTP dev mode
|
|||||||
|
|
||||||
## 📊 Metrics & Coverage
|
## 📊 Metrics & Coverage
|
||||||
|
|
||||||
Current database coverage (n8n v1.113.3):
|
Current database coverage (n8n v1.117.2):
|
||||||
|
|
||||||
- ✅ **536/536** nodes loaded (100%)
|
- ✅ **541/541** nodes loaded (100%)
|
||||||
- ✅ **528** nodes with properties (98.7%)
|
- ✅ **541** nodes with properties (100%)
|
||||||
- ✅ **470** nodes with documentation (88%)
|
- ✅ **470** nodes with documentation (87%)
|
||||||
- ✅ **267** AI-capable tools detected
|
- ✅ **271** AI-capable tools detected
|
||||||
- ✅ **2,646** pre-extracted template configurations
|
- ✅ **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
|
- ✅ **AI Agent & LangChain nodes** fully documented
|
||||||
- ⚡ **Average response time**: ~12ms
|
- ⚡ **Average response time**: ~12ms
|
||||||
- 💾 **Database size**: ~15MB (optimized)
|
- 💾 **Database size**: ~68MB (includes templates with metadata)
|
||||||
|
|
||||||
## 🔄 Recent Updates
|
## 🔄 Recent Updates
|
||||||
|
|
||||||
|
|||||||
BIN
data/nodes.db
BIN
data/nodes.db
Binary file not shown.
@@ -20,19 +20,19 @@ services:
|
|||||||
image: n8n-mcp:latest
|
image: n8n-mcp:latest
|
||||||
container_name: n8n-mcp
|
container_name: n8n-mcp
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "${PORT:-3000}:${PORT:-3000}"
|
||||||
environment:
|
environment:
|
||||||
- MCP_MODE=${MCP_MODE:-http}
|
- MCP_MODE=${MCP_MODE:-http}
|
||||||
- AUTH_TOKEN=${AUTH_TOKEN}
|
- AUTH_TOKEN=${AUTH_TOKEN}
|
||||||
- NODE_ENV=${NODE_ENV:-production}
|
- NODE_ENV=${NODE_ENV:-production}
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-info}
|
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
- PORT=3000
|
- PORT=${PORT:-3000}
|
||||||
volumes:
|
volumes:
|
||||||
# Mount data directory for persistence
|
# Mount data directory for persistence
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
test: ["CMD", "sh", "-c", "curl -f http://localhost:$${PORT:-3000}/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -37,11 +37,12 @@ services:
|
|||||||
container_name: n8n-mcp
|
container_name: n8n-mcp
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${MCP_PORT:-3000}:3000"
|
- "${MCP_PORT:-3000}:${MCP_PORT:-3000}"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- N8N_MODE=true
|
- N8N_MODE=true
|
||||||
- MCP_MODE=http
|
- MCP_MODE=http
|
||||||
|
- PORT=${MCP_PORT:-3000}
|
||||||
- N8N_API_URL=http://n8n:5678
|
- N8N_API_URL=http://n8n:5678
|
||||||
- N8N_API_KEY=${N8N_API_KEY}
|
- N8N_API_KEY=${N8N_API_KEY}
|
||||||
- MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN}
|
- MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN}
|
||||||
@@ -56,7 +57,7 @@ services:
|
|||||||
n8n:
|
n8n:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
test: ["CMD", "sh", "-c", "curl -f http://localhost:$${MCP_PORT:-3000}/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ services:
|
|||||||
|
|
||||||
# Port mapping
|
# Port mapping
|
||||||
ports:
|
ports:
|
||||||
- "${PORT:-3000}:3000"
|
- "${PORT:-3000}:${PORT:-3000}"
|
||||||
|
|
||||||
# Resource limits
|
# Resource limits
|
||||||
deploy:
|
deploy:
|
||||||
@@ -53,7 +53,7 @@ services:
|
|||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/health"]
|
test: ["CMD", "sh", "-c", "curl -f http://127.0.0.1:$${PORT:-3000}/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ Connect n8n-MCP to Claude Code CLI for enhanced n8n workflow development from th
|
|||||||
|
|
||||||
## Quick Setup via CLI
|
## Quick Setup via CLI
|
||||||
|
|
||||||
### Basic configuration (documentation tools only):
|
### Basic configuration (documentation tools only)
|
||||||
|
|
||||||
|
**For Linux, macOS, or Windows (WSL/Git Bash):**
|
||||||
```bash
|
```bash
|
||||||
claude mcp add n8n-mcp \
|
claude mcp add n8n-mcp \
|
||||||
-e MCP_MODE=stdio \
|
-e MCP_MODE=stdio \
|
||||||
@@ -13,9 +15,21 @@ claude mcp add n8n-mcp \
|
|||||||
-- npx 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
|
||||||
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Full configuration (with n8n management tools):
|
### Full configuration (with n8n management tools)
|
||||||
|
|
||||||
|
**For Linux, macOS, or Windows (WSL/Git Bash):**
|
||||||
```bash
|
```bash
|
||||||
claude mcp add n8n-mcp \
|
claude mcp add n8n-mcp \
|
||||||
-e MCP_MODE=stdio \
|
-e MCP_MODE=stdio \
|
||||||
@@ -26,6 +40,18 @@ claude mcp add n8n-mcp \
|
|||||||
-- npx 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.
|
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
|
## Alternative Setup Methods
|
||||||
@@ -133,9 +159,11 @@ For optimal results, create a `CLAUDE.md` file in your project root with the ins
|
|||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
- If you're running n8n locally, use `http://localhost:5678` as the N8N_API_URL
|
- 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
|
- 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.
|
||||||
- With API credentials, you'll get full workflow management capabilities
|
- **Scope Management:**
|
||||||
- Use `--scope local` (default) to keep your API credentials private
|
- 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.
|
||||||
- Use `--scope project` to share configuration with your team (put credentials in environment variables)
|
- 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.
|
||||||
- Claude Code will automatically start the MCP server when you begin a conversation
|
- **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.
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ docker compose up -d
|
|||||||
- n8n-mcp-data:/app/data
|
- n8n-mcp-data:/app/data
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "${PORT:-3000}:3000"
|
- "${PORT:-3000}:${PORT:-3000}"
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/health"]
|
test: ["CMD", "sh", "-c", "curl -f http://127.0.0.1:$${PORT:-3000}/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
1923
package-lock.json
generated
1923
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-mcp",
|
"name": "n8n-mcp",
|
||||||
"version": "2.22.5",
|
"version": "2.22.10",
|
||||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
@@ -140,15 +140,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
"@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",
|
"@supabase/supabase-js": "^2.57.4",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^7.1.5",
|
"express-rate-limit": "^7.1.5",
|
||||||
"lru-cache": "^11.2.1",
|
"lru-cache": "^11.2.1",
|
||||||
"n8n": "^1.116.2",
|
"n8n": "^1.118.1",
|
||||||
"n8n-core": "^1.115.1",
|
"n8n-core": "^1.117.0",
|
||||||
"n8n-workflow": "^1.113.0",
|
"n8n-workflow": "^1.115.0",
|
||||||
"openai": "^4.77.0",
|
"openai": "^4.77.0",
|
||||||
"sql.js": "^1.13.0",
|
"sql.js": "^1.13.0",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-mcp-runtime",
|
"name": "n8n-mcp-runtime",
|
||||||
"version": "2.22.5",
|
"version": "2.22.10",
|
||||||
"description": "n8n MCP Server Runtime Dependencies Only",
|
"description": "n8n MCP Server Runtime Dependencies Only",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
45
scripts/generate-initial-release-notes.js
Normal file
45
scripts/generate-initial-release-notes.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate release notes for the initial release
|
||||||
|
* Used by GitHub Actions when no previous tag exists
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
function generateInitialReleaseNotes(version) {
|
||||||
|
try {
|
||||||
|
// Get total commit count
|
||||||
|
const commitCount = execSync('git rev-list --count HEAD', { encoding: 'utf8' }).trim();
|
||||||
|
|
||||||
|
// Generate release notes
|
||||||
|
const releaseNotes = [
|
||||||
|
'### 🎉 Initial Release',
|
||||||
|
'',
|
||||||
|
`This is the initial release of n8n-mcp v${version}.`,
|
||||||
|
'',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
'**Release Statistics:**',
|
||||||
|
`- Commit count: ${commitCount}`,
|
||||||
|
'- First release setup'
|
||||||
|
];
|
||||||
|
|
||||||
|
return releaseNotes.join('\n');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error generating initial release notes: ${error.message}`);
|
||||||
|
return `Failed to generate initial release notes: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
const version = process.argv[2];
|
||||||
|
|
||||||
|
if (!version) {
|
||||||
|
console.error('Usage: generate-initial-release-notes.js <version>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseNotes = generateInitialReleaseNotes(version);
|
||||||
|
console.log(releaseNotes);
|
||||||
99
scripts/process-batch-metadata.ts
Normal file
99
scripts/process-batch-metadata.ts
Normal 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);
|
||||||
|
});
|
||||||
@@ -296,19 +296,24 @@ export class N8NDocumentationMCPServer {
|
|||||||
throw new Error('Database is empty. Run "npm run rebuild" to populate node data.');
|
throw new Error('Database is empty. Run "npm run rebuild" to populate node data.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if FTS5 table exists
|
// Check if FTS5 table exists (wrap in try-catch for sql.js compatibility)
|
||||||
const ftsExists = this.db.prepare(`
|
try {
|
||||||
SELECT name FROM sqlite_master
|
const ftsExists = this.db.prepare(`
|
||||||
WHERE type='table' AND name='nodes_fts'
|
SELECT name FROM sqlite_master
|
||||||
`).get();
|
WHERE type='table' AND name='nodes_fts'
|
||||||
|
`).get();
|
||||||
|
|
||||||
if (!ftsExists) {
|
if (!ftsExists) {
|
||||||
logger.warn('FTS5 table missing - search performance will be degraded. Please run: npm run rebuild');
|
logger.warn('FTS5 table missing - search performance will be degraded. Please run: npm run rebuild');
|
||||||
} else {
|
} else {
|
||||||
const ftsCount = this.db.prepare('SELECT COUNT(*) as count FROM nodes_fts').get() as { count: number };
|
const ftsCount = this.db.prepare('SELECT COUNT(*) as count FROM nodes_fts').get() as { count: number };
|
||||||
if (ftsCount.count === 0) {
|
if (ftsCount.count === 0) {
|
||||||
logger.warn('FTS5 index is empty - search will not work properly. Please run: npm run rebuild');
|
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`);
|
logger.info(`Database health check passed: ${nodeCount.count} nodes loaded`);
|
||||||
|
|||||||
@@ -186,7 +186,115 @@ Please choose a different name.
|
|||||||
- Simply rename nodes with updateNode - no manual connection operations needed
|
- Simply rename nodes with updateNode - no manual connection operations needed
|
||||||
- Multiple renames in one call work atomically
|
- Multiple renames in one call work atomically
|
||||||
- Can rename a node and add/remove connections using the new name in the same batch
|
- Can rename a node and add/remove connections using the new name in the same batch
|
||||||
- Use \`validateOnly: true\` to preview effects before applying`,
|
- Use \`validateOnly: true\` to preview effects before applying
|
||||||
|
|
||||||
|
## Removing Properties with undefined
|
||||||
|
|
||||||
|
To remove a property from a node, set its value to \`undefined\` in the updates object. This is essential when migrating from deprecated properties or cleaning up optional configuration fields.
|
||||||
|
|
||||||
|
### Why Use undefined?
|
||||||
|
- **Property removal vs. null**: Setting a property to \`undefined\` removes it completely from the node object, while \`null\` sets the property to a null value
|
||||||
|
- **Validation constraints**: Some properties are mutually exclusive (e.g., \`continueOnFail\` and \`onError\`). Simply setting one without removing the other will fail validation
|
||||||
|
- **Deprecated property migration**: When n8n deprecates properties, you must remove the old property before the new one will work
|
||||||
|
|
||||||
|
### Basic Property Removal
|
||||||
|
\`\`\`javascript
|
||||||
|
// Remove error handling configuration
|
||||||
|
n8n_update_partial_workflow({
|
||||||
|
id: "wf_123",
|
||||||
|
operations: [{
|
||||||
|
type: "updateNode",
|
||||||
|
nodeName: "HTTP Request",
|
||||||
|
updates: { onError: undefined }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove disabled flag
|
||||||
|
n8n_update_partial_workflow({
|
||||||
|
id: "wf_456",
|
||||||
|
operations: [{
|
||||||
|
type: "updateNode",
|
||||||
|
nodeId: "node_abc",
|
||||||
|
updates: { disabled: undefined }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Nested Property Removal
|
||||||
|
Use dot notation to remove nested properties:
|
||||||
|
\`\`\`javascript
|
||||||
|
// Remove nested parameter
|
||||||
|
n8n_update_partial_workflow({
|
||||||
|
id: "wf_789",
|
||||||
|
operations: [{
|
||||||
|
type: "updateNode",
|
||||||
|
nodeName: "API Request",
|
||||||
|
updates: { "parameters.authentication": undefined }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove entire array property
|
||||||
|
n8n_update_partial_workflow({
|
||||||
|
id: "wf_012",
|
||||||
|
operations: [{
|
||||||
|
type: "updateNode",
|
||||||
|
nodeName: "HTTP Request",
|
||||||
|
updates: { "parameters.headers": undefined }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Migrating from Deprecated Properties
|
||||||
|
Common scenario: replacing \`continueOnFail\` with \`onError\`:
|
||||||
|
\`\`\`javascript
|
||||||
|
// WRONG: Setting only the new property leaves the old one
|
||||||
|
n8n_update_partial_workflow({
|
||||||
|
id: "wf_123",
|
||||||
|
operations: [{
|
||||||
|
type: "updateNode",
|
||||||
|
nodeName: "HTTP Request",
|
||||||
|
updates: { onError: "continueErrorOutput" }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
// Error: continueOnFail and onError are mutually exclusive
|
||||||
|
|
||||||
|
// CORRECT: Remove the old property first
|
||||||
|
n8n_update_partial_workflow({
|
||||||
|
id: "wf_123",
|
||||||
|
operations: [{
|
||||||
|
type: "updateNode",
|
||||||
|
nodeName: "HTTP Request",
|
||||||
|
updates: {
|
||||||
|
continueOnFail: undefined,
|
||||||
|
onError: "continueErrorOutput"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Batch Property Removal
|
||||||
|
Remove multiple properties in one operation:
|
||||||
|
\`\`\`javascript
|
||||||
|
n8n_update_partial_workflow({
|
||||||
|
id: "wf_345",
|
||||||
|
operations: [{
|
||||||
|
type: "updateNode",
|
||||||
|
nodeName: "Data Processor",
|
||||||
|
updates: {
|
||||||
|
continueOnFail: undefined,
|
||||||
|
alwaysOutputData: undefined,
|
||||||
|
"parameters.legacy_option": undefined
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### When to Use undefined
|
||||||
|
- Removing deprecated properties during migration
|
||||||
|
- Cleaning up optional configuration flags
|
||||||
|
- Resolving mutual exclusivity validation errors
|
||||||
|
- Removing stale or unnecessary node metadata
|
||||||
|
- Simplifying node configuration`,
|
||||||
parameters: {
|
parameters: {
|
||||||
id: { type: 'string', required: true, description: 'Workflow ID to update' },
|
id: { type: 'string', required: true, description: 'Workflow ID to update' },
|
||||||
operations: {
|
operations: {
|
||||||
@@ -223,7 +331,13 @@ Please choose a different name.
|
|||||||
'// Vector Store setup: Connect embeddings and documents\nn8n_update_partial_workflow({id: "ai7", operations: [\n {type: "addConnection", source: "Embeddings OpenAI", target: "Pinecone Vector Store", sourceOutput: "ai_embedding"},\n {type: "addConnection", source: "Default Data Loader", target: "Pinecone Vector Store", sourceOutput: "ai_document"}\n]})',
|
'// Vector Store setup: Connect embeddings and documents\nn8n_update_partial_workflow({id: "ai7", operations: [\n {type: "addConnection", source: "Embeddings OpenAI", target: "Pinecone Vector Store", sourceOutput: "ai_embedding"},\n {type: "addConnection", source: "Default Data Loader", target: "Pinecone Vector Store", sourceOutput: "ai_document"}\n]})',
|
||||||
'// Connect Vector Store Tool to AI Agent (retrieval setup)\nn8n_update_partial_workflow({id: "ai8", operations: [\n {type: "addConnection", source: "Pinecone Vector Store", target: "Vector Store Tool", sourceOutput: "ai_vectorStore"},\n {type: "addConnection", source: "Vector Store Tool", target: "AI Agent", sourceOutput: "ai_tool"}\n]})',
|
'// Connect Vector Store Tool to AI Agent (retrieval setup)\nn8n_update_partial_workflow({id: "ai8", operations: [\n {type: "addConnection", source: "Pinecone Vector Store", target: "Vector Store Tool", sourceOutput: "ai_vectorStore"},\n {type: "addConnection", source: "Vector Store Tool", target: "AI Agent", sourceOutput: "ai_tool"}\n]})',
|
||||||
'// Rewire AI Agent to use different language model\nn8n_update_partial_workflow({id: "ai9", operations: [{type: "rewireConnection", source: "AI Agent", from: "OpenAI Chat Model", to: "Anthropic Chat Model", sourceOutput: "ai_languageModel"}]})',
|
'// Rewire AI Agent to use different language model\nn8n_update_partial_workflow({id: "ai9", operations: [{type: "rewireConnection", source: "AI Agent", from: "OpenAI Chat Model", to: "Anthropic Chat Model", sourceOutput: "ai_languageModel"}]})',
|
||||||
'// Replace all AI tools for an agent\nn8n_update_partial_workflow({id: "ai10", operations: [\n {type: "removeConnection", source: "Old Tool 1", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "removeConnection", source: "Old Tool 2", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "addConnection", source: "New HTTP Tool", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "addConnection", source: "New Code Tool", target: "AI Agent", sourceOutput: "ai_tool"}\n]})'
|
'// Replace all AI tools for an agent\nn8n_update_partial_workflow({id: "ai10", operations: [\n {type: "removeConnection", source: "Old Tool 1", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "removeConnection", source: "Old Tool 2", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "addConnection", source: "New HTTP Tool", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "addConnection", source: "New Code Tool", target: "AI Agent", sourceOutput: "ai_tool"}\n]})',
|
||||||
|
'\n// ============ REMOVING PROPERTIES EXAMPLES ============',
|
||||||
|
'// Remove a simple property\nn8n_update_partial_workflow({id: "rm1", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {onError: undefined}}]})',
|
||||||
|
'// Migrate from deprecated continueOnFail to onError\nn8n_update_partial_workflow({id: "rm2", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {continueOnFail: undefined, onError: "continueErrorOutput"}}]})',
|
||||||
|
'// Remove nested property\nn8n_update_partial_workflow({id: "rm3", operations: [{type: "updateNode", nodeName: "API Request", updates: {"parameters.authentication": undefined}}]})',
|
||||||
|
'// Remove multiple properties\nn8n_update_partial_workflow({id: "rm4", operations: [{type: "updateNode", nodeName: "Data Processor", updates: {continueOnFail: undefined, alwaysOutputData: undefined, "parameters.legacy_option": undefined}}]})',
|
||||||
|
'// Remove entire array property\nn8n_update_partial_workflow({id: "rm5", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {"parameters.headers": undefined}}]})'
|
||||||
],
|
],
|
||||||
useCases: [
|
useCases: [
|
||||||
'Rewire connections when replacing nodes',
|
'Rewire connections when replacing nodes',
|
||||||
@@ -259,7 +373,11 @@ Please choose a different name.
|
|||||||
'Connect language model BEFORE adding AI Agent to ensure validation passes',
|
'Connect language model BEFORE adding AI Agent to ensure validation passes',
|
||||||
'Use targetIndex for fallback models (primary=0, fallback=1)',
|
'Use targetIndex for fallback models (primary=0, fallback=1)',
|
||||||
'Batch AI component connections in a single operation for atomicity',
|
'Batch AI component connections in a single operation for atomicity',
|
||||||
'Validate AI workflows after connection changes to catch configuration errors'
|
'Validate AI workflows after connection changes to catch configuration errors',
|
||||||
|
'To remove properties, set them to undefined (not null) in the updates object',
|
||||||
|
'When migrating from deprecated properties, remove the old property and add the new one in the same operation',
|
||||||
|
'Use undefined to resolve mutual exclusivity validation errors between properties',
|
||||||
|
'Batch multiple property removals in a single updateNode operation for efficiency'
|
||||||
],
|
],
|
||||||
pitfalls: [
|
pitfalls: [
|
||||||
'**REQUIRES N8N_API_URL and N8N_API_KEY environment variables** - will not work without n8n API access',
|
'**REQUIRES N8N_API_URL and N8N_API_KEY environment variables** - will not work without n8n API access',
|
||||||
@@ -279,7 +397,12 @@ Please choose a different name.
|
|||||||
'**Auto-sanitization behavior**: Binary operators (equals, contains) automatically have singleValue removed; unary operators (isEmpty, isNotEmpty) automatically get singleValue:true added',
|
'**Auto-sanitization behavior**: Binary operators (equals, contains) automatically have singleValue removed; unary operators (isEmpty, isNotEmpty) automatically get singleValue:true added',
|
||||||
'**Auto-sanitization runs on ALL nodes**: When ANY update is made, ALL nodes in the workflow are sanitized (not just modified ones)',
|
'**Auto-sanitization runs on ALL nodes**: When ANY update is made, ALL nodes in the workflow are sanitized (not just modified ones)',
|
||||||
'**Auto-sanitization cannot fix everything**: It fixes operator structures and missing metadata, but cannot fix broken connections or branch mismatches',
|
'**Auto-sanitization cannot fix everything**: It fixes operator structures and missing metadata, but cannot fix broken connections or branch mismatches',
|
||||||
'**Corrupted workflows beyond repair**: Workflows in paradoxical states (API returns corrupt, API rejects updates) cannot be fixed via API - must be recreated'
|
'**Corrupted workflows beyond repair**: Workflows in paradoxical states (API returns corrupt, API rejects updates) cannot be fixed via API - must be recreated',
|
||||||
|
'Setting a property to null does NOT remove it - use undefined instead',
|
||||||
|
'When properties are mutually exclusive (e.g., continueOnFail and onError), setting only the new property will fail - you must remove the old one with undefined',
|
||||||
|
'Removing a required property may cause validation errors - check node documentation first',
|
||||||
|
'Nested property removal with dot notation only removes the specific nested field, not the entire parent object',
|
||||||
|
'Array index notation (e.g., "parameters.headers[0]") is not supported - remove the entire array property instead'
|
||||||
],
|
],
|
||||||
relatedTools: ['n8n_update_full_workflow', 'n8n_get_workflow', 'validate_workflow', 'tools_documentation']
|
relatedTools: ['n8n_update_full_workflow', 'n8n_get_workflow', 'validate_workflow', 'tools_documentation']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,10 +75,15 @@ async function fetchTemplatesRobust() {
|
|||||||
|
|
||||||
// Fetch detail
|
// Fetch detail
|
||||||
const detail = await fetcher.fetchTemplateDetail(template.id);
|
const detail = await fetcher.fetchTemplateDetail(template.id);
|
||||||
|
|
||||||
// Save immediately
|
if (detail !== null) {
|
||||||
repository.saveTemplate(template, detail);
|
// Save immediately
|
||||||
saved++;
|
repository.saveTemplate(template, detail);
|
||||||
|
saved++;
|
||||||
|
} else {
|
||||||
|
errors++;
|
||||||
|
console.error(`\n❌ Failed to fetch template ${template.id} (${template.name}) after retries`);
|
||||||
|
}
|
||||||
|
|
||||||
// Rate limiting
|
// Rate limiting
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> {
|
|||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
versionId,
|
versionId,
|
||||||
|
versionCounter, // Added: n8n 1.118.1+ returns this but rejects it in updates
|
||||||
meta,
|
meta,
|
||||||
staticData,
|
staticData,
|
||||||
// Remove fields that cause API errors
|
// Remove fields that cause API errors
|
||||||
|
|||||||
@@ -40,7 +40,37 @@ export interface TemplateDetail {
|
|||||||
export class TemplateFetcher {
|
export class TemplateFetcher {
|
||||||
private readonly baseUrl = 'https://api.n8n.io/api/templates';
|
private readonly baseUrl = 'https://api.n8n.io/api/templates';
|
||||||
private readonly pageSize = 250; // Maximum allowed by API
|
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
|
* Fetch all templates and filter to last 12 months
|
||||||
* This fetches ALL pages first, then applies date filter locally
|
* This fetches ALL pages first, then applies date filter locally
|
||||||
@@ -73,93 +103,105 @@ export class TemplateFetcher {
|
|||||||
let page = 1;
|
let page = 1;
|
||||||
let hasMore = true;
|
let hasMore = true;
|
||||||
let totalWorkflows = 0;
|
let totalWorkflows = 0;
|
||||||
|
|
||||||
logger.info('Starting complete template fetch from n8n.io API');
|
logger.info('Starting complete template fetch from n8n.io API');
|
||||||
|
|
||||||
while (hasMore) {
|
while (hasMore) {
|
||||||
try {
|
const result = await this.retryWithBackoff(
|
||||||
const response = await axios.get(`${this.baseUrl}/search`, {
|
async () => {
|
||||||
params: {
|
const response = await axios.get(`${this.baseUrl}/search`, {
|
||||||
page,
|
params: {
|
||||||
rows: this.pageSize
|
page,
|
||||||
// Note: sort_by parameter doesn't work, templates come in popularity order
|
rows: this.pageSize
|
||||||
}
|
// Note: sort_by parameter doesn't work, templates come in popularity order
|
||||||
});
|
}
|
||||||
|
});
|
||||||
const { workflows } = response.data;
|
return response.data;
|
||||||
totalWorkflows = response.data.totalWorkflows || totalWorkflows;
|
},
|
||||||
|
`Fetching templates page ${page}`
|
||||||
allTemplates.push(...workflows);
|
);
|
||||||
|
|
||||||
// Calculate total pages for better progress reporting
|
if (result === null) {
|
||||||
const totalPages = Math.ceil(totalWorkflows / this.pageSize);
|
// All retries failed for this page, skip it and continue
|
||||||
|
logger.warn(`Skipping page ${page} after ${this.maxRetries} failed attempts`);
|
||||||
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++;
|
page++;
|
||||||
|
continue;
|
||||||
// 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)
|
const { workflows } = result;
|
||||||
}
|
totalWorkflows = result.totalWorkflows || totalWorkflows;
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error fetching templates page ${page}:`, error);
|
allTemplates.push(...workflows);
|
||||||
throw error;
|
|
||||||
|
// 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`);
|
logger.info(`Fetched all ${allTemplates.length} templates from n8n.io`);
|
||||||
return allTemplates;
|
return allTemplates;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchTemplateDetail(workflowId: number): Promise<TemplateDetail> {
|
async fetchTemplateDetail(workflowId: number): Promise<TemplateDetail | null> {
|
||||||
try {
|
const result = await this.retryWithBackoff(
|
||||||
const response = await axios.get(`${this.baseUrl}/workflows/${workflowId}`);
|
async () => {
|
||||||
return response.data.workflow;
|
const response = await axios.get(`${this.baseUrl}/workflows/${workflowId}`);
|
||||||
} catch (error) {
|
return response.data.workflow;
|
||||||
logger.error(`Error fetching template detail for ${workflowId}:`, error);
|
},
|
||||||
throw error;
|
`Fetching template detail for workflow ${workflowId}`
|
||||||
}
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchAllTemplateDetails(
|
async fetchAllTemplateDetails(
|
||||||
workflows: TemplateWorkflow[],
|
workflows: TemplateWorkflow[],
|
||||||
progressCallback?: (current: number, total: number) => void
|
progressCallback?: (current: number, total: number) => void
|
||||||
): Promise<Map<number, TemplateDetail>> {
|
): Promise<Map<number, TemplateDetail>> {
|
||||||
const details = new Map<number, TemplateDetail>();
|
const details = new Map<number, TemplateDetail>();
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
logger.info(`Fetching details for ${workflows.length} templates`);
|
logger.info(`Fetching details for ${workflows.length} templates`);
|
||||||
|
|
||||||
for (let i = 0; i < workflows.length; i++) {
|
for (let i = 0; i < workflows.length; i++) {
|
||||||
const workflow = workflows[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);
|
details.set(workflow.id, detail);
|
||||||
|
} else {
|
||||||
if (progressCallback) {
|
skipped++;
|
||||||
progressCallback(i + 1, workflows.length);
|
logger.warn(`Skipped workflow ${workflow.id} after ${this.maxRetries} failed attempts`);
|
||||||
}
|
|
||||||
|
|
||||||
// Rate limiting (conservative to avoid API throttling)
|
|
||||||
await this.sleep(150); // 150ms between requests
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to fetch details for workflow ${workflow.id}:`, error);
|
|
||||||
// Continue with other templates
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(i + 1, workflows.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting (conservative to avoid API throttling)
|
||||||
|
await this.sleep(150); // 150ms between requests
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Successfully fetched ${details.size} template details`);
|
logger.info(`Successfully fetched ${details.size} template details (${skipped} skipped)`);
|
||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -496,10 +496,17 @@ export class TemplateRepository {
|
|||||||
// Count node usage
|
// Count node usage
|
||||||
const nodeCount: Record<string, number> = {};
|
const nodeCount: Record<string, number> = {};
|
||||||
topNodes.forEach(t => {
|
topNodes.forEach(t => {
|
||||||
const nodes = JSON.parse(t.nodes_used);
|
if (!t.nodes_used) return;
|
||||||
nodes.forEach((n: string) => {
|
try {
|
||||||
nodeCount[n] = (nodeCount[n] || 0) + 1;
|
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
|
// Get top 10 most used nodes
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export interface Workflow {
|
|||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
versionId?: string;
|
versionId?: string;
|
||||||
|
versionCounter?: number; // Added: n8n 1.118.1+ returns this in GET responses
|
||||||
meta?: {
|
meta?: {
|
||||||
instanceId?: string;
|
instanceId?: string;
|
||||||
};
|
};
|
||||||
@@ -152,6 +153,7 @@ export interface WorkflowExport {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
pinData?: Record<string, unknown>;
|
pinData?: Record<string, unknown>;
|
||||||
versionId?: string;
|
versionId?: string;
|
||||||
|
versionCounter?: number; // Added: n8n 1.118.1+
|
||||||
meta?: Record<string, unknown>;
|
meta?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -313,6 +313,7 @@ describe('n8n-validation', () => {
|
|||||||
createdAt: '2023-01-01',
|
createdAt: '2023-01-01',
|
||||||
updatedAt: '2023-01-01',
|
updatedAt: '2023-01-01',
|
||||||
versionId: 'v123',
|
versionId: 'v123',
|
||||||
|
versionCounter: 5, // n8n 1.118.1+ field
|
||||||
meta: { test: 'data' },
|
meta: { test: 'data' },
|
||||||
staticData: { some: 'data' },
|
staticData: { some: 'data' },
|
||||||
pinData: { pin: 'data' },
|
pinData: { pin: 'data' },
|
||||||
@@ -333,6 +334,7 @@ describe('n8n-validation', () => {
|
|||||||
expect(cleaned).not.toHaveProperty('createdAt');
|
expect(cleaned).not.toHaveProperty('createdAt');
|
||||||
expect(cleaned).not.toHaveProperty('updatedAt');
|
expect(cleaned).not.toHaveProperty('updatedAt');
|
||||||
expect(cleaned).not.toHaveProperty('versionId');
|
expect(cleaned).not.toHaveProperty('versionId');
|
||||||
|
expect(cleaned).not.toHaveProperty('versionCounter'); // n8n 1.118.1+ compatibility
|
||||||
expect(cleaned).not.toHaveProperty('meta');
|
expect(cleaned).not.toHaveProperty('meta');
|
||||||
expect(cleaned).not.toHaveProperty('staticData');
|
expect(cleaned).not.toHaveProperty('staticData');
|
||||||
expect(cleaned).not.toHaveProperty('pinData');
|
expect(cleaned).not.toHaveProperty('pinData');
|
||||||
@@ -349,6 +351,22 @@ describe('n8n-validation', () => {
|
|||||||
expect(cleaned.settings).toEqual({ executionOrder: 'v1' });
|
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', () => {
|
it('should add empty settings object for cloud API compatibility', () => {
|
||||||
const workflow = {
|
const workflow = {
|
||||||
name: 'Test Workflow',
|
name: 'Test Workflow',
|
||||||
|
|||||||
Reference in New Issue
Block a user