mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-17 07:53:08 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65ab94deb2 | ||
|
|
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 | ||
|
|
533b105f03 | ||
|
|
28667736cd |
12
.github/workflows/dependency-check.yml
vendored
12
.github/workflows/dependency-check.yml
vendored
@@ -59,7 +59,9 @@ jobs:
|
||||
run: |
|
||||
npm init -y
|
||||
# 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
|
||||
working-directory: /tmp/fresh-install-test
|
||||
@@ -75,15 +77,15 @@ jobs:
|
||||
echo "Zod version: $ZOD_VERSION"
|
||||
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
|
||||
echo "❌ FAILED: Could not determine MCP SDK version!"
|
||||
echo " The dependency may not have been installed correctly."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$SDK_VERSION" != "1.20.1" ]]; then
|
||||
if [[ "$SDK_VERSION" != "1.27.1" ]]; then
|
||||
echo "❌ FAILED: MCP SDK version mismatch!"
|
||||
echo " Expected: 1.20.1"
|
||||
echo " Expected: 1.27.1"
|
||||
echo " Got: $SDK_VERSION"
|
||||
echo ""
|
||||
echo "This can cause runtime errors. See issues #440, #444, #446, #447, #450"
|
||||
@@ -98,7 +100,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
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 " Got: $ZOD_VERSION"
|
||||
echo ""
|
||||
|
||||
30
.github/workflows/docker-build.yml
vendored
30
.github/workflows/docker-build.yml
vendored
@@ -53,13 +53,24 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
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
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
@@ -144,13 +155,24 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
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
|
||||
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
|
||||
run: npm ci
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
- name: Build project (server + UI apps)
|
||||
run: npm run build:all
|
||||
|
||||
# Database is already built and committed during development
|
||||
# Rebuilding here causes segfault due to memory pressure (exit code 139)
|
||||
@@ -322,8 +322,8 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
- name: Build project (server + UI apps)
|
||||
run: npm run build:all
|
||||
|
||||
# Database is already built and committed during development
|
||||
- name: Verify database exists
|
||||
@@ -347,6 +347,8 @@ jobs:
|
||||
# Copy necessary files
|
||||
cp -r dist $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 LICENSE $PUBLISH_DIR/
|
||||
cp .env.example $PUBLISH_DIR/
|
||||
@@ -377,7 +379,7 @@ jobs:
|
||||
pkg.license = 'MIT';
|
||||
pkg.bugs = { url: 'https://github.com/czlonkowski/n8n-mcp/issues' };
|
||||
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;
|
||||
require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
|
||||
"
|
||||
@@ -427,7 +429,18 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -138,5 +138,9 @@ n8n-mcp-wrapper.sh
|
||||
# MCP configuration files
|
||||
.mcp.json
|
||||
|
||||
# UI Apps build output
|
||||
ui-apps/dist/
|
||||
ui-apps/node_modules/
|
||||
|
||||
# Telemetry configuration (user-specific)
|
||||
~/.n8n-mcp/
|
||||
|
||||
1123
CHANGELOG.md
1123
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 && \
|
||||
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 \
|
||||
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
|
||||
|
||||
# Copy source and build
|
||||
|
||||
@@ -18,21 +18,27 @@ npm run update:n8n:check
|
||||
# 4. Run update and skip tests (we'll test in CI)
|
||||
yes y | npm run update:n8n
|
||||
|
||||
# 5. Create feature branch
|
||||
# 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
|
||||
|
||||
# 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!)
|
||||
|
||||
# 7. Update CHANGELOG.md
|
||||
# 8. Update CHANGELOG.md
|
||||
# - Change version number to match package.json
|
||||
# - Update date to today
|
||||
# - 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
|
||||
# Update total node count in description (core + community)
|
||||
|
||||
# 9. Commit and push
|
||||
# 10. Commit and push
|
||||
git add -A
|
||||
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/n8n-nodes-langchain from X.X.X to X.X.X
|
||||
- Rebuilt node database with XXX nodes (XXX from n8n-nodes-base, XXX from @n8n/n8n-nodes-langchain)
|
||||
- Updated README badge with new n8n version
|
||||
- Refreshed community nodes (XXX verified + XXX npm)
|
||||
- Updated README badge with new n8n version and node counts
|
||||
- Updated CHANGELOG with dependency changes
|
||||
|
||||
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
|
||||
|
||||
# 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..."
|
||||
|
||||
# 11. After PR is merged, verify release triggered
|
||||
# 12. After PR is merged, verify release triggered
|
||||
gh release list | head -1
|
||||
# If the new version appears, you're done!
|
||||
# If not, the version might have already been released - bump version again and create new PR
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
[](https://www.npmjs.com/package/n8n-mcp)
|
||||
[](https://codecov.io/gh/czlonkowski/n8n-mcp)
|
||||
[](https://github.com/czlonkowski/n8n-mcp/actions)
|
||||
[](https://github.com/n8n-io/n8n)
|
||||
[](https://github.com/n8n-io/n8n)
|
||||
[](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp)
|
||||
[](https://railway.com/deploy/n8n-mcp?referralCode=n8n-mcp)
|
||||
|
||||
A Model Context Protocol (MCP) server that provides AI assistants with comprehensive access to n8n node documentation, properties, and operations. Deploy in minutes to give Claude and other AI assistants deep knowledge about n8n's 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
|
||||
|
||||
|
||||
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',
|
||||
],
|
||||
},
|
||||
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 = {
|
||||
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.onModify = onModify;
|
||||
this.boundParams = null;
|
||||
this.freed = false;
|
||||
}
|
||||
freeStatement() {
|
||||
if (!this.freed && this.stmt) {
|
||||
try {
|
||||
this.stmt.free();
|
||||
this.freed = true;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
run(...params) {
|
||||
try {
|
||||
@@ -331,6 +342,9 @@ class SQLJSStatement {
|
||||
this.stmt.reset();
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
this.freeStatement();
|
||||
}
|
||||
}
|
||||
get(...params) {
|
||||
try {
|
||||
@@ -352,6 +366,9 @@ class SQLJSStatement {
|
||||
this.stmt.reset();
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
this.freeStatement();
|
||||
}
|
||||
}
|
||||
all(...params) {
|
||||
try {
|
||||
@@ -372,6 +389,9 @@ class SQLJSStatement {
|
||||
this.stmt.reset();
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
this.freeStatement();
|
||||
}
|
||||
}
|
||||
iterate(...params) {
|
||||
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 { ParsedNode } from '../parsers/node-parser';
|
||||
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 {
|
||||
private db;
|
||||
constructor(dbOrService: DatabaseAdapter | SQLiteStorageService);
|
||||
saveNode(node: ParsedNode): void;
|
||||
saveNode(node: ParsedNode & Partial<CommunityNodeFields>): void;
|
||||
getNode(nodeType: string): any;
|
||||
getAITools(): any[];
|
||||
private safeJsonParse;
|
||||
@@ -29,6 +39,30 @@ export declare class NodeRepository {
|
||||
getAllResources(): Map<string, any[]>;
|
||||
getNodePropertyDefaults(nodeType: string): Record<string, any>;
|
||||
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: {
|
||||
nodeType: 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,
|
||||
has_tool_variant, version, documentation,
|
||||
properties_schema, operations, credentials_required,
|
||||
outputs, output_names
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
outputs, output_names,
|
||||
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) {
|
||||
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
||||
@@ -37,6 +39,14 @@ class NodeRepository {
|
||||
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)
|
||||
return null;
|
||||
return this.parseNodeRow(row);
|
||||
@@ -214,7 +224,20 @@ class NodeRepository {
|
||||
credentials: this.safeJsonParse(row.credentials_required, []),
|
||||
hasDocumentation: !!row.documentation,
|
||||
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) {
|
||||
@@ -360,6 +383,98 @@ class NodeRepository {
|
||||
}
|
||||
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) {
|
||||
const stmt = this.db.prepare(`
|
||||
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 protocol_version_1 = require("./utils/protocol-version");
|
||||
const instance_context_1 = require("./types/instance-context");
|
||||
const shared_database_1 = require("./database/shared-database");
|
||||
dotenv_1.default.config();
|
||||
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));
|
||||
@@ -52,7 +53,7 @@ class SingleSessionHTTPServer {
|
||||
this.contextSwitchLocks = new Map();
|
||||
this.session = null;
|
||||
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.cleanupTimer = null;
|
||||
this.validateEnvironment();
|
||||
@@ -290,6 +291,25 @@ class SingleSessionHTTPServer {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
const isMultiTenantEnabled = process.env.ENABLE_MULTI_TENANT === 'true';
|
||||
const sessionStrategy = process.env.MULTI_TENANT_SESSION_STRATEGY || 'instance';
|
||||
@@ -434,12 +454,21 @@ class SingleSessionHTTPServer {
|
||||
}
|
||||
async resetSessionSSE(res) {
|
||||
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 {
|
||||
logger_1.logger.info('Closing previous session for SSE', { sessionId: this.session.sessionId });
|
||||
await this.session.transport.close();
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.logger.warn('Error closing previous session:', error);
|
||||
catch (transportError) {
|
||||
logger_1.logger.warn('Error closing transport for SSE session', { sessionId, error: transportError });
|
||||
}
|
||||
}
|
||||
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');
|
||||
}
|
||||
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() {
|
||||
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();
|
||||
const app = (0, express_1.default)();
|
||||
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 { validateInstanceContext, isInstanceContext } from './types/instance-context';
|
||||
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';
|
||||
import N8NMCPEngine from './mcp-engine';
|
||||
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 };
|
||||
};
|
||||
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");
|
||||
Object.defineProperty(exports, "N8NMCPEngine", { enumerable: true, get: function () { return mcp_engine_1.N8NMCPEngine; } });
|
||||
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");
|
||||
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; } });
|
||||
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"));
|
||||
exports.default = mcp_engine_2.default;
|
||||
//# 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 {
|
||||
private readonly CORE_PACKAGES;
|
||||
loadAllNodes(): Promise<LoadedNode[]>;
|
||||
private resolvePackageDir;
|
||||
private loadNodeModule;
|
||||
private loadPackageNodes;
|
||||
}
|
||||
//# 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;
|
||||
}
|
||||
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) {
|
||||
const n8nConfig = packageJson.n8n || {};
|
||||
const nodes = [];
|
||||
const packageDir = this.resolvePackageDir(packagePath);
|
||||
const nodesList = n8nConfig.nodes || [];
|
||||
if (Array.isArray(nodesList)) {
|
||||
for (const nodePath of nodesList) {
|
||||
try {
|
||||
const fullPath = require.resolve(`${packagePath}/${nodePath}`);
|
||||
const nodeModule = require(fullPath);
|
||||
const fullPath = path_1.default.join(packageDir, nodePath);
|
||||
const nodeModule = this.loadNodeModule(fullPath);
|
||||
const nodeNameMatch = nodePath.match(/\/([^\/]+)\.node\.(js|ts)$/);
|
||||
const nodeName = nodeNameMatch ? nodeNameMatch[1] : path_1.default.basename(nodePath, '.node.js');
|
||||
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
|
||||
@@ -56,8 +64,8 @@ class N8nNodeLoader {
|
||||
else {
|
||||
for (const [nodeName, nodePath] of Object.entries(nodesList)) {
|
||||
try {
|
||||
const fullPath = require.resolve(`${packagePath}/${nodePath}`);
|
||||
const nodeModule = require(fullPath);
|
||||
const fullPath = path_1.default.join(packageDir, nodePath);
|
||||
const nodeModule = this.loadNodeModule(fullPath);
|
||||
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
|
||||
if (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',
|
||||
'webhook-missing-path',
|
||||
'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(),
|
||||
confidenceThreshold: zod_1.z.enum(['high', 'medium', 'low']).optional().default('medium'),
|
||||
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);
|
||||
if (mode === 'http') {
|
||||
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')));
|
||||
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 earlyLogger;
|
||||
private disabledToolsCache;
|
||||
private useSharedDatabase;
|
||||
private sharedDbState;
|
||||
private isShutdown;
|
||||
constructor(instanceContext?: InstanceContext, earlyLogger?: EarlyErrorLogger);
|
||||
close(): Promise<void>;
|
||||
private initializeDatabase;
|
||||
@@ -27,6 +30,7 @@ export declare class N8NDocumentationMCPServer {
|
||||
private validateToolParams;
|
||||
private validateToolParamsBasic;
|
||||
private validateExtractedArgs;
|
||||
private coerceStringifiedJsonParams;
|
||||
private listNodes;
|
||||
private getNodeInfo;
|
||||
private searchNodes;
|
||||
@@ -40,6 +44,7 @@ export declare class N8NDocumentationMCPServer {
|
||||
private rankSearchResults;
|
||||
private listAITools;
|
||||
private getNodeDocumentation;
|
||||
private safeJsonParse;
|
||||
private getDatabaseStatistics;
|
||||
private getNodeEssentials;
|
||||
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 path_1 = __importDefault(require("path"));
|
||||
const tools_1 = require("./tools");
|
||||
const ui_1 = require("./ui");
|
||||
const tools_n8n_manager_1 = require("./tools-n8n-manager");
|
||||
const tools_n8n_friendly_1 = require("./tools-n8n-friendly");
|
||||
const workflow_examples_1 = require("./workflow-examples");
|
||||
const logger_1 = require("../utils/logger");
|
||||
const node_repository_1 = require("../database/node-repository");
|
||||
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 task_templates_1 = require("../services/task-templates");
|
||||
const config_validator_1 = require("../services/config-validator");
|
||||
@@ -80,6 +82,9 @@ class N8NDocumentationMCPServer {
|
||||
this.previousToolTimestamp = Date.now();
|
||||
this.earlyLogger = null;
|
||||
this.disabledToolsCache = null;
|
||||
this.useSharedDatabase = false;
|
||||
this.sharedDbState = null;
|
||||
this.isShutdown = false;
|
||||
this.dbHealthChecked = false;
|
||||
this.instanceContext = instanceContext;
|
||||
this.earlyLogger = earlyLogger || null;
|
||||
@@ -144,15 +149,29 @@ class N8NDocumentationMCPServer {
|
||||
}, {
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
},
|
||||
});
|
||||
ui_1.UIAppRegistry.load();
|
||||
this.setupHandlers();
|
||||
}
|
||||
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 {
|
||||
await this.server.close();
|
||||
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 {
|
||||
this.db.close();
|
||||
}
|
||||
@@ -166,6 +185,7 @@ class N8NDocumentationMCPServer {
|
||||
this.repository = null;
|
||||
this.templateService = null;
|
||||
this.earlyLogger = null;
|
||||
this.sharedDbState = null;
|
||||
}
|
||||
catch (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);
|
||||
}
|
||||
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:') {
|
||||
this.db = await (0, database_adapter_1.createDatabaseAdapter)(dbPath);
|
||||
logger_1.logger.debug('Database adapter created (in-memory mode)');
|
||||
await this.initializeInMemorySchema();
|
||||
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');
|
||||
this.templateService = new template_service_1.TemplateService(this.db);
|
||||
logger_1.logger.debug('Template service initialized');
|
||||
enhanced_config_validator_1.EnhancedConfigValidator.initializeSimilarityServices(this.repository);
|
||||
logger_1.logger.debug('Similarity services initialized');
|
||||
if (this.earlyLogger) {
|
||||
this.earlyLogger.logCheckpoint(startup_checkpoints_1.STARTUP_CHECKPOINTS.DATABASE_CONNECTED);
|
||||
@@ -341,6 +371,7 @@ class N8NDocumentationMCPServer {
|
||||
protocolVersion: negotiationResult.version,
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: 'n8n-documentation-mcp',
|
||||
@@ -396,6 +427,7 @@ class N8NDocumentationMCPServer {
|
||||
description: tool.description
|
||||
});
|
||||
});
|
||||
ui_1.UIAppRegistry.injectToolMeta(tools);
|
||||
return { tools };
|
||||
});
|
||||
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
||||
@@ -425,6 +457,18 @@ class N8NDocumentationMCPServer {
|
||||
};
|
||||
}
|
||||
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) {
|
||||
try {
|
||||
const possibleNestedData = args.output;
|
||||
@@ -453,6 +497,7 @@ class N8NDocumentationMCPServer {
|
||||
});
|
||||
}
|
||||
}
|
||||
processedArgs = this.coerceStringifiedJsonParams(name, processedArgs);
|
||||
try {
|
||||
logger_1.logger.debug(`Executing tool: ${name}`, { args: processedArgs });
|
||||
const startTime = Date.now();
|
||||
@@ -524,6 +569,13 @@ class N8NDocumentationMCPServer {
|
||||
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., {})';
|
||||
}
|
||||
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 {
|
||||
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) {
|
||||
if (!result || typeof result !== 'object') {
|
||||
@@ -730,6 +815,93 @@ class N8NDocumentationMCPServer {
|
||||
}
|
||||
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) {
|
||||
args = args || {};
|
||||
const disabledTools = this.getDisabledTools();
|
||||
@@ -750,7 +922,11 @@ class N8NDocumentationMCPServer {
|
||||
case 'search_nodes':
|
||||
this.validateToolParams(name, args, ['query']);
|
||||
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':
|
||||
this.validateToolParams(name, args, ['nodeType']);
|
||||
if (args.mode === 'docs') {
|
||||
@@ -1089,6 +1265,19 @@ class N8NDocumentationMCPServer {
|
||||
}
|
||||
}
|
||||
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(`
|
||||
SELECT
|
||||
n.*,
|
||||
@@ -1096,6 +1285,7 @@ class N8NDocumentationMCPServer {
|
||||
FROM nodes n
|
||||
JOIN nodes_fts ON n.rowid = nodes_fts.rowid
|
||||
WHERE nodes_fts MATCH ?
|
||||
${sourceFilter}
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN LOWER(n.display_name) = LOWER(?) THEN 0
|
||||
@@ -1128,15 +1318,28 @@ class N8NDocumentationMCPServer {
|
||||
}
|
||||
const result = {
|
||||
query,
|
||||
results: scoredNodes.map(node => ({
|
||||
nodeType: node.node_type,
|
||||
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
||||
displayName: node.display_name,
|
||||
description: node.description,
|
||||
category: node.category,
|
||||
package: node.package_name,
|
||||
relevance: this.calculateRelevance(node, cleanedQuery)
|
||||
})),
|
||||
results: scoredNodes.map(node => {
|
||||
const nodeResult = {
|
||||
nodeType: node.node_type,
|
||||
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
||||
displayName: node.display_name,
|
||||
description: node.description,
|
||||
category: node.category,
|
||||
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
|
||||
};
|
||||
if (mode !== 'OR') {
|
||||
@@ -1298,24 +1501,51 @@ class N8NDocumentationMCPServer {
|
||||
async searchNodesLIKE(query, limit, options) {
|
||||
if (!this.db)
|
||||
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('"')) {
|
||||
const exactPhrase = query.slice(1, -1);
|
||||
const nodes = this.db.prepare(`
|
||||
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 ?
|
||||
`).all(`%${exactPhrase}%`, `%${exactPhrase}%`, `%${exactPhrase}%`, limit * 3);
|
||||
const rankedNodes = this.rankSearchResults(nodes, exactPhrase, limit);
|
||||
const result = {
|
||||
query,
|
||||
results: rankedNodes.map(node => ({
|
||||
nodeType: node.node_type,
|
||||
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
||||
displayName: node.display_name,
|
||||
description: node.description,
|
||||
category: node.category,
|
||||
package: node.package_name
|
||||
})),
|
||||
results: rankedNodes.map(node => {
|
||||
const nodeResult = {
|
||||
nodeType: node.node_type,
|
||||
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
||||
displayName: node.display_name,
|
||||
description: node.description,
|
||||
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
|
||||
};
|
||||
if (options?.includeExamples) {
|
||||
@@ -1354,21 +1584,35 @@ class N8NDocumentationMCPServer {
|
||||
const params = words.flatMap(w => [`%${w}%`, `%${w}%`, `%${w}%`]);
|
||||
params.push(limit * 3);
|
||||
const nodes = this.db.prepare(`
|
||||
SELECT DISTINCT * FROM nodes
|
||||
WHERE ${conditions}
|
||||
SELECT DISTINCT * FROM nodes
|
||||
WHERE (${conditions})
|
||||
${sourceFilter}
|
||||
LIMIT ?
|
||||
`).all(...params);
|
||||
const rankedNodes = this.rankSearchResults(nodes, query, limit);
|
||||
const result = {
|
||||
query,
|
||||
results: rankedNodes.map(node => ({
|
||||
nodeType: node.node_type,
|
||||
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
||||
displayName: node.display_name,
|
||||
description: node.description,
|
||||
category: node.category,
|
||||
package: node.package_name
|
||||
})),
|
||||
results: rankedNodes.map(node => {
|
||||
const nodeResult = {
|
||||
nodeType: node.node_type,
|
||||
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
||||
displayName: node.display_name,
|
||||
description: node.description,
|
||||
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
|
||||
};
|
||||
if (options?.includeExamples) {
|
||||
@@ -1545,14 +1789,16 @@ class N8NDocumentationMCPServer {
|
||||
throw new Error('Database not initialized');
|
||||
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
||||
let node = this.db.prepare(`
|
||||
SELECT node_type, display_name, documentation, description
|
||||
FROM nodes
|
||||
SELECT node_type, display_name, documentation, description,
|
||||
ai_documentation_summary, ai_summary_generated_at
|
||||
FROM nodes
|
||||
WHERE node_type = ?
|
||||
`).get(normalizedType);
|
||||
if (!node && normalizedType !== nodeType) {
|
||||
node = this.db.prepare(`
|
||||
SELECT node_type, display_name, documentation, description
|
||||
FROM nodes
|
||||
SELECT node_type, display_name, documentation, description,
|
||||
ai_documentation_summary, ai_summary_generated_at
|
||||
FROM nodes
|
||||
WHERE node_type = ?
|
||||
`).get(nodeType);
|
||||
}
|
||||
@@ -1560,8 +1806,9 @@ class N8NDocumentationMCPServer {
|
||||
const alternatives = (0, node_utils_1.getNodeTypeAlternatives)(normalizedType);
|
||||
for (const alt of alternatives) {
|
||||
node = this.db.prepare(`
|
||||
SELECT node_type, display_name, documentation, description
|
||||
FROM nodes
|
||||
SELECT node_type, display_name, documentation, description,
|
||||
ai_documentation_summary, ai_summary_generated_at
|
||||
FROM nodes
|
||||
WHERE node_type = ?
|
||||
`).get(alt);
|
||||
if (node)
|
||||
@@ -1571,6 +1818,9 @@ class N8NDocumentationMCPServer {
|
||||
if (!node) {
|
||||
throw new Error(`Node ${nodeType} not found`);
|
||||
}
|
||||
const aiDocSummary = node.ai_documentation_summary
|
||||
? this.safeJsonParse(node.ai_documentation_summary, null)
|
||||
: null;
|
||||
if (!node.documentation) {
|
||||
const essentials = await this.getNodeEssentials(nodeType);
|
||||
return {
|
||||
@@ -1590,7 +1840,9 @@ ${essentials?.commonProperties?.length > 0 ?
|
||||
## Note
|
||||
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 {
|
||||
@@ -1598,8 +1850,18 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
displayName: node.display_name || 'Unknown Node',
|
||||
documentation: node.documentation,
|
||||
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() {
|
||||
await this.ensureInitialized();
|
||||
if (!this.db)
|
||||
@@ -2799,7 +3061,26 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
process.stdin.resume();
|
||||
}
|
||||
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...');
|
||||
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) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (this.db) {
|
||||
if (this.useSharedDatabase && this.sharedDbState) {
|
||||
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');
|
||||
}
|
||||
catch (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;
|
||||
|
||||
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',
|
||||
category: 'discovery',
|
||||
essentials: {
|
||||
description: 'Text search across node names and descriptions. Returns most relevant nodes first, with frequently-used nodes (HTTP Request, Webhook, Set, Code, Slack) prioritized in results. Searches all 500+ nodes in the database.',
|
||||
keyParameters: ['query', 'mode', 'limit'],
|
||||
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', 'source', 'includeExamples'],
|
||||
example: 'search_nodes({query: "webhook"})',
|
||||
performance: '<20ms even for complex queries',
|
||||
tips: [
|
||||
'OR mode (default): Matches any search word',
|
||||
'AND mode: Requires all words present',
|
||||
'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: {
|
||||
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: {
|
||||
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 },
|
||||
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: [
|
||||
'search_nodes({query: "webhook"}) - Returns Webhook node as top result',
|
||||
'search_nodes({query: "database"}) - Returns MySQL, Postgres, MongoDB, Redis, etc.',
|
||||
'search_nodes({query: "google sheets", mode: "AND"}) - Requires both words',
|
||||
'search_nodes({query: "slak", mode: "FUZZY"}) - Finds Slack despite typo',
|
||||
'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: [
|
||||
'Finding nodes when you know partial names',
|
||||
'Discovering nodes by functionality (e.g., "email", "database", "transform")',
|
||||
'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',
|
||||
bestPractices: [
|
||||
'Start with single keywords for broadest results',
|
||||
'Use FUZZY mode when users might misspell node names',
|
||||
'AND mode works best for 2-3 word searches',
|
||||
'Combine with get_node 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: [
|
||||
'AND mode searches all fields (name, description) not just node names',
|
||||
'FUZZY mode with very short queries (1-2 chars) may return unexpected results',
|
||||
'Exact matches in quotes are case-sensitive'
|
||||
'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']
|
||||
}
|
||||
|
||||
@@ -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"}
|
||||
@@ -195,12 +195,12 @@ Please choose a different name.
|
||||
- Can rename a node and add/remove connections using the new name in the same batch
|
||||
- Use \`validateOnly: true\` to preview effects before applying
|
||||
|
||||
## Removing Properties with undefined
|
||||
## Removing Properties with null
|
||||
|
||||
To remove a property from a node, set its value to \`undefined\` in the updates object. This is essential when migrating from deprecated properties or cleaning up optional configuration fields.
|
||||
To remove a property from a node, set its value to \`null\` in the updates object. This is essential when migrating from deprecated properties or cleaning up optional configuration fields.
|
||||
|
||||
### Why Use undefined?
|
||||
- **Property removal vs. null**: Setting a property to \`undefined\` removes it completely from the node object, while \`null\` sets the property to a null value
|
||||
### Why Use null?
|
||||
- **Property removal**: Setting a property to \`null\` removes it completely from the node object
|
||||
- **Validation constraints**: Some properties are mutually exclusive (e.g., \`continueOnFail\` and \`onError\`). Simply setting one without removing the other will fail validation
|
||||
- **Deprecated property migration**: When n8n deprecates properties, you must remove the old property before the new one will work
|
||||
|
||||
@@ -212,7 +212,7 @@ n8n_update_partial_workflow({
|
||||
operations: [{
|
||||
type: "updateNode",
|
||||
nodeName: "HTTP Request",
|
||||
updates: { onError: undefined }
|
||||
updates: { onError: null }
|
||||
}]
|
||||
});
|
||||
|
||||
@@ -222,7 +222,7 @@ n8n_update_partial_workflow({
|
||||
operations: [{
|
||||
type: "updateNode",
|
||||
nodeId: "node_abc",
|
||||
updates: { disabled: undefined }
|
||||
updates: { disabled: null }
|
||||
}]
|
||||
});
|
||||
\`\`\`
|
||||
@@ -236,7 +236,7 @@ n8n_update_partial_workflow({
|
||||
operations: [{
|
||||
type: "updateNode",
|
||||
nodeName: "API Request",
|
||||
updates: { "parameters.authentication": undefined }
|
||||
updates: { "parameters.authentication": null }
|
||||
}]
|
||||
});
|
||||
|
||||
@@ -246,7 +246,7 @@ n8n_update_partial_workflow({
|
||||
operations: [{
|
||||
type: "updateNode",
|
||||
nodeName: "HTTP Request",
|
||||
updates: { "parameters.headers": undefined }
|
||||
updates: { "parameters.headers": null }
|
||||
}]
|
||||
});
|
||||
\`\`\`
|
||||
@@ -272,7 +272,7 @@ n8n_update_partial_workflow({
|
||||
type: "updateNode",
|
||||
nodeName: "HTTP Request",
|
||||
updates: {
|
||||
continueOnFail: undefined,
|
||||
continueOnFail: null,
|
||||
onError: "continueErrorOutput"
|
||||
}
|
||||
}]
|
||||
@@ -288,15 +288,15 @@ n8n_update_partial_workflow({
|
||||
type: "updateNode",
|
||||
nodeName: "Data Processor",
|
||||
updates: {
|
||||
continueOnFail: undefined,
|
||||
alwaysOutputData: undefined,
|
||||
"parameters.legacy_option": undefined
|
||||
continueOnFail: null,
|
||||
alwaysOutputData: null,
|
||||
"parameters.legacy_option": null
|
||||
}
|
||||
}]
|
||||
});
|
||||
\`\`\`
|
||||
|
||||
### When to Use undefined
|
||||
### When to Use null
|
||||
- Removing deprecated properties during migration
|
||||
- Cleaning up optional configuration flags
|
||||
- Resolving mutual exclusivity validation errors
|
||||
@@ -342,11 +342,11 @@ n8n_update_partial_workflow({
|
||||
'// Rewire AI Agent to use different language model\nn8n_update_partial_workflow({id: "ai9", operations: [{type: "rewireConnection", source: "AI Agent", from: "OpenAI Chat Model", to: "Anthropic Chat Model", sourceOutput: "ai_languageModel"}]})',
|
||||
'// Replace all AI tools for an agent\nn8n_update_partial_workflow({id: "ai10", operations: [\n {type: "removeConnection", source: "Old Tool 1", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "removeConnection", source: "Old Tool 2", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "addConnection", source: "New HTTP Tool", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "addConnection", source: "New Code Tool", target: "AI Agent", sourceOutput: "ai_tool"}\n]})',
|
||||
'\n// ============ REMOVING PROPERTIES EXAMPLES ============',
|
||||
'// Remove a simple property\nn8n_update_partial_workflow({id: "rm1", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {onError: undefined}}]})',
|
||||
'// Migrate from deprecated continueOnFail to onError\nn8n_update_partial_workflow({id: "rm2", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {continueOnFail: undefined, onError: "continueErrorOutput"}}]})',
|
||||
'// Remove nested property\nn8n_update_partial_workflow({id: "rm3", operations: [{type: "updateNode", nodeName: "API Request", updates: {"parameters.authentication": undefined}}]})',
|
||||
'// Remove multiple properties\nn8n_update_partial_workflow({id: "rm4", operations: [{type: "updateNode", nodeName: "Data Processor", updates: {continueOnFail: undefined, alwaysOutputData: undefined, "parameters.legacy_option": undefined}}]})',
|
||||
'// Remove entire array property\nn8n_update_partial_workflow({id: "rm5", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {"parameters.headers": undefined}}]})'
|
||||
'// Remove a simple property\nn8n_update_partial_workflow({id: "rm1", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {onError: null}}]})',
|
||||
'// Migrate from deprecated continueOnFail to onError\nn8n_update_partial_workflow({id: "rm2", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {continueOnFail: null, onError: "continueErrorOutput"}}]})',
|
||||
'// Remove nested property\nn8n_update_partial_workflow({id: "rm3", operations: [{type: "updateNode", nodeName: "API Request", updates: {"parameters.authentication": null}}]})',
|
||||
'// Remove multiple properties\nn8n_update_partial_workflow({id: "rm4", operations: [{type: "updateNode", nodeName: "Data Processor", updates: {continueOnFail: null, alwaysOutputData: null, "parameters.legacy_option": null}}]})',
|
||||
'// Remove entire array property\nn8n_update_partial_workflow({id: "rm5", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {"parameters.headers": null}}]})'
|
||||
],
|
||||
useCases: [
|
||||
'Rewire connections when replacing nodes',
|
||||
@@ -384,9 +384,9 @@ n8n_update_partial_workflow({
|
||||
'Use targetIndex for fallback models (primary=0, fallback=1)',
|
||||
'Batch AI component connections in a single operation for atomicity',
|
||||
'Validate AI workflows after connection changes to catch configuration errors',
|
||||
'To remove properties, set them to undefined (not null) in the updates object',
|
||||
'To remove properties, set them to null in the updates object',
|
||||
'When migrating from deprecated properties, remove the old property and add the new one in the same operation',
|
||||
'Use undefined to resolve mutual exclusivity validation errors between properties',
|
||||
'Use null to resolve mutual exclusivity validation errors between properties',
|
||||
'Batch multiple property removals in a single updateNode operation for efficiency'
|
||||
],
|
||||
pitfalls: [
|
||||
@@ -408,8 +408,8 @@ n8n_update_partial_workflow({
|
||||
'**Auto-sanitization runs on ALL nodes**: When ANY update is made, ALL nodes in the workflow are sanitized (not just modified ones)',
|
||||
'**Auto-sanitization cannot fix everything**: It fixes operator structures and missing metadata, but cannot fix broken connections or branch mismatches',
|
||||
'**Corrupted workflows beyond repair**: Workflows in paradoxical states (API returns corrupt, API rejects updates) cannot be fixed via API - must be recreated',
|
||||
'Setting a property to null does NOT remove it - use undefined instead',
|
||||
'When properties are mutually exclusive (e.g., continueOnFail and onError), setting only the new property will fail - you must remove the old one with undefined',
|
||||
'To remove a property, set it to null in the updates object',
|
||||
'When properties are mutually exclusive (e.g., continueOnFail and onError), setting only the new property will fail - you must remove the old one with null',
|
||||
'Removing a required property may cause validation errors - check node documentation first',
|
||||
'Nested property removal with dot notation only removes the specific nested field, not the entire parent object',
|
||||
'Array index notation (e.g., "parameters.headers[0]") is not supported - remove the entire array property instead'
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"n8n-update-partial-workflow.js","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"n8n-update-partial-workflow.js","sourceRoot":"","sources":["../../../../src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts"],"names":[],"mappings":"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"}
|
||||
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',
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -295,7 +295,7 @@ exports.n8nManagementTools = [
|
||||
description: 'Types of fixes to apply (default: all)',
|
||||
items: {
|
||||
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: {
|
||||
|
||||
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)',
|
||||
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'],
|
||||
},
|
||||
|
||||
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>;
|
||||
credentials?: Record<string, unknown> | undefined;
|
||||
retryOnFail?: boolean | undefined;
|
||||
continueOnFail?: boolean | undefined;
|
||||
maxTries?: number | undefined;
|
||||
waitBetweenTries?: number | undefined;
|
||||
alwaysOutputData?: boolean | undefined;
|
||||
continueOnFail?: boolean | undefined;
|
||||
executeOnce?: boolean | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
notes?: string | undefined;
|
||||
@@ -43,10 +43,10 @@ export declare const workflowNodeSchema: z.ZodObject<{
|
||||
parameters: Record<string, unknown>;
|
||||
credentials?: Record<string, unknown> | undefined;
|
||||
retryOnFail?: boolean | undefined;
|
||||
continueOnFail?: boolean | undefined;
|
||||
maxTries?: number | undefined;
|
||||
waitBetweenTries?: number | undefined;
|
||||
alwaysOutputData?: boolean | undefined;
|
||||
continueOnFail?: boolean | undefined;
|
||||
executeOnce?: boolean | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
notes?: string | undefined;
|
||||
@@ -155,6 +155,11 @@ export declare const workflowConnectionSchema: z.ZodRecord<z.ZodString, z.ZodObj
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_tool?: {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_languageModel?: {
|
||||
type: string;
|
||||
node: string;
|
||||
@@ -165,11 +170,6 @@ export declare const workflowConnectionSchema: z.ZodRecord<z.ZodString, z.ZodObj
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_tool?: {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_embedding?: {
|
||||
type: string;
|
||||
node: string;
|
||||
@@ -191,6 +191,11 @@ export declare const workflowConnectionSchema: z.ZodRecord<z.ZodString, z.ZodObj
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_tool?: {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_languageModel?: {
|
||||
type: string;
|
||||
node: string;
|
||||
@@ -201,11 +206,6 @@ export declare const workflowConnectionSchema: z.ZodRecord<z.ZodString, z.ZodObj
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_tool?: {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_embedding?: {
|
||||
type: string;
|
||||
node: string;
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"node-specific-validators.d.ts","sourceRoot":"","sources":["../../src/services/node-specific-validators.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAExE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC9B;AAED,qBAAa,sBAAsB;IAIjC,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAqE1D,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAkDvC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAsBzC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA4BzC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA2CzC,MAAM,CAAC,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAiDjE,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA0BzC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAkBvC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAsBzC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA4BzC,OAAO,CAAC,MAAM,CAAC,yBAAyB;IAwCxC,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAwF3D,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAyG5D,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAkI7D,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAmG5D,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IA6F1D,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAkF/B,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAiGhE,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IA6D5D,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IA8DzD,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAuDrC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAuDjC,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAsFtC,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAuKnC,OAAO,CAAC,MAAM,CAAC,oBAAoB;IA2EnC,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;CAoDzD"}
|
||||
{"version":3,"file":"node-specific-validators.d.ts","sourceRoot":"","sources":["../../src/services/node-specific-validators.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAExE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC9B;AAED,qBAAa,sBAAsB;IAIjC,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAqE1D,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAkDvC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAsBzC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA4BzC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA2CzC,MAAM,CAAC,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAiDjE,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA0BzC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAkBvC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAsBzC,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA4BzC,OAAO,CAAC,MAAM,CAAC,yBAAyB;IAwCxC,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAwF3D,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAyG5D,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAkI7D,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAmG5D,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IA6F1D,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAkF/B,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAiGhE,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IA6D5D,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IA8DzD,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAuDrC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAuDjC,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAyFtC,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAuKnC,OAAO,CAAC,MAAM,CAAC,oBAAoB;IA2EnC,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;CAoDzD"}
|
||||
5
dist/services/node-specific-validators.js
vendored
5
dist/services/node-specific-validators.js
vendored
@@ -1067,7 +1067,8 @@ class NodeSpecificValidators {
|
||||
fix: 'Wrap in array: return [{json: yourObject}]'
|
||||
});
|
||||
}
|
||||
if (/return\s+(true|false|null|undefined|\d+|['"`])/m.test(code)) {
|
||||
const hasHelperFunctions = /(?:function\s+\w+\s*\(|(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>|\w+\s*=>))/.test(code);
|
||||
if (!hasHelperFunctions && /return\s+(true|false|null|undefined|\d+|['"`])/m.test(code)) {
|
||||
errors.push({
|
||||
type: 'invalid_value',
|
||||
property: 'jsCode',
|
||||
@@ -1149,7 +1150,7 @@ class NodeSpecificValidators {
|
||||
}
|
||||
});
|
||||
if (language === 'javaScript') {
|
||||
if (/\$(?![a-zA-Z])/.test(code) && !code.includes('${')) {
|
||||
if (/\$(?![a-zA-Z_(])/.test(code) && !code.includes('${')) {
|
||||
warnings.push({
|
||||
type: 'best_practice',
|
||||
message: 'Invalid $ usage detected',
|
||||
|
||||
File diff suppressed because one or more lines are too long
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 { PostUpdateGuidance } from './post-update-validator';
|
||||
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 {
|
||||
applyFixes: boolean;
|
||||
fixTypes?: FixType[];
|
||||
@@ -68,6 +69,12 @@ export declare class WorkflowAutoFixer {
|
||||
private filterOperationsByFixes;
|
||||
private calculateStats;
|
||||
private generateSummary;
|
||||
private processConnectionFixes;
|
||||
private fixNumericKeys;
|
||||
private fixIdToName;
|
||||
private fixInvalidTypes;
|
||||
private fixInputIndices;
|
||||
private fixDuplicateConnections;
|
||||
private processVersionUpgradeFixes;
|
||||
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 };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.WorkflowAutoFixer = void 0;
|
||||
exports.WorkflowAutoFixer = exports.CONNECTION_FIX_TYPES = void 0;
|
||||
exports.isNodeFormatIssue = isNodeFormatIssue;
|
||||
const crypto_1 = __importDefault(require("crypto"));
|
||||
const workflow_validator_1 = require("./workflow-validator");
|
||||
const node_similarity_service_1 = require("./node-similarity-service");
|
||||
const logger_1 = require("../utils/logger");
|
||||
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 post_update_validator_1 = require("./post-update-validator");
|
||||
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) {
|
||||
return 'nodeName' in issue && 'nodeId' in issue &&
|
||||
typeof issue.nodeName === 'string' &&
|
||||
@@ -72,6 +80,7 @@ class WorkflowAutoFixer {
|
||||
if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('version-migration')) {
|
||||
await this.processVersionMigrationFixes(workflow, nodeMap, operations, fixes, postUpdateGuidance);
|
||||
}
|
||||
this.processConnectionFixes(workflow, validationResult, fullConfig, operations, fixes);
|
||||
const filteredFixes = this.filterByConfidence(fixes, fullConfig.confidenceThreshold);
|
||||
const filteredOperations = this.filterOperationsByFixes(operations, filteredFixes, fixes);
|
||||
const limitedFixes = filteredFixes.slice(0, fullConfig.maxFixes);
|
||||
@@ -393,10 +402,14 @@ class WorkflowAutoFixer {
|
||||
}
|
||||
filterOperationsByFixes(operations, filteredFixes, allFixes) {
|
||||
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 => {
|
||||
if (op.type === 'updateNode') {
|
||||
return fixedNodes.has(op.nodeId || '');
|
||||
}
|
||||
if (op.type === 'replaceConnections') {
|
||||
return hasConnectionFixes;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -411,7 +424,12 @@ class WorkflowAutoFixer {
|
||||
'webhook-missing-path': 0,
|
||||
'typeversion-upgrade': 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: {
|
||||
'high': 0,
|
||||
@@ -454,11 +472,299 @@ class WorkflowAutoFixer {
|
||||
if (stats.byType['tool-variant-correction'] > 0) {
|
||||
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) {
|
||||
return `Fixed ${stats.total} ${stats.total === 1 ? 'issue' : 'issues'}`;
|
||||
}
|
||||
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) {
|
||||
if (!this.versionService || !this.migrationService || !this.postUpdateValidator) {
|
||||
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
4
dist/services/workflow-diff-engine.d.ts
vendored
4
dist/services/workflow-diff-engine.d.ts
vendored
@@ -3,6 +3,10 @@ import { Workflow } from '../types/n8n-api';
|
||||
export declare class WorkflowDiffEngine {
|
||||
private renameMap;
|
||||
private warnings;
|
||||
private modifiedNodeIds;
|
||||
private removedNodeNames;
|
||||
private tagsToAdd;
|
||||
private tagsToRemove;
|
||||
applyDiff(workflow: Workflow, request: WorkflowDiffRequest): Promise<WorkflowDiffResult>;
|
||||
private validateOperation;
|
||||
private applyOperation;
|
||||
|
||||
2
dist/services/workflow-diff-engine.d.ts.map
vendored
2
dist/services/workflow-diff-engine.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"workflow-diff-engine.d.ts","sourceRoot":"","sources":["../../src/services/workflow-diff-engine.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,mBAAmB,EACnB,kBAAkB,EAsBnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAoC,MAAM,kBAAkB,CAAC;AAQ9E,qBAAa,kBAAkB;IAE7B,OAAO,CAAC,SAAS,CAAkC;IAEnD,OAAO,CAAC,QAAQ,CAAqC;IAK/C,SAAS,CACb,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC;IA0M9B,OAAO,CAAC,iBAAiB;IAwCzB,OAAO,CAAC,cAAc;IAyDtB,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,kBAAkB;IAuB1B,OAAO,CAAC,kBAAkB;IAoC1B,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,qBAAqB;IAkD7B,OAAO,CAAC,wBAAwB;IAuChC,OAAO,CAAC,wBAAwB;IAmDhC,OAAO,CAAC,YAAY;IA2BpB,OAAO,CAAC,eAAe;IAkCvB,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,sBAAsB;IAgD9B,OAAO,CAAC,kBAAkB;IA4C1B,OAAO,CAAC,qBAAqB;IA2C7B,OAAO,CAAC,qBAAqB;IA0B7B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,wBAAwB;IAchC,OAAO,CAAC,0BAA0B;IAMlC,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,uBAAuB;IAO/B,OAAO,CAAC,6BAA6B;IAKrC,OAAO,CAAC,0BAA0B;IA0BlC,OAAO,CAAC,0BAA0B;IA0ElC,OAAO,CAAC,uBAAuB;IAe/B,OAAO,CAAC,0BAA0B;IAkElC,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,QAAQ;IAsChB,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,iBAAiB;CAc1B"}
|
||||
{"version":3,"file":"workflow-diff-engine.d.ts","sourceRoot":"","sources":["../../src/services/workflow-diff-engine.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,mBAAmB,EACnB,kBAAkB,EAsBnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAoC,MAAM,kBAAkB,CAAC;AAY9E,qBAAa,kBAAkB;IAE7B,OAAO,CAAC,SAAS,CAAkC;IAEnD,OAAO,CAAC,QAAQ,CAAqC;IAErD,OAAO,CAAC,eAAe,CAAqB;IAE5C,OAAO,CAAC,gBAAgB,CAAqB;IAE7C,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,YAAY,CAAgB;IAK9B,SAAS,CACb,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC;IAqN9B,OAAO,CAAC,iBAAiB;IAwCzB,OAAO,CAAC,cAAc;IAyDtB,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,kBAAkB;IAuB1B,OAAO,CAAC,kBAAkB;IAoC1B,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,qBAAqB;IAkD7B,OAAO,CAAC,wBAAwB;IA6ChC,OAAO,CAAC,wBAAwB;IAmDhC,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,eAAe;IAwCvB,OAAO,CAAC,eAAe;IA0BvB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,sBAAsB;IAwD9B,OAAO,CAAC,kBAAkB;IA6C1B,OAAO,CAAC,qBAAqB;IAuC7B,OAAO,CAAC,qBAAqB;IA0B7B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,wBAAwB;IAchC,OAAO,CAAC,0BAA0B;IAMlC,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,uBAAuB;IAO/B,OAAO,CAAC,6BAA6B;IAKrC,OAAO,CAAC,0BAA0B;IA0BlC,OAAO,CAAC,0BAA0B;IA+ElC,OAAO,CAAC,uBAAuB;IAe/B,OAAO,CAAC,0BAA0B;IAkElC,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,QAAQ;IAsChB,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,iBAAiB;CAoB1B"}
|
||||
106
dist/services/workflow-diff-engine.js
vendored
106
dist/services/workflow-diff-engine.js
vendored
@@ -10,11 +10,19 @@ class WorkflowDiffEngine {
|
||||
constructor() {
|
||||
this.renameMap = new Map();
|
||||
this.warnings = [];
|
||||
this.modifiedNodeIds = new Set();
|
||||
this.removedNodeNames = new Set();
|
||||
this.tagsToAdd = [];
|
||||
this.tagsToRemove = [];
|
||||
}
|
||||
async applyDiff(workflow, request) {
|
||||
try {
|
||||
this.renameMap.clear();
|
||||
this.warnings = [];
|
||||
this.modifiedNodeIds.clear();
|
||||
this.removedNodeNames.clear();
|
||||
this.tagsToAdd = [];
|
||||
this.tagsToRemove = [];
|
||||
const workflowCopy = JSON.parse(JSON.stringify(workflow));
|
||||
const nodeOperationTypes = ['addNode', 'removeNode', 'updateNode', 'moveNode', 'enableNode', 'disableNode'];
|
||||
const nodeOperations = [];
|
||||
@@ -82,7 +90,9 @@ class WorkflowDiffEngine {
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
warnings: this.warnings.length > 0 ? this.warnings : undefined,
|
||||
applied: appliedIndices,
|
||||
failed: failedIndices
|
||||
failed: failedIndices,
|
||||
tagsToAdd: this.tagsToAdd.length > 0 ? this.tagsToAdd : undefined,
|
||||
tagsToRemove: this.tagsToRemove.length > 0 ? this.tagsToRemove : undefined
|
||||
};
|
||||
}
|
||||
else {
|
||||
@@ -142,8 +152,15 @@ class WorkflowDiffEngine {
|
||||
};
|
||||
}
|
||||
}
|
||||
workflowCopy.nodes = workflowCopy.nodes.map((node) => (0, node_sanitizer_1.sanitizeNode)(node));
|
||||
logger.debug('Applied full-workflow sanitization to all nodes');
|
||||
if (this.modifiedNodeIds.size > 0) {
|
||||
workflowCopy.nodes = workflowCopy.nodes.map((node) => {
|
||||
if (this.modifiedNodeIds.has(node.id)) {
|
||||
return (0, node_sanitizer_1.sanitizeNode)(node);
|
||||
}
|
||||
return node;
|
||||
});
|
||||
logger.debug(`Sanitized ${this.modifiedNodeIds.size} modified nodes`);
|
||||
}
|
||||
if (request.validateOnly) {
|
||||
return {
|
||||
success: true,
|
||||
@@ -162,7 +179,9 @@ class WorkflowDiffEngine {
|
||||
message: `Successfully applied ${operationsApplied} operations (${nodeOperations.length} node ops, ${otherOperations.length} other ops)`,
|
||||
warnings: this.warnings.length > 0 ? this.warnings : 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -302,7 +321,7 @@ class WorkflowDiffEngine {
|
||||
return `Invalid parameter 'changes'. The updateNode operation requires 'updates' (not 'changes'). Example: {type: "updateNode", nodeId: "abc", updates: {name: "New Name", "parameters.url": "https://example.com"}}`;
|
||||
}
|
||||
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);
|
||||
if (!node) {
|
||||
@@ -382,12 +401,18 @@ class WorkflowDiffEngine {
|
||||
const sourceNode = this.findNode(workflow, operation.source, operation.source);
|
||||
const targetNode = this.findNode(workflow, operation.target, operation.target);
|
||||
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
|
||||
.map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
|
||||
.join(', ');
|
||||
return `Source node not found: "${operation.source}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters.`;
|
||||
}
|
||||
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
|
||||
.map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
|
||||
.join(', ');
|
||||
@@ -461,34 +486,40 @@ class WorkflowDiffEngine {
|
||||
executeOnce: operation.node.executeOnce
|
||||
};
|
||||
const sanitizedNode = (0, node_sanitizer_1.sanitizeNode)(newNode);
|
||||
this.modifiedNodeIds.add(sanitizedNode.id);
|
||||
workflow.nodes.push(sanitizedNode);
|
||||
}
|
||||
applyRemoveNode(workflow, operation) {
|
||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||
if (!node)
|
||||
return;
|
||||
this.removedNodeNames.add(node.name);
|
||||
const index = workflow.nodes.findIndex(n => n.id === node.id);
|
||||
if (index !== -1) {
|
||||
workflow.nodes.splice(index, 1);
|
||||
}
|
||||
delete workflow.connections[node.name];
|
||||
Object.keys(workflow.connections).forEach(sourceName => {
|
||||
const sourceConnections = workflow.connections[sourceName];
|
||||
Object.keys(sourceConnections).forEach(outputName => {
|
||||
sourceConnections[outputName] = sourceConnections[outputName].map(connections => connections.filter(conn => conn.node !== node.name)).filter(connections => connections.length > 0);
|
||||
if (sourceConnections[outputName].length === 0) {
|
||||
for (const [sourceName, sourceConnections] of Object.entries(workflow.connections)) {
|
||||
for (const [outputName, outputConns] of Object.entries(sourceConnections)) {
|
||||
sourceConnections[outputName] = outputConns.map(connections => connections.filter(conn => conn.node !== node.name));
|
||||
const trimmed = sourceConnections[outputName];
|
||||
while (trimmed.length > 0 && trimmed[trimmed.length - 1].length === 0) {
|
||||
trimmed.pop();
|
||||
}
|
||||
if (trimmed.length === 0) {
|
||||
delete sourceConnections[outputName];
|
||||
}
|
||||
});
|
||||
}
|
||||
if (Object.keys(sourceConnections).length === 0) {
|
||||
delete workflow.connections[sourceName];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
applyUpdateNode(workflow, operation) {
|
||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||
if (!node)
|
||||
return;
|
||||
this.modifiedNodeIds.add(node.id);
|
||||
if (operation.updates.name && operation.updates.name !== node.name) {
|
||||
const oldName = node.name;
|
||||
const newName = operation.updates.name;
|
||||
@@ -521,8 +552,13 @@ class WorkflowDiffEngine {
|
||||
}
|
||||
resolveSmartParameters(workflow, operation) {
|
||||
const sourceNode = this.findNode(workflow, operation.source, operation.source);
|
||||
let sourceOutput = operation.sourceOutput ?? 'main';
|
||||
let sourceOutput = String(operation.sourceOutput ?? 'main');
|
||||
let sourceIndex = operation.sourceIndex ?? 0;
|
||||
if (/^\d+$/.test(sourceOutput) && operation.sourceIndex === undefined
|
||||
&& operation.branch === undefined && operation.case === undefined) {
|
||||
sourceIndex = parseInt(sourceOutput, 10);
|
||||
sourceOutput = 'main';
|
||||
}
|
||||
if (operation.branch !== undefined && operation.sourceIndex === undefined) {
|
||||
if (sourceNode?.type === 'n8n-nodes-base.if') {
|
||||
sourceIndex = operation.branch === 'true' ? 0 : 1;
|
||||
@@ -556,7 +592,7 @@ class WorkflowDiffEngine {
|
||||
if (!sourceNode || !targetNode)
|
||||
return;
|
||||
const { sourceOutput, sourceIndex } = this.resolveSmartParameters(workflow, operation);
|
||||
const targetInput = operation.targetInput ?? sourceOutput;
|
||||
const targetInput = String(operation.targetInput ?? sourceOutput);
|
||||
const targetIndex = operation.targetIndex ?? 0;
|
||||
if (!workflow.connections[sourceNode.name]) {
|
||||
workflow.connections[sourceNode.name] = {};
|
||||
@@ -581,12 +617,9 @@ class WorkflowDiffEngine {
|
||||
const sourceNode = this.findNode(workflow, operation.source, operation.source);
|
||||
const targetNode = this.findNode(workflow, operation.target, operation.target);
|
||||
if (!sourceNode || !targetNode) {
|
||||
if (operation.ignoreErrors) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const sourceOutput = operation.sourceOutput || 'main';
|
||||
const sourceOutput = String(operation.sourceOutput ?? 'main');
|
||||
const connections = workflow.connections[sourceNode.name]?.[sourceOutput];
|
||||
if (!connections)
|
||||
return;
|
||||
@@ -633,19 +666,21 @@ class WorkflowDiffEngine {
|
||||
workflow.name = operation.name;
|
||||
}
|
||||
applyAddTag(workflow, operation) {
|
||||
if (!workflow.tags) {
|
||||
workflow.tags = [];
|
||||
const removeIdx = this.tagsToRemove.indexOf(operation.tag);
|
||||
if (removeIdx !== -1) {
|
||||
this.tagsToRemove.splice(removeIdx, 1);
|
||||
}
|
||||
if (!workflow.tags.includes(operation.tag)) {
|
||||
workflow.tags.push(operation.tag);
|
||||
if (!this.tagsToAdd.includes(operation.tag)) {
|
||||
this.tagsToAdd.push(operation.tag);
|
||||
}
|
||||
}
|
||||
applyRemoveTag(workflow, operation) {
|
||||
if (!workflow.tags)
|
||||
return;
|
||||
const index = workflow.tags.indexOf(operation.tag);
|
||||
if (index !== -1) {
|
||||
workflow.tags.splice(index, 1);
|
||||
const addIdx = this.tagsToAdd.indexOf(operation.tag);
|
||||
if (addIdx !== -1) {
|
||||
this.tagsToAdd.splice(addIdx, 1);
|
||||
}
|
||||
if (!this.tagsToRemove.includes(operation.tag)) {
|
||||
this.tagsToRemove.push(operation.tag);
|
||||
}
|
||||
}
|
||||
validateActivateWorkflow(workflow, operation) {
|
||||
@@ -734,7 +769,10 @@ class WorkflowDiffEngine {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})).filter(conns => conns.length > 0);
|
||||
}));
|
||||
while (filteredConnections.length > 0 && filteredConnections[filteredConnections.length - 1].length === 0) {
|
||||
filteredConnections.pop();
|
||||
}
|
||||
if (filteredConnections.length === 0) {
|
||||
delete outputs[outputName];
|
||||
}
|
||||
@@ -818,12 +856,20 @@ class WorkflowDiffEngine {
|
||||
let current = obj;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in current) || typeof current[key] !== 'object') {
|
||||
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
||||
if (value === null)
|
||||
return;
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
current[keys[keys.length - 1]] = value;
|
||||
const finalKey = keys[keys.length - 1];
|
||||
if (value === null) {
|
||||
delete current[finalKey];
|
||||
}
|
||||
else {
|
||||
current[finalKey] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.WorkflowDiffEngine = WorkflowDiffEngine;
|
||||
|
||||
2
dist/services/workflow-diff-engine.js.map
vendored
2
dist/services/workflow-diff-engine.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 { EnhancedConfigValidator } from './enhanced-config-validator';
|
||||
export declare const VALID_CONNECTION_TYPES: Set<string>;
|
||||
interface WorkflowNode {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -21,17 +22,7 @@ interface WorkflowNode {
|
||||
}
|
||||
interface WorkflowConnection {
|
||||
[sourceNode: string]: {
|
||||
main?: Array<Array<{
|
||||
node: string;
|
||||
type: string;
|
||||
index: number;
|
||||
}>>;
|
||||
error?: Array<Array<{
|
||||
node: string;
|
||||
type: string;
|
||||
index: number;
|
||||
}>>;
|
||||
ai_tool?: Array<Array<{
|
||||
[outputType: string]: Array<Array<{
|
||||
node: string;
|
||||
type: string;
|
||||
index: number;
|
||||
@@ -94,6 +85,15 @@ export declare class WorkflowValidator {
|
||||
private validateErrorOutputConfiguration;
|
||||
private validateAIToolConnection;
|
||||
private validateAIToolSource;
|
||||
private getNodeOutputTypes;
|
||||
private validateNotAISubNode;
|
||||
private getShortNodeType;
|
||||
private getConditionalOutputInfo;
|
||||
private validateOutputIndexBounds;
|
||||
private validateConditionalBranchUsage;
|
||||
private validateInputIndexBounds;
|
||||
private flagOrphanedNodes;
|
||||
private validateTriggerReachability;
|
||||
private hasCycle;
|
||||
private validateExpressions;
|
||||
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 };
|
||||
};
|
||||
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 expression_validator_1 = require("./expression-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 tool_variant_generator_1 = require("./tool-variant-generator");
|
||||
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 {
|
||||
constructor(nodeRepository, nodeValidator) {
|
||||
this.nodeRepository = nodeRepository;
|
||||
@@ -393,51 +402,37 @@ class WorkflowValidator {
|
||||
result.statistics.invalidConnections++;
|
||||
continue;
|
||||
}
|
||||
if (outputs.main) {
|
||||
this.validateConnectionOutputs(sourceName, outputs.main, nodeMap, nodeIdMap, result, 'main');
|
||||
}
|
||||
if (outputs.error) {
|
||||
this.validateConnectionOutputs(sourceName, outputs.error, nodeMap, nodeIdMap, result, 'error');
|
||||
}
|
||||
if (outputs.ai_tool) {
|
||||
this.validateAIToolSource(sourceNode, result);
|
||||
this.validateConnectionOutputs(sourceName, outputs.ai_tool, nodeMap, nodeIdMap, result, 'ai_tool');
|
||||
for (const [outputKey, outputConnections] of Object.entries(outputs)) {
|
||||
if (!exports.VALID_CONNECTION_TYPES.has(outputKey)) {
|
||||
let suggestion = '';
|
||||
if (/^\d+$/.test(outputKey)) {
|
||||
suggestion = ` If you meant to use output index ${outputKey}, use main[${outputKey}] instead.`;
|
||||
}
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
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();
|
||||
Object.keys(workflow.connections).forEach(name => connectedNodes.add(name));
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
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.validateTriggerReachability(workflow, result);
|
||||
}
|
||||
else {
|
||||
this.flagOrphanedNodes(workflow, result);
|
||||
}
|
||||
if (profile !== 'minimal' && this.hasCycle(workflow)) {
|
||||
result.errors.push({
|
||||
@@ -450,6 +445,8 @@ class WorkflowValidator {
|
||||
const sourceNode = nodeMap.get(sourceName);
|
||||
if (outputType === 'main' && sourceNode) {
|
||||
this.validateErrorOutputConfiguration(sourceName, sourceNode, outputs, nodeMap, result);
|
||||
this.validateOutputIndexBounds(sourceNode, outputs, result);
|
||||
this.validateConditionalBranchUsage(sourceNode, outputs, result);
|
||||
}
|
||||
outputs.forEach((outputConnections, outputIndex) => {
|
||||
if (!outputConnections)
|
||||
@@ -463,6 +460,20 @@ class WorkflowValidator {
|
||||
result.statistics.invalidConnections++;
|
||||
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' ||
|
||||
sourceNode.type === 'nodes-base.splitInBatches');
|
||||
if (isSplitInBatches) {
|
||||
@@ -506,6 +517,9 @@ class WorkflowValidator {
|
||||
if (outputType === 'ai_tool') {
|
||||
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'
|
||||
});
|
||||
}
|
||||
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) {
|
||||
const visited = new Set();
|
||||
const recursionStack = new Set();
|
||||
@@ -657,23 +919,13 @@ class WorkflowValidator {
|
||||
const connections = workflow.connections[nodeName];
|
||||
if (connections) {
|
||||
const allTargets = [];
|
||||
if (connections.main) {
|
||||
connections.main.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);
|
||||
});
|
||||
for (const outputConns of Object.values(connections)) {
|
||||
if (Array.isArray(outputConns)) {
|
||||
outputConns.flat().forEach(conn => {
|
||||
if (conn)
|
||||
allTargets.push(conn.node);
|
||||
});
|
||||
}
|
||||
}
|
||||
const currentNodeType = nodeTypeMap.get(nodeName);
|
||||
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 deadLetterQueue;
|
||||
private readonly maxDeadLetterSize;
|
||||
private eventListeners;
|
||||
private started;
|
||||
constructor(supabase: SupabaseClient | null, isEnabled: () => boolean);
|
||||
start(): 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.deadLetterQueue = [];
|
||||
this.maxDeadLetterSize = 100;
|
||||
this.eventListeners = {};
|
||||
this.started = false;
|
||||
this.circuitBreaker = new telemetry_error_1.TelemetryCircuitBreaker();
|
||||
}
|
||||
start() {
|
||||
if (!this.isEnabled() || !this.supabase)
|
||||
return;
|
||||
if (this.started) {
|
||||
logger_1.logger.debug('Telemetry batch processor already started, skipping');
|
||||
return;
|
||||
}
|
||||
this.flushTimer = setInterval(() => {
|
||||
this.flush();
|
||||
}, telemetry_types_1.TELEMETRY_CONFIG.BATCH_FLUSH_INTERVAL);
|
||||
if (typeof this.flushTimer === 'object' && 'unref' in this.flushTimer) {
|
||||
this.flushTimer.unref();
|
||||
}
|
||||
process.on('beforeExit', () => this.flush());
|
||||
process.on('SIGINT', () => {
|
||||
this.eventListeners.beforeExit = () => this.flush();
|
||||
this.eventListeners.sigint = () => {
|
||||
this.flush();
|
||||
process.exit(0);
|
||||
});
|
||||
process.on('SIGTERM', () => {
|
||||
};
|
||||
this.eventListeners.sigterm = () => {
|
||||
this.flush();
|
||||
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');
|
||||
}
|
||||
stop() {
|
||||
@@ -60,6 +70,17 @@ class TelemetryBatchProcessor {
|
||||
clearInterval(this.flushTimer);
|
||||
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');
|
||||
}
|
||||
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>;
|
||||
};
|
||||
annotations?: ToolAnnotations;
|
||||
_meta?: {
|
||||
ui?: {
|
||||
resourceUri?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
export interface ResourceDefinition {
|
||||
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
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.32.0",
|
||||
"version": "2.37.2",
|
||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -16,6 +16,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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:optimized": "node dist/scripts/rebuild-optimized.js",
|
||||
"validate": "node dist/scripts/validate.js",
|
||||
@@ -53,6 +55,12 @@
|
||||
"fetch:community": "node dist/scripts/fetch-community-nodes.js",
|
||||
"fetch:community:verified": "node dist/scripts/fetch-community-nodes.js --verified-only",
|
||||
"fetch:community:update": "node dist/scripts/fetch-community-nodes.js --update",
|
||||
"generate:docs": "node dist/scripts/generate-community-docs.js",
|
||||
"generate:docs:readme-only": "node dist/scripts/generate-community-docs.js --readme-only",
|
||||
"generate:docs:summary-only": "node dist/scripts/generate-community-docs.js --summary-only",
|
||||
"generate:docs:incremental": "node dist/scripts/generate-community-docs.js --incremental",
|
||||
"generate:docs:stats": "node dist/scripts/generate-community-docs.js --stats",
|
||||
"migrate:readme-columns": "node dist/scripts/migrate-readme-columns.js",
|
||||
"prebuild:fts5": "npx tsx scripts/prebuild-fts5.ts",
|
||||
"test:templates": "node dist/scripts/test-templates.js",
|
||||
"test:protocol-negotiation": "npx tsx src/scripts/test-protocol-negotiation.ts",
|
||||
@@ -117,6 +125,7 @@
|
||||
"homepage": "https://github.com/czlonkowski/n8n-mcp#readme",
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"ui-apps/dist/**/*",
|
||||
"data/nodes.db",
|
||||
".env.example",
|
||||
"README.md",
|
||||
@@ -143,17 +152,17 @@
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.20.1",
|
||||
"@n8n/n8n-nodes-langchain": "^2.2.2",
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"@n8n/n8n-nodes-langchain": "^2.11.2",
|
||||
"@supabase/supabase-js": "^2.57.4",
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"form-data": "^4.0.5",
|
||||
"lru-cache": "^11.2.1",
|
||||
"n8n": "^2.2.3",
|
||||
"n8n-core": "^2.2.2",
|
||||
"n8n-workflow": "^2.2.2",
|
||||
"n8n": "^2.11.4",
|
||||
"n8n-core": "^2.11.1",
|
||||
"n8n-workflow": "^2.11.1",
|
||||
"openai": "^4.77.0",
|
||||
"sql.js": "^1.13.0",
|
||||
"tslib": "^2.6.2",
|
||||
@@ -166,6 +175,7 @@
|
||||
"better-sqlite3": "^11.10.0"
|
||||
},
|
||||
"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",
|
||||
"version": "2.29.5",
|
||||
"version": "2.33.2",
|
||||
"description": "n8n MCP Server Runtime Dependencies Only",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -105,6 +105,27 @@ export interface NpmSearchResponse {
|
||||
time: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response type for full package data including README
|
||||
*/
|
||||
export interface NpmPackageWithReadme {
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
readme?: string;
|
||||
readmeFilename?: string;
|
||||
homepage?: string;
|
||||
repository?: {
|
||||
type?: string;
|
||||
url?: string;
|
||||
};
|
||||
keywords?: string[];
|
||||
license?: string;
|
||||
'dist-tags'?: {
|
||||
latest?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches community nodes from n8n Strapi API and npm registry.
|
||||
* Follows the pattern from template-fetcher.ts.
|
||||
@@ -390,6 +411,85 @@ export class CommunityNodeFetcher {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch full package data including README from npm registry.
|
||||
* Uses the base package URL (not /latest) to get the README field.
|
||||
* Validates package name to prevent path traversal attacks.
|
||||
*
|
||||
* @param packageName npm package name (e.g., "n8n-nodes-brightdata")
|
||||
* @returns Full package data including readme, or null if fetch failed
|
||||
*/
|
||||
async fetchPackageWithReadme(packageName: string): Promise<NpmPackageWithReadme | null> {
|
||||
// Validate package name to prevent path traversal
|
||||
if (!this.validatePackageName(packageName)) {
|
||||
logger.warn(`Invalid package name rejected for README fetch: ${packageName}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = `${this.npmRegistryUrl}/${encodeURIComponent(packageName)}`;
|
||||
|
||||
return this.retryWithBackoff(
|
||||
async () => {
|
||||
const response = await axios.get<NpmPackageWithReadme>(url, {
|
||||
timeout: FETCH_CONFIG.NPM_REGISTRY_TIMEOUT,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
`Fetching package with README for ${packageName}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch READMEs for multiple packages in batch with rate limiting.
|
||||
* Returns a Map of packageName -> readme content.
|
||||
*
|
||||
* @param packageNames Array of npm package names
|
||||
* @param progressCallback Optional callback for progress updates
|
||||
* @param concurrency Number of concurrent requests (default: 1 for rate limiting)
|
||||
* @returns Map of packageName to README content (null if not found)
|
||||
*/
|
||||
async fetchReadmesBatch(
|
||||
packageNames: string[],
|
||||
progressCallback?: (message: string, current: number, total: number) => void,
|
||||
concurrency: number = 1
|
||||
): Promise<Map<string, string | null>> {
|
||||
const results = new Map<string, string | null>();
|
||||
const total = packageNames.length;
|
||||
|
||||
logger.info(`Fetching READMEs for ${total} packages (concurrency: ${concurrency})...`);
|
||||
|
||||
// Process in batches based on concurrency
|
||||
for (let i = 0; i < packageNames.length; i += concurrency) {
|
||||
const batch = packageNames.slice(i, i + concurrency);
|
||||
|
||||
// Process batch concurrently
|
||||
const batchPromises = batch.map(async (packageName) => {
|
||||
const data = await this.fetchPackageWithReadme(packageName);
|
||||
return { packageName, readme: data?.readme || null };
|
||||
});
|
||||
|
||||
const batchResults = await Promise.all(batchPromises);
|
||||
|
||||
for (const { packageName, readme } of batchResults) {
|
||||
results.set(packageName, readme);
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Fetching READMEs', Math.min(i + concurrency, total), total);
|
||||
}
|
||||
|
||||
// Rate limiting between batches
|
||||
if (i + concurrency < packageNames.length) {
|
||||
await this.sleep(FETCH_CONFIG.RATE_LIMIT_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
const foundCount = Array.from(results.values()).filter((v) => v !== null).length;
|
||||
logger.info(`Fetched ${foundCount}/${total} READMEs successfully`);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get download statistics for a package from npm.
|
||||
* Validates package name to prevent path traversal attacks.
|
||||
|
||||
@@ -355,9 +355,13 @@ export class CommunityNodeService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a readable node name from npm package name.
|
||||
* e.g., "n8n-nodes-globals" -> "Globals"
|
||||
* e.g., "@company/n8n-nodes-mynode" -> "Mynode"
|
||||
* Extract node name from npm package name.
|
||||
* n8n community nodes typically use lowercase node class names.
|
||||
* e.g., "n8n-nodes-chatwoot" -> "chatwoot"
|
||||
* e.g., "@company/n8n-nodes-mynode" -> "mynode"
|
||||
*
|
||||
* Note: We use lowercase because most community nodes follow this convention.
|
||||
* Verified nodes from Strapi have the correct casing in nodeDesc.name.
|
||||
*/
|
||||
private extractNodeNameFromPackage(packageName: string): string {
|
||||
// Remove scope if present
|
||||
@@ -366,11 +370,9 @@ export class CommunityNodeService {
|
||||
// Remove n8n-nodes- prefix
|
||||
name = name.replace(/^n8n-nodes-/, '');
|
||||
|
||||
// Capitalize first letter of each word
|
||||
return name
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('');
|
||||
// Remove hyphens and keep lowercase (n8n community node convention)
|
||||
// e.g., "bright-data" -> "brightdata", "chatwoot" -> "chatwoot"
|
||||
return name.replace(/-/g, '').toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
291
src/community/documentation-batch-processor.ts
Normal file
291
src/community/documentation-batch-processor.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Batch processor for community node documentation generation.
|
||||
*
|
||||
* Orchestrates the full workflow:
|
||||
* 1. Fetch READMEs from npm registry
|
||||
* 2. Generate AI documentation summaries
|
||||
* 3. Store results in database
|
||||
*/
|
||||
|
||||
import { NodeRepository } from '../database/node-repository';
|
||||
import { CommunityNodeFetcher } from './community-node-fetcher';
|
||||
import {
|
||||
DocumentationGenerator,
|
||||
DocumentationInput,
|
||||
DocumentationResult,
|
||||
createDocumentationGenerator,
|
||||
} from './documentation-generator';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
/**
|
||||
* Options for batch processing
|
||||
*/
|
||||
export interface BatchProcessorOptions {
|
||||
/** Skip nodes that already have READMEs (default: false) */
|
||||
skipExistingReadme?: boolean;
|
||||
/** Skip nodes that already have AI summaries (default: false) */
|
||||
skipExistingSummary?: boolean;
|
||||
/** Only fetch READMEs, skip AI generation (default: false) */
|
||||
readmeOnly?: boolean;
|
||||
/** Only generate AI summaries, skip README fetch (default: false) */
|
||||
summaryOnly?: boolean;
|
||||
/** Max nodes to process (default: unlimited) */
|
||||
limit?: number;
|
||||
/** Concurrency for npm README fetches (default: 5) */
|
||||
readmeConcurrency?: number;
|
||||
/** Concurrency for LLM API calls (default: 3) */
|
||||
llmConcurrency?: number;
|
||||
/** Progress callback */
|
||||
progressCallback?: (message: string, current: number, total: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of batch processing
|
||||
*/
|
||||
export interface BatchProcessorResult {
|
||||
/** Number of READMEs fetched */
|
||||
readmesFetched: number;
|
||||
/** Number of READMEs that failed to fetch */
|
||||
readmesFailed: number;
|
||||
/** Number of AI summaries generated */
|
||||
summariesGenerated: number;
|
||||
/** Number of AI summaries that failed */
|
||||
summariesFailed: number;
|
||||
/** Nodes that were skipped (already had data) */
|
||||
skipped: number;
|
||||
/** Total duration in seconds */
|
||||
durationSeconds: number;
|
||||
/** Errors encountered */
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch processor for generating documentation for community nodes
|
||||
*/
|
||||
export class DocumentationBatchProcessor {
|
||||
private repository: NodeRepository;
|
||||
private fetcher: CommunityNodeFetcher;
|
||||
private generator: DocumentationGenerator;
|
||||
|
||||
constructor(
|
||||
repository: NodeRepository,
|
||||
fetcher?: CommunityNodeFetcher,
|
||||
generator?: DocumentationGenerator
|
||||
) {
|
||||
this.repository = repository;
|
||||
this.fetcher = fetcher || new CommunityNodeFetcher();
|
||||
this.generator = generator || createDocumentationGenerator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all community nodes to generate documentation
|
||||
*/
|
||||
async processAll(options: BatchProcessorOptions = {}): Promise<BatchProcessorResult> {
|
||||
const startTime = Date.now();
|
||||
const result: BatchProcessorResult = {
|
||||
readmesFetched: 0,
|
||||
readmesFailed: 0,
|
||||
summariesGenerated: 0,
|
||||
summariesFailed: 0,
|
||||
skipped: 0,
|
||||
durationSeconds: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const {
|
||||
skipExistingReadme = false,
|
||||
skipExistingSummary = false,
|
||||
readmeOnly = false,
|
||||
summaryOnly = false,
|
||||
limit,
|
||||
readmeConcurrency = 5,
|
||||
llmConcurrency = 3,
|
||||
progressCallback,
|
||||
} = options;
|
||||
|
||||
try {
|
||||
// Step 1: Fetch READMEs (unless summaryOnly)
|
||||
if (!summaryOnly) {
|
||||
const readmeResult = await this.fetchReadmes({
|
||||
skipExisting: skipExistingReadme,
|
||||
limit,
|
||||
concurrency: readmeConcurrency,
|
||||
progressCallback,
|
||||
});
|
||||
result.readmesFetched = readmeResult.fetched;
|
||||
result.readmesFailed = readmeResult.failed;
|
||||
result.skipped += readmeResult.skipped;
|
||||
result.errors.push(...readmeResult.errors);
|
||||
}
|
||||
|
||||
// Step 2: Generate AI summaries (unless readmeOnly)
|
||||
if (!readmeOnly) {
|
||||
const summaryResult = await this.generateSummaries({
|
||||
skipExisting: skipExistingSummary,
|
||||
limit,
|
||||
concurrency: llmConcurrency,
|
||||
progressCallback,
|
||||
});
|
||||
result.summariesGenerated = summaryResult.generated;
|
||||
result.summariesFailed = summaryResult.failed;
|
||||
result.skipped += summaryResult.skipped;
|
||||
result.errors.push(...summaryResult.errors);
|
||||
}
|
||||
|
||||
result.durationSeconds = (Date.now() - startTime) / 1000;
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
result.errors.push(`Batch processing failed: ${errorMessage}`);
|
||||
result.durationSeconds = (Date.now() - startTime) / 1000;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch READMEs for community nodes
|
||||
*/
|
||||
private async fetchReadmes(options: {
|
||||
skipExisting?: boolean;
|
||||
limit?: number;
|
||||
concurrency?: number;
|
||||
progressCallback?: (message: string, current: number, total: number) => void;
|
||||
}): Promise<{ fetched: number; failed: number; skipped: number; errors: string[] }> {
|
||||
const { skipExisting = false, limit, concurrency = 5, progressCallback } = options;
|
||||
|
||||
// Get nodes that need READMEs
|
||||
let nodes = skipExisting
|
||||
? this.repository.getCommunityNodesWithoutReadme()
|
||||
: this.repository.getCommunityNodes({ orderBy: 'downloads' });
|
||||
|
||||
if (limit) {
|
||||
nodes = nodes.slice(0, limit);
|
||||
}
|
||||
|
||||
logger.info(`Fetching READMEs for ${nodes.length} community nodes...`);
|
||||
|
||||
if (nodes.length === 0) {
|
||||
return { fetched: 0, failed: 0, skipped: 0, errors: [] };
|
||||
}
|
||||
|
||||
// Get package names
|
||||
const packageNames = nodes
|
||||
.map((n) => n.npmPackageName)
|
||||
.filter((name): name is string => !!name);
|
||||
|
||||
// Fetch READMEs in batches
|
||||
const readmeMap = await this.fetcher.fetchReadmesBatch(
|
||||
packageNames,
|
||||
progressCallback,
|
||||
concurrency
|
||||
);
|
||||
|
||||
// Store READMEs in database
|
||||
let fetched = 0;
|
||||
let failed = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
if (!node.npmPackageName) continue;
|
||||
|
||||
const readme = readmeMap.get(node.npmPackageName);
|
||||
if (readme) {
|
||||
try {
|
||||
this.repository.updateNodeReadme(node.nodeType, readme);
|
||||
fetched++;
|
||||
} catch (error) {
|
||||
const msg = `Failed to save README for ${node.nodeType}: ${error}`;
|
||||
errors.push(msg);
|
||||
failed++;
|
||||
}
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`README fetch complete: ${fetched} fetched, ${failed} failed`);
|
||||
return { fetched, failed, skipped: 0, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate AI documentation summaries
|
||||
*/
|
||||
private async generateSummaries(options: {
|
||||
skipExisting?: boolean;
|
||||
limit?: number;
|
||||
concurrency?: number;
|
||||
progressCallback?: (message: string, current: number, total: number) => void;
|
||||
}): Promise<{ generated: number; failed: number; skipped: number; errors: string[] }> {
|
||||
const { skipExisting = false, limit, concurrency = 3, progressCallback } = options;
|
||||
|
||||
// Get nodes that need summaries (must have READMEs first)
|
||||
let nodes = skipExisting
|
||||
? this.repository.getCommunityNodesWithoutAISummary()
|
||||
: this.repository.getCommunityNodes({ orderBy: 'downloads' }).filter(
|
||||
(n) => n.npmReadme && n.npmReadme.length > 0
|
||||
);
|
||||
|
||||
if (limit) {
|
||||
nodes = nodes.slice(0, limit);
|
||||
}
|
||||
|
||||
logger.info(`Generating AI summaries for ${nodes.length} nodes...`);
|
||||
|
||||
if (nodes.length === 0) {
|
||||
return { generated: 0, failed: 0, skipped: 0, errors: [] };
|
||||
}
|
||||
|
||||
// Test LLM connection first
|
||||
const connectionTest = await this.generator.testConnection();
|
||||
if (!connectionTest.success) {
|
||||
const error = `LLM connection failed: ${connectionTest.message}`;
|
||||
logger.error(error);
|
||||
return { generated: 0, failed: nodes.length, skipped: 0, errors: [error] };
|
||||
}
|
||||
|
||||
logger.info(`LLM connection successful: ${connectionTest.message}`);
|
||||
|
||||
// Prepare inputs for batch generation
|
||||
const inputs: DocumentationInput[] = nodes.map((node) => ({
|
||||
nodeType: node.nodeType,
|
||||
displayName: node.displayName,
|
||||
description: node.description,
|
||||
readme: node.npmReadme || '',
|
||||
npmPackageName: node.npmPackageName,
|
||||
}));
|
||||
|
||||
// Generate summaries in parallel
|
||||
const results = await this.generator.generateBatch(inputs, concurrency, progressCallback);
|
||||
|
||||
// Store summaries in database
|
||||
let generated = 0;
|
||||
let failed = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const result of results) {
|
||||
if (result.error) {
|
||||
errors.push(`${result.nodeType}: ${result.error}`);
|
||||
failed++;
|
||||
} else {
|
||||
try {
|
||||
this.repository.updateNodeAISummary(result.nodeType, result.summary);
|
||||
generated++;
|
||||
} catch (error) {
|
||||
const msg = `Failed to save summary for ${result.nodeType}: ${error}`;
|
||||
errors.push(msg);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`AI summary generation complete: ${generated} generated, ${failed} failed`);
|
||||
return { generated, failed, skipped: 0, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current documentation statistics
|
||||
*/
|
||||
getStats(): ReturnType<NodeRepository['getDocumentationStats']> {
|
||||
return this.repository.getDocumentationStats();
|
||||
}
|
||||
}
|
||||
371
src/community/documentation-generator.ts
Normal file
371
src/community/documentation-generator.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* AI-powered documentation generator for community nodes.
|
||||
*
|
||||
* Uses a local LLM (Qwen or compatible) via OpenAI-compatible API
|
||||
* to generate structured documentation summaries from README content.
|
||||
*/
|
||||
|
||||
import OpenAI from 'openai';
|
||||
import { z } from 'zod';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
/**
|
||||
* Schema for AI-generated documentation summary
|
||||
*/
|
||||
export const DocumentationSummarySchema = z.object({
|
||||
purpose: z.string().describe('What this node does in 1-2 sentences'),
|
||||
capabilities: z.array(z.string()).max(10).describe('Key features and operations'),
|
||||
authentication: z.string().describe('How to authenticate (API key, OAuth, None, etc.)'),
|
||||
commonUseCases: z.array(z.string()).max(5).describe('Practical use case examples'),
|
||||
limitations: z.array(z.string()).max(5).describe('Known limitations or caveats'),
|
||||
relatedNodes: z.array(z.string()).max(5).describe('Related n8n nodes if mentioned'),
|
||||
});
|
||||
|
||||
export type DocumentationSummary = z.infer<typeof DocumentationSummarySchema>;
|
||||
|
||||
/**
|
||||
* Input for documentation generation
|
||||
*/
|
||||
export interface DocumentationInput {
|
||||
nodeType: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
readme: string;
|
||||
npmPackageName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of documentation generation
|
||||
*/
|
||||
export interface DocumentationResult {
|
||||
nodeType: string;
|
||||
summary: DocumentationSummary;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for the documentation generator
|
||||
*/
|
||||
export interface DocumentationGeneratorConfig {
|
||||
/** Base URL for the LLM server (e.g., http://localhost:1234/v1) */
|
||||
baseUrl: string;
|
||||
/** Model name to use (default: qwen3-4b-thinking-2507) */
|
||||
model?: string;
|
||||
/** API key (default: 'not-needed' for local servers) */
|
||||
apiKey?: string;
|
||||
/** Request timeout in ms (default: 60000) */
|
||||
timeout?: number;
|
||||
/** Max tokens for response (default: 2000) */
|
||||
maxTokens?: number;
|
||||
/** Temperature for generation (default: 0.3, set to undefined to omit) */
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default configuration
|
||||
*/
|
||||
const DEFAULT_CONFIG: Required<Omit<DocumentationGeneratorConfig, 'baseUrl' | 'temperature'>> = {
|
||||
model: 'qwen3-4b-thinking-2507',
|
||||
apiKey: 'not-needed',
|
||||
timeout: 60000,
|
||||
maxTokens: 2000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates structured documentation summaries for community nodes
|
||||
* using a local LLM via OpenAI-compatible API.
|
||||
*/
|
||||
export class DocumentationGenerator {
|
||||
private client: OpenAI;
|
||||
private model: string;
|
||||
private maxTokens: number;
|
||||
private timeout: number;
|
||||
private temperature?: number;
|
||||
|
||||
constructor(config: DocumentationGeneratorConfig) {
|
||||
const fullConfig = { ...DEFAULT_CONFIG, ...config };
|
||||
|
||||
this.client = new OpenAI({
|
||||
baseURL: config.baseUrl,
|
||||
apiKey: fullConfig.apiKey,
|
||||
timeout: fullConfig.timeout,
|
||||
});
|
||||
this.model = fullConfig.model;
|
||||
this.maxTokens = fullConfig.maxTokens;
|
||||
this.timeout = fullConfig.timeout;
|
||||
this.temperature = fullConfig.temperature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate documentation summary for a single node
|
||||
*/
|
||||
async generateSummary(input: DocumentationInput): Promise<DocumentationResult> {
|
||||
try {
|
||||
const prompt = this.buildPrompt(input);
|
||||
|
||||
const completion = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
max_completion_tokens: this.maxTokens,
|
||||
...(this.temperature !== undefined ? { temperature: this.temperature } : {}),
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: this.getSystemPrompt(),
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const content = completion.choices[0]?.message?.content;
|
||||
if (!content) {
|
||||
throw new Error('No content in LLM response');
|
||||
}
|
||||
|
||||
// Extract JSON from response (handle markdown code blocks)
|
||||
const jsonContent = this.extractJson(content);
|
||||
const parsed = JSON.parse(jsonContent);
|
||||
|
||||
// Truncate arrays to fit schema limits before validation
|
||||
const truncated = this.truncateArrayFields(parsed);
|
||||
|
||||
// Validate with Zod
|
||||
const validated = DocumentationSummarySchema.parse(truncated);
|
||||
|
||||
return {
|
||||
nodeType: input.nodeType,
|
||||
summary: validated,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error(`Error generating documentation for ${input.nodeType}:`, error);
|
||||
|
||||
return {
|
||||
nodeType: input.nodeType,
|
||||
summary: this.getDefaultSummary(input),
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate documentation for multiple nodes in parallel
|
||||
*
|
||||
* @param inputs Array of documentation inputs
|
||||
* @param concurrency Number of parallel requests (default: 3)
|
||||
* @param progressCallback Optional progress callback
|
||||
* @returns Array of documentation results
|
||||
*/
|
||||
async generateBatch(
|
||||
inputs: DocumentationInput[],
|
||||
concurrency: number = 3,
|
||||
progressCallback?: (message: string, current: number, total: number) => void
|
||||
): Promise<DocumentationResult[]> {
|
||||
const results: DocumentationResult[] = [];
|
||||
const total = inputs.length;
|
||||
|
||||
logger.info(`Generating documentation for ${total} nodes (concurrency: ${concurrency})...`);
|
||||
|
||||
// Process in batches based on concurrency
|
||||
for (let i = 0; i < inputs.length; i += concurrency) {
|
||||
const batch = inputs.slice(i, i + concurrency);
|
||||
|
||||
// Process batch concurrently
|
||||
const batchPromises = batch.map((input) => this.generateSummary(input));
|
||||
const batchResults = await Promise.all(batchPromises);
|
||||
|
||||
results.push(...batchResults);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Generating documentation', Math.min(i + concurrency, total), total);
|
||||
}
|
||||
|
||||
// Small delay between batches to avoid overwhelming the LLM server
|
||||
if (i + concurrency < inputs.length) {
|
||||
await this.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
const successCount = results.filter((r) => !r.error).length;
|
||||
logger.info(`Generated ${successCount}/${total} documentation summaries successfully`);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the prompt for documentation generation
|
||||
*/
|
||||
private buildPrompt(input: DocumentationInput): string {
|
||||
// Truncate README to avoid token limits (keep first ~6000 chars)
|
||||
const truncatedReadme = this.truncateReadme(input.readme, 6000);
|
||||
|
||||
return `
|
||||
Node Information:
|
||||
- Name: ${input.displayName}
|
||||
- Type: ${input.nodeType}
|
||||
- Package: ${input.npmPackageName || 'unknown'}
|
||||
- Description: ${input.description || 'No description provided'}
|
||||
|
||||
README Content:
|
||||
${truncatedReadme}
|
||||
|
||||
Based on the README and node information above, generate a structured documentation summary.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the system prompt for documentation generation
|
||||
*/
|
||||
private getSystemPrompt(): string {
|
||||
return `You are analyzing an n8n community node to generate documentation for AI assistants.
|
||||
|
||||
Your task: Extract key information from the README and create a structured JSON summary.
|
||||
|
||||
Output format (JSON only, no markdown):
|
||||
{
|
||||
"purpose": "What this node does in 1-2 sentences",
|
||||
"capabilities": ["feature1", "feature2", "feature3"],
|
||||
"authentication": "How to authenticate (e.g., 'API key required', 'OAuth2', 'None')",
|
||||
"commonUseCases": ["use case 1", "use case 2"],
|
||||
"limitations": ["limitation 1"] or [] if none mentioned,
|
||||
"relatedNodes": ["related n8n node types"] or [] if none mentioned
|
||||
}
|
||||
|
||||
Guidelines:
|
||||
- Focus on information useful for AI assistants configuring workflows
|
||||
- Be concise but comprehensive
|
||||
- For capabilities, list specific operations/actions supported
|
||||
- For authentication, identify the auth method from README
|
||||
- For limitations, note any mentioned constraints or missing features
|
||||
- Respond with valid JSON only, no additional text`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract JSON from LLM response (handles markdown code blocks)
|
||||
*/
|
||||
private extractJson(content: string): string {
|
||||
// Try to extract from markdown code block
|
||||
const jsonBlockMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||
if (jsonBlockMatch) {
|
||||
return jsonBlockMatch[1].trim();
|
||||
}
|
||||
|
||||
// Try to find JSON object directly
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
return jsonMatch[0];
|
||||
}
|
||||
|
||||
// Return as-is if no extraction needed
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate array fields to fit schema limits
|
||||
* Ensures LLM responses with extra items still validate
|
||||
*/
|
||||
private truncateArrayFields(parsed: Record<string, unknown>): Record<string, unknown> {
|
||||
const limits: Record<string, number> = {
|
||||
capabilities: 10,
|
||||
commonUseCases: 5,
|
||||
limitations: 5,
|
||||
relatedNodes: 5,
|
||||
};
|
||||
|
||||
const result = { ...parsed };
|
||||
|
||||
for (const [field, maxLength] of Object.entries(limits)) {
|
||||
if (Array.isArray(result[field]) && result[field].length > maxLength) {
|
||||
result[field] = (result[field] as unknown[]).slice(0, maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate README to avoid token limits while keeping useful content
|
||||
*/
|
||||
private truncateReadme(readme: string, maxLength: number): string {
|
||||
if (readme.length <= maxLength) {
|
||||
return readme;
|
||||
}
|
||||
|
||||
// Try to truncate at a paragraph boundary
|
||||
const truncated = readme.slice(0, maxLength);
|
||||
const lastParagraph = truncated.lastIndexOf('\n\n');
|
||||
|
||||
if (lastParagraph > maxLength * 0.7) {
|
||||
return truncated.slice(0, lastParagraph) + '\n\n[README truncated...]';
|
||||
}
|
||||
|
||||
return truncated + '\n\n[README truncated...]';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default summary when generation fails
|
||||
*/
|
||||
private getDefaultSummary(input: DocumentationInput): DocumentationSummary {
|
||||
return {
|
||||
purpose: input.description || `Community node: ${input.displayName}`,
|
||||
capabilities: [],
|
||||
authentication: 'See README for authentication details',
|
||||
commonUseCases: [],
|
||||
limitations: ['Documentation could not be automatically generated'],
|
||||
relatedNodes: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connection to the LLM server
|
||||
*/
|
||||
async testConnection(): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const completion = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
max_completion_tokens: 200,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (completion.choices[0]?.message?.content) {
|
||||
return { success: true, message: `Connected to ${this.model}` };
|
||||
}
|
||||
|
||||
return { success: false, message: 'No response from LLM' };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, message: `Connection failed: ${message}` };
|
||||
}
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a documentation generator with environment variable configuration
|
||||
*/
|
||||
export function createDocumentationGenerator(): DocumentationGenerator {
|
||||
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 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({
|
||||
baseUrl,
|
||||
model,
|
||||
timeout,
|
||||
...(apiKey ? { apiKey } : {}),
|
||||
...(isLocalServer ? { temperature: 0.3 } : {}),
|
||||
});
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export {
|
||||
NpmPackageInfo,
|
||||
NpmSearchResult,
|
||||
NpmSearchResponse,
|
||||
NpmPackageWithReadme,
|
||||
} from './community-node-fetcher';
|
||||
|
||||
export {
|
||||
@@ -14,3 +15,19 @@ export {
|
||||
SyncResult,
|
||||
SyncOptions,
|
||||
} from './community-node-service';
|
||||
|
||||
export {
|
||||
DocumentationGenerator,
|
||||
DocumentationGeneratorConfig,
|
||||
DocumentationInput,
|
||||
DocumentationResult,
|
||||
DocumentationSummary,
|
||||
DocumentationSummarySchema,
|
||||
createDocumentationGenerator,
|
||||
} from './documentation-generator';
|
||||
|
||||
export {
|
||||
DocumentationBatchProcessor,
|
||||
BatchProcessorOptions,
|
||||
BatchProcessorResult,
|
||||
} from './documentation-batch-processor';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* These structures define the expected data format, JavaScript 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
|
||||
* @since 2.23.0
|
||||
@@ -15,7 +15,7 @@ import type { NodePropertyTypes } from 'n8n-workflow';
|
||||
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:
|
||||
* - type: Category (primitive/object/collection/special)
|
||||
@@ -620,6 +620,23 @@ export const TYPE_STRUCTURES: Record<NodePropertyTypes, TypeStructure> = {
|
||||
'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
|
||||
*
|
||||
* 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 {
|
||||
private boundParams: any = null;
|
||||
|
||||
private freed: boolean = false;
|
||||
|
||||
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 {
|
||||
try {
|
||||
if (params.length > 0) {
|
||||
@@ -433,10 +457,10 @@ class SQLJSStatement implements PreparedStatement {
|
||||
this.stmt.bind(this.boundParams);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.stmt.run();
|
||||
this.onModify();
|
||||
|
||||
|
||||
// sql.js doesn't provide changes/lastInsertRowid easily
|
||||
return {
|
||||
changes: 1, // Assume success means 1 change
|
||||
@@ -445,9 +469,12 @@ class SQLJSStatement implements PreparedStatement {
|
||||
} catch (error) {
|
||||
this.stmt.reset();
|
||||
throw error;
|
||||
} finally {
|
||||
// Free statement memory after write operation completes
|
||||
this.freeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get(...params: any[]): any {
|
||||
try {
|
||||
if (params.length > 0) {
|
||||
@@ -456,21 +483,24 @@ class SQLJSStatement implements PreparedStatement {
|
||||
this.stmt.bind(this.boundParams);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.stmt.step()) {
|
||||
const result = this.stmt.getAsObject();
|
||||
this.stmt.reset();
|
||||
return this.convertIntegerColumns(result);
|
||||
}
|
||||
|
||||
|
||||
this.stmt.reset();
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
this.stmt.reset();
|
||||
throw error;
|
||||
} finally {
|
||||
// Free statement memory after read operation completes
|
||||
this.freeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
all(...params: any[]): any[] {
|
||||
try {
|
||||
if (params.length > 0) {
|
||||
@@ -479,17 +509,20 @@ class SQLJSStatement implements PreparedStatement {
|
||||
this.stmt.bind(this.boundParams);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const results: any[] = [];
|
||||
while (this.stmt.step()) {
|
||||
results.push(this.convertIntegerColumns(this.stmt.getAsObject()));
|
||||
}
|
||||
|
||||
|
||||
this.stmt.reset();
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.stmt.reset();
|
||||
throw error;
|
||||
} finally {
|
||||
// Free statement memory after read operation completes
|
||||
this.freeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,18 @@ export class NodeRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: case-insensitive lookup for community nodes
|
||||
// Handles cases where node type casing differs (e.g., .Chatwoot vs .chatwoot)
|
||||
if (!row) {
|
||||
const caseInsensitiveRow = this.db.prepare(`
|
||||
SELECT * FROM nodes WHERE LOWER(node_type) = LOWER(?)
|
||||
`).get(nodeType) as any;
|
||||
|
||||
if (caseInsensitiveRow) {
|
||||
return this.parseNodeRow(caseInsensitiveRow);
|
||||
}
|
||||
}
|
||||
|
||||
if (!row) return null;
|
||||
|
||||
return this.parseNodeRow(row);
|
||||
@@ -350,7 +362,13 @@ export class NodeRepository {
|
||||
npmPackageName: row.npm_package_name || null,
|
||||
npmVersion: row.npm_version || null,
|
||||
npmDownloads: row.npm_downloads || 0,
|
||||
communityFetchedAt: row.community_fetched_at || null
|
||||
communityFetchedAt: row.community_fetched_at || null,
|
||||
// AI documentation fields
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -650,6 +668,89 @@ export class NodeRepository {
|
||||
return result.changes;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// AI Documentation Methods
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Update the README content for a node
|
||||
*/
|
||||
updateNodeReadme(nodeType: string, readme: string): void {
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE nodes SET npm_readme = ? WHERE node_type = ?
|
||||
`);
|
||||
stmt.run(readme, nodeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the AI-generated documentation summary for a node
|
||||
*/
|
||||
updateNodeAISummary(nodeType: string, summary: object): void {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get community nodes that are missing README content
|
||||
*/
|
||||
getCommunityNodesWithoutReadme(): any[] {
|
||||
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() as any[];
|
||||
return rows.map(row => this.parseNodeRow(row));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get community nodes that are missing AI documentation summary
|
||||
*/
|
||||
getCommunityNodesWithoutAISummary(): any[] {
|
||||
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() as any[];
|
||||
return rows.map(row => this.parseNodeRow(row));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documentation statistics for community nodes
|
||||
*/
|
||||
getDocumentationStats(): {
|
||||
total: number;
|
||||
withReadme: number;
|
||||
withAISummary: number;
|
||||
needingReadme: number;
|
||||
needingAISummary: number;
|
||||
} {
|
||||
const total = (this.db.prepare(
|
||||
'SELECT COUNT(*) as count FROM nodes WHERE is_community = 1'
|
||||
).get() as any).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() as any).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() as any).count;
|
||||
|
||||
return {
|
||||
total,
|
||||
withReadme,
|
||||
withAISummary,
|
||||
needingReadme: total - withReadme,
|
||||
needingAISummary: withReadme - withAISummary
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* VERSION MANAGEMENT METHODS
|
||||
* Methods for working with node_versions and version_property_changes tables
|
||||
|
||||
@@ -29,6 +29,10 @@ CREATE TABLE IF NOT EXISTS nodes (
|
||||
npm_version TEXT, -- npm package version
|
||||
npm_downloads INTEGER DEFAULT 0, -- Weekly/monthly download count
|
||||
community_fetched_at DATETIME, -- When the community node was last synced
|
||||
-- AI-enhanced documentation fields
|
||||
npm_readme TEXT, -- Raw README markdown from npm registry
|
||||
ai_documentation_summary TEXT, -- AI-generated structured summary (JSON)
|
||||
ai_summary_generated_at DATETIME, -- When the AI summary was generated
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
|
||||
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';
|
||||
import { InstanceContext, validateInstanceContext } from './types/instance-context';
|
||||
import { SessionState } from './types/session-state';
|
||||
import { closeSharedDatabase } from './database/shared-database';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -106,7 +107,12 @@ export class SingleSessionHTTPServer {
|
||||
private session: Session | null = null; // Keep for SSE compatibility
|
||||
private consoleManager = new ConsoleManager();
|
||||
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 cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
@@ -492,6 +498,29 @@ export class SingleSessionHTTPServer {
|
||||
// For initialize requests: always create new transport and server
|
||||
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
|
||||
let sessionIdToUse: string;
|
||||
|
||||
@@ -677,11 +706,25 @@ export class SingleSessionHTTPServer {
|
||||
private async resetSessionSSE(res: express.Response): Promise<void> {
|
||||
// Clean up old session if exists
|
||||
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 {
|
||||
logger.info('Closing previous session for SSE', { sessionId: this.session.sessionId });
|
||||
await this.session.transport.close();
|
||||
} catch (error) {
|
||||
logger.warn('Error closing previous session:', error);
|
||||
} catch (transportError) {
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ export type {
|
||||
SessionState
|
||||
} 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
|
||||
export type {
|
||||
Tool,
|
||||
|
||||
@@ -31,24 +31,44 @@ export class N8nNodeLoader {
|
||||
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[]> {
|
||||
const n8nConfig = packageJson.n8n || {};
|
||||
const nodes: LoadedNode[] = [];
|
||||
|
||||
const packageDir = this.resolvePackageDir(packagePath);
|
||||
|
||||
// Check if nodes is an array or object
|
||||
const nodesList = n8nConfig.nodes || [];
|
||||
|
||||
|
||||
if (Array.isArray(nodesList)) {
|
||||
// Handle array format (n8n-nodes-base uses this)
|
||||
for (const nodePath of nodesList) {
|
||||
try {
|
||||
const fullPath = require.resolve(`${packagePath}/${nodePath}`);
|
||||
const nodeModule = require(fullPath);
|
||||
|
||||
// Resolve absolute path directly to bypass package exports restrictions
|
||||
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")
|
||||
const nodeNameMatch = nodePath.match(/\/([^\/]+)\.node\.(js|ts)$/);
|
||||
const nodeName = nodeNameMatch ? nodeNameMatch[1] : path.basename(nodePath, '.node.js');
|
||||
|
||||
|
||||
// Handle default export and various export patterns
|
||||
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
|
||||
if (NodeClass) {
|
||||
@@ -65,9 +85,9 @@ export class N8nNodeLoader {
|
||||
// Handle object format (for other packages)
|
||||
for (const [nodeName, nodePath] of Object.entries(nodesList)) {
|
||||
try {
|
||||
const fullPath = require.resolve(`${packagePath}/${nodePath as string}`);
|
||||
const nodeModule = require(fullPath);
|
||||
|
||||
const fullPath = path.join(packageDir, nodePath as string);
|
||||
const nodeModule = this.loadNodeModule(fullPath);
|
||||
|
||||
// Handle default export and various export patterns
|
||||
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
|
||||
if (NodeClass) {
|
||||
@@ -81,7 +101,7 @@ export class N8nNodeLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
@@ -424,7 +424,13 @@ const autofixWorkflowSchema = z.object({
|
||||
'node-type-correction',
|
||||
'webhook-missing-path',
|
||||
'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(),
|
||||
confidenceThreshold: z.enum(['high', 'medium', 'low']).optional().default('medium'),
|
||||
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)
|
||||
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
|
||||
telemetry.trackWorkflowCreation(workflow, true);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
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 { getN8nApiClient } from './handlers-n8n-manager';
|
||||
import { N8nApiError, getUserFriendlyErrorMessage } from '../utils/n8n-errors';
|
||||
@@ -48,8 +48,8 @@ const workflowDiffSchema = z.object({
|
||||
target: z.string().optional(),
|
||||
from: z.string().optional(), // For rewireConnection
|
||||
to: z.string().optional(), // For rewireConnection
|
||||
sourceOutput: z.string().optional(),
|
||||
targetInput: z.string().optional(),
|
||||
sourceOutput: z.union([z.string(), z.number()]).transform(String).optional(),
|
||||
targetInput: z.union([z.string(), z.number()]).transform(String).optional(),
|
||||
sourceIndex: z.number().optional(),
|
||||
targetIndex: z.number().optional(),
|
||||
// Smart parameters (Phase 1 UX improvement)
|
||||
@@ -178,11 +178,12 @@ export async function handleUpdatePartialWorkflow(
|
||||
// Complete failure - return error
|
||||
return {
|
||||
success: false,
|
||||
saved: false,
|
||||
error: 'Failed to apply diff operations',
|
||||
operationsApplied: diffResult.operationsApplied,
|
||||
details: {
|
||||
errors: diffResult.errors,
|
||||
warnings: diffResult.warnings,
|
||||
operationsApplied: diffResult.operationsApplied,
|
||||
applied: diffResult.applied,
|
||||
failed: diffResult.failed
|
||||
}
|
||||
@@ -265,6 +266,7 @@ export async function handleUpdatePartialWorkflow(
|
||||
if (!skipValidation) {
|
||||
return {
|
||||
success: false,
|
||||
saved: false,
|
||||
error: errorMessage,
|
||||
details: {
|
||||
errors: structureErrors,
|
||||
@@ -273,7 +275,7 @@ export async function handleUpdatePartialWorkflow(
|
||||
applied: diffResult.applied,
|
||||
recoveryGuidance: recoverySteps,
|
||||
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 {
|
||||
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
|
||||
let finalWorkflow = updatedWorkflow;
|
||||
let activationMessage = '';
|
||||
@@ -319,6 +378,7 @@ export async function handleUpdatePartialWorkflow(
|
||||
logger.error('Failed to activate workflow after update', activationError);
|
||||
return {
|
||||
success: false,
|
||||
saved: true,
|
||||
error: 'Workflow updated successfully but activation failed',
|
||||
details: {
|
||||
workflowUpdated: true,
|
||||
@@ -334,6 +394,7 @@ export async function handleUpdatePartialWorkflow(
|
||||
logger.error('Failed to deactivate workflow after update', deactivationError);
|
||||
return {
|
||||
success: false,
|
||||
saved: true,
|
||||
error: 'Workflow updated successfully but deactivation failed',
|
||||
details: {
|
||||
workflowUpdated: true,
|
||||
@@ -363,6 +424,7 @@ export async function handleUpdatePartialWorkflow(
|
||||
|
||||
return {
|
||||
success: true,
|
||||
saved: true,
|
||||
data: {
|
||||
id: finalWorkflow.id,
|
||||
name: finalWorkflow.name,
|
||||
@@ -375,7 +437,7 @@ export async function handleUpdatePartialWorkflow(
|
||||
applied: diffResult.applied,
|
||||
failed: diffResult.failed,
|
||||
errors: diffResult.errors,
|
||||
warnings: diffResult.warnings
|
||||
warnings: mergeWarnings(diffResult.warnings, tagWarnings)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -413,7 +475,9 @@ export async function handleUpdatePartialWorkflow(
|
||||
return {
|
||||
success: false,
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
InitializeRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { existsSync, promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { n8nDocumentationToolsFinal } from './tools';
|
||||
import { UIAppRegistry } from './ui';
|
||||
import { n8nManagementTools } from './tools-n8n-manager';
|
||||
import { makeToolsN8nFriendly } from './tools-n8n-friendly';
|
||||
import { getWorkflowExampleString } from './workflow-examples';
|
||||
import { logger } from '../utils/logger';
|
||||
import { NodeRepository } from '../database/node-repository';
|
||||
import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter';
|
||||
import { getSharedDatabase, releaseSharedDatabase, SharedDatabaseState } from '../database/shared-database';
|
||||
import { PropertyFilter } from '../services/property-filter';
|
||||
import { TaskTemplates } from '../services/task-templates';
|
||||
import { ConfigValidator } from '../services/config-validator';
|
||||
@@ -60,6 +64,9 @@ interface NodeRow {
|
||||
properties_schema?: string;
|
||||
operations?: string;
|
||||
credentials_required?: string;
|
||||
// AI documentation fields
|
||||
ai_documentation_summary?: string;
|
||||
ai_summary_generated_at?: string;
|
||||
}
|
||||
|
||||
interface VersionSummary {
|
||||
@@ -147,6 +154,9 @@ export class N8NDocumentationMCPServer {
|
||||
private previousToolTimestamp: number = Date.now();
|
||||
private earlyLogger: EarlyErrorLogger | 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) {
|
||||
this.instanceContext = instanceContext;
|
||||
@@ -200,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');
|
||||
|
||||
this.server = new Server(
|
||||
@@ -228,10 +245,12 @@ export class N8NDocumentationMCPServer {
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
UIAppRegistry.load();
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
@@ -242,18 +261,39 @@ export class N8NDocumentationMCPServer {
|
||||
* Order of cleanup:
|
||||
* 1. Close MCP server connection
|
||||
* 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
|
||||
*
|
||||
* 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> {
|
||||
// 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 {
|
||||
await this.server.close();
|
||||
|
||||
// Use destroy() not clear() - also stops the cleanup timer
|
||||
this.cache.destroy();
|
||||
|
||||
// Close database connection before nullifying reference
|
||||
if (this.db) {
|
||||
// Handle database cleanup based on whether it's shared or dedicated
|
||||
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 {
|
||||
this.db.close();
|
||||
} catch (dbError) {
|
||||
@@ -268,6 +308,7 @@ export class N8NDocumentationMCPServer {
|
||||
this.repository = null;
|
||||
this.templateService = null;
|
||||
this.earlyLogger = null;
|
||||
this.sharedDbState = null;
|
||||
} catch (error) {
|
||||
// Log but don't throw - cleanup should be best-effort
|
||||
logger.warn('Error closing MCP server', { error: error instanceof Error ? error.message : String(error) });
|
||||
@@ -283,23 +324,32 @@ export class N8NDocumentationMCPServer {
|
||||
|
||||
logger.debug('Database initialization starting...', { dbPath });
|
||||
|
||||
this.db = await createDatabaseAdapter(dbPath);
|
||||
logger.debug('Database adapter created');
|
||||
|
||||
// If using in-memory database for tests, initialize schema
|
||||
// For in-memory databases (tests), create a dedicated connection
|
||||
// For regular databases, use the shared connection to prevent memory leaks
|
||||
if (dbPath === ':memory:') {
|
||||
this.db = await createDatabaseAdapter(dbPath);
|
||||
logger.debug('Database adapter created (in-memory mode)');
|
||||
await this.initializeInMemorySchema();
|
||||
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');
|
||||
|
||||
this.templateService = new TemplateService(this.db);
|
||||
logger.debug('Template service initialized');
|
||||
|
||||
// Initialize similarity services for enhanced validation
|
||||
EnhancedConfigValidator.initializeSimilarityServices(this.repository);
|
||||
logger.debug('Similarity services initialized');
|
||||
|
||||
// Checkpoint: Database connected (v2.18.3)
|
||||
@@ -525,6 +575,7 @@ export class N8NDocumentationMCPServer {
|
||||
protocolVersion: negotiationResult.version,
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: 'n8n-documentation-mcp',
|
||||
@@ -607,6 +658,7 @@ export class N8NDocumentationMCPServer {
|
||||
});
|
||||
});
|
||||
|
||||
UIAppRegistry.injectToolMeta(tools);
|
||||
return { tools };
|
||||
});
|
||||
|
||||
@@ -642,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
|
||||
// Check if args contains nested 'output' structure from n8n's memory corruption
|
||||
let processedArgs = args;
|
||||
if (args && typeof args === 'object' && 'output' in args) {
|
||||
try {
|
||||
const possibleNestedData = args.output;
|
||||
@@ -675,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 {
|
||||
logger.debug(`Executing tool: ${name}`, { args: processedArgs });
|
||||
const startTime = Date.now();
|
||||
@@ -736,7 +808,7 @@ export class N8NDocumentationMCPServer {
|
||||
if (name.startsWith('validate_') && structuredContent !== null) {
|
||||
mcpResponse.structuredContent = structuredContent;
|
||||
}
|
||||
|
||||
|
||||
return mcpResponse;
|
||||
} catch (error) {
|
||||
logger.error(`Error executing tool ${name}`, error);
|
||||
@@ -763,7 +835,7 @@ export class N8NDocumentationMCPServer {
|
||||
|
||||
// Provide more helpful error messages for common n8n issues
|
||||
let helpfulMessage = `Error executing tool ${name}: ${errorMessage}`;
|
||||
|
||||
|
||||
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.';
|
||||
} else if (errorMessage.includes('type') || errorMessage.includes('expected')) {
|
||||
@@ -771,12 +843,20 @@ export class N8NDocumentationMCPServer {
|
||||
} 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.';
|
||||
}
|
||||
|
||||
|
||||
// For n8n schema errors, add specific guidance
|
||||
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., {})';
|
||||
}
|
||||
|
||||
|
||||
// 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 {
|
||||
content: [
|
||||
{
|
||||
@@ -788,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,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1040,6 +1160,109 @@ export class N8NDocumentationMCPServer {
|
||||
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> {
|
||||
// Ensure args is an object and validate it
|
||||
args = args || {};
|
||||
@@ -2191,31 +2414,34 @@ export class N8NDocumentationMCPServer {
|
||||
// First try with normalized type
|
||||
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
||||
let node = this.db!.prepare(`
|
||||
SELECT node_type, display_name, documentation, description
|
||||
FROM nodes
|
||||
SELECT node_type, display_name, documentation, description,
|
||||
ai_documentation_summary, ai_summary_generated_at
|
||||
FROM nodes
|
||||
WHERE node_type = ?
|
||||
`).get(normalizedType) as NodeRow | undefined;
|
||||
|
||||
|
||||
// If not found and normalization changed the type, try original
|
||||
if (!node && normalizedType !== nodeType) {
|
||||
node = this.db!.prepare(`
|
||||
SELECT node_type, display_name, documentation, description
|
||||
FROM nodes
|
||||
SELECT node_type, display_name, documentation, description,
|
||||
ai_documentation_summary, ai_summary_generated_at
|
||||
FROM nodes
|
||||
WHERE node_type = ?
|
||||
`).get(nodeType) as NodeRow | undefined;
|
||||
}
|
||||
|
||||
|
||||
// If still not found, try alternatives
|
||||
if (!node) {
|
||||
const alternatives = getNodeTypeAlternatives(normalizedType);
|
||||
|
||||
|
||||
for (const alt of alternatives) {
|
||||
node = this.db!.prepare(`
|
||||
SELECT node_type, display_name, documentation, description
|
||||
FROM nodes
|
||||
SELECT node_type, display_name, documentation, description,
|
||||
ai_documentation_summary, ai_summary_generated_at
|
||||
FROM nodes
|
||||
WHERE node_type = ?
|
||||
`).get(alt) as NodeRow | undefined;
|
||||
|
||||
|
||||
if (node) break;
|
||||
}
|
||||
}
|
||||
@@ -2224,6 +2450,11 @@ export class N8NDocumentationMCPServer {
|
||||
throw new Error(`Node ${nodeType} not found`);
|
||||
}
|
||||
|
||||
// Parse AI documentation summary if present
|
||||
const aiDocSummary = node.ai_documentation_summary
|
||||
? this.safeJsonParse(node.ai_documentation_summary, null)
|
||||
: null;
|
||||
|
||||
// If no documentation, generate fallback with null safety
|
||||
if (!node.documentation) {
|
||||
const essentials = await this.getNodeEssentials(nodeType);
|
||||
@@ -2247,7 +2478,9 @@ ${essentials?.commonProperties?.length > 0 ?
|
||||
## Note
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2256,9 +2489,19 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
displayName: node.display_name || 'Unknown Node',
|
||||
documentation: node.documentation,
|
||||
hasDocumentation: true,
|
||||
aiDocumentationSummary: aiDocSummary,
|
||||
aiSummaryGeneratedAt: node.ai_summary_generated_at || null,
|
||||
};
|
||||
}
|
||||
|
||||
private safeJsonParse(json: string, defaultValue: any = null): any {
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private async getDatabaseStatistics(): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
@@ -3887,8 +4130,33 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
}
|
||||
|
||||
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...');
|
||||
|
||||
|
||||
// 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
|
||||
if (this.cache) {
|
||||
try {
|
||||
@@ -3898,15 +4166,31 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
logger.error('Error cleaning up cache:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Close database connection if it exists
|
||||
if (this.db) {
|
||||
|
||||
// Handle database cleanup based on whether it's shared or dedicated
|
||||
// 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 {
|
||||
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');
|
||||
} catch (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;
|
||||
}
|
||||
}
|
||||
@@ -194,12 +194,12 @@ Please choose a different name.
|
||||
- Can rename a node and add/remove connections using the new name in the same batch
|
||||
- Use \`validateOnly: true\` to preview effects before applying
|
||||
|
||||
## Removing Properties with undefined
|
||||
## Removing Properties with null
|
||||
|
||||
To remove a property from a node, set its value to \`undefined\` in the updates object. This is essential when migrating from deprecated properties or cleaning up optional configuration fields.
|
||||
To remove a property from a node, set its value to \`null\` in the updates object. This is essential when migrating from deprecated properties or cleaning up optional configuration fields.
|
||||
|
||||
### Why Use undefined?
|
||||
- **Property removal vs. null**: Setting a property to \`undefined\` removes it completely from the node object, while \`null\` sets the property to a null value
|
||||
### Why Use null?
|
||||
- **Property removal**: Setting a property to \`null\` removes it completely from the node object
|
||||
- **Validation constraints**: Some properties are mutually exclusive (e.g., \`continueOnFail\` and \`onError\`). Simply setting one without removing the other will fail validation
|
||||
- **Deprecated property migration**: When n8n deprecates properties, you must remove the old property before the new one will work
|
||||
|
||||
@@ -211,7 +211,7 @@ n8n_update_partial_workflow({
|
||||
operations: [{
|
||||
type: "updateNode",
|
||||
nodeName: "HTTP Request",
|
||||
updates: { onError: undefined }
|
||||
updates: { onError: null }
|
||||
}]
|
||||
});
|
||||
|
||||
@@ -221,7 +221,7 @@ n8n_update_partial_workflow({
|
||||
operations: [{
|
||||
type: "updateNode",
|
||||
nodeId: "node_abc",
|
||||
updates: { disabled: undefined }
|
||||
updates: { disabled: null }
|
||||
}]
|
||||
});
|
||||
\`\`\`
|
||||
@@ -235,7 +235,7 @@ n8n_update_partial_workflow({
|
||||
operations: [{
|
||||
type: "updateNode",
|
||||
nodeName: "API Request",
|
||||
updates: { "parameters.authentication": undefined }
|
||||
updates: { "parameters.authentication": null }
|
||||
}]
|
||||
});
|
||||
|
||||
@@ -245,7 +245,7 @@ n8n_update_partial_workflow({
|
||||
operations: [{
|
||||
type: "updateNode",
|
||||
nodeName: "HTTP Request",
|
||||
updates: { "parameters.headers": undefined }
|
||||
updates: { "parameters.headers": null }
|
||||
}]
|
||||
});
|
||||
\`\`\`
|
||||
@@ -271,7 +271,7 @@ n8n_update_partial_workflow({
|
||||
type: "updateNode",
|
||||
nodeName: "HTTP Request",
|
||||
updates: {
|
||||
continueOnFail: undefined,
|
||||
continueOnFail: null,
|
||||
onError: "continueErrorOutput"
|
||||
}
|
||||
}]
|
||||
@@ -287,15 +287,15 @@ n8n_update_partial_workflow({
|
||||
type: "updateNode",
|
||||
nodeName: "Data Processor",
|
||||
updates: {
|
||||
continueOnFail: undefined,
|
||||
alwaysOutputData: undefined,
|
||||
"parameters.legacy_option": undefined
|
||||
continueOnFail: null,
|
||||
alwaysOutputData: null,
|
||||
"parameters.legacy_option": null
|
||||
}
|
||||
}]
|
||||
});
|
||||
\`\`\`
|
||||
|
||||
### When to Use undefined
|
||||
### When to Use null
|
||||
- Removing deprecated properties during migration
|
||||
- Cleaning up optional configuration flags
|
||||
- Resolving mutual exclusivity validation errors
|
||||
@@ -341,11 +341,11 @@ n8n_update_partial_workflow({
|
||||
'// Rewire AI Agent to use different language model\nn8n_update_partial_workflow({id: "ai9", operations: [{type: "rewireConnection", source: "AI Agent", from: "OpenAI Chat Model", to: "Anthropic Chat Model", sourceOutput: "ai_languageModel"}]})',
|
||||
'// Replace all AI tools for an agent\nn8n_update_partial_workflow({id: "ai10", operations: [\n {type: "removeConnection", source: "Old Tool 1", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "removeConnection", source: "Old Tool 2", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "addConnection", source: "New HTTP Tool", target: "AI Agent", sourceOutput: "ai_tool"},\n {type: "addConnection", source: "New Code Tool", target: "AI Agent", sourceOutput: "ai_tool"}\n]})',
|
||||
'\n// ============ REMOVING PROPERTIES EXAMPLES ============',
|
||||
'// Remove a simple property\nn8n_update_partial_workflow({id: "rm1", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {onError: undefined}}]})',
|
||||
'// Migrate from deprecated continueOnFail to onError\nn8n_update_partial_workflow({id: "rm2", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {continueOnFail: undefined, onError: "continueErrorOutput"}}]})',
|
||||
'// Remove nested property\nn8n_update_partial_workflow({id: "rm3", operations: [{type: "updateNode", nodeName: "API Request", updates: {"parameters.authentication": undefined}}]})',
|
||||
'// Remove multiple properties\nn8n_update_partial_workflow({id: "rm4", operations: [{type: "updateNode", nodeName: "Data Processor", updates: {continueOnFail: undefined, alwaysOutputData: undefined, "parameters.legacy_option": undefined}}]})',
|
||||
'// Remove entire array property\nn8n_update_partial_workflow({id: "rm5", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {"parameters.headers": undefined}}]})'
|
||||
'// Remove a simple property\nn8n_update_partial_workflow({id: "rm1", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {onError: null}}]})',
|
||||
'// Migrate from deprecated continueOnFail to onError\nn8n_update_partial_workflow({id: "rm2", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {continueOnFail: null, onError: "continueErrorOutput"}}]})',
|
||||
'// Remove nested property\nn8n_update_partial_workflow({id: "rm3", operations: [{type: "updateNode", nodeName: "API Request", updates: {"parameters.authentication": null}}]})',
|
||||
'// Remove multiple properties\nn8n_update_partial_workflow({id: "rm4", operations: [{type: "updateNode", nodeName: "Data Processor", updates: {continueOnFail: null, alwaysOutputData: null, "parameters.legacy_option": null}}]})',
|
||||
'// Remove entire array property\nn8n_update_partial_workflow({id: "rm5", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {"parameters.headers": null}}]})'
|
||||
],
|
||||
useCases: [
|
||||
'Rewire connections when replacing nodes',
|
||||
@@ -383,9 +383,9 @@ n8n_update_partial_workflow({
|
||||
'Use targetIndex for fallback models (primary=0, fallback=1)',
|
||||
'Batch AI component connections in a single operation for atomicity',
|
||||
'Validate AI workflows after connection changes to catch configuration errors',
|
||||
'To remove properties, set them to undefined (not null) in the updates object',
|
||||
'To remove properties, set them to null in the updates object',
|
||||
'When migrating from deprecated properties, remove the old property and add the new one in the same operation',
|
||||
'Use undefined to resolve mutual exclusivity validation errors between properties',
|
||||
'Use null to resolve mutual exclusivity validation errors between properties',
|
||||
'Batch multiple property removals in a single updateNode operation for efficiency'
|
||||
],
|
||||
pitfalls: [
|
||||
@@ -407,8 +407,8 @@ n8n_update_partial_workflow({
|
||||
'**Auto-sanitization runs on ALL nodes**: When ANY update is made, ALL nodes in the workflow are sanitized (not just modified ones)',
|
||||
'**Auto-sanitization cannot fix everything**: It fixes operator structures and missing metadata, but cannot fix broken connections or branch mismatches',
|
||||
'**Corrupted workflows beyond repair**: Workflows in paradoxical states (API returns corrupt, API rejects updates) cannot be fixed via API - must be recreated',
|
||||
'Setting a property to null does NOT remove it - use undefined instead',
|
||||
'When properties are mutually exclusive (e.g., continueOnFail and onError), setting only the new property will fail - you must remove the old one with undefined',
|
||||
'To remove a property, set it to null in the updates object',
|
||||
'When properties are mutually exclusive (e.g., continueOnFail and onError), setting only the new property will fail - you must remove the old one with null',
|
||||
'Removing a required property may cause validation errors - check node documentation first',
|
||||
'Nested property removal with dot notation only removes the specific nested field, not the entire parent object',
|
||||
'Array index notation (e.g., "parameters.headers[0]") is not supported - remove the entire array property instead'
|
||||
|
||||
@@ -284,7 +284,7 @@ export const n8nManagementTools: ToolDefinition[] = [
|
||||
},
|
||||
{
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -301,7 +301,7 @@ export const n8nManagementTools: ToolDefinition[] = [
|
||||
description: 'Types of fixes to apply (default: all)',
|
||||
items: {
|
||||
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: {
|
||||
|
||||
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;
|
||||
}
|
||||
225
src/scripts/generate-community-docs.ts
Normal file
225
src/scripts/generate-community-docs.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CLI script for generating AI-powered documentation for community nodes.
|
||||
*
|
||||
* Usage:
|
||||
* npm run generate:docs # Full generation (README + AI summary)
|
||||
* npm run generate:docs:readme-only # Only fetch READMEs
|
||||
* npm run generate:docs:summary-only # Only generate AI summaries
|
||||
* npm run generate:docs:incremental # Skip nodes with existing data
|
||||
*
|
||||
* Environment variables:
|
||||
* 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_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_DB_PATH - Database path (default: ./data/nodes.db)
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { createDatabaseAdapter } from '../database/database-adapter';
|
||||
import { NodeRepository } from '../database/node-repository';
|
||||
import { CommunityNodeFetcher } from '../community/community-node-fetcher';
|
||||
import {
|
||||
DocumentationBatchProcessor,
|
||||
BatchProcessorOptions,
|
||||
} from '../community/documentation-batch-processor';
|
||||
import { createDocumentationGenerator } from '../community/documentation-generator';
|
||||
|
||||
// Parse command line arguments
|
||||
function parseArgs(): BatchProcessorOptions & { help?: boolean; stats?: boolean } {
|
||||
const args = process.argv.slice(2);
|
||||
const options: BatchProcessorOptions & { help?: boolean; stats?: boolean } = {};
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
options.help = true;
|
||||
} else if (arg === '--readme-only') {
|
||||
options.readmeOnly = true;
|
||||
} else if (arg === '--summary-only') {
|
||||
options.summaryOnly = true;
|
||||
} else if (arg === '--incremental' || arg === '-i') {
|
||||
options.skipExistingReadme = true;
|
||||
options.skipExistingSummary = true;
|
||||
} else if (arg === '--skip-existing-readme') {
|
||||
options.skipExistingReadme = true;
|
||||
} else if (arg === '--skip-existing-summary') {
|
||||
options.skipExistingSummary = true;
|
||||
} else if (arg === '--stats') {
|
||||
options.stats = true;
|
||||
} else if (arg.startsWith('--limit=')) {
|
||||
options.limit = parseInt(arg.split('=')[1], 10);
|
||||
} else if (arg.startsWith('--readme-concurrency=')) {
|
||||
options.readmeConcurrency = parseInt(arg.split('=')[1], 10);
|
||||
} else if (arg.startsWith('--llm-concurrency=')) {
|
||||
options.llmConcurrency = parseInt(arg.split('=')[1], 10);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function printHelp(): void {
|
||||
console.log(`
|
||||
============================================================
|
||||
n8n-mcp Community Node Documentation Generator
|
||||
============================================================
|
||||
|
||||
Usage: npm run generate:docs [options]
|
||||
|
||||
Options:
|
||||
--help, -h Show this help message
|
||||
--readme-only Only fetch READMEs from npm (skip AI generation)
|
||||
--summary-only Only generate AI summaries (requires existing READMEs)
|
||||
--incremental, -i Skip nodes that already have data
|
||||
--skip-existing-readme Skip nodes with existing READMEs
|
||||
--skip-existing-summary Skip nodes with existing AI summaries
|
||||
--stats Show documentation statistics only
|
||||
--limit=N Process only N nodes (for testing)
|
||||
--readme-concurrency=N Parallel npm requests (default: 5)
|
||||
--llm-concurrency=N Parallel LLM requests (default: 3)
|
||||
|
||||
Environment Variables:
|
||||
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_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_DB_PATH Database path (default: ./data/nodes.db)
|
||||
|
||||
Examples:
|
||||
npm run generate:docs # Full generation
|
||||
npm run generate:docs -- --readme-only # Only fetch READMEs
|
||||
npm run generate:docs -- --incremental # Skip existing data
|
||||
npm run generate:docs -- --limit=10 # Process 10 nodes (testing)
|
||||
npm run generate:docs -- --stats # Show current statistics
|
||||
`);
|
||||
}
|
||||
|
||||
function createProgressBar(current: number, total: number, width: number = 50): string {
|
||||
const percentage = total > 0 ? current / total : 0;
|
||||
const filled = Math.round(width * percentage);
|
||||
const empty = width - filled;
|
||||
const bar = '='.repeat(filled) + ' '.repeat(empty);
|
||||
const pct = Math.round(percentage * 100);
|
||||
return `[${bar}] ${pct}% - ${current}/${total}`;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const options = parseArgs();
|
||||
|
||||
if (options.help) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('============================================================');
|
||||
console.log(' n8n-mcp Community Node Documentation Generator');
|
||||
console.log('============================================================\n');
|
||||
|
||||
// Initialize database
|
||||
const dbPath = process.env.N8N_MCP_DB_PATH || path.join(process.cwd(), 'data', 'nodes.db');
|
||||
console.log(`Database: ${dbPath}`);
|
||||
|
||||
const db = await createDatabaseAdapter(dbPath);
|
||||
const repository = new NodeRepository(db);
|
||||
const fetcher = new CommunityNodeFetcher();
|
||||
const generator = createDocumentationGenerator();
|
||||
|
||||
const processor = new DocumentationBatchProcessor(repository, fetcher, generator);
|
||||
|
||||
// Show current stats
|
||||
const stats = processor.getStats();
|
||||
console.log('\nCurrent Documentation Statistics:');
|
||||
console.log(` Total community nodes: ${stats.total}`);
|
||||
console.log(` With README: ${stats.withReadme} (${stats.needingReadme} need fetching)`);
|
||||
console.log(` With AI summary: ${stats.withAISummary} (${stats.needingAISummary} need generation)`);
|
||||
|
||||
if (options.stats) {
|
||||
console.log('\n============================================================');
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Show configuration
|
||||
console.log('\nConfiguration:');
|
||||
console.log(` LLM Base URL: ${process.env.N8N_MCP_LLM_BASE_URL || 'http://localhost:1234/v1'}`);
|
||||
console.log(` LLM Model: ${process.env.N8N_MCP_LLM_MODEL || 'qwen3-4b-thinking-2507'}`);
|
||||
console.log(` README concurrency: ${options.readmeConcurrency || 5}`);
|
||||
console.log(` LLM concurrency: ${options.llmConcurrency || 3}`);
|
||||
if (options.limit) console.log(` Limit: ${options.limit} nodes`);
|
||||
if (options.readmeOnly) console.log(` Mode: README only`);
|
||||
if (options.summaryOnly) console.log(` Mode: Summary only`);
|
||||
if (options.skipExistingReadme || options.skipExistingSummary) console.log(` Mode: Incremental`);
|
||||
|
||||
console.log('\n------------------------------------------------------------');
|
||||
console.log('Processing...\n');
|
||||
|
||||
// Add progress callback
|
||||
let lastMessage = '';
|
||||
options.progressCallback = (message: string, current: number, total: number) => {
|
||||
const bar = createProgressBar(current, total);
|
||||
const fullMessage = `${bar} - ${message}`;
|
||||
if (fullMessage !== lastMessage) {
|
||||
process.stdout.write(`\r${fullMessage}`);
|
||||
lastMessage = fullMessage;
|
||||
}
|
||||
};
|
||||
|
||||
// Run processing
|
||||
const result = await processor.processAll(options);
|
||||
|
||||
// Clear progress line
|
||||
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
||||
|
||||
// Show results
|
||||
console.log('\n============================================================');
|
||||
console.log(' Results');
|
||||
console.log('============================================================');
|
||||
|
||||
if (!options.summaryOnly) {
|
||||
console.log(`\nREADME Fetching:`);
|
||||
console.log(` Fetched: ${result.readmesFetched}`);
|
||||
console.log(` Failed: ${result.readmesFailed}`);
|
||||
}
|
||||
|
||||
if (!options.readmeOnly) {
|
||||
console.log(`\nAI Summary Generation:`);
|
||||
console.log(` Generated: ${result.summariesGenerated}`);
|
||||
console.log(` Failed: ${result.summariesFailed}`);
|
||||
}
|
||||
|
||||
console.log(`\nSkipped: ${result.skipped}`);
|
||||
console.log(`Duration: ${result.durationSeconds.toFixed(1)}s`);
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
console.log(`\nErrors (${result.errors.length}):`);
|
||||
// Show first 10 errors
|
||||
for (const error of result.errors.slice(0, 10)) {
|
||||
console.log(` - ${error}`);
|
||||
}
|
||||
if (result.errors.length > 10) {
|
||||
console.log(` ... and ${result.errors.length - 10} more`);
|
||||
}
|
||||
}
|
||||
|
||||
// Show final stats
|
||||
const finalStats = processor.getStats();
|
||||
console.log('\nFinal Documentation Statistics:');
|
||||
console.log(` With README: ${finalStats.withReadme}/${finalStats.total}`);
|
||||
console.log(` With AI summary: ${finalStats.withAISummary}/${finalStats.total}`);
|
||||
|
||||
console.log('\n============================================================\n');
|
||||
|
||||
db.close();
|
||||
|
||||
// Exit with error code if there were failures
|
||||
if (result.readmesFailed > 0 || result.summariesFailed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run main
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user