mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-23 10:53:07 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
599bc664d0 | ||
|
|
9590f751d2 | ||
|
|
248f859c49 | ||
|
|
25b8a8145d | ||
|
|
0918cd5425 | ||
|
|
0998e5486e | ||
|
|
87f26eef18 | ||
|
|
4bad880f44 | ||
|
|
77048347b3 | ||
|
|
6f695be482 | ||
|
|
34159f4ece | ||
|
|
8217229e2f | ||
|
|
89146186d8 | ||
|
|
c601581714 | ||
|
|
020bc3d43d | ||
|
|
a57b400bd0 | ||
|
|
38aa70261a | ||
|
|
1b328d8168 | ||
|
|
23b90d01a6 | ||
|
|
1f45cc6dcc | ||
|
|
6814880410 | ||
|
|
c8c76e435d | ||
|
|
fad3437977 | ||
|
|
0f15b82f1e | ||
|
|
974a9fb349 | ||
|
|
a6dcbd2473 | ||
|
|
ec5340c7e4 | ||
|
|
a9c4400a92 |
12
.github/workflows/dependency-check.yml
vendored
12
.github/workflows/dependency-check.yml
vendored
@@ -59,7 +59,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
npm init -y
|
npm init -y
|
||||||
# Install from tarball WITHOUT lockfile (simulates npm install n8n-mcp)
|
# Install from tarball WITHOUT lockfile (simulates npm install n8n-mcp)
|
||||||
npm install ./n8n-mcp-*.tgz
|
# Use --ignore-scripts to skip native compilation of transitive deps like isolated-vm
|
||||||
|
# (n8n-mcp only reads node metadata, it doesn't execute n8n nodes at runtime)
|
||||||
|
npm install --ignore-scripts ./n8n-mcp-*.tgz
|
||||||
|
|
||||||
- name: Verify critical dependency versions
|
- name: Verify critical dependency versions
|
||||||
working-directory: /tmp/fresh-install-test
|
working-directory: /tmp/fresh-install-test
|
||||||
@@ -75,15 +77,15 @@ jobs:
|
|||||||
echo "Zod version: $ZOD_VERSION"
|
echo "Zod version: $ZOD_VERSION"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check MCP SDK version - must be exactly 1.20.1
|
# Check MCP SDK version - must be exactly 1.27.1
|
||||||
if [[ "$SDK_VERSION" == "not found" ]]; then
|
if [[ "$SDK_VERSION" == "not found" ]]; then
|
||||||
echo "❌ FAILED: Could not determine MCP SDK version!"
|
echo "❌ FAILED: Could not determine MCP SDK version!"
|
||||||
echo " The dependency may not have been installed correctly."
|
echo " The dependency may not have been installed correctly."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ "$SDK_VERSION" != "1.20.1" ]]; then
|
if [[ "$SDK_VERSION" != "1.27.1" ]]; then
|
||||||
echo "❌ FAILED: MCP SDK version mismatch!"
|
echo "❌ FAILED: MCP SDK version mismatch!"
|
||||||
echo " Expected: 1.20.1"
|
echo " Expected: 1.27.1"
|
||||||
echo " Got: $SDK_VERSION"
|
echo " Got: $SDK_VERSION"
|
||||||
echo ""
|
echo ""
|
||||||
echo "This can cause runtime errors. See issues #440, #444, #446, #447, #450"
|
echo "This can cause runtime errors. See issues #440, #444, #446, #447, #450"
|
||||||
@@ -98,7 +100,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ "$ZOD_VERSION" =~ ^4\. ]]; then
|
if [[ "$ZOD_VERSION" =~ ^4\. ]]; then
|
||||||
echo "❌ FAILED: Zod v4 detected - incompatible with MCP SDK 1.20.1!"
|
echo "❌ FAILED: Zod v4 detected - incompatible with MCP SDK 1.27.1!"
|
||||||
echo " Expected: 3.x"
|
echo " Expected: 3.x"
|
||||||
echo " Got: $ZOD_VERSION"
|
echo " Got: $ZOD_VERSION"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
30
.github/workflows/docker-build.yml
vendored
30
.github/workflows/docker-build.yml
vendored
@@ -53,13 +53,24 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
|
- name: Sync runtime version
|
||||||
|
run: |
|
||||||
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const pkg = JSON.parse(fs.readFileSync('package.runtime.json'));
|
||||||
|
pkg.version = '$VERSION';
|
||||||
|
fs.writeFileSync('package.runtime.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||||
|
"
|
||||||
|
echo "✅ Synced package.runtime.json to version $VERSION"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
@@ -144,13 +155,24 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
|
- name: Sync runtime version
|
||||||
|
run: |
|
||||||
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const pkg = JSON.parse(fs.readFileSync('package.runtime.json'));
|
||||||
|
pkg.version = '$VERSION';
|
||||||
|
fs.writeFileSync('package.runtime.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||||
|
"
|
||||||
|
echo "✅ Synced package.runtime.json to version $VERSION"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
|||||||
25
.github/workflows/release.yml
vendored
25
.github/workflows/release.yml
vendored
@@ -283,8 +283,8 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Build project
|
- name: Build project (server + UI apps)
|
||||||
run: npm run build
|
run: npm run build:all
|
||||||
|
|
||||||
# Database is already built and committed during development
|
# Database is already built and committed during development
|
||||||
# Rebuilding here causes segfault due to memory pressure (exit code 139)
|
# Rebuilding here causes segfault due to memory pressure (exit code 139)
|
||||||
@@ -322,8 +322,8 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Build project
|
- name: Build project (server + UI apps)
|
||||||
run: npm run build
|
run: npm run build:all
|
||||||
|
|
||||||
# Database is already built and committed during development
|
# Database is already built and committed during development
|
||||||
- name: Verify database exists
|
- name: Verify database exists
|
||||||
@@ -347,6 +347,8 @@ jobs:
|
|||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
cp -r dist $PUBLISH_DIR/
|
cp -r dist $PUBLISH_DIR/
|
||||||
cp -r data $PUBLISH_DIR/
|
cp -r data $PUBLISH_DIR/
|
||||||
|
mkdir -p $PUBLISH_DIR/ui-apps
|
||||||
|
cp -r ui-apps/dist $PUBLISH_DIR/ui-apps/
|
||||||
cp README.md $PUBLISH_DIR/
|
cp README.md $PUBLISH_DIR/
|
||||||
cp LICENSE $PUBLISH_DIR/
|
cp LICENSE $PUBLISH_DIR/
|
||||||
cp .env.example $PUBLISH_DIR/
|
cp .env.example $PUBLISH_DIR/
|
||||||
@@ -377,7 +379,7 @@ jobs:
|
|||||||
pkg.license = 'MIT';
|
pkg.license = 'MIT';
|
||||||
pkg.bugs = { url: 'https://github.com/czlonkowski/n8n-mcp/issues' };
|
pkg.bugs = { url: 'https://github.com/czlonkowski/n8n-mcp/issues' };
|
||||||
pkg.homepage = 'https://github.com/czlonkowski/n8n-mcp#readme';
|
pkg.homepage = 'https://github.com/czlonkowski/n8n-mcp#readme';
|
||||||
pkg.files = ['dist/**/*', 'data/nodes.db', '.env.example', 'README.md', 'LICENSE'];
|
pkg.files = ['dist/**/*', 'ui-apps/dist/**/*', 'data/nodes.db', '.env.example', 'README.md', 'LICENSE'];
|
||||||
delete pkg.private;
|
delete pkg.private;
|
||||||
require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
|
require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
|
||||||
"
|
"
|
||||||
@@ -427,7 +429,18 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "✅ Sufficient disk space: ${AVAILABLE_GB}GB available"
|
echo "✅ Sufficient disk space: ${AVAILABLE_GB}GB available"
|
||||||
|
|
||||||
|
- name: Sync runtime version for Docker
|
||||||
|
run: |
|
||||||
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const pkg = JSON.parse(fs.readFileSync('package.runtime.json'));
|
||||||
|
pkg.version = '$VERSION';
|
||||||
|
fs.writeFileSync('package.runtime.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||||
|
"
|
||||||
|
echo "✅ Synced package.runtime.json to version $VERSION"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -138,5 +138,9 @@ n8n-mcp-wrapper.sh
|
|||||||
# MCP configuration files
|
# MCP configuration files
|
||||||
.mcp.json
|
.mcp.json
|
||||||
|
|
||||||
|
# UI Apps build output
|
||||||
|
ui-apps/dist/
|
||||||
|
ui-apps/node_modules/
|
||||||
|
|
||||||
# Telemetry configuration (user-specific)
|
# Telemetry configuration (user-specific)
|
||||||
~/.n8n-mcp/
|
~/.n8n-mcp/
|
||||||
|
|||||||
1072
CHANGELOG.md
1072
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ RUN --mount=type=cache,target=/root/.npm \
|
|||||||
echo '{}' > package.json && \
|
echo '{}' > package.json && \
|
||||||
npm install --no-save typescript@^5.8.3 @types/node@^22.15.30 @types/express@^5.0.3 \
|
npm install --no-save typescript@^5.8.3 @types/node@^22.15.30 @types/express@^5.0.3 \
|
||||||
@modelcontextprotocol/sdk@1.20.1 dotenv@^16.5.0 express@^5.1.0 axios@^1.10.0 \
|
@modelcontextprotocol/sdk@1.20.1 dotenv@^16.5.0 express@^5.1.0 axios@^1.10.0 \
|
||||||
n8n-workflow@^1.96.0 uuid@^11.0.5 @types/uuid@^10.0.0 \
|
n8n-workflow@^2.4.2 uuid@^11.0.5 @types/uuid@^10.0.0 \
|
||||||
openai@^4.77.0 zod@3.24.1 lru-cache@^11.2.1 @supabase/supabase-js@^2.57.4
|
openai@^4.77.0 zod@3.24.1 lru-cache@^11.2.1 @supabase/supabase-js@^2.57.4
|
||||||
|
|
||||||
# Copy source and build
|
# Copy source and build
|
||||||
|
|||||||
@@ -18,21 +18,27 @@ npm run update:n8n:check
|
|||||||
# 4. Run update and skip tests (we'll test in CI)
|
# 4. Run update and skip tests (we'll test in CI)
|
||||||
yes y | npm run update:n8n
|
yes y | npm run update:n8n
|
||||||
|
|
||||||
# 5. Create feature branch
|
# 5. Refresh community nodes (standard practice!)
|
||||||
|
npm run fetch:community
|
||||||
|
npm run generate:docs
|
||||||
|
|
||||||
|
# 6. Create feature branch
|
||||||
git checkout -b update/n8n-X.X.X
|
git checkout -b update/n8n-X.X.X
|
||||||
|
|
||||||
# 6. Update version in package.json (must be HIGHER than latest release!)
|
# 7. Update version in package.json (must be HIGHER than latest release!)
|
||||||
# Edit: "version": "2.XX.X" (not the version from the release list!)
|
# Edit: "version": "2.XX.X" (not the version from the release list!)
|
||||||
|
|
||||||
# 7. Update CHANGELOG.md
|
# 8. Update CHANGELOG.md
|
||||||
# - Change version number to match package.json
|
# - Change version number to match package.json
|
||||||
# - Update date to today
|
# - Update date to today
|
||||||
# - Update dependency versions
|
# - Update dependency versions
|
||||||
|
# - Include community node refresh counts
|
||||||
|
|
||||||
# 8. Update README badge
|
# 9. Update README badge and node counts
|
||||||
# Edit line 8: Change n8n version badge to new n8n version
|
# Edit line 8: Change n8n version badge to new n8n version
|
||||||
|
# Update total node count in description (core + community)
|
||||||
|
|
||||||
# 9. Commit and push
|
# 10. Commit and push
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "chore: update n8n to X.X.X and bump version to 2.XX.X
|
git commit -m "chore: update n8n to X.X.X and bump version to 2.XX.X
|
||||||
|
|
||||||
@@ -41,7 +47,8 @@ git commit -m "chore: update n8n to X.X.X and bump version to 2.XX.X
|
|||||||
- Updated n8n-workflow 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
|
- 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)
|
- Rebuilt node database with XXX nodes (XXX from n8n-nodes-base, XXX from @n8n/n8n-nodes-langchain)
|
||||||
- Updated README badge with new n8n version
|
- Refreshed community nodes (XXX verified + XXX npm)
|
||||||
|
- Updated README badge with new n8n version and node counts
|
||||||
- Updated CHANGELOG with dependency changes
|
- Updated CHANGELOG with dependency changes
|
||||||
|
|
||||||
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
|
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
|
||||||
@@ -52,10 +59,10 @@ Co-Authored-By: Claude <noreply@anthropic.com>"
|
|||||||
|
|
||||||
git push -u origin update/n8n-X.X.X
|
git push -u origin update/n8n-X.X.X
|
||||||
|
|
||||||
# 10. Create PR
|
# 11. Create PR
|
||||||
gh pr create --title "chore: update n8n to X.X.X" --body "Updates n8n and all related dependencies to the latest versions..."
|
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
|
# 12. After PR is merged, verify release triggered
|
||||||
gh release list | head -1
|
gh release list | head -1
|
||||||
# If the new version appears, you're done!
|
# If the new version appears, you're done!
|
||||||
# If not, the version might have already been released - bump version again and create new PR
|
# If not, the version might have already been released - bump version again and create new PR
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
[](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 1,084 workflow automation nodes (537 core + 547 community).
|
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 1,239 workflow automation nodes (809 core + 430 community).
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
|||||||
BIN
data/nodes.db
BIN
data/nodes.db
Binary file not shown.
2
dist/constants/type-structures.d.ts.map
vendored
2
dist/constants/type-structures.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"type-structures.d.ts","sourceRoot":"","sources":["../../src/constants/type-structures.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAe9D,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,iBAAiB,EAAE,aAAa,CAilBpE,CAAC;AAUF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4GjC,CAAC"}
|
{"version":3,"file":"type-structures.d.ts","sourceRoot":"","sources":["../../src/constants/type-structures.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAe9D,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,iBAAiB,EAAE,aAAa,CAkmBpE,CAAC;AAUF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4GjC,CAAC"}
|
||||||
16
dist/constants/type-structures.js
vendored
16
dist/constants/type-structures.js
vendored
@@ -545,6 +545,22 @@ exports.TYPE_STRUCTURES = {
|
|||||||
'One-time import feature',
|
'One-time import feature',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
icon: {
|
||||||
|
type: 'primitive',
|
||||||
|
jsType: 'string',
|
||||||
|
description: 'Icon identifier for visual representation',
|
||||||
|
example: 'fa:envelope',
|
||||||
|
examples: ['fa:envelope', 'fa:user', 'fa:cog', 'file:slack.svg'],
|
||||||
|
validation: {
|
||||||
|
allowEmpty: false,
|
||||||
|
allowExpressions: false,
|
||||||
|
},
|
||||||
|
notes: [
|
||||||
|
'References icon by name or file path',
|
||||||
|
'Supports Font Awesome icons (fa:) and file paths (file:)',
|
||||||
|
'Used for visual customization in UI',
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
exports.COMPLEX_TYPE_EXAMPLES = {
|
exports.COMPLEX_TYPE_EXAMPLES = {
|
||||||
collection: {
|
collection: {
|
||||||
|
|||||||
2
dist/constants/type-structures.js.map
vendored
2
dist/constants/type-structures.js.map
vendored
File diff suppressed because one or more lines are too long
20
dist/database/database-adapter.js
vendored
20
dist/database/database-adapter.js
vendored
@@ -311,6 +311,17 @@ class SQLJSStatement {
|
|||||||
this.stmt = stmt;
|
this.stmt = stmt;
|
||||||
this.onModify = onModify;
|
this.onModify = onModify;
|
||||||
this.boundParams = null;
|
this.boundParams = null;
|
||||||
|
this.freed = false;
|
||||||
|
}
|
||||||
|
freeStatement() {
|
||||||
|
if (!this.freed && this.stmt) {
|
||||||
|
try {
|
||||||
|
this.stmt.free();
|
||||||
|
this.freed = true;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
run(...params) {
|
run(...params) {
|
||||||
try {
|
try {
|
||||||
@@ -331,6 +342,9 @@ class SQLJSStatement {
|
|||||||
this.stmt.reset();
|
this.stmt.reset();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
this.freeStatement();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
get(...params) {
|
get(...params) {
|
||||||
try {
|
try {
|
||||||
@@ -352,6 +366,9 @@ class SQLJSStatement {
|
|||||||
this.stmt.reset();
|
this.stmt.reset();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
this.freeStatement();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
all(...params) {
|
all(...params) {
|
||||||
try {
|
try {
|
||||||
@@ -372,6 +389,9 @@ class SQLJSStatement {
|
|||||||
this.stmt.reset();
|
this.stmt.reset();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
this.freeStatement();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
iterate(...params) {
|
iterate(...params) {
|
||||||
return this.all(...params)[Symbol.iterator]();
|
return this.all(...params)[Symbol.iterator]();
|
||||||
|
|||||||
2
dist/database/database-adapter.js.map
vendored
2
dist/database/database-adapter.js.map
vendored
File diff suppressed because one or more lines are too long
36
dist/database/node-repository.d.ts
vendored
36
dist/database/node-repository.d.ts
vendored
@@ -1,10 +1,20 @@
|
|||||||
import { DatabaseAdapter } from './database-adapter';
|
import { DatabaseAdapter } from './database-adapter';
|
||||||
import { ParsedNode } from '../parsers/node-parser';
|
import { ParsedNode } from '../parsers/node-parser';
|
||||||
import { SQLiteStorageService } from '../services/sqlite-storage-service';
|
import { SQLiteStorageService } from '../services/sqlite-storage-service';
|
||||||
|
export interface CommunityNodeFields {
|
||||||
|
isCommunity: boolean;
|
||||||
|
isVerified: boolean;
|
||||||
|
authorName?: string;
|
||||||
|
authorGithubUrl?: string;
|
||||||
|
npmPackageName?: string;
|
||||||
|
npmVersion?: string;
|
||||||
|
npmDownloads?: number;
|
||||||
|
communityFetchedAt?: string;
|
||||||
|
}
|
||||||
export declare class NodeRepository {
|
export declare class NodeRepository {
|
||||||
private db;
|
private db;
|
||||||
constructor(dbOrService: DatabaseAdapter | SQLiteStorageService);
|
constructor(dbOrService: DatabaseAdapter | SQLiteStorageService);
|
||||||
saveNode(node: ParsedNode): void;
|
saveNode(node: ParsedNode & Partial<CommunityNodeFields>): void;
|
||||||
getNode(nodeType: string): any;
|
getNode(nodeType: string): any;
|
||||||
getAITools(): any[];
|
getAITools(): any[];
|
||||||
private safeJsonParse;
|
private safeJsonParse;
|
||||||
@@ -29,6 +39,30 @@ export declare class NodeRepository {
|
|||||||
getAllResources(): Map<string, any[]>;
|
getAllResources(): Map<string, any[]>;
|
||||||
getNodePropertyDefaults(nodeType: string): Record<string, any>;
|
getNodePropertyDefaults(nodeType: string): Record<string, any>;
|
||||||
getDefaultOperationForResource(nodeType: string, resource?: string): string | undefined;
|
getDefaultOperationForResource(nodeType: string, resource?: string): string | undefined;
|
||||||
|
getCommunityNodes(options?: {
|
||||||
|
verified?: boolean;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: 'downloads' | 'name' | 'updated';
|
||||||
|
}): any[];
|
||||||
|
getCommunityStats(): {
|
||||||
|
total: number;
|
||||||
|
verified: number;
|
||||||
|
unverified: number;
|
||||||
|
};
|
||||||
|
hasNodeByNpmPackage(npmPackageName: string): boolean;
|
||||||
|
getNodeByNpmPackage(npmPackageName: string): any | null;
|
||||||
|
deleteCommunityNodes(): number;
|
||||||
|
updateNodeReadme(nodeType: string, readme: string): void;
|
||||||
|
updateNodeAISummary(nodeType: string, summary: object): void;
|
||||||
|
getCommunityNodesWithoutReadme(): any[];
|
||||||
|
getCommunityNodesWithoutAISummary(): any[];
|
||||||
|
getDocumentationStats(): {
|
||||||
|
total: number;
|
||||||
|
withReadme: number;
|
||||||
|
withAISummary: number;
|
||||||
|
needingReadme: number;
|
||||||
|
needingAISummary: number;
|
||||||
|
};
|
||||||
saveNodeVersion(versionData: {
|
saveNodeVersion(versionData: {
|
||||||
nodeType: string;
|
nodeType: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
2
dist/database/node-repository.d.ts.map
vendored
2
dist/database/node-repository.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"node-repository.d.ts","sourceRoot":"","sources":["../../src/database/node-repository.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"node-repository.d.ts","sourceRoot":"","sources":["../../src/database/node-repository.ts"],"names":[],"mappings":"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"}
|
||||||
123
dist/database/node-repository.js
vendored
123
dist/database/node-repository.js
vendored
@@ -19,10 +19,12 @@ class NodeRepository {
|
|||||||
is_webhook, is_versioned, is_tool_variant, tool_variant_of,
|
is_webhook, is_versioned, is_tool_variant, tool_variant_of,
|
||||||
has_tool_variant, version, documentation,
|
has_tool_variant, version, documentation,
|
||||||
properties_schema, operations, credentials_required,
|
properties_schema, operations, credentials_required,
|
||||||
outputs, output_names
|
outputs, output_names,
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
is_community, is_verified, author_name, author_github_url,
|
||||||
|
npm_package_name, npm_version, npm_downloads, community_fetched_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
stmt.run(node.nodeType, node.packageName, node.displayName, node.description, node.category, node.style, node.isAITool ? 1 : 0, node.isTrigger ? 1 : 0, node.isWebhook ? 1 : 0, node.isVersioned ? 1 : 0, node.isToolVariant ? 1 : 0, node.toolVariantOf || null, node.hasToolVariant ? 1 : 0, node.version, node.documentation || null, JSON.stringify(node.properties, null, 2), JSON.stringify(node.operations, null, 2), JSON.stringify(node.credentials, null, 2), node.outputs ? JSON.stringify(node.outputs, null, 2) : null, node.outputNames ? JSON.stringify(node.outputNames, null, 2) : null);
|
stmt.run(node.nodeType, node.packageName, node.displayName, node.description, node.category, node.style, node.isAITool ? 1 : 0, node.isTrigger ? 1 : 0, node.isWebhook ? 1 : 0, node.isVersioned ? 1 : 0, node.isToolVariant ? 1 : 0, node.toolVariantOf || null, node.hasToolVariant ? 1 : 0, node.version, node.documentation || null, JSON.stringify(node.properties, null, 2), JSON.stringify(node.operations, null, 2), JSON.stringify(node.credentials, null, 2), node.outputs ? JSON.stringify(node.outputs, null, 2) : null, node.outputNames ? JSON.stringify(node.outputNames, null, 2) : null, node.isCommunity ? 1 : 0, node.isVerified ? 1 : 0, node.authorName || null, node.authorGithubUrl || null, node.npmPackageName || null, node.npmVersion || null, node.npmDownloads || 0, node.communityFetchedAt || null);
|
||||||
}
|
}
|
||||||
getNode(nodeType) {
|
getNode(nodeType) {
|
||||||
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
||||||
@@ -37,6 +39,14 @@ class NodeRepository {
|
|||||||
return this.parseNodeRow(originalRow);
|
return this.parseNodeRow(originalRow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!row) {
|
||||||
|
const caseInsensitiveRow = this.db.prepare(`
|
||||||
|
SELECT * FROM nodes WHERE LOWER(node_type) = LOWER(?)
|
||||||
|
`).get(nodeType);
|
||||||
|
if (caseInsensitiveRow) {
|
||||||
|
return this.parseNodeRow(caseInsensitiveRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!row)
|
if (!row)
|
||||||
return null;
|
return null;
|
||||||
return this.parseNodeRow(row);
|
return this.parseNodeRow(row);
|
||||||
@@ -214,7 +224,20 @@ class NodeRepository {
|
|||||||
credentials: this.safeJsonParse(row.credentials_required, []),
|
credentials: this.safeJsonParse(row.credentials_required, []),
|
||||||
hasDocumentation: !!row.documentation,
|
hasDocumentation: !!row.documentation,
|
||||||
outputs: row.outputs ? this.safeJsonParse(row.outputs, null) : null,
|
outputs: row.outputs ? this.safeJsonParse(row.outputs, null) : null,
|
||||||
outputNames: row.output_names ? this.safeJsonParse(row.output_names, null) : null
|
outputNames: row.output_names ? this.safeJsonParse(row.output_names, null) : null,
|
||||||
|
isCommunity: Number(row.is_community) === 1,
|
||||||
|
isVerified: Number(row.is_verified) === 1,
|
||||||
|
authorName: row.author_name || null,
|
||||||
|
authorGithubUrl: row.author_github_url || null,
|
||||||
|
npmPackageName: row.npm_package_name || null,
|
||||||
|
npmVersion: row.npm_version || null,
|
||||||
|
npmDownloads: row.npm_downloads || 0,
|
||||||
|
communityFetchedAt: row.community_fetched_at || null,
|
||||||
|
npmReadme: row.npm_readme || null,
|
||||||
|
aiDocumentationSummary: row.ai_documentation_summary
|
||||||
|
? this.safeJsonParse(row.ai_documentation_summary, null)
|
||||||
|
: null,
|
||||||
|
aiSummaryGeneratedAt: row.ai_summary_generated_at || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
getNodeOperations(nodeType, resource) {
|
getNodeOperations(nodeType, resource) {
|
||||||
@@ -360,6 +383,98 @@ class NodeRepository {
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
getCommunityNodes(options) {
|
||||||
|
let sql = 'SELECT * FROM nodes WHERE is_community = 1';
|
||||||
|
const params = [];
|
||||||
|
if (options?.verified !== undefined) {
|
||||||
|
sql += ' AND is_verified = ?';
|
||||||
|
params.push(options.verified ? 1 : 0);
|
||||||
|
}
|
||||||
|
switch (options?.orderBy) {
|
||||||
|
case 'downloads':
|
||||||
|
sql += ' ORDER BY npm_downloads DESC';
|
||||||
|
break;
|
||||||
|
case 'updated':
|
||||||
|
sql += ' ORDER BY community_fetched_at DESC';
|
||||||
|
break;
|
||||||
|
case 'name':
|
||||||
|
default:
|
||||||
|
sql += ' ORDER BY display_name';
|
||||||
|
}
|
||||||
|
if (options?.limit) {
|
||||||
|
sql += ' LIMIT ?';
|
||||||
|
params.push(options.limit);
|
||||||
|
}
|
||||||
|
const rows = this.db.prepare(sql).all(...params);
|
||||||
|
return rows.map(row => this.parseNodeRow(row));
|
||||||
|
}
|
||||||
|
getCommunityStats() {
|
||||||
|
const totalResult = this.db.prepare('SELECT COUNT(*) as count FROM nodes WHERE is_community = 1').get();
|
||||||
|
const verifiedResult = this.db.prepare('SELECT COUNT(*) as count FROM nodes WHERE is_community = 1 AND is_verified = 1').get();
|
||||||
|
return {
|
||||||
|
total: totalResult.count,
|
||||||
|
verified: verifiedResult.count,
|
||||||
|
unverified: totalResult.count - verifiedResult.count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
hasNodeByNpmPackage(npmPackageName) {
|
||||||
|
const result = this.db.prepare('SELECT 1 FROM nodes WHERE npm_package_name = ? LIMIT 1').get(npmPackageName);
|
||||||
|
return !!result;
|
||||||
|
}
|
||||||
|
getNodeByNpmPackage(npmPackageName) {
|
||||||
|
const row = this.db.prepare('SELECT * FROM nodes WHERE npm_package_name = ?').get(npmPackageName);
|
||||||
|
if (!row)
|
||||||
|
return null;
|
||||||
|
return this.parseNodeRow(row);
|
||||||
|
}
|
||||||
|
deleteCommunityNodes() {
|
||||||
|
const result = this.db.prepare('DELETE FROM nodes WHERE is_community = 1').run();
|
||||||
|
return result.changes;
|
||||||
|
}
|
||||||
|
updateNodeReadme(nodeType, readme) {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
UPDATE nodes SET npm_readme = ? WHERE node_type = ?
|
||||||
|
`);
|
||||||
|
stmt.run(readme, nodeType);
|
||||||
|
}
|
||||||
|
updateNodeAISummary(nodeType, summary) {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
UPDATE nodes
|
||||||
|
SET ai_documentation_summary = ?, ai_summary_generated_at = datetime('now')
|
||||||
|
WHERE node_type = ?
|
||||||
|
`);
|
||||||
|
stmt.run(JSON.stringify(summary), nodeType);
|
||||||
|
}
|
||||||
|
getCommunityNodesWithoutReadme() {
|
||||||
|
const rows = this.db.prepare(`
|
||||||
|
SELECT * FROM nodes
|
||||||
|
WHERE is_community = 1 AND (npm_readme IS NULL OR npm_readme = '')
|
||||||
|
ORDER BY npm_downloads DESC
|
||||||
|
`).all();
|
||||||
|
return rows.map(row => this.parseNodeRow(row));
|
||||||
|
}
|
||||||
|
getCommunityNodesWithoutAISummary() {
|
||||||
|
const rows = this.db.prepare(`
|
||||||
|
SELECT * FROM nodes
|
||||||
|
WHERE is_community = 1
|
||||||
|
AND npm_readme IS NOT NULL AND npm_readme != ''
|
||||||
|
AND (ai_documentation_summary IS NULL OR ai_documentation_summary = '')
|
||||||
|
ORDER BY npm_downloads DESC
|
||||||
|
`).all();
|
||||||
|
return rows.map(row => this.parseNodeRow(row));
|
||||||
|
}
|
||||||
|
getDocumentationStats() {
|
||||||
|
const total = this.db.prepare('SELECT COUNT(*) as count FROM nodes WHERE is_community = 1').get().count;
|
||||||
|
const withReadme = this.db.prepare("SELECT COUNT(*) as count FROM nodes WHERE is_community = 1 AND npm_readme IS NOT NULL AND npm_readme != ''").get().count;
|
||||||
|
const withAISummary = this.db.prepare("SELECT COUNT(*) as count FROM nodes WHERE is_community = 1 AND ai_documentation_summary IS NOT NULL AND ai_documentation_summary != ''").get().count;
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
withReadme,
|
||||||
|
withAISummary,
|
||||||
|
needingReadme: total - withReadme,
|
||||||
|
needingAISummary: withReadme - withAISummary
|
||||||
|
};
|
||||||
|
}
|
||||||
saveNodeVersion(versionData) {
|
saveNodeVersion(versionData) {
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
INSERT OR REPLACE INTO node_versions (
|
INSERT OR REPLACE INTO node_versions (
|
||||||
|
|||||||
2
dist/database/node-repository.js.map
vendored
2
dist/database/node-repository.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/http-server-single-session.d.ts.map
vendored
2
dist/http-server-single-session.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"http-server-single-session.d.ts","sourceRoot":"","sources":["../src/http-server-single-session.ts"],"names":[],"mappings":";AAMA,OAAO,OAAO,MAAM,SAAS,CAAC;AAoB9B,OAAO,EAAE,eAAe,EAA2B,MAAM,0BAA0B,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAuErD,qBAAa,uBAAuB;IAElC,OAAO,CAAC,UAAU,CAA8D;IAChF,OAAO,CAAC,OAAO,CAA0D;IACzE,OAAO,CAAC,eAAe,CAAsE;IAC7F,OAAO,CAAC,eAAe,CAA4D;IACnF,OAAO,CAAC,kBAAkB,CAAyC;IACnE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,aAAa,CAAM;IAC3B,OAAO,CAAC,cAAc,CAAkB;IACxC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,YAAY,CAA+B;;IAcnD,OAAO,CAAC,mBAAmB;IAmB3B,OAAO,CAAC,sBAAsB;YAqChB,aAAa;IAuC3B,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,sBAAsB;IAkC9B,OAAO,CAAC,mBAAmB;YASb,oBAAoB;YAwBpB,oBAAoB;IAwBlC,OAAO,CAAC,iBAAiB;IAsBzB,OAAO,CAAC,aAAa;IA2BrB,OAAO,CAAC,mBAAmB;IAoDrB,aAAa,CACjB,GAAG,EAAE,OAAO,CAAC,OAAO,EACpB,GAAG,EAAE,OAAO,CAAC,QAAQ,EACrB,eAAe,CAAC,EAAE,eAAe,GAChC,OAAO,CAAC,IAAI,CAAC;YAmOF,eAAe;IA8C7B,OAAO,CAAC,SAAS;IAYjB,OAAO,CAAC,gBAAgB;IASlB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgnBtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAkD/B,cAAc,IAAI;QAChB,MAAM,EAAE,OAAO,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE;YACT,KAAK,EAAE,MAAM,CAAC;YACd,MAAM,EAAE,MAAM,CAAC;YACf,OAAO,EAAE,MAAM,CAAC;YAChB,GAAG,EAAE,MAAM,CAAC;YACZ,UAAU,EAAE,MAAM,EAAE,CAAC;SACtB,CAAC;KACH;IAmDM,kBAAkB,IAAI,YAAY,EAAE;IAoEpC,mBAAmB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,MAAM;CAsG7D"}
|
{"version":3,"file":"http-server-single-session.d.ts","sourceRoot":"","sources":["../src/http-server-single-session.ts"],"names":[],"mappings":";AAMA,OAAO,OAAO,MAAM,SAAS,CAAC;AAoB9B,OAAO,EAAE,eAAe,EAA2B,MAAM,0BAA0B,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAwErD,qBAAa,uBAAuB;IAElC,OAAO,CAAC,UAAU,CAA8D;IAChF,OAAO,CAAC,OAAO,CAA0D;IACzE,OAAO,CAAC,eAAe,CAAsE;IAC7F,OAAO,CAAC,eAAe,CAA4D;IACnF,OAAO,CAAC,kBAAkB,CAAyC;IACnE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,aAAa,CAAM;IAI3B,OAAO,CAAC,cAAc,CAER;IACd,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,YAAY,CAA+B;;IAcnD,OAAO,CAAC,mBAAmB;IAmB3B,OAAO,CAAC,sBAAsB;YAqChB,aAAa;IAuC3B,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,sBAAsB;IAkC9B,OAAO,CAAC,mBAAmB;YASb,oBAAoB;YAwBpB,oBAAoB;IAwBlC,OAAO,CAAC,iBAAiB;IAsBzB,OAAO,CAAC,aAAa;IA2BrB,OAAO,CAAC,mBAAmB;IAoDrB,aAAa,CACjB,GAAG,EAAE,OAAO,CAAC,OAAO,EACpB,GAAG,EAAE,OAAO,CAAC,QAAQ,EACrB,eAAe,CAAC,EAAE,eAAe,GAChC,OAAO,CAAC,IAAI,CAAC;YA0PF,eAAe;IA4D7B,OAAO,CAAC,SAAS;IAYjB,OAAO,CAAC,gBAAgB;IASlB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgnBtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IA2D/B,cAAc,IAAI;QAChB,MAAM,EAAE,OAAO,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE;YACT,KAAK,EAAE,MAAM,CAAC;YACd,MAAM,EAAE,MAAM,CAAC;YACf,OAAO,EAAE,MAAM,CAAC;YAChB,GAAG,EAAE,MAAM,CAAC;YACZ,UAAU,EAAE,MAAM,EAAE,CAAC;SACtB,CAAC;KACH;IAmDM,kBAAkB,IAAI,YAAY,EAAE;IAoEpC,mBAAmB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,MAAM;CAsG7D"}
|
||||||
44
dist/http-server-single-session.js
vendored
44
dist/http-server-single-session.js
vendored
@@ -22,6 +22,7 @@ const crypto_1 = require("crypto");
|
|||||||
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
||||||
const protocol_version_1 = require("./utils/protocol-version");
|
const protocol_version_1 = require("./utils/protocol-version");
|
||||||
const instance_context_1 = require("./types/instance-context");
|
const instance_context_1 = require("./types/instance-context");
|
||||||
|
const shared_database_1 = require("./database/shared-database");
|
||||||
dotenv_1.default.config();
|
dotenv_1.default.config();
|
||||||
const DEFAULT_PROTOCOL_VERSION = protocol_version_1.STANDARD_PROTOCOL_VERSION;
|
const DEFAULT_PROTOCOL_VERSION = protocol_version_1.STANDARD_PROTOCOL_VERSION;
|
||||||
const MAX_SESSIONS = Math.max(1, parseInt(process.env.N8N_MCP_MAX_SESSIONS || '100', 10));
|
const MAX_SESSIONS = Math.max(1, parseInt(process.env.N8N_MCP_MAX_SESSIONS || '100', 10));
|
||||||
@@ -52,7 +53,7 @@ class SingleSessionHTTPServer {
|
|||||||
this.contextSwitchLocks = new Map();
|
this.contextSwitchLocks = new Map();
|
||||||
this.session = null;
|
this.session = null;
|
||||||
this.consoleManager = new console_manager_1.ConsoleManager();
|
this.consoleManager = new console_manager_1.ConsoleManager();
|
||||||
this.sessionTimeout = 30 * 60 * 1000;
|
this.sessionTimeout = parseInt(process.env.SESSION_TIMEOUT_MINUTES || '5', 10) * 60 * 1000;
|
||||||
this.authToken = null;
|
this.authToken = null;
|
||||||
this.cleanupTimer = null;
|
this.cleanupTimer = null;
|
||||||
this.validateEnvironment();
|
this.validateEnvironment();
|
||||||
@@ -290,6 +291,25 @@ class SingleSessionHTTPServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger_1.logger.info('handleRequest: Creating new transport for initialize request');
|
logger_1.logger.info('handleRequest: Creating new transport for initialize request');
|
||||||
|
if (instanceContext?.instanceId) {
|
||||||
|
const sessionsToRemove = [];
|
||||||
|
for (const [existingSessionId, context] of Object.entries(this.sessionContexts)) {
|
||||||
|
if (context?.instanceId === instanceContext.instanceId) {
|
||||||
|
sessionsToRemove.push(existingSessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const oldSessionId of sessionsToRemove) {
|
||||||
|
if (!this.transports[oldSessionId]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logger_1.logger.info('Cleaning up previous session for instance', {
|
||||||
|
instanceId: instanceContext.instanceId,
|
||||||
|
oldSession: oldSessionId,
|
||||||
|
reason: 'instance_reconnect'
|
||||||
|
});
|
||||||
|
await this.removeSession(oldSessionId, 'instance_reconnect');
|
||||||
|
}
|
||||||
|
}
|
||||||
let sessionIdToUse;
|
let sessionIdToUse;
|
||||||
const isMultiTenantEnabled = process.env.ENABLE_MULTI_TENANT === 'true';
|
const isMultiTenantEnabled = process.env.ENABLE_MULTI_TENANT === 'true';
|
||||||
const sessionStrategy = process.env.MULTI_TENANT_SESSION_STRATEGY || 'instance';
|
const sessionStrategy = process.env.MULTI_TENANT_SESSION_STRATEGY || 'instance';
|
||||||
@@ -434,12 +454,21 @@ class SingleSessionHTTPServer {
|
|||||||
}
|
}
|
||||||
async resetSessionSSE(res) {
|
async resetSessionSSE(res) {
|
||||||
if (this.session) {
|
if (this.session) {
|
||||||
|
const sessionId = this.session.sessionId;
|
||||||
|
logger_1.logger.info('Closing previous session for SSE', { sessionId });
|
||||||
|
if (this.session.server && typeof this.session.server.close === 'function') {
|
||||||
|
try {
|
||||||
|
await this.session.server.close();
|
||||||
|
}
|
||||||
|
catch (serverError) {
|
||||||
|
logger_1.logger.warn('Error closing server for SSE session', { sessionId, error: serverError });
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
logger_1.logger.info('Closing previous session for SSE', { sessionId: this.session.sessionId });
|
|
||||||
await this.session.transport.close();
|
await this.session.transport.close();
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (transportError) {
|
||||||
logger_1.logger.warn('Error closing previous session:', error);
|
logger_1.logger.warn('Error closing transport for SSE session', { sessionId, error: transportError });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -1014,6 +1043,13 @@ class SingleSessionHTTPServer {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await (0, shared_database_1.closeSharedDatabase)();
|
||||||
|
logger_1.logger.info('Shared database closed');
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
logger_1.logger.warn('Error closing shared database:', error);
|
||||||
|
}
|
||||||
logger_1.logger.info('Single-Session HTTP server shutdown completed');
|
logger_1.logger.info('Single-Session HTTP server shutdown completed');
|
||||||
}
|
}
|
||||||
getSessionInfo() {
|
getSessionInfo() {
|
||||||
|
|||||||
2
dist/http-server-single-session.js.map
vendored
2
dist/http-server-single-session.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/http-server.d.ts.map
vendored
2
dist/http-server.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":";AA0CA,wBAAgB,aAAa,IAAI,MAAM,GAAG,IAAI,CAsB7C;AA+DD,wBAAsB,oBAAoB,kBA+dzC;AAGD,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,yBAAyB;QACjC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;KACpD;CACF"}
|
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":";AAiDA,wBAAgB,aAAa,IAAI,MAAM,GAAG,IAAI,CAsB7C;AAmED,wBAAsB,oBAAoB,kBAsezC;AAGD,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,yBAAyB;QACjC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;KACpD;CACF"}
|
||||||
3
dist/http-server.js
vendored
3
dist/http-server.js
vendored
@@ -85,6 +85,9 @@ async function shutdown() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function startFixedHTTPServer() {
|
async function startFixedHTTPServer() {
|
||||||
|
logger_1.logger.warn('DEPRECATION: startFixedHTTPServer() is deprecated as of v2.31.8. ' +
|
||||||
|
'Use SingleSessionHTTPServer which supports SSE streaming. ' +
|
||||||
|
'See: https://github.com/czlonkowski/n8n-mcp/issues/524');
|
||||||
validateEnvironment();
|
validateEnvironment();
|
||||||
const app = (0, express_1.default)();
|
const app = (0, express_1.default)();
|
||||||
const trustProxy = process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) : 0;
|
const trustProxy = process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) : 0;
|
||||||
|
|||||||
2
dist/http-server.js.map
vendored
2
dist/http-server.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/index.d.ts
vendored
2
dist/index.d.ts
vendored
@@ -5,6 +5,8 @@ export { N8NDocumentationMCPServer } from './mcp/server';
|
|||||||
export type { InstanceContext } from './types/instance-context';
|
export type { InstanceContext } from './types/instance-context';
|
||||||
export { validateInstanceContext, isInstanceContext } from './types/instance-context';
|
export { validateInstanceContext, isInstanceContext } from './types/instance-context';
|
||||||
export type { SessionState } from './types/session-state';
|
export type { SessionState } from './types/session-state';
|
||||||
|
export type { UIAppConfig, UIMetadata } from './mcp/ui/types';
|
||||||
|
export { UI_APP_CONFIGS } from './mcp/ui/app-configs';
|
||||||
export type { Tool, CallToolResult, ListToolsResult } from '@modelcontextprotocol/sdk/types.js';
|
export type { Tool, CallToolResult, ListToolsResult } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import N8NMCPEngine from './mcp-engine';
|
import N8NMCPEngine from './mcp-engine';
|
||||||
export default N8NMCPEngine;
|
export default N8NMCPEngine;
|
||||||
|
|||||||
2
dist/index.d.ts.map
vendored
2
dist/index.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAGzD,YAAY,EACV,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,YAAY,EACb,MAAM,uBAAuB,CAAC;AAG/B,YAAY,EACV,IAAI,EACJ,cAAc,EACd,eAAe,EAChB,MAAM,oCAAoC,CAAC;AAG5C,OAAO,YAAY,MAAM,cAAc,CAAC;AACxC,eAAe,YAAY,CAAC"}
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAGzD,YAAY,EACV,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,YAAY,EACb,MAAM,uBAAuB,CAAC;AAG/B,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGtD,YAAY,EACV,IAAI,EACJ,cAAc,EACd,eAAe,EAChB,MAAM,oCAAoC,CAAC;AAG5C,OAAO,YAAY,MAAM,cAAc,CAAC;AACxC,eAAe,YAAY,CAAC"}
|
||||||
4
dist/index.js
vendored
4
dist/index.js
vendored
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.isInstanceContext = exports.validateInstanceContext = exports.N8NDocumentationMCPServer = exports.ConsoleManager = exports.SingleSessionHTTPServer = exports.N8NMCPEngine = void 0;
|
exports.UI_APP_CONFIGS = exports.isInstanceContext = exports.validateInstanceContext = exports.N8NDocumentationMCPServer = exports.ConsoleManager = exports.SingleSessionHTTPServer = exports.N8NMCPEngine = void 0;
|
||||||
var mcp_engine_1 = require("./mcp-engine");
|
var mcp_engine_1 = require("./mcp-engine");
|
||||||
Object.defineProperty(exports, "N8NMCPEngine", { enumerable: true, get: function () { return mcp_engine_1.N8NMCPEngine; } });
|
Object.defineProperty(exports, "N8NMCPEngine", { enumerable: true, get: function () { return mcp_engine_1.N8NMCPEngine; } });
|
||||||
var http_server_single_session_1 = require("./http-server-single-session");
|
var http_server_single_session_1 = require("./http-server-single-session");
|
||||||
@@ -15,6 +15,8 @@ Object.defineProperty(exports, "N8NDocumentationMCPServer", { enumerable: true,
|
|||||||
var instance_context_1 = require("./types/instance-context");
|
var instance_context_1 = require("./types/instance-context");
|
||||||
Object.defineProperty(exports, "validateInstanceContext", { enumerable: true, get: function () { return instance_context_1.validateInstanceContext; } });
|
Object.defineProperty(exports, "validateInstanceContext", { enumerable: true, get: function () { return instance_context_1.validateInstanceContext; } });
|
||||||
Object.defineProperty(exports, "isInstanceContext", { enumerable: true, get: function () { return instance_context_1.isInstanceContext; } });
|
Object.defineProperty(exports, "isInstanceContext", { enumerable: true, get: function () { return instance_context_1.isInstanceContext; } });
|
||||||
|
var app_configs_1 = require("./mcp/ui/app-configs");
|
||||||
|
Object.defineProperty(exports, "UI_APP_CONFIGS", { enumerable: true, get: function () { return app_configs_1.UI_APP_CONFIGS; } });
|
||||||
const mcp_engine_2 = __importDefault(require("./mcp-engine"));
|
const mcp_engine_2 = __importDefault(require("./mcp-engine"));
|
||||||
exports.default = mcp_engine_2.default;
|
exports.default = mcp_engine_2.default;
|
||||||
//# sourceMappingURL=index.js.map
|
//# sourceMappingURL=index.js.map
|
||||||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAOA,2CAAyE;AAAhE,0GAAA,YAAY,OAAA;AACrB,2EAAuE;AAA9D,qIAAA,uBAAuB,OAAA;AAChC,2DAAyD;AAAhD,iHAAA,cAAc,OAAA;AACvB,uCAAyD;AAAhD,mHAAA,yBAAyB,OAAA;AAMlC,6DAGkC;AAFhC,2HAAA,uBAAuB,OAAA;AACvB,qHAAA,iBAAiB,OAAA;AAcnB,8DAAwC;AACxC,kBAAe,oBAAY,CAAC"}
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAOA,2CAAyE;AAAhE,0GAAA,YAAY,OAAA;AACrB,2EAAuE;AAA9D,qIAAA,uBAAuB,OAAA;AAChC,2DAAyD;AAAhD,iHAAA,cAAc,OAAA;AACvB,uCAAyD;AAAhD,mHAAA,yBAAyB,OAAA;AAMlC,6DAGkC;AAFhC,2HAAA,uBAAuB,OAAA;AACvB,qHAAA,iBAAiB,OAAA;AAQnB,oDAAsD;AAA7C,6GAAA,cAAc,OAAA;AAUvB,8DAAwC;AACxC,kBAAe,oBAAY,CAAC"}
|
||||||
2
dist/loaders/node-loader.d.ts
vendored
2
dist/loaders/node-loader.d.ts
vendored
@@ -6,6 +6,8 @@ export interface LoadedNode {
|
|||||||
export declare class N8nNodeLoader {
|
export declare class N8nNodeLoader {
|
||||||
private readonly CORE_PACKAGES;
|
private readonly CORE_PACKAGES;
|
||||||
loadAllNodes(): Promise<LoadedNode[]>;
|
loadAllNodes(): Promise<LoadedNode[]>;
|
||||||
|
private resolvePackageDir;
|
||||||
|
private loadNodeModule;
|
||||||
private loadPackageNodes;
|
private loadPackageNodes;
|
||||||
}
|
}
|
||||||
//# sourceMappingURL=node-loader.d.ts.map
|
//# sourceMappingURL=node-loader.d.ts.map
|
||||||
2
dist/loaders/node-loader.d.ts.map
vendored
2
dist/loaders/node-loader.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"node-loader.d.ts","sourceRoot":"","sources":["../../src/loaders/node-loader.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,GAAG,CAAC;CAChB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAG5B;IAEI,YAAY,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YAmB7B,gBAAgB;CAqD/B"}
|
{"version":3,"file":"node-loader.d.ts","sourceRoot":"","sources":["../../src/loaders/node-loader.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,GAAG,CAAC;CAChB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAG5B;IAEI,YAAY,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAuB3C,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,cAAc;YAIR,gBAAgB;CAuD/B"}
|
||||||
16
dist/loaders/node-loader.js
vendored
16
dist/loaders/node-loader.js
vendored
@@ -28,15 +28,23 @@ class N8nNodeLoader {
|
|||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
resolvePackageDir(packagePath) {
|
||||||
|
const pkgJsonPath = require.resolve(`${packagePath}/package.json`);
|
||||||
|
return path_1.default.dirname(pkgJsonPath);
|
||||||
|
}
|
||||||
|
loadNodeModule(absolutePath) {
|
||||||
|
return require(absolutePath);
|
||||||
|
}
|
||||||
async loadPackageNodes(packageName, packagePath, packageJson) {
|
async loadPackageNodes(packageName, packagePath, packageJson) {
|
||||||
const n8nConfig = packageJson.n8n || {};
|
const n8nConfig = packageJson.n8n || {};
|
||||||
const nodes = [];
|
const nodes = [];
|
||||||
|
const packageDir = this.resolvePackageDir(packagePath);
|
||||||
const nodesList = n8nConfig.nodes || [];
|
const nodesList = n8nConfig.nodes || [];
|
||||||
if (Array.isArray(nodesList)) {
|
if (Array.isArray(nodesList)) {
|
||||||
for (const nodePath of nodesList) {
|
for (const nodePath of nodesList) {
|
||||||
try {
|
try {
|
||||||
const fullPath = require.resolve(`${packagePath}/${nodePath}`);
|
const fullPath = path_1.default.join(packageDir, nodePath);
|
||||||
const nodeModule = require(fullPath);
|
const nodeModule = this.loadNodeModule(fullPath);
|
||||||
const nodeNameMatch = nodePath.match(/\/([^\/]+)\.node\.(js|ts)$/);
|
const nodeNameMatch = nodePath.match(/\/([^\/]+)\.node\.(js|ts)$/);
|
||||||
const nodeName = nodeNameMatch ? nodeNameMatch[1] : path_1.default.basename(nodePath, '.node.js');
|
const nodeName = nodeNameMatch ? nodeNameMatch[1] : path_1.default.basename(nodePath, '.node.js');
|
||||||
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
|
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
|
||||||
@@ -56,8 +64,8 @@ class N8nNodeLoader {
|
|||||||
else {
|
else {
|
||||||
for (const [nodeName, nodePath] of Object.entries(nodesList)) {
|
for (const [nodeName, nodePath] of Object.entries(nodesList)) {
|
||||||
try {
|
try {
|
||||||
const fullPath = require.resolve(`${packagePath}/${nodePath}`);
|
const fullPath = path_1.default.join(packageDir, nodePath);
|
||||||
const nodeModule = require(fullPath);
|
const nodeModule = this.loadNodeModule(fullPath);
|
||||||
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
|
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
|
||||||
if (NodeClass) {
|
if (NodeClass) {
|
||||||
nodes.push({ packageName, nodeName, NodeClass });
|
nodes.push({ packageName, nodeName, NodeClass });
|
||||||
|
|||||||
2
dist/loaders/node-loader.js.map
vendored
2
dist/loaders/node-loader.js.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"node-loader.js","sourceRoot":"","sources":["../../src/loaders/node-loader.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"node-loader.js","sourceRoot":"","sources":["../../src/loaders/node-loader.ts"],"names":[],"mappings":"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"}
|
||||||
2
dist/mcp/handlers-n8n-manager.d.ts.map
vendored
2
dist/mcp/handlers-n8n-manager.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"handlers-n8n-manager.d.ts","sourceRoot":"","sources":["../../src/mcp/handlers-n8n-manager.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"handlers-n8n-manager.d.ts","sourceRoot":"","sources":["../../src/mcp/handlers-n8n-manager.ts"],"names":[],"mappings":"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"}
|
||||||
8
dist/mcp/handlers-n8n-manager.js
vendored
8
dist/mcp/handlers-n8n-manager.js
vendored
@@ -212,7 +212,13 @@ const autofixWorkflowSchema = zod_1.z.object({
|
|||||||
'node-type-correction',
|
'node-type-correction',
|
||||||
'webhook-missing-path',
|
'webhook-missing-path',
|
||||||
'typeversion-upgrade',
|
'typeversion-upgrade',
|
||||||
'version-migration'
|
'version-migration',
|
||||||
|
'tool-variant-correction',
|
||||||
|
'connection-numeric-keys',
|
||||||
|
'connection-invalid-type',
|
||||||
|
'connection-id-to-name',
|
||||||
|
'connection-duplicate-removal',
|
||||||
|
'connection-input-index'
|
||||||
])).optional(),
|
])).optional(),
|
||||||
confidenceThreshold: zod_1.z.enum(['high', 'medium', 'low']).optional().default('medium'),
|
confidenceThreshold: zod_1.z.enum(['high', 'medium', 'low']).optional().default('medium'),
|
||||||
maxFixes: zod_1.z.number().optional().default(50)
|
maxFixes: zod_1.z.number().optional().default(50)
|
||||||
|
|||||||
2
dist/mcp/handlers-n8n-manager.js.map
vendored
2
dist/mcp/handlers-n8n-manager.js.map
vendored
File diff suppressed because one or more lines are too long
9
dist/mcp/index.js
vendored
9
dist/mcp/index.js
vendored
@@ -124,6 +124,15 @@ Learn more: https://github.com/czlonkowski/n8n-mcp/blob/main/PRIVACY.md
|
|||||||
checkpoints.push(startup_checkpoints_1.STARTUP_CHECKPOINTS.MCP_HANDSHAKE_STARTING);
|
checkpoints.push(startup_checkpoints_1.STARTUP_CHECKPOINTS.MCP_HANDSHAKE_STARTING);
|
||||||
if (mode === 'http') {
|
if (mode === 'http') {
|
||||||
if (process.env.USE_FIXED_HTTP === 'true') {
|
if (process.env.USE_FIXED_HTTP === 'true') {
|
||||||
|
logger_1.logger.warn('DEPRECATION WARNING: USE_FIXED_HTTP=true is deprecated as of v2.31.8. ' +
|
||||||
|
'The fixed HTTP implementation does not support SSE streaming required by clients like OpenAI Codex. ' +
|
||||||
|
'Please unset USE_FIXED_HTTP to use the modern SingleSessionHTTPServer which supports both JSON-RPC and SSE. ' +
|
||||||
|
'This option will be removed in a future version. See: https://github.com/czlonkowski/n8n-mcp/issues/524');
|
||||||
|
console.warn('\n⚠️ DEPRECATION WARNING ⚠️');
|
||||||
|
console.warn('USE_FIXED_HTTP=true is deprecated as of v2.31.8.');
|
||||||
|
console.warn('The fixed HTTP implementation does not support SSE streaming.');
|
||||||
|
console.warn('Please unset USE_FIXED_HTTP to use SingleSessionHTTPServer.');
|
||||||
|
console.warn('See: https://github.com/czlonkowski/n8n-mcp/issues/524\n');
|
||||||
const { startFixedHTTPServer } = await Promise.resolve().then(() => __importStar(require('../http-server')));
|
const { startFixedHTTPServer } = await Promise.resolve().then(() => __importStar(require('../http-server')));
|
||||||
await startFixedHTTPServer();
|
await startFixedHTTPServer();
|
||||||
}
|
}
|
||||||
|
|||||||
2
dist/mcp/index.js.map
vendored
2
dist/mcp/index.js.map
vendored
File diff suppressed because one or more lines are too long
5
dist/mcp/server.d.ts
vendored
5
dist/mcp/server.d.ts
vendored
@@ -13,6 +13,9 @@ export declare class N8NDocumentationMCPServer {
|
|||||||
private previousToolTimestamp;
|
private previousToolTimestamp;
|
||||||
private earlyLogger;
|
private earlyLogger;
|
||||||
private disabledToolsCache;
|
private disabledToolsCache;
|
||||||
|
private useSharedDatabase;
|
||||||
|
private sharedDbState;
|
||||||
|
private isShutdown;
|
||||||
constructor(instanceContext?: InstanceContext, earlyLogger?: EarlyErrorLogger);
|
constructor(instanceContext?: InstanceContext, earlyLogger?: EarlyErrorLogger);
|
||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
private initializeDatabase;
|
private initializeDatabase;
|
||||||
@@ -27,6 +30,7 @@ export declare class N8NDocumentationMCPServer {
|
|||||||
private validateToolParams;
|
private validateToolParams;
|
||||||
private validateToolParamsBasic;
|
private validateToolParamsBasic;
|
||||||
private validateExtractedArgs;
|
private validateExtractedArgs;
|
||||||
|
private coerceStringifiedJsonParams;
|
||||||
private listNodes;
|
private listNodes;
|
||||||
private getNodeInfo;
|
private getNodeInfo;
|
||||||
private searchNodes;
|
private searchNodes;
|
||||||
@@ -40,6 +44,7 @@ export declare class N8NDocumentationMCPServer {
|
|||||||
private rankSearchResults;
|
private rankSearchResults;
|
||||||
private listAITools;
|
private listAITools;
|
||||||
private getNodeDocumentation;
|
private getNodeDocumentation;
|
||||||
|
private safeJsonParse;
|
||||||
private getDatabaseStatistics;
|
private getDatabaseStatistics;
|
||||||
private getNodeEssentials;
|
private getNodeEssentials;
|
||||||
private getNode;
|
private getNode;
|
||||||
|
|||||||
2
dist/mcp/server.d.ts.map
vendored
2
dist/mcp/server.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAsCA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAgGnE,qBAAa,yBAAyB;IACpC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,EAAE,CAAgC;IAC1C,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,qBAAqB,CAAsB;IACnD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,kBAAkB,CAA4B;gBAE1C,eAAe,CAAC,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,gBAAgB;IAiGvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YA6Bd,kBAAkB;YAwClB,wBAAwB;IA0BtC,OAAO,CAAC,kBAAkB;YA6CZ,iBAAiB;IAa/B,OAAO,CAAC,eAAe,CAAkB;YAE3B,sBAAsB;IAgDpC,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,aAAa;IAoTrB,OAAO,CAAC,wBAAwB;IAoFhC,OAAO,CAAC,kBAAkB;IAqE1B,OAAO,CAAC,uBAAuB;IAwB/B,OAAO,CAAC,qBAAqB;YAgTf,SAAS;YA2DT,WAAW;YAkFX,WAAW;YAyCX,cAAc;YAyKd,gBAAgB;IAqD9B,OAAO,CAAC,mBAAmB;IAwE3B,OAAO,CAAC,eAAe;YAsBT,eAAe;IAqI7B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,uBAAuB;IA0D/B,OAAO,CAAC,iBAAiB;YAqFX,WAAW;YAgCX,oBAAoB;YA2EpB,qBAAqB;YAwDrB,iBAAiB;YAiKjB,OAAO;YAgDP,cAAc;YAwFd,iBAAiB;IAqC/B,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,eAAe;IAwCvB,OAAO,CAAC,kBAAkB;IAiC1B,OAAO,CAAC,aAAa;IAoCrB,OAAO,CAAC,0BAA0B;IAgClC,OAAO,CAAC,4BAA4B;YAKtB,oBAAoB;IAsDlC,OAAO,CAAC,gBAAgB;YAiBV,SAAS;YA6CT,kBAAkB;YAqElB,uBAAuB;YAsDvB,iBAAiB;IAqE/B,OAAO,CAAC,qBAAqB;IA8C7B,OAAO,CAAC,uBAAuB;IA4D/B,OAAO,CAAC,wBAAwB;IAkChC,OAAO,CAAC,iBAAiB;YAoDX,mBAAmB;YAoEnB,qBAAqB;IAS7B,OAAO,CAAC,SAAS,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;YAS9B,aAAa;YAcb,iBAAiB;YAoBjB,WAAW;YAwBX,eAAe;YAqBf,mBAAmB;YAwBnB,yBAAyB;IA4CvC,OAAO,CAAC,kBAAkB;YAiBZ,gBAAgB;YA6HhB,2BAA2B;YAiE3B,2BAA2B;IAyEnC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BpB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAuBhC"}
|
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AA0CA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAmGnE,qBAAa,yBAAyB;IACpC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,EAAE,CAAgC;IAC1C,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,qBAAqB,CAAsB;IACnD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,kBAAkB,CAA4B;IACtD,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,UAAU,CAAkB;gBAExB,eAAe,CAAC,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,gBAAgB;IAuGvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YA+Cd,kBAAkB;YAiDlB,wBAAwB;IA0BtC,OAAO,CAAC,kBAAkB;YA6CZ,iBAAiB;IAa/B,OAAO,CAAC,eAAe,CAAkB;YAE3B,sBAAsB;IAgDpC,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,aAAa;IA0XrB,OAAO,CAAC,wBAAwB;IAoFhC,OAAO,CAAC,kBAAkB;IAqE1B,OAAO,CAAC,uBAAuB;IAwB/B,OAAO,CAAC,qBAAqB;IAiF7B,OAAO,CAAC,2BAA2B;YA0UrB,SAAS;YA2DT,WAAW;YAkFX,WAAW;YA0CX,cAAc;YA8Md,gBAAgB;IAqD9B,OAAO,CAAC,mBAAmB;IAwE3B,OAAO,CAAC,eAAe;YAsBT,eAAe;IA2L7B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,uBAAuB;IA0D/B,OAAO,CAAC,iBAAiB;YAqFX,WAAW;YAgCX,oBAAoB;IAuFlC,OAAO,CAAC,aAAa;YAQP,qBAAqB;YAwDrB,iBAAiB;YAiKjB,OAAO;YAgDP,cAAc;YAwFd,iBAAiB;IAqC/B,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,eAAe;IAwCvB,OAAO,CAAC,kBAAkB;IAiC1B,OAAO,CAAC,aAAa;IAoCrB,OAAO,CAAC,0BAA0B;IAgClC,OAAO,CAAC,4BAA4B;YAKtB,oBAAoB;IAsDlC,OAAO,CAAC,gBAAgB;YAiBV,SAAS;YA6CT,kBAAkB;YAqElB,uBAAuB;YAsDvB,iBAAiB;IAqE/B,OAAO,CAAC,qBAAqB;IA8C7B,OAAO,CAAC,uBAAuB;IA4D/B,OAAO,CAAC,wBAAwB;IAkChC,OAAO,CAAC,iBAAiB;YAoDX,mBAAmB;YAoEnB,qBAAqB;IAS7B,OAAO,CAAC,SAAS,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;YAS9B,aAAa;YAcb,iBAAiB;YAoBjB,WAAW;YAwBX,eAAe;YAqBf,mBAAmB;YAwBnB,yBAAyB;IA4CvC,OAAO,CAAC,kBAAkB;YAiBZ,gBAAgB;YA6HhB,2BAA2B;YAiE3B,2BAA2B;IAyEnC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BpB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAgEhC"}
|
||||||
383
dist/mcp/server.js
vendored
383
dist/mcp/server.js
vendored
@@ -43,12 +43,14 @@ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|||||||
const fs_1 = require("fs");
|
const fs_1 = require("fs");
|
||||||
const path_1 = __importDefault(require("path"));
|
const path_1 = __importDefault(require("path"));
|
||||||
const tools_1 = require("./tools");
|
const tools_1 = require("./tools");
|
||||||
|
const ui_1 = require("./ui");
|
||||||
const tools_n8n_manager_1 = require("./tools-n8n-manager");
|
const tools_n8n_manager_1 = require("./tools-n8n-manager");
|
||||||
const tools_n8n_friendly_1 = require("./tools-n8n-friendly");
|
const tools_n8n_friendly_1 = require("./tools-n8n-friendly");
|
||||||
const workflow_examples_1 = require("./workflow-examples");
|
const workflow_examples_1 = require("./workflow-examples");
|
||||||
const logger_1 = require("../utils/logger");
|
const logger_1 = require("../utils/logger");
|
||||||
const node_repository_1 = require("../database/node-repository");
|
const node_repository_1 = require("../database/node-repository");
|
||||||
const database_adapter_1 = require("../database/database-adapter");
|
const database_adapter_1 = require("../database/database-adapter");
|
||||||
|
const shared_database_1 = require("../database/shared-database");
|
||||||
const property_filter_1 = require("../services/property-filter");
|
const property_filter_1 = require("../services/property-filter");
|
||||||
const task_templates_1 = require("../services/task-templates");
|
const task_templates_1 = require("../services/task-templates");
|
||||||
const config_validator_1 = require("../services/config-validator");
|
const config_validator_1 = require("../services/config-validator");
|
||||||
@@ -80,6 +82,9 @@ class N8NDocumentationMCPServer {
|
|||||||
this.previousToolTimestamp = Date.now();
|
this.previousToolTimestamp = Date.now();
|
||||||
this.earlyLogger = null;
|
this.earlyLogger = null;
|
||||||
this.disabledToolsCache = null;
|
this.disabledToolsCache = null;
|
||||||
|
this.useSharedDatabase = false;
|
||||||
|
this.sharedDbState = null;
|
||||||
|
this.isShutdown = false;
|
||||||
this.dbHealthChecked = false;
|
this.dbHealthChecked = false;
|
||||||
this.instanceContext = instanceContext;
|
this.instanceContext = instanceContext;
|
||||||
this.earlyLogger = earlyLogger || null;
|
this.earlyLogger = earlyLogger || null;
|
||||||
@@ -144,15 +149,29 @@ class N8NDocumentationMCPServer {
|
|||||||
}, {
|
}, {
|
||||||
capabilities: {
|
capabilities: {
|
||||||
tools: {},
|
tools: {},
|
||||||
|
resources: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
ui_1.UIAppRegistry.load();
|
||||||
this.setupHandlers();
|
this.setupHandlers();
|
||||||
}
|
}
|
||||||
async close() {
|
async close() {
|
||||||
|
try {
|
||||||
|
await this.initialized;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
logger_1.logger.debug('Initialization had failed, proceeding with cleanup', {
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await this.server.close();
|
await this.server.close();
|
||||||
this.cache.destroy();
|
this.cache.destroy();
|
||||||
if (this.db) {
|
if (this.useSharedDatabase && this.sharedDbState) {
|
||||||
|
(0, shared_database_1.releaseSharedDatabase)(this.sharedDbState);
|
||||||
|
logger_1.logger.debug('Released shared database reference');
|
||||||
|
}
|
||||||
|
else if (this.db) {
|
||||||
try {
|
try {
|
||||||
this.db.close();
|
this.db.close();
|
||||||
}
|
}
|
||||||
@@ -166,6 +185,7 @@ class N8NDocumentationMCPServer {
|
|||||||
this.repository = null;
|
this.repository = null;
|
||||||
this.templateService = null;
|
this.templateService = null;
|
||||||
this.earlyLogger = null;
|
this.earlyLogger = null;
|
||||||
|
this.sharedDbState = null;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
logger_1.logger.warn('Error closing MCP server', { error: error instanceof Error ? error.message : String(error) });
|
logger_1.logger.warn('Error closing MCP server', { error: error instanceof Error ? error.message : String(error) });
|
||||||
@@ -177,17 +197,27 @@ class N8NDocumentationMCPServer {
|
|||||||
this.earlyLogger.logCheckpoint(startup_checkpoints_1.STARTUP_CHECKPOINTS.DATABASE_CONNECTING);
|
this.earlyLogger.logCheckpoint(startup_checkpoints_1.STARTUP_CHECKPOINTS.DATABASE_CONNECTING);
|
||||||
}
|
}
|
||||||
logger_1.logger.debug('Database initialization starting...', { dbPath });
|
logger_1.logger.debug('Database initialization starting...', { dbPath });
|
||||||
this.db = await (0, database_adapter_1.createDatabaseAdapter)(dbPath);
|
|
||||||
logger_1.logger.debug('Database adapter created');
|
|
||||||
if (dbPath === ':memory:') {
|
if (dbPath === ':memory:') {
|
||||||
|
this.db = await (0, database_adapter_1.createDatabaseAdapter)(dbPath);
|
||||||
|
logger_1.logger.debug('Database adapter created (in-memory mode)');
|
||||||
await this.initializeInMemorySchema();
|
await this.initializeInMemorySchema();
|
||||||
logger_1.logger.debug('In-memory schema initialized');
|
logger_1.logger.debug('In-memory schema initialized');
|
||||||
|
this.repository = new node_repository_1.NodeRepository(this.db);
|
||||||
|
this.templateService = new template_service_1.TemplateService(this.db);
|
||||||
|
enhanced_config_validator_1.EnhancedConfigValidator.initializeSimilarityServices(this.repository);
|
||||||
|
this.useSharedDatabase = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const sharedState = await (0, shared_database_1.getSharedDatabase)(dbPath);
|
||||||
|
this.db = sharedState.db;
|
||||||
|
this.repository = sharedState.repository;
|
||||||
|
this.templateService = sharedState.templateService;
|
||||||
|
this.sharedDbState = sharedState;
|
||||||
|
this.useSharedDatabase = true;
|
||||||
|
logger_1.logger.debug('Using shared database connection');
|
||||||
}
|
}
|
||||||
this.repository = new node_repository_1.NodeRepository(this.db);
|
|
||||||
logger_1.logger.debug('Node repository initialized');
|
logger_1.logger.debug('Node repository initialized');
|
||||||
this.templateService = new template_service_1.TemplateService(this.db);
|
|
||||||
logger_1.logger.debug('Template service initialized');
|
logger_1.logger.debug('Template service initialized');
|
||||||
enhanced_config_validator_1.EnhancedConfigValidator.initializeSimilarityServices(this.repository);
|
|
||||||
logger_1.logger.debug('Similarity services initialized');
|
logger_1.logger.debug('Similarity services initialized');
|
||||||
if (this.earlyLogger) {
|
if (this.earlyLogger) {
|
||||||
this.earlyLogger.logCheckpoint(startup_checkpoints_1.STARTUP_CHECKPOINTS.DATABASE_CONNECTED);
|
this.earlyLogger.logCheckpoint(startup_checkpoints_1.STARTUP_CHECKPOINTS.DATABASE_CONNECTED);
|
||||||
@@ -341,6 +371,7 @@ class N8NDocumentationMCPServer {
|
|||||||
protocolVersion: negotiationResult.version,
|
protocolVersion: negotiationResult.version,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
tools: {},
|
tools: {},
|
||||||
|
resources: {},
|
||||||
},
|
},
|
||||||
serverInfo: {
|
serverInfo: {
|
||||||
name: 'n8n-documentation-mcp',
|
name: 'n8n-documentation-mcp',
|
||||||
@@ -396,6 +427,7 @@ class N8NDocumentationMCPServer {
|
|||||||
description: tool.description
|
description: tool.description
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
ui_1.UIAppRegistry.injectToolMeta(tools);
|
||||||
return { tools };
|
return { tools };
|
||||||
});
|
});
|
||||||
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
||||||
@@ -425,6 +457,18 @@ class N8NDocumentationMCPServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
let processedArgs = args;
|
let processedArgs = args;
|
||||||
|
if (typeof args === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(args);
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
|
processedArgs = parsed;
|
||||||
|
logger_1.logger.warn(`Coerced stringified args object for tool "${name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
logger_1.logger.warn(`Tool "${name}" received string args that are not valid JSON`);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (args && typeof args === 'object' && 'output' in args) {
|
if (args && typeof args === 'object' && 'output' in args) {
|
||||||
try {
|
try {
|
||||||
const possibleNestedData = args.output;
|
const possibleNestedData = args.output;
|
||||||
@@ -453,6 +497,7 @@ class N8NDocumentationMCPServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
processedArgs = this.coerceStringifiedJsonParams(name, processedArgs);
|
||||||
try {
|
try {
|
||||||
logger_1.logger.debug(`Executing tool: ${name}`, { args: processedArgs });
|
logger_1.logger.debug(`Executing tool: ${name}`, { args: processedArgs });
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
@@ -524,6 +569,13 @@ class N8NDocumentationMCPServer {
|
|||||||
if (name.startsWith('validate_') && (errorMessage.includes('config') || errorMessage.includes('nodeType'))) {
|
if (name.startsWith('validate_') && (errorMessage.includes('config') || errorMessage.includes('nodeType'))) {
|
||||||
helpfulMessage += '\n\nFor validation tools:\n- nodeType should be a string (e.g., "nodes-base.webhook")\n- config should be an object (e.g., {})';
|
helpfulMessage += '\n\nFor validation tools:\n- nodeType should be a string (e.g., "nodes-base.webhook")\n- config should be an object (e.g., {})';
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const argDiag = processedArgs && typeof processedArgs === 'object'
|
||||||
|
? Object.entries(processedArgs).map(([k, v]) => `${k}: ${typeof v}`).join(', ')
|
||||||
|
: `args type: ${typeof processedArgs}`;
|
||||||
|
helpfulMessage += `\n\n[Diagnostic] Received arg types: {${argDiag}}`;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -535,6 +587,39 @@ class N8NDocumentationMCPServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
|
||||||
|
const apps = ui_1.UIAppRegistry.getAllApps();
|
||||||
|
return {
|
||||||
|
resources: apps
|
||||||
|
.filter(app => app.html !== null)
|
||||||
|
.map(app => ({
|
||||||
|
uri: app.config.uri,
|
||||||
|
name: app.config.displayName,
|
||||||
|
description: app.config.description,
|
||||||
|
mimeType: app.config.mimeType,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
|
||||||
|
const uri = request.params.uri;
|
||||||
|
const match = uri.match(/^ui:\/\/n8n-mcp\/(.+)$/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Unknown resource URI: ${uri}`);
|
||||||
|
}
|
||||||
|
const app = ui_1.UIAppRegistry.getAppById(match[1]);
|
||||||
|
if (!app || !app.html) {
|
||||||
|
throw new Error(`UI app not found or not built: ${match[1]}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: app.config.uri,
|
||||||
|
mimeType: app.config.mimeType,
|
||||||
|
text: app.html,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
sanitizeValidationResult(result, toolName) {
|
sanitizeValidationResult(result, toolName) {
|
||||||
if (!result || typeof result !== 'object') {
|
if (!result || typeof result !== 'object') {
|
||||||
@@ -730,6 +815,93 @@ class N8NDocumentationMCPServer {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
coerceStringifiedJsonParams(toolName, args) {
|
||||||
|
if (!args || typeof args !== 'object')
|
||||||
|
return args;
|
||||||
|
const allTools = [...tools_1.n8nDocumentationToolsFinal, ...tools_n8n_manager_1.n8nManagementTools];
|
||||||
|
const tool = allTools.find(t => t.name === toolName);
|
||||||
|
if (!tool?.inputSchema?.properties)
|
||||||
|
return args;
|
||||||
|
const properties = tool.inputSchema.properties;
|
||||||
|
const coerced = { ...args };
|
||||||
|
let coercedAny = false;
|
||||||
|
for (const [key, value] of Object.entries(coerced)) {
|
||||||
|
if (value === undefined || value === null)
|
||||||
|
continue;
|
||||||
|
const propSchema = properties[key];
|
||||||
|
if (!propSchema)
|
||||||
|
continue;
|
||||||
|
const expectedType = propSchema.type;
|
||||||
|
if (!expectedType)
|
||||||
|
continue;
|
||||||
|
const actualType = typeof value;
|
||||||
|
if (expectedType === 'string' && actualType === 'string')
|
||||||
|
continue;
|
||||||
|
if ((expectedType === 'number' || expectedType === 'integer') && actualType === 'number')
|
||||||
|
continue;
|
||||||
|
if (expectedType === 'boolean' && actualType === 'boolean')
|
||||||
|
continue;
|
||||||
|
if (expectedType === 'object' && actualType === 'object' && !Array.isArray(value))
|
||||||
|
continue;
|
||||||
|
if (expectedType === 'array' && Array.isArray(value))
|
||||||
|
continue;
|
||||||
|
if (actualType === 'string') {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (expectedType === 'object' && trimmed.startsWith('{')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||||
|
coerced[key] = parsed;
|
||||||
|
coercedAny = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (expectedType === 'array' && trimmed.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
coerced[key] = parsed;
|
||||||
|
coercedAny = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (expectedType === 'number' || expectedType === 'integer') {
|
||||||
|
const num = Number(trimmed);
|
||||||
|
if (!isNaN(num) && trimmed !== '') {
|
||||||
|
coerced[key] = expectedType === 'integer' ? Math.trunc(num) : num;
|
||||||
|
coercedAny = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (expectedType === 'boolean') {
|
||||||
|
if (trimmed === 'true') {
|
||||||
|
coerced[key] = true;
|
||||||
|
coercedAny = true;
|
||||||
|
}
|
||||||
|
else if (trimmed === 'false') {
|
||||||
|
coerced[key] = false;
|
||||||
|
coercedAny = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expectedType === 'string' && (actualType === 'number' || actualType === 'boolean')) {
|
||||||
|
coerced[key] = String(value);
|
||||||
|
coercedAny = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (coercedAny) {
|
||||||
|
logger_1.logger.warn(`Coerced mistyped params for tool "${toolName}"`, {
|
||||||
|
original: Object.fromEntries(Object.entries(args).map(([k, v]) => [k, `${typeof v}: ${typeof v === 'string' ? v.substring(0, 80) : v}`])),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return coerced;
|
||||||
|
}
|
||||||
async executeTool(name, args) {
|
async executeTool(name, args) {
|
||||||
args = args || {};
|
args = args || {};
|
||||||
const disabledTools = this.getDisabledTools();
|
const disabledTools = this.getDisabledTools();
|
||||||
@@ -750,7 +922,11 @@ class N8NDocumentationMCPServer {
|
|||||||
case 'search_nodes':
|
case 'search_nodes':
|
||||||
this.validateToolParams(name, args, ['query']);
|
this.validateToolParams(name, args, ['query']);
|
||||||
const limit = args.limit !== undefined ? Number(args.limit) || 20 : 20;
|
const limit = args.limit !== undefined ? Number(args.limit) || 20 : 20;
|
||||||
return this.searchNodes(args.query, limit, { mode: args.mode, includeExamples: args.includeExamples });
|
return this.searchNodes(args.query, limit, {
|
||||||
|
mode: args.mode,
|
||||||
|
includeExamples: args.includeExamples,
|
||||||
|
source: args.source
|
||||||
|
});
|
||||||
case 'get_node':
|
case 'get_node':
|
||||||
this.validateToolParams(name, args, ['nodeType']);
|
this.validateToolParams(name, args, ['nodeType']);
|
||||||
if (args.mode === 'docs') {
|
if (args.mode === 'docs') {
|
||||||
@@ -1089,6 +1265,19 @@ class N8NDocumentationMCPServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
let sourceFilter = '';
|
||||||
|
const sourceValue = options?.source || 'all';
|
||||||
|
switch (sourceValue) {
|
||||||
|
case 'core':
|
||||||
|
sourceFilter = 'AND n.is_community = 0';
|
||||||
|
break;
|
||||||
|
case 'community':
|
||||||
|
sourceFilter = 'AND n.is_community = 1';
|
||||||
|
break;
|
||||||
|
case 'verified':
|
||||||
|
sourceFilter = 'AND n.is_community = 1 AND n.is_verified = 1';
|
||||||
|
break;
|
||||||
|
}
|
||||||
const nodes = this.db.prepare(`
|
const nodes = this.db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
n.*,
|
n.*,
|
||||||
@@ -1096,6 +1285,7 @@ class N8NDocumentationMCPServer {
|
|||||||
FROM nodes n
|
FROM nodes n
|
||||||
JOIN nodes_fts ON n.rowid = nodes_fts.rowid
|
JOIN nodes_fts ON n.rowid = nodes_fts.rowid
|
||||||
WHERE nodes_fts MATCH ?
|
WHERE nodes_fts MATCH ?
|
||||||
|
${sourceFilter}
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
WHEN LOWER(n.display_name) = LOWER(?) THEN 0
|
WHEN LOWER(n.display_name) = LOWER(?) THEN 0
|
||||||
@@ -1128,15 +1318,28 @@ class N8NDocumentationMCPServer {
|
|||||||
}
|
}
|
||||||
const result = {
|
const result = {
|
||||||
query,
|
query,
|
||||||
results: scoredNodes.map(node => ({
|
results: scoredNodes.map(node => {
|
||||||
nodeType: node.node_type,
|
const nodeResult = {
|
||||||
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
nodeType: node.node_type,
|
||||||
displayName: node.display_name,
|
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
||||||
description: node.description,
|
displayName: node.display_name,
|
||||||
category: node.category,
|
description: node.description,
|
||||||
package: node.package_name,
|
category: node.category,
|
||||||
relevance: this.calculateRelevance(node, cleanedQuery)
|
package: node.package_name,
|
||||||
})),
|
relevance: this.calculateRelevance(node, cleanedQuery)
|
||||||
|
};
|
||||||
|
if (node.is_community === 1) {
|
||||||
|
nodeResult.isCommunity = true;
|
||||||
|
nodeResult.isVerified = node.is_verified === 1;
|
||||||
|
if (node.author_name) {
|
||||||
|
nodeResult.authorName = node.author_name;
|
||||||
|
}
|
||||||
|
if (node.npm_downloads) {
|
||||||
|
nodeResult.npmDownloads = node.npm_downloads;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodeResult;
|
||||||
|
}),
|
||||||
totalCount: scoredNodes.length
|
totalCount: scoredNodes.length
|
||||||
};
|
};
|
||||||
if (mode !== 'OR') {
|
if (mode !== 'OR') {
|
||||||
@@ -1298,24 +1501,51 @@ class N8NDocumentationMCPServer {
|
|||||||
async searchNodesLIKE(query, limit, options) {
|
async searchNodesLIKE(query, limit, options) {
|
||||||
if (!this.db)
|
if (!this.db)
|
||||||
throw new Error('Database not initialized');
|
throw new Error('Database not initialized');
|
||||||
|
let sourceFilter = '';
|
||||||
|
const sourceValue = options?.source || 'all';
|
||||||
|
switch (sourceValue) {
|
||||||
|
case 'core':
|
||||||
|
sourceFilter = 'AND is_community = 0';
|
||||||
|
break;
|
||||||
|
case 'community':
|
||||||
|
sourceFilter = 'AND is_community = 1';
|
||||||
|
break;
|
||||||
|
case 'verified':
|
||||||
|
sourceFilter = 'AND is_community = 1 AND is_verified = 1';
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (query.startsWith('"') && query.endsWith('"')) {
|
if (query.startsWith('"') && query.endsWith('"')) {
|
||||||
const exactPhrase = query.slice(1, -1);
|
const exactPhrase = query.slice(1, -1);
|
||||||
const nodes = this.db.prepare(`
|
const nodes = this.db.prepare(`
|
||||||
SELECT * FROM nodes
|
SELECT * FROM nodes
|
||||||
WHERE node_type LIKE ? OR display_name LIKE ? OR description LIKE ?
|
WHERE (node_type LIKE ? OR display_name LIKE ? OR description LIKE ?)
|
||||||
|
${sourceFilter}
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(`%${exactPhrase}%`, `%${exactPhrase}%`, `%${exactPhrase}%`, limit * 3);
|
`).all(`%${exactPhrase}%`, `%${exactPhrase}%`, `%${exactPhrase}%`, limit * 3);
|
||||||
const rankedNodes = this.rankSearchResults(nodes, exactPhrase, limit);
|
const rankedNodes = this.rankSearchResults(nodes, exactPhrase, limit);
|
||||||
const result = {
|
const result = {
|
||||||
query,
|
query,
|
||||||
results: rankedNodes.map(node => ({
|
results: rankedNodes.map(node => {
|
||||||
nodeType: node.node_type,
|
const nodeResult = {
|
||||||
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
nodeType: node.node_type,
|
||||||
displayName: node.display_name,
|
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
||||||
description: node.description,
|
displayName: node.display_name,
|
||||||
category: node.category,
|
description: node.description,
|
||||||
package: node.package_name
|
category: node.category,
|
||||||
})),
|
package: node.package_name
|
||||||
|
};
|
||||||
|
if (node.is_community === 1) {
|
||||||
|
nodeResult.isCommunity = true;
|
||||||
|
nodeResult.isVerified = node.is_verified === 1;
|
||||||
|
if (node.author_name) {
|
||||||
|
nodeResult.authorName = node.author_name;
|
||||||
|
}
|
||||||
|
if (node.npm_downloads) {
|
||||||
|
nodeResult.npmDownloads = node.npm_downloads;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodeResult;
|
||||||
|
}),
|
||||||
totalCount: rankedNodes.length
|
totalCount: rankedNodes.length
|
||||||
};
|
};
|
||||||
if (options?.includeExamples) {
|
if (options?.includeExamples) {
|
||||||
@@ -1354,21 +1584,35 @@ class N8NDocumentationMCPServer {
|
|||||||
const params = words.flatMap(w => [`%${w}%`, `%${w}%`, `%${w}%`]);
|
const params = words.flatMap(w => [`%${w}%`, `%${w}%`, `%${w}%`]);
|
||||||
params.push(limit * 3);
|
params.push(limit * 3);
|
||||||
const nodes = this.db.prepare(`
|
const nodes = this.db.prepare(`
|
||||||
SELECT DISTINCT * FROM nodes
|
SELECT DISTINCT * FROM nodes
|
||||||
WHERE ${conditions}
|
WHERE (${conditions})
|
||||||
|
${sourceFilter}
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(...params);
|
`).all(...params);
|
||||||
const rankedNodes = this.rankSearchResults(nodes, query, limit);
|
const rankedNodes = this.rankSearchResults(nodes, query, limit);
|
||||||
const result = {
|
const result = {
|
||||||
query,
|
query,
|
||||||
results: rankedNodes.map(node => ({
|
results: rankedNodes.map(node => {
|
||||||
nodeType: node.node_type,
|
const nodeResult = {
|
||||||
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
nodeType: node.node_type,
|
||||||
displayName: node.display_name,
|
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
||||||
description: node.description,
|
displayName: node.display_name,
|
||||||
category: node.category,
|
description: node.description,
|
||||||
package: node.package_name
|
category: node.category,
|
||||||
})),
|
package: node.package_name
|
||||||
|
};
|
||||||
|
if (node.is_community === 1) {
|
||||||
|
nodeResult.isCommunity = true;
|
||||||
|
nodeResult.isVerified = node.is_verified === 1;
|
||||||
|
if (node.author_name) {
|
||||||
|
nodeResult.authorName = node.author_name;
|
||||||
|
}
|
||||||
|
if (node.npm_downloads) {
|
||||||
|
nodeResult.npmDownloads = node.npm_downloads;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodeResult;
|
||||||
|
}),
|
||||||
totalCount: rankedNodes.length
|
totalCount: rankedNodes.length
|
||||||
};
|
};
|
||||||
if (options?.includeExamples) {
|
if (options?.includeExamples) {
|
||||||
@@ -1545,14 +1789,16 @@ class N8NDocumentationMCPServer {
|
|||||||
throw new Error('Database not initialized');
|
throw new Error('Database not initialized');
|
||||||
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
||||||
let node = this.db.prepare(`
|
let node = this.db.prepare(`
|
||||||
SELECT node_type, display_name, documentation, description
|
SELECT node_type, display_name, documentation, description,
|
||||||
FROM nodes
|
ai_documentation_summary, ai_summary_generated_at
|
||||||
|
FROM nodes
|
||||||
WHERE node_type = ?
|
WHERE node_type = ?
|
||||||
`).get(normalizedType);
|
`).get(normalizedType);
|
||||||
if (!node && normalizedType !== nodeType) {
|
if (!node && normalizedType !== nodeType) {
|
||||||
node = this.db.prepare(`
|
node = this.db.prepare(`
|
||||||
SELECT node_type, display_name, documentation, description
|
SELECT node_type, display_name, documentation, description,
|
||||||
FROM nodes
|
ai_documentation_summary, ai_summary_generated_at
|
||||||
|
FROM nodes
|
||||||
WHERE node_type = ?
|
WHERE node_type = ?
|
||||||
`).get(nodeType);
|
`).get(nodeType);
|
||||||
}
|
}
|
||||||
@@ -1560,8 +1806,9 @@ class N8NDocumentationMCPServer {
|
|||||||
const alternatives = (0, node_utils_1.getNodeTypeAlternatives)(normalizedType);
|
const alternatives = (0, node_utils_1.getNodeTypeAlternatives)(normalizedType);
|
||||||
for (const alt of alternatives) {
|
for (const alt of alternatives) {
|
||||||
node = this.db.prepare(`
|
node = this.db.prepare(`
|
||||||
SELECT node_type, display_name, documentation, description
|
SELECT node_type, display_name, documentation, description,
|
||||||
FROM nodes
|
ai_documentation_summary, ai_summary_generated_at
|
||||||
|
FROM nodes
|
||||||
WHERE node_type = ?
|
WHERE node_type = ?
|
||||||
`).get(alt);
|
`).get(alt);
|
||||||
if (node)
|
if (node)
|
||||||
@@ -1571,6 +1818,9 @@ class N8NDocumentationMCPServer {
|
|||||||
if (!node) {
|
if (!node) {
|
||||||
throw new Error(`Node ${nodeType} not found`);
|
throw new Error(`Node ${nodeType} not found`);
|
||||||
}
|
}
|
||||||
|
const aiDocSummary = node.ai_documentation_summary
|
||||||
|
? this.safeJsonParse(node.ai_documentation_summary, null)
|
||||||
|
: null;
|
||||||
if (!node.documentation) {
|
if (!node.documentation) {
|
||||||
const essentials = await this.getNodeEssentials(nodeType);
|
const essentials = await this.getNodeEssentials(nodeType);
|
||||||
return {
|
return {
|
||||||
@@ -1590,7 +1840,9 @@ ${essentials?.commonProperties?.length > 0 ?
|
|||||||
## Note
|
## Note
|
||||||
Full documentation is being prepared. For now, use get_node_essentials for configuration help.
|
Full documentation is being prepared. For now, use get_node_essentials for configuration help.
|
||||||
`,
|
`,
|
||||||
hasDocumentation: false
|
hasDocumentation: false,
|
||||||
|
aiDocumentationSummary: aiDocSummary,
|
||||||
|
aiSummaryGeneratedAt: node.ai_summary_generated_at || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -1598,8 +1850,18 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
|||||||
displayName: node.display_name || 'Unknown Node',
|
displayName: node.display_name || 'Unknown Node',
|
||||||
documentation: node.documentation,
|
documentation: node.documentation,
|
||||||
hasDocumentation: true,
|
hasDocumentation: true,
|
||||||
|
aiDocumentationSummary: aiDocSummary,
|
||||||
|
aiSummaryGeneratedAt: node.ai_summary_generated_at || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
safeJsonParse(json, defaultValue = null) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(json);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
async getDatabaseStatistics() {
|
async getDatabaseStatistics() {
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
if (!this.db)
|
if (!this.db)
|
||||||
@@ -2799,7 +3061,26 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
|||||||
process.stdin.resume();
|
process.stdin.resume();
|
||||||
}
|
}
|
||||||
async shutdown() {
|
async shutdown() {
|
||||||
|
if (this.isShutdown) {
|
||||||
|
logger_1.logger.debug('Shutdown already called, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isShutdown = true;
|
||||||
logger_1.logger.info('Shutting down MCP server...');
|
logger_1.logger.info('Shutting down MCP server...');
|
||||||
|
try {
|
||||||
|
await this.initialized;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
logger_1.logger.debug('Initialization had failed, proceeding with cleanup', {
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.server.close();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
logger_1.logger.error('Error closing MCP server:', error);
|
||||||
|
}
|
||||||
if (this.cache) {
|
if (this.cache) {
|
||||||
try {
|
try {
|
||||||
this.cache.destroy();
|
this.cache.destroy();
|
||||||
@@ -2809,15 +3090,29 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
|||||||
logger_1.logger.error('Error cleaning up cache:', error);
|
logger_1.logger.error('Error cleaning up cache:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.db) {
|
if (this.useSharedDatabase && this.sharedDbState) {
|
||||||
try {
|
try {
|
||||||
await this.db.close();
|
(0, shared_database_1.releaseSharedDatabase)(this.sharedDbState);
|
||||||
|
logger_1.logger.info('Released shared database reference');
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
logger_1.logger.error('Error releasing shared database:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (this.db) {
|
||||||
|
try {
|
||||||
|
this.db.close();
|
||||||
logger_1.logger.info('Database connection closed');
|
logger_1.logger.info('Database connection closed');
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
logger_1.logger.error('Error closing database:', error);
|
logger_1.logger.error('Error closing database:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.db = null;
|
||||||
|
this.repository = null;
|
||||||
|
this.templateService = null;
|
||||||
|
this.earlyLogger = null;
|
||||||
|
this.sharedDbState = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exports.N8NDocumentationMCPServer = N8NDocumentationMCPServer;
|
exports.N8NDocumentationMCPServer = N8NDocumentationMCPServer;
|
||||||
|
|||||||
2
dist/mcp/server.js.map
vendored
2
dist/mcp/server.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"search-nodes.d.ts","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/discovery/search-nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,eAAO,MAAM,cAAc,EAAE,iBAmD5B,CAAC"}
|
{"version":3,"file":"search-nodes.d.ts","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/discovery/search-nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,eAAO,MAAM,cAAc,EAAE,iBAiE5B,CAAC"}
|
||||||
34
dist/mcp/tool-docs/discovery/search-nodes.js
vendored
34
dist/mcp/tool-docs/discovery/search-nodes.js
vendored
@@ -5,50 +5,64 @@ exports.searchNodesDoc = {
|
|||||||
name: 'search_nodes',
|
name: 'search_nodes',
|
||||||
category: 'discovery',
|
category: 'discovery',
|
||||||
essentials: {
|
essentials: {
|
||||||
description: 'Text search across node names and descriptions. Returns most relevant nodes first, with frequently-used nodes (HTTP Request, Webhook, Set, Code, Slack) prioritized in results. Searches all 500+ nodes in the database.',
|
description: 'Text search across node names and descriptions. Returns most relevant nodes first, with frequently-used nodes (HTTP Request, Webhook, Set, Code, Slack) prioritized in results. Searches all 800+ nodes including 300+ verified community nodes.',
|
||||||
keyParameters: ['query', 'mode', 'limit'],
|
keyParameters: ['query', 'mode', 'limit', 'source', 'includeExamples'],
|
||||||
example: 'search_nodes({query: "webhook"})',
|
example: 'search_nodes({query: "webhook"})',
|
||||||
performance: '<20ms even for complex queries',
|
performance: '<20ms even for complex queries',
|
||||||
tips: [
|
tips: [
|
||||||
'OR mode (default): Matches any search word',
|
'OR mode (default): Matches any search word',
|
||||||
'AND mode: Requires all words present',
|
'AND mode: Requires all words present',
|
||||||
'FUZZY mode: Handles typos and spelling errors',
|
'FUZZY mode: Handles typos and spelling errors',
|
||||||
'Use quotes for exact phrases: "google sheets"'
|
'Use quotes for exact phrases: "google sheets"',
|
||||||
|
'Use source="community" to search only community nodes',
|
||||||
|
'Use source="verified" for verified community nodes only'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
full: {
|
full: {
|
||||||
description: 'Full-text search engine for n8n nodes using SQLite FTS5. Searches across node names, descriptions, and aliases. Results are ranked by relevance with commonly-used nodes given priority. Common nodes include: HTTP Request, Webhook, Set, Code, IF, Switch, Merge, SplitInBatches, Slack, Google Sheets.',
|
description: 'Full-text search engine for n8n nodes using SQLite FTS5. Searches across node names, descriptions, and aliases. Results are ranked by relevance with commonly-used nodes given priority. Includes 500+ core nodes and 300+ community nodes. Common core nodes include: HTTP Request, Webhook, Set, Code, IF, Switch, Merge, SplitInBatches, Slack, Google Sheets. Community nodes include verified integrations like BrightData, ScrapingBee, CraftMyPDF, and more.',
|
||||||
parameters: {
|
parameters: {
|
||||||
query: { type: 'string', description: 'Search keywords. Use quotes for exact phrases like "google sheets"', required: true },
|
query: { type: 'string', description: 'Search keywords. Use quotes for exact phrases like "google sheets"', required: true },
|
||||||
limit: { type: 'number', description: 'Maximum results to return. Default: 20, Max: 100', required: false },
|
limit: { type: 'number', description: 'Maximum results to return. Default: 20, Max: 100', required: false },
|
||||||
mode: { type: 'string', description: 'Search mode: "OR" (any word matches, default), "AND" (all words required), "FUZZY" (typo-tolerant)', required: false }
|
mode: { type: 'string', description: 'Search mode: "OR" (any word matches, default), "AND" (all words required), "FUZZY" (typo-tolerant)', required: false },
|
||||||
|
source: { type: 'string', description: 'Filter by node source: "all" (default, everything), "core" (n8n base nodes only), "community" (community nodes only), "verified" (verified community nodes only)', required: false },
|
||||||
|
includeExamples: { type: 'boolean', description: 'Include top 2 real-world configuration examples from popular templates for each node. Default: false. Adds ~200-400 tokens per node.', required: false }
|
||||||
},
|
},
|
||||||
returns: 'Array of node objects sorted by relevance score. Each object contains: nodeType, displayName, description, category, relevance score. Common nodes appear first when relevance is similar.',
|
returns: 'Array of node objects sorted by relevance score. Each object contains: nodeType, displayName, description, category, relevance score. For community nodes, also includes: isCommunity (boolean), isVerified (boolean), authorName (string), npmDownloads (number). Common nodes appear first when relevance is similar.',
|
||||||
examples: [
|
examples: [
|
||||||
'search_nodes({query: "webhook"}) - Returns Webhook node as top result',
|
'search_nodes({query: "webhook"}) - Returns Webhook node as top result',
|
||||||
'search_nodes({query: "database"}) - Returns MySQL, Postgres, MongoDB, Redis, etc.',
|
'search_nodes({query: "database"}) - Returns MySQL, Postgres, MongoDB, Redis, etc.',
|
||||||
'search_nodes({query: "google sheets", mode: "AND"}) - Requires both words',
|
'search_nodes({query: "google sheets", mode: "AND"}) - Requires both words',
|
||||||
'search_nodes({query: "slak", mode: "FUZZY"}) - Finds Slack despite typo',
|
'search_nodes({query: "slak", mode: "FUZZY"}) - Finds Slack despite typo',
|
||||||
'search_nodes({query: "http api"}) - Finds HTTP Request, GraphQL, REST nodes',
|
'search_nodes({query: "http api"}) - Finds HTTP Request, GraphQL, REST nodes',
|
||||||
'search_nodes({query: "transform data"}) - Finds Set, Code, Function, Item Lists nodes'
|
'search_nodes({query: "transform data"}) - Finds Set, Code, Function, Item Lists nodes',
|
||||||
|
'search_nodes({query: "scraping", source: "community"}) - Find community scraping nodes',
|
||||||
|
'search_nodes({query: "pdf", source: "verified"}) - Find verified community PDF nodes',
|
||||||
|
'search_nodes({query: "brightdata"}) - Find BrightData community node',
|
||||||
|
'search_nodes({query: "slack", includeExamples: true}) - Get Slack with template examples'
|
||||||
],
|
],
|
||||||
useCases: [
|
useCases: [
|
||||||
'Finding nodes when you know partial names',
|
'Finding nodes when you know partial names',
|
||||||
'Discovering nodes by functionality (e.g., "email", "database", "transform")',
|
'Discovering nodes by functionality (e.g., "email", "database", "transform")',
|
||||||
'Handling user typos in node names',
|
'Handling user typos in node names',
|
||||||
'Finding all nodes related to a service (e.g., "google", "aws", "microsoft")'
|
'Finding all nodes related to a service (e.g., "google", "aws", "microsoft")',
|
||||||
|
'Discovering community integrations for specific services',
|
||||||
|
'Finding verified community nodes for enhanced trust'
|
||||||
],
|
],
|
||||||
performance: '<20ms for simple queries, <50ms for complex FUZZY searches. Uses FTS5 index for speed',
|
performance: '<20ms for simple queries, <50ms for complex FUZZY searches. Uses FTS5 index for speed',
|
||||||
bestPractices: [
|
bestPractices: [
|
||||||
'Start with single keywords for broadest results',
|
'Start with single keywords for broadest results',
|
||||||
'Use FUZZY mode when users might misspell node names',
|
'Use FUZZY mode when users might misspell node names',
|
||||||
'AND mode works best for 2-3 word searches',
|
'AND mode works best for 2-3 word searches',
|
||||||
'Combine with get_node after finding the right node'
|
'Combine with get_node after finding the right node',
|
||||||
|
'Use source="verified" when recommending community nodes for production',
|
||||||
|
'Check isVerified flag to ensure community node quality'
|
||||||
],
|
],
|
||||||
pitfalls: [
|
pitfalls: [
|
||||||
'AND mode searches all fields (name, description) not just node names',
|
'AND mode searches all fields (name, description) not just node names',
|
||||||
'FUZZY mode with very short queries (1-2 chars) may return unexpected results',
|
'FUZZY mode with very short queries (1-2 chars) may return unexpected results',
|
||||||
'Exact matches in quotes are case-sensitive'
|
'Exact matches in quotes are case-sensitive',
|
||||||
|
'Community nodes require npm installation (n8n npm install <package-name>)',
|
||||||
|
'Unverified community nodes (isVerified: false) may have limited support'
|
||||||
],
|
],
|
||||||
relatedTools: ['get_node to configure found nodes', 'search_templates to find workflow examples', 'validate_node to check configurations']
|
relatedTools: ['get_node to configure found nodes', 'search_templates to find workflow examples', 'validate_node to check configurations']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"search-nodes.js","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/discovery/search-nodes.ts"],"names":[],"mappings":";;;AAEa,QAAA,cAAc,GAAsB;IAC/C,IAAI,EAAE,cAAc;IACpB,QAAQ,EAAE,WAAW;IACrB,UAAU,EAAE;QACV,WAAW,EAAE,0NAA0N;QACvO,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC;QACzC,OAAO,EAAE,kCAAkC;QAC3C,WAAW,EAAE,gCAAgC;QAC7C,IAAI,EAAE;YACJ,4CAA4C;YAC5C,sCAAsC;YACtC,+CAA+C;YAC/C,+CAA+C;SAChD;KACF;IACD,IAAI,EAAE;QACJ,WAAW,EAAE,2SAA2S;QACxT,UAAU,EAAE;YACV,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,oEAAoE,EAAE,QAAQ,EAAE,IAAI,EAAE;YAC5H,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,kDAAkD,EAAE,QAAQ,EAAE,KAAK,EAAE;YAC3G,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,oGAAoG,EAAE,QAAQ,EAAE,KAAK,EAAE;SAC7J;QACD,OAAO,EAAE,4LAA4L;QACrM,QAAQ,EAAE;YACR,uEAAuE;YACvE,mFAAmF;YACnF,2EAA2E;YAC3E,yEAAyE;YACzE,6EAA6E;YAC7E,uFAAuF;SACxF;QACD,QAAQ,EAAE;YACR,2CAA2C;YAC3C,6EAA6E;YAC7E,mCAAmC;YACnC,6EAA6E;SAC9E;QACD,WAAW,EAAE,uFAAuF;QACpG,aAAa,EAAE;YACb,iDAAiD;YACjD,qDAAqD;YACrD,2CAA2C;YAC3C,oDAAoD;SACrD;QACD,QAAQ,EAAE;YACR,sEAAsE;YACtE,8EAA8E;YAC9E,4CAA4C;SAC7C;QACD,YAAY,EAAE,CAAC,mCAAmC,EAAE,4CAA4C,EAAE,uCAAuC,CAAC;KAC3I;CACF,CAAC"}
|
{"version":3,"file":"search-nodes.js","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/discovery/search-nodes.ts"],"names":[],"mappings":";;;AAEa,QAAA,cAAc,GAAsB;IAC/C,IAAI,EAAE,cAAc;IACpB,QAAQ,EAAE,WAAW;IACrB,UAAU,EAAE;QACV,WAAW,EAAE,kPAAkP;QAC/P,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,CAAC;QACtE,OAAO,EAAE,kCAAkC;QAC3C,WAAW,EAAE,gCAAgC;QAC7C,IAAI,EAAE;YACJ,4CAA4C;YAC5C,sCAAsC;YACtC,+CAA+C;YAC/C,+CAA+C;YAC/C,uDAAuD;YACvD,yDAAyD;SAC1D;KACF;IACD,IAAI,EAAE;QACJ,WAAW,EAAE,qcAAqc;QACld,UAAU,EAAE;YACV,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,oEAAoE,EAAE,QAAQ,EAAE,IAAI,EAAE;YAC5H,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,kDAAkD,EAAE,QAAQ,EAAE,KAAK,EAAE;YAC3G,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,oGAAoG,EAAE,QAAQ,EAAE,KAAK,EAAE;YAC5J,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,kKAAkK,EAAE,QAAQ,EAAE,KAAK,EAAE;YAC5N,eAAe,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sIAAsI,EAAE,QAAQ,EAAE,KAAK,EAAE;SAC3M;QACD,OAAO,EAAE,yTAAyT;QAClU,QAAQ,EAAE;YACR,uEAAuE;YACvE,mFAAmF;YACnF,2EAA2E;YAC3E,yEAAyE;YACzE,6EAA6E;YAC7E,uFAAuF;YACvF,wFAAwF;YACxF,sFAAsF;YACtF,sEAAsE;YACtE,0FAA0F;SAC3F;QACD,QAAQ,EAAE;YACR,2CAA2C;YAC3C,6EAA6E;YAC7E,mCAAmC;YACnC,6EAA6E;YAC7E,0DAA0D;YAC1D,qDAAqD;SACtD;QACD,WAAW,EAAE,uFAAuF;QACpG,aAAa,EAAE;YACb,iDAAiD;YACjD,qDAAqD;YACrD,2CAA2C;YAC3C,oDAAoD;YACpD,wEAAwE;YACxE,wDAAwD;SACzD;QACD,QAAQ,EAAE;YACR,sEAAsE;YACtE,8EAA8E;YAC9E,4CAA4C;YAC5C,2EAA2E;YAC3E,yEAAyE;SAC1E;QACD,YAAY,EAAE,CAAC,mCAAmC,EAAE,4CAA4C,EAAE,uCAAuC,CAAC;KAC3I;CACF,CAAC"}
|
||||||
4
dist/mcp/tools-n8n-manager.js
vendored
4
dist/mcp/tools-n8n-manager.js
vendored
@@ -278,7 +278,7 @@ exports.n8nManagementTools = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'n8n_autofix_workflow',
|
name: 'n8n_autofix_workflow',
|
||||||
description: `Automatically fix common workflow validation errors. Preview fixes or apply them. Fixes expression format, typeVersion, error output config, webhook paths.`,
|
description: `Automatically fix common workflow validation errors. Preview fixes or apply them. Fixes expression format, typeVersion, error output config, webhook paths, connection structure issues (numeric keys, invalid types, ID-to-name, duplicates, out-of-bounds indices).`,
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -295,7 +295,7 @@ exports.n8nManagementTools = [
|
|||||||
description: 'Types of fixes to apply (default: all)',
|
description: 'Types of fixes to apply (default: all)',
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['expression-format', 'typeversion-correction', 'error-output-config', 'node-type-correction', 'webhook-missing-path', 'typeversion-upgrade', 'version-migration']
|
enum: ['expression-format', 'typeversion-correction', 'error-output-config', 'node-type-correction', 'webhook-missing-path', 'typeversion-upgrade', 'version-migration', 'tool-variant-correction', 'connection-numeric-keys', 'connection-invalid-type', 'connection-id-to-name', 'connection-duplicate-removal', 'connection-input-index']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confidenceThreshold: {
|
confidenceThreshold: {
|
||||||
|
|||||||
2
dist/mcp/tools-n8n-manager.js.map
vendored
2
dist/mcp/tools-n8n-manager.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/mcp/tools.d.ts.map
vendored
2
dist/mcp/tools.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAQ1C,eAAO,MAAM,0BAA0B,EAAE,cAAc,EAkatD,CAAC"}
|
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAQ1C,eAAO,MAAM,0BAA0B,EAAE,cAAc,EAwatD,CAAC"}
|
||||||
6
dist/mcp/tools.js
vendored
6
dist/mcp/tools.js
vendored
@@ -52,6 +52,12 @@ exports.n8nDocumentationToolsFinal = [
|
|||||||
description: 'Include top 2 real-world configuration examples from popular templates (default: false)',
|
description: 'Include top 2 real-world configuration examples from popular templates (default: false)',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
source: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['all', 'core', 'community', 'verified'],
|
||||||
|
description: 'Filter by node source: all=everything (default), core=n8n base nodes, community=community nodes, verified=verified community nodes only',
|
||||||
|
default: 'all',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ['query'],
|
required: ['query'],
|
||||||
},
|
},
|
||||||
|
|||||||
2
dist/mcp/tools.js.map
vendored
2
dist/mcp/tools.js.map
vendored
File diff suppressed because one or more lines are too long
24
dist/services/n8n-validation.d.ts
vendored
24
dist/services/n8n-validation.d.ts
vendored
@@ -26,10 +26,10 @@ export declare const workflowNodeSchema: z.ZodObject<{
|
|||||||
parameters: Record<string, unknown>;
|
parameters: Record<string, unknown>;
|
||||||
credentials?: Record<string, unknown> | undefined;
|
credentials?: Record<string, unknown> | undefined;
|
||||||
retryOnFail?: boolean | undefined;
|
retryOnFail?: boolean | undefined;
|
||||||
|
continueOnFail?: boolean | undefined;
|
||||||
maxTries?: number | undefined;
|
maxTries?: number | undefined;
|
||||||
waitBetweenTries?: number | undefined;
|
waitBetweenTries?: number | undefined;
|
||||||
alwaysOutputData?: boolean | undefined;
|
alwaysOutputData?: boolean | undefined;
|
||||||
continueOnFail?: boolean | undefined;
|
|
||||||
executeOnce?: boolean | undefined;
|
executeOnce?: boolean | undefined;
|
||||||
disabled?: boolean | undefined;
|
disabled?: boolean | undefined;
|
||||||
notes?: string | undefined;
|
notes?: string | undefined;
|
||||||
@@ -43,10 +43,10 @@ export declare const workflowNodeSchema: z.ZodObject<{
|
|||||||
parameters: Record<string, unknown>;
|
parameters: Record<string, unknown>;
|
||||||
credentials?: Record<string, unknown> | undefined;
|
credentials?: Record<string, unknown> | undefined;
|
||||||
retryOnFail?: boolean | undefined;
|
retryOnFail?: boolean | undefined;
|
||||||
|
continueOnFail?: boolean | undefined;
|
||||||
maxTries?: number | undefined;
|
maxTries?: number | undefined;
|
||||||
waitBetweenTries?: number | undefined;
|
waitBetweenTries?: number | undefined;
|
||||||
alwaysOutputData?: boolean | undefined;
|
alwaysOutputData?: boolean | undefined;
|
||||||
continueOnFail?: boolean | undefined;
|
|
||||||
executeOnce?: boolean | undefined;
|
executeOnce?: boolean | undefined;
|
||||||
disabled?: boolean | undefined;
|
disabled?: boolean | undefined;
|
||||||
notes?: string | undefined;
|
notes?: string | undefined;
|
||||||
@@ -155,6 +155,11 @@ export declare const workflowConnectionSchema: z.ZodRecord<z.ZodString, z.ZodObj
|
|||||||
node: string;
|
node: string;
|
||||||
index: number;
|
index: number;
|
||||||
}[][] | undefined;
|
}[][] | undefined;
|
||||||
|
ai_tool?: {
|
||||||
|
type: string;
|
||||||
|
node: string;
|
||||||
|
index: number;
|
||||||
|
}[][] | undefined;
|
||||||
ai_languageModel?: {
|
ai_languageModel?: {
|
||||||
type: string;
|
type: string;
|
||||||
node: string;
|
node: string;
|
||||||
@@ -165,11 +170,6 @@ export declare const workflowConnectionSchema: z.ZodRecord<z.ZodString, z.ZodObj
|
|||||||
node: string;
|
node: string;
|
||||||
index: number;
|
index: number;
|
||||||
}[][] | undefined;
|
}[][] | undefined;
|
||||||
ai_tool?: {
|
|
||||||
type: string;
|
|
||||||
node: string;
|
|
||||||
index: number;
|
|
||||||
}[][] | undefined;
|
|
||||||
ai_embedding?: {
|
ai_embedding?: {
|
||||||
type: string;
|
type: string;
|
||||||
node: string;
|
node: string;
|
||||||
@@ -191,6 +191,11 @@ export declare const workflowConnectionSchema: z.ZodRecord<z.ZodString, z.ZodObj
|
|||||||
node: string;
|
node: string;
|
||||||
index: number;
|
index: number;
|
||||||
}[][] | undefined;
|
}[][] | undefined;
|
||||||
|
ai_tool?: {
|
||||||
|
type: string;
|
||||||
|
node: string;
|
||||||
|
index: number;
|
||||||
|
}[][] | undefined;
|
||||||
ai_languageModel?: {
|
ai_languageModel?: {
|
||||||
type: string;
|
type: string;
|
||||||
node: string;
|
node: string;
|
||||||
@@ -201,11 +206,6 @@ export declare const workflowConnectionSchema: z.ZodRecord<z.ZodString, z.ZodObj
|
|||||||
node: string;
|
node: string;
|
||||||
index: number;
|
index: number;
|
||||||
}[][] | undefined;
|
}[][] | undefined;
|
||||||
ai_tool?: {
|
|
||||||
type: string;
|
|
||||||
node: string;
|
|
||||||
index: number;
|
|
||||||
}[][] | undefined;
|
|
||||||
ai_embedding?: {
|
ai_embedding?: {
|
||||||
type: string;
|
type: string;
|
||||||
node: string;
|
node: string;
|
||||||
|
|||||||
9
dist/services/workflow-auto-fixer.d.ts
vendored
9
dist/services/workflow-auto-fixer.d.ts
vendored
@@ -5,7 +5,8 @@ import { WorkflowDiffOperation } from '../types/workflow-diff';
|
|||||||
import { Workflow } from '../types/n8n-api';
|
import { Workflow } from '../types/n8n-api';
|
||||||
import { PostUpdateGuidance } from './post-update-validator';
|
import { PostUpdateGuidance } from './post-update-validator';
|
||||||
export type FixConfidenceLevel = 'high' | 'medium' | 'low';
|
export type FixConfidenceLevel = 'high' | 'medium' | 'low';
|
||||||
export type FixType = 'expression-format' | 'typeversion-correction' | 'error-output-config' | 'node-type-correction' | 'webhook-missing-path' | 'typeversion-upgrade' | 'version-migration' | 'tool-variant-correction';
|
export type FixType = 'expression-format' | 'typeversion-correction' | 'error-output-config' | 'node-type-correction' | 'webhook-missing-path' | 'typeversion-upgrade' | 'version-migration' | 'tool-variant-correction' | 'connection-numeric-keys' | 'connection-invalid-type' | 'connection-id-to-name' | 'connection-duplicate-removal' | 'connection-input-index';
|
||||||
|
export declare const CONNECTION_FIX_TYPES: FixType[];
|
||||||
export interface AutoFixConfig {
|
export interface AutoFixConfig {
|
||||||
applyFixes: boolean;
|
applyFixes: boolean;
|
||||||
fixTypes?: FixType[];
|
fixTypes?: FixType[];
|
||||||
@@ -68,6 +69,12 @@ export declare class WorkflowAutoFixer {
|
|||||||
private filterOperationsByFixes;
|
private filterOperationsByFixes;
|
||||||
private calculateStats;
|
private calculateStats;
|
||||||
private generateSummary;
|
private generateSummary;
|
||||||
|
private processConnectionFixes;
|
||||||
|
private fixNumericKeys;
|
||||||
|
private fixIdToName;
|
||||||
|
private fixInvalidTypes;
|
||||||
|
private fixInputIndices;
|
||||||
|
private fixDuplicateConnections;
|
||||||
private processVersionUpgradeFixes;
|
private processVersionUpgradeFixes;
|
||||||
private processVersionMigrationFixes;
|
private processVersionMigrationFixes;
|
||||||
}
|
}
|
||||||
|
|||||||
2
dist/services/workflow-auto-fixer.d.ts.map
vendored
2
dist/services/workflow-auto-fixer.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"workflow-auto-fixer.d.ts","sourceRoot":"","sources":["../../src/services/workflow-auto-fixer.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"workflow-auto-fixer.d.ts","sourceRoot":"","sources":["../../src/services/workflow-auto-fixer.ts"],"names":[],"mappings":"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"}
|
||||||
310
dist/services/workflow-auto-fixer.js
vendored
310
dist/services/workflow-auto-fixer.js
vendored
@@ -3,9 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.WorkflowAutoFixer = void 0;
|
exports.WorkflowAutoFixer = exports.CONNECTION_FIX_TYPES = void 0;
|
||||||
exports.isNodeFormatIssue = isNodeFormatIssue;
|
exports.isNodeFormatIssue = isNodeFormatIssue;
|
||||||
const crypto_1 = __importDefault(require("crypto"));
|
const crypto_1 = __importDefault(require("crypto"));
|
||||||
|
const workflow_validator_1 = require("./workflow-validator");
|
||||||
const node_similarity_service_1 = require("./node-similarity-service");
|
const node_similarity_service_1 = require("./node-similarity-service");
|
||||||
const logger_1 = require("../utils/logger");
|
const logger_1 = require("../utils/logger");
|
||||||
const node_version_service_1 = require("./node-version-service");
|
const node_version_service_1 = require("./node-version-service");
|
||||||
@@ -13,6 +14,13 @@ const breaking_change_detector_1 = require("./breaking-change-detector");
|
|||||||
const node_migration_service_1 = require("./node-migration-service");
|
const node_migration_service_1 = require("./node-migration-service");
|
||||||
const post_update_validator_1 = require("./post-update-validator");
|
const post_update_validator_1 = require("./post-update-validator");
|
||||||
const logger = new logger_1.Logger({ prefix: '[WorkflowAutoFixer]' });
|
const logger = new logger_1.Logger({ prefix: '[WorkflowAutoFixer]' });
|
||||||
|
exports.CONNECTION_FIX_TYPES = [
|
||||||
|
'connection-numeric-keys',
|
||||||
|
'connection-invalid-type',
|
||||||
|
'connection-id-to-name',
|
||||||
|
'connection-duplicate-removal',
|
||||||
|
'connection-input-index'
|
||||||
|
];
|
||||||
function isNodeFormatIssue(issue) {
|
function isNodeFormatIssue(issue) {
|
||||||
return 'nodeName' in issue && 'nodeId' in issue &&
|
return 'nodeName' in issue && 'nodeId' in issue &&
|
||||||
typeof issue.nodeName === 'string' &&
|
typeof issue.nodeName === 'string' &&
|
||||||
@@ -72,6 +80,7 @@ class WorkflowAutoFixer {
|
|||||||
if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('version-migration')) {
|
if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('version-migration')) {
|
||||||
await this.processVersionMigrationFixes(workflow, nodeMap, operations, fixes, postUpdateGuidance);
|
await this.processVersionMigrationFixes(workflow, nodeMap, operations, fixes, postUpdateGuidance);
|
||||||
}
|
}
|
||||||
|
this.processConnectionFixes(workflow, validationResult, fullConfig, operations, fixes);
|
||||||
const filteredFixes = this.filterByConfidence(fixes, fullConfig.confidenceThreshold);
|
const filteredFixes = this.filterByConfidence(fixes, fullConfig.confidenceThreshold);
|
||||||
const filteredOperations = this.filterOperationsByFixes(operations, filteredFixes, fixes);
|
const filteredOperations = this.filterOperationsByFixes(operations, filteredFixes, fixes);
|
||||||
const limitedFixes = filteredFixes.slice(0, fullConfig.maxFixes);
|
const limitedFixes = filteredFixes.slice(0, fullConfig.maxFixes);
|
||||||
@@ -393,10 +402,14 @@ class WorkflowAutoFixer {
|
|||||||
}
|
}
|
||||||
filterOperationsByFixes(operations, filteredFixes, allFixes) {
|
filterOperationsByFixes(operations, filteredFixes, allFixes) {
|
||||||
const fixedNodes = new Set(filteredFixes.map(f => f.node));
|
const fixedNodes = new Set(filteredFixes.map(f => f.node));
|
||||||
|
const hasConnectionFixes = filteredFixes.some(f => exports.CONNECTION_FIX_TYPES.includes(f.type));
|
||||||
return operations.filter(op => {
|
return operations.filter(op => {
|
||||||
if (op.type === 'updateNode') {
|
if (op.type === 'updateNode') {
|
||||||
return fixedNodes.has(op.nodeId || '');
|
return fixedNodes.has(op.nodeId || '');
|
||||||
}
|
}
|
||||||
|
if (op.type === 'replaceConnections') {
|
||||||
|
return hasConnectionFixes;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -411,7 +424,12 @@ class WorkflowAutoFixer {
|
|||||||
'webhook-missing-path': 0,
|
'webhook-missing-path': 0,
|
||||||
'typeversion-upgrade': 0,
|
'typeversion-upgrade': 0,
|
||||||
'version-migration': 0,
|
'version-migration': 0,
|
||||||
'tool-variant-correction': 0
|
'tool-variant-correction': 0,
|
||||||
|
'connection-numeric-keys': 0,
|
||||||
|
'connection-invalid-type': 0,
|
||||||
|
'connection-id-to-name': 0,
|
||||||
|
'connection-duplicate-removal': 0,
|
||||||
|
'connection-input-index': 0
|
||||||
},
|
},
|
||||||
byConfidence: {
|
byConfidence: {
|
||||||
'high': 0,
|
'high': 0,
|
||||||
@@ -454,11 +472,299 @@ class WorkflowAutoFixer {
|
|||||||
if (stats.byType['tool-variant-correction'] > 0) {
|
if (stats.byType['tool-variant-correction'] > 0) {
|
||||||
parts.push(`${stats.byType['tool-variant-correction']} tool variant ${stats.byType['tool-variant-correction'] === 1 ? 'correction' : 'corrections'}`);
|
parts.push(`${stats.byType['tool-variant-correction']} tool variant ${stats.byType['tool-variant-correction'] === 1 ? 'correction' : 'corrections'}`);
|
||||||
}
|
}
|
||||||
|
const connectionIssueCount = (stats.byType['connection-numeric-keys'] || 0) +
|
||||||
|
(stats.byType['connection-invalid-type'] || 0) +
|
||||||
|
(stats.byType['connection-id-to-name'] || 0) +
|
||||||
|
(stats.byType['connection-duplicate-removal'] || 0) +
|
||||||
|
(stats.byType['connection-input-index'] || 0);
|
||||||
|
if (connectionIssueCount > 0) {
|
||||||
|
parts.push(`${connectionIssueCount} connection ${connectionIssueCount === 1 ? 'issue' : 'issues'}`);
|
||||||
|
}
|
||||||
if (parts.length === 0) {
|
if (parts.length === 0) {
|
||||||
return `Fixed ${stats.total} ${stats.total === 1 ? 'issue' : 'issues'}`;
|
return `Fixed ${stats.total} ${stats.total === 1 ? 'issue' : 'issues'}`;
|
||||||
}
|
}
|
||||||
return `Fixed ${parts.join(', ')}`;
|
return `Fixed ${parts.join(', ')}`;
|
||||||
}
|
}
|
||||||
|
processConnectionFixes(workflow, validationResult, config, operations, fixes) {
|
||||||
|
if (!workflow.connections || Object.keys(workflow.connections).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const idToNameMap = new Map();
|
||||||
|
const nameSet = new Set();
|
||||||
|
for (const node of workflow.nodes) {
|
||||||
|
idToNameMap.set(node.id, node.name);
|
||||||
|
nameSet.add(node.name);
|
||||||
|
}
|
||||||
|
const conn = JSON.parse(JSON.stringify(workflow.connections));
|
||||||
|
let anyFixed = false;
|
||||||
|
if (!config.fixTypes || config.fixTypes.includes('connection-numeric-keys')) {
|
||||||
|
const numericKeyResult = this.fixNumericKeys(conn);
|
||||||
|
if (numericKeyResult.length > 0) {
|
||||||
|
fixes.push(...numericKeyResult);
|
||||||
|
anyFixed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!config.fixTypes || config.fixTypes.includes('connection-id-to-name')) {
|
||||||
|
const idToNameResult = this.fixIdToName(conn, idToNameMap, nameSet);
|
||||||
|
if (idToNameResult.length > 0) {
|
||||||
|
fixes.push(...idToNameResult);
|
||||||
|
anyFixed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!config.fixTypes || config.fixTypes.includes('connection-invalid-type')) {
|
||||||
|
const invalidTypeResult = this.fixInvalidTypes(conn);
|
||||||
|
if (invalidTypeResult.length > 0) {
|
||||||
|
fixes.push(...invalidTypeResult);
|
||||||
|
anyFixed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!config.fixTypes || config.fixTypes.includes('connection-input-index')) {
|
||||||
|
const inputIndexResult = this.fixInputIndices(conn, validationResult, workflow);
|
||||||
|
if (inputIndexResult.length > 0) {
|
||||||
|
fixes.push(...inputIndexResult);
|
||||||
|
anyFixed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!config.fixTypes || config.fixTypes.includes('connection-duplicate-removal')) {
|
||||||
|
const dedupResult = this.fixDuplicateConnections(conn);
|
||||||
|
if (dedupResult.length > 0) {
|
||||||
|
fixes.push(...dedupResult);
|
||||||
|
anyFixed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (anyFixed) {
|
||||||
|
const op = {
|
||||||
|
type: 'replaceConnections',
|
||||||
|
connections: conn
|
||||||
|
};
|
||||||
|
operations.push(op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fixNumericKeys(conn) {
|
||||||
|
const fixes = [];
|
||||||
|
const sourceNodes = Object.keys(conn);
|
||||||
|
for (const sourceName of sourceNodes) {
|
||||||
|
const nodeConn = conn[sourceName];
|
||||||
|
const numericKeys = Object.keys(nodeConn).filter(k => /^\d+$/.test(k));
|
||||||
|
if (numericKeys.length === 0)
|
||||||
|
continue;
|
||||||
|
if (!nodeConn['main']) {
|
||||||
|
nodeConn['main'] = [];
|
||||||
|
}
|
||||||
|
for (const numKey of numericKeys) {
|
||||||
|
const index = parseInt(numKey, 10);
|
||||||
|
const entries = nodeConn[numKey];
|
||||||
|
while (nodeConn['main'].length <= index) {
|
||||||
|
nodeConn['main'].push([]);
|
||||||
|
}
|
||||||
|
const hadExisting = nodeConn['main'][index] && nodeConn['main'][index].length > 0;
|
||||||
|
if (Array.isArray(entries)) {
|
||||||
|
for (const outputGroup of entries) {
|
||||||
|
if (Array.isArray(outputGroup)) {
|
||||||
|
nodeConn['main'][index] = [
|
||||||
|
...nodeConn['main'][index],
|
||||||
|
...outputGroup
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hadExisting) {
|
||||||
|
logger.warn(`Merged numeric key "${numKey}" into existing main[${index}] on node "${sourceName}" - dedup pass will clean exact duplicates`);
|
||||||
|
}
|
||||||
|
fixes.push({
|
||||||
|
node: sourceName,
|
||||||
|
field: `connections.${sourceName}.${numKey}`,
|
||||||
|
type: 'connection-numeric-keys',
|
||||||
|
before: numKey,
|
||||||
|
after: `main[${index}]`,
|
||||||
|
confidence: hadExisting ? 'medium' : 'high',
|
||||||
|
description: hadExisting
|
||||||
|
? `Merged numeric connection key "${numKey}" into existing main[${index}] on node "${sourceName}"`
|
||||||
|
: `Converted numeric connection key "${numKey}" to main[${index}] on node "${sourceName}"`
|
||||||
|
});
|
||||||
|
delete nodeConn[numKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fixes;
|
||||||
|
}
|
||||||
|
fixIdToName(conn, idToNameMap, nameSet) {
|
||||||
|
const fixes = [];
|
||||||
|
const renames = [];
|
||||||
|
const sourceKeys = Object.keys(conn);
|
||||||
|
for (const sourceKey of sourceKeys) {
|
||||||
|
if (idToNameMap.has(sourceKey) && !nameSet.has(sourceKey)) {
|
||||||
|
renames.push({ oldKey: sourceKey, newKey: idToNameMap.get(sourceKey) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newKeyCount = new Map();
|
||||||
|
for (const r of renames) {
|
||||||
|
newKeyCount.set(r.newKey, (newKeyCount.get(r.newKey) || 0) + 1);
|
||||||
|
}
|
||||||
|
const safeRenames = renames.filter(r => {
|
||||||
|
if ((newKeyCount.get(r.newKey) || 0) > 1) {
|
||||||
|
logger.warn(`Skipping ambiguous ID-to-name rename: "${r.oldKey}" → "${r.newKey}" (multiple IDs map to same name)`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
for (const { oldKey, newKey } of safeRenames) {
|
||||||
|
conn[newKey] = conn[oldKey];
|
||||||
|
delete conn[oldKey];
|
||||||
|
fixes.push({
|
||||||
|
node: newKey,
|
||||||
|
field: `connections.sourceKey`,
|
||||||
|
type: 'connection-id-to-name',
|
||||||
|
before: oldKey,
|
||||||
|
after: newKey,
|
||||||
|
confidence: 'high',
|
||||||
|
description: `Replaced node ID "${oldKey}" with name "${newKey}" as connection source key`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const sourceName of Object.keys(conn)) {
|
||||||
|
const nodeConn = conn[sourceName];
|
||||||
|
for (const outputKey of Object.keys(nodeConn)) {
|
||||||
|
const outputs = nodeConn[outputKey];
|
||||||
|
if (!Array.isArray(outputs))
|
||||||
|
continue;
|
||||||
|
for (const outputGroup of outputs) {
|
||||||
|
if (!Array.isArray(outputGroup))
|
||||||
|
continue;
|
||||||
|
for (const entry of outputGroup) {
|
||||||
|
if (entry && entry.node && idToNameMap.has(entry.node) && !nameSet.has(entry.node)) {
|
||||||
|
const oldNode = entry.node;
|
||||||
|
const newNode = idToNameMap.get(entry.node);
|
||||||
|
entry.node = newNode;
|
||||||
|
fixes.push({
|
||||||
|
node: sourceName,
|
||||||
|
field: `connections.${sourceName}.${outputKey}[].node`,
|
||||||
|
type: 'connection-id-to-name',
|
||||||
|
before: oldNode,
|
||||||
|
after: newNode,
|
||||||
|
confidence: 'high',
|
||||||
|
description: `Replaced target node ID "${oldNode}" with name "${newNode}" in connection from "${sourceName}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fixes;
|
||||||
|
}
|
||||||
|
fixInvalidTypes(conn) {
|
||||||
|
const fixes = [];
|
||||||
|
for (const sourceName of Object.keys(conn)) {
|
||||||
|
const nodeConn = conn[sourceName];
|
||||||
|
for (const outputKey of Object.keys(nodeConn)) {
|
||||||
|
const outputs = nodeConn[outputKey];
|
||||||
|
if (!Array.isArray(outputs))
|
||||||
|
continue;
|
||||||
|
for (const outputGroup of outputs) {
|
||||||
|
if (!Array.isArray(outputGroup))
|
||||||
|
continue;
|
||||||
|
for (const entry of outputGroup) {
|
||||||
|
if (entry && entry.type && !workflow_validator_1.VALID_CONNECTION_TYPES.has(entry.type)) {
|
||||||
|
const oldType = entry.type;
|
||||||
|
const newType = workflow_validator_1.VALID_CONNECTION_TYPES.has(outputKey) ? outputKey : 'main';
|
||||||
|
entry.type = newType;
|
||||||
|
fixes.push({
|
||||||
|
node: sourceName,
|
||||||
|
field: `connections.${sourceName}.${outputKey}[].type`,
|
||||||
|
type: 'connection-invalid-type',
|
||||||
|
before: oldType,
|
||||||
|
after: newType,
|
||||||
|
confidence: 'high',
|
||||||
|
description: `Fixed invalid connection type "${oldType}" → "${newType}" in connection from "${sourceName}" to "${entry.node}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fixes;
|
||||||
|
}
|
||||||
|
fixInputIndices(conn, validationResult, workflow) {
|
||||||
|
const fixes = [];
|
||||||
|
for (const error of validationResult.errors) {
|
||||||
|
if (error.code !== 'INPUT_INDEX_OUT_OF_BOUNDS')
|
||||||
|
continue;
|
||||||
|
const targetNodeName = error.nodeName;
|
||||||
|
if (!targetNodeName)
|
||||||
|
continue;
|
||||||
|
const match = error.message.match(/Input index (\d+).*?has (\d+) main input/);
|
||||||
|
if (!match) {
|
||||||
|
logger.warn(`Could not parse INPUT_INDEX_OUT_OF_BOUNDS error for node "${targetNodeName}": ${error.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const badIndex = parseInt(match[1], 10);
|
||||||
|
const inputCount = parseInt(match[2], 10);
|
||||||
|
const clampedIndex = inputCount > 1 ? Math.min(badIndex, inputCount - 1) : 0;
|
||||||
|
for (const sourceName of Object.keys(conn)) {
|
||||||
|
const nodeConn = conn[sourceName];
|
||||||
|
for (const outputKey of Object.keys(nodeConn)) {
|
||||||
|
const outputs = nodeConn[outputKey];
|
||||||
|
if (!Array.isArray(outputs))
|
||||||
|
continue;
|
||||||
|
for (const outputGroup of outputs) {
|
||||||
|
if (!Array.isArray(outputGroup))
|
||||||
|
continue;
|
||||||
|
for (const entry of outputGroup) {
|
||||||
|
if (entry && entry.node === targetNodeName && entry.index === badIndex) {
|
||||||
|
entry.index = clampedIndex;
|
||||||
|
fixes.push({
|
||||||
|
node: sourceName,
|
||||||
|
field: `connections.${sourceName}.${outputKey}[].index`,
|
||||||
|
type: 'connection-input-index',
|
||||||
|
before: badIndex,
|
||||||
|
after: clampedIndex,
|
||||||
|
confidence: 'medium',
|
||||||
|
description: `Clamped input index ${badIndex} → ${clampedIndex} for target node "${targetNodeName}" (has ${inputCount} input${inputCount === 1 ? '' : 's'})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fixes;
|
||||||
|
}
|
||||||
|
fixDuplicateConnections(conn) {
|
||||||
|
const fixes = [];
|
||||||
|
for (const sourceName of Object.keys(conn)) {
|
||||||
|
const nodeConn = conn[sourceName];
|
||||||
|
for (const outputKey of Object.keys(nodeConn)) {
|
||||||
|
const outputs = nodeConn[outputKey];
|
||||||
|
if (!Array.isArray(outputs))
|
||||||
|
continue;
|
||||||
|
for (let i = 0; i < outputs.length; i++) {
|
||||||
|
const outputGroup = outputs[i];
|
||||||
|
if (!Array.isArray(outputGroup))
|
||||||
|
continue;
|
||||||
|
const seen = new Set();
|
||||||
|
const deduped = [];
|
||||||
|
for (const entry of outputGroup) {
|
||||||
|
const key = JSON.stringify({ node: entry.node, type: entry.type, index: entry.index });
|
||||||
|
if (seen.has(key)) {
|
||||||
|
fixes.push({
|
||||||
|
node: sourceName,
|
||||||
|
field: `connections.${sourceName}.${outputKey}[${i}]`,
|
||||||
|
type: 'connection-duplicate-removal',
|
||||||
|
before: entry,
|
||||||
|
after: null,
|
||||||
|
confidence: 'high',
|
||||||
|
description: `Removed duplicate connection from "${sourceName}" to "${entry.node}" (type: ${entry.type}, index: ${entry.index})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
seen.add(key);
|
||||||
|
deduped.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outputs[i] = deduped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fixes;
|
||||||
|
}
|
||||||
async processVersionUpgradeFixes(workflow, nodeMap, operations, fixes, postUpdateGuidance) {
|
async processVersionUpgradeFixes(workflow, nodeMap, operations, fixes, postUpdateGuidance) {
|
||||||
if (!this.versionService || !this.migrationService || !this.postUpdateValidator) {
|
if (!this.versionService || !this.migrationService || !this.postUpdateValidator) {
|
||||||
logger.warn('Version services not initialized. Skipping version upgrade fixes.');
|
logger.warn('Version services not initialized. Skipping version upgrade fixes.');
|
||||||
|
|||||||
2
dist/services/workflow-auto-fixer.js.map
vendored
2
dist/services/workflow-auto-fixer.js.map
vendored
File diff suppressed because one or more lines are too long
22
dist/services/workflow-validator.d.ts
vendored
22
dist/services/workflow-validator.d.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
import { NodeRepository } from '../database/node-repository';
|
import { NodeRepository } from '../database/node-repository';
|
||||||
import { EnhancedConfigValidator } from './enhanced-config-validator';
|
import { EnhancedConfigValidator } from './enhanced-config-validator';
|
||||||
|
export declare const VALID_CONNECTION_TYPES: Set<string>;
|
||||||
interface WorkflowNode {
|
interface WorkflowNode {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -21,17 +22,7 @@ interface WorkflowNode {
|
|||||||
}
|
}
|
||||||
interface WorkflowConnection {
|
interface WorkflowConnection {
|
||||||
[sourceNode: string]: {
|
[sourceNode: string]: {
|
||||||
main?: Array<Array<{
|
[outputType: string]: Array<Array<{
|
||||||
node: string;
|
|
||||||
type: string;
|
|
||||||
index: number;
|
|
||||||
}>>;
|
|
||||||
error?: Array<Array<{
|
|
||||||
node: string;
|
|
||||||
type: string;
|
|
||||||
index: number;
|
|
||||||
}>>;
|
|
||||||
ai_tool?: Array<Array<{
|
|
||||||
node: string;
|
node: string;
|
||||||
type: string;
|
type: string;
|
||||||
index: number;
|
index: number;
|
||||||
@@ -94,6 +85,15 @@ export declare class WorkflowValidator {
|
|||||||
private validateErrorOutputConfiguration;
|
private validateErrorOutputConfiguration;
|
||||||
private validateAIToolConnection;
|
private validateAIToolConnection;
|
||||||
private validateAIToolSource;
|
private validateAIToolSource;
|
||||||
|
private getNodeOutputTypes;
|
||||||
|
private validateNotAISubNode;
|
||||||
|
private getShortNodeType;
|
||||||
|
private getConditionalOutputInfo;
|
||||||
|
private validateOutputIndexBounds;
|
||||||
|
private validateConditionalBranchUsage;
|
||||||
|
private validateInputIndexBounds;
|
||||||
|
private flagOrphanedNodes;
|
||||||
|
private validateTriggerReachability;
|
||||||
private hasCycle;
|
private hasCycle;
|
||||||
private validateExpressions;
|
private validateExpressions;
|
||||||
private countExpressionsInObject;
|
private countExpressionsInObject;
|
||||||
|
|||||||
2
dist/services/workflow-validator.d.ts.map
vendored
2
dist/services/workflow-validator.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"workflow-validator.d.ts","sourceRoot":"","sources":["../../src/services/workflow-validator.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"workflow-validator.d.ts","sourceRoot":"","sources":["../../src/services/workflow-validator.ts"],"names":[],"mappings":"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"}
|
||||||
374
dist/services/workflow-validator.js
vendored
374
dist/services/workflow-validator.js
vendored
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.WorkflowValidator = void 0;
|
exports.WorkflowValidator = exports.VALID_CONNECTION_TYPES = void 0;
|
||||||
const crypto_1 = __importDefault(require("crypto"));
|
const crypto_1 = __importDefault(require("crypto"));
|
||||||
const expression_validator_1 = require("./expression-validator");
|
const expression_validator_1 = require("./expression-validator");
|
||||||
const expression_format_validator_1 = require("./expression-format-validator");
|
const expression_format_validator_1 = require("./expression-format-validator");
|
||||||
@@ -16,6 +16,15 @@ const node_type_utils_1 = require("../utils/node-type-utils");
|
|||||||
const node_classification_1 = require("../utils/node-classification");
|
const node_classification_1 = require("../utils/node-classification");
|
||||||
const tool_variant_generator_1 = require("./tool-variant-generator");
|
const tool_variant_generator_1 = require("./tool-variant-generator");
|
||||||
const logger = new logger_1.Logger({ prefix: '[WorkflowValidator]' });
|
const logger = new logger_1.Logger({ prefix: '[WorkflowValidator]' });
|
||||||
|
exports.VALID_CONNECTION_TYPES = new Set([
|
||||||
|
'main',
|
||||||
|
'error',
|
||||||
|
...ai_node_validator_1.AI_CONNECTION_TYPES,
|
||||||
|
'ai_agent',
|
||||||
|
'ai_chain',
|
||||||
|
'ai_retriever',
|
||||||
|
'ai_reranker',
|
||||||
|
]);
|
||||||
class WorkflowValidator {
|
class WorkflowValidator {
|
||||||
constructor(nodeRepository, nodeValidator) {
|
constructor(nodeRepository, nodeValidator) {
|
||||||
this.nodeRepository = nodeRepository;
|
this.nodeRepository = nodeRepository;
|
||||||
@@ -393,51 +402,37 @@ class WorkflowValidator {
|
|||||||
result.statistics.invalidConnections++;
|
result.statistics.invalidConnections++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (outputs.main) {
|
for (const [outputKey, outputConnections] of Object.entries(outputs)) {
|
||||||
this.validateConnectionOutputs(sourceName, outputs.main, nodeMap, nodeIdMap, result, 'main');
|
if (!exports.VALID_CONNECTION_TYPES.has(outputKey)) {
|
||||||
}
|
let suggestion = '';
|
||||||
if (outputs.error) {
|
if (/^\d+$/.test(outputKey)) {
|
||||||
this.validateConnectionOutputs(sourceName, outputs.error, nodeMap, nodeIdMap, result, 'error');
|
suggestion = ` If you meant to use output index ${outputKey}, use main[${outputKey}] instead.`;
|
||||||
}
|
}
|
||||||
if (outputs.ai_tool) {
|
result.errors.push({
|
||||||
this.validateAIToolSource(sourceNode, result);
|
type: 'error',
|
||||||
this.validateConnectionOutputs(sourceName, outputs.ai_tool, nodeMap, nodeIdMap, result, 'ai_tool');
|
nodeName: sourceName,
|
||||||
|
message: `Unknown connection output key "${outputKey}" on node "${sourceName}". Valid keys are: ${[...exports.VALID_CONNECTION_TYPES].join(', ')}.${suggestion}`,
|
||||||
|
code: 'UNKNOWN_CONNECTION_KEY'
|
||||||
|
});
|
||||||
|
result.statistics.invalidConnections++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!outputConnections || !Array.isArray(outputConnections))
|
||||||
|
continue;
|
||||||
|
if (outputKey === 'ai_tool') {
|
||||||
|
this.validateAIToolSource(sourceNode, result);
|
||||||
|
}
|
||||||
|
if (outputKey === 'main') {
|
||||||
|
this.validateNotAISubNode(sourceNode, result);
|
||||||
|
}
|
||||||
|
this.validateConnectionOutputs(sourceName, outputConnections, nodeMap, nodeIdMap, result, outputKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const connectedNodes = new Set();
|
if (profile !== 'minimal') {
|
||||||
Object.keys(workflow.connections).forEach(name => connectedNodes.add(name));
|
this.validateTriggerReachability(workflow, result);
|
||||||
Object.values(workflow.connections).forEach(outputs => {
|
}
|
||||||
if (outputs.main) {
|
else {
|
||||||
outputs.main.flat().forEach(conn => {
|
this.flagOrphanedNodes(workflow, result);
|
||||||
if (conn)
|
|
||||||
connectedNodes.add(conn.node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (outputs.error) {
|
|
||||||
outputs.error.flat().forEach(conn => {
|
|
||||||
if (conn)
|
|
||||||
connectedNodes.add(conn.node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (outputs.ai_tool) {
|
|
||||||
outputs.ai_tool.flat().forEach(conn => {
|
|
||||||
if (conn)
|
|
||||||
connectedNodes.add(conn.node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for (const node of workflow.nodes) {
|
|
||||||
if (node.disabled || (0, node_classification_1.isNonExecutableNode)(node.type))
|
|
||||||
continue;
|
|
||||||
const isNodeTrigger = (0, node_type_utils_1.isTriggerNode)(node.type);
|
|
||||||
if (!connectedNodes.has(node.name) && !isNodeTrigger) {
|
|
||||||
result.warnings.push({
|
|
||||||
type: 'warning',
|
|
||||||
nodeId: node.id,
|
|
||||||
nodeName: node.name,
|
|
||||||
message: 'Node is not connected to any other nodes'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (profile !== 'minimal' && this.hasCycle(workflow)) {
|
if (profile !== 'minimal' && this.hasCycle(workflow)) {
|
||||||
result.errors.push({
|
result.errors.push({
|
||||||
@@ -450,6 +445,8 @@ class WorkflowValidator {
|
|||||||
const sourceNode = nodeMap.get(sourceName);
|
const sourceNode = nodeMap.get(sourceName);
|
||||||
if (outputType === 'main' && sourceNode) {
|
if (outputType === 'main' && sourceNode) {
|
||||||
this.validateErrorOutputConfiguration(sourceName, sourceNode, outputs, nodeMap, result);
|
this.validateErrorOutputConfiguration(sourceName, sourceNode, outputs, nodeMap, result);
|
||||||
|
this.validateOutputIndexBounds(sourceNode, outputs, result);
|
||||||
|
this.validateConditionalBranchUsage(sourceNode, outputs, result);
|
||||||
}
|
}
|
||||||
outputs.forEach((outputConnections, outputIndex) => {
|
outputs.forEach((outputConnections, outputIndex) => {
|
||||||
if (!outputConnections)
|
if (!outputConnections)
|
||||||
@@ -463,6 +460,20 @@ class WorkflowValidator {
|
|||||||
result.statistics.invalidConnections++;
|
result.statistics.invalidConnections++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (connection.type && !exports.VALID_CONNECTION_TYPES.has(connection.type)) {
|
||||||
|
let suggestion = '';
|
||||||
|
if (/^\d+$/.test(connection.type)) {
|
||||||
|
suggestion = ` Numeric types are not valid - use "main", "error", or an AI connection type.`;
|
||||||
|
}
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
nodeName: sourceName,
|
||||||
|
message: `Invalid connection type "${connection.type}" in connection from "${sourceName}" to "${connection.node}". Expected "main", "error", or an AI connection type (ai_tool, ai_languageModel, etc.).${suggestion}`,
|
||||||
|
code: 'INVALID_CONNECTION_TYPE'
|
||||||
|
});
|
||||||
|
result.statistics.invalidConnections++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const isSplitInBatches = sourceNode && (sourceNode.type === 'n8n-nodes-base.splitInBatches' ||
|
const isSplitInBatches = sourceNode && (sourceNode.type === 'n8n-nodes-base.splitInBatches' ||
|
||||||
sourceNode.type === 'nodes-base.splitInBatches');
|
sourceNode.type === 'nodes-base.splitInBatches');
|
||||||
if (isSplitInBatches) {
|
if (isSplitInBatches) {
|
||||||
@@ -506,6 +517,9 @@ class WorkflowValidator {
|
|||||||
if (outputType === 'ai_tool') {
|
if (outputType === 'ai_tool') {
|
||||||
this.validateAIToolConnection(sourceName, targetNode, result);
|
this.validateAIToolConnection(sourceName, targetNode, result);
|
||||||
}
|
}
|
||||||
|
if (outputType === 'main') {
|
||||||
|
this.validateInputIndexBounds(sourceName, targetNode, connection, result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -634,6 +648,254 @@ class WorkflowValidator {
|
|||||||
code: 'INVALID_AI_TOOL_SOURCE'
|
code: 'INVALID_AI_TOOL_SOURCE'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
getNodeOutputTypes(nodeType) {
|
||||||
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
||||||
|
const nodeInfo = this.nodeRepository.getNode(normalizedType);
|
||||||
|
if (!nodeInfo || !nodeInfo.outputs)
|
||||||
|
return null;
|
||||||
|
const outputs = nodeInfo.outputs;
|
||||||
|
if (!Array.isArray(outputs))
|
||||||
|
return null;
|
||||||
|
for (const output of outputs) {
|
||||||
|
if (typeof output === 'string' && output.startsWith('={{')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
validateNotAISubNode(sourceNode, result) {
|
||||||
|
const outputTypes = this.getNodeOutputTypes(sourceNode.type);
|
||||||
|
if (!outputTypes)
|
||||||
|
return;
|
||||||
|
const hasMainOutput = outputTypes.some(t => t === 'main');
|
||||||
|
if (hasMainOutput)
|
||||||
|
return;
|
||||||
|
const aiTypes = outputTypes.filter(t => t !== 'main');
|
||||||
|
const expectedType = aiTypes[0] || 'ai_languageModel';
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
nodeId: sourceNode.id,
|
||||||
|
nodeName: sourceNode.name,
|
||||||
|
message: `Node "${sourceNode.name}" (${sourceNode.type}) is an AI sub-node that outputs "${expectedType}" connections. ` +
|
||||||
|
`It cannot be used with "main" connections. Connect it to an AI Agent or Chain via "${expectedType}" instead.`,
|
||||||
|
code: 'AI_SUBNODE_MAIN_CONNECTION'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getShortNodeType(sourceNode) {
|
||||||
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(sourceNode.type);
|
||||||
|
return normalizedType.replace(/^(n8n-)?nodes-base\./, '');
|
||||||
|
}
|
||||||
|
getConditionalOutputInfo(sourceNode) {
|
||||||
|
const shortType = this.getShortNodeType(sourceNode);
|
||||||
|
if (shortType === 'if' || shortType === 'filter') {
|
||||||
|
return { shortType, expectedOutputs: 2 };
|
||||||
|
}
|
||||||
|
if (shortType === 'switch') {
|
||||||
|
const rules = sourceNode.parameters?.rules?.values || sourceNode.parameters?.rules;
|
||||||
|
if (Array.isArray(rules)) {
|
||||||
|
return { shortType, expectedOutputs: rules.length + 1 };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
validateOutputIndexBounds(sourceNode, outputs, result) {
|
||||||
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(sourceNode.type);
|
||||||
|
const nodeInfo = this.nodeRepository.getNode(normalizedType);
|
||||||
|
if (!nodeInfo || !nodeInfo.outputs)
|
||||||
|
return;
|
||||||
|
let mainOutputCount;
|
||||||
|
if (Array.isArray(nodeInfo.outputs)) {
|
||||||
|
mainOutputCount = nodeInfo.outputs.filter((o) => typeof o === 'string' ? o === 'main' : (o.type === 'main' || !o.type)).length;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mainOutputCount === 0)
|
||||||
|
return;
|
||||||
|
const conditionalInfo = this.getConditionalOutputInfo(sourceNode);
|
||||||
|
if (conditionalInfo) {
|
||||||
|
mainOutputCount = conditionalInfo.expectedOutputs;
|
||||||
|
}
|
||||||
|
else if (this.getShortNodeType(sourceNode) === 'switch') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sourceNode.onError === 'continueErrorOutput') {
|
||||||
|
mainOutputCount += 1;
|
||||||
|
}
|
||||||
|
const maxOutputIndex = outputs.length - 1;
|
||||||
|
if (maxOutputIndex >= mainOutputCount) {
|
||||||
|
for (let i = mainOutputCount; i < outputs.length; i++) {
|
||||||
|
if (outputs[i] && outputs[i].length > 0) {
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
nodeId: sourceNode.id,
|
||||||
|
nodeName: sourceNode.name,
|
||||||
|
message: `Output index ${i} on node "${sourceNode.name}" exceeds its output count (${mainOutputCount}). ` +
|
||||||
|
`This node has ${mainOutputCount} main output(s) (indices 0-${mainOutputCount - 1}).`,
|
||||||
|
code: 'OUTPUT_INDEX_OUT_OF_BOUNDS'
|
||||||
|
});
|
||||||
|
result.statistics.invalidConnections++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validateConditionalBranchUsage(sourceNode, outputs, result) {
|
||||||
|
const conditionalInfo = this.getConditionalOutputInfo(sourceNode);
|
||||||
|
if (!conditionalInfo || conditionalInfo.expectedOutputs < 2)
|
||||||
|
return;
|
||||||
|
const { shortType, expectedOutputs } = conditionalInfo;
|
||||||
|
const main0Count = outputs[0]?.length || 0;
|
||||||
|
if (main0Count < 2)
|
||||||
|
return;
|
||||||
|
const hasHigherIndexConnections = outputs.slice(1).some(conns => conns && conns.length > 0);
|
||||||
|
if (hasHigherIndexConnections)
|
||||||
|
return;
|
||||||
|
let message;
|
||||||
|
if (shortType === 'if' || shortType === 'filter') {
|
||||||
|
const isFilter = shortType === 'filter';
|
||||||
|
const displayName = isFilter ? 'Filter' : 'IF';
|
||||||
|
const trueLabel = isFilter ? 'matched' : 'true';
|
||||||
|
const falseLabel = isFilter ? 'unmatched' : 'false';
|
||||||
|
message = `${displayName} node "${sourceNode.name}" has ${main0Count} connections on the "${trueLabel}" branch (main[0]) ` +
|
||||||
|
`but no connections on the "${falseLabel}" branch (main[1]). ` +
|
||||||
|
`All ${main0Count} target nodes execute together on the "${trueLabel}" branch, ` +
|
||||||
|
`while the "${falseLabel}" branch has no effect. ` +
|
||||||
|
`Split connections: main[0] for ${trueLabel}, main[1] for ${falseLabel}.`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = `Switch node "${sourceNode.name}" has ${main0Count} connections on output 0 ` +
|
||||||
|
`but no connections on any other outputs (1-${expectedOutputs - 1}). ` +
|
||||||
|
`All ${main0Count} target nodes execute together on output 0, ` +
|
||||||
|
`while other switch branches have no effect. ` +
|
||||||
|
`Distribute connections across outputs to match switch rules.`;
|
||||||
|
}
|
||||||
|
result.warnings.push({
|
||||||
|
type: 'warning',
|
||||||
|
nodeId: sourceNode.id,
|
||||||
|
nodeName: sourceNode.name,
|
||||||
|
message,
|
||||||
|
code: 'CONDITIONAL_BRANCH_FANOUT'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
validateInputIndexBounds(sourceName, targetNode, connection, result) {
|
||||||
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(targetNode.type);
|
||||||
|
const nodeInfo = this.nodeRepository.getNode(normalizedType);
|
||||||
|
if (!nodeInfo)
|
||||||
|
return;
|
||||||
|
const shortType = normalizedType.replace(/^(n8n-)?nodes-base\./, '');
|
||||||
|
let mainInputCount = 1;
|
||||||
|
if (shortType === 'merge' || shortType === 'compareDatasets') {
|
||||||
|
mainInputCount = 2;
|
||||||
|
}
|
||||||
|
if (nodeInfo.isTrigger || (0, node_type_utils_1.isTriggerNode)(targetNode.type)) {
|
||||||
|
mainInputCount = 0;
|
||||||
|
}
|
||||||
|
if (mainInputCount > 0 && connection.index >= mainInputCount) {
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
nodeName: targetNode.name,
|
||||||
|
message: `Input index ${connection.index} on node "${targetNode.name}" exceeds its input count (${mainInputCount}). ` +
|
||||||
|
`Connection from "${sourceName}" targets input ${connection.index}, but this node has ${mainInputCount} main input(s) (indices 0-${mainInputCount - 1}).`,
|
||||||
|
code: 'INPUT_INDEX_OUT_OF_BOUNDS'
|
||||||
|
});
|
||||||
|
result.statistics.invalidConnections++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flagOrphanedNodes(workflow, result) {
|
||||||
|
const connectedNodes = new Set();
|
||||||
|
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
|
||||||
|
connectedNodes.add(sourceName);
|
||||||
|
for (const outputConns of Object.values(outputs)) {
|
||||||
|
if (!Array.isArray(outputConns))
|
||||||
|
continue;
|
||||||
|
for (const conns of outputConns) {
|
||||||
|
if (!conns)
|
||||||
|
continue;
|
||||||
|
for (const conn of conns) {
|
||||||
|
if (conn)
|
||||||
|
connectedNodes.add(conn.node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const node of workflow.nodes) {
|
||||||
|
if (node.disabled || (0, node_classification_1.isNonExecutableNode)(node.type))
|
||||||
|
continue;
|
||||||
|
if ((0, node_type_utils_1.isTriggerNode)(node.type))
|
||||||
|
continue;
|
||||||
|
if (!connectedNodes.has(node.name)) {
|
||||||
|
result.warnings.push({
|
||||||
|
type: 'warning',
|
||||||
|
nodeId: node.id,
|
||||||
|
nodeName: node.name,
|
||||||
|
message: 'Node is not connected to any other nodes'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validateTriggerReachability(workflow, result) {
|
||||||
|
const adjacency = new Map();
|
||||||
|
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
|
||||||
|
if (!adjacency.has(sourceName))
|
||||||
|
adjacency.set(sourceName, new Set());
|
||||||
|
for (const outputConns of Object.values(outputs)) {
|
||||||
|
if (Array.isArray(outputConns)) {
|
||||||
|
for (const conns of outputConns) {
|
||||||
|
if (!conns)
|
||||||
|
continue;
|
||||||
|
for (const conn of conns) {
|
||||||
|
if (conn) {
|
||||||
|
adjacency.get(sourceName).add(conn.node);
|
||||||
|
if (!adjacency.has(conn.node))
|
||||||
|
adjacency.set(conn.node, new Set());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const triggerNodes = [];
|
||||||
|
for (const node of workflow.nodes) {
|
||||||
|
if ((0, node_type_utils_1.isTriggerNode)(node.type) && !node.disabled) {
|
||||||
|
triggerNodes.push(node.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (triggerNodes.length === 0) {
|
||||||
|
this.flagOrphanedNodes(workflow, result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reachable = new Set();
|
||||||
|
const queue = [...triggerNodes];
|
||||||
|
for (const t of triggerNodes)
|
||||||
|
reachable.add(t);
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift();
|
||||||
|
const neighbors = adjacency.get(current);
|
||||||
|
if (neighbors) {
|
||||||
|
for (const neighbor of neighbors) {
|
||||||
|
if (!reachable.has(neighbor)) {
|
||||||
|
reachable.add(neighbor);
|
||||||
|
queue.push(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const node of workflow.nodes) {
|
||||||
|
if (node.disabled || (0, node_classification_1.isNonExecutableNode)(node.type))
|
||||||
|
continue;
|
||||||
|
if ((0, node_type_utils_1.isTriggerNode)(node.type))
|
||||||
|
continue;
|
||||||
|
if (!reachable.has(node.name)) {
|
||||||
|
result.warnings.push({
|
||||||
|
type: 'warning',
|
||||||
|
nodeId: node.id,
|
||||||
|
nodeName: node.name,
|
||||||
|
message: 'Node is not reachable from any trigger node'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
hasCycle(workflow) {
|
hasCycle(workflow) {
|
||||||
const visited = new Set();
|
const visited = new Set();
|
||||||
const recursionStack = new Set();
|
const recursionStack = new Set();
|
||||||
@@ -657,23 +919,13 @@ class WorkflowValidator {
|
|||||||
const connections = workflow.connections[nodeName];
|
const connections = workflow.connections[nodeName];
|
||||||
if (connections) {
|
if (connections) {
|
||||||
const allTargets = [];
|
const allTargets = [];
|
||||||
if (connections.main) {
|
for (const outputConns of Object.values(connections)) {
|
||||||
connections.main.flat().forEach(conn => {
|
if (Array.isArray(outputConns)) {
|
||||||
if (conn)
|
outputConns.flat().forEach(conn => {
|
||||||
allTargets.push(conn.node);
|
if (conn)
|
||||||
});
|
allTargets.push(conn.node);
|
||||||
}
|
});
|
||||||
if (connections.error) {
|
}
|
||||||
connections.error.flat().forEach(conn => {
|
|
||||||
if (conn)
|
|
||||||
allTargets.push(conn.node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (connections.ai_tool) {
|
|
||||||
connections.ai_tool.flat().forEach(conn => {
|
|
||||||
if (conn)
|
|
||||||
allTargets.push(conn.node);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const currentNodeType = nodeTypeMap.get(nodeName);
|
const currentNodeType = nodeTypeMap.get(nodeName);
|
||||||
const isLoopNode = loopNodeTypes.includes(currentNodeType || '');
|
const isLoopNode = loopNodeTypes.includes(currentNodeType || '');
|
||||||
|
|||||||
2
dist/services/workflow-validator.js.map
vendored
2
dist/services/workflow-validator.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/telemetry/batch-processor.d.ts
vendored
2
dist/telemetry/batch-processor.d.ts
vendored
@@ -12,6 +12,8 @@ export declare class TelemetryBatchProcessor {
|
|||||||
private flushTimes;
|
private flushTimes;
|
||||||
private deadLetterQueue;
|
private deadLetterQueue;
|
||||||
private readonly maxDeadLetterSize;
|
private readonly maxDeadLetterSize;
|
||||||
|
private eventListeners;
|
||||||
|
private started;
|
||||||
constructor(supabase: SupabaseClient | null, isEnabled: () => boolean);
|
constructor(supabase: SupabaseClient | null, isEnabled: () => boolean);
|
||||||
start(): void;
|
start(): void;
|
||||||
stop(): void;
|
stop(): void;
|
||||||
|
|||||||
2
dist/telemetry/batch-processor.d.ts.map
vendored
2
dist/telemetry/batch-processor.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"batch-processor.d.ts","sourceRoot":"","sources":["../../src/telemetry/batch-processor.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,sBAAsB,EAAoB,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAoClI,qBAAa,uBAAuB;IAoBhC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,SAAS;IApBnB,OAAO,CAAC,UAAU,CAAC,CAAiB;IACpC,OAAO,CAAC,gBAAgB,CAAkB;IAC1C,OAAO,CAAC,mBAAmB,CAAkB;IAC7C,OAAO,CAAC,mBAAmB,CAAkB;IAC7C,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,OAAO,CAQb;IACF,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,eAAe,CAAuE;IAC9F,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAO;gBAG/B,QAAQ,EAAE,cAAc,GAAG,IAAI,EAC/B,SAAS,EAAE,MAAM,OAAO;IAQlC,KAAK,IAAI,IAAI;IA+Bb,IAAI,IAAI,IAAI;IAWN,KAAK,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,EAAE,SAAS,CAAC,EAAE,iBAAiB,EAAE,EAAE,SAAS,CAAC,EAAE,sBAAsB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAgD9G,WAAW;YAmDX,cAAc;YAuDd,cAAc;YAiEd,gBAAgB;IAgD9B,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,oBAAoB;IAiB5B,OAAO,CAAC,oBAAoB;YAmBd,sBAAsB;IAgCpC,OAAO,CAAC,eAAe;IAiBvB,UAAU,IAAI,gBAAgB,GAAG;QAAE,mBAAmB,EAAE,GAAG,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAA;KAAE;IAW1F,YAAY,IAAI,IAAI;CAarB"}
|
{"version":3,"file":"batch-processor.d.ts","sourceRoot":"","sources":["../../src/telemetry/batch-processor.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,sBAAsB,EAAoB,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAoClI,qBAAa,uBAAuB;IA2BhC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,SAAS;IA3BnB,OAAO,CAAC,UAAU,CAAC,CAAiB;IACpC,OAAO,CAAC,gBAAgB,CAAkB;IAC1C,OAAO,CAAC,mBAAmB,CAAkB;IAC7C,OAAO,CAAC,mBAAmB,CAAkB;IAC7C,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,OAAO,CAQb;IACF,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,eAAe,CAAuE;IAC9F,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IAEzC,OAAO,CAAC,cAAc,CAIf;IACP,OAAO,CAAC,OAAO,CAAkB;gBAGvB,QAAQ,EAAE,cAAc,GAAG,IAAI,EAC/B,SAAS,EAAE,MAAM,OAAO;IAQlC,KAAK,IAAI,IAAI;IA0Cb,IAAI,IAAI,IAAI;IAyBN,KAAK,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,EAAE,SAAS,CAAC,EAAE,iBAAiB,EAAE,EAAE,SAAS,CAAC,EAAE,sBAAsB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAgD9G,WAAW;YAmDX,cAAc;YAuDd,cAAc;YAiEd,gBAAgB;IAgD9B,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,oBAAoB;IAiB5B,OAAO,CAAC,oBAAoB;YAmBd,sBAAsB;IAgCpC,OAAO,CAAC,eAAe;IAiBvB,UAAU,IAAI,gBAAgB,GAAG;QAAE,mBAAmB,EAAE,GAAG,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAA;KAAE;IAW1F,YAAY,IAAI,IAAI;CAarB"}
|
||||||
31
dist/telemetry/batch-processor.js
vendored
31
dist/telemetry/batch-processor.js
vendored
@@ -33,26 +33,36 @@ class TelemetryBatchProcessor {
|
|||||||
this.flushTimes = [];
|
this.flushTimes = [];
|
||||||
this.deadLetterQueue = [];
|
this.deadLetterQueue = [];
|
||||||
this.maxDeadLetterSize = 100;
|
this.maxDeadLetterSize = 100;
|
||||||
|
this.eventListeners = {};
|
||||||
|
this.started = false;
|
||||||
this.circuitBreaker = new telemetry_error_1.TelemetryCircuitBreaker();
|
this.circuitBreaker = new telemetry_error_1.TelemetryCircuitBreaker();
|
||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
if (!this.isEnabled() || !this.supabase)
|
if (!this.isEnabled() || !this.supabase)
|
||||||
return;
|
return;
|
||||||
|
if (this.started) {
|
||||||
|
logger_1.logger.debug('Telemetry batch processor already started, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.flushTimer = setInterval(() => {
|
this.flushTimer = setInterval(() => {
|
||||||
this.flush();
|
this.flush();
|
||||||
}, telemetry_types_1.TELEMETRY_CONFIG.BATCH_FLUSH_INTERVAL);
|
}, telemetry_types_1.TELEMETRY_CONFIG.BATCH_FLUSH_INTERVAL);
|
||||||
if (typeof this.flushTimer === 'object' && 'unref' in this.flushTimer) {
|
if (typeof this.flushTimer === 'object' && 'unref' in this.flushTimer) {
|
||||||
this.flushTimer.unref();
|
this.flushTimer.unref();
|
||||||
}
|
}
|
||||||
process.on('beforeExit', () => this.flush());
|
this.eventListeners.beforeExit = () => this.flush();
|
||||||
process.on('SIGINT', () => {
|
this.eventListeners.sigint = () => {
|
||||||
this.flush();
|
this.flush();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
};
|
||||||
process.on('SIGTERM', () => {
|
this.eventListeners.sigterm = () => {
|
||||||
this.flush();
|
this.flush();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
};
|
||||||
|
process.on('beforeExit', this.eventListeners.beforeExit);
|
||||||
|
process.on('SIGINT', this.eventListeners.sigint);
|
||||||
|
process.on('SIGTERM', this.eventListeners.sigterm);
|
||||||
|
this.started = true;
|
||||||
logger_1.logger.debug('Telemetry batch processor started');
|
logger_1.logger.debug('Telemetry batch processor started');
|
||||||
}
|
}
|
||||||
stop() {
|
stop() {
|
||||||
@@ -60,6 +70,17 @@ class TelemetryBatchProcessor {
|
|||||||
clearInterval(this.flushTimer);
|
clearInterval(this.flushTimer);
|
||||||
this.flushTimer = undefined;
|
this.flushTimer = undefined;
|
||||||
}
|
}
|
||||||
|
if (this.eventListeners.beforeExit) {
|
||||||
|
process.removeListener('beforeExit', this.eventListeners.beforeExit);
|
||||||
|
}
|
||||||
|
if (this.eventListeners.sigint) {
|
||||||
|
process.removeListener('SIGINT', this.eventListeners.sigint);
|
||||||
|
}
|
||||||
|
if (this.eventListeners.sigterm) {
|
||||||
|
process.removeListener('SIGTERM', this.eventListeners.sigterm);
|
||||||
|
}
|
||||||
|
this.eventListeners = {};
|
||||||
|
this.started = false;
|
||||||
logger_1.logger.debug('Telemetry batch processor stopped');
|
logger_1.logger.debug('Telemetry batch processor stopped');
|
||||||
}
|
}
|
||||||
async flush(events, workflows, mutations) {
|
async flush(events, workflows, mutations) {
|
||||||
|
|||||||
2
dist/telemetry/batch-processor.js.map
vendored
2
dist/telemetry/batch-processor.js.map
vendored
File diff suppressed because one or more lines are too long
5
dist/types/index.d.ts
vendored
5
dist/types/index.d.ts
vendored
@@ -30,6 +30,11 @@ export interface ToolDefinition {
|
|||||||
additionalProperties?: boolean | Record<string, any>;
|
additionalProperties?: boolean | Record<string, any>;
|
||||||
};
|
};
|
||||||
annotations?: ToolAnnotations;
|
annotations?: ToolAnnotations;
|
||||||
|
_meta?: {
|
||||||
|
ui?: {
|
||||||
|
resourceUri?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export interface ResourceDefinition {
|
export interface ResourceDefinition {
|
||||||
uri: string;
|
uri: string;
|
||||||
|
|||||||
2
dist/types/index.d.ts.map
vendored
2
dist/types/index.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAEhC,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,MAAM,WAAW,eAAe;IAE9B,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE;QACX,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAChC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,oBAAoB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;KACtD,CAAC;IACF,YAAY,CAAC,EAAE;QACb,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAChC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,oBAAoB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;KACtD,CAAC;IAEF,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC,CAAC;CACJ"}
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAEhC,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,MAAM,WAAW,eAAe;IAE9B,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE;QACX,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAChC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,oBAAoB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;KACtD,CAAC;IACF,YAAY,CAAC,EAAE;QACb,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAChC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,oBAAoB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;KACtD,CAAC;IAEF,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,KAAK,CAAC,EAAE;QACN,EAAE,CAAC,EAAE;YACH,WAAW,CAAC,EAAE,MAAM,CAAC;SACtB,CAAC;KACH,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC,CAAC;CACJ"}
|
||||||
1652
docs/CHANGELOG.md
1652
docs/CHANGELOG.md
File diff suppressed because it is too large
Load Diff
18757
package-lock.json
generated
18757
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-mcp",
|
"name": "n8n-mcp",
|
||||||
"version": "2.33.0",
|
"version": "2.37.1",
|
||||||
"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",
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.build.json",
|
"build": "tsc -p tsconfig.build.json",
|
||||||
|
"build:ui": "cd ui-apps && npm install && npm run build",
|
||||||
|
"build:all": "npm run build:ui && npm run build",
|
||||||
"rebuild": "node dist/scripts/rebuild.js",
|
"rebuild": "node dist/scripts/rebuild.js",
|
||||||
"rebuild:optimized": "node dist/scripts/rebuild-optimized.js",
|
"rebuild:optimized": "node dist/scripts/rebuild-optimized.js",
|
||||||
"validate": "node dist/scripts/validate.js",
|
"validate": "node dist/scripts/validate.js",
|
||||||
@@ -123,6 +125,7 @@
|
|||||||
"homepage": "https://github.com/czlonkowski/n8n-mcp#readme",
|
"homepage": "https://github.com/czlonkowski/n8n-mcp#readme",
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
|
"ui-apps/dist/**/*",
|
||||||
"data/nodes.db",
|
"data/nodes.db",
|
||||||
".env.example",
|
".env.example",
|
||||||
"README.md",
|
"README.md",
|
||||||
@@ -149,17 +152,17 @@
|
|||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "1.20.1",
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||||
"@n8n/n8n-nodes-langchain": "^2.2.2",
|
"@n8n/n8n-nodes-langchain": "^2.11.2",
|
||||||
"@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",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
"lru-cache": "^11.2.1",
|
"lru-cache": "^11.2.1",
|
||||||
"n8n": "^2.2.3",
|
"n8n": "^2.11.4",
|
||||||
"n8n-core": "^2.2.2",
|
"n8n-core": "^2.11.1",
|
||||||
"n8n-workflow": "^2.2.2",
|
"n8n-workflow": "^2.11.1",
|
||||||
"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",
|
||||||
@@ -172,6 +175,7 @@
|
|||||||
"better-sqlite3": "^11.10.0"
|
"better-sqlite3": "^11.10.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"pyodide": "0.26.4"
|
"pyodide": "0.26.4",
|
||||||
|
"isolated-vm": "npm:empty-npm-package@1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-mcp-runtime",
|
"name": "n8n-mcp-runtime",
|
||||||
"version": "2.29.5",
|
"version": "2.33.2",
|
||||||
"description": "n8n MCP Server Runtime Dependencies Only",
|
"description": "n8n MCP Server Runtime Dependencies Only",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -57,12 +57,14 @@ export interface DocumentationGeneratorConfig {
|
|||||||
timeout?: number;
|
timeout?: number;
|
||||||
/** Max tokens for response (default: 2000) */
|
/** Max tokens for response (default: 2000) */
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
|
/** Temperature for generation (default: 0.3, set to undefined to omit) */
|
||||||
|
temperature?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default configuration
|
* Default configuration
|
||||||
*/
|
*/
|
||||||
const DEFAULT_CONFIG: Required<Omit<DocumentationGeneratorConfig, 'baseUrl'>> = {
|
const DEFAULT_CONFIG: Required<Omit<DocumentationGeneratorConfig, 'baseUrl' | 'temperature'>> = {
|
||||||
model: 'qwen3-4b-thinking-2507',
|
model: 'qwen3-4b-thinking-2507',
|
||||||
apiKey: 'not-needed',
|
apiKey: 'not-needed',
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
@@ -78,6 +80,7 @@ export class DocumentationGenerator {
|
|||||||
private model: string;
|
private model: string;
|
||||||
private maxTokens: number;
|
private maxTokens: number;
|
||||||
private timeout: number;
|
private timeout: number;
|
||||||
|
private temperature?: number;
|
||||||
|
|
||||||
constructor(config: DocumentationGeneratorConfig) {
|
constructor(config: DocumentationGeneratorConfig) {
|
||||||
const fullConfig = { ...DEFAULT_CONFIG, ...config };
|
const fullConfig = { ...DEFAULT_CONFIG, ...config };
|
||||||
@@ -90,6 +93,7 @@ export class DocumentationGenerator {
|
|||||||
this.model = fullConfig.model;
|
this.model = fullConfig.model;
|
||||||
this.maxTokens = fullConfig.maxTokens;
|
this.maxTokens = fullConfig.maxTokens;
|
||||||
this.timeout = fullConfig.timeout;
|
this.timeout = fullConfig.timeout;
|
||||||
|
this.temperature = fullConfig.temperature;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,8 +105,8 @@ export class DocumentationGenerator {
|
|||||||
|
|
||||||
const completion = await this.client.chat.completions.create({
|
const completion = await this.client.chat.completions.create({
|
||||||
model: this.model,
|
model: this.model,
|
||||||
max_tokens: this.maxTokens,
|
max_completion_tokens: this.maxTokens,
|
||||||
temperature: 0.3, // Lower temperature for more consistent output
|
...(this.temperature !== undefined ? { temperature: this.temperature } : {}),
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
@@ -321,7 +325,7 @@ Guidelines:
|
|||||||
try {
|
try {
|
||||||
const completion = await this.client.chat.completions.create({
|
const completion = await this.client.chat.completions.create({
|
||||||
model: this.model,
|
model: this.model,
|
||||||
max_tokens: 10,
|
max_completion_tokens: 200,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@@ -353,10 +357,15 @@ export function createDocumentationGenerator(): DocumentationGenerator {
|
|||||||
const baseUrl = process.env.N8N_MCP_LLM_BASE_URL || 'http://localhost:1234/v1';
|
const baseUrl = process.env.N8N_MCP_LLM_BASE_URL || 'http://localhost:1234/v1';
|
||||||
const model = process.env.N8N_MCP_LLM_MODEL || 'qwen3-4b-thinking-2507';
|
const model = process.env.N8N_MCP_LLM_MODEL || 'qwen3-4b-thinking-2507';
|
||||||
const timeout = parseInt(process.env.N8N_MCP_LLM_TIMEOUT || '60000', 10);
|
const timeout = parseInt(process.env.N8N_MCP_LLM_TIMEOUT || '60000', 10);
|
||||||
|
const apiKey = process.env.N8N_MCP_LLM_API_KEY || process.env.OPENAI_API_KEY;
|
||||||
|
// Only set temperature for local LLM servers; cloud APIs like OpenAI may not support custom values
|
||||||
|
const isLocalServer = !baseUrl.includes('openai.com') && !baseUrl.includes('anthropic.com');
|
||||||
|
|
||||||
return new DocumentationGenerator({
|
return new DocumentationGenerator({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
model,
|
model,
|
||||||
timeout,
|
timeout,
|
||||||
|
...(apiKey ? { apiKey } : {}),
|
||||||
|
...(isLocalServer ? { temperature: 0.3 } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* These structures define the expected data format, JavaScript type,
|
* These structures define the expected data format, JavaScript type,
|
||||||
* validation rules, and examples for each property type.
|
* validation rules, and examples for each property type.
|
||||||
*
|
*
|
||||||
* Based on n8n-workflow v1.120.3 NodePropertyTypes
|
* Based on n8n-workflow v2.4.2 NodePropertyTypes
|
||||||
*
|
*
|
||||||
* @module constants/type-structures
|
* @module constants/type-structures
|
||||||
* @since 2.23.0
|
* @since 2.23.0
|
||||||
@@ -15,7 +15,7 @@ import type { NodePropertyTypes } from 'n8n-workflow';
|
|||||||
import type { TypeStructure } from '../types/type-structures';
|
import type { TypeStructure } from '../types/type-structures';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete type structure definitions for all 22 NodePropertyTypes
|
* Complete type structure definitions for all 23 NodePropertyTypes
|
||||||
*
|
*
|
||||||
* Each entry defines:
|
* Each entry defines:
|
||||||
* - type: Category (primitive/object/collection/special)
|
* - type: Category (primitive/object/collection/special)
|
||||||
@@ -620,6 +620,23 @@ export const TYPE_STRUCTURES: Record<NodePropertyTypes, TypeStructure> = {
|
|||||||
'One-time import feature',
|
'One-time import feature',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
icon: {
|
||||||
|
type: 'primitive',
|
||||||
|
jsType: 'string',
|
||||||
|
description: 'Icon identifier for visual representation',
|
||||||
|
example: 'fa:envelope',
|
||||||
|
examples: ['fa:envelope', 'fa:user', 'fa:cog', 'file:slack.svg'],
|
||||||
|
validation: {
|
||||||
|
allowEmpty: false,
|
||||||
|
allowExpressions: false,
|
||||||
|
},
|
||||||
|
notes: [
|
||||||
|
'References icon by name or file path',
|
||||||
|
'Supports Font Awesome icons (fa:) and file paths (file:)',
|
||||||
|
'Used for visual customization in UI',
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -419,12 +419,36 @@ class BetterSQLiteStatement implements PreparedStatement {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Statement wrapper for sql.js
|
* Statement wrapper for sql.js
|
||||||
|
*
|
||||||
|
* IMPORTANT: sql.js requires explicit memory management via Statement.free().
|
||||||
|
* This wrapper automatically frees statement memory after each operation
|
||||||
|
* to prevent memory leaks during sustained traffic.
|
||||||
|
*
|
||||||
|
* See: https://sql.js.org/documentation/Statement.html
|
||||||
|
* "After calling db.prepare() you must manually free the assigned memory
|
||||||
|
* by calling Statement.free()."
|
||||||
*/
|
*/
|
||||||
class SQLJSStatement implements PreparedStatement {
|
class SQLJSStatement implements PreparedStatement {
|
||||||
private boundParams: any = null;
|
private boundParams: any = null;
|
||||||
|
private freed: boolean = false;
|
||||||
|
|
||||||
constructor(private stmt: any, private onModify: () => void) {}
|
constructor(private stmt: any, private onModify: () => void) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Free the underlying sql.js statement memory.
|
||||||
|
* Safe to call multiple times - subsequent calls are no-ops.
|
||||||
|
*/
|
||||||
|
private freeStatement(): void {
|
||||||
|
if (!this.freed && this.stmt) {
|
||||||
|
try {
|
||||||
|
this.stmt.free();
|
||||||
|
this.freed = true;
|
||||||
|
} catch (e) {
|
||||||
|
// Statement may already be freed or invalid - ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
run(...params: any[]): RunResult {
|
run(...params: any[]): RunResult {
|
||||||
try {
|
try {
|
||||||
if (params.length > 0) {
|
if (params.length > 0) {
|
||||||
@@ -433,10 +457,10 @@ class SQLJSStatement implements PreparedStatement {
|
|||||||
this.stmt.bind(this.boundParams);
|
this.stmt.bind(this.boundParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stmt.run();
|
this.stmt.run();
|
||||||
this.onModify();
|
this.onModify();
|
||||||
|
|
||||||
// sql.js doesn't provide changes/lastInsertRowid easily
|
// sql.js doesn't provide changes/lastInsertRowid easily
|
||||||
return {
|
return {
|
||||||
changes: 1, // Assume success means 1 change
|
changes: 1, // Assume success means 1 change
|
||||||
@@ -445,9 +469,12 @@ class SQLJSStatement implements PreparedStatement {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.stmt.reset();
|
this.stmt.reset();
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Free statement memory after write operation completes
|
||||||
|
this.freeStatement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get(...params: any[]): any {
|
get(...params: any[]): any {
|
||||||
try {
|
try {
|
||||||
if (params.length > 0) {
|
if (params.length > 0) {
|
||||||
@@ -456,21 +483,24 @@ class SQLJSStatement implements PreparedStatement {
|
|||||||
this.stmt.bind(this.boundParams);
|
this.stmt.bind(this.boundParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.stmt.step()) {
|
if (this.stmt.step()) {
|
||||||
const result = this.stmt.getAsObject();
|
const result = this.stmt.getAsObject();
|
||||||
this.stmt.reset();
|
this.stmt.reset();
|
||||||
return this.convertIntegerColumns(result);
|
return this.convertIntegerColumns(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stmt.reset();
|
this.stmt.reset();
|
||||||
return undefined;
|
return undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.stmt.reset();
|
this.stmt.reset();
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Free statement memory after read operation completes
|
||||||
|
this.freeStatement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
all(...params: any[]): any[] {
|
all(...params: any[]): any[] {
|
||||||
try {
|
try {
|
||||||
if (params.length > 0) {
|
if (params.length > 0) {
|
||||||
@@ -479,17 +509,20 @@ class SQLJSStatement implements PreparedStatement {
|
|||||||
this.stmt.bind(this.boundParams);
|
this.stmt.bind(this.boundParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const results: any[] = [];
|
const results: any[] = [];
|
||||||
while (this.stmt.step()) {
|
while (this.stmt.step()) {
|
||||||
results.push(this.convertIntegerColumns(this.stmt.getAsObject()));
|
results.push(this.convertIntegerColumns(this.stmt.getAsObject()));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stmt.reset();
|
this.stmt.reset();
|
||||||
return results;
|
return results;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.stmt.reset();
|
this.stmt.reset();
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Free statement memory after read operation completes
|
||||||
|
this.freeStatement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
203
src/database/shared-database.ts
Normal file
203
src/database/shared-database.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* Shared Database Manager - Singleton for cross-session database connection
|
||||||
|
*
|
||||||
|
* This module implements a singleton pattern to share a single database connection
|
||||||
|
* across all MCP server sessions. This prevents memory leaks caused by each session
|
||||||
|
* creating its own database connection (~900MB per session).
|
||||||
|
*
|
||||||
|
* Memory impact: Reduces per-session memory from ~900MB to near-zero by sharing
|
||||||
|
* a single ~68MB database connection across all sessions.
|
||||||
|
*
|
||||||
|
* Issue: https://github.com/czlonkowski/n8n-mcp/issues/XXX
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import { DatabaseAdapter, createDatabaseAdapter } from './database-adapter';
|
||||||
|
import { NodeRepository } from './node-repository';
|
||||||
|
import { TemplateService } from '../templates/template-service';
|
||||||
|
import { EnhancedConfigValidator } from '../services/enhanced-config-validator';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared database state - holds the singleton connection and services
|
||||||
|
*/
|
||||||
|
export interface SharedDatabaseState {
|
||||||
|
db: DatabaseAdapter;
|
||||||
|
repository: NodeRepository;
|
||||||
|
templateService: TemplateService;
|
||||||
|
dbPath: string;
|
||||||
|
refCount: number;
|
||||||
|
initialized: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module-level singleton state
|
||||||
|
let sharedState: SharedDatabaseState | null = null;
|
||||||
|
let initializationPromise: Promise<SharedDatabaseState> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create the shared database connection
|
||||||
|
*
|
||||||
|
* Thread-safe initialization using a promise lock pattern.
|
||||||
|
* Multiple concurrent calls will wait for the same initialization.
|
||||||
|
*
|
||||||
|
* @param dbPath - Path to the SQLite database file
|
||||||
|
* @returns Shared database state with connection and services
|
||||||
|
*/
|
||||||
|
export async function getSharedDatabase(dbPath: string): Promise<SharedDatabaseState> {
|
||||||
|
// Normalize to a canonical absolute path so that callers using different
|
||||||
|
// relative or join-based paths (e.g. "./data/nodes.db" vs an absolute path)
|
||||||
|
// resolve to the same string and do not trigger a false "different path" error.
|
||||||
|
const normalizedPath = dbPath === ':memory:' ? dbPath : path.resolve(dbPath);
|
||||||
|
|
||||||
|
// If already initialized with the same path, increment ref count and return
|
||||||
|
if (sharedState && sharedState.initialized && sharedState.dbPath === normalizedPath) {
|
||||||
|
sharedState.refCount++;
|
||||||
|
logger.debug('Reusing shared database connection', {
|
||||||
|
refCount: sharedState.refCount,
|
||||||
|
dbPath: normalizedPath
|
||||||
|
});
|
||||||
|
return sharedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If already initialized with a DIFFERENT path, this is a configuration error
|
||||||
|
if (sharedState && sharedState.initialized && sharedState.dbPath !== normalizedPath) {
|
||||||
|
logger.error('Attempted to initialize shared database with different path', {
|
||||||
|
existingPath: sharedState.dbPath,
|
||||||
|
requestedPath: normalizedPath
|
||||||
|
});
|
||||||
|
throw new Error(`Shared database already initialized with different path: ${sharedState.dbPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If initialization is in progress, wait for it
|
||||||
|
if (initializationPromise) {
|
||||||
|
try {
|
||||||
|
const state = await initializationPromise;
|
||||||
|
state.refCount++;
|
||||||
|
logger.debug('Reusing shared database (waited for init)', {
|
||||||
|
refCount: state.refCount,
|
||||||
|
dbPath: normalizedPath
|
||||||
|
});
|
||||||
|
return state;
|
||||||
|
} catch (error) {
|
||||||
|
// Initialization failed while we were waiting, clear promise and rethrow
|
||||||
|
initializationPromise = null;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new initialization
|
||||||
|
initializationPromise = initializeSharedDatabase(normalizedPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const state = await initializationPromise;
|
||||||
|
// Clear the promise on success to allow future re-initialization after close
|
||||||
|
initializationPromise = null;
|
||||||
|
return state;
|
||||||
|
} catch (error) {
|
||||||
|
// Clear promise on failure to allow retry
|
||||||
|
initializationPromise = null;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the shared database connection and services
|
||||||
|
*/
|
||||||
|
async function initializeSharedDatabase(dbPath: string): Promise<SharedDatabaseState> {
|
||||||
|
logger.info('Initializing shared database connection', { dbPath });
|
||||||
|
|
||||||
|
const db = await createDatabaseAdapter(dbPath);
|
||||||
|
const repository = new NodeRepository(db);
|
||||||
|
const templateService = new TemplateService(db);
|
||||||
|
|
||||||
|
// Initialize similarity services for enhanced validation
|
||||||
|
EnhancedConfigValidator.initializeSimilarityServices(repository);
|
||||||
|
|
||||||
|
sharedState = {
|
||||||
|
db,
|
||||||
|
repository,
|
||||||
|
templateService,
|
||||||
|
dbPath,
|
||||||
|
refCount: 1,
|
||||||
|
initialized: true
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('Shared database initialized successfully', {
|
||||||
|
dbPath,
|
||||||
|
refCount: sharedState.refCount
|
||||||
|
});
|
||||||
|
|
||||||
|
return sharedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release a reference to the shared database
|
||||||
|
*
|
||||||
|
* Decrements the reference count. Does NOT close the database
|
||||||
|
* as it's shared across all sessions for the lifetime of the process.
|
||||||
|
*
|
||||||
|
* @param state - The shared database state to release
|
||||||
|
*/
|
||||||
|
export function releaseSharedDatabase(state: SharedDatabaseState): void {
|
||||||
|
if (!state || !sharedState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard against double-release (refCount going negative)
|
||||||
|
if (sharedState.refCount <= 0) {
|
||||||
|
logger.warn('Attempted to release shared database with refCount already at or below 0', {
|
||||||
|
refCount: sharedState.refCount
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedState.refCount--;
|
||||||
|
logger.debug('Released shared database reference', {
|
||||||
|
refCount: sharedState.refCount
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: We intentionally do NOT close the database even when refCount hits 0
|
||||||
|
// The database should remain open for the lifetime of the process to handle
|
||||||
|
// new sessions. Only process shutdown should close it.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force close the shared database (for graceful shutdown only)
|
||||||
|
*
|
||||||
|
* This should only be called during process shutdown, not during normal
|
||||||
|
* session cleanup. Closing the database would break other active sessions.
|
||||||
|
*/
|
||||||
|
export async function closeSharedDatabase(): Promise<void> {
|
||||||
|
if (!sharedState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Closing shared database connection', {
|
||||||
|
refCount: sharedState.refCount
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
sharedState.db.close();
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error closing shared database', {
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedState = null;
|
||||||
|
initializationPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if shared database is initialized
|
||||||
|
*/
|
||||||
|
export function isSharedDatabaseInitialized(): boolean {
|
||||||
|
return sharedState !== null && sharedState.initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current reference count (for debugging/monitoring)
|
||||||
|
*/
|
||||||
|
export function getSharedDatabaseRefCount(): number {
|
||||||
|
return sharedState?.refCount ?? 0;
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
} from './utils/protocol-version';
|
} from './utils/protocol-version';
|
||||||
import { InstanceContext, validateInstanceContext } from './types/instance-context';
|
import { InstanceContext, validateInstanceContext } from './types/instance-context';
|
||||||
import { SessionState } from './types/session-state';
|
import { SessionState } from './types/session-state';
|
||||||
|
import { closeSharedDatabase } from './database/shared-database';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@@ -106,7 +107,12 @@ export class SingleSessionHTTPServer {
|
|||||||
private session: Session | null = null; // Keep for SSE compatibility
|
private session: Session | null = null; // Keep for SSE compatibility
|
||||||
private consoleManager = new ConsoleManager();
|
private consoleManager = new ConsoleManager();
|
||||||
private expressServer: any;
|
private expressServer: any;
|
||||||
private sessionTimeout = 30 * 60 * 1000; // 30 minutes
|
// Session timeout reduced from 30 minutes to 5 minutes for faster cleanup
|
||||||
|
// Configurable via SESSION_TIMEOUT_MINUTES environment variable
|
||||||
|
// This prevents memory buildup from stale sessions
|
||||||
|
private sessionTimeout = parseInt(
|
||||||
|
process.env.SESSION_TIMEOUT_MINUTES || '5', 10
|
||||||
|
) * 60 * 1000;
|
||||||
private authToken: string | null = null;
|
private authToken: string | null = null;
|
||||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
@@ -492,6 +498,29 @@ export class SingleSessionHTTPServer {
|
|||||||
// For initialize requests: always create new transport and server
|
// For initialize requests: always create new transport and server
|
||||||
logger.info('handleRequest: Creating new transport for initialize request');
|
logger.info('handleRequest: Creating new transport for initialize request');
|
||||||
|
|
||||||
|
// EAGER CLEANUP: Remove existing sessions for the same instance
|
||||||
|
// This prevents memory buildup when clients reconnect without proper cleanup
|
||||||
|
if (instanceContext?.instanceId) {
|
||||||
|
const sessionsToRemove: string[] = [];
|
||||||
|
for (const [existingSessionId, context] of Object.entries(this.sessionContexts)) {
|
||||||
|
if (context?.instanceId === instanceContext.instanceId) {
|
||||||
|
sessionsToRemove.push(existingSessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const oldSessionId of sessionsToRemove) {
|
||||||
|
// Double-check session still exists (may have been cleaned by concurrent request)
|
||||||
|
if (!this.transports[oldSessionId]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logger.info('Cleaning up previous session for instance', {
|
||||||
|
instanceId: instanceContext.instanceId,
|
||||||
|
oldSession: oldSessionId,
|
||||||
|
reason: 'instance_reconnect'
|
||||||
|
});
|
||||||
|
await this.removeSession(oldSessionId, 'instance_reconnect');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate session ID based on multi-tenant configuration
|
// Generate session ID based on multi-tenant configuration
|
||||||
let sessionIdToUse: string;
|
let sessionIdToUse: string;
|
||||||
|
|
||||||
@@ -677,11 +706,25 @@ export class SingleSessionHTTPServer {
|
|||||||
private async resetSessionSSE(res: express.Response): Promise<void> {
|
private async resetSessionSSE(res: express.Response): Promise<void> {
|
||||||
// Clean up old session if exists
|
// Clean up old session if exists
|
||||||
if (this.session) {
|
if (this.session) {
|
||||||
|
const sessionId = this.session.sessionId;
|
||||||
|
logger.info('Closing previous session for SSE', { sessionId });
|
||||||
|
|
||||||
|
// Close server first to free resources (database, cache timer, etc.)
|
||||||
|
// This mirrors the cleanup pattern in removeSession() (issue #542)
|
||||||
|
// Handle server close errors separately so transport close still runs
|
||||||
|
if (this.session.server && typeof this.session.server.close === 'function') {
|
||||||
|
try {
|
||||||
|
await this.session.server.close();
|
||||||
|
} catch (serverError) {
|
||||||
|
logger.warn('Error closing server for SSE session', { sessionId, error: serverError });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close transport last - always attempt even if server.close() failed
|
||||||
try {
|
try {
|
||||||
logger.info('Closing previous session for SSE', { sessionId: this.session.sessionId });
|
|
||||||
await this.session.transport.close();
|
await this.session.transport.close();
|
||||||
} catch (error) {
|
} catch (transportError) {
|
||||||
logger.warn('Error closing previous session:', error);
|
logger.warn('Error closing transport for SSE session', { sessionId, error: transportError });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1408,7 +1451,16 @@ export class SingleSessionHTTPServer {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the shared database connection (only during process shutdown)
|
||||||
|
// This must happen after all sessions are closed
|
||||||
|
try {
|
||||||
|
await closeSharedDatabase();
|
||||||
|
logger.info('Shared database closed');
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error closing shared database:', error);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('Single-Session HTTP server shutdown completed');
|
logger.info('Single-Session HTTP server shutdown completed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export type {
|
|||||||
SessionState
|
SessionState
|
||||||
} from './types/session-state';
|
} from './types/session-state';
|
||||||
|
|
||||||
|
// UI module exports
|
||||||
|
export type { UIAppConfig, UIMetadata } from './mcp/ui/types';
|
||||||
|
export { UI_APP_CONFIGS } from './mcp/ui/app-configs';
|
||||||
|
|
||||||
// Re-export MCP SDK types for convenience
|
// Re-export MCP SDK types for convenience
|
||||||
export type {
|
export type {
|
||||||
Tool,
|
Tool,
|
||||||
|
|||||||
@@ -31,24 +31,44 @@ export class N8nNodeLoader {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the absolute directory of an installed package.
|
||||||
|
* Uses require.resolve on package.json (always exported) and strips the filename.
|
||||||
|
*/
|
||||||
|
private resolvePackageDir(packagePath: string): string {
|
||||||
|
const pkgJsonPath = require.resolve(`${packagePath}/package.json`);
|
||||||
|
return path.dirname(pkgJsonPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a node module by absolute file path, bypassing package.json "exports".
|
||||||
|
* Some packages (e.g. @n8n/n8n-nodes-langchain >=2.9) restrict exports but
|
||||||
|
* still list node files in the n8n.nodes array — we need direct filesystem access.
|
||||||
|
*/
|
||||||
|
private loadNodeModule(absolutePath: string): any {
|
||||||
|
return require(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
private async loadPackageNodes(packageName: string, packagePath: string, packageJson: any): Promise<LoadedNode[]> {
|
private async loadPackageNodes(packageName: string, packagePath: string, packageJson: any): Promise<LoadedNode[]> {
|
||||||
const n8nConfig = packageJson.n8n || {};
|
const n8nConfig = packageJson.n8n || {};
|
||||||
const nodes: LoadedNode[] = [];
|
const nodes: LoadedNode[] = [];
|
||||||
|
const packageDir = this.resolvePackageDir(packagePath);
|
||||||
|
|
||||||
// Check if nodes is an array or object
|
// Check if nodes is an array or object
|
||||||
const nodesList = n8nConfig.nodes || [];
|
const nodesList = n8nConfig.nodes || [];
|
||||||
|
|
||||||
if (Array.isArray(nodesList)) {
|
if (Array.isArray(nodesList)) {
|
||||||
// Handle array format (n8n-nodes-base uses this)
|
// Handle array format (n8n-nodes-base uses this)
|
||||||
for (const nodePath of nodesList) {
|
for (const nodePath of nodesList) {
|
||||||
try {
|
try {
|
||||||
const fullPath = require.resolve(`${packagePath}/${nodePath}`);
|
// Resolve absolute path directly to bypass package exports restrictions
|
||||||
const nodeModule = require(fullPath);
|
const fullPath = path.join(packageDir, nodePath);
|
||||||
|
const nodeModule = this.loadNodeModule(fullPath);
|
||||||
|
|
||||||
// Extract node name from path (e.g., "dist/nodes/Slack/Slack.node.js" -> "Slack")
|
// Extract node name from path (e.g., "dist/nodes/Slack/Slack.node.js" -> "Slack")
|
||||||
const nodeNameMatch = nodePath.match(/\/([^\/]+)\.node\.(js|ts)$/);
|
const nodeNameMatch = nodePath.match(/\/([^\/]+)\.node\.(js|ts)$/);
|
||||||
const nodeName = nodeNameMatch ? nodeNameMatch[1] : path.basename(nodePath, '.node.js');
|
const nodeName = nodeNameMatch ? nodeNameMatch[1] : path.basename(nodePath, '.node.js');
|
||||||
|
|
||||||
// Handle default export and various export patterns
|
// Handle default export and various export patterns
|
||||||
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
|
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
|
||||||
if (NodeClass) {
|
if (NodeClass) {
|
||||||
@@ -65,9 +85,9 @@ export class N8nNodeLoader {
|
|||||||
// Handle object format (for other packages)
|
// Handle object format (for other packages)
|
||||||
for (const [nodeName, nodePath] of Object.entries(nodesList)) {
|
for (const [nodeName, nodePath] of Object.entries(nodesList)) {
|
||||||
try {
|
try {
|
||||||
const fullPath = require.resolve(`${packagePath}/${nodePath as string}`);
|
const fullPath = path.join(packageDir, nodePath as string);
|
||||||
const nodeModule = require(fullPath);
|
const nodeModule = this.loadNodeModule(fullPath);
|
||||||
|
|
||||||
// Handle default export and various export patterns
|
// Handle default export and various export patterns
|
||||||
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
|
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
|
||||||
if (NodeClass) {
|
if (NodeClass) {
|
||||||
@@ -81,7 +101,7 @@ export class N8nNodeLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,7 +424,13 @@ const autofixWorkflowSchema = z.object({
|
|||||||
'node-type-correction',
|
'node-type-correction',
|
||||||
'webhook-missing-path',
|
'webhook-missing-path',
|
||||||
'typeversion-upgrade',
|
'typeversion-upgrade',
|
||||||
'version-migration'
|
'version-migration',
|
||||||
|
'tool-variant-correction',
|
||||||
|
'connection-numeric-keys',
|
||||||
|
'connection-invalid-type',
|
||||||
|
'connection-id-to-name',
|
||||||
|
'connection-duplicate-removal',
|
||||||
|
'connection-input-index'
|
||||||
])).optional(),
|
])).optional(),
|
||||||
confidenceThreshold: z.enum(['high', 'medium', 'low']).optional().default('medium'),
|
confidenceThreshold: z.enum(['high', 'medium', 'low']).optional().default('medium'),
|
||||||
maxFixes: z.number().optional().default(50)
|
maxFixes: z.number().optional().default(50)
|
||||||
@@ -513,6 +519,17 @@ export async function handleCreateWorkflow(args: unknown, context?: InstanceCont
|
|||||||
// Create workflow (n8n API expects node types in FULL form)
|
// Create workflow (n8n API expects node types in FULL form)
|
||||||
const workflow = await client.createWorkflow(input);
|
const workflow = await client.createWorkflow(input);
|
||||||
|
|
||||||
|
// Defensive check: ensure the API returned a valid workflow with an ID
|
||||||
|
if (!workflow || !workflow.id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Workflow creation failed: n8n API returned an empty or invalid response. Verify your N8N_API_URL points to the correct /api/v1 endpoint and that the n8n instance supports workflow creation.',
|
||||||
|
details: {
|
||||||
|
response: workflow ? { keys: Object.keys(workflow) } : null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Track successful workflow creation
|
// Track successful workflow creation
|
||||||
telemetry.trackWorkflowCreation(workflow, true);
|
telemetry.trackWorkflowCreation(workflow, true);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { McpToolResponse } from '../types/n8n-api';
|
import { McpToolResponse } from '../types/n8n-api';
|
||||||
import { WorkflowDiffRequest, WorkflowDiffOperation } from '../types/workflow-diff';
|
import { WorkflowDiffRequest, WorkflowDiffOperation, WorkflowDiffValidationError } from '../types/workflow-diff';
|
||||||
import { WorkflowDiffEngine } from '../services/workflow-diff-engine';
|
import { WorkflowDiffEngine } from '../services/workflow-diff-engine';
|
||||||
import { getN8nApiClient } from './handlers-n8n-manager';
|
import { getN8nApiClient } from './handlers-n8n-manager';
|
||||||
import { N8nApiError, getUserFriendlyErrorMessage } from '../utils/n8n-errors';
|
import { N8nApiError, getUserFriendlyErrorMessage } from '../utils/n8n-errors';
|
||||||
@@ -48,8 +48,8 @@ const workflowDiffSchema = z.object({
|
|||||||
target: z.string().optional(),
|
target: z.string().optional(),
|
||||||
from: z.string().optional(), // For rewireConnection
|
from: z.string().optional(), // For rewireConnection
|
||||||
to: z.string().optional(), // For rewireConnection
|
to: z.string().optional(), // For rewireConnection
|
||||||
sourceOutput: z.string().optional(),
|
sourceOutput: z.union([z.string(), z.number()]).transform(String).optional(),
|
||||||
targetInput: z.string().optional(),
|
targetInput: z.union([z.string(), z.number()]).transform(String).optional(),
|
||||||
sourceIndex: z.number().optional(),
|
sourceIndex: z.number().optional(),
|
||||||
targetIndex: z.number().optional(),
|
targetIndex: z.number().optional(),
|
||||||
// Smart parameters (Phase 1 UX improvement)
|
// Smart parameters (Phase 1 UX improvement)
|
||||||
@@ -178,11 +178,12 @@ export async function handleUpdatePartialWorkflow(
|
|||||||
// Complete failure - return error
|
// Complete failure - return error
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
saved: false,
|
||||||
error: 'Failed to apply diff operations',
|
error: 'Failed to apply diff operations',
|
||||||
|
operationsApplied: diffResult.operationsApplied,
|
||||||
details: {
|
details: {
|
||||||
errors: diffResult.errors,
|
errors: diffResult.errors,
|
||||||
warnings: diffResult.warnings,
|
warnings: diffResult.warnings,
|
||||||
operationsApplied: diffResult.operationsApplied,
|
|
||||||
applied: diffResult.applied,
|
applied: diffResult.applied,
|
||||||
failed: diffResult.failed
|
failed: diffResult.failed
|
||||||
}
|
}
|
||||||
@@ -265,6 +266,7 @@ export async function handleUpdatePartialWorkflow(
|
|||||||
if (!skipValidation) {
|
if (!skipValidation) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
saved: false,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
details: {
|
details: {
|
||||||
errors: structureErrors,
|
errors: structureErrors,
|
||||||
@@ -273,7 +275,7 @@ export async function handleUpdatePartialWorkflow(
|
|||||||
applied: diffResult.applied,
|
applied: diffResult.applied,
|
||||||
recoveryGuidance: recoverySteps,
|
recoveryGuidance: recoverySteps,
|
||||||
note: 'Operations were applied but created an invalid workflow structure. The workflow was NOT saved to n8n to prevent UI rendering errors.',
|
note: 'Operations were applied but created an invalid workflow structure. The workflow was NOT saved to n8n to prevent UI rendering errors.',
|
||||||
autoSanitizationNote: 'Auto-sanitization runs on all nodes during updates to fix operator structures and add missing metadata. However, it cannot fix all issues (e.g., broken connections, branch mismatches). Use the recovery guidance above to resolve remaining issues.'
|
autoSanitizationNote: 'Auto-sanitization runs on modified nodes during updates to fix operator structures and add missing metadata. However, it cannot fix all issues (e.g., broken connections, branch mismatches). Use the recovery guidance above to resolve remaining issues.'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -289,6 +291,63 @@ export async function handleUpdatePartialWorkflow(
|
|||||||
try {
|
try {
|
||||||
const updatedWorkflow = await client.updateWorkflow(input.id, diffResult.workflow!);
|
const updatedWorkflow = await client.updateWorkflow(input.id, diffResult.workflow!);
|
||||||
|
|
||||||
|
// Handle tag operations via dedicated API (#599)
|
||||||
|
let tagWarnings: string[] = [];
|
||||||
|
if (diffResult.tagsToAdd?.length || diffResult.tagsToRemove?.length) {
|
||||||
|
try {
|
||||||
|
// Get existing tags from the updated workflow
|
||||||
|
const existingTags: Array<{ id: string; name: string }> = Array.isArray(updatedWorkflow.tags)
|
||||||
|
? updatedWorkflow.tags.map((t: any) => typeof t === 'object' ? { id: t.id, name: t.name } : { id: '', name: t })
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Resolve tag names to IDs
|
||||||
|
const allTags = await client.listTags();
|
||||||
|
const tagMap = new Map<string, string>();
|
||||||
|
for (const t of allTags.data) {
|
||||||
|
if (t.id) tagMap.set(t.name.toLowerCase(), t.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create any tags that don't exist yet
|
||||||
|
for (const tagName of (diffResult.tagsToAdd || [])) {
|
||||||
|
if (!tagMap.has(tagName.toLowerCase())) {
|
||||||
|
try {
|
||||||
|
const newTag = await client.createTag({ name: tagName });
|
||||||
|
if (newTag.id) tagMap.set(tagName.toLowerCase(), newTag.id);
|
||||||
|
} catch (createErr) {
|
||||||
|
tagWarnings.push(`Failed to create tag "${tagName}": ${createErr instanceof Error ? createErr.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute final tag set — resolve string-type tags via tagMap
|
||||||
|
const currentTagIds = new Set<string>();
|
||||||
|
for (const et of existingTags) {
|
||||||
|
if (et.id) {
|
||||||
|
currentTagIds.add(et.id);
|
||||||
|
} else {
|
||||||
|
const resolved = tagMap.get(et.name.toLowerCase());
|
||||||
|
if (resolved) currentTagIds.add(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tagName of (diffResult.tagsToAdd || [])) {
|
||||||
|
const tagId = tagMap.get(tagName.toLowerCase());
|
||||||
|
if (tagId) currentTagIds.add(tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tagName of (diffResult.tagsToRemove || [])) {
|
||||||
|
const tagId = tagMap.get(tagName.toLowerCase());
|
||||||
|
if (tagId) currentTagIds.delete(tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update workflow tags via dedicated API
|
||||||
|
await client.updateWorkflowTags(input.id, Array.from(currentTagIds));
|
||||||
|
} catch (tagError) {
|
||||||
|
tagWarnings.push(`Tag update failed: ${tagError instanceof Error ? tagError.message : 'Unknown error'}`);
|
||||||
|
logger.warn('Tag operations failed (non-blocking)', tagError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle activation/deactivation if requested
|
// Handle activation/deactivation if requested
|
||||||
let finalWorkflow = updatedWorkflow;
|
let finalWorkflow = updatedWorkflow;
|
||||||
let activationMessage = '';
|
let activationMessage = '';
|
||||||
@@ -319,6 +378,7 @@ export async function handleUpdatePartialWorkflow(
|
|||||||
logger.error('Failed to activate workflow after update', activationError);
|
logger.error('Failed to activate workflow after update', activationError);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
saved: true,
|
||||||
error: 'Workflow updated successfully but activation failed',
|
error: 'Workflow updated successfully but activation failed',
|
||||||
details: {
|
details: {
|
||||||
workflowUpdated: true,
|
workflowUpdated: true,
|
||||||
@@ -334,6 +394,7 @@ export async function handleUpdatePartialWorkflow(
|
|||||||
logger.error('Failed to deactivate workflow after update', deactivationError);
|
logger.error('Failed to deactivate workflow after update', deactivationError);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
saved: true,
|
||||||
error: 'Workflow updated successfully but deactivation failed',
|
error: 'Workflow updated successfully but deactivation failed',
|
||||||
details: {
|
details: {
|
||||||
workflowUpdated: true,
|
workflowUpdated: true,
|
||||||
@@ -363,6 +424,7 @@ export async function handleUpdatePartialWorkflow(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
saved: true,
|
||||||
data: {
|
data: {
|
||||||
id: finalWorkflow.id,
|
id: finalWorkflow.id,
|
||||||
name: finalWorkflow.name,
|
name: finalWorkflow.name,
|
||||||
@@ -375,7 +437,7 @@ export async function handleUpdatePartialWorkflow(
|
|||||||
applied: diffResult.applied,
|
applied: diffResult.applied,
|
||||||
failed: diffResult.failed,
|
failed: diffResult.failed,
|
||||||
errors: diffResult.errors,
|
errors: diffResult.errors,
|
||||||
warnings: diffResult.warnings
|
warnings: mergeWarnings(diffResult.warnings, tagWarnings)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -413,7 +475,9 @@ export async function handleUpdatePartialWorkflow(
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Invalid input',
|
error: 'Invalid input',
|
||||||
details: { errors: error.errors }
|
details: {
|
||||||
|
errors: error.errors.map(e => `${e.path.join('.')}: ${e.message}`)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,6 +489,21 @@ export async function handleUpdatePartialWorkflow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge diff engine warnings with tag operation warnings into a single array.
|
||||||
|
* Returns undefined when there are no warnings to keep the response clean.
|
||||||
|
*/
|
||||||
|
function mergeWarnings(
|
||||||
|
diffWarnings: WorkflowDiffValidationError[] | undefined,
|
||||||
|
tagWarnings: string[]
|
||||||
|
): WorkflowDiffValidationError[] | undefined {
|
||||||
|
const merged: WorkflowDiffValidationError[] = [
|
||||||
|
...(diffWarnings || []),
|
||||||
|
...tagWarnings.map(w => ({ operation: -1, message: w }))
|
||||||
|
];
|
||||||
|
return merged.length > 0 ? merged : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Infer intent from operations when not explicitly provided
|
* Infer intent from operations when not explicitly provided
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
import {
|
import {
|
||||||
CallToolRequestSchema,
|
CallToolRequestSchema,
|
||||||
ListToolsRequestSchema,
|
ListToolsRequestSchema,
|
||||||
InitializeRequestSchema,
|
InitializeRequestSchema,
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
} from '@modelcontextprotocol/sdk/types.js';
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { existsSync, promises as fs } from 'fs';
|
import { existsSync, promises as fs } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { n8nDocumentationToolsFinal } from './tools';
|
import { n8nDocumentationToolsFinal } from './tools';
|
||||||
|
import { UIAppRegistry } from './ui';
|
||||||
import { n8nManagementTools } from './tools-n8n-manager';
|
import { n8nManagementTools } from './tools-n8n-manager';
|
||||||
import { makeToolsN8nFriendly } from './tools-n8n-friendly';
|
import { makeToolsN8nFriendly } from './tools-n8n-friendly';
|
||||||
import { getWorkflowExampleString } from './workflow-examples';
|
import { getWorkflowExampleString } from './workflow-examples';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { NodeRepository } from '../database/node-repository';
|
import { NodeRepository } from '../database/node-repository';
|
||||||
import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter';
|
import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter';
|
||||||
|
import { getSharedDatabase, releaseSharedDatabase, SharedDatabaseState } from '../database/shared-database';
|
||||||
import { PropertyFilter } from '../services/property-filter';
|
import { PropertyFilter } from '../services/property-filter';
|
||||||
import { TaskTemplates } from '../services/task-templates';
|
import { TaskTemplates } from '../services/task-templates';
|
||||||
import { ConfigValidator } from '../services/config-validator';
|
import { ConfigValidator } from '../services/config-validator';
|
||||||
@@ -150,6 +154,9 @@ export class N8NDocumentationMCPServer {
|
|||||||
private previousToolTimestamp: number = Date.now();
|
private previousToolTimestamp: number = Date.now();
|
||||||
private earlyLogger: EarlyErrorLogger | null = null;
|
private earlyLogger: EarlyErrorLogger | null = null;
|
||||||
private disabledToolsCache: Set<string> | null = null;
|
private disabledToolsCache: Set<string> | null = null;
|
||||||
|
private useSharedDatabase: boolean = false; // Track if using shared DB for cleanup
|
||||||
|
private sharedDbState: SharedDatabaseState | null = null; // Reference to shared DB state for release
|
||||||
|
private isShutdown: boolean = false; // Prevent double-shutdown
|
||||||
|
|
||||||
constructor(instanceContext?: InstanceContext, earlyLogger?: EarlyErrorLogger) {
|
constructor(instanceContext?: InstanceContext, earlyLogger?: EarlyErrorLogger) {
|
||||||
this.instanceContext = instanceContext;
|
this.instanceContext = instanceContext;
|
||||||
@@ -203,6 +210,13 @@ export class N8NDocumentationMCPServer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Attach a no-op catch handler to prevent Node.js from flagging this as an
|
||||||
|
// unhandled rejection in the interval between construction and the first
|
||||||
|
// await of this.initialized (via ensureInitialized). This does NOT suppress
|
||||||
|
// the error: the original this.initialized promise still rejects, and
|
||||||
|
// ensureInitialized() will re-throw it when awaited.
|
||||||
|
this.initialized.catch(() => {});
|
||||||
|
|
||||||
logger.info('Initializing n8n Documentation MCP server');
|
logger.info('Initializing n8n Documentation MCP server');
|
||||||
|
|
||||||
this.server = new Server(
|
this.server = new Server(
|
||||||
@@ -231,10 +245,12 @@ export class N8NDocumentationMCPServer {
|
|||||||
{
|
{
|
||||||
capabilities: {
|
capabilities: {
|
||||||
tools: {},
|
tools: {},
|
||||||
|
resources: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
UIAppRegistry.load();
|
||||||
this.setupHandlers();
|
this.setupHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,18 +261,39 @@ export class N8NDocumentationMCPServer {
|
|||||||
* Order of cleanup:
|
* Order of cleanup:
|
||||||
* 1. Close MCP server connection
|
* 1. Close MCP server connection
|
||||||
* 2. Destroy cache (clears entries AND stops cleanup timer)
|
* 2. Destroy cache (clears entries AND stops cleanup timer)
|
||||||
* 3. Close database connection
|
* 3. Release shared database OR close dedicated connection
|
||||||
* 4. Null out references to help GC
|
* 4. Null out references to help GC
|
||||||
|
*
|
||||||
|
* IMPORTANT: For shared databases, we only release the reference (decrement refCount),
|
||||||
|
* NOT close the database. The database stays open for other sessions.
|
||||||
|
* For in-memory databases (tests), we close the dedicated connection.
|
||||||
*/
|
*/
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
|
// Wait for initialization to complete (or fail) before cleanup
|
||||||
|
// This prevents race conditions where close runs while init is in progress
|
||||||
|
try {
|
||||||
|
await this.initialized;
|
||||||
|
} catch (error) {
|
||||||
|
// Initialization failed - that's OK, we still need to clean up
|
||||||
|
logger.debug('Initialization had failed, proceeding with cleanup', {
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.server.close();
|
await this.server.close();
|
||||||
|
|
||||||
// Use destroy() not clear() - also stops the cleanup timer
|
// Use destroy() not clear() - also stops the cleanup timer
|
||||||
this.cache.destroy();
|
this.cache.destroy();
|
||||||
|
|
||||||
// Close database connection before nullifying reference
|
// Handle database cleanup based on whether it's shared or dedicated
|
||||||
if (this.db) {
|
if (this.useSharedDatabase && this.sharedDbState) {
|
||||||
|
// Shared database: release reference, don't close
|
||||||
|
// The database stays open for other sessions
|
||||||
|
releaseSharedDatabase(this.sharedDbState);
|
||||||
|
logger.debug('Released shared database reference');
|
||||||
|
} else if (this.db) {
|
||||||
|
// Dedicated database (in-memory for tests): close it
|
||||||
try {
|
try {
|
||||||
this.db.close();
|
this.db.close();
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
@@ -271,6 +308,7 @@ export class N8NDocumentationMCPServer {
|
|||||||
this.repository = null;
|
this.repository = null;
|
||||||
this.templateService = null;
|
this.templateService = null;
|
||||||
this.earlyLogger = null;
|
this.earlyLogger = null;
|
||||||
|
this.sharedDbState = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log but don't throw - cleanup should be best-effort
|
// Log but don't throw - cleanup should be best-effort
|
||||||
logger.warn('Error closing MCP server', { error: error instanceof Error ? error.message : String(error) });
|
logger.warn('Error closing MCP server', { error: error instanceof Error ? error.message : String(error) });
|
||||||
@@ -286,23 +324,32 @@ export class N8NDocumentationMCPServer {
|
|||||||
|
|
||||||
logger.debug('Database initialization starting...', { dbPath });
|
logger.debug('Database initialization starting...', { dbPath });
|
||||||
|
|
||||||
this.db = await createDatabaseAdapter(dbPath);
|
// For in-memory databases (tests), create a dedicated connection
|
||||||
logger.debug('Database adapter created');
|
// For regular databases, use the shared connection to prevent memory leaks
|
||||||
|
|
||||||
// If using in-memory database for tests, initialize schema
|
|
||||||
if (dbPath === ':memory:') {
|
if (dbPath === ':memory:') {
|
||||||
|
this.db = await createDatabaseAdapter(dbPath);
|
||||||
|
logger.debug('Database adapter created (in-memory mode)');
|
||||||
await this.initializeInMemorySchema();
|
await this.initializeInMemorySchema();
|
||||||
logger.debug('In-memory schema initialized');
|
logger.debug('In-memory schema initialized');
|
||||||
|
this.repository = new NodeRepository(this.db);
|
||||||
|
this.templateService = new TemplateService(this.db);
|
||||||
|
// Initialize similarity services for enhanced validation
|
||||||
|
EnhancedConfigValidator.initializeSimilarityServices(this.repository);
|
||||||
|
this.useSharedDatabase = false;
|
||||||
|
} else {
|
||||||
|
// Use shared database connection to prevent ~900MB memory leak per session
|
||||||
|
// See: Memory leak fix - database was being duplicated per session
|
||||||
|
const sharedState = await getSharedDatabase(dbPath);
|
||||||
|
this.db = sharedState.db;
|
||||||
|
this.repository = sharedState.repository;
|
||||||
|
this.templateService = sharedState.templateService;
|
||||||
|
this.sharedDbState = sharedState;
|
||||||
|
this.useSharedDatabase = true;
|
||||||
|
logger.debug('Using shared database connection');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.repository = new NodeRepository(this.db);
|
|
||||||
logger.debug('Node repository initialized');
|
logger.debug('Node repository initialized');
|
||||||
|
|
||||||
this.templateService = new TemplateService(this.db);
|
|
||||||
logger.debug('Template service initialized');
|
logger.debug('Template service initialized');
|
||||||
|
|
||||||
// Initialize similarity services for enhanced validation
|
|
||||||
EnhancedConfigValidator.initializeSimilarityServices(this.repository);
|
|
||||||
logger.debug('Similarity services initialized');
|
logger.debug('Similarity services initialized');
|
||||||
|
|
||||||
// Checkpoint: Database connected (v2.18.3)
|
// Checkpoint: Database connected (v2.18.3)
|
||||||
@@ -528,6 +575,7 @@ export class N8NDocumentationMCPServer {
|
|||||||
protocolVersion: negotiationResult.version,
|
protocolVersion: negotiationResult.version,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
tools: {},
|
tools: {},
|
||||||
|
resources: {},
|
||||||
},
|
},
|
||||||
serverInfo: {
|
serverInfo: {
|
||||||
name: 'n8n-documentation-mcp',
|
name: 'n8n-documentation-mcp',
|
||||||
@@ -610,6 +658,7 @@ export class N8NDocumentationMCPServer {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
UIAppRegistry.injectToolMeta(tools);
|
||||||
return { tools };
|
return { tools };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -645,9 +694,23 @@ export class N8NDocumentationMCPServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safeguard: if the entire args object arrives as a JSON string, parse it.
|
||||||
|
// Some MCP clients may serialize the arguments object itself.
|
||||||
|
let processedArgs: Record<string, any> | undefined = args;
|
||||||
|
if (typeof args === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(args as unknown as string);
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
|
processedArgs = parsed;
|
||||||
|
logger.warn(`Coerced stringified args object for tool "${name}"`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.warn(`Tool "${name}" received string args that are not valid JSON`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Workaround for n8n's nested output bug
|
// Workaround for n8n's nested output bug
|
||||||
// Check if args contains nested 'output' structure from n8n's memory corruption
|
// Check if args contains nested 'output' structure from n8n's memory corruption
|
||||||
let processedArgs = args;
|
|
||||||
if (args && typeof args === 'object' && 'output' in args) {
|
if (args && typeof args === 'object' && 'output' in args) {
|
||||||
try {
|
try {
|
||||||
const possibleNestedData = args.output;
|
const possibleNestedData = args.output;
|
||||||
@@ -678,7 +741,13 @@ export class N8NDocumentationMCPServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workaround for Claude Desktop / Claude.ai MCP client bugs that
|
||||||
|
// serialize parameters with wrong types. Coerces ALL mismatched types
|
||||||
|
// (string↔object, string↔number, string↔boolean, etc.) using the
|
||||||
|
// tool's inputSchema as the source of truth.
|
||||||
|
processedArgs = this.coerceStringifiedJsonParams(name, processedArgs);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug(`Executing tool: ${name}`, { args: processedArgs });
|
logger.debug(`Executing tool: ${name}`, { args: processedArgs });
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
@@ -739,7 +808,7 @@ export class N8NDocumentationMCPServer {
|
|||||||
if (name.startsWith('validate_') && structuredContent !== null) {
|
if (name.startsWith('validate_') && structuredContent !== null) {
|
||||||
mcpResponse.structuredContent = structuredContent;
|
mcpResponse.structuredContent = structuredContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mcpResponse;
|
return mcpResponse;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error executing tool ${name}`, error);
|
logger.error(`Error executing tool ${name}`, error);
|
||||||
@@ -766,7 +835,7 @@ export class N8NDocumentationMCPServer {
|
|||||||
|
|
||||||
// Provide more helpful error messages for common n8n issues
|
// Provide more helpful error messages for common n8n issues
|
||||||
let helpfulMessage = `Error executing tool ${name}: ${errorMessage}`;
|
let helpfulMessage = `Error executing tool ${name}: ${errorMessage}`;
|
||||||
|
|
||||||
if (errorMessage.includes('required') || errorMessage.includes('missing')) {
|
if (errorMessage.includes('required') || errorMessage.includes('missing')) {
|
||||||
helpfulMessage += '\n\nNote: This error often occurs when the AI agent sends incomplete or incorrectly formatted parameters. Please ensure all required fields are provided with the correct types.';
|
helpfulMessage += '\n\nNote: This error often occurs when the AI agent sends incomplete or incorrectly formatted parameters. Please ensure all required fields are provided with the correct types.';
|
||||||
} else if (errorMessage.includes('type') || errorMessage.includes('expected')) {
|
} else if (errorMessage.includes('type') || errorMessage.includes('expected')) {
|
||||||
@@ -774,12 +843,20 @@ export class N8NDocumentationMCPServer {
|
|||||||
} else if (errorMessage.includes('Unknown category') || errorMessage.includes('not found')) {
|
} else if (errorMessage.includes('Unknown category') || errorMessage.includes('not found')) {
|
||||||
helpfulMessage += '\n\nNote: The requested resource or category was not found. Please check the available options.';
|
helpfulMessage += '\n\nNote: The requested resource or category was not found. Please check the available options.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// For n8n schema errors, add specific guidance
|
// For n8n schema errors, add specific guidance
|
||||||
if (name.startsWith('validate_') && (errorMessage.includes('config') || errorMessage.includes('nodeType'))) {
|
if (name.startsWith('validate_') && (errorMessage.includes('config') || errorMessage.includes('nodeType'))) {
|
||||||
helpfulMessage += '\n\nFor validation tools:\n- nodeType should be a string (e.g., "nodes-base.webhook")\n- config should be an object (e.g., {})';
|
helpfulMessage += '\n\nFor validation tools:\n- nodeType should be a string (e.g., "nodes-base.webhook")\n- config should be an object (e.g., {})';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include diagnostic info about received args to help debug client issues
|
||||||
|
try {
|
||||||
|
const argDiag = processedArgs && typeof processedArgs === 'object'
|
||||||
|
? Object.entries(processedArgs).map(([k, v]) => `${k}: ${typeof v}`).join(', ')
|
||||||
|
: `args type: ${typeof processedArgs}`;
|
||||||
|
helpfulMessage += `\n\n[Diagnostic] Received arg types: {${argDiag}}`;
|
||||||
|
} catch { /* ignore diagnostic errors */ }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -791,6 +868,46 @@ export class N8NDocumentationMCPServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle ListResources for UI apps
|
||||||
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||||
|
const apps = UIAppRegistry.getAllApps();
|
||||||
|
return {
|
||||||
|
resources: apps
|
||||||
|
.filter(app => app.html !== null)
|
||||||
|
.map(app => ({
|
||||||
|
uri: app.config.uri,
|
||||||
|
name: app.config.displayName,
|
||||||
|
description: app.config.description,
|
||||||
|
mimeType: app.config.mimeType,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle ReadResource for UI apps
|
||||||
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
const uri = request.params.uri;
|
||||||
|
// Parse ui://n8n-mcp/{id} pattern
|
||||||
|
const match = uri.match(/^ui:\/\/n8n-mcp\/(.+)$/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Unknown resource URI: ${uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = UIAppRegistry.getAppById(match[1]);
|
||||||
|
if (!app || !app.html) {
|
||||||
|
throw new Error(`UI app not found or not built: ${match[1]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: app.config.uri,
|
||||||
|
mimeType: app.config.mimeType,
|
||||||
|
text: app.html,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1043,6 +1160,109 @@ export class N8NDocumentationMCPServer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce mistyped parameters back to their expected types.
|
||||||
|
* Workaround for Claude Desktop / Claude.ai MCP client bugs that serialize
|
||||||
|
* parameters incorrectly (objects as strings, numbers as strings, etc.).
|
||||||
|
*
|
||||||
|
* Handles ALL type mismatches based on the tool's inputSchema:
|
||||||
|
* string→object, string→array : JSON.parse
|
||||||
|
* string→number, string→integer : Number()
|
||||||
|
* string→boolean : "true"/"false" parsing
|
||||||
|
* number→string, boolean→string : .toString()
|
||||||
|
*/
|
||||||
|
private coerceStringifiedJsonParams(
|
||||||
|
toolName: string,
|
||||||
|
args: Record<string, any> | undefined
|
||||||
|
): Record<string, any> | undefined {
|
||||||
|
if (!args || typeof args !== 'object') return args;
|
||||||
|
|
||||||
|
const allTools = [...n8nDocumentationToolsFinal, ...n8nManagementTools];
|
||||||
|
const tool = allTools.find(t => t.name === toolName);
|
||||||
|
if (!tool?.inputSchema?.properties) return args;
|
||||||
|
|
||||||
|
const properties = tool.inputSchema.properties;
|
||||||
|
const coerced = { ...args };
|
||||||
|
let coercedAny = false;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(coerced)) {
|
||||||
|
if (value === undefined || value === null) continue;
|
||||||
|
|
||||||
|
const propSchema = (properties as any)[key];
|
||||||
|
if (!propSchema) continue;
|
||||||
|
const expectedType = propSchema.type;
|
||||||
|
if (!expectedType) continue;
|
||||||
|
|
||||||
|
const actualType = typeof value;
|
||||||
|
|
||||||
|
// Already correct type — skip
|
||||||
|
if (expectedType === 'string' && actualType === 'string') continue;
|
||||||
|
if ((expectedType === 'number' || expectedType === 'integer') && actualType === 'number') continue;
|
||||||
|
if (expectedType === 'boolean' && actualType === 'boolean') continue;
|
||||||
|
if (expectedType === 'object' && actualType === 'object' && !Array.isArray(value)) continue;
|
||||||
|
if (expectedType === 'array' && Array.isArray(value)) continue;
|
||||||
|
|
||||||
|
// --- Coercion: string value → expected type ---
|
||||||
|
if (actualType === 'string') {
|
||||||
|
const trimmed = (value as string).trim();
|
||||||
|
|
||||||
|
if (expectedType === 'object' && trimmed.startsWith('{')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||||
|
coerced[key] = parsed;
|
||||||
|
coercedAny = true;
|
||||||
|
}
|
||||||
|
} catch { /* keep original */ }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedType === 'array' && trimmed.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
coerced[key] = parsed;
|
||||||
|
coercedAny = true;
|
||||||
|
}
|
||||||
|
} catch { /* keep original */ }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedType === 'number' || expectedType === 'integer') {
|
||||||
|
const num = Number(trimmed);
|
||||||
|
if (!isNaN(num) && trimmed !== '') {
|
||||||
|
coerced[key] = expectedType === 'integer' ? Math.trunc(num) : num;
|
||||||
|
coercedAny = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedType === 'boolean') {
|
||||||
|
if (trimmed === 'true') { coerced[key] = true; coercedAny = true; }
|
||||||
|
else if (trimmed === 'false') { coerced[key] = false; coercedAny = true; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Coercion: number/boolean value → expected string ---
|
||||||
|
if (expectedType === 'string' && (actualType === 'number' || actualType === 'boolean')) {
|
||||||
|
coerced[key] = String(value);
|
||||||
|
coercedAny = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coercedAny) {
|
||||||
|
logger.warn(`Coerced mistyped params for tool "${toolName}"`, {
|
||||||
|
original: Object.fromEntries(
|
||||||
|
Object.entries(args).map(([k, v]) => [k, `${typeof v}: ${typeof v === 'string' ? v.substring(0, 80) : v}`])
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return coerced;
|
||||||
|
}
|
||||||
|
|
||||||
async executeTool(name: string, args: any): Promise<any> {
|
async executeTool(name: string, args: any): Promise<any> {
|
||||||
// Ensure args is an object and validate it
|
// Ensure args is an object and validate it
|
||||||
args = args || {};
|
args = args || {};
|
||||||
@@ -3910,8 +4130,33 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
|||||||
}
|
}
|
||||||
|
|
||||||
async shutdown(): Promise<void> {
|
async shutdown(): Promise<void> {
|
||||||
|
// Prevent double-shutdown
|
||||||
|
if (this.isShutdown) {
|
||||||
|
logger.debug('Shutdown already called, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isShutdown = true;
|
||||||
|
|
||||||
logger.info('Shutting down MCP server...');
|
logger.info('Shutting down MCP server...');
|
||||||
|
|
||||||
|
// Wait for initialization to complete (or fail) before cleanup
|
||||||
|
// This prevents race conditions where shutdown runs while init is in progress
|
||||||
|
try {
|
||||||
|
await this.initialized;
|
||||||
|
} catch (error) {
|
||||||
|
// Initialization failed - that's OK, we still need to clean up
|
||||||
|
logger.debug('Initialization had failed, proceeding with cleanup', {
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close MCP server connection (for consistency with close() method)
|
||||||
|
try {
|
||||||
|
await this.server.close();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error closing MCP server:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up cache timers to prevent memory leaks
|
// Clean up cache timers to prevent memory leaks
|
||||||
if (this.cache) {
|
if (this.cache) {
|
||||||
try {
|
try {
|
||||||
@@ -3921,15 +4166,31 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
|||||||
logger.error('Error cleaning up cache:', error);
|
logger.error('Error cleaning up cache:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close database connection if it exists
|
// Handle database cleanup based on whether it's shared or dedicated
|
||||||
if (this.db) {
|
// For shared databases, we only release the reference (decrement refCount)
|
||||||
|
// For dedicated databases (in-memory for tests), we close the connection
|
||||||
|
if (this.useSharedDatabase && this.sharedDbState) {
|
||||||
try {
|
try {
|
||||||
await this.db.close();
|
releaseSharedDatabase(this.sharedDbState);
|
||||||
|
logger.info('Released shared database reference');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error releasing shared database:', error);
|
||||||
|
}
|
||||||
|
} else if (this.db) {
|
||||||
|
try {
|
||||||
|
this.db.close();
|
||||||
logger.info('Database connection closed');
|
logger.info('Database connection closed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error closing database:', error);
|
logger.error('Error closing database:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Null out references to help garbage collection
|
||||||
|
this.db = null;
|
||||||
|
this.repository = null;
|
||||||
|
this.templateService = null;
|
||||||
|
this.earlyLogger = null;
|
||||||
|
this.sharedDbState = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,7 +284,7 @@ export const n8nManagementTools: ToolDefinition[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'n8n_autofix_workflow',
|
name: 'n8n_autofix_workflow',
|
||||||
description: `Automatically fix common workflow validation errors. Preview fixes or apply them. Fixes expression format, typeVersion, error output config, webhook paths.`,
|
description: `Automatically fix common workflow validation errors. Preview fixes or apply them. Fixes expression format, typeVersion, error output config, webhook paths, connection structure issues (numeric keys, invalid types, ID-to-name, duplicates, out-of-bounds indices).`,
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -301,7 +301,7 @@ export const n8nManagementTools: ToolDefinition[] = [
|
|||||||
description: 'Types of fixes to apply (default: all)',
|
description: 'Types of fixes to apply (default: all)',
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['expression-format', 'typeversion-correction', 'error-output-config', 'node-type-correction', 'webhook-missing-path', 'typeversion-upgrade', 'version-migration']
|
enum: ['expression-format', 'typeversion-correction', 'error-output-config', 'node-type-correction', 'webhook-missing-path', 'typeversion-upgrade', 'version-migration', 'tool-variant-correction', 'connection-numeric-keys', 'connection-invalid-type', 'connection-id-to-name', 'connection-duplicate-removal', 'connection-input-index']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confidenceThreshold: {
|
confidenceThreshold: {
|
||||||
|
|||||||
36
src/mcp/ui/app-configs.ts
Normal file
36
src/mcp/ui/app-configs.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { UIAppConfig } from './types';
|
||||||
|
|
||||||
|
export const UI_APP_CONFIGS: UIAppConfig[] = [
|
||||||
|
{
|
||||||
|
id: 'operation-result',
|
||||||
|
displayName: 'Operation Result',
|
||||||
|
description: 'Visual summary of workflow operations (create, update, delete, test)',
|
||||||
|
uri: 'ui://n8n-mcp/operation-result',
|
||||||
|
mimeType: 'text/html;profile=mcp-app',
|
||||||
|
toolPatterns: [
|
||||||
|
'n8n_create_workflow',
|
||||||
|
'n8n_update_full_workflow',
|
||||||
|
'n8n_update_partial_workflow',
|
||||||
|
'n8n_delete_workflow',
|
||||||
|
'n8n_test_workflow',
|
||||||
|
'n8n_autofix_workflow',
|
||||||
|
// n8n_deploy_template disabled: Claude.ai renders blank content for this tool
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'validation-summary',
|
||||||
|
displayName: 'Validation Summary',
|
||||||
|
description: 'Visual summary of node and workflow validation results',
|
||||||
|
uri: 'ui://n8n-mcp/validation-summary',
|
||||||
|
mimeType: 'text/html;profile=mcp-app',
|
||||||
|
toolPatterns: [
|
||||||
|
'validate_node',
|
||||||
|
'validate_workflow',
|
||||||
|
'n8n_validate_workflow',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// workflow-list, execution-history, health-dashboard disabled:
|
||||||
|
// Claude.ai does not render these apps (shows collapsed accordions).
|
||||||
|
// The server sets _meta correctly on the wire but the host ignores it.
|
||||||
|
// Re-enable once the host-side issue is resolved.
|
||||||
|
];
|
||||||
3
src/mcp/ui/index.ts
Normal file
3
src/mcp/ui/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export type { UIAppConfig, UIMetadata, UIAppEntry } from './types';
|
||||||
|
export { UI_APP_CONFIGS } from './app-configs';
|
||||||
|
export { UIAppRegistry } from './registry';
|
||||||
90
src/mcp/ui/registry.ts
Normal file
90
src/mcp/ui/registry.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { existsSync, readFileSync } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { logger } from '../../utils/logger';
|
||||||
|
import type { UIAppConfig, UIAppEntry } from './types';
|
||||||
|
import { UI_APP_CONFIGS } from './app-configs';
|
||||||
|
|
||||||
|
export class UIAppRegistry {
|
||||||
|
private static entries: Map<string, UIAppEntry> = new Map();
|
||||||
|
private static toolIndex: Map<string, UIAppEntry> = new Map();
|
||||||
|
private static loaded = false;
|
||||||
|
|
||||||
|
static load(): void {
|
||||||
|
// Resolve dist directory relative to package root
|
||||||
|
// In production: package-root/ui-apps/dist/
|
||||||
|
// __dirname will be src/mcp/ui or dist/mcp/ui
|
||||||
|
const packageRoot = path.resolve(__dirname, '..', '..', '..');
|
||||||
|
const distDir = path.join(packageRoot, 'ui-apps', 'dist');
|
||||||
|
|
||||||
|
this.entries.clear();
|
||||||
|
this.toolIndex.clear();
|
||||||
|
|
||||||
|
for (const config of UI_APP_CONFIGS) {
|
||||||
|
let html: string | null = null;
|
||||||
|
const htmlPath = path.join(distDir, config.id, 'index.html');
|
||||||
|
|
||||||
|
if (existsSync(htmlPath)) {
|
||||||
|
try {
|
||||||
|
html = readFileSync(htmlPath, 'utf-8');
|
||||||
|
logger.info(`Loaded UI app: ${config.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Failed to read UI app HTML: ${config.id}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry: UIAppEntry = { config, html };
|
||||||
|
this.entries.set(config.id, entry);
|
||||||
|
|
||||||
|
// Build tool -> entry index
|
||||||
|
for (const pattern of config.toolPatterns) {
|
||||||
|
this.toolIndex.set(pattern, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loaded = true;
|
||||||
|
logger.info(`UI App Registry loaded: ${this.entries.size} apps, ${this.toolIndex.size} tool mappings`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getAppForTool(toolName: string): UIAppEntry | null {
|
||||||
|
if (!this.loaded) return null;
|
||||||
|
return this.toolIndex.get(toolName) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getAppById(id: string): UIAppEntry | null {
|
||||||
|
if (!this.loaded) return null;
|
||||||
|
return this.entries.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getAllApps(): UIAppEntry[] {
|
||||||
|
if (!this.loaded) return [];
|
||||||
|
return Array.from(this.entries.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enrich tool definitions with _meta.ui.resourceUri for tools that have
|
||||||
|
* a matching UI app. Per MCP ext-apps spec, this goes on the tool
|
||||||
|
* definition (tools/list), not the tool call response.
|
||||||
|
*
|
||||||
|
* Sets both nested (_meta.ui.resourceUri) and flat (_meta["ui/resourceUri"])
|
||||||
|
* keys for compatibility with hosts that read either format.
|
||||||
|
*/
|
||||||
|
static injectToolMeta(tools: Array<{ name: string; [key: string]: any }>): void {
|
||||||
|
if (!this.loaded) return;
|
||||||
|
for (const tool of tools) {
|
||||||
|
const entry = this.toolIndex.get(tool.name);
|
||||||
|
if (entry && entry.html) {
|
||||||
|
tool._meta = {
|
||||||
|
ui: { resourceUri: entry.config.uri },
|
||||||
|
'ui/resourceUri': entry.config.uri,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset registry state. Intended for testing only. */
|
||||||
|
static reset(): void {
|
||||||
|
this.entries.clear();
|
||||||
|
this.toolIndex.clear();
|
||||||
|
this.loaded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/mcp/ui/types.ts
Normal file
23
src/mcp/ui/types.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* MCP Apps UI type definitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface UIAppConfig {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
uri: string;
|
||||||
|
mimeType: string;
|
||||||
|
toolPatterns: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UIMetadata {
|
||||||
|
ui: {
|
||||||
|
resourceUri: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UIAppEntry {
|
||||||
|
config: UIAppConfig;
|
||||||
|
html: string | null;
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
* Environment variables:
|
* Environment variables:
|
||||||
* N8N_MCP_LLM_BASE_URL - LLM server URL (default: http://localhost:1234/v1)
|
* N8N_MCP_LLM_BASE_URL - LLM server URL (default: http://localhost:1234/v1)
|
||||||
* N8N_MCP_LLM_MODEL - LLM model name (default: qwen3-4b-thinking-2507)
|
* N8N_MCP_LLM_MODEL - LLM model name (default: qwen3-4b-thinking-2507)
|
||||||
|
* N8N_MCP_LLM_API_KEY - LLM API key (falls back to OPENAI_API_KEY; default: 'not-needed')
|
||||||
* N8N_MCP_LLM_TIMEOUT - Request timeout in ms (default: 60000)
|
* N8N_MCP_LLM_TIMEOUT - Request timeout in ms (default: 60000)
|
||||||
* N8N_MCP_DB_PATH - Database path (default: ./data/nodes.db)
|
* N8N_MCP_DB_PATH - Database path (default: ./data/nodes.db)
|
||||||
*/
|
*/
|
||||||
@@ -81,6 +82,7 @@ Options:
|
|||||||
Environment Variables:
|
Environment Variables:
|
||||||
N8N_MCP_LLM_BASE_URL LLM server URL (default: http://localhost:1234/v1)
|
N8N_MCP_LLM_BASE_URL LLM server URL (default: http://localhost:1234/v1)
|
||||||
N8N_MCP_LLM_MODEL LLM model name (default: qwen3-4b-thinking-2507)
|
N8N_MCP_LLM_MODEL LLM model name (default: qwen3-4b-thinking-2507)
|
||||||
|
N8N_MCP_LLM_API_KEY LLM API key (falls back to OPENAI_API_KEY; default: 'not-needed')
|
||||||
N8N_MCP_LLM_TIMEOUT Request timeout in ms (default: 60000)
|
N8N_MCP_LLM_TIMEOUT Request timeout in ms (default: 60000)
|
||||||
N8N_MCP_DB_PATH Database path (default: ./data/nodes.db)
|
N8N_MCP_DB_PATH Database path (default: ./data/nodes.db)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,21 @@ export interface WorkflowNode {
|
|||||||
typeVersion?: number;
|
typeVersion?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tool description from node, checking all possible property locations.
|
||||||
|
* Different n8n tool types store descriptions in different places:
|
||||||
|
* - toolDescription: HTTP Request Tool, Vector Store Tool
|
||||||
|
* - description: Workflow Tool, Code Tool, AI Agent Tool
|
||||||
|
* - options.description: SerpApi, Wikipedia, SearXNG
|
||||||
|
*/
|
||||||
|
function getToolDescription(node: WorkflowNode): string | undefined {
|
||||||
|
return (
|
||||||
|
node.parameters.toolDescription ||
|
||||||
|
node.parameters.description ||
|
||||||
|
node.parameters.options?.description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkflowJson {
|
export interface WorkflowJson {
|
||||||
name?: string;
|
name?: string;
|
||||||
nodes: WorkflowNode[];
|
nodes: WorkflowNode[];
|
||||||
@@ -58,7 +73,7 @@ export function validateHTTPRequestTool(node: WorkflowNode): ValidationIssue[] {
|
|||||||
const issues: ValidationIssue[] = [];
|
const issues: ValidationIssue[] = [];
|
||||||
|
|
||||||
// 1. Check toolDescription (REQUIRED)
|
// 1. Check toolDescription (REQUIRED)
|
||||||
if (!node.parameters.toolDescription) {
|
if (!getToolDescription(node)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
@@ -66,7 +81,7 @@ export function validateHTTPRequestTool(node: WorkflowNode): ValidationIssue[] {
|
|||||||
message: `HTTP Request Tool "${node.name}" has no toolDescription. Add a clear description to help the LLM know when to use this API.`,
|
message: `HTTP Request Tool "${node.name}" has no toolDescription. Add a clear description to help the LLM know when to use this API.`,
|
||||||
code: 'MISSING_TOOL_DESCRIPTION'
|
code: 'MISSING_TOOL_DESCRIPTION'
|
||||||
});
|
});
|
||||||
} else if (node.parameters.toolDescription.trim().length < MIN_DESCRIPTION_LENGTH_MEDIUM) {
|
} else if (getToolDescription(node)!.trim().length < MIN_DESCRIPTION_LENGTH_MEDIUM) {
|
||||||
issues.push({
|
issues.push({
|
||||||
severity: 'warning',
|
severity: 'warning',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
@@ -214,8 +229,8 @@ export function validateHTTPRequestTool(node: WorkflowNode): ValidationIssue[] {
|
|||||||
export function validateCodeTool(node: WorkflowNode): ValidationIssue[] {
|
export function validateCodeTool(node: WorkflowNode): ValidationIssue[] {
|
||||||
const issues: ValidationIssue[] = [];
|
const issues: ValidationIssue[] = [];
|
||||||
|
|
||||||
// 1. Check toolDescription (REQUIRED)
|
// 1. Check toolDescription (REQUIRED) - check all possible locations
|
||||||
if (!node.parameters.toolDescription) {
|
if (!getToolDescription(node)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
@@ -261,7 +276,7 @@ export function validateVectorStoreTool(
|
|||||||
const issues: ValidationIssue[] = [];
|
const issues: ValidationIssue[] = [];
|
||||||
|
|
||||||
// 1. Check toolDescription (REQUIRED)
|
// 1. Check toolDescription (REQUIRED)
|
||||||
if (!node.parameters.toolDescription) {
|
if (!getToolDescription(node)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
@@ -302,7 +317,7 @@ export function validateWorkflowTool(node: WorkflowNode, reverseConnections?: Ma
|
|||||||
const issues: ValidationIssue[] = [];
|
const issues: ValidationIssue[] = [];
|
||||||
|
|
||||||
// 1. Check toolDescription (REQUIRED)
|
// 1. Check toolDescription (REQUIRED)
|
||||||
if (!node.parameters.toolDescription) {
|
if (!getToolDescription(node)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
@@ -337,7 +352,7 @@ export function validateAIAgentTool(
|
|||||||
const issues: ValidationIssue[] = [];
|
const issues: ValidationIssue[] = [];
|
||||||
|
|
||||||
// 1. Check toolDescription (REQUIRED)
|
// 1. Check toolDescription (REQUIRED)
|
||||||
if (!node.parameters.toolDescription) {
|
if (!getToolDescription(node)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
@@ -378,7 +393,7 @@ export function validateMCPClientTool(node: WorkflowNode): ValidationIssue[] {
|
|||||||
const issues: ValidationIssue[] = [];
|
const issues: ValidationIssue[] = [];
|
||||||
|
|
||||||
// 1. Check toolDescription (REQUIRED)
|
// 1. Check toolDescription (REQUIRED)
|
||||||
if (!node.parameters.toolDescription) {
|
if (!getToolDescription(node)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
@@ -406,20 +421,14 @@ export function validateMCPClientTool(node: WorkflowNode): ValidationIssue[] {
|
|||||||
* 7-8. Simple Tools (Calculator, Think) Validators
|
* 7-8. Simple Tools (Calculator, Think) Validators
|
||||||
* From spec lines 1868-2009
|
* From spec lines 1868-2009
|
||||||
*/
|
*/
|
||||||
export function validateCalculatorTool(node: WorkflowNode): ValidationIssue[] {
|
export function validateCalculatorTool(_node: WorkflowNode): ValidationIssue[] {
|
||||||
const issues: ValidationIssue[] = [];
|
// Calculator Tool has a built-in description - no validation needed
|
||||||
|
return [];
|
||||||
// Calculator Tool has a built-in description and is self-explanatory
|
|
||||||
// toolDescription is optional - no validation needed
|
|
||||||
return issues;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateThinkTool(node: WorkflowNode): ValidationIssue[] {
|
export function validateThinkTool(_node: WorkflowNode): ValidationIssue[] {
|
||||||
const issues: ValidationIssue[] = [];
|
// Think Tool has a built-in description - no validation needed
|
||||||
|
return [];
|
||||||
// Think Tool has a built-in description and is self-explanatory
|
|
||||||
// toolDescription is optional - no validation needed
|
|
||||||
return issues;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -430,7 +439,7 @@ export function validateSerpApiTool(node: WorkflowNode): ValidationIssue[] {
|
|||||||
const issues: ValidationIssue[] = [];
|
const issues: ValidationIssue[] = [];
|
||||||
|
|
||||||
// 1. Check toolDescription (REQUIRED)
|
// 1. Check toolDescription (REQUIRED)
|
||||||
if (!node.parameters.toolDescription) {
|
if (!getToolDescription(node)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
@@ -457,7 +466,7 @@ export function validateWikipediaTool(node: WorkflowNode): ValidationIssue[] {
|
|||||||
const issues: ValidationIssue[] = [];
|
const issues: ValidationIssue[] = [];
|
||||||
|
|
||||||
// 1. Check toolDescription (REQUIRED)
|
// 1. Check toolDescription (REQUIRED)
|
||||||
if (!node.parameters.toolDescription) {
|
if (!getToolDescription(node)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
@@ -487,7 +496,7 @@ export function validateSearXngTool(node: WorkflowNode): ValidationIssue[] {
|
|||||||
const issues: ValidationIssue[] = [];
|
const issues: ValidationIssue[] = [];
|
||||||
|
|
||||||
// 1. Check toolDescription (REQUIRED)
|
// 1. Check toolDescription (REQUIRED)
|
||||||
if (!node.parameters.toolDescription) {
|
if (!getToolDescription(node)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
@@ -526,7 +535,7 @@ export function validateWolframAlphaTool(node: WorkflowNode): ValidationIssue[]
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check description (INFO)
|
// 2. Check description (INFO)
|
||||||
if (!node.parameters.description && !node.parameters.toolDescription) {
|
if (!getToolDescription(node)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
severity: 'info',
|
severity: 'info',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export class N8nApiClient {
|
|||||||
|
|
||||||
async activateWorkflow(id: string): Promise<Workflow> {
|
async activateWorkflow(id: string): Promise<Workflow> {
|
||||||
try {
|
try {
|
||||||
const response = await this.client.post(`/workflows/${id}/activate`);
|
const response = await this.client.post(`/workflows/${id}/activate`, {});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleN8nApiError(error);
|
throw handleN8nApiError(error);
|
||||||
@@ -263,7 +263,7 @@ export class N8nApiClient {
|
|||||||
|
|
||||||
async deactivateWorkflow(id: string): Promise<Workflow> {
|
async deactivateWorkflow(id: string): Promise<Workflow> {
|
||||||
try {
|
try {
|
||||||
const response = await this.client.post(`/workflows/${id}/deactivate`);
|
const response = await this.client.post(`/workflows/${id}/deactivate`, {});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleN8nApiError(error);
|
throw handleN8nApiError(error);
|
||||||
@@ -493,6 +493,15 @@ export class N8nApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateWorkflowTags(workflowId: string, tagIds: string[]): Promise<Tag[]> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.put(`/workflows/${workflowId}/tags`, tagIds.filter(id => id).map(id => ({ id })));
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw handleN8nApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Source Control Management (Enterprise feature)
|
// Source Control Management (Enterprise feature)
|
||||||
async getSourceControlStatus(): Promise<SourceControlStatus> {
|
async getSourceControlStatus(): Promise<SourceControlStatus> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function sanitizeWorkflowNodes(workflow: any): any {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...workflow,
|
...workflow,
|
||||||
nodes: workflow.nodes.map((node: any) => sanitizeNode(node))
|
nodes: workflow.nodes.map(sanitizeNode)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,9 +121,7 @@ function sanitizeFilterConditions(conditions: any): any {
|
|||||||
|
|
||||||
// Sanitize conditions array
|
// Sanitize conditions array
|
||||||
if (sanitized.conditions && Array.isArray(sanitized.conditions)) {
|
if (sanitized.conditions && Array.isArray(sanitized.conditions)) {
|
||||||
sanitized.conditions = sanitized.conditions.map((condition: any) =>
|
sanitized.conditions = sanitized.conditions.map(sanitizeCondition);
|
||||||
sanitizeCondition(condition)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sanitized;
|
return sanitized;
|
||||||
@@ -214,18 +212,25 @@ function inferDataType(operation: string): string {
|
|||||||
return 'boolean';
|
return 'boolean';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Number operations
|
// Number operations (partial match to catch variants like "greaterThan" containing "gt")
|
||||||
const numberOps = ['isNumeric', 'gt', 'gte', 'lt', 'lte'];
|
const numberOps = ['isNumeric', 'gt', 'gte', 'lt', 'lte'];
|
||||||
if (numberOps.some(op => operation.includes(op))) {
|
if (numberOps.some(op => operation.includes(op))) {
|
||||||
return 'number';
|
return 'number';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date operations
|
// Date operations (partial match to catch variants like "isAfter" containing "after")
|
||||||
const dateOps = ['after', 'before', 'afterDate', 'beforeDate'];
|
const dateOps = ['after', 'before', 'afterDate', 'beforeDate'];
|
||||||
if (dateOps.some(op => operation.includes(op))) {
|
if (dateOps.some(op => operation.includes(op))) {
|
||||||
return 'dateTime';
|
return 'dateTime';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Object operations: empty/notEmpty/exists/notExists are generic object-level checks
|
||||||
|
// (distinct from isEmpty/isNotEmpty which are boolean-typed operations)
|
||||||
|
const objectOps = ['empty', 'notEmpty', 'exists', 'notExists'];
|
||||||
|
if (objectOps.includes(operation)) {
|
||||||
|
return 'object';
|
||||||
|
}
|
||||||
|
|
||||||
// Default to string
|
// Default to string
|
||||||
return 'string';
|
return 'string';
|
||||||
}
|
}
|
||||||
@@ -239,7 +244,11 @@ function isUnaryOperator(operation: string): boolean {
|
|||||||
'isNotEmpty',
|
'isNotEmpty',
|
||||||
'true',
|
'true',
|
||||||
'false',
|
'false',
|
||||||
'isNumeric'
|
'isNumeric',
|
||||||
|
'empty',
|
||||||
|
'notEmpty',
|
||||||
|
'exists',
|
||||||
|
'notExists'
|
||||||
];
|
];
|
||||||
return unaryOps.includes(operation);
|
return unaryOps.includes(operation);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export class TypeStructureService {
|
|||||||
/**
|
/**
|
||||||
* Get all type structure definitions
|
* Get all type structure definitions
|
||||||
*
|
*
|
||||||
* Returns a record of all 22 NodePropertyTypes with their structures.
|
* Returns a record of all 23 NodePropertyTypes with their structures.
|
||||||
* Useful for documentation, validation setup, or UI generation.
|
* Useful for documentation, validation setup, or UI generation.
|
||||||
*
|
*
|
||||||
* @returns Record mapping all types to their structures
|
* @returns Record mapping all types to their structures
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { WorkflowValidationResult } from './workflow-validator';
|
import { WorkflowValidationResult, VALID_CONNECTION_TYPES } from './workflow-validator';
|
||||||
import { ExpressionFormatIssue } from './expression-format-validator';
|
import { ExpressionFormatIssue } from './expression-format-validator';
|
||||||
import { NodeSimilarityService } from './node-similarity-service';
|
import { NodeSimilarityService } from './node-similarity-service';
|
||||||
import { NodeRepository } from '../database/node-repository';
|
import { NodeRepository } from '../database/node-repository';
|
||||||
import {
|
import {
|
||||||
WorkflowDiffOperation,
|
WorkflowDiffOperation,
|
||||||
UpdateNodeOperation
|
UpdateNodeOperation,
|
||||||
|
ReplaceConnectionsOperation
|
||||||
} from '../types/workflow-diff';
|
} from '../types/workflow-diff';
|
||||||
import { WorkflowNode, Workflow } from '../types/n8n-api';
|
import { WorkflowNode, Workflow } from '../types/n8n-api';
|
||||||
import { Logger } from '../utils/logger';
|
import { Logger } from '../utils/logger';
|
||||||
@@ -30,9 +31,22 @@ export type FixType =
|
|||||||
| 'error-output-config'
|
| 'error-output-config'
|
||||||
| 'node-type-correction'
|
| 'node-type-correction'
|
||||||
| 'webhook-missing-path'
|
| 'webhook-missing-path'
|
||||||
| 'typeversion-upgrade' // Proactive version upgrades
|
| 'typeversion-upgrade' // Proactive version upgrades
|
||||||
| 'version-migration' // Smart version migrations with breaking changes
|
| 'version-migration' // Smart version migrations with breaking changes
|
||||||
| 'tool-variant-correction'; // Fix base nodes used as AI tools when Tool variant exists
|
| 'tool-variant-correction' // Fix base nodes used as AI tools when Tool variant exists
|
||||||
|
| 'connection-numeric-keys' // "0","1" keys → main[0], main[1]
|
||||||
|
| 'connection-invalid-type' // type:"0" → type:"main"
|
||||||
|
| 'connection-id-to-name' // node ID refs → node name refs
|
||||||
|
| 'connection-duplicate-removal' // Dedup identical connection entries
|
||||||
|
| 'connection-input-index'; // Out-of-bounds input index → clamped
|
||||||
|
|
||||||
|
export const CONNECTION_FIX_TYPES: FixType[] = [
|
||||||
|
'connection-numeric-keys',
|
||||||
|
'connection-invalid-type',
|
||||||
|
'connection-id-to-name',
|
||||||
|
'connection-duplicate-removal',
|
||||||
|
'connection-input-index'
|
||||||
|
];
|
||||||
|
|
||||||
export interface AutoFixConfig {
|
export interface AutoFixConfig {
|
||||||
applyFixes: boolean;
|
applyFixes: boolean;
|
||||||
@@ -175,6 +189,9 @@ export class WorkflowAutoFixer {
|
|||||||
await this.processVersionMigrationFixes(workflow, nodeMap, operations, fixes, postUpdateGuidance);
|
await this.processVersionMigrationFixes(workflow, nodeMap, operations, fixes, postUpdateGuidance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process connection structure fixes (HIGH/MEDIUM confidence)
|
||||||
|
this.processConnectionFixes(workflow, validationResult, fullConfig, operations, fixes);
|
||||||
|
|
||||||
// Filter by confidence threshold
|
// Filter by confidence threshold
|
||||||
const filteredFixes = this.filterByConfidence(fixes, fullConfig.confidenceThreshold);
|
const filteredFixes = this.filterByConfidence(fixes, fullConfig.confidenceThreshold);
|
||||||
const filteredOperations = this.filterOperationsByFixes(operations, filteredFixes, fixes);
|
const filteredOperations = this.filterOperationsByFixes(operations, filteredFixes, fixes);
|
||||||
@@ -655,10 +672,14 @@ export class WorkflowAutoFixer {
|
|||||||
allFixes: FixOperation[]
|
allFixes: FixOperation[]
|
||||||
): WorkflowDiffOperation[] {
|
): WorkflowDiffOperation[] {
|
||||||
const fixedNodes = new Set(filteredFixes.map(f => f.node));
|
const fixedNodes = new Set(filteredFixes.map(f => f.node));
|
||||||
|
const hasConnectionFixes = filteredFixes.some(f => CONNECTION_FIX_TYPES.includes(f.type));
|
||||||
return operations.filter(op => {
|
return operations.filter(op => {
|
||||||
if (op.type === 'updateNode') {
|
if (op.type === 'updateNode') {
|
||||||
return fixedNodes.has(op.nodeId || '');
|
return fixedNodes.has(op.nodeId || '');
|
||||||
}
|
}
|
||||||
|
if (op.type === 'replaceConnections') {
|
||||||
|
return hasConnectionFixes;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -677,7 +698,12 @@ export class WorkflowAutoFixer {
|
|||||||
'webhook-missing-path': 0,
|
'webhook-missing-path': 0,
|
||||||
'typeversion-upgrade': 0,
|
'typeversion-upgrade': 0,
|
||||||
'version-migration': 0,
|
'version-migration': 0,
|
||||||
'tool-variant-correction': 0
|
'tool-variant-correction': 0,
|
||||||
|
'connection-numeric-keys': 0,
|
||||||
|
'connection-invalid-type': 0,
|
||||||
|
'connection-id-to-name': 0,
|
||||||
|
'connection-duplicate-removal': 0,
|
||||||
|
'connection-input-index': 0
|
||||||
},
|
},
|
||||||
byConfidence: {
|
byConfidence: {
|
||||||
'high': 0,
|
'high': 0,
|
||||||
@@ -730,6 +756,16 @@ export class WorkflowAutoFixer {
|
|||||||
parts.push(`${stats.byType['tool-variant-correction']} tool variant ${stats.byType['tool-variant-correction'] === 1 ? 'correction' : 'corrections'}`);
|
parts.push(`${stats.byType['tool-variant-correction']} tool variant ${stats.byType['tool-variant-correction'] === 1 ? 'correction' : 'corrections'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connectionIssueCount =
|
||||||
|
(stats.byType['connection-numeric-keys'] || 0) +
|
||||||
|
(stats.byType['connection-invalid-type'] || 0) +
|
||||||
|
(stats.byType['connection-id-to-name'] || 0) +
|
||||||
|
(stats.byType['connection-duplicate-removal'] || 0) +
|
||||||
|
(stats.byType['connection-input-index'] || 0);
|
||||||
|
if (connectionIssueCount > 0) {
|
||||||
|
parts.push(`${connectionIssueCount} connection ${connectionIssueCount === 1 ? 'issue' : 'issues'}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (parts.length === 0) {
|
if (parts.length === 0) {
|
||||||
return `Fixed ${stats.total} ${stats.total === 1 ? 'issue' : 'issues'}`;
|
return `Fixed ${stats.total} ${stats.total === 1 ? 'issue' : 'issues'}`;
|
||||||
}
|
}
|
||||||
@@ -737,6 +773,370 @@ export class WorkflowAutoFixer {
|
|||||||
return `Fixed ${parts.join(', ')}`;
|
return `Fixed ${parts.join(', ')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process connection structure fixes.
|
||||||
|
* Deep-clones workflow.connections, applies fixes in order:
|
||||||
|
* numeric keys → ID-to-name → invalid type → input index → dedup
|
||||||
|
* Emits a single ReplaceConnectionsOperation if any corrections were made.
|
||||||
|
*/
|
||||||
|
private processConnectionFixes(
|
||||||
|
workflow: Workflow,
|
||||||
|
validationResult: WorkflowValidationResult,
|
||||||
|
config: AutoFixConfig,
|
||||||
|
operations: WorkflowDiffOperation[],
|
||||||
|
fixes: FixOperation[]
|
||||||
|
): void {
|
||||||
|
if (!workflow.connections || Object.keys(workflow.connections).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build lookup maps
|
||||||
|
const idToNameMap = new Map<string, string>();
|
||||||
|
const nameSet = new Set<string>();
|
||||||
|
for (const node of workflow.nodes) {
|
||||||
|
idToNameMap.set(node.id, node.name);
|
||||||
|
nameSet.add(node.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep-clone connections
|
||||||
|
const conn: any = JSON.parse(JSON.stringify(workflow.connections));
|
||||||
|
let anyFixed = false;
|
||||||
|
|
||||||
|
// 1. Fix numeric source keys ("0" → main[0])
|
||||||
|
if (!config.fixTypes || config.fixTypes.includes('connection-numeric-keys')) {
|
||||||
|
const numericKeyResult = this.fixNumericKeys(conn);
|
||||||
|
if (numericKeyResult.length > 0) {
|
||||||
|
fixes.push(...numericKeyResult);
|
||||||
|
anyFixed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fix ID-to-name references (source keys and .node values)
|
||||||
|
if (!config.fixTypes || config.fixTypes.includes('connection-id-to-name')) {
|
||||||
|
const idToNameResult = this.fixIdToName(conn, idToNameMap, nameSet);
|
||||||
|
if (idToNameResult.length > 0) {
|
||||||
|
fixes.push(...idToNameResult);
|
||||||
|
anyFixed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fix invalid connection types
|
||||||
|
if (!config.fixTypes || config.fixTypes.includes('connection-invalid-type')) {
|
||||||
|
const invalidTypeResult = this.fixInvalidTypes(conn);
|
||||||
|
if (invalidTypeResult.length > 0) {
|
||||||
|
fixes.push(...invalidTypeResult);
|
||||||
|
anyFixed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fix out-of-bounds input indices
|
||||||
|
if (!config.fixTypes || config.fixTypes.includes('connection-input-index')) {
|
||||||
|
const inputIndexResult = this.fixInputIndices(conn, validationResult, workflow);
|
||||||
|
if (inputIndexResult.length > 0) {
|
||||||
|
fixes.push(...inputIndexResult);
|
||||||
|
anyFixed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Dedup identical connection entries
|
||||||
|
if (!config.fixTypes || config.fixTypes.includes('connection-duplicate-removal')) {
|
||||||
|
const dedupResult = this.fixDuplicateConnections(conn);
|
||||||
|
if (dedupResult.length > 0) {
|
||||||
|
fixes.push(...dedupResult);
|
||||||
|
anyFixed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyFixed) {
|
||||||
|
const op: ReplaceConnectionsOperation = {
|
||||||
|
type: 'replaceConnections',
|
||||||
|
connections: conn
|
||||||
|
};
|
||||||
|
operations.push(op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix numeric connection output keys ("0", "1" → main[0], main[1])
|
||||||
|
*/
|
||||||
|
private fixNumericKeys(conn: any): FixOperation[] {
|
||||||
|
const fixes: FixOperation[] = [];
|
||||||
|
const sourceNodes = Object.keys(conn);
|
||||||
|
|
||||||
|
for (const sourceName of sourceNodes) {
|
||||||
|
const nodeConn = conn[sourceName];
|
||||||
|
const numericKeys = Object.keys(nodeConn).filter(k => /^\d+$/.test(k));
|
||||||
|
|
||||||
|
if (numericKeys.length === 0) continue;
|
||||||
|
|
||||||
|
// Ensure main array exists
|
||||||
|
if (!nodeConn['main']) {
|
||||||
|
nodeConn['main'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const numKey of numericKeys) {
|
||||||
|
const index = parseInt(numKey, 10);
|
||||||
|
const entries = nodeConn[numKey];
|
||||||
|
|
||||||
|
// Extend main array if needed (fill gaps with empty arrays)
|
||||||
|
while (nodeConn['main'].length <= index) {
|
||||||
|
nodeConn['main'].push([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge entries into main[index]
|
||||||
|
const hadExisting = nodeConn['main'][index] && nodeConn['main'][index].length > 0;
|
||||||
|
if (Array.isArray(entries)) {
|
||||||
|
for (const outputGroup of entries) {
|
||||||
|
if (Array.isArray(outputGroup)) {
|
||||||
|
nodeConn['main'][index] = [
|
||||||
|
...nodeConn['main'][index],
|
||||||
|
...outputGroup
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hadExisting) {
|
||||||
|
logger.warn(`Merged numeric key "${numKey}" into existing main[${index}] on node "${sourceName}" - dedup pass will clean exact duplicates`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fixes.push({
|
||||||
|
node: sourceName,
|
||||||
|
field: `connections.${sourceName}.${numKey}`,
|
||||||
|
type: 'connection-numeric-keys',
|
||||||
|
before: numKey,
|
||||||
|
after: `main[${index}]`,
|
||||||
|
confidence: hadExisting ? 'medium' : 'high',
|
||||||
|
description: hadExisting
|
||||||
|
? `Merged numeric connection key "${numKey}" into existing main[${index}] on node "${sourceName}"`
|
||||||
|
: `Converted numeric connection key "${numKey}" to main[${index}] on node "${sourceName}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
delete nodeConn[numKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix node ID references in connections (replace IDs with names)
|
||||||
|
*/
|
||||||
|
private fixIdToName(
|
||||||
|
conn: any,
|
||||||
|
idToNameMap: Map<string, string>,
|
||||||
|
nameSet: Set<string>
|
||||||
|
): FixOperation[] {
|
||||||
|
const fixes: FixOperation[] = [];
|
||||||
|
|
||||||
|
// Build rename plan for source keys, then check for collisions
|
||||||
|
const renames: Array<{ oldKey: string; newKey: string }> = [];
|
||||||
|
const sourceKeys = Object.keys(conn);
|
||||||
|
for (const sourceKey of sourceKeys) {
|
||||||
|
if (idToNameMap.has(sourceKey) && !nameSet.has(sourceKey)) {
|
||||||
|
renames.push({ oldKey: sourceKey, newKey: idToNameMap.get(sourceKey)! });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for collisions among renames (two IDs mapping to the same name)
|
||||||
|
const newKeyCount = new Map<string, number>();
|
||||||
|
for (const r of renames) {
|
||||||
|
newKeyCount.set(r.newKey, (newKeyCount.get(r.newKey) || 0) + 1);
|
||||||
|
}
|
||||||
|
const safeRenames = renames.filter(r => {
|
||||||
|
if ((newKeyCount.get(r.newKey) || 0) > 1) {
|
||||||
|
logger.warn(`Skipping ambiguous ID-to-name rename: "${r.oldKey}" → "${r.newKey}" (multiple IDs map to same name)`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const { oldKey, newKey } of safeRenames) {
|
||||||
|
conn[newKey] = conn[oldKey];
|
||||||
|
delete conn[oldKey];
|
||||||
|
fixes.push({
|
||||||
|
node: newKey,
|
||||||
|
field: `connections.sourceKey`,
|
||||||
|
type: 'connection-id-to-name',
|
||||||
|
before: oldKey,
|
||||||
|
after: newKey,
|
||||||
|
confidence: 'high',
|
||||||
|
description: `Replaced node ID "${oldKey}" with name "${newKey}" as connection source key`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix .node values that are node IDs
|
||||||
|
for (const sourceName of Object.keys(conn)) {
|
||||||
|
const nodeConn = conn[sourceName];
|
||||||
|
for (const outputKey of Object.keys(nodeConn)) {
|
||||||
|
const outputs = nodeConn[outputKey];
|
||||||
|
if (!Array.isArray(outputs)) continue;
|
||||||
|
for (const outputGroup of outputs) {
|
||||||
|
if (!Array.isArray(outputGroup)) continue;
|
||||||
|
for (const entry of outputGroup) {
|
||||||
|
if (entry && entry.node && idToNameMap.has(entry.node) && !nameSet.has(entry.node)) {
|
||||||
|
const oldNode = entry.node;
|
||||||
|
const newNode = idToNameMap.get(entry.node)!;
|
||||||
|
entry.node = newNode;
|
||||||
|
fixes.push({
|
||||||
|
node: sourceName,
|
||||||
|
field: `connections.${sourceName}.${outputKey}[].node`,
|
||||||
|
type: 'connection-id-to-name',
|
||||||
|
before: oldNode,
|
||||||
|
after: newNode,
|
||||||
|
confidence: 'high',
|
||||||
|
description: `Replaced target node ID "${oldNode}" with name "${newNode}" in connection from "${sourceName}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix invalid connection types in entries (e.g., type:"0" → type:"main")
|
||||||
|
*/
|
||||||
|
private fixInvalidTypes(conn: any): FixOperation[] {
|
||||||
|
const fixes: FixOperation[] = [];
|
||||||
|
|
||||||
|
for (const sourceName of Object.keys(conn)) {
|
||||||
|
const nodeConn = conn[sourceName];
|
||||||
|
for (const outputKey of Object.keys(nodeConn)) {
|
||||||
|
const outputs = nodeConn[outputKey];
|
||||||
|
if (!Array.isArray(outputs)) continue;
|
||||||
|
for (const outputGroup of outputs) {
|
||||||
|
if (!Array.isArray(outputGroup)) continue;
|
||||||
|
for (const entry of outputGroup) {
|
||||||
|
if (entry && entry.type && !VALID_CONNECTION_TYPES.has(entry.type)) {
|
||||||
|
const oldType = entry.type;
|
||||||
|
// Use the parent output key if it's valid, otherwise default to "main"
|
||||||
|
const newType = VALID_CONNECTION_TYPES.has(outputKey) ? outputKey : 'main';
|
||||||
|
entry.type = newType;
|
||||||
|
fixes.push({
|
||||||
|
node: sourceName,
|
||||||
|
field: `connections.${sourceName}.${outputKey}[].type`,
|
||||||
|
type: 'connection-invalid-type',
|
||||||
|
before: oldType,
|
||||||
|
after: newType,
|
||||||
|
confidence: 'high',
|
||||||
|
description: `Fixed invalid connection type "${oldType}" → "${newType}" in connection from "${sourceName}" to "${entry.node}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix out-of-bounds input indices (clamp to valid range)
|
||||||
|
*/
|
||||||
|
private fixInputIndices(
|
||||||
|
conn: any,
|
||||||
|
validationResult: WorkflowValidationResult,
|
||||||
|
workflow: Workflow
|
||||||
|
): FixOperation[] {
|
||||||
|
const fixes: FixOperation[] = [];
|
||||||
|
|
||||||
|
// Parse INPUT_INDEX_OUT_OF_BOUNDS errors from validation
|
||||||
|
for (const error of validationResult.errors) {
|
||||||
|
if (error.code !== 'INPUT_INDEX_OUT_OF_BOUNDS') continue;
|
||||||
|
|
||||||
|
const targetNodeName = error.nodeName;
|
||||||
|
if (!targetNodeName) continue;
|
||||||
|
|
||||||
|
// Extract the bad index and input count from the error message
|
||||||
|
const match = error.message.match(/Input index (\d+).*?has (\d+) main input/);
|
||||||
|
if (!match) {
|
||||||
|
logger.warn(`Could not parse INPUT_INDEX_OUT_OF_BOUNDS error for node "${targetNodeName}": ${error.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const badIndex = parseInt(match[1], 10);
|
||||||
|
const inputCount = parseInt(match[2], 10);
|
||||||
|
|
||||||
|
// For multi-input nodes, clamp to max valid index; for single-input, reset to 0
|
||||||
|
const clampedIndex = inputCount > 1 ? Math.min(badIndex, inputCount - 1) : 0;
|
||||||
|
|
||||||
|
// Find and fix the bad index in connections
|
||||||
|
for (const sourceName of Object.keys(conn)) {
|
||||||
|
const nodeConn = conn[sourceName];
|
||||||
|
for (const outputKey of Object.keys(nodeConn)) {
|
||||||
|
const outputs = nodeConn[outputKey];
|
||||||
|
if (!Array.isArray(outputs)) continue;
|
||||||
|
for (const outputGroup of outputs) {
|
||||||
|
if (!Array.isArray(outputGroup)) continue;
|
||||||
|
for (const entry of outputGroup) {
|
||||||
|
if (entry && entry.node === targetNodeName && entry.index === badIndex) {
|
||||||
|
entry.index = clampedIndex;
|
||||||
|
fixes.push({
|
||||||
|
node: sourceName,
|
||||||
|
field: `connections.${sourceName}.${outputKey}[].index`,
|
||||||
|
type: 'connection-input-index',
|
||||||
|
before: badIndex,
|
||||||
|
after: clampedIndex,
|
||||||
|
confidence: 'medium',
|
||||||
|
description: `Clamped input index ${badIndex} → ${clampedIndex} for target node "${targetNodeName}" (has ${inputCount} input${inputCount === 1 ? '' : 's'})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove duplicate connection entries (same node, type, index)
|
||||||
|
*/
|
||||||
|
private fixDuplicateConnections(conn: any): FixOperation[] {
|
||||||
|
const fixes: FixOperation[] = [];
|
||||||
|
|
||||||
|
for (const sourceName of Object.keys(conn)) {
|
||||||
|
const nodeConn = conn[sourceName];
|
||||||
|
for (const outputKey of Object.keys(nodeConn)) {
|
||||||
|
const outputs = nodeConn[outputKey];
|
||||||
|
if (!Array.isArray(outputs)) continue;
|
||||||
|
for (let i = 0; i < outputs.length; i++) {
|
||||||
|
const outputGroup = outputs[i];
|
||||||
|
if (!Array.isArray(outputGroup)) continue;
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const deduped: any[] = [];
|
||||||
|
|
||||||
|
for (const entry of outputGroup) {
|
||||||
|
const key = JSON.stringify({ node: entry.node, type: entry.type, index: entry.index });
|
||||||
|
if (seen.has(key)) {
|
||||||
|
fixes.push({
|
||||||
|
node: sourceName,
|
||||||
|
field: `connections.${sourceName}.${outputKey}[${i}]`,
|
||||||
|
type: 'connection-duplicate-removal',
|
||||||
|
before: entry,
|
||||||
|
after: null,
|
||||||
|
confidence: 'high',
|
||||||
|
description: `Removed duplicate connection from "${sourceName}" to "${entry.node}" (type: ${entry.type}, index: ${entry.index})`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
seen.add(key);
|
||||||
|
deduped.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs[i] = deduped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixes;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process version upgrade fixes (proactive upgrades to latest versions)
|
* Process version upgrade fixes (proactive upgrades to latest versions)
|
||||||
* HIGH confidence for non-breaking upgrades, MEDIUM for upgrades with auto-migratable changes
|
* HIGH confidence for non-breaking upgrades, MEDIUM for upgrades with auto-migratable changes
|
||||||
|
|||||||
@@ -38,11 +38,22 @@ import { isActivatableTrigger } from '../utils/node-type-utils';
|
|||||||
|
|
||||||
const logger = new Logger({ prefix: '[WorkflowDiffEngine]' });
|
const logger = new Logger({ prefix: '[WorkflowDiffEngine]' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not safe for concurrent use — create a new instance per request.
|
||||||
|
* Instance state is reset at the start of each applyDiff() call.
|
||||||
|
*/
|
||||||
export class WorkflowDiffEngine {
|
export class WorkflowDiffEngine {
|
||||||
// Track node name changes during operations for connection reference updates
|
// Track node name changes during operations for connection reference updates
|
||||||
private renameMap: Map<string, string> = new Map();
|
private renameMap: Map<string, string> = new Map();
|
||||||
// Track warnings during operation processing
|
// Track warnings during operation processing
|
||||||
private warnings: WorkflowDiffValidationError[] = [];
|
private warnings: WorkflowDiffValidationError[] = [];
|
||||||
|
// Track which nodes were added/updated so sanitization only runs on them
|
||||||
|
private modifiedNodeIds = new Set<string>();
|
||||||
|
// Track removed node names for better error messages
|
||||||
|
private removedNodeNames = new Set<string>();
|
||||||
|
// Track tag operations for dedicated API calls
|
||||||
|
private tagsToAdd: string[] = [];
|
||||||
|
private tagsToRemove: string[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply diff operations to a workflow
|
* Apply diff operations to a workflow
|
||||||
@@ -55,6 +66,10 @@ export class WorkflowDiffEngine {
|
|||||||
// Reset tracking for this diff operation
|
// Reset tracking for this diff operation
|
||||||
this.renameMap.clear();
|
this.renameMap.clear();
|
||||||
this.warnings = [];
|
this.warnings = [];
|
||||||
|
this.modifiedNodeIds.clear();
|
||||||
|
this.removedNodeNames.clear();
|
||||||
|
this.tagsToAdd = [];
|
||||||
|
this.tagsToRemove = [];
|
||||||
|
|
||||||
// Clone workflow to avoid modifying original
|
// Clone workflow to avoid modifying original
|
||||||
const workflowCopy = JSON.parse(JSON.stringify(workflow));
|
const workflowCopy = JSON.parse(JSON.stringify(workflow));
|
||||||
@@ -135,7 +150,9 @@ export class WorkflowDiffEngine {
|
|||||||
errors: errors.length > 0 ? errors : undefined,
|
errors: errors.length > 0 ? errors : undefined,
|
||||||
warnings: this.warnings.length > 0 ? this.warnings : undefined,
|
warnings: this.warnings.length > 0 ? this.warnings : undefined,
|
||||||
applied: appliedIndices,
|
applied: appliedIndices,
|
||||||
failed: failedIndices
|
failed: failedIndices,
|
||||||
|
tagsToAdd: this.tagsToAdd.length > 0 ? this.tagsToAdd : undefined,
|
||||||
|
tagsToRemove: this.tagsToRemove.length > 0 ? this.tagsToRemove : undefined
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Atomic mode: all operations must succeed
|
// Atomic mode: all operations must succeed
|
||||||
@@ -201,12 +218,16 @@ export class WorkflowDiffEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize ALL nodes in the workflow after operations are applied
|
// Sanitize only modified nodes to avoid breaking unrelated nodes (#592)
|
||||||
// This ensures existing invalid nodes (e.g., binary operators with singleValue: true)
|
if (this.modifiedNodeIds.size > 0) {
|
||||||
// are fixed automatically when any update is made to the workflow
|
workflowCopy.nodes = workflowCopy.nodes.map((node: WorkflowNode) => {
|
||||||
workflowCopy.nodes = workflowCopy.nodes.map((node: WorkflowNode) => sanitizeNode(node));
|
if (this.modifiedNodeIds.has(node.id)) {
|
||||||
|
return sanitizeNode(node);
|
||||||
logger.debug('Applied full-workflow sanitization to all nodes');
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
logger.debug(`Sanitized ${this.modifiedNodeIds.size} modified nodes`);
|
||||||
|
}
|
||||||
|
|
||||||
// If validateOnly flag is set, return success without applying
|
// If validateOnly flag is set, return success without applying
|
||||||
if (request.validateOnly) {
|
if (request.validateOnly) {
|
||||||
@@ -233,7 +254,9 @@ export class WorkflowDiffEngine {
|
|||||||
message: `Successfully applied ${operationsApplied} operations (${nodeOperations.length} node ops, ${otherOperations.length} other ops)`,
|
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,
|
shouldActivate: shouldActivate || undefined,
|
||||||
shouldDeactivate: shouldDeactivate || undefined
|
shouldDeactivate: shouldDeactivate || undefined,
|
||||||
|
tagsToAdd: this.tagsToAdd.length > 0 ? this.tagsToAdd : undefined,
|
||||||
|
tagsToRemove: this.tagsToRemove.length > 0 ? this.tagsToRemove : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -248,7 +271,6 @@ export class WorkflowDiffEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a single operation
|
* Validate a single operation
|
||||||
*/
|
*/
|
||||||
@@ -405,7 +427,7 @@ export class WorkflowDiffEngine {
|
|||||||
|
|
||||||
// Check for missing required parameter
|
// Check for missing required parameter
|
||||||
if (!operation.updates) {
|
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"}}`;
|
return `Missing required parameter 'updates'. The updateNode operation requires an 'updates' object. Correct structure: {type: "updateNode", nodeId: "abc-123" OR nodeName: "My Node", updates: {name: "New Name", "parameters.url": "https://example.com"}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||||
@@ -510,12 +532,18 @@ export class WorkflowDiffEngine {
|
|||||||
const targetNode = this.findNode(workflow, operation.target, operation.target);
|
const targetNode = this.findNode(workflow, operation.target, operation.target);
|
||||||
|
|
||||||
if (!sourceNode) {
|
if (!sourceNode) {
|
||||||
|
if (this.removedNodeNames.has(operation.source)) {
|
||||||
|
return `Source node "${operation.source}" was already removed by a prior removeNode operation. Its connections were automatically cleaned up — no separate removeConnection needed.`;
|
||||||
|
}
|
||||||
const availableNodes = workflow.nodes
|
const availableNodes = workflow.nodes
|
||||||
.map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
|
.map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
return `Source node not found: "${operation.source}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters.`;
|
return `Source node not found: "${operation.source}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters.`;
|
||||||
}
|
}
|
||||||
if (!targetNode) {
|
if (!targetNode) {
|
||||||
|
if (this.removedNodeNames.has(operation.target)) {
|
||||||
|
return `Target node "${operation.target}" was already removed by a prior removeNode operation. Its connections were automatically cleaned up — no separate removeConnection needed.`;
|
||||||
|
}
|
||||||
const availableNodes = workflow.nodes
|
const availableNodes = workflow.nodes
|
||||||
.map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
|
.map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
@@ -614,13 +642,16 @@ export class WorkflowDiffEngine {
|
|||||||
// Sanitize node to ensure complete metadata (filter options, operator structure, etc.)
|
// Sanitize node to ensure complete metadata (filter options, operator structure, etc.)
|
||||||
const sanitizedNode = sanitizeNode(newNode);
|
const sanitizedNode = sanitizeNode(newNode);
|
||||||
|
|
||||||
|
this.modifiedNodeIds.add(sanitizedNode.id);
|
||||||
workflow.nodes.push(sanitizedNode);
|
workflow.nodes.push(sanitizedNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyRemoveNode(workflow: Workflow, operation: RemoveNodeOperation): void {
|
private applyRemoveNode(workflow: Workflow, operation: RemoveNodeOperation): void {
|
||||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
|
this.removedNodeNames.add(node.name);
|
||||||
|
|
||||||
// Remove node from array
|
// Remove node from array
|
||||||
const index = workflow.nodes.findIndex(n => n.id === node.id);
|
const index = workflow.nodes.findIndex(n => n.id === node.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
@@ -631,30 +662,36 @@ export class WorkflowDiffEngine {
|
|||||||
delete workflow.connections[node.name];
|
delete workflow.connections[node.name];
|
||||||
|
|
||||||
// Remove all connections to this node
|
// Remove all connections to this node
|
||||||
Object.keys(workflow.connections).forEach(sourceName => {
|
for (const [sourceName, sourceConnections] of Object.entries(workflow.connections)) {
|
||||||
const sourceConnections = workflow.connections[sourceName];
|
for (const [outputName, outputConns] of Object.entries(sourceConnections)) {
|
||||||
Object.keys(sourceConnections).forEach(outputName => {
|
sourceConnections[outputName] = outputConns.map(connections =>
|
||||||
sourceConnections[outputName] = sourceConnections[outputName].map(connections =>
|
|
||||||
connections.filter(conn => conn.node !== node.name)
|
connections.filter(conn => conn.node !== node.name)
|
||||||
).filter(connections => connections.length > 0);
|
);
|
||||||
|
|
||||||
// Clean up empty arrays
|
// Trim trailing empty arrays only (preserve intermediate empty arrays for positional indices)
|
||||||
if (sourceConnections[outputName].length === 0) {
|
const trimmed = sourceConnections[outputName];
|
||||||
|
while (trimmed.length > 0 && trimmed[trimmed.length - 1].length === 0) {
|
||||||
|
trimmed.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.length === 0) {
|
||||||
delete sourceConnections[outputName];
|
delete sourceConnections[outputName];
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Clean up empty connection objects
|
// Clean up empty connection objects
|
||||||
if (Object.keys(sourceConnections).length === 0) {
|
if (Object.keys(sourceConnections).length === 0) {
|
||||||
delete workflow.connections[sourceName];
|
delete workflow.connections[sourceName];
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyUpdateNode(workflow: Workflow, operation: UpdateNodeOperation): void {
|
private applyUpdateNode(workflow: Workflow, operation: UpdateNodeOperation): void {
|
||||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
|
this.modifiedNodeIds.add(node.id);
|
||||||
|
|
||||||
// Track node renames for connection reference updates
|
// Track node renames for connection reference updates
|
||||||
if (operation.updates.name && operation.updates.name !== node.name) {
|
if (operation.updates.name && operation.updates.name !== node.name) {
|
||||||
const oldName = node.name;
|
const oldName = node.name;
|
||||||
@@ -706,10 +743,18 @@ export class WorkflowDiffEngine {
|
|||||||
): { sourceOutput: string; sourceIndex: number } {
|
): { sourceOutput: string; sourceIndex: number } {
|
||||||
const sourceNode = this.findNode(workflow, operation.source, operation.source);
|
const sourceNode = this.findNode(workflow, operation.source, operation.source);
|
||||||
|
|
||||||
// Start with explicit values or defaults
|
// Start with explicit values or defaults, coercing to correct types
|
||||||
let sourceOutput = operation.sourceOutput ?? 'main';
|
let sourceOutput = String(operation.sourceOutput ?? 'main');
|
||||||
let sourceIndex = operation.sourceIndex ?? 0;
|
let sourceIndex = operation.sourceIndex ?? 0;
|
||||||
|
|
||||||
|
// Remap numeric sourceOutput (e.g., "0", "1") to "main" with sourceIndex (#537)
|
||||||
|
// Skip when smart parameters (branch, case) are present — they take precedence
|
||||||
|
if (/^\d+$/.test(sourceOutput) && operation.sourceIndex === undefined
|
||||||
|
&& operation.branch === undefined && operation.case === undefined) {
|
||||||
|
sourceIndex = parseInt(sourceOutput, 10);
|
||||||
|
sourceOutput = 'main';
|
||||||
|
}
|
||||||
|
|
||||||
// Smart parameter: branch (for IF nodes)
|
// Smart parameter: branch (for IF nodes)
|
||||||
// IF nodes use 'main' output with index 0 (true) or 1 (false)
|
// IF nodes use 'main' output with index 0 (true) or 1 (false)
|
||||||
if (operation.branch !== undefined && operation.sourceIndex === undefined) {
|
if (operation.branch !== undefined && operation.sourceIndex === undefined) {
|
||||||
@@ -758,7 +803,8 @@ export class WorkflowDiffEngine {
|
|||||||
|
|
||||||
// Use nullish coalescing to properly handle explicit 0 values
|
// Use nullish coalescing to properly handle explicit 0 values
|
||||||
// Default targetInput to sourceOutput to preserve connection type for AI connections (ai_tool, ai_memory, etc.)
|
// Default targetInput to sourceOutput to preserve connection type for AI connections (ai_tool, ai_memory, etc.)
|
||||||
const targetInput = operation.targetInput ?? sourceOutput;
|
// Coerce to string to handle numeric values passed as sourceOutput/targetInput
|
||||||
|
const targetInput = String(operation.targetInput ?? sourceOutput);
|
||||||
const targetIndex = operation.targetIndex ?? 0;
|
const targetIndex = operation.targetIndex ?? 0;
|
||||||
|
|
||||||
// Initialize source node connections object
|
// Initialize source node connections object
|
||||||
@@ -795,18 +841,14 @@ export class WorkflowDiffEngine {
|
|||||||
private applyRemoveConnection(workflow: Workflow, operation: RemoveConnectionOperation): void {
|
private applyRemoveConnection(workflow: Workflow, operation: RemoveConnectionOperation): void {
|
||||||
const sourceNode = this.findNode(workflow, operation.source, operation.source);
|
const sourceNode = this.findNode(workflow, operation.source, operation.source);
|
||||||
const targetNode = this.findNode(workflow, operation.target, operation.target);
|
const targetNode = this.findNode(workflow, operation.target, operation.target);
|
||||||
// If ignoreErrors is true, silently succeed even if nodes don't exist
|
|
||||||
if (!sourceNode || !targetNode) {
|
if (!sourceNode || !targetNode) {
|
||||||
if (operation.ignoreErrors) {
|
return;
|
||||||
return; // Gracefully handle missing nodes
|
|
||||||
}
|
|
||||||
return; // Should never reach here if validation passed, but safety check
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceOutput = operation.sourceOutput || 'main';
|
const sourceOutput = String(operation.sourceOutput ?? 'main');
|
||||||
const connections = workflow.connections[sourceNode.name]?.[sourceOutput];
|
const connections = workflow.connections[sourceNode.name]?.[sourceOutput];
|
||||||
if (!connections) return;
|
if (!connections) return;
|
||||||
|
|
||||||
// Remove connection from all indices
|
// Remove connection from all indices
|
||||||
workflow.connections[sourceNode.name][sourceOutput] = connections.map(conns =>
|
workflow.connections[sourceNode.name][sourceOutput] = connections.map(conns =>
|
||||||
conns.filter(conn => conn.node !== targetNode.name)
|
conns.filter(conn => conn.node !== targetNode.name)
|
||||||
@@ -877,20 +919,26 @@ export class WorkflowDiffEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private applyAddTag(workflow: Workflow, operation: AddTagOperation): void {
|
private applyAddTag(workflow: Workflow, operation: AddTagOperation): void {
|
||||||
if (!workflow.tags) {
|
// Track for dedicated API call instead of modifying workflow.tags directly
|
||||||
workflow.tags = [];
|
// Reconcile: if previously marked for removal, cancel the removal instead
|
||||||
|
const removeIdx = this.tagsToRemove.indexOf(operation.tag);
|
||||||
|
if (removeIdx !== -1) {
|
||||||
|
this.tagsToRemove.splice(removeIdx, 1);
|
||||||
}
|
}
|
||||||
if (!workflow.tags.includes(operation.tag)) {
|
if (!this.tagsToAdd.includes(operation.tag)) {
|
||||||
workflow.tags.push(operation.tag);
|
this.tagsToAdd.push(operation.tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyRemoveTag(workflow: Workflow, operation: RemoveTagOperation): void {
|
private applyRemoveTag(workflow: Workflow, operation: RemoveTagOperation): void {
|
||||||
if (!workflow.tags) return;
|
// Track for dedicated API call instead of modifying workflow.tags directly
|
||||||
|
// Reconcile: if previously marked for addition, cancel the addition instead
|
||||||
const index = workflow.tags.indexOf(operation.tag);
|
const addIdx = this.tagsToAdd.indexOf(operation.tag);
|
||||||
if (index !== -1) {
|
if (addIdx !== -1) {
|
||||||
workflow.tags.splice(index, 1);
|
this.tagsToAdd.splice(addIdx, 1);
|
||||||
|
}
|
||||||
|
if (!this.tagsToRemove.includes(operation.tag)) {
|
||||||
|
this.tagsToRemove.push(operation.tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,7 +1063,12 @@ export class WorkflowDiffEngine {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
).filter(conns => conns.length > 0);
|
);
|
||||||
|
|
||||||
|
// Trim trailing empty arrays only (preserve intermediate for positional indices)
|
||||||
|
while (filteredConnections.length > 0 && filteredConnections[filteredConnections.length - 1].length === 0) {
|
||||||
|
filteredConnections.pop();
|
||||||
|
}
|
||||||
|
|
||||||
if (filteredConnections.length === 0) {
|
if (filteredConnections.length === 0) {
|
||||||
delete outputs[outputName];
|
delete outputs[outputName];
|
||||||
|
|||||||
@@ -11,13 +11,28 @@ import { ExpressionFormatValidator } from './expression-format-validator';
|
|||||||
import { NodeSimilarityService, NodeSuggestion } from './node-similarity-service';
|
import { NodeSimilarityService, NodeSuggestion } from './node-similarity-service';
|
||||||
import { NodeTypeNormalizer } from '../utils/node-type-normalizer';
|
import { NodeTypeNormalizer } from '../utils/node-type-normalizer';
|
||||||
import { Logger } from '../utils/logger';
|
import { Logger } from '../utils/logger';
|
||||||
import { validateAISpecificNodes, hasAINodes } from './ai-node-validator';
|
import { validateAISpecificNodes, hasAINodes, AI_CONNECTION_TYPES } from './ai-node-validator';
|
||||||
import { isAIToolSubNode } from './ai-tool-validators';
|
import { isAIToolSubNode } from './ai-tool-validators';
|
||||||
import { isTriggerNode } from '../utils/node-type-utils';
|
import { isTriggerNode } from '../utils/node-type-utils';
|
||||||
import { isNonExecutableNode } from '../utils/node-classification';
|
import { isNonExecutableNode } from '../utils/node-classification';
|
||||||
import { ToolVariantGenerator } from './tool-variant-generator';
|
import { ToolVariantGenerator } from './tool-variant-generator';
|
||||||
const logger = new Logger({ prefix: '[WorkflowValidator]' });
|
const logger = new Logger({ prefix: '[WorkflowValidator]' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All valid connection output keys in n8n workflows.
|
||||||
|
* Any key not in this set is malformed and should be flagged.
|
||||||
|
*/
|
||||||
|
export const VALID_CONNECTION_TYPES = new Set<string>([
|
||||||
|
'main',
|
||||||
|
'error',
|
||||||
|
...AI_CONNECTION_TYPES,
|
||||||
|
// Additional AI types from n8n-workflow NodeConnectionTypes not in AI_CONNECTION_TYPES
|
||||||
|
'ai_agent',
|
||||||
|
'ai_chain',
|
||||||
|
'ai_retriever',
|
||||||
|
'ai_reranker',
|
||||||
|
]);
|
||||||
|
|
||||||
interface WorkflowNode {
|
interface WorkflowNode {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -40,9 +55,7 @@ interface WorkflowNode {
|
|||||||
|
|
||||||
interface WorkflowConnection {
|
interface WorkflowConnection {
|
||||||
[sourceNode: string]: {
|
[sourceNode: string]: {
|
||||||
main?: Array<Array<{ node: string; type: string; index: number }>>;
|
[outputType: string]: Array<Array<{ node: string; type: string; index: number }>>;
|
||||||
error?: Array<Array<{ node: string; type: string; index: number }>>;
|
|
||||||
ai_tool?: Array<Array<{ node: string; type: string; index: number }>>;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,86 +625,52 @@ export class WorkflowValidator {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check main outputs
|
// Detect unknown output keys and validate known ones
|
||||||
if (outputs.main) {
|
for (const [outputKey, outputConnections] of Object.entries(outputs)) {
|
||||||
this.validateConnectionOutputs(
|
if (!VALID_CONNECTION_TYPES.has(outputKey)) {
|
||||||
sourceName,
|
// Flag unknown connection output key
|
||||||
outputs.main,
|
let suggestion = '';
|
||||||
nodeMap,
|
if (/^\d+$/.test(outputKey)) {
|
||||||
nodeIdMap,
|
suggestion = ` If you meant to use output index ${outputKey}, use main[${outputKey}] instead.`;
|
||||||
result,
|
}
|
||||||
'main'
|
result.errors.push({
|
||||||
);
|
type: 'error',
|
||||||
}
|
nodeName: sourceName,
|
||||||
|
message: `Unknown connection output key "${outputKey}" on node "${sourceName}". Valid keys are: ${[...VALID_CONNECTION_TYPES].join(', ')}.${suggestion}`,
|
||||||
|
code: 'UNKNOWN_CONNECTION_KEY'
|
||||||
|
});
|
||||||
|
result.statistics.invalidConnections++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Check error outputs
|
if (!outputConnections || !Array.isArray(outputConnections)) continue;
|
||||||
if (outputs.error) {
|
|
||||||
this.validateConnectionOutputs(
|
|
||||||
sourceName,
|
|
||||||
outputs.error,
|
|
||||||
nodeMap,
|
|
||||||
nodeIdMap,
|
|
||||||
result,
|
|
||||||
'error'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check AI tool outputs
|
|
||||||
if (outputs.ai_tool) {
|
|
||||||
// Validate that the source node can actually output ai_tool
|
// Validate that the source node can actually output ai_tool
|
||||||
this.validateAIToolSource(sourceNode, result);
|
if (outputKey === 'ai_tool') {
|
||||||
|
this.validateAIToolSource(sourceNode, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that AI sub-nodes are not connected via main
|
||||||
|
if (outputKey === 'main') {
|
||||||
|
this.validateNotAISubNode(sourceNode, result);
|
||||||
|
}
|
||||||
|
|
||||||
this.validateConnectionOutputs(
|
this.validateConnectionOutputs(
|
||||||
sourceName,
|
sourceName,
|
||||||
outputs.ai_tool,
|
outputConnections,
|
||||||
nodeMap,
|
nodeMap,
|
||||||
nodeIdMap,
|
nodeIdMap,
|
||||||
result,
|
result,
|
||||||
'ai_tool'
|
outputKey
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for orphaned nodes (not connected and not triggers)
|
// Trigger reachability analysis: BFS from all triggers to find unreachable nodes
|
||||||
const connectedNodes = new Set<string>();
|
if (profile !== 'minimal') {
|
||||||
|
this.validateTriggerReachability(workflow, result);
|
||||||
// Add all source nodes
|
} else {
|
||||||
Object.keys(workflow.connections).forEach(name => connectedNodes.add(name));
|
this.flagOrphanedNodes(workflow, result);
|
||||||
|
|
||||||
// Add all target nodes
|
|
||||||
Object.values(workflow.connections).forEach(outputs => {
|
|
||||||
if (outputs.main) {
|
|
||||||
outputs.main.flat().forEach(conn => {
|
|
||||||
if (conn) connectedNodes.add(conn.node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (outputs.error) {
|
|
||||||
outputs.error.flat().forEach(conn => {
|
|
||||||
if (conn) connectedNodes.add(conn.node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (outputs.ai_tool) {
|
|
||||||
outputs.ai_tool.flat().forEach(conn => {
|
|
||||||
if (conn) connectedNodes.add(conn.node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for orphaned nodes (exclude sticky notes)
|
|
||||||
for (const node of workflow.nodes) {
|
|
||||||
if (node.disabled || isNonExecutableNode(node.type)) continue;
|
|
||||||
|
|
||||||
// Use shared trigger detection function for consistency
|
|
||||||
const isNodeTrigger = isTriggerNode(node.type);
|
|
||||||
|
|
||||||
if (!connectedNodes.has(node.name) && !isNodeTrigger) {
|
|
||||||
result.warnings.push({
|
|
||||||
type: 'warning',
|
|
||||||
nodeId: node.id,
|
|
||||||
nodeName: node.name,
|
|
||||||
message: 'Node is not connected to any other nodes'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for cycles (skip in minimal profile to reduce false positives)
|
// Check for cycles (skip in minimal profile to reduce false positives)
|
||||||
@@ -712,19 +691,21 @@ export class WorkflowValidator {
|
|||||||
nodeMap: Map<string, WorkflowNode>,
|
nodeMap: Map<string, WorkflowNode>,
|
||||||
nodeIdMap: Map<string, WorkflowNode>,
|
nodeIdMap: Map<string, WorkflowNode>,
|
||||||
result: WorkflowValidationResult,
|
result: WorkflowValidationResult,
|
||||||
outputType: 'main' | 'error' | 'ai_tool'
|
outputType: string
|
||||||
): void {
|
): void {
|
||||||
// Get source node for special validation
|
// Get source node for special validation
|
||||||
const sourceNode = nodeMap.get(sourceName);
|
const sourceNode = nodeMap.get(sourceName);
|
||||||
|
|
||||||
// Special validation for main outputs with error handling
|
// Main-output-specific validation: error handling config and index bounds
|
||||||
if (outputType === 'main' && sourceNode) {
|
if (outputType === 'main' && sourceNode) {
|
||||||
this.validateErrorOutputConfiguration(sourceName, sourceNode, outputs, nodeMap, result);
|
this.validateErrorOutputConfiguration(sourceName, sourceNode, outputs, nodeMap, result);
|
||||||
|
this.validateOutputIndexBounds(sourceNode, outputs, result);
|
||||||
|
this.validateConditionalBranchUsage(sourceNode, outputs, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
outputs.forEach((outputConnections, outputIndex) => {
|
outputs.forEach((outputConnections, outputIndex) => {
|
||||||
if (!outputConnections) return;
|
if (!outputConnections) return;
|
||||||
|
|
||||||
outputConnections.forEach(connection => {
|
outputConnections.forEach(connection => {
|
||||||
// Check for negative index
|
// Check for negative index
|
||||||
if (connection.index < 0) {
|
if (connection.index < 0) {
|
||||||
@@ -736,6 +717,22 @@ export class WorkflowValidator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate connection type field
|
||||||
|
if (connection.type && !VALID_CONNECTION_TYPES.has(connection.type)) {
|
||||||
|
let suggestion = '';
|
||||||
|
if (/^\d+$/.test(connection.type)) {
|
||||||
|
suggestion = ` Numeric types are not valid - use "main", "error", or an AI connection type.`;
|
||||||
|
}
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
nodeName: sourceName,
|
||||||
|
message: `Invalid connection type "${connection.type}" in connection from "${sourceName}" to "${connection.node}". Expected "main", "error", or an AI connection type (ai_tool, ai_languageModel, etc.).${suggestion}`,
|
||||||
|
code: 'INVALID_CONNECTION_TYPE'
|
||||||
|
});
|
||||||
|
result.statistics.invalidConnections++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Special validation for SplitInBatches node
|
// Special validation for SplitInBatches node
|
||||||
// Check both full form (n8n-nodes-base.*) and short form (nodes-base.*)
|
// Check both full form (n8n-nodes-base.*) and short form (nodes-base.*)
|
||||||
const isSplitInBatches = sourceNode && (
|
const isSplitInBatches = sourceNode && (
|
||||||
@@ -789,11 +786,16 @@ export class WorkflowValidator {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
result.statistics.validConnections++;
|
result.statistics.validConnections++;
|
||||||
|
|
||||||
// Additional validation for AI tool connections
|
// Additional validation for AI tool connections
|
||||||
if (outputType === 'ai_tool') {
|
if (outputType === 'ai_tool') {
|
||||||
this.validateAIToolConnection(sourceName, targetNode, result);
|
this.validateAIToolConnection(sourceName, targetNode, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Input index bounds checking
|
||||||
|
if (outputType === 'main') {
|
||||||
|
this.validateInputIndexBounds(sourceName, targetNode, connection, result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -991,6 +993,348 @@ export class WorkflowValidator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the static output types for a node from the database.
|
||||||
|
* Returns null if outputs contain expressions (dynamic) or node not found.
|
||||||
|
*/
|
||||||
|
private getNodeOutputTypes(nodeType: string): string[] | null {
|
||||||
|
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
||||||
|
const nodeInfo = this.nodeRepository.getNode(normalizedType);
|
||||||
|
if (!nodeInfo || !nodeInfo.outputs) return null;
|
||||||
|
|
||||||
|
const outputs = nodeInfo.outputs;
|
||||||
|
if (!Array.isArray(outputs)) return null;
|
||||||
|
|
||||||
|
// Skip if any output is an expression (dynamic — can't determine statically)
|
||||||
|
for (const output of outputs) {
|
||||||
|
if (typeof output === 'string' && output.startsWith('={{')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that AI sub-nodes (nodes that only output AI connection types)
|
||||||
|
* are not connected via "main" connections.
|
||||||
|
*/
|
||||||
|
private validateNotAISubNode(
|
||||||
|
sourceNode: WorkflowNode,
|
||||||
|
result: WorkflowValidationResult
|
||||||
|
): void {
|
||||||
|
const outputTypes = this.getNodeOutputTypes(sourceNode.type);
|
||||||
|
if (!outputTypes) return; // Unknown or dynamic — skip
|
||||||
|
|
||||||
|
// Check if the node outputs ONLY AI types (no 'main')
|
||||||
|
const hasMainOutput = outputTypes.some(t => t === 'main');
|
||||||
|
if (hasMainOutput) return; // Node can legitimately output main
|
||||||
|
|
||||||
|
// All outputs are AI types — this node should not be connected via main
|
||||||
|
const aiTypes = outputTypes.filter(t => t !== 'main');
|
||||||
|
const expectedType = aiTypes[0] || 'ai_languageModel';
|
||||||
|
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
nodeId: sourceNode.id,
|
||||||
|
nodeName: sourceNode.name,
|
||||||
|
message: `Node "${sourceNode.name}" (${sourceNode.type}) is an AI sub-node that outputs "${expectedType}" connections. ` +
|
||||||
|
`It cannot be used with "main" connections. Connect it to an AI Agent or Chain via "${expectedType}" instead.`,
|
||||||
|
code: 'AI_SUBNODE_MAIN_CONNECTION'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive the short node type name (e.g., "if", "switch", "set") from a workflow node.
|
||||||
|
*/
|
||||||
|
private getShortNodeType(sourceNode: WorkflowNode): string {
|
||||||
|
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(sourceNode.type);
|
||||||
|
return normalizedType.replace(/^(n8n-)?nodes-base\./, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the expected main output count for a conditional node (IF, Filter, Switch).
|
||||||
|
* Returns null for non-conditional nodes or when the count cannot be determined.
|
||||||
|
*/
|
||||||
|
private getConditionalOutputInfo(sourceNode: WorkflowNode): { shortType: string; expectedOutputs: number } | null {
|
||||||
|
const shortType = this.getShortNodeType(sourceNode);
|
||||||
|
|
||||||
|
if (shortType === 'if' || shortType === 'filter') {
|
||||||
|
return { shortType, expectedOutputs: 2 };
|
||||||
|
}
|
||||||
|
if (shortType === 'switch') {
|
||||||
|
const rules = sourceNode.parameters?.rules?.values || sourceNode.parameters?.rules;
|
||||||
|
if (Array.isArray(rules)) {
|
||||||
|
return { shortType, expectedOutputs: rules.length + 1 }; // rules + fallback
|
||||||
|
}
|
||||||
|
return null; // Cannot determine dynamic output count
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that output indices don't exceed what the node type supports.
|
||||||
|
*/
|
||||||
|
private validateOutputIndexBounds(
|
||||||
|
sourceNode: WorkflowNode,
|
||||||
|
outputs: Array<Array<{ node: string; type: string; index: number }>>,
|
||||||
|
result: WorkflowValidationResult
|
||||||
|
): void {
|
||||||
|
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(sourceNode.type);
|
||||||
|
const nodeInfo = this.nodeRepository.getNode(normalizedType);
|
||||||
|
if (!nodeInfo || !nodeInfo.outputs) return;
|
||||||
|
|
||||||
|
// Count main outputs from node description
|
||||||
|
let mainOutputCount: number;
|
||||||
|
if (Array.isArray(nodeInfo.outputs)) {
|
||||||
|
// outputs can be strings like "main" or objects with { type: "main" }
|
||||||
|
mainOutputCount = nodeInfo.outputs.filter((o: any) =>
|
||||||
|
typeof o === 'string' ? o === 'main' : (o.type === 'main' || !o.type)
|
||||||
|
).length;
|
||||||
|
} else {
|
||||||
|
return; // Dynamic outputs (expression string), skip check
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainOutputCount === 0) return;
|
||||||
|
|
||||||
|
// Override with dynamic output counts for conditional nodes
|
||||||
|
const conditionalInfo = this.getConditionalOutputInfo(sourceNode);
|
||||||
|
if (conditionalInfo) {
|
||||||
|
mainOutputCount = conditionalInfo.expectedOutputs;
|
||||||
|
} else if (this.getShortNodeType(sourceNode) === 'switch') {
|
||||||
|
// Switch without determinable rules -- skip bounds check
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account for continueErrorOutput adding an extra output
|
||||||
|
if (sourceNode.onError === 'continueErrorOutput') {
|
||||||
|
mainOutputCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any output index exceeds bounds
|
||||||
|
const maxOutputIndex = outputs.length - 1;
|
||||||
|
if (maxOutputIndex >= mainOutputCount) {
|
||||||
|
// Only flag if there are actual connections at the out-of-bounds indices
|
||||||
|
for (let i = mainOutputCount; i < outputs.length; i++) {
|
||||||
|
if (outputs[i] && outputs[i].length > 0) {
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
nodeId: sourceNode.id,
|
||||||
|
nodeName: sourceNode.name,
|
||||||
|
message: `Output index ${i} on node "${sourceNode.name}" exceeds its output count (${mainOutputCount}). ` +
|
||||||
|
`This node has ${mainOutputCount} main output(s) (indices 0-${mainOutputCount - 1}).`,
|
||||||
|
code: 'OUTPUT_INDEX_OUT_OF_BOUNDS'
|
||||||
|
});
|
||||||
|
result.statistics.invalidConnections++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect when a conditional node (IF, Filter, Switch) has all connections
|
||||||
|
* crammed into main[0] with higher-index outputs empty. This usually means
|
||||||
|
* both branches execute together on one condition, while the other branches
|
||||||
|
* have no effect.
|
||||||
|
*/
|
||||||
|
private validateConditionalBranchUsage(
|
||||||
|
sourceNode: WorkflowNode,
|
||||||
|
outputs: Array<Array<{ node: string; type: string; index: number }>>,
|
||||||
|
result: WorkflowValidationResult
|
||||||
|
): void {
|
||||||
|
const conditionalInfo = this.getConditionalOutputInfo(sourceNode);
|
||||||
|
if (!conditionalInfo || conditionalInfo.expectedOutputs < 2) return;
|
||||||
|
|
||||||
|
const { shortType, expectedOutputs } = conditionalInfo;
|
||||||
|
|
||||||
|
// Check: main[0] has >= 2 connections AND all main[1+] are empty
|
||||||
|
const main0Count = outputs[0]?.length || 0;
|
||||||
|
if (main0Count < 2) return;
|
||||||
|
|
||||||
|
const hasHigherIndexConnections = outputs.slice(1).some(
|
||||||
|
conns => conns && conns.length > 0
|
||||||
|
);
|
||||||
|
if (hasHigherIndexConnections) return;
|
||||||
|
|
||||||
|
// Build a context-appropriate warning message
|
||||||
|
let message: string;
|
||||||
|
if (shortType === 'if' || shortType === 'filter') {
|
||||||
|
const isFilter = shortType === 'filter';
|
||||||
|
const displayName = isFilter ? 'Filter' : 'IF';
|
||||||
|
const trueLabel = isFilter ? 'matched' : 'true';
|
||||||
|
const falseLabel = isFilter ? 'unmatched' : 'false';
|
||||||
|
message = `${displayName} node "${sourceNode.name}" has ${main0Count} connections on the "${trueLabel}" branch (main[0]) ` +
|
||||||
|
`but no connections on the "${falseLabel}" branch (main[1]). ` +
|
||||||
|
`All ${main0Count} target nodes execute together on the "${trueLabel}" branch, ` +
|
||||||
|
`while the "${falseLabel}" branch has no effect. ` +
|
||||||
|
`Split connections: main[0] for ${trueLabel}, main[1] for ${falseLabel}.`;
|
||||||
|
} else {
|
||||||
|
message = `Switch node "${sourceNode.name}" has ${main0Count} connections on output 0 ` +
|
||||||
|
`but no connections on any other outputs (1-${expectedOutputs - 1}). ` +
|
||||||
|
`All ${main0Count} target nodes execute together on output 0, ` +
|
||||||
|
`while other switch branches have no effect. ` +
|
||||||
|
`Distribute connections across outputs to match switch rules.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.warnings.push({
|
||||||
|
type: 'warning',
|
||||||
|
nodeId: sourceNode.id,
|
||||||
|
nodeName: sourceNode.name,
|
||||||
|
message,
|
||||||
|
code: 'CONDITIONAL_BRANCH_FANOUT'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that input index doesn't exceed what the target node accepts.
|
||||||
|
*/
|
||||||
|
private validateInputIndexBounds(
|
||||||
|
sourceName: string,
|
||||||
|
targetNode: WorkflowNode,
|
||||||
|
connection: { node: string; type: string; index: number },
|
||||||
|
result: WorkflowValidationResult
|
||||||
|
): void {
|
||||||
|
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(targetNode.type);
|
||||||
|
const nodeInfo = this.nodeRepository.getNode(normalizedType);
|
||||||
|
if (!nodeInfo) return;
|
||||||
|
|
||||||
|
// Most nodes have 1 main input. Known exceptions:
|
||||||
|
const shortType = normalizedType.replace(/^(n8n-)?nodes-base\./, '');
|
||||||
|
let mainInputCount = 1; // Default: most nodes have 1 input
|
||||||
|
|
||||||
|
if (shortType === 'merge' || shortType === 'compareDatasets') {
|
||||||
|
mainInputCount = 2; // Merge nodes have 2 inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger nodes have 0 inputs
|
||||||
|
if (nodeInfo.isTrigger || isTriggerNode(targetNode.type)) {
|
||||||
|
mainInputCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainInputCount > 0 && connection.index >= mainInputCount) {
|
||||||
|
result.errors.push({
|
||||||
|
type: 'error',
|
||||||
|
nodeName: targetNode.name,
|
||||||
|
message: `Input index ${connection.index} on node "${targetNode.name}" exceeds its input count (${mainInputCount}). ` +
|
||||||
|
`Connection from "${sourceName}" targets input ${connection.index}, but this node has ${mainInputCount} main input(s) (indices 0-${mainInputCount - 1}).`,
|
||||||
|
code: 'INPUT_INDEX_OUT_OF_BOUNDS'
|
||||||
|
});
|
||||||
|
result.statistics.invalidConnections++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag nodes that are not referenced in any connection (source or target).
|
||||||
|
* Used as a lightweight check when BFS reachability is not applicable.
|
||||||
|
*/
|
||||||
|
private flagOrphanedNodes(
|
||||||
|
workflow: WorkflowJson,
|
||||||
|
result: WorkflowValidationResult
|
||||||
|
): void {
|
||||||
|
const connectedNodes = new Set<string>();
|
||||||
|
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
|
||||||
|
connectedNodes.add(sourceName);
|
||||||
|
for (const outputConns of Object.values(outputs)) {
|
||||||
|
if (!Array.isArray(outputConns)) continue;
|
||||||
|
for (const conns of outputConns) {
|
||||||
|
if (!conns) continue;
|
||||||
|
for (const conn of conns) {
|
||||||
|
if (conn) connectedNodes.add(conn.node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of workflow.nodes) {
|
||||||
|
if (node.disabled || isNonExecutableNode(node.type)) continue;
|
||||||
|
if (isTriggerNode(node.type)) continue;
|
||||||
|
if (!connectedNodes.has(node.name)) {
|
||||||
|
result.warnings.push({
|
||||||
|
type: 'warning',
|
||||||
|
nodeId: node.id,
|
||||||
|
nodeName: node.name,
|
||||||
|
message: 'Node is not connected to any other nodes'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BFS from all trigger nodes to detect unreachable nodes.
|
||||||
|
* Replaces the simple "is node in any connection" check with proper graph traversal.
|
||||||
|
*/
|
||||||
|
private validateTriggerReachability(
|
||||||
|
workflow: WorkflowJson,
|
||||||
|
result: WorkflowValidationResult
|
||||||
|
): void {
|
||||||
|
// Build adjacency list (forward direction)
|
||||||
|
const adjacency = new Map<string, Set<string>>();
|
||||||
|
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
|
||||||
|
if (!adjacency.has(sourceName)) adjacency.set(sourceName, new Set());
|
||||||
|
for (const outputConns of Object.values(outputs)) {
|
||||||
|
if (Array.isArray(outputConns)) {
|
||||||
|
for (const conns of outputConns) {
|
||||||
|
if (!conns) continue;
|
||||||
|
for (const conn of conns) {
|
||||||
|
if (conn) {
|
||||||
|
adjacency.get(sourceName)!.add(conn.node);
|
||||||
|
// Also track that the target exists in the graph
|
||||||
|
if (!adjacency.has(conn.node)) adjacency.set(conn.node, new Set());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify trigger nodes
|
||||||
|
const triggerNodes: string[] = [];
|
||||||
|
for (const node of workflow.nodes) {
|
||||||
|
if (isTriggerNode(node.type) && !node.disabled) {
|
||||||
|
triggerNodes.push(node.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no trigger nodes, fall back to simple orphaned check
|
||||||
|
if (triggerNodes.length === 0) {
|
||||||
|
this.flagOrphanedNodes(workflow, result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BFS from all trigger nodes
|
||||||
|
const reachable = new Set<string>();
|
||||||
|
const queue: string[] = [...triggerNodes];
|
||||||
|
for (const t of triggerNodes) reachable.add(t);
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift()!;
|
||||||
|
const neighbors = adjacency.get(current);
|
||||||
|
if (neighbors) {
|
||||||
|
for (const neighbor of neighbors) {
|
||||||
|
if (!reachable.has(neighbor)) {
|
||||||
|
reachable.add(neighbor);
|
||||||
|
queue.push(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag unreachable nodes
|
||||||
|
for (const node of workflow.nodes) {
|
||||||
|
if (node.disabled || isNonExecutableNode(node.type)) continue;
|
||||||
|
if (isTriggerNode(node.type)) continue;
|
||||||
|
|
||||||
|
if (!reachable.has(node.name)) {
|
||||||
|
result.warnings.push({
|
||||||
|
type: 'warning',
|
||||||
|
nodeId: node.id,
|
||||||
|
nodeName: node.name,
|
||||||
|
message: 'Node is not reachable from any trigger node'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if workflow has cycles
|
* Check if workflow has cycles
|
||||||
* Allow legitimate loops for SplitInBatches and similar loop nodes
|
* Allow legitimate loops for SplitInBatches and similar loop nodes
|
||||||
@@ -1024,23 +1368,13 @@ export class WorkflowValidator {
|
|||||||
const connections = workflow.connections[nodeName];
|
const connections = workflow.connections[nodeName];
|
||||||
if (connections) {
|
if (connections) {
|
||||||
const allTargets: string[] = [];
|
const allTargets: string[] = [];
|
||||||
|
|
||||||
if (connections.main) {
|
for (const outputConns of Object.values(connections)) {
|
||||||
connections.main.flat().forEach(conn => {
|
if (Array.isArray(outputConns)) {
|
||||||
if (conn) allTargets.push(conn.node);
|
outputConns.flat().forEach(conn => {
|
||||||
});
|
if (conn) allTargets.push(conn.node);
|
||||||
}
|
});
|
||||||
|
}
|
||||||
if (connections.error) {
|
|
||||||
connections.error.flat().forEach(conn => {
|
|
||||||
if (conn) allTargets.push(conn.node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connections.ai_tool) {
|
|
||||||
connections.ai_tool.flat().forEach(conn => {
|
|
||||||
if (conn) allTargets.push(conn.node);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentNodeType = nodeTypeMap.get(nodeName);
|
const currentNodeType = nodeTypeMap.get(nodeName);
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ export class TelemetryBatchProcessor {
|
|||||||
private flushTimes: number[] = [];
|
private flushTimes: number[] = [];
|
||||||
private deadLetterQueue: (TelemetryEvent | WorkflowTelemetry | WorkflowMutationRecord)[] = [];
|
private deadLetterQueue: (TelemetryEvent | WorkflowTelemetry | WorkflowMutationRecord)[] = [];
|
||||||
private readonly maxDeadLetterSize = 100;
|
private readonly maxDeadLetterSize = 100;
|
||||||
|
// Track event listeners for proper cleanup to prevent memory leaks
|
||||||
|
private eventListeners: {
|
||||||
|
beforeExit?: () => void;
|
||||||
|
sigint?: () => void;
|
||||||
|
sigterm?: () => void;
|
||||||
|
} = {};
|
||||||
|
private started: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private supabase: SupabaseClient | null,
|
private supabase: SupabaseClient | null,
|
||||||
@@ -72,6 +79,12 @@ export class TelemetryBatchProcessor {
|
|||||||
start(): void {
|
start(): void {
|
||||||
if (!this.isEnabled() || !this.supabase) return;
|
if (!this.isEnabled() || !this.supabase) return;
|
||||||
|
|
||||||
|
// Guard against multiple starts (prevents event listener accumulation)
|
||||||
|
if (this.started) {
|
||||||
|
logger.debug('Telemetry batch processor already started, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Set up periodic flushing
|
// Set up periodic flushing
|
||||||
this.flushTimer = setInterval(() => {
|
this.flushTimer = setInterval(() => {
|
||||||
this.flush();
|
this.flush();
|
||||||
@@ -83,17 +96,22 @@ export class TelemetryBatchProcessor {
|
|||||||
this.flushTimer.unref();
|
this.flushTimer.unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up process exit handlers
|
// Set up process exit handlers with stored references for cleanup
|
||||||
process.on('beforeExit', () => this.flush());
|
this.eventListeners.beforeExit = () => this.flush();
|
||||||
process.on('SIGINT', () => {
|
this.eventListeners.sigint = () => {
|
||||||
this.flush();
|
this.flush();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
};
|
||||||
process.on('SIGTERM', () => {
|
this.eventListeners.sigterm = () => {
|
||||||
this.flush();
|
this.flush();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
process.on('beforeExit', this.eventListeners.beforeExit);
|
||||||
|
process.on('SIGINT', this.eventListeners.sigint);
|
||||||
|
process.on('SIGTERM', this.eventListeners.sigterm);
|
||||||
|
|
||||||
|
this.started = true;
|
||||||
logger.debug('Telemetry batch processor started');
|
logger.debug('Telemetry batch processor started');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +123,20 @@ export class TelemetryBatchProcessor {
|
|||||||
clearInterval(this.flushTimer);
|
clearInterval(this.flushTimer);
|
||||||
this.flushTimer = undefined;
|
this.flushTimer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove event listeners to prevent memory leaks
|
||||||
|
if (this.eventListeners.beforeExit) {
|
||||||
|
process.removeListener('beforeExit', this.eventListeners.beforeExit);
|
||||||
|
}
|
||||||
|
if (this.eventListeners.sigint) {
|
||||||
|
process.removeListener('SIGINT', this.eventListeners.sigint);
|
||||||
|
}
|
||||||
|
if (this.eventListeners.sigterm) {
|
||||||
|
process.removeListener('SIGTERM', this.eventListeners.sigterm);
|
||||||
|
}
|
||||||
|
this.eventListeners = {};
|
||||||
|
this.started = false;
|
||||||
|
|
||||||
logger.debug('Telemetry batch processor stopped');
|
logger.debug('Telemetry batch processor stopped');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ export interface ToolDefinition {
|
|||||||
};
|
};
|
||||||
/** Tool behavior hints for AI assistants */
|
/** Tool behavior hints for AI assistants */
|
||||||
annotations?: ToolAnnotations;
|
annotations?: ToolAnnotations;
|
||||||
|
_meta?: {
|
||||||
|
ui?: {
|
||||||
|
resourceUri?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResourceDefinition {
|
export interface ResourceDefinition {
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ export interface WebhookRequest {
|
|||||||
// MCP Tool Response Type
|
// MCP Tool Response Type
|
||||||
export interface McpToolResponse {
|
export interface McpToolResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
saved?: boolean;
|
||||||
data?: unknown;
|
data?: unknown;
|
||||||
error?: string;
|
error?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -318,6 +319,7 @@ export interface McpToolResponse {
|
|||||||
details?: Record<string, unknown>;
|
details?: Record<string, unknown>;
|
||||||
executionId?: string;
|
executionId?: string;
|
||||||
workflowId?: string;
|
workflowId?: string;
|
||||||
|
operationsApplied?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execution Filtering Types
|
// Execution Filtering Types
|
||||||
|
|||||||
@@ -190,6 +190,8 @@ export interface WorkflowDiffResult {
|
|||||||
staleConnectionsRemoved?: Array<{ from: string; to: string }>; // For cleanStaleConnections operation
|
staleConnectionsRemoved?: Array<{ from: string; to: string }>; // For cleanStaleConnections operation
|
||||||
shouldActivate?: boolean; // Flag to activate workflow after update (for activateWorkflow operation)
|
shouldActivate?: boolean; // Flag to activate workflow after update (for activateWorkflow operation)
|
||||||
shouldDeactivate?: boolean; // Flag to deactivate workflow after update (for deactivateWorkflow operation)
|
shouldDeactivate?: boolean; // Flag to deactivate workflow after update (for deactivateWorkflow operation)
|
||||||
|
tagsToAdd?: string[];
|
||||||
|
tagsToRemove?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper type for node reference (supports both ID and name)
|
// Helper type for node reference (supports both ID and name)
|
||||||
|
|||||||
@@ -172,14 +172,14 @@ export function isTriggerNode(nodeType: string): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for specific trigger types that don't have 'trigger' in their name
|
// Check for polling-based triggers that don't have 'trigger' in their name
|
||||||
const specificTriggers = [
|
if (lowerType.includes('emailread') || lowerType.includes('emailreadimap')) {
|
||||||
'nodes-base.start',
|
return true;
|
||||||
'nodes-base.manualTrigger',
|
}
|
||||||
'nodes-base.formTrigger'
|
|
||||||
];
|
|
||||||
|
|
||||||
return specificTriggers.includes(normalized);
|
// Check for specific trigger types that don't have 'trigger' in their name
|
||||||
|
// (manualTrigger and formTrigger are already caught by the 'trigger' check above)
|
||||||
|
return normalized === 'nodes-base.start';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ describe('Database Performance Tests', () => {
|
|||||||
// Adjusted based on actual CI performance measurements + type safety overhead
|
// Adjusted based on actual CI performance measurements + type safety overhead
|
||||||
// CI environments show ratios of ~7-10 for 1000:100 and ~6-7 for 5000:1000
|
// CI environments show ratios of ~7-10 for 1000:100 and ~6-7 for 5000:1000
|
||||||
// Increased thresholds to account for community node columns (8 additional fields)
|
// Increased thresholds to account for community node columns (8 additional fields)
|
||||||
expect(ratio1000to100).toBeLessThan(15); // Allow for CI variability + community columns (was 12)
|
expect(ratio1000to100).toBeLessThan(20); // Allow for CI variability + community columns (was 15)
|
||||||
expect(ratio5000to1000).toBeLessThan(12); // Allow for type safety overhead + community columns (was 11)
|
expect(ratio5000to1000).toBeLessThan(12); // Allow for type safety overhead + community columns (was 11)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -105,21 +105,14 @@ describe('MCP Protocol Compliance', () => {
|
|||||||
|
|
||||||
describe('Message Format Validation', () => {
|
describe('Message Format Validation', () => {
|
||||||
it('should reject messages without method', async () => {
|
it('should reject messages without method', async () => {
|
||||||
// Test by sending raw message through transport
|
// MCP SDK 1.27+ enforces single-connection per Server instance,
|
||||||
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
|
// so use the existing client from beforeEach instead of a new one.
|
||||||
const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
|
|
||||||
|
|
||||||
await mcpServer.connectToTransport(serverTransport);
|
|
||||||
await testClient.connect(clientTransport);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This should fail as MCP SDK validates method
|
// This should fail as MCP SDK validates method
|
||||||
await (testClient as any).request({ method: '', params: {} });
|
await (client as any).request({ method: '', params: {} });
|
||||||
expect.fail('Should have thrown an error');
|
expect.fail('Should have thrown an error');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeDefined();
|
expect(error).toBeDefined();
|
||||||
} finally {
|
|
||||||
await testClient.close();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,10 +243,15 @@ describe('MCP Protocol Compliance', () => {
|
|||||||
|
|
||||||
describe('Transport Layer', () => {
|
describe('Transport Layer', () => {
|
||||||
it('should handle transport disconnection gracefully', async () => {
|
it('should handle transport disconnection gracefully', async () => {
|
||||||
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
|
// Use a dedicated server instance so we don't conflict with the
|
||||||
const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
|
// shared mcpServer that beforeEach already connected a transport to.
|
||||||
|
const dedicatedServer = new TestableN8NMCPServer();
|
||||||
|
await dedicatedServer.initialize();
|
||||||
|
|
||||||
await mcpServer.connectToTransport(serverTransport);
|
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
|
||||||
|
await dedicatedServer.connectToTransport(serverTransport);
|
||||||
|
|
||||||
|
const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
|
||||||
await testClient.connect(clientTransport);
|
await testClient.connect(clientTransport);
|
||||||
|
|
||||||
// Make a request
|
// Make a request
|
||||||
@@ -270,6 +268,8 @@ describe('MCP Protocol Compliance', () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeDefined();
|
expect(error).toBeDefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await dedicatedServer.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle multiple sequential connections', async () => {
|
it('should handle multiple sequential connections', async () => {
|
||||||
|
|||||||
@@ -73,10 +73,11 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
|||||||
const serverInfo = await client.getServerVersion();
|
const serverInfo = await client.getServerVersion();
|
||||||
expect(serverInfo).toBeDefined();
|
expect(serverInfo).toBeDefined();
|
||||||
expect(serverInfo?.name).toBe('n8n-documentation-mcp');
|
expect(serverInfo?.name).toBe('n8n-documentation-mcp');
|
||||||
|
|
||||||
// Check capabilities if they exist
|
// Check capabilities via the dedicated method
|
||||||
if (serverInfo?.capabilities) {
|
const capabilities = client.getServerCapabilities();
|
||||||
expect(serverInfo.capabilities).toHaveProperty('tools');
|
if (capabilities) {
|
||||||
|
expect(capabilities).toHaveProperty('tools');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up - ensure proper order
|
// Clean up - ensure proper order
|
||||||
@@ -340,9 +341,9 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
|||||||
it('should handle different client versions', async () => {
|
it('should handle different client versions', async () => {
|
||||||
const mcpServer = new TestableN8NMCPServer();
|
const mcpServer = new TestableN8NMCPServer();
|
||||||
await mcpServer.initialize();
|
await mcpServer.initialize();
|
||||||
|
|
||||||
const clients = [];
|
|
||||||
|
|
||||||
|
// MCP SDK 1.27+ enforces single-connection per Server instance,
|
||||||
|
// so we test each version sequentially rather than concurrently.
|
||||||
for (const version of ['1.0.0', '1.1.0', '2.0.0']) {
|
for (const version of ['1.0.0', '1.1.0', '2.0.0']) {
|
||||||
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
|
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
|
||||||
await mcpServer.connectToTransport(serverTransport);
|
await mcpServer.connectToTransport(serverTransport);
|
||||||
@@ -353,21 +354,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
|||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
await client.connect(clientTransport);
|
await client.connect(clientTransport);
|
||||||
clients.push(client);
|
|
||||||
|
const info = await client.getServerVersion();
|
||||||
|
expect(info!.name).toBe('n8n-documentation-mcp');
|
||||||
|
|
||||||
|
await client.close();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
}
|
}
|
||||||
|
|
||||||
// All versions should work
|
|
||||||
const responses = await Promise.all(
|
|
||||||
clients.map(client => client.getServerVersion())
|
|
||||||
);
|
|
||||||
|
|
||||||
responses.forEach(info => {
|
|
||||||
expect(info!.name).toBe('n8n-documentation-mcp');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
await Promise.all(clients.map(client => client.close()));
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100)); // Give time for all clients to fully close
|
|
||||||
await mcpServer.close();
|
await mcpServer.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
import {
|
import {
|
||||||
CallToolRequestSchema,
|
CallToolRequestSchema,
|
||||||
ListToolsRequestSchema,
|
ListToolsRequestSchema,
|
||||||
InitializeRequestSchema,
|
InitializeRequestSchema,
|
||||||
} from '@modelcontextprotocol/sdk/types.js';
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
@@ -14,18 +14,30 @@ export class TestableN8NMCPServer {
|
|||||||
private mcpServer: N8NDocumentationMCPServer;
|
private mcpServer: N8NDocumentationMCPServer;
|
||||||
private server: Server;
|
private server: Server;
|
||||||
private transports = new Set<Transport>();
|
private transports = new Set<Transport>();
|
||||||
private connections = new Set<any>();
|
|
||||||
private static instanceCount = 0;
|
private static instanceCount = 0;
|
||||||
private testDbPath: string;
|
private testDbPath: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Use a unique test database for each instance to avoid conflicts
|
// Use path.resolve to produce a canonical absolute path so the shared
|
||||||
// This prevents concurrent test issues with database locking
|
// database singleton always sees the exact same string, preventing
|
||||||
const instanceId = TestableN8NMCPServer.instanceCount++;
|
// "Shared database already initialized with different path" errors.
|
||||||
this.testDbPath = `/tmp/n8n-mcp-test-${process.pid}-${instanceId}.db`;
|
const path = require('path');
|
||||||
|
this.testDbPath = path.resolve(process.cwd(), 'data', 'nodes.db');
|
||||||
process.env.NODE_DB_PATH = this.testDbPath;
|
process.env.NODE_DB_PATH = this.testDbPath;
|
||||||
|
|
||||||
this.server = new Server({
|
this.server = this.createServer();
|
||||||
|
|
||||||
|
this.mcpServer = new N8NDocumentationMCPServer();
|
||||||
|
this.setupHandlers(this.server);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fresh MCP SDK Server instance.
|
||||||
|
* MCP SDK 1.27+ enforces single-connection per Protocol instance,
|
||||||
|
* so we create a new one each time we need to connect to a transport.
|
||||||
|
*/
|
||||||
|
private createServer(): Server {
|
||||||
|
return new Server({
|
||||||
name: 'n8n-documentation-mcp',
|
name: 'n8n-documentation-mcp',
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
}, {
|
}, {
|
||||||
@@ -33,14 +45,11 @@ export class TestableN8NMCPServer {
|
|||||||
tools: {}
|
tools: {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.mcpServer = new N8NDocumentationMCPServer();
|
|
||||||
this.setupHandlers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupHandlers() {
|
private setupHandlers(server: Server) {
|
||||||
// Initialize handler
|
// Initialize handler
|
||||||
this.server.setRequestHandler(InitializeRequestSchema, async () => {
|
server.setRequestHandler(InitializeRequestSchema, async () => {
|
||||||
return {
|
return {
|
||||||
protocolVersion: '2024-11-05',
|
protocolVersion: '2024-11-05',
|
||||||
capabilities: {
|
capabilities: {
|
||||||
@@ -54,27 +63,27 @@ export class TestableN8NMCPServer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// List tools handler
|
// List tools handler
|
||||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
// Import the tools directly from the tools module
|
// Import the tools directly from the tools module
|
||||||
const { n8nDocumentationToolsFinal } = await import('../../../src/mcp/tools');
|
const { n8nDocumentationToolsFinal } = await import('../../../src/mcp/tools');
|
||||||
const { n8nManagementTools } = await import('../../../src/mcp/tools-n8n-manager');
|
const { n8nManagementTools } = await import('../../../src/mcp/tools-n8n-manager');
|
||||||
const { isN8nApiConfigured } = await import('../../../src/config/n8n-api');
|
const { isN8nApiConfigured } = await import('../../../src/config/n8n-api');
|
||||||
|
|
||||||
// Combine documentation tools with management tools if API is configured
|
// Combine documentation tools with management tools if API is configured
|
||||||
const tools = [...n8nDocumentationToolsFinal];
|
const tools = [...n8nDocumentationToolsFinal];
|
||||||
if (isN8nApiConfigured()) {
|
if (isN8nApiConfigured()) {
|
||||||
tools.push(...n8nManagementTools);
|
tools.push(...n8nManagementTools);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { tools };
|
return { tools };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call tool handler
|
// Call tool handler
|
||||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
try {
|
try {
|
||||||
// The mcpServer.executeTool returns raw data, we need to wrap it in the MCP response format
|
// The mcpServer.executeTool returns raw data, we need to wrap it in the MCP response format
|
||||||
const result = await this.mcpServer.executeTool(request.params.name, request.params.arguments || {});
|
const result = await this.mcpServer.executeTool(request.params.name, request.params.arguments || {});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -98,21 +107,8 @@ export class TestableN8NMCPServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
// Copy production database to test location for realistic testing
|
// The MCP server initializes its database lazily via the shared
|
||||||
try {
|
// database singleton. Trigger initialization by calling executeTool.
|
||||||
const fs = await import('fs');
|
|
||||||
const path = await import('path');
|
|
||||||
const prodDbPath = path.join(process.cwd(), 'data', 'nodes.db');
|
|
||||||
|
|
||||||
if (await fs.promises.access(prodDbPath).then(() => true).catch(() => false)) {
|
|
||||||
await fs.promises.copyFile(prodDbPath, this.testDbPath);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore copy errors, database will be created fresh
|
|
||||||
}
|
|
||||||
|
|
||||||
// The MCP server initializes its database lazily
|
|
||||||
// We can trigger initialization by calling executeTool
|
|
||||||
try {
|
try {
|
||||||
await this.mcpServer.executeTool('tools_documentation', {});
|
await this.mcpServer.executeTool('tools_documentation', {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -125,20 +121,26 @@ export class TestableN8NMCPServer {
|
|||||||
if (!transport || typeof transport !== 'object') {
|
if (!transport || typeof transport !== 'object') {
|
||||||
throw new Error('Invalid transport provided');
|
throw new Error('Invalid transport provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up any missing transport handlers to prevent "Cannot set properties of undefined" errors
|
// MCP SDK 1.27+ enforces single-connection per Protocol instance.
|
||||||
if (transport && typeof transport === 'object') {
|
// Close the current server and create a fresh one so that _transport
|
||||||
const transportAny = transport as any;
|
// is guaranteed to be undefined. Reusing the same Server after close()
|
||||||
if (transportAny.serverTransport && !transportAny.serverTransport.onclose) {
|
// is unreliable because _transport is cleared asynchronously via the
|
||||||
transportAny.serverTransport.onclose = () => {};
|
// transport onclose callback chain, which can fail in CI.
|
||||||
}
|
try {
|
||||||
|
await this.server.close();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors during cleanup of previous transport
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a brand-new Server instance for this connection
|
||||||
|
this.server = this.createServer();
|
||||||
|
this.setupHandlers(this.server);
|
||||||
|
|
||||||
// Track this transport for cleanup
|
// Track this transport for cleanup
|
||||||
this.transports.add(transport);
|
this.transports.add(transport);
|
||||||
|
|
||||||
const connection = await this.server.connect(transport);
|
await this.server.connect(transport);
|
||||||
this.connections.add(connection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
@@ -151,78 +153,47 @@ export class TestableN8NMCPServer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const performClose = async () => {
|
const performClose = async () => {
|
||||||
// Close all connections first with timeout protection
|
// Close the MCP SDK Server (resets _transport via _onclose)
|
||||||
const connectionPromises = Array.from(this.connections).map(async (connection) => {
|
try {
|
||||||
const connTimeout = new Promise<void>((resolve) => setTimeout(resolve, 500));
|
await this.server.close();
|
||||||
|
} catch {
|
||||||
try {
|
// Ignore errors during server close
|
||||||
if (connection && typeof connection.close === 'function') {
|
}
|
||||||
await Promise.race([connection.close(), connTimeout]);
|
|
||||||
}
|
// Shut down the inner N8NDocumentationMCPServer to release the
|
||||||
} catch (error) {
|
// shared database reference and prevent resource leaks.
|
||||||
// Ignore errors during connection cleanup
|
try {
|
||||||
}
|
await this.mcpServer.shutdown();
|
||||||
});
|
} catch {
|
||||||
|
// Ignore errors during inner server shutdown
|
||||||
await Promise.allSettled(connectionPromises);
|
}
|
||||||
this.connections.clear();
|
|
||||||
|
|
||||||
// Close all tracked transports with timeout protection
|
// Close all tracked transports with timeout protection
|
||||||
const transportPromises: Promise<void>[] = [];
|
const transportPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
for (const transport of this.transports) {
|
for (const transport of this.transports) {
|
||||||
const transportTimeout = new Promise<void>((resolve) => setTimeout(resolve, 500));
|
const transportTimeout = new Promise<void>((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Force close all transports
|
|
||||||
const transportAny = transport as any;
|
const transportAny = transport as any;
|
||||||
|
|
||||||
// Try different close methods
|
|
||||||
if (transportAny.close && typeof transportAny.close === 'function') {
|
if (transportAny.close && typeof transportAny.close === 'function') {
|
||||||
transportPromises.push(
|
transportPromises.push(
|
||||||
Promise.race([transportAny.close(), transportTimeout])
|
Promise.race([transportAny.close(), transportTimeout])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (transportAny.serverTransport?.close) {
|
} catch {
|
||||||
transportPromises.push(
|
|
||||||
Promise.race([transportAny.serverTransport.close(), transportTimeout])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (transportAny.clientTransport?.close) {
|
|
||||||
transportPromises.push(
|
|
||||||
Promise.race([transportAny.clientTransport.close(), transportTimeout])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore errors during transport cleanup
|
// Ignore errors during transport cleanup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all transports to close with timeout
|
|
||||||
await Promise.allSettled(transportPromises);
|
await Promise.allSettled(transportPromises);
|
||||||
|
|
||||||
// Clear the transports set
|
|
||||||
this.transports.clear();
|
this.transports.clear();
|
||||||
|
|
||||||
// Don't shut down the shared MCP server instance
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Race between actual close and timeout
|
// Race between actual close and timeout
|
||||||
await Promise.race([performClose(), closeTimeout]);
|
await Promise.race([performClose(), closeTimeout]);
|
||||||
|
|
||||||
// Clean up test database
|
|
||||||
if (this.testDbPath) {
|
|
||||||
try {
|
|
||||||
const fs = await import('fs');
|
|
||||||
await fs.promises.unlink(this.testDbPath).catch(() => {});
|
|
||||||
await fs.promises.unlink(`${this.testDbPath}-shm`).catch(() => {});
|
|
||||||
await fs.promises.unlink(`${this.testDbPath}-wal`).catch(() => {});
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore cleanup errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async shutdownShared(): Promise<void> {
|
static async shutdownShared(): Promise<void> {
|
||||||
if (sharedMcpServer) {
|
if (sharedMcpServer) {
|
||||||
await sharedMcpServer.shutdown();
|
await sharedMcpServer.shutdown();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user