mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-16 23:43:07 +00:00
Compare commits
15 Commits
feature/ui
...
v2.37.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7a1cfe8bf | ||
|
|
65ab94deb2 | ||
|
|
599bc664d0 | ||
|
|
9590f751d2 | ||
|
|
248f859c49 | ||
|
|
25b8a8145d | ||
|
|
0918cd5425 | ||
|
|
0998e5486e | ||
|
|
87f26eef18 | ||
|
|
4bad880f44 | ||
|
|
77048347b3 | ||
|
|
6f695be482 | ||
|
|
34159f4ece | ||
|
|
8217229e2f | ||
|
|
89146186d8 |
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 ""
|
||||
|
||||
177
CHANGELOG.md
177
CHANGELOG.md
@@ -7,6 +7,183 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.37.3] - 2026-03-15
|
||||
|
||||
### Fixed
|
||||
|
||||
- **updateNode `name`/`id` field normalization**: LLMs sending `{type: "updateNode", name: "Code", ...}` instead of `nodeName` no longer get "Node not found" errors. The Zod schema now normalizes `name` → `nodeName` and `id` → `nodeId` for node-targeting operations (updateNode, removeNode, moveNode, enableNode, disableNode)
|
||||
- **AI connection types in disconnected-node detection** (Issue #581): Replaced hardcoded 7-type list with dynamic iteration over all connection types present in workflow data. Nodes connected via `ai_outputParser`, `ai_document`, `ai_textSplitter`, `ai_agent`, `ai_chain`, `ai_retriever` are no longer falsely flagged as disconnected during save
|
||||
- **Connection schema and reference validation** (Issue #581): Added `.catchall()` to `workflowConnectionSchema` for unknown AI connection types, and extended connection reference validation to check all connection types (not just `main`)
|
||||
- **autofix `filterOperationsByFixes` ID-vs-name mismatch**: Typeversion-upgrade operations now include `nodeName` alongside `nodeId`, and the filter checks both fields. Previously, `applyFixes=true` silently dropped all typeversion fixes because `fixedNodes` contained names but the filter only checked `nodeId` (UUID)
|
||||
|
||||
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.37.2] - 2026-03-15
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Code validator `$()` false positive** (Issue #294): `$('Previous Node').first().json` no longer triggers "Invalid $ usage detected" warning. Added `(` and `_` to the regex negative lookahead to support standard n8n cross-node references and valid JS identifiers like `$_var`
|
||||
- **Code validator helper function return false positive** (Issue #293): `function isValid(item) { return false; }` no longer triggers "Cannot return primitive values directly" error. Added helper function detection to skip primitive return checks when named functions or arrow function assignments are present
|
||||
- **Null property removal in diff engine** (Issue #611): `{continueOnFail: null}` no longer causes Zod validation error "Expected boolean, received null". The diff engine now treats `null` values as property deletion (`delete` operator), and documentation updated from `undefined` to `null` for property removal
|
||||
|
||||
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.37.1] - 2026-03-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Numeric sourceOutput remapping** (Issue #537): `addConnection` with numeric `sourceOutput` values like `"0"` or `"1"` now correctly maps to `"main"` with the corresponding `sourceIndex`, preventing malformed connection keys
|
||||
- **IMAP Email Trigger activation** (Issue #538): `n8n-nodes-base.emailReadImap` and other IMAP-based polling triggers are now recognized as activatable triggers, allowing workflow activation
|
||||
- **AI tool description false positives** (Issue #477): Validators now check `description` and `options.description` in addition to `toolDescription`, fixing false `MISSING_TOOL_DESCRIPTION` errors for toolWorkflow, toolCode, and toolSerpApi nodes
|
||||
- **n8n_create_workflow undefined ID** (Issue #602): Added defensive check for missing workflow ID in API response with actionable error message
|
||||
- **Flaky CI performance test**: Relaxed bulk insert ratio threshold from 15 to 20 to accommodate CI runner variability
|
||||
|
||||
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.37.0] - 2026-03-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Unary operator sanitization** (Issue #592): Added missing `empty`, `notEmpty`, `exists`, `notExists` operators to the sanitizer's unary operator list, preventing IF/Switch node corruption during partial updates
|
||||
- **Positional connection array preservation** (Issue #610): `removeNode` and `cleanStaleConnections` now trim only trailing empty arrays, preserving intermediate positional indices for IF/Switch multi-output nodes
|
||||
- **Scoped sanitization**: Auto-sanitization now only runs on nodes that were actually added or updated, preventing unrelated nodes (e.g., HTTP Request parameters) from being silently modified
|
||||
- **Activate/deactivate 415 errors** (Issue #633): Added empty body `{}` to POST calls for workflow activation/deactivation endpoints
|
||||
- **Zod error readability** (Issue #630): Validation errors now return human-readable `"path: message"` strings instead of raw Zod error objects
|
||||
- **updateNode error hints** (Issue #623): Improved error message when `updates` parameter is missing, showing correct structure with `nodeId`/`nodeName` and `updates` fields
|
||||
- **removeConnection after removeNode** (Issue #624): When a node was already removed by a prior `removeNode` operation, the error message now explains that connections were automatically cleaned up
|
||||
- **Connection type coercion** (Issue #629): `sourceOutput` and `targetInput` are now coerced to strings, handling numeric values (0, 1) passed by MCP clients
|
||||
|
||||
### Added
|
||||
|
||||
- **`saved` field in responses** (Issue #625): All `n8n_update_partial_workflow` responses now include `saved: true/false` to distinguish whether the workflow was persisted to n8n
|
||||
- **Tag operations via dedicated API** (Issue #599): `addTag`/`removeTag` now use the n8n tag API (`PUT /workflows/{id}/tags`) instead of embedding tags in the workflow body, fixing silent tag failures. Includes automatic tag creation, case-insensitive name resolution, and last-operation-wins reconciliation for conflicting add/remove
|
||||
- **`updateWorkflowTags` API client method**: New method on `N8nApiClient` for managing workflow tag associations via the dedicated endpoint
|
||||
- **`operationsApplied` in top-level response**: Promoted from nested `details` to top-level for easier consumption by MCP clients
|
||||
|
||||
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.36.2] - 2026-03-14
|
||||
|
||||
### Changed
|
||||
|
||||
- **Updated n8n dependencies**: n8n 2.10.3 → 2.11.4, n8n-core 2.10.1 → 2.11.1, n8n-workflow 2.10.1 → 2.11.1, @n8n/n8n-nodes-langchain 2.10.1 → 2.11.2
|
||||
- **Updated @modelcontextprotocol/sdk**: 1.20.1 → 1.27.1 (fixes critical cross-client data leak vulnerability CVE GHSA-345p-7cg4-v4c7)
|
||||
- Rebuilt node database with 1,239 nodes (809 core + 430 community preserved)
|
||||
- Updated README badge with new n8n version and node counts
|
||||
|
||||
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.36.1] - 2026-03-08
|
||||
|
||||
### Added
|
||||
|
||||
- **Conditional branch fan-out detection** (`CONDITIONAL_BRANCH_FANOUT`): Warns when IF, Filter, or Switch nodes have all connections crammed into `main[0]` with higher-index outputs empty, which usually means all target nodes execute together on one branch while other branches have no effect
|
||||
- Detects IF nodes with both true/false targets on `main[0]`
|
||||
- Detects Filter nodes with both matched/unmatched targets on `main[0]`
|
||||
- Detects Switch nodes with all targets on output 0 and other outputs unused
|
||||
- Skips warning when fan-out is legitimate (higher outputs also have connections)
|
||||
- Skips warning for single connections (intentional true-only/matched-only usage)
|
||||
|
||||
### Changed
|
||||
|
||||
- **Refactored output index validation**: Extracted `getShortNodeType()` and `getConditionalOutputInfo()` helpers to eliminate duplicated conditional node detection logic between `validateOutputIndexBounds` and the new `validateConditionalBranchUsage`
|
||||
|
||||
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.36.0] - 2026-03-07
|
||||
|
||||
### Added
|
||||
|
||||
- **Connection validation: detect broken/malformed workflow connections** (Issue #620):
|
||||
- Unknown output keys (`UNKNOWN_CONNECTION_KEY`): Flags invalid connection keys like `"0"`, `"1"`, `"output"` with fix suggestions (e.g., "use main[1] instead" for numeric keys)
|
||||
- Invalid type field (`INVALID_CONNECTION_TYPE`): Detects invalid `type` values in connection targets (e.g., `"0"` instead of `"main"`)
|
||||
- Output index bounds checking (`OUTPUT_INDEX_OUT_OF_BOUNDS`): Catches connections using output indices beyond what a node supports, with awareness of `onError: 'continueErrorOutput'`, Switch rules, and IF/Filter nodes
|
||||
- Input index bounds checking (`INPUT_INDEX_OUT_OF_BOUNDS`): Validates target input indices against known node input counts (Merge=2, triggers=0, others=1)
|
||||
- BFS-based trigger reachability analysis: Replaces simple orphan detection with proper graph traversal from trigger nodes, flagging unreachable subgraphs
|
||||
- Flexible `WorkflowConnection` interface: Changed from explicit `main?/error?/ai_tool?` to `[outputType: string]` for accurate validation of all connection types
|
||||
|
||||
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.35.6] - 2026-03-04
|
||||
|
||||
### Changed
|
||||
|
||||
- **Updated n8n dependencies**: n8n 2.8.3 → 2.10.3, n8n-core 2.8.1 → 2.10.1, n8n-workflow 2.8.0 → 2.10.1, @n8n/n8n-nodes-langchain 2.8.1 → 2.10.1
|
||||
- Rebuilt node database with 806 core nodes (community nodes preserved from previous build)
|
||||
|
||||
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.35.5] - 2026-02-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Comprehensive parameter type coercion for Claude Desktop / Claude.ai** (Issue #605): Expanded the v2.35.4 fix to handle ALL type mismatches, not just stringified objects/arrays. Testing revealed 6/9 tools still failing in Claude Desktop after the initial fix.
|
||||
- Extended `coerceStringifiedJsonParams()` to coerce every schema type: `string→number`, `string→boolean`, `number→string`, `boolean→string` (in addition to existing `string→object` and `string→array`)
|
||||
- Added top-level safeguard to parse the entire `args` object if it arrives as a JSON string
|
||||
- Added `[Diagnostic]` section to error responses showing received argument types, enabling users to report exactly what their MCP client sends
|
||||
- Added 9 new unit tests (24 total) covering number, boolean, and number-to-string coercion
|
||||
|
||||
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.35.4] - 2026-02-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Defensive JSON.parse for stringified object/array parameters** (Issue #605): Claude Desktop 1.1.3189 serializes JSON object/array MCP parameters as strings, causing ZodError failures for ~60% of tools that accept nested parameters
|
||||
- Added schema-driven `coerceStringifiedJsonParams()` in the central `CallToolRequestSchema` handler
|
||||
- Automatically detects string values where the tool's `inputSchema` expects `object` or `array`, and parses them back
|
||||
- Safe: prefix check before parsing, type verification after, try/catch preserves original on failure
|
||||
- No-op for correct clients: native objects pass through unchanged
|
||||
- Affects 9 tools with object/array params: `validate_node`, `validate_workflow`, `n8n_create_workflow`, `n8n_update_full_workflow`, `n8n_update_partial_workflow`, `n8n_validate_workflow`, `n8n_autofix_workflow`, `n8n_test_workflow`, `n8n_executions`
|
||||
- Added 15 unit tests covering coercion, no-op, safety, and end-to-end scenarios
|
||||
|
||||
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.35.3] - 2026-02-19
|
||||
|
||||
### Changed
|
||||
|
||||
- **Updated n8n dependencies**: n8n 2.6.3 → 2.8.3, n8n-core 2.6.1 → 2.8.1, n8n-workflow 2.6.0 → 2.8.0, @n8n/n8n-nodes-langchain 2.6.2 → 2.8.1
|
||||
- **Fixed node loader for langchain package**: Adapted node loader to bypass restricted package.json `exports` field in @n8n/n8n-nodes-langchain >=2.9.0, resolving node files via absolute paths instead of `require.resolve()`
|
||||
- **Fixed community doc generation for cloud LLMs**: Added `N8N_MCP_LLM_API_KEY`/`OPENAI_API_KEY` env var support, switched to `max_completion_tokens`, and auto-omit `temperature` for cloud API endpoints
|
||||
- Rebuilt node database with 1,236 nodes (673 from n8n-nodes-base, 133 from @n8n/n8n-nodes-langchain, 430 community)
|
||||
- Refreshed community nodes (361 verified + 69 npm) with 424/430 AI documentation summaries
|
||||
|
||||
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.35.2] - 2026-02-09
|
||||
|
||||
### Changed
|
||||
|
||||
- **MCP Apps: Disable non-rendering apps in Claude.ai**: Disabled 3 MCP Apps (workflow-list, execution-history, health-dashboard) that render as collapsed accordions in Claude.ai, and removed `n8n_deploy_template` tool mapping which renders blank content. The server sets `_meta` correctly on the wire but the Claude.ai host ignores it for these tools. The 2 working apps (operation-result for 6 tools, validation-summary for 3 tools) remain active. Disabled apps can be re-enabled once the host-side issue is resolved.
|
||||
|
||||
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.35.1] - 2026-02-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- **MCP Apps: Fix UI not rendering for some tools in Claude**: Added legacy flat `_meta["ui/resourceUri"]` key alongside the nested `_meta.ui.resourceUri` in tool definitions. Claude.ai reads the flat key format; without it, tools like `n8n_health_check` and `n8n_list_workflows` showed as collapsed accordions instead of rendering their rich UI apps. Both key formats are now set by `injectToolMeta()`, matching the behavior of the official `registerAppTool` helper from `@modelcontextprotocol/ext-apps/server`.
|
||||
|
||||
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.35.0] - 2026-02-09
|
||||
|
||||
### Added
|
||||
|
||||
- **3 new MCP Apps**: workflow-list (compact table with status/tags), execution-history (status summary bar + execution table), health-dashboard (connection status, versions, performance metrics)
|
||||
- **Enhanced operation-result**: operation-aware headers (create/update/delete/test/deploy), detail panels with workflow metadata, copy-to-clipboard for IDs/URLs, autofix diff viewer
|
||||
- **CopyButton shared component**: reusable clipboard button with visual feedback
|
||||
- **Local preview harness** (`ui-apps/preview.html`): test all 5 apps with mock data, dark/light theme toggle, JSON-RPC protocol simulation
|
||||
- **Expanded shared types**: TypeScript types for workflow-list, execution-history, and health-dashboard data
|
||||
|
||||
### Fixed
|
||||
|
||||
- **React hooks violation**: Fixed `useMemo` called after early returns in `execution-history/App.tsx` and `validation-summary/App.tsx`, causing React error #310 ("Rendered more hooks than during the previous render") and blank iframes
|
||||
- **JSON-RPC catch-all handler**: Preview harness responds to unknown SDK requests to prevent hangs
|
||||
|
||||
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
## [2.34.5] - 2026-02-08
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -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/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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"}
|
||||
17
dist/mcp/handlers-n8n-manager.js
vendored
17
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)
|
||||
@@ -282,6 +288,15 @@ async function handleCreateWorkflow(args, context) {
|
||||
};
|
||||
}
|
||||
const workflow = await client.createWorkflow(input);
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
telemetry_1.telemetry.trackWorkflowCreation(workflow, true);
|
||||
return {
|
||||
success: true,
|
||||
|
||||
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
2
dist/mcp/handlers-workflow-diff.d.ts.map
vendored
2
dist/mcp/handlers-workflow-diff.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"handlers-workflow-diff.d.ts","sourceRoot":"","sources":["../../src/mcp/handlers-workflow-diff.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAMnD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AA0D7D,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,eAAe,CAAC,CA6V1B"}
|
||||
{"version":3,"file":"handlers-workflow-diff.d.ts","sourceRoot":"","sources":["../../src/mcp/handlers-workflow-diff.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAMnD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAgF7D,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,eAAe,CAAC,CA6Z1B"}
|
||||
94
dist/mcp/handlers-workflow-diff.js
vendored
94
dist/mcp/handlers-workflow-diff.js
vendored
@@ -50,6 +50,9 @@ function getValidator(repository) {
|
||||
}
|
||||
return cachedValidator;
|
||||
}
|
||||
const NODE_TARGETING_OPERATIONS = new Set([
|
||||
'updateNode', 'removeNode', 'moveNode', 'enableNode', 'disableNode'
|
||||
]);
|
||||
const workflowDiffSchema = zod_1.z.object({
|
||||
id: zod_1.z.string(),
|
||||
operations: zod_1.z.array(zod_1.z.object({
|
||||
@@ -64,8 +67,8 @@ const workflowDiffSchema = zod_1.z.object({
|
||||
target: zod_1.z.string().optional(),
|
||||
from: zod_1.z.string().optional(),
|
||||
to: zod_1.z.string().optional(),
|
||||
sourceOutput: zod_1.z.string().optional(),
|
||||
targetInput: zod_1.z.string().optional(),
|
||||
sourceOutput: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).transform(String).optional(),
|
||||
targetInput: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).transform(String).optional(),
|
||||
sourceIndex: zod_1.z.number().optional(),
|
||||
targetIndex: zod_1.z.number().optional(),
|
||||
branch: zod_1.z.enum(['true', 'false']).optional(),
|
||||
@@ -76,6 +79,19 @@ const workflowDiffSchema = zod_1.z.object({
|
||||
settings: zod_1.z.any().optional(),
|
||||
name: zod_1.z.string().optional(),
|
||||
tag: zod_1.z.string().optional(),
|
||||
id: zod_1.z.string().optional(),
|
||||
}).transform((op) => {
|
||||
if (NODE_TARGETING_OPERATIONS.has(op.type)) {
|
||||
if (!op.nodeName && !op.nodeId && op.name) {
|
||||
op.nodeName = op.name;
|
||||
op.name = undefined;
|
||||
}
|
||||
if (!op.nodeId && op.id) {
|
||||
op.nodeId = op.id;
|
||||
op.id = undefined;
|
||||
}
|
||||
}
|
||||
return op;
|
||||
})),
|
||||
validateOnly: zod_1.z.boolean().optional(),
|
||||
continueOnError: zod_1.z.boolean().optional(),
|
||||
@@ -167,11 +183,12 @@ async function handleUpdatePartialWorkflow(args, repository, context) {
|
||||
else {
|
||||
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
|
||||
}
|
||||
@@ -239,6 +256,7 @@ async function handleUpdatePartialWorkflow(args, repository, context) {
|
||||
if (!skipValidation) {
|
||||
return {
|
||||
success: false,
|
||||
saved: false,
|
||||
error: errorMessage,
|
||||
details: {
|
||||
errors: structureErrors,
|
||||
@@ -247,7 +265,7 @@ async function handleUpdatePartialWorkflow(args, repository, context) {
|
||||
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.'
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -259,6 +277,58 @@ async function handleUpdatePartialWorkflow(args, repository, context) {
|
||||
}
|
||||
try {
|
||||
const updatedWorkflow = await client.updateWorkflow(input.id, diffResult.workflow);
|
||||
let tagWarnings = [];
|
||||
if (diffResult.tagsToAdd?.length || diffResult.tagsToRemove?.length) {
|
||||
try {
|
||||
const existingTags = Array.isArray(updatedWorkflow.tags)
|
||||
? updatedWorkflow.tags.map((t) => typeof t === 'object' ? { id: t.id, name: t.name } : { id: '', name: t })
|
||||
: [];
|
||||
const allTags = await client.listTags();
|
||||
const tagMap = new Map();
|
||||
for (const t of allTags.data) {
|
||||
if (t.id)
|
||||
tagMap.set(t.name.toLowerCase(), t.id);
|
||||
}
|
||||
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'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const currentTagIds = new Set();
|
||||
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);
|
||||
}
|
||||
await client.updateWorkflowTags(input.id, Array.from(currentTagIds));
|
||||
}
|
||||
catch (tagError) {
|
||||
tagWarnings.push(`Tag update failed: ${tagError instanceof Error ? tagError.message : 'Unknown error'}`);
|
||||
logger_1.logger.warn('Tag operations failed (non-blocking)', tagError);
|
||||
}
|
||||
}
|
||||
let finalWorkflow = updatedWorkflow;
|
||||
let activationMessage = '';
|
||||
try {
|
||||
@@ -286,6 +356,7 @@ async function handleUpdatePartialWorkflow(args, repository, context) {
|
||||
logger_1.logger.error('Failed to activate workflow after update', activationError);
|
||||
return {
|
||||
success: false,
|
||||
saved: true,
|
||||
error: 'Workflow updated successfully but activation failed',
|
||||
details: {
|
||||
workflowUpdated: true,
|
||||
@@ -303,6 +374,7 @@ async function handleUpdatePartialWorkflow(args, repository, context) {
|
||||
logger_1.logger.error('Failed to deactivate workflow after update', deactivationError);
|
||||
return {
|
||||
success: false,
|
||||
saved: true,
|
||||
error: 'Workflow updated successfully but deactivation failed',
|
||||
details: {
|
||||
workflowUpdated: true,
|
||||
@@ -329,6 +401,7 @@ async function handleUpdatePartialWorkflow(args, repository, context) {
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
saved: true,
|
||||
data: {
|
||||
id: finalWorkflow.id,
|
||||
name: finalWorkflow.name,
|
||||
@@ -341,7 +414,7 @@ async function handleUpdatePartialWorkflow(args, repository, context) {
|
||||
applied: diffResult.applied,
|
||||
failed: diffResult.failed,
|
||||
errors: diffResult.errors,
|
||||
warnings: diffResult.warnings
|
||||
warnings: mergeWarnings(diffResult.warnings, tagWarnings)
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -379,7 +452,9 @@ async function handleUpdatePartialWorkflow(args, repository, context) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid input',
|
||||
details: { errors: error.errors }
|
||||
details: {
|
||||
errors: error.errors.map(e => `${e.path.join('.')}: ${e.message}`)
|
||||
}
|
||||
};
|
||||
}
|
||||
logger_1.logger.error('Failed to update partial workflow', error);
|
||||
@@ -389,6 +464,13 @@ async function handleUpdatePartialWorkflow(args, repository, context) {
|
||||
};
|
||||
}
|
||||
}
|
||||
function mergeWarnings(diffWarnings, tagWarnings) {
|
||||
const merged = [
|
||||
...(diffWarnings || []),
|
||||
...tagWarnings.map(w => ({ operation: -1, message: w }))
|
||||
];
|
||||
return merged.length > 0 ? merged : undefined;
|
||||
}
|
||||
function inferIntentFromOperations(operations) {
|
||||
if (!operations || operations.length === 0) {
|
||||
return 'Partial workflow update';
|
||||
|
||||
2
dist/mcp/handlers-workflow-diff.js.map
vendored
2
dist/mcp/handlers-workflow-diff.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/mcp/server.d.ts
vendored
1
dist/mcp/server.d.ts
vendored
@@ -30,6 +30,7 @@ export declare class N8NDocumentationMCPServer {
|
||||
private validateToolParams;
|
||||
private validateToolParamsBasic;
|
||||
private validateExtractedArgs;
|
||||
private coerceStringifiedJsonParams;
|
||||
private listNodes;
|
||||
private getNodeInfo;
|
||||
private searchNodes;
|
||||
|
||||
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":"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;IAmWrB,OAAO,CAAC,wBAAwB;IAoFhC,OAAO,CAAC,kBAAkB;IAqE1B,OAAO,CAAC,uBAAuB;IAwB/B,OAAO,CAAC,qBAAqB;YAoTf,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"}
|
||||
{"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;IA8GvE,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"}
|
||||
115
dist/mcp/server.js
vendored
115
dist/mcp/server.js
vendored
@@ -124,6 +124,7 @@ class N8NDocumentationMCPServer {
|
||||
this.earlyLogger.logCheckpoint(startup_checkpoints_1.STARTUP_CHECKPOINTS.N8N_API_READY);
|
||||
}
|
||||
});
|
||||
this.initialized.catch(() => { });
|
||||
logger_1.logger.info('Initializing n8n Documentation MCP server');
|
||||
this.server = new index_js_1.Server({
|
||||
name: 'n8n-documentation-mcp',
|
||||
@@ -427,6 +428,7 @@ class N8NDocumentationMCPServer {
|
||||
description: tool.description
|
||||
});
|
||||
});
|
||||
ui_1.UIAppRegistry.injectToolMeta(tools);
|
||||
return { tools };
|
||||
});
|
||||
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
||||
@@ -456,6 +458,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;
|
||||
@@ -484,6 +498,7 @@ class N8NDocumentationMCPServer {
|
||||
});
|
||||
}
|
||||
}
|
||||
processedArgs = this.coerceStringifiedJsonParams(name, processedArgs);
|
||||
try {
|
||||
logger_1.logger.debug(`Executing tool: ${name}`, { args: processedArgs });
|
||||
const startTime = Date.now();
|
||||
@@ -529,10 +544,6 @@ class N8NDocumentationMCPServer {
|
||||
if (name.startsWith('validate_') && structuredContent !== null) {
|
||||
mcpResponse.structuredContent = structuredContent;
|
||||
}
|
||||
const uiApp = ui_1.UIAppRegistry.getAppForTool(name);
|
||||
if (uiApp && uiApp.html) {
|
||||
mcpResponse._meta = { ui: { app: uiApp.config.uri } };
|
||||
}
|
||||
return mcpResponse;
|
||||
}
|
||||
catch (error) {
|
||||
@@ -559,6 +570,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: [
|
||||
{
|
||||
@@ -585,7 +603,7 @@ class N8NDocumentationMCPServer {
|
||||
});
|
||||
this.server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
|
||||
const uri = request.params.uri;
|
||||
const match = uri.match(/^n8n-mcp:\/\/ui\/(.+)$/);
|
||||
const match = uri.match(/^ui:\/\/n8n-mcp\/(.+)$/);
|
||||
if (!match) {
|
||||
throw new Error(`Unknown resource URI: ${uri}`);
|
||||
}
|
||||
@@ -798,6 +816,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();
|
||||
|
||||
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
@@ -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
4
dist/services/ai-tool-validators.d.ts
vendored
4
dist/services/ai-tool-validators.d.ts
vendored
@@ -33,8 +33,8 @@ export declare function validateVectorStoreTool(node: WorkflowNode, reverseConne
|
||||
export declare function validateWorkflowTool(node: WorkflowNode, reverseConnections?: Map<string, ReverseConnection[]>): ValidationIssue[];
|
||||
export declare function validateAIAgentTool(node: WorkflowNode, reverseConnections: Map<string, ReverseConnection[]>): ValidationIssue[];
|
||||
export declare function validateMCPClientTool(node: WorkflowNode): ValidationIssue[];
|
||||
export declare function validateCalculatorTool(node: WorkflowNode): ValidationIssue[];
|
||||
export declare function validateThinkTool(node: WorkflowNode): ValidationIssue[];
|
||||
export declare function validateCalculatorTool(_node: WorkflowNode): ValidationIssue[];
|
||||
export declare function validateThinkTool(_node: WorkflowNode): ValidationIssue[];
|
||||
export declare function validateSerpApiTool(node: WorkflowNode): ValidationIssue[];
|
||||
export declare function validateWikipediaTool(node: WorkflowNode): ValidationIssue[];
|
||||
export declare function validateSearXngTool(node: WorkflowNode): ValidationIssue[];
|
||||
|
||||
2
dist/services/ai-tool-validators.d.ts.map
vendored
2
dist/services/ai-tool-validators.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"ai-tool-validators.d.ts","sourceRoot":"","sources":["../../src/services/ai-tool-validators.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,UAAU,EAAE,GAAG,CAAC;IAChB,WAAW,CAAC,EAAE,GAAG,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAuJ7E;AAMD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAoCtE;AAMD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,YAAY,EAClB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,EACpD,QAAQ,EAAE,YAAY,GACrB,eAAe,EAAE,CAmCnB;AAMD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,EAAE,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,GAAG,eAAe,EAAE,CA0BjI;AAMD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,YAAY,EAClB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,GACnD,eAAe,EAAE,CAmCnB;AAMD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CA0B3E;AAMD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAM5E;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAMvE;AAMD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAyBzE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CA4B3E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CA0BzE;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAyB9E;AAKD,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;CAarB,CAAC;AAKX,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGzD;AAKD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,MAAM,EAChB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,EACpD,QAAQ,EAAE,YAAY,GACrB,eAAe,EAAE,CAgCnB"}
|
||||
{"version":3,"file":"ai-tool-validators.d.ts","sourceRoot":"","sources":["../../src/services/ai-tool-validators.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,UAAU,EAAE,GAAG,CAAC;IAChB,WAAW,CAAC,EAAE,GAAG,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAiBD,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAuJ7E;AAMD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAoCtE;AAMD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,YAAY,EAClB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,EACpD,QAAQ,EAAE,YAAY,GACrB,eAAe,EAAE,CAmCnB;AAMD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,EAAE,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,GAAG,eAAe,EAAE,CA0BjI;AAMD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,YAAY,EAClB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,GACnD,eAAe,EAAE,CAmCnB;AAMD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CA0B3E;AAMD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,YAAY,GAAG,eAAe,EAAE,CAG7E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,eAAe,EAAE,CAGxE;AAMD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAyBzE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CA4B3E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CA0BzE;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,YAAY,GAAG,eAAe,EAAE,CAyB9E;AAKD,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;CAarB,CAAC;AAKX,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGzD;AAKD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,MAAM,EAChB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,EACpD,QAAQ,EAAE,YAAY,GACrB,eAAe,EAAE,CAgCnB"}
|
||||
37
dist/services/ai-tool-validators.js
vendored
37
dist/services/ai-tool-validators.js
vendored
@@ -21,9 +21,14 @@ const MIN_DESCRIPTION_LENGTH_MEDIUM = 15;
|
||||
const MIN_DESCRIPTION_LENGTH_LONG = 20;
|
||||
const MAX_ITERATIONS_WARNING_THRESHOLD = 50;
|
||||
const MAX_TOPK_WARNING_THRESHOLD = 20;
|
||||
function getToolDescription(node) {
|
||||
return (node.parameters.toolDescription ||
|
||||
node.parameters.description ||
|
||||
node.parameters.options?.description);
|
||||
}
|
||||
function validateHTTPRequestTool(node) {
|
||||
const issues = [];
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -32,7 +37,7 @@ function validateHTTPRequestTool(node) {
|
||||
code: 'MISSING_TOOL_DESCRIPTION'
|
||||
});
|
||||
}
|
||||
else if (node.parameters.toolDescription.trim().length < MIN_DESCRIPTION_LENGTH_MEDIUM) {
|
||||
else if (getToolDescription(node).trim().length < MIN_DESCRIPTION_LENGTH_MEDIUM) {
|
||||
issues.push({
|
||||
severity: 'warning',
|
||||
nodeId: node.id,
|
||||
@@ -154,7 +159,7 @@ function validateHTTPRequestTool(node) {
|
||||
}
|
||||
function validateCodeTool(node) {
|
||||
const issues = [];
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -184,7 +189,7 @@ function validateCodeTool(node) {
|
||||
}
|
||||
function validateVectorStoreTool(node, reverseConnections, workflow) {
|
||||
const issues = [];
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -216,7 +221,7 @@ function validateVectorStoreTool(node, reverseConnections, workflow) {
|
||||
}
|
||||
function validateWorkflowTool(node, reverseConnections) {
|
||||
const issues = [];
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -238,7 +243,7 @@ function validateWorkflowTool(node, reverseConnections) {
|
||||
}
|
||||
function validateAIAgentTool(node, reverseConnections) {
|
||||
const issues = [];
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -270,7 +275,7 @@ function validateAIAgentTool(node, reverseConnections) {
|
||||
}
|
||||
function validateMCPClientTool(node) {
|
||||
const issues = [];
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -290,17 +295,15 @@ function validateMCPClientTool(node) {
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
function validateCalculatorTool(node) {
|
||||
const issues = [];
|
||||
return issues;
|
||||
function validateCalculatorTool(_node) {
|
||||
return [];
|
||||
}
|
||||
function validateThinkTool(node) {
|
||||
const issues = [];
|
||||
return issues;
|
||||
function validateThinkTool(_node) {
|
||||
return [];
|
||||
}
|
||||
function validateSerpApiTool(node) {
|
||||
const issues = [];
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -321,7 +324,7 @@ function validateSerpApiTool(node) {
|
||||
}
|
||||
function validateWikipediaTool(node) {
|
||||
const issues = [];
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -345,7 +348,7 @@ function validateWikipediaTool(node) {
|
||||
}
|
||||
function validateSearXngTool(node) {
|
||||
const issues = [];
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -376,7 +379,7 @@ function validateWolframAlphaTool(node) {
|
||||
code: 'MISSING_CREDENTIALS'
|
||||
});
|
||||
}
|
||||
if (!node.parameters.description && !node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'info',
|
||||
nodeId: node.id,
|
||||
|
||||
2
dist/services/ai-tool-validators.js.map
vendored
2
dist/services/ai-tool-validators.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/services/n8n-api-client.d.ts
vendored
1
dist/services/n8n-api-client.d.ts
vendored
@@ -36,6 +36,7 @@ export declare class N8nApiClient {
|
||||
createTag(tag: Partial<Tag>): Promise<Tag>;
|
||||
updateTag(id: string, tag: Partial<Tag>): Promise<Tag>;
|
||||
deleteTag(id: string): Promise<void>;
|
||||
updateWorkflowTags(workflowId: string, tagIds: string[]): Promise<Tag[]>;
|
||||
getSourceControlStatus(): Promise<SourceControlStatus>;
|
||||
pullSourceControl(force?: boolean): Promise<SourceControlPullResult>;
|
||||
pushSourceControl(message: string, fileNames?: string[]): Promise<SourceControlPushResult>;
|
||||
|
||||
2
dist/services/n8n-api-client.d.ts.map
vendored
2
dist/services/n8n-api-client.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"n8n-api-client.d.ts","sourceRoot":"","sources":["../../src/services/n8n-api-client.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"n8n-api-client.d.ts","sourceRoot":"","sources":["../../src/services/n8n-api-client.ts"],"names":[],"mappings":"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"}
|
||||
13
dist/services/n8n-api-client.js
vendored
13
dist/services/n8n-api-client.js
vendored
@@ -196,7 +196,7 @@ class N8nApiClient {
|
||||
}
|
||||
async activateWorkflow(id) {
|
||||
try {
|
||||
const response = await this.client.post(`/workflows/${id}/activate`);
|
||||
const response = await this.client.post(`/workflows/${id}/activate`, {});
|
||||
return response.data;
|
||||
}
|
||||
catch (error) {
|
||||
@@ -205,7 +205,7 @@ class N8nApiClient {
|
||||
}
|
||||
async deactivateWorkflow(id) {
|
||||
try {
|
||||
const response = await this.client.post(`/workflows/${id}/deactivate`);
|
||||
const response = await this.client.post(`/workflows/${id}/deactivate`, {});
|
||||
return response.data;
|
||||
}
|
||||
catch (error) {
|
||||
@@ -365,6 +365,15 @@ class N8nApiClient {
|
||||
throw (0, n8n_errors_1.handleN8nApiError)(error);
|
||||
}
|
||||
}
|
||||
async updateWorkflowTags(workflowId, tagIds) {
|
||||
try {
|
||||
const response = await this.client.put(`/workflows/${workflowId}/tags`, tagIds.filter(id => id).map(id => ({ id })));
|
||||
return response.data;
|
||||
}
|
||||
catch (error) {
|
||||
throw (0, n8n_errors_1.handleN8nApiError)(error);
|
||||
}
|
||||
}
|
||||
async getSourceControlStatus() {
|
||||
try {
|
||||
const response = await this.client.get('/source-control/status');
|
||||
|
||||
2
dist/services/n8n-api-client.js.map
vendored
2
dist/services/n8n-api-client.js.map
vendored
File diff suppressed because one or more lines are too long
248
dist/services/n8n-validation.d.ts
vendored
248
dist/services/n8n-validation.d.ts
vendored
@@ -144,79 +144,227 @@ export declare const workflowConnectionSchema: z.ZodRecord<z.ZodString, z.ZodObj
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">>;
|
||||
}, "strip", z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
error?: {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
main?: {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_tool?: {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_languageModel?: {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_memory?: {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_embedding?: {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_vectorStore?: {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
error?: {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">, z.objectOutputType<{
|
||||
main: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
main?: {
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_tool?: {
|
||||
}>, "many">, "many">>;
|
||||
error: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_languageModel?: {
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_memory?: {
|
||||
}>, "many">, "many">>;
|
||||
ai_tool: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_embedding?: {
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
ai_vectorStore?: {
|
||||
}>, "many">, "many">>;
|
||||
ai_languageModel: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}[][] | undefined;
|
||||
}>>;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">>;
|
||||
ai_memory: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">>;
|
||||
ai_embedding: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">>;
|
||||
ai_vectorStore: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">>;
|
||||
}, z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">, "strip">, z.objectInputType<{
|
||||
main: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">>;
|
||||
error: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">>;
|
||||
ai_tool: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">>;
|
||||
ai_languageModel: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">>;
|
||||
ai_memory: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">>;
|
||||
ai_embedding: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">>;
|
||||
ai_vectorStore: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">>;
|
||||
}, z.ZodArray<z.ZodArray<z.ZodObject<{
|
||||
node: z.ZodString;
|
||||
type: z.ZodString;
|
||||
index: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}, {
|
||||
type: string;
|
||||
node: string;
|
||||
index: number;
|
||||
}>, "many">, "many">, "strip">>>;
|
||||
export declare const workflowSettingsSchema: z.ZodObject<{
|
||||
executionOrder: z.ZodDefault<z.ZodEnum<["v0", "v1"]>>;
|
||||
timezone: z.ZodOptional<z.ZodString>;
|
||||
|
||||
2
dist/services/n8n-validation.d.ts.map
vendored
2
dist/services/n8n-validation.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"n8n-validation.d.ts","sourceRoot":"","sources":["../../src/services/n8n-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAM9E,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiB7B,CAAC;AAkBH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAUpC,CAAC;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWjC,CAAC;AAGH,eAAO,MAAM,uBAAuB;;;;;;CAMnC,CAAC;AAGF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,OAAO,GAAG,kBAAkB,CAEpF;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAElG;AAGD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsBrF;AAiBD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoE5E;AAGD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,CA6P/E;AAGD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAK7D;AAMD,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CA+F5E;AAMD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA0D/E;AAGD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAmB/D;AAGD,wBAAgB,2BAA2B,IAAI,MAAM,CA6CpD;AAGD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAmBpE"}
|
||||
{"version":3,"file":"n8n-validation.d.ts","sourceRoot":"","sources":["../../src/services/n8n-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAM9E,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiB7B,CAAC;AAkBH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAUpC,CAAC;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWjC,CAAC;AAGH,eAAO,MAAM,uBAAuB;;;;;;CAMnC,CAAC;AAGF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,OAAO,GAAG,kBAAkB,CAEpF;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAElG;AAGD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsBrF;AAiBD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoE5E;AAGD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,CAkQ/E;AAGD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAK7D;AAMD,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CA+F5E;AAMD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA0D/E;AAGD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAmB/D;AAGD,wBAAgB,2BAA2B,IAAI,MAAM,CA6CpD;AAGD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAmBpE"}
|
||||
44
dist/services/n8n-validation.js
vendored
44
dist/services/n8n-validation.js
vendored
@@ -47,7 +47,7 @@ exports.workflowConnectionSchema = zod_1.z.record(zod_1.z.object({
|
||||
ai_memory: connectionArraySchema.optional(),
|
||||
ai_embedding: connectionArraySchema.optional(),
|
||||
ai_vectorStore: connectionArraySchema.optional(),
|
||||
}));
|
||||
}).catchall(connectionArraySchema));
|
||||
exports.workflowSettingsSchema = zod_1.z.object({
|
||||
executionOrder: zod_1.z.enum(['v0', 'v1']).default('v1'),
|
||||
timezone: zod_1.z.string().optional(),
|
||||
@@ -152,11 +152,10 @@ function validateWorkflowStructure(workflow) {
|
||||
}
|
||||
else if (connectionCount > 0 || executableNodes.length > 1) {
|
||||
const connectedNodes = new Set();
|
||||
const ALL_CONNECTION_TYPES = ['main', 'error', 'ai_tool', 'ai_languageModel', 'ai_memory', 'ai_embedding', 'ai_vectorStore'];
|
||||
Object.entries(workflow.connections).forEach(([sourceName, connection]) => {
|
||||
connectedNodes.add(sourceName);
|
||||
ALL_CONNECTION_TYPES.forEach(connType => {
|
||||
const connData = connection[connType];
|
||||
const connectionRecord = connection;
|
||||
Object.values(connectionRecord).forEach((connData) => {
|
||||
if (connData && Array.isArray(connData)) {
|
||||
connData.forEach((outputs) => {
|
||||
if (Array.isArray(outputs)) {
|
||||
@@ -282,23 +281,28 @@ function validateWorkflowStructure(workflow) {
|
||||
errors.push(`Connection references non-existent node: ${sourceName}`);
|
||||
}
|
||||
}
|
||||
if (connection.main && Array.isArray(connection.main)) {
|
||||
connection.main.forEach((outputs, outputIndex) => {
|
||||
if (Array.isArray(outputs)) {
|
||||
outputs.forEach((target, targetIndex) => {
|
||||
if (!nodeNames.has(target.node)) {
|
||||
if (nodeIds.has(target.node)) {
|
||||
const correctName = nodeIdToName.get(target.node);
|
||||
errors.push(`Connection target uses node ID '${target.node}' but must use node name '${correctName}' (from ${sourceName}[${outputIndex}][${targetIndex}])`);
|
||||
const connectionRecord = connection;
|
||||
Object.values(connectionRecord).forEach((connData) => {
|
||||
if (connData && Array.isArray(connData)) {
|
||||
connData.forEach((outputs, outputIndex) => {
|
||||
if (Array.isArray(outputs)) {
|
||||
outputs.forEach((target, targetIndex) => {
|
||||
if (!target?.node)
|
||||
return;
|
||||
if (!nodeNames.has(target.node)) {
|
||||
if (nodeIds.has(target.node)) {
|
||||
const correctName = nodeIdToName.get(target.node);
|
||||
errors.push(`Connection target uses node ID '${target.node}' but must use node name '${correctName}' (from ${sourceName}[${outputIndex}][${targetIndex}])`);
|
||||
}
|
||||
else {
|
||||
errors.push(`Connection references non-existent target node: ${target.node} (from ${sourceName}[${outputIndex}][${targetIndex}])`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
errors.push(`Connection references non-existent target node: ${target.node} (from ${sourceName}[${outputIndex}][${targetIndex}])`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return errors;
|
||||
|
||||
2
dist/services/n8n-validation.js.map
vendored
2
dist/services/n8n-validation.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/services/node-sanitizer.d.ts.map
vendored
2
dist/services/node-sanitizer.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"node-sanitizer.d.ts","sourceRoot":"","sources":["../../src/services/node-sanitizer.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAKhD,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAa7D;AAKD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,GAAG,GAAG,GAAG,CASxD;AAoND,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CAgEjE"}
|
||||
{"version":3,"file":"node-sanitizer.d.ts","sourceRoot":"","sources":["../../src/services/node-sanitizer.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAKhD,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAa7D;AAKD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,GAAG,GAAG,GAAG,CASxD;AA6ND,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CAgEjE"}
|
||||
14
dist/services/node-sanitizer.js
vendored
14
dist/services/node-sanitizer.js
vendored
@@ -17,7 +17,7 @@ function sanitizeWorkflowNodes(workflow) {
|
||||
}
|
||||
return {
|
||||
...workflow,
|
||||
nodes: workflow.nodes.map((node) => sanitizeNode(node))
|
||||
nodes: workflow.nodes.map(sanitizeNode)
|
||||
};
|
||||
}
|
||||
function isFilterBasedNode(nodeType, typeVersion) {
|
||||
@@ -66,7 +66,7 @@ function sanitizeFilterConditions(conditions) {
|
||||
...sanitized.options
|
||||
};
|
||||
if (sanitized.conditions && Array.isArray(sanitized.conditions)) {
|
||||
sanitized.conditions = sanitized.conditions.map((condition) => sanitizeCondition(condition));
|
||||
sanitized.conditions = sanitized.conditions.map(sanitizeCondition);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
@@ -124,6 +124,10 @@ function inferDataType(operation) {
|
||||
if (dateOps.some(op => operation.includes(op))) {
|
||||
return 'dateTime';
|
||||
}
|
||||
const objectOps = ['empty', 'notEmpty', 'exists', 'notExists'];
|
||||
if (objectOps.includes(operation)) {
|
||||
return 'object';
|
||||
}
|
||||
return 'string';
|
||||
}
|
||||
function isUnaryOperator(operation) {
|
||||
@@ -132,7 +136,11 @@ function isUnaryOperator(operation) {
|
||||
'isNotEmpty',
|
||||
'true',
|
||||
'false',
|
||||
'isNumeric'
|
||||
'isNumeric',
|
||||
'empty',
|
||||
'notEmpty',
|
||||
'exists',
|
||||
'notExists'
|
||||
];
|
||||
return unaryOps.includes(operation);
|
||||
}
|
||||
|
||||
2
dist/services/node-sanitizer.js.map
vendored
2
dist/services/node-sanitizer.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -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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"}
|
||||
313
dist/services/workflow-auto-fixer.js
vendored
313
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,9 +402,13 @@ 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 || '');
|
||||
return fixedNodes.has(op.nodeName || '') || 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.');
|
||||
@@ -488,6 +794,7 @@ class WorkflowAutoFixer {
|
||||
const operation = {
|
||||
type: 'updateNode',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
updates: {
|
||||
typeVersion: parseFloat(latestVersion),
|
||||
parameters: migrationResult.updatedNode.parameters,
|
||||
|
||||
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
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"}
|
||||
2
dist/types/n8n-api.d.ts
vendored
2
dist/types/n8n-api.d.ts
vendored
@@ -259,6 +259,7 @@ export interface WebhookRequest {
|
||||
}
|
||||
export interface McpToolResponse {
|
||||
success: boolean;
|
||||
saved?: boolean;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
message?: string;
|
||||
@@ -266,6 +267,7 @@ export interface McpToolResponse {
|
||||
details?: Record<string, unknown>;
|
||||
executionId?: string;
|
||||
workflowId?: string;
|
||||
operationsApplied?: number;
|
||||
}
|
||||
export type ExecutionMode = 'preview' | 'summary' | 'filtered' | 'full' | 'error';
|
||||
export interface ExecutionPreview {
|
||||
|
||||
2
dist/types/n8n-api.d.ts.map
vendored
2
dist/types/n8n-api.d.ts.map
vendored
File diff suppressed because one or more lines are too long
2
dist/types/workflow-diff.d.ts
vendored
2
dist/types/workflow-diff.d.ts
vendored
@@ -137,6 +137,8 @@ export interface WorkflowDiffResult {
|
||||
}>;
|
||||
shouldActivate?: boolean;
|
||||
shouldDeactivate?: boolean;
|
||||
tagsToAdd?: string[];
|
||||
tagsToRemove?: string[];
|
||||
}
|
||||
export interface NodeReference {
|
||||
id?: string;
|
||||
|
||||
2
dist/types/workflow-diff.d.ts.map
vendored
2
dist/types/workflow-diff.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"workflow-diff.d.ts","sourceRoot":"","sources":["../../src/types/workflow-diff.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"workflow-diff.d.ts","sourceRoot":"","sources":["../../src/types/workflow-diff.ts"],"names":[],"mappings":"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"}
|
||||
2
dist/types/workflow-diff.js.map
vendored
2
dist/types/workflow-diff.js.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"workflow-diff.js","sourceRoot":"","sources":["../../src/types/workflow-diff.ts"],"names":[],"mappings":";;AAyMA,0CAIC;AAED,sDAGC;AAED,kDAGC;AAdD,SAAgB,eAAe,CAAC,EAAyB;IAGvD,OAAO,CAAC,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AAC5G,CAAC;AAED,SAAgB,qBAAqB,CAAC,EAAyB;IAE7D,OAAO,CAAC,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,oBAAoB,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AACpI,CAAC;AAED,SAAgB,mBAAmB,CAAC,EAAyB;IAE3D,OAAO,CAAC,gBAAgB,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AACnF,CAAC"}
|
||||
{"version":3,"file":"workflow-diff.js","sourceRoot":"","sources":["../../src/types/workflow-diff.ts"],"names":[],"mappings":";;AA2MA,0CAIC;AAED,sDAGC;AAED,kDAGC;AAdD,SAAgB,eAAe,CAAC,EAAyB;IAGvD,OAAO,CAAC,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AAC5G,CAAC;AAED,SAAgB,qBAAqB,CAAC,EAAyB;IAE7D,OAAO,CAAC,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,oBAAoB,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AACpI,CAAC;AAED,SAAgB,mBAAmB,CAAC,EAAyB;IAE3D,OAAO,CAAC,gBAAgB,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AACnF,CAAC"}
|
||||
10
dist/utils/node-type-utils.js
vendored
10
dist/utils/node-type-utils.js
vendored
@@ -87,12 +87,10 @@ function isTriggerNode(nodeType) {
|
||||
if (lowerType.includes('webhook') && !lowerType.includes('respond')) {
|
||||
return true;
|
||||
}
|
||||
const specificTriggers = [
|
||||
'nodes-base.start',
|
||||
'nodes-base.manualTrigger',
|
||||
'nodes-base.formTrigger'
|
||||
];
|
||||
return specificTriggers.includes(normalized);
|
||||
if (lowerType.includes('emailread') || lowerType.includes('emailreadimap')) {
|
||||
return true;
|
||||
}
|
||||
return normalized === 'nodes-base.start';
|
||||
}
|
||||
function isActivatableTrigger(nodeType) {
|
||||
return isTriggerNode(nodeType);
|
||||
|
||||
2
dist/utils/node-type-utils.js.map
vendored
2
dist/utils/node-type-utils.js.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"node-type-utils.js","sourceRoot":"","sources":["../../src/utils/node-type-utils.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"node-type-utils.js","sourceRoot":"","sources":["../../src/utils/node-type-utils.ts"],"names":[],"mappings":"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"}
|
||||
15284
package-lock.json
generated
15284
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.34.5",
|
||||
"version": "2.37.3",
|
||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -152,17 +152,17 @@
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.20.1",
|
||||
"@n8n/n8n-nodes-langchain": "^2.6.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.6.3",
|
||||
"n8n-core": "^2.6.1",
|
||||
"n8n-workflow": "^2.6.0",
|
||||
"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",
|
||||
@@ -175,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +57,14 @@ export interface DocumentationGeneratorConfig {
|
||||
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'>> = {
|
||||
const DEFAULT_CONFIG: Required<Omit<DocumentationGeneratorConfig, 'baseUrl' | 'temperature'>> = {
|
||||
model: 'qwen3-4b-thinking-2507',
|
||||
apiKey: 'not-needed',
|
||||
timeout: 60000,
|
||||
@@ -78,6 +80,7 @@ export class DocumentationGenerator {
|
||||
private model: string;
|
||||
private maxTokens: number;
|
||||
private timeout: number;
|
||||
private temperature?: number;
|
||||
|
||||
constructor(config: DocumentationGeneratorConfig) {
|
||||
const fullConfig = { ...DEFAULT_CONFIG, ...config };
|
||||
@@ -90,6 +93,7 @@ export class DocumentationGenerator {
|
||||
this.model = fullConfig.model;
|
||||
this.maxTokens = fullConfig.maxTokens;
|
||||
this.timeout = fullConfig.timeout;
|
||||
this.temperature = fullConfig.temperature;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,8 +105,8 @@ export class DocumentationGenerator {
|
||||
|
||||
const completion = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
max_tokens: this.maxTokens,
|
||||
temperature: 0.3, // Lower temperature for more consistent output
|
||||
max_completion_tokens: this.maxTokens,
|
||||
...(this.temperature !== undefined ? { temperature: this.temperature } : {}),
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -321,7 +325,7 @@ Guidelines:
|
||||
try {
|
||||
const completion = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
max_tokens: 10,
|
||||
max_completion_tokens: 200,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -353,10 +357,15 @@ 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 } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* 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';
|
||||
@@ -43,21 +44,26 @@ let initializationPromise: Promise<SharedDatabaseState> | null = null;
|
||||
* @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 === dbPath) {
|
||||
if (sharedState && sharedState.initialized && sharedState.dbPath === normalizedPath) {
|
||||
sharedState.refCount++;
|
||||
logger.debug('Reusing shared database connection', {
|
||||
refCount: sharedState.refCount,
|
||||
dbPath
|
||||
dbPath: normalizedPath
|
||||
});
|
||||
return sharedState;
|
||||
}
|
||||
|
||||
// If already initialized with a DIFFERENT path, this is a configuration error
|
||||
if (sharedState && sharedState.initialized && sharedState.dbPath !== dbPath) {
|
||||
if (sharedState && sharedState.initialized && sharedState.dbPath !== normalizedPath) {
|
||||
logger.error('Attempted to initialize shared database with different path', {
|
||||
existingPath: sharedState.dbPath,
|
||||
requestedPath: dbPath
|
||||
requestedPath: normalizedPath
|
||||
});
|
||||
throw new Error(`Shared database already initialized with different path: ${sharedState.dbPath}`);
|
||||
}
|
||||
@@ -69,7 +75,7 @@ export async function getSharedDatabase(dbPath: string): Promise<SharedDatabaseS
|
||||
state.refCount++;
|
||||
logger.debug('Reusing shared database (waited for init)', {
|
||||
refCount: state.refCount,
|
||||
dbPath
|
||||
dbPath: normalizedPath
|
||||
});
|
||||
return state;
|
||||
} catch (error) {
|
||||
@@ -80,7 +86,7 @@ export async function getSharedDatabase(dbPath: string): Promise<SharedDatabaseS
|
||||
}
|
||||
|
||||
// Start new initialization
|
||||
initializationPromise = initializeSharedDatabase(dbPath);
|
||||
initializationPromise = initializeSharedDatabase(normalizedPath);
|
||||
|
||||
try {
|
||||
const state = await initializationPromise;
|
||||
|
||||
@@ -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';
|
||||
@@ -31,6 +31,11 @@ function getValidator(repository: NodeRepository): WorkflowValidator {
|
||||
return cachedValidator;
|
||||
}
|
||||
|
||||
// Operation types that identify nodes by nodeId/nodeName
|
||||
const NODE_TARGETING_OPERATIONS = new Set([
|
||||
'updateNode', 'removeNode', 'moveNode', 'enableNode', 'disableNode'
|
||||
]);
|
||||
|
||||
// Zod schema for the diff request
|
||||
const workflowDiffSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -48,8 +53,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)
|
||||
@@ -63,6 +68,23 @@ const workflowDiffSchema = z.object({
|
||||
settings: z.any().optional(),
|
||||
name: z.string().optional(),
|
||||
tag: z.string().optional(),
|
||||
// Aliases: LLMs often use "id" instead of "nodeId" — accept both
|
||||
id: z.string().optional(),
|
||||
}).transform((op) => {
|
||||
// Normalize common field aliases for node-targeting operations:
|
||||
// - "name" → "nodeName" (LLMs confuse the updateName "name" field with node identification)
|
||||
// - "id" → "nodeId" (natural alias)
|
||||
if (NODE_TARGETING_OPERATIONS.has(op.type)) {
|
||||
if (!op.nodeName && !op.nodeId && op.name) {
|
||||
op.nodeName = op.name;
|
||||
op.name = undefined;
|
||||
}
|
||||
if (!op.nodeId && op.id) {
|
||||
op.nodeId = op.id;
|
||||
op.id = undefined;
|
||||
}
|
||||
}
|
||||
return op;
|
||||
})),
|
||||
validateOnly: z.boolean().optional(),
|
||||
continueOnError: z.boolean().optional(),
|
||||
@@ -178,11 +200,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 +288,7 @@ export async function handleUpdatePartialWorkflow(
|
||||
if (!skipValidation) {
|
||||
return {
|
||||
success: false,
|
||||
saved: false,
|
||||
error: errorMessage,
|
||||
details: {
|
||||
errors: structureErrors,
|
||||
@@ -273,7 +297,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 +313,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 +400,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 +416,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 +446,7 @@ export async function handleUpdatePartialWorkflow(
|
||||
|
||||
return {
|
||||
success: true,
|
||||
saved: true,
|
||||
data: {
|
||||
id: finalWorkflow.id,
|
||||
name: finalWorkflow.name,
|
||||
@@ -375,7 +459,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 +497,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 +511,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
|
||||
*/
|
||||
|
||||
@@ -210,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(
|
||||
@@ -687,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;
|
||||
@@ -720,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();
|
||||
@@ -808,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')) {
|
||||
@@ -816,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: [
|
||||
{
|
||||
@@ -1125,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 || {};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const UI_APP_CONFIGS: UIAppConfig[] = [
|
||||
'n8n_delete_workflow',
|
||||
'n8n_test_workflow',
|
||||
'n8n_autofix_workflow',
|
||||
'n8n_deploy_template',
|
||||
// n8n_deploy_template disabled: Claude.ai renders blank content for this tool
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -29,4 +29,8 @@ export const UI_APP_CONFIGS: UIAppConfig[] = [
|
||||
'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.
|
||||
];
|
||||
|
||||
@@ -64,13 +64,19 @@ export class UIAppRegistry {
|
||||
* 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 } };
|
||||
tool._meta = {
|
||||
ui: { resourceUri: entry.config.uri },
|
||||
'ui/resourceUri': entry.config.uri,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* 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)
|
||||
*/
|
||||
@@ -81,6 +82,7 @@ Options:
|
||||
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)
|
||||
|
||||
|
||||
@@ -28,6 +28,21 @@ export interface WorkflowNode {
|
||||
typeVersion?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool description from node, checking all possible property locations.
|
||||
* Different n8n tool types store descriptions in different places:
|
||||
* - toolDescription: HTTP Request Tool, Vector Store Tool
|
||||
* - description: Workflow Tool, Code Tool, AI Agent Tool
|
||||
* - options.description: SerpApi, Wikipedia, SearXNG
|
||||
*/
|
||||
function getToolDescription(node: WorkflowNode): string | undefined {
|
||||
return (
|
||||
node.parameters.toolDescription ||
|
||||
node.parameters.description ||
|
||||
node.parameters.options?.description
|
||||
);
|
||||
}
|
||||
|
||||
export interface WorkflowJson {
|
||||
name?: string;
|
||||
nodes: WorkflowNode[];
|
||||
@@ -58,7 +73,7 @@ export function validateHTTPRequestTool(node: WorkflowNode): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// 1. Check toolDescription (REQUIRED)
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -66,7 +81,7 @@ export function validateHTTPRequestTool(node: WorkflowNode): ValidationIssue[] {
|
||||
message: `HTTP Request Tool "${node.name}" has no toolDescription. Add a clear description to help the LLM know when to use this API.`,
|
||||
code: 'MISSING_TOOL_DESCRIPTION'
|
||||
});
|
||||
} else if (node.parameters.toolDescription.trim().length < MIN_DESCRIPTION_LENGTH_MEDIUM) {
|
||||
} else if (getToolDescription(node)!.trim().length < MIN_DESCRIPTION_LENGTH_MEDIUM) {
|
||||
issues.push({
|
||||
severity: 'warning',
|
||||
nodeId: node.id,
|
||||
@@ -214,8 +229,8 @@ export function validateHTTPRequestTool(node: WorkflowNode): ValidationIssue[] {
|
||||
export function validateCodeTool(node: WorkflowNode): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// 1. Check toolDescription (REQUIRED)
|
||||
if (!node.parameters.toolDescription) {
|
||||
// 1. Check toolDescription (REQUIRED) - check all possible locations
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -261,7 +276,7 @@ export function validateVectorStoreTool(
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// 1. Check toolDescription (REQUIRED)
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -302,7 +317,7 @@ export function validateWorkflowTool(node: WorkflowNode, reverseConnections?: Ma
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// 1. Check toolDescription (REQUIRED)
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -337,7 +352,7 @@ export function validateAIAgentTool(
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// 1. Check toolDescription (REQUIRED)
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -378,7 +393,7 @@ export function validateMCPClientTool(node: WorkflowNode): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// 1. Check toolDescription (REQUIRED)
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -406,20 +421,14 @@ export function validateMCPClientTool(node: WorkflowNode): ValidationIssue[] {
|
||||
* 7-8. Simple Tools (Calculator, Think) Validators
|
||||
* From spec lines 1868-2009
|
||||
*/
|
||||
export function validateCalculatorTool(node: WorkflowNode): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// Calculator Tool has a built-in description and is self-explanatory
|
||||
// toolDescription is optional - no validation needed
|
||||
return issues;
|
||||
export function validateCalculatorTool(_node: WorkflowNode): ValidationIssue[] {
|
||||
// Calculator Tool has a built-in description - no validation needed
|
||||
return [];
|
||||
}
|
||||
|
||||
export function validateThinkTool(node: WorkflowNode): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// Think Tool has a built-in description and is self-explanatory
|
||||
// toolDescription is optional - no validation needed
|
||||
return issues;
|
||||
export function validateThinkTool(_node: WorkflowNode): ValidationIssue[] {
|
||||
// Think Tool has a built-in description - no validation needed
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -430,7 +439,7 @@ export function validateSerpApiTool(node: WorkflowNode): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// 1. Check toolDescription (REQUIRED)
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -457,7 +466,7 @@ export function validateWikipediaTool(node: WorkflowNode): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// 1. Check toolDescription (REQUIRED)
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -487,7 +496,7 @@ export function validateSearXngTool(node: WorkflowNode): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// 1. Check toolDescription (REQUIRED)
|
||||
if (!node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
nodeId: node.id,
|
||||
@@ -526,7 +535,7 @@ export function validateWolframAlphaTool(node: WorkflowNode): ValidationIssue[]
|
||||
}
|
||||
|
||||
// 2. Check description (INFO)
|
||||
if (!node.parameters.description && !node.parameters.toolDescription) {
|
||||
if (!getToolDescription(node)) {
|
||||
issues.push({
|
||||
severity: 'info',
|
||||
nodeId: node.id,
|
||||
|
||||
@@ -254,7 +254,7 @@ export class N8nApiClient {
|
||||
|
||||
async activateWorkflow(id: string): Promise<Workflow> {
|
||||
try {
|
||||
const response = await this.client.post(`/workflows/${id}/activate`);
|
||||
const response = await this.client.post(`/workflows/${id}/activate`, {});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleN8nApiError(error);
|
||||
@@ -263,7 +263,7 @@ export class N8nApiClient {
|
||||
|
||||
async deactivateWorkflow(id: string): Promise<Workflow> {
|
||||
try {
|
||||
const response = await this.client.post(`/workflows/${id}/deactivate`);
|
||||
const response = await this.client.post(`/workflows/${id}/deactivate`, {});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleN8nApiError(error);
|
||||
@@ -493,6 +493,15 @@ export class N8nApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
async updateWorkflowTags(workflowId: string, tagIds: string[]): Promise<Tag[]> {
|
||||
try {
|
||||
const response = await this.client.put(`/workflows/${workflowId}/tags`, tagIds.filter(id => id).map(id => ({ id })));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleN8nApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Source Control Management (Enterprise feature)
|
||||
async getSourceControlStatus(): Promise<SourceControlStatus> {
|
||||
try {
|
||||
|
||||
@@ -49,7 +49,7 @@ export const workflowConnectionSchema = z.record(
|
||||
ai_memory: connectionArraySchema.optional(),
|
||||
ai_embedding: connectionArraySchema.optional(),
|
||||
ai_vectorStore: connectionArraySchema.optional(),
|
||||
})
|
||||
}).catchall(connectionArraySchema) // Allow additional AI connection types (ai_outputParser, ai_document, ai_textSplitter, etc.)
|
||||
);
|
||||
|
||||
export const workflowSettingsSchema = z.object({
|
||||
@@ -248,15 +248,15 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
||||
const connectedNodes = new Set<string>();
|
||||
|
||||
// Collect all nodes that appear in connections (as source or target)
|
||||
// Check ALL connection types, not just 'main' - AI workflows use ai_tool, ai_languageModel, etc.
|
||||
const ALL_CONNECTION_TYPES = ['main', 'error', 'ai_tool', 'ai_languageModel', 'ai_memory', 'ai_embedding', 'ai_vectorStore'] as const;
|
||||
|
||||
// Iterate over ALL connection types present in the data — not a hardcoded list —
|
||||
// so that every AI connection type (ai_outputParser, ai_document, ai_textSplitter,
|
||||
// ai_agent, ai_chain, ai_retriever, etc.) is covered automatically.
|
||||
Object.entries(workflow.connections).forEach(([sourceName, connection]) => {
|
||||
connectedNodes.add(sourceName); // Node has outgoing connection
|
||||
|
||||
// Check all connection types for target nodes
|
||||
ALL_CONNECTION_TYPES.forEach(connType => {
|
||||
const connData = (connection as Record<string, unknown>)[connType];
|
||||
// Check every connection type key present on this source node
|
||||
const connectionRecord = connection as Record<string, unknown>;
|
||||
Object.values(connectionRecord).forEach((connData) => {
|
||||
if (connData && Array.isArray(connData)) {
|
||||
connData.forEach((outputs) => {
|
||||
if (Array.isArray(outputs)) {
|
||||
@@ -429,24 +429,29 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
||||
}
|
||||
}
|
||||
|
||||
if (connection.main && Array.isArray(connection.main)) {
|
||||
connection.main.forEach((outputs, outputIndex) => {
|
||||
if (Array.isArray(outputs)) {
|
||||
outputs.forEach((target, targetIndex) => {
|
||||
// Check if target exists by name (correct)
|
||||
if (!nodeNames.has(target.node)) {
|
||||
// Check if they're using an ID instead of name
|
||||
if (nodeIds.has(target.node)) {
|
||||
const correctName = nodeIdToName.get(target.node);
|
||||
errors.push(`Connection target uses node ID '${target.node}' but must use node name '${correctName}' (from ${sourceName}[${outputIndex}][${targetIndex}])`);
|
||||
} else {
|
||||
errors.push(`Connection references non-existent target node: ${target.node} (from ${sourceName}[${outputIndex}][${targetIndex}])`);
|
||||
// Check all connection types (main, error, ai_tool, ai_languageModel, etc.)
|
||||
const connectionRecord = connection as Record<string, unknown>;
|
||||
Object.values(connectionRecord).forEach((connData) => {
|
||||
if (connData && Array.isArray(connData)) {
|
||||
connData.forEach((outputs: any, outputIndex: number) => {
|
||||
if (Array.isArray(outputs)) {
|
||||
outputs.forEach((target: any, targetIndex: number) => {
|
||||
if (!target?.node) return;
|
||||
// Check if target exists by name (correct)
|
||||
if (!nodeNames.has(target.node)) {
|
||||
// Check if they're using an ID instead of name
|
||||
if (nodeIds.has(target.node)) {
|
||||
const correctName = nodeIdToName.get(target.node);
|
||||
errors.push(`Connection target uses node ID '${target.node}' but must use node name '${correctName}' (from ${sourceName}[${outputIndex}][${targetIndex}])`);
|
||||
} else {
|
||||
errors.push(`Connection references non-existent target node: ${target.node} (from ${sourceName}[${outputIndex}][${targetIndex}])`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export function sanitizeWorkflowNodes(workflow: any): any {
|
||||
|
||||
return {
|
||||
...workflow,
|
||||
nodes: workflow.nodes.map((node: any) => sanitizeNode(node))
|
||||
nodes: workflow.nodes.map(sanitizeNode)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,9 +121,7 @@ function sanitizeFilterConditions(conditions: any): any {
|
||||
|
||||
// Sanitize conditions array
|
||||
if (sanitized.conditions && Array.isArray(sanitized.conditions)) {
|
||||
sanitized.conditions = sanitized.conditions.map((condition: any) =>
|
||||
sanitizeCondition(condition)
|
||||
);
|
||||
sanitized.conditions = sanitized.conditions.map(sanitizeCondition);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
@@ -214,18 +212,25 @@ function inferDataType(operation: string): string {
|
||||
return 'boolean';
|
||||
}
|
||||
|
||||
// Number operations
|
||||
// Number operations (partial match to catch variants like "greaterThan" containing "gt")
|
||||
const numberOps = ['isNumeric', 'gt', 'gte', 'lt', 'lte'];
|
||||
if (numberOps.some(op => operation.includes(op))) {
|
||||
return 'number';
|
||||
}
|
||||
|
||||
// Date operations
|
||||
// Date operations (partial match to catch variants like "isAfter" containing "after")
|
||||
const dateOps = ['after', 'before', 'afterDate', 'beforeDate'];
|
||||
if (dateOps.some(op => operation.includes(op))) {
|
||||
return 'dateTime';
|
||||
}
|
||||
|
||||
// Object operations: empty/notEmpty/exists/notExists are generic object-level checks
|
||||
// (distinct from isEmpty/isNotEmpty which are boolean-typed operations)
|
||||
const objectOps = ['empty', 'notEmpty', 'exists', 'notExists'];
|
||||
if (objectOps.includes(operation)) {
|
||||
return 'object';
|
||||
}
|
||||
|
||||
// Default to string
|
||||
return 'string';
|
||||
}
|
||||
@@ -239,7 +244,11 @@ function isUnaryOperator(operation: string): boolean {
|
||||
'isNotEmpty',
|
||||
'true',
|
||||
'false',
|
||||
'isNumeric'
|
||||
'isNumeric',
|
||||
'empty',
|
||||
'notEmpty',
|
||||
'exists',
|
||||
'notExists'
|
||||
];
|
||||
return unaryOps.includes(operation);
|
||||
}
|
||||
|
||||
@@ -1374,8 +1374,11 @@ export class NodeSpecificValidators {
|
||||
});
|
||||
}
|
||||
|
||||
// Check for primitive return
|
||||
if (/return\s+(true|false|null|undefined|\d+|['"`])/m.test(code)) {
|
||||
// Skip primitive return check when helper functions are present,
|
||||
// since we can't distinguish top-level vs nested returns without AST.
|
||||
// Matches: function name(), const/let/var name = [async] function/arrow
|
||||
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',
|
||||
@@ -1487,7 +1490,7 @@ export class NodeSpecificValidators {
|
||||
// Check for common variable mistakes
|
||||
if (language === 'javaScript') {
|
||||
// Using $ without proper variable
|
||||
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',
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
import { WorkflowValidationResult } from './workflow-validator';
|
||||
import { WorkflowValidationResult, VALID_CONNECTION_TYPES } from './workflow-validator';
|
||||
import { ExpressionFormatIssue } from './expression-format-validator';
|
||||
import { NodeSimilarityService } from './node-similarity-service';
|
||||
import { NodeRepository } from '../database/node-repository';
|
||||
import {
|
||||
WorkflowDiffOperation,
|
||||
UpdateNodeOperation
|
||||
UpdateNodeOperation,
|
||||
ReplaceConnectionsOperation
|
||||
} from '../types/workflow-diff';
|
||||
import { WorkflowNode, Workflow } from '../types/n8n-api';
|
||||
import { Logger } from '../utils/logger';
|
||||
@@ -30,9 +31,22 @@ export type FixType =
|
||||
| 'error-output-config'
|
||||
| 'node-type-correction'
|
||||
| 'webhook-missing-path'
|
||||
| 'typeversion-upgrade' // Proactive version upgrades
|
||||
| 'version-migration' // Smart version migrations with breaking changes
|
||||
| 'tool-variant-correction'; // Fix base nodes used as AI tools when Tool variant exists
|
||||
| 'typeversion-upgrade' // Proactive version upgrades
|
||||
| 'version-migration' // Smart version migrations with breaking changes
|
||||
| 'tool-variant-correction' // Fix base nodes used as AI tools when Tool variant exists
|
||||
| 'connection-numeric-keys' // "0","1" keys → main[0], main[1]
|
||||
| 'connection-invalid-type' // type:"0" → type:"main"
|
||||
| 'connection-id-to-name' // node ID refs → node name refs
|
||||
| 'connection-duplicate-removal' // Dedup identical connection entries
|
||||
| 'connection-input-index'; // Out-of-bounds input index → clamped
|
||||
|
||||
export const CONNECTION_FIX_TYPES: FixType[] = [
|
||||
'connection-numeric-keys',
|
||||
'connection-invalid-type',
|
||||
'connection-id-to-name',
|
||||
'connection-duplicate-removal',
|
||||
'connection-input-index'
|
||||
];
|
||||
|
||||
export interface AutoFixConfig {
|
||||
applyFixes: boolean;
|
||||
@@ -175,6 +189,9 @@ export class WorkflowAutoFixer {
|
||||
await this.processVersionMigrationFixes(workflow, nodeMap, operations, fixes, postUpdateGuidance);
|
||||
}
|
||||
|
||||
// Process connection structure fixes (HIGH/MEDIUM confidence)
|
||||
this.processConnectionFixes(workflow, validationResult, fullConfig, operations, fixes);
|
||||
|
||||
// Filter by confidence threshold
|
||||
const filteredFixes = this.filterByConfidence(fixes, fullConfig.confidenceThreshold);
|
||||
const filteredOperations = this.filterOperationsByFixes(operations, filteredFixes, fixes);
|
||||
@@ -654,10 +671,16 @@ export class WorkflowAutoFixer {
|
||||
filteredFixes: FixOperation[],
|
||||
allFixes: FixOperation[]
|
||||
): WorkflowDiffOperation[] {
|
||||
// fixedNodes contains node names (FixOperation.node = node.name)
|
||||
const fixedNodes = new Set(filteredFixes.map(f => f.node));
|
||||
const hasConnectionFixes = filteredFixes.some(f => CONNECTION_FIX_TYPES.includes(f.type));
|
||||
return operations.filter(op => {
|
||||
if (op.type === 'updateNode') {
|
||||
return fixedNodes.has(op.nodeId || '');
|
||||
// Check both nodeName and nodeId — operations may use either
|
||||
return fixedNodes.has(op.nodeName || '') || fixedNodes.has(op.nodeId || '');
|
||||
}
|
||||
if (op.type === 'replaceConnections') {
|
||||
return hasConnectionFixes;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -677,7 +700,12 @@ export 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,
|
||||
@@ -730,6 +758,16 @@ export class WorkflowAutoFixer {
|
||||
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'}`;
|
||||
}
|
||||
@@ -737,6 +775,370 @@ export class WorkflowAutoFixer {
|
||||
return `Fixed ${parts.join(', ')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process connection structure fixes.
|
||||
* Deep-clones workflow.connections, applies fixes in order:
|
||||
* numeric keys → ID-to-name → invalid type → input index → dedup
|
||||
* Emits a single ReplaceConnectionsOperation if any corrections were made.
|
||||
*/
|
||||
private processConnectionFixes(
|
||||
workflow: Workflow,
|
||||
validationResult: WorkflowValidationResult,
|
||||
config: AutoFixConfig,
|
||||
operations: WorkflowDiffOperation[],
|
||||
fixes: FixOperation[]
|
||||
): void {
|
||||
if (!workflow.connections || Object.keys(workflow.connections).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build lookup maps
|
||||
const idToNameMap = new Map<string, string>();
|
||||
const nameSet = new Set<string>();
|
||||
for (const node of workflow.nodes) {
|
||||
idToNameMap.set(node.id, node.name);
|
||||
nameSet.add(node.name);
|
||||
}
|
||||
|
||||
// Deep-clone connections
|
||||
const conn: any = JSON.parse(JSON.stringify(workflow.connections));
|
||||
let anyFixed = false;
|
||||
|
||||
// 1. Fix numeric source keys ("0" → main[0])
|
||||
if (!config.fixTypes || config.fixTypes.includes('connection-numeric-keys')) {
|
||||
const numericKeyResult = this.fixNumericKeys(conn);
|
||||
if (numericKeyResult.length > 0) {
|
||||
fixes.push(...numericKeyResult);
|
||||
anyFixed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fix ID-to-name references (source keys and .node values)
|
||||
if (!config.fixTypes || config.fixTypes.includes('connection-id-to-name')) {
|
||||
const idToNameResult = this.fixIdToName(conn, idToNameMap, nameSet);
|
||||
if (idToNameResult.length > 0) {
|
||||
fixes.push(...idToNameResult);
|
||||
anyFixed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fix invalid connection types
|
||||
if (!config.fixTypes || config.fixTypes.includes('connection-invalid-type')) {
|
||||
const invalidTypeResult = this.fixInvalidTypes(conn);
|
||||
if (invalidTypeResult.length > 0) {
|
||||
fixes.push(...invalidTypeResult);
|
||||
anyFixed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fix out-of-bounds input indices
|
||||
if (!config.fixTypes || config.fixTypes.includes('connection-input-index')) {
|
||||
const inputIndexResult = this.fixInputIndices(conn, validationResult, workflow);
|
||||
if (inputIndexResult.length > 0) {
|
||||
fixes.push(...inputIndexResult);
|
||||
anyFixed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Dedup identical connection entries
|
||||
if (!config.fixTypes || config.fixTypes.includes('connection-duplicate-removal')) {
|
||||
const dedupResult = this.fixDuplicateConnections(conn);
|
||||
if (dedupResult.length > 0) {
|
||||
fixes.push(...dedupResult);
|
||||
anyFixed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyFixed) {
|
||||
const op: ReplaceConnectionsOperation = {
|
||||
type: 'replaceConnections',
|
||||
connections: conn
|
||||
};
|
||||
operations.push(op);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix numeric connection output keys ("0", "1" → main[0], main[1])
|
||||
*/
|
||||
private fixNumericKeys(conn: any): FixOperation[] {
|
||||
const fixes: FixOperation[] = [];
|
||||
const sourceNodes = Object.keys(conn);
|
||||
|
||||
for (const sourceName of sourceNodes) {
|
||||
const nodeConn = conn[sourceName];
|
||||
const numericKeys = Object.keys(nodeConn).filter(k => /^\d+$/.test(k));
|
||||
|
||||
if (numericKeys.length === 0) continue;
|
||||
|
||||
// Ensure main array exists
|
||||
if (!nodeConn['main']) {
|
||||
nodeConn['main'] = [];
|
||||
}
|
||||
|
||||
for (const numKey of numericKeys) {
|
||||
const index = parseInt(numKey, 10);
|
||||
const entries = nodeConn[numKey];
|
||||
|
||||
// Extend main array if needed (fill gaps with empty arrays)
|
||||
while (nodeConn['main'].length <= index) {
|
||||
nodeConn['main'].push([]);
|
||||
}
|
||||
|
||||
// Merge entries into main[index]
|
||||
const hadExisting = nodeConn['main'][index] && nodeConn['main'][index].length > 0;
|
||||
if (Array.isArray(entries)) {
|
||||
for (const outputGroup of entries) {
|
||||
if (Array.isArray(outputGroup)) {
|
||||
nodeConn['main'][index] = [
|
||||
...nodeConn['main'][index],
|
||||
...outputGroup
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hadExisting) {
|
||||
logger.warn(`Merged numeric key "${numKey}" into existing main[${index}] on node "${sourceName}" - dedup pass will clean exact duplicates`);
|
||||
}
|
||||
|
||||
fixes.push({
|
||||
node: sourceName,
|
||||
field: `connections.${sourceName}.${numKey}`,
|
||||
type: 'connection-numeric-keys',
|
||||
before: numKey,
|
||||
after: `main[${index}]`,
|
||||
confidence: hadExisting ? 'medium' : 'high',
|
||||
description: hadExisting
|
||||
? `Merged numeric connection key "${numKey}" into existing main[${index}] on node "${sourceName}"`
|
||||
: `Converted numeric connection key "${numKey}" to main[${index}] on node "${sourceName}"`
|
||||
});
|
||||
|
||||
delete nodeConn[numKey];
|
||||
}
|
||||
}
|
||||
|
||||
return fixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix node ID references in connections (replace IDs with names)
|
||||
*/
|
||||
private fixIdToName(
|
||||
conn: any,
|
||||
idToNameMap: Map<string, string>,
|
||||
nameSet: Set<string>
|
||||
): FixOperation[] {
|
||||
const fixes: FixOperation[] = [];
|
||||
|
||||
// Build rename plan for source keys, then check for collisions
|
||||
const renames: Array<{ oldKey: string; newKey: string }> = [];
|
||||
const sourceKeys = Object.keys(conn);
|
||||
for (const sourceKey of sourceKeys) {
|
||||
if (idToNameMap.has(sourceKey) && !nameSet.has(sourceKey)) {
|
||||
renames.push({ oldKey: sourceKey, newKey: idToNameMap.get(sourceKey)! });
|
||||
}
|
||||
}
|
||||
|
||||
// Check for collisions among renames (two IDs mapping to the same name)
|
||||
const newKeyCount = new Map<string, number>();
|
||||
for (const r of renames) {
|
||||
newKeyCount.set(r.newKey, (newKeyCount.get(r.newKey) || 0) + 1);
|
||||
}
|
||||
const safeRenames = renames.filter(r => {
|
||||
if ((newKeyCount.get(r.newKey) || 0) > 1) {
|
||||
logger.warn(`Skipping ambiguous ID-to-name rename: "${r.oldKey}" → "${r.newKey}" (multiple IDs map to same name)`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
for (const { oldKey, newKey } of safeRenames) {
|
||||
conn[newKey] = conn[oldKey];
|
||||
delete conn[oldKey];
|
||||
fixes.push({
|
||||
node: newKey,
|
||||
field: `connections.sourceKey`,
|
||||
type: 'connection-id-to-name',
|
||||
before: oldKey,
|
||||
after: newKey,
|
||||
confidence: 'high',
|
||||
description: `Replaced node ID "${oldKey}" with name "${newKey}" as connection source key`
|
||||
});
|
||||
}
|
||||
|
||||
// Fix .node values that are node IDs
|
||||
for (const sourceName of Object.keys(conn)) {
|
||||
const nodeConn = conn[sourceName];
|
||||
for (const outputKey of Object.keys(nodeConn)) {
|
||||
const outputs = nodeConn[outputKey];
|
||||
if (!Array.isArray(outputs)) continue;
|
||||
for (const outputGroup of outputs) {
|
||||
if (!Array.isArray(outputGroup)) continue;
|
||||
for (const entry of outputGroup) {
|
||||
if (entry && entry.node && idToNameMap.has(entry.node) && !nameSet.has(entry.node)) {
|
||||
const oldNode = entry.node;
|
||||
const newNode = idToNameMap.get(entry.node)!;
|
||||
entry.node = newNode;
|
||||
fixes.push({
|
||||
node: sourceName,
|
||||
field: `connections.${sourceName}.${outputKey}[].node`,
|
||||
type: 'connection-id-to-name',
|
||||
before: oldNode,
|
||||
after: newNode,
|
||||
confidence: 'high',
|
||||
description: `Replaced target node ID "${oldNode}" with name "${newNode}" in connection from "${sourceName}"`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix invalid connection types in entries (e.g., type:"0" → type:"main")
|
||||
*/
|
||||
private fixInvalidTypes(conn: any): FixOperation[] {
|
||||
const fixes: FixOperation[] = [];
|
||||
|
||||
for (const sourceName of Object.keys(conn)) {
|
||||
const nodeConn = conn[sourceName];
|
||||
for (const outputKey of Object.keys(nodeConn)) {
|
||||
const outputs = nodeConn[outputKey];
|
||||
if (!Array.isArray(outputs)) continue;
|
||||
for (const outputGroup of outputs) {
|
||||
if (!Array.isArray(outputGroup)) continue;
|
||||
for (const entry of outputGroup) {
|
||||
if (entry && entry.type && !VALID_CONNECTION_TYPES.has(entry.type)) {
|
||||
const oldType = entry.type;
|
||||
// Use the parent output key if it's valid, otherwise default to "main"
|
||||
const newType = VALID_CONNECTION_TYPES.has(outputKey) ? outputKey : 'main';
|
||||
entry.type = newType;
|
||||
fixes.push({
|
||||
node: sourceName,
|
||||
field: `connections.${sourceName}.${outputKey}[].type`,
|
||||
type: 'connection-invalid-type',
|
||||
before: oldType,
|
||||
after: newType,
|
||||
confidence: 'high',
|
||||
description: `Fixed invalid connection type "${oldType}" → "${newType}" in connection from "${sourceName}" to "${entry.node}"`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix out-of-bounds input indices (clamp to valid range)
|
||||
*/
|
||||
private fixInputIndices(
|
||||
conn: any,
|
||||
validationResult: WorkflowValidationResult,
|
||||
workflow: Workflow
|
||||
): FixOperation[] {
|
||||
const fixes: FixOperation[] = [];
|
||||
|
||||
// Parse INPUT_INDEX_OUT_OF_BOUNDS errors from validation
|
||||
for (const error of validationResult.errors) {
|
||||
if (error.code !== 'INPUT_INDEX_OUT_OF_BOUNDS') continue;
|
||||
|
||||
const targetNodeName = error.nodeName;
|
||||
if (!targetNodeName) continue;
|
||||
|
||||
// Extract the bad index and input count from the error message
|
||||
const match = error.message.match(/Input index (\d+).*?has (\d+) main input/);
|
||||
if (!match) {
|
||||
logger.warn(`Could not parse INPUT_INDEX_OUT_OF_BOUNDS error for node "${targetNodeName}": ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const badIndex = parseInt(match[1], 10);
|
||||
const inputCount = parseInt(match[2], 10);
|
||||
|
||||
// For multi-input nodes, clamp to max valid index; for single-input, reset to 0
|
||||
const clampedIndex = inputCount > 1 ? Math.min(badIndex, inputCount - 1) : 0;
|
||||
|
||||
// Find and fix the bad index in connections
|
||||
for (const sourceName of Object.keys(conn)) {
|
||||
const nodeConn = conn[sourceName];
|
||||
for (const outputKey of Object.keys(nodeConn)) {
|
||||
const outputs = nodeConn[outputKey];
|
||||
if (!Array.isArray(outputs)) continue;
|
||||
for (const outputGroup of outputs) {
|
||||
if (!Array.isArray(outputGroup)) continue;
|
||||
for (const entry of outputGroup) {
|
||||
if (entry && entry.node === targetNodeName && entry.index === badIndex) {
|
||||
entry.index = clampedIndex;
|
||||
fixes.push({
|
||||
node: sourceName,
|
||||
field: `connections.${sourceName}.${outputKey}[].index`,
|
||||
type: 'connection-input-index',
|
||||
before: badIndex,
|
||||
after: clampedIndex,
|
||||
confidence: 'medium',
|
||||
description: `Clamped input index ${badIndex} → ${clampedIndex} for target node "${targetNodeName}" (has ${inputCount} input${inputCount === 1 ? '' : 's'})`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicate connection entries (same node, type, index)
|
||||
*/
|
||||
private fixDuplicateConnections(conn: any): FixOperation[] {
|
||||
const fixes: FixOperation[] = [];
|
||||
|
||||
for (const sourceName of Object.keys(conn)) {
|
||||
const nodeConn = conn[sourceName];
|
||||
for (const outputKey of Object.keys(nodeConn)) {
|
||||
const outputs = nodeConn[outputKey];
|
||||
if (!Array.isArray(outputs)) continue;
|
||||
for (let i = 0; i < outputs.length; i++) {
|
||||
const outputGroup = outputs[i];
|
||||
if (!Array.isArray(outputGroup)) continue;
|
||||
|
||||
const seen = new Set<string>();
|
||||
const deduped: any[] = [];
|
||||
|
||||
for (const entry of outputGroup) {
|
||||
const key = JSON.stringify({ node: entry.node, type: entry.type, index: entry.index });
|
||||
if (seen.has(key)) {
|
||||
fixes.push({
|
||||
node: sourceName,
|
||||
field: `connections.${sourceName}.${outputKey}[${i}]`,
|
||||
type: 'connection-duplicate-removal',
|
||||
before: entry,
|
||||
after: null,
|
||||
confidence: 'high',
|
||||
description: `Removed duplicate connection from "${sourceName}" to "${entry.node}" (type: ${entry.type}, index: ${entry.index})`
|
||||
});
|
||||
} else {
|
||||
seen.add(key);
|
||||
deduped.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
outputs[i] = deduped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process version upgrade fixes (proactive upgrades to latest versions)
|
||||
* HIGH confidence for non-breaking upgrades, MEDIUM for upgrades with auto-migratable changes
|
||||
@@ -786,10 +1188,11 @@ export class WorkflowAutoFixer {
|
||||
description: `Upgrade ${node.name} from v${currentVersion} to v${latestVersion}. ${analysis.reason}`
|
||||
});
|
||||
|
||||
// Create update operation
|
||||
// Create update operation — both nodeId and nodeName needed for fix filtering
|
||||
const operation: UpdateNodeOperation = {
|
||||
type: 'updateNode',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
updates: {
|
||||
typeVersion: parseFloat(latestVersion),
|
||||
parameters: migrationResult.updatedNode.parameters,
|
||||
|
||||
@@ -38,11 +38,22 @@ import { isActivatableTrigger } from '../utils/node-type-utils';
|
||||
|
||||
const logger = new Logger({ prefix: '[WorkflowDiffEngine]' });
|
||||
|
||||
/**
|
||||
* Not safe for concurrent use — create a new instance per request.
|
||||
* Instance state is reset at the start of each applyDiff() call.
|
||||
*/
|
||||
export class WorkflowDiffEngine {
|
||||
// Track node name changes during operations for connection reference updates
|
||||
private renameMap: Map<string, string> = new Map();
|
||||
// Track warnings during operation processing
|
||||
private warnings: WorkflowDiffValidationError[] = [];
|
||||
// Track which nodes were added/updated so sanitization only runs on them
|
||||
private modifiedNodeIds = new Set<string>();
|
||||
// Track removed node names for better error messages
|
||||
private removedNodeNames = new Set<string>();
|
||||
// Track tag operations for dedicated API calls
|
||||
private tagsToAdd: string[] = [];
|
||||
private tagsToRemove: string[] = [];
|
||||
|
||||
/**
|
||||
* Apply diff operations to a workflow
|
||||
@@ -55,6 +66,10 @@ export class WorkflowDiffEngine {
|
||||
// Reset tracking for this diff operation
|
||||
this.renameMap.clear();
|
||||
this.warnings = [];
|
||||
this.modifiedNodeIds.clear();
|
||||
this.removedNodeNames.clear();
|
||||
this.tagsToAdd = [];
|
||||
this.tagsToRemove = [];
|
||||
|
||||
// Clone workflow to avoid modifying original
|
||||
const workflowCopy = JSON.parse(JSON.stringify(workflow));
|
||||
@@ -135,7 +150,9 @@ export 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 {
|
||||
// Atomic mode: all operations must succeed
|
||||
@@ -201,12 +218,16 @@ export class WorkflowDiffEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize ALL nodes in the workflow after operations are applied
|
||||
// This ensures existing invalid nodes (e.g., binary operators with singleValue: true)
|
||||
// are fixed automatically when any update is made to the workflow
|
||||
workflowCopy.nodes = workflowCopy.nodes.map((node: WorkflowNode) => sanitizeNode(node));
|
||||
|
||||
logger.debug('Applied full-workflow sanitization to all nodes');
|
||||
// Sanitize only modified nodes to avoid breaking unrelated nodes (#592)
|
||||
if (this.modifiedNodeIds.size > 0) {
|
||||
workflowCopy.nodes = workflowCopy.nodes.map((node: WorkflowNode) => {
|
||||
if (this.modifiedNodeIds.has(node.id)) {
|
||||
return sanitizeNode(node);
|
||||
}
|
||||
return node;
|
||||
});
|
||||
logger.debug(`Sanitized ${this.modifiedNodeIds.size} modified nodes`);
|
||||
}
|
||||
|
||||
// If validateOnly flag is set, return success without applying
|
||||
if (request.validateOnly) {
|
||||
@@ -233,7 +254,9 @@ export 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
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -248,7 +271,6 @@ export class WorkflowDiffEngine {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validate a single operation
|
||||
*/
|
||||
@@ -405,7 +427,7 @@ export class WorkflowDiffEngine {
|
||||
|
||||
// Check for missing required parameter
|
||||
if (!operation.updates) {
|
||||
return `Missing required parameter 'updates'. The updateNode operation requires an 'updates' object containing properties to modify. Example: {type: "updateNode", nodeId: "abc", updates: {name: "New Name"}}`;
|
||||
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);
|
||||
@@ -510,12 +532,18 @@ export class WorkflowDiffEngine {
|
||||
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(', ');
|
||||
@@ -614,13 +642,16 @@ export class WorkflowDiffEngine {
|
||||
// Sanitize node to ensure complete metadata (filter options, operator structure, etc.)
|
||||
const sanitizedNode = sanitizeNode(newNode);
|
||||
|
||||
this.modifiedNodeIds.add(sanitizedNode.id);
|
||||
workflow.nodes.push(sanitizedNode);
|
||||
}
|
||||
|
||||
private applyRemoveNode(workflow: Workflow, operation: RemoveNodeOperation): void {
|
||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||
if (!node) return;
|
||||
|
||||
|
||||
this.removedNodeNames.add(node.name);
|
||||
|
||||
// Remove node from array
|
||||
const index = workflow.nodes.findIndex(n => n.id === node.id);
|
||||
if (index !== -1) {
|
||||
@@ -631,30 +662,36 @@ export class WorkflowDiffEngine {
|
||||
delete workflow.connections[node.name];
|
||||
|
||||
// Remove all connections to this node
|
||||
Object.keys(workflow.connections).forEach(sourceName => {
|
||||
const sourceConnections = workflow.connections[sourceName];
|
||||
Object.keys(sourceConnections).forEach(outputName => {
|
||||
sourceConnections[outputName] = sourceConnections[outputName].map(connections =>
|
||||
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)
|
||||
).filter(connections => connections.length > 0);
|
||||
|
||||
// Clean up empty arrays
|
||||
if (sourceConnections[outputName].length === 0) {
|
||||
);
|
||||
|
||||
// Trim trailing empty arrays only (preserve intermediate empty arrays for positional indices)
|
||||
const trimmed = sourceConnections[outputName];
|
||||
while (trimmed.length > 0 && trimmed[trimmed.length - 1].length === 0) {
|
||||
trimmed.pop();
|
||||
}
|
||||
|
||||
if (trimmed.length === 0) {
|
||||
delete sourceConnections[outputName];
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Clean up empty connection objects
|
||||
if (Object.keys(sourceConnections).length === 0) {
|
||||
delete workflow.connections[sourceName];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private applyUpdateNode(workflow: Workflow, operation: UpdateNodeOperation): void {
|
||||
const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
|
||||
if (!node) return;
|
||||
|
||||
this.modifiedNodeIds.add(node.id);
|
||||
|
||||
// Track node renames for connection reference updates
|
||||
if (operation.updates.name && operation.updates.name !== node.name) {
|
||||
const oldName = node.name;
|
||||
@@ -706,10 +743,18 @@ export class WorkflowDiffEngine {
|
||||
): { sourceOutput: string; sourceIndex: number } {
|
||||
const sourceNode = this.findNode(workflow, operation.source, operation.source);
|
||||
|
||||
// Start with explicit values or defaults
|
||||
let sourceOutput = operation.sourceOutput ?? 'main';
|
||||
// Start with explicit values or defaults, coercing to correct types
|
||||
let sourceOutput = String(operation.sourceOutput ?? 'main');
|
||||
let sourceIndex = operation.sourceIndex ?? 0;
|
||||
|
||||
// Remap numeric sourceOutput (e.g., "0", "1") to "main" with sourceIndex (#537)
|
||||
// Skip when smart parameters (branch, case) are present — they take precedence
|
||||
if (/^\d+$/.test(sourceOutput) && operation.sourceIndex === undefined
|
||||
&& operation.branch === undefined && operation.case === undefined) {
|
||||
sourceIndex = parseInt(sourceOutput, 10);
|
||||
sourceOutput = 'main';
|
||||
}
|
||||
|
||||
// Smart parameter: branch (for IF nodes)
|
||||
// IF nodes use 'main' output with index 0 (true) or 1 (false)
|
||||
if (operation.branch !== undefined && operation.sourceIndex === undefined) {
|
||||
@@ -758,7 +803,8 @@ export class WorkflowDiffEngine {
|
||||
|
||||
// Use nullish coalescing to properly handle explicit 0 values
|
||||
// Default targetInput to sourceOutput to preserve connection type for AI connections (ai_tool, ai_memory, etc.)
|
||||
const targetInput = operation.targetInput ?? sourceOutput;
|
||||
// Coerce to string to handle numeric values passed as sourceOutput/targetInput
|
||||
const targetInput = String(operation.targetInput ?? sourceOutput);
|
||||
const targetIndex = operation.targetIndex ?? 0;
|
||||
|
||||
// Initialize source node connections object
|
||||
@@ -795,18 +841,14 @@ export class WorkflowDiffEngine {
|
||||
private applyRemoveConnection(workflow: Workflow, operation: RemoveConnectionOperation): void {
|
||||
const sourceNode = this.findNode(workflow, operation.source, operation.source);
|
||||
const targetNode = this.findNode(workflow, operation.target, operation.target);
|
||||
// If ignoreErrors is true, silently succeed even if nodes don't exist
|
||||
if (!sourceNode || !targetNode) {
|
||||
if (operation.ignoreErrors) {
|
||||
return; // Gracefully handle missing nodes
|
||||
}
|
||||
return; // Should never reach here if validation passed, but safety check
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceOutput = operation.sourceOutput || 'main';
|
||||
const sourceOutput = String(operation.sourceOutput ?? 'main');
|
||||
const connections = workflow.connections[sourceNode.name]?.[sourceOutput];
|
||||
if (!connections) return;
|
||||
|
||||
|
||||
// Remove connection from all indices
|
||||
workflow.connections[sourceNode.name][sourceOutput] = connections.map(conns =>
|
||||
conns.filter(conn => conn.node !== targetNode.name)
|
||||
@@ -877,20 +919,26 @@ export class WorkflowDiffEngine {
|
||||
}
|
||||
|
||||
private applyAddTag(workflow: Workflow, operation: AddTagOperation): void {
|
||||
if (!workflow.tags) {
|
||||
workflow.tags = [];
|
||||
// Track for dedicated API call instead of modifying workflow.tags directly
|
||||
// Reconcile: if previously marked for removal, cancel the removal instead
|
||||
const removeIdx = this.tagsToRemove.indexOf(operation.tag);
|
||||
if (removeIdx !== -1) {
|
||||
this.tagsToRemove.splice(removeIdx, 1);
|
||||
}
|
||||
if (!workflow.tags.includes(operation.tag)) {
|
||||
workflow.tags.push(operation.tag);
|
||||
if (!this.tagsToAdd.includes(operation.tag)) {
|
||||
this.tagsToAdd.push(operation.tag);
|
||||
}
|
||||
}
|
||||
|
||||
private applyRemoveTag(workflow: Workflow, operation: RemoveTagOperation): void {
|
||||
if (!workflow.tags) return;
|
||||
|
||||
const index = workflow.tags.indexOf(operation.tag);
|
||||
if (index !== -1) {
|
||||
workflow.tags.splice(index, 1);
|
||||
// Track for dedicated API call instead of modifying workflow.tags directly
|
||||
// Reconcile: if previously marked for addition, cancel the addition instead
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1015,7 +1063,12 @@ export class WorkflowDiffEngine {
|
||||
}
|
||||
return true;
|
||||
})
|
||||
).filter(conns => conns.length > 0);
|
||||
);
|
||||
|
||||
// Trim trailing empty arrays only (preserve intermediate for positional indices)
|
||||
while (filteredConnections.length > 0 && filteredConnections[filteredConnections.length - 1].length === 0) {
|
||||
filteredConnections.pop();
|
||||
}
|
||||
|
||||
if (filteredConnections.length === 0) {
|
||||
delete outputs[outputName];
|
||||
@@ -1184,15 +1237,21 @@ export class WorkflowDiffEngine {
|
||||
private setNestedProperty(obj: any, path: string, value: any): void {
|
||||
const keys = path.split('.');
|
||||
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; // parent path doesn't exist, nothing to delete
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,28 @@ import { ExpressionFormatValidator } from './expression-format-validator';
|
||||
import { NodeSimilarityService, NodeSuggestion } from './node-similarity-service';
|
||||
import { NodeTypeNormalizer } from '../utils/node-type-normalizer';
|
||||
import { Logger } from '../utils/logger';
|
||||
import { validateAISpecificNodes, hasAINodes } from './ai-node-validator';
|
||||
import { validateAISpecificNodes, hasAINodes, AI_CONNECTION_TYPES } from './ai-node-validator';
|
||||
import { isAIToolSubNode } from './ai-tool-validators';
|
||||
import { isTriggerNode } from '../utils/node-type-utils';
|
||||
import { isNonExecutableNode } from '../utils/node-classification';
|
||||
import { ToolVariantGenerator } from './tool-variant-generator';
|
||||
const logger = new Logger({ prefix: '[WorkflowValidator]' });
|
||||
|
||||
/**
|
||||
* All valid connection output keys in n8n workflows.
|
||||
* Any key not in this set is malformed and should be flagged.
|
||||
*/
|
||||
export const VALID_CONNECTION_TYPES = new Set<string>([
|
||||
'main',
|
||||
'error',
|
||||
...AI_CONNECTION_TYPES,
|
||||
// Additional AI types from n8n-workflow NodeConnectionTypes not in AI_CONNECTION_TYPES
|
||||
'ai_agent',
|
||||
'ai_chain',
|
||||
'ai_retriever',
|
||||
'ai_reranker',
|
||||
]);
|
||||
|
||||
interface WorkflowNode {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -40,9 +55,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<{ node: string; type: string; index: number }>>;
|
||||
[outputType: string]: Array<Array<{ node: string; type: string; index: number }>>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -612,86 +625,52 @@ export class WorkflowValidator {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check main outputs
|
||||
if (outputs.main) {
|
||||
this.validateConnectionOutputs(
|
||||
sourceName,
|
||||
outputs.main,
|
||||
nodeMap,
|
||||
nodeIdMap,
|
||||
result,
|
||||
'main'
|
||||
);
|
||||
}
|
||||
// Detect unknown output keys and validate known ones
|
||||
for (const [outputKey, outputConnections] of Object.entries(outputs)) {
|
||||
if (!VALID_CONNECTION_TYPES.has(outputKey)) {
|
||||
// Flag unknown connection output key
|
||||
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: ${[...VALID_CONNECTION_TYPES].join(', ')}.${suggestion}`,
|
||||
code: 'UNKNOWN_CONNECTION_KEY'
|
||||
});
|
||||
result.statistics.invalidConnections++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check error outputs
|
||||
if (outputs.error) {
|
||||
this.validateConnectionOutputs(
|
||||
sourceName,
|
||||
outputs.error,
|
||||
nodeMap,
|
||||
nodeIdMap,
|
||||
result,
|
||||
'error'
|
||||
);
|
||||
}
|
||||
if (!outputConnections || !Array.isArray(outputConnections)) continue;
|
||||
|
||||
// Check AI tool outputs
|
||||
if (outputs.ai_tool) {
|
||||
// Validate that the source node can actually output ai_tool
|
||||
this.validateAIToolSource(sourceNode, result);
|
||||
if (outputKey === 'ai_tool') {
|
||||
this.validateAIToolSource(sourceNode, result);
|
||||
}
|
||||
|
||||
// Validate that AI sub-nodes are not connected via main
|
||||
if (outputKey === 'main') {
|
||||
this.validateNotAISubNode(sourceNode, result);
|
||||
}
|
||||
|
||||
this.validateConnectionOutputs(
|
||||
sourceName,
|
||||
outputs.ai_tool,
|
||||
outputConnections,
|
||||
nodeMap,
|
||||
nodeIdMap,
|
||||
result,
|
||||
'ai_tool'
|
||||
outputKey
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for orphaned nodes (not connected and not triggers)
|
||||
const connectedNodes = new Set<string>();
|
||||
|
||||
// Add all source nodes
|
||||
Object.keys(workflow.connections).forEach(name => connectedNodes.add(name));
|
||||
|
||||
// Add all target nodes
|
||||
Object.values(workflow.connections).forEach(outputs => {
|
||||
if (outputs.main) {
|
||||
outputs.main.flat().forEach(conn => {
|
||||
if (conn) connectedNodes.add(conn.node);
|
||||
});
|
||||
}
|
||||
if (outputs.error) {
|
||||
outputs.error.flat().forEach(conn => {
|
||||
if (conn) connectedNodes.add(conn.node);
|
||||
});
|
||||
}
|
||||
if (outputs.ai_tool) {
|
||||
outputs.ai_tool.flat().forEach(conn => {
|
||||
if (conn) connectedNodes.add(conn.node);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Check for orphaned nodes (exclude sticky notes)
|
||||
for (const node of workflow.nodes) {
|
||||
if (node.disabled || isNonExecutableNode(node.type)) continue;
|
||||
|
||||
// Use shared trigger detection function for consistency
|
||||
const isNodeTrigger = isTriggerNode(node.type);
|
||||
|
||||
if (!connectedNodes.has(node.name) && !isNodeTrigger) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'Node is not connected to any other nodes'
|
||||
});
|
||||
}
|
||||
// Trigger reachability analysis: BFS from all triggers to find unreachable nodes
|
||||
if (profile !== 'minimal') {
|
||||
this.validateTriggerReachability(workflow, result);
|
||||
} else {
|
||||
this.flagOrphanedNodes(workflow, result);
|
||||
}
|
||||
|
||||
// Check for cycles (skip in minimal profile to reduce false positives)
|
||||
@@ -712,19 +691,21 @@ export class WorkflowValidator {
|
||||
nodeMap: Map<string, WorkflowNode>,
|
||||
nodeIdMap: Map<string, WorkflowNode>,
|
||||
result: WorkflowValidationResult,
|
||||
outputType: 'main' | 'error' | 'ai_tool'
|
||||
outputType: string
|
||||
): void {
|
||||
// Get source node for special validation
|
||||
const sourceNode = nodeMap.get(sourceName);
|
||||
|
||||
// Special validation for main outputs with error handling
|
||||
// Main-output-specific validation: error handling config and index bounds
|
||||
if (outputType === 'main' && sourceNode) {
|
||||
this.validateErrorOutputConfiguration(sourceName, sourceNode, outputs, nodeMap, result);
|
||||
this.validateOutputIndexBounds(sourceNode, outputs, result);
|
||||
this.validateConditionalBranchUsage(sourceNode, outputs, result);
|
||||
}
|
||||
|
||||
|
||||
outputs.forEach((outputConnections, outputIndex) => {
|
||||
if (!outputConnections) return;
|
||||
|
||||
|
||||
outputConnections.forEach(connection => {
|
||||
// Check for negative index
|
||||
if (connection.index < 0) {
|
||||
@@ -736,6 +717,22 @@ export class WorkflowValidator {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate connection type field
|
||||
if (connection.type && !VALID_CONNECTION_TYPES.has(connection.type)) {
|
||||
let suggestion = '';
|
||||
if (/^\d+$/.test(connection.type)) {
|
||||
suggestion = ` Numeric types are not valid - use "main", "error", or an AI connection type.`;
|
||||
}
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeName: sourceName,
|
||||
message: `Invalid connection type "${connection.type}" in connection from "${sourceName}" to "${connection.node}". Expected "main", "error", or an AI connection type (ai_tool, ai_languageModel, etc.).${suggestion}`,
|
||||
code: 'INVALID_CONNECTION_TYPE'
|
||||
});
|
||||
result.statistics.invalidConnections++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Special validation for SplitInBatches node
|
||||
// Check both full form (n8n-nodes-base.*) and short form (nodes-base.*)
|
||||
const isSplitInBatches = sourceNode && (
|
||||
@@ -789,11 +786,16 @@ export class WorkflowValidator {
|
||||
});
|
||||
} else {
|
||||
result.statistics.validConnections++;
|
||||
|
||||
|
||||
// Additional validation for AI tool connections
|
||||
if (outputType === 'ai_tool') {
|
||||
this.validateAIToolConnection(sourceName, targetNode, result);
|
||||
}
|
||||
|
||||
// Input index bounds checking
|
||||
if (outputType === 'main') {
|
||||
this.validateInputIndexBounds(sourceName, targetNode, connection, result);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -991,6 +993,348 @@ export class WorkflowValidator {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the static output types for a node from the database.
|
||||
* Returns null if outputs contain expressions (dynamic) or node not found.
|
||||
*/
|
||||
private getNodeOutputTypes(nodeType: string): string[] | null {
|
||||
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
||||
const nodeInfo = this.nodeRepository.getNode(normalizedType);
|
||||
if (!nodeInfo || !nodeInfo.outputs) return null;
|
||||
|
||||
const outputs = nodeInfo.outputs;
|
||||
if (!Array.isArray(outputs)) return null;
|
||||
|
||||
// Skip if any output is an expression (dynamic — can't determine statically)
|
||||
for (const output of outputs) {
|
||||
if (typeof output === 'string' && output.startsWith('={{')) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that AI sub-nodes (nodes that only output AI connection types)
|
||||
* are not connected via "main" connections.
|
||||
*/
|
||||
private validateNotAISubNode(
|
||||
sourceNode: WorkflowNode,
|
||||
result: WorkflowValidationResult
|
||||
): void {
|
||||
const outputTypes = this.getNodeOutputTypes(sourceNode.type);
|
||||
if (!outputTypes) return; // Unknown or dynamic — skip
|
||||
|
||||
// Check if the node outputs ONLY AI types (no 'main')
|
||||
const hasMainOutput = outputTypes.some(t => t === 'main');
|
||||
if (hasMainOutput) return; // Node can legitimately output main
|
||||
|
||||
// All outputs are AI types — this node should not be connected via main
|
||||
const aiTypes = outputTypes.filter(t => t !== 'main');
|
||||
const expectedType = aiTypes[0] || 'ai_languageModel';
|
||||
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: sourceNode.id,
|
||||
nodeName: sourceNode.name,
|
||||
message: `Node "${sourceNode.name}" (${sourceNode.type}) is an AI sub-node that outputs "${expectedType}" connections. ` +
|
||||
`It cannot be used with "main" connections. Connect it to an AI Agent or Chain via "${expectedType}" instead.`,
|
||||
code: 'AI_SUBNODE_MAIN_CONNECTION'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the short node type name (e.g., "if", "switch", "set") from a workflow node.
|
||||
*/
|
||||
private getShortNodeType(sourceNode: WorkflowNode): string {
|
||||
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(sourceNode.type);
|
||||
return normalizedType.replace(/^(n8n-)?nodes-base\./, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expected main output count for a conditional node (IF, Filter, Switch).
|
||||
* Returns null for non-conditional nodes or when the count cannot be determined.
|
||||
*/
|
||||
private getConditionalOutputInfo(sourceNode: WorkflowNode): { shortType: string; expectedOutputs: number } | null {
|
||||
const shortType = this.getShortNodeType(sourceNode);
|
||||
|
||||
if (shortType === 'if' || shortType === 'filter') {
|
||||
return { shortType, expectedOutputs: 2 };
|
||||
}
|
||||
if (shortType === 'switch') {
|
||||
const rules = sourceNode.parameters?.rules?.values || sourceNode.parameters?.rules;
|
||||
if (Array.isArray(rules)) {
|
||||
return { shortType, expectedOutputs: rules.length + 1 }; // rules + fallback
|
||||
}
|
||||
return null; // Cannot determine dynamic output count
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that output indices don't exceed what the node type supports.
|
||||
*/
|
||||
private validateOutputIndexBounds(
|
||||
sourceNode: WorkflowNode,
|
||||
outputs: Array<Array<{ node: string; type: string; index: number }>>,
|
||||
result: WorkflowValidationResult
|
||||
): void {
|
||||
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(sourceNode.type);
|
||||
const nodeInfo = this.nodeRepository.getNode(normalizedType);
|
||||
if (!nodeInfo || !nodeInfo.outputs) return;
|
||||
|
||||
// Count main outputs from node description
|
||||
let mainOutputCount: number;
|
||||
if (Array.isArray(nodeInfo.outputs)) {
|
||||
// outputs can be strings like "main" or objects with { type: "main" }
|
||||
mainOutputCount = nodeInfo.outputs.filter((o: any) =>
|
||||
typeof o === 'string' ? o === 'main' : (o.type === 'main' || !o.type)
|
||||
).length;
|
||||
} else {
|
||||
return; // Dynamic outputs (expression string), skip check
|
||||
}
|
||||
|
||||
if (mainOutputCount === 0) return;
|
||||
|
||||
// Override with dynamic output counts for conditional nodes
|
||||
const conditionalInfo = this.getConditionalOutputInfo(sourceNode);
|
||||
if (conditionalInfo) {
|
||||
mainOutputCount = conditionalInfo.expectedOutputs;
|
||||
} else if (this.getShortNodeType(sourceNode) === 'switch') {
|
||||
// Switch without determinable rules -- skip bounds check
|
||||
return;
|
||||
}
|
||||
|
||||
// Account for continueErrorOutput adding an extra output
|
||||
if (sourceNode.onError === 'continueErrorOutput') {
|
||||
mainOutputCount += 1;
|
||||
}
|
||||
|
||||
// Check if any output index exceeds bounds
|
||||
const maxOutputIndex = outputs.length - 1;
|
||||
if (maxOutputIndex >= mainOutputCount) {
|
||||
// Only flag if there are actual connections at the out-of-bounds indices
|
||||
for (let i = mainOutputCount; i < outputs.length; i++) {
|
||||
if (outputs[i] && outputs[i].length > 0) {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: sourceNode.id,
|
||||
nodeName: sourceNode.name,
|
||||
message: `Output index ${i} on node "${sourceNode.name}" exceeds its output count (${mainOutputCount}). ` +
|
||||
`This node has ${mainOutputCount} main output(s) (indices 0-${mainOutputCount - 1}).`,
|
||||
code: 'OUTPUT_INDEX_OUT_OF_BOUNDS'
|
||||
});
|
||||
result.statistics.invalidConnections++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect when a conditional node (IF, Filter, Switch) has all connections
|
||||
* crammed into main[0] with higher-index outputs empty. This usually means
|
||||
* both branches execute together on one condition, while the other branches
|
||||
* have no effect.
|
||||
*/
|
||||
private validateConditionalBranchUsage(
|
||||
sourceNode: WorkflowNode,
|
||||
outputs: Array<Array<{ node: string; type: string; index: number }>>,
|
||||
result: WorkflowValidationResult
|
||||
): void {
|
||||
const conditionalInfo = this.getConditionalOutputInfo(sourceNode);
|
||||
if (!conditionalInfo || conditionalInfo.expectedOutputs < 2) return;
|
||||
|
||||
const { shortType, expectedOutputs } = conditionalInfo;
|
||||
|
||||
// Check: main[0] has >= 2 connections AND all main[1+] are empty
|
||||
const main0Count = outputs[0]?.length || 0;
|
||||
if (main0Count < 2) return;
|
||||
|
||||
const hasHigherIndexConnections = outputs.slice(1).some(
|
||||
conns => conns && conns.length > 0
|
||||
);
|
||||
if (hasHigherIndexConnections) return;
|
||||
|
||||
// Build a context-appropriate warning message
|
||||
let message: string;
|
||||
if (shortType === 'if' || shortType === 'filter') {
|
||||
const isFilter = shortType === 'filter';
|
||||
const displayName = isFilter ? 'Filter' : 'IF';
|
||||
const trueLabel = isFilter ? 'matched' : 'true';
|
||||
const falseLabel = isFilter ? 'unmatched' : 'false';
|
||||
message = `${displayName} node "${sourceNode.name}" has ${main0Count} connections on the "${trueLabel}" branch (main[0]) ` +
|
||||
`but no connections on the "${falseLabel}" branch (main[1]). ` +
|
||||
`All ${main0Count} target nodes execute together on the "${trueLabel}" branch, ` +
|
||||
`while the "${falseLabel}" branch has no effect. ` +
|
||||
`Split connections: main[0] for ${trueLabel}, main[1] for ${falseLabel}.`;
|
||||
} else {
|
||||
message = `Switch node "${sourceNode.name}" has ${main0Count} connections on output 0 ` +
|
||||
`but no connections on any other outputs (1-${expectedOutputs - 1}). ` +
|
||||
`All ${main0Count} target nodes execute together on output 0, ` +
|
||||
`while other switch branches have no effect. ` +
|
||||
`Distribute connections across outputs to match switch rules.`;
|
||||
}
|
||||
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: sourceNode.id,
|
||||
nodeName: sourceNode.name,
|
||||
message,
|
||||
code: 'CONDITIONAL_BRANCH_FANOUT'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that input index doesn't exceed what the target node accepts.
|
||||
*/
|
||||
private validateInputIndexBounds(
|
||||
sourceName: string,
|
||||
targetNode: WorkflowNode,
|
||||
connection: { node: string; type: string; index: number },
|
||||
result: WorkflowValidationResult
|
||||
): void {
|
||||
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(targetNode.type);
|
||||
const nodeInfo = this.nodeRepository.getNode(normalizedType);
|
||||
if (!nodeInfo) return;
|
||||
|
||||
// Most nodes have 1 main input. Known exceptions:
|
||||
const shortType = normalizedType.replace(/^(n8n-)?nodes-base\./, '');
|
||||
let mainInputCount = 1; // Default: most nodes have 1 input
|
||||
|
||||
if (shortType === 'merge' || shortType === 'compareDatasets') {
|
||||
mainInputCount = 2; // Merge nodes have 2 inputs
|
||||
}
|
||||
|
||||
// Trigger nodes have 0 inputs
|
||||
if (nodeInfo.isTrigger || isTriggerNode(targetNode.type)) {
|
||||
mainInputCount = 0;
|
||||
}
|
||||
|
||||
if (mainInputCount > 0 && connection.index >= mainInputCount) {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeName: targetNode.name,
|
||||
message: `Input index ${connection.index} on node "${targetNode.name}" exceeds its input count (${mainInputCount}). ` +
|
||||
`Connection from "${sourceName}" targets input ${connection.index}, but this node has ${mainInputCount} main input(s) (indices 0-${mainInputCount - 1}).`,
|
||||
code: 'INPUT_INDEX_OUT_OF_BOUNDS'
|
||||
});
|
||||
result.statistics.invalidConnections++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag nodes that are not referenced in any connection (source or target).
|
||||
* Used as a lightweight check when BFS reachability is not applicable.
|
||||
*/
|
||||
private flagOrphanedNodes(
|
||||
workflow: WorkflowJson,
|
||||
result: WorkflowValidationResult
|
||||
): void {
|
||||
const connectedNodes = new Set<string>();
|
||||
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
|
||||
connectedNodes.add(sourceName);
|
||||
for (const outputConns of Object.values(outputs)) {
|
||||
if (!Array.isArray(outputConns)) continue;
|
||||
for (const conns of outputConns) {
|
||||
if (!conns) continue;
|
||||
for (const conn of conns) {
|
||||
if (conn) connectedNodes.add(conn.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of workflow.nodes) {
|
||||
if (node.disabled || isNonExecutableNode(node.type)) continue;
|
||||
if (isTriggerNode(node.type)) continue;
|
||||
if (!connectedNodes.has(node.name)) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'Node is not connected to any other nodes'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BFS from all trigger nodes to detect unreachable nodes.
|
||||
* Replaces the simple "is node in any connection" check with proper graph traversal.
|
||||
*/
|
||||
private validateTriggerReachability(
|
||||
workflow: WorkflowJson,
|
||||
result: WorkflowValidationResult
|
||||
): void {
|
||||
// Build adjacency list (forward direction)
|
||||
const adjacency = new Map<string, Set<string>>();
|
||||
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
|
||||
if (!adjacency.has(sourceName)) adjacency.set(sourceName, new Set());
|
||||
for (const outputConns of Object.values(outputs)) {
|
||||
if (Array.isArray(outputConns)) {
|
||||
for (const conns of outputConns) {
|
||||
if (!conns) continue;
|
||||
for (const conn of conns) {
|
||||
if (conn) {
|
||||
adjacency.get(sourceName)!.add(conn.node);
|
||||
// Also track that the target exists in the graph
|
||||
if (!adjacency.has(conn.node)) adjacency.set(conn.node, new Set());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Identify trigger nodes
|
||||
const triggerNodes: string[] = [];
|
||||
for (const node of workflow.nodes) {
|
||||
if (isTriggerNode(node.type) && !node.disabled) {
|
||||
triggerNodes.push(node.name);
|
||||
}
|
||||
}
|
||||
|
||||
// If no trigger nodes, fall back to simple orphaned check
|
||||
if (triggerNodes.length === 0) {
|
||||
this.flagOrphanedNodes(workflow, result);
|
||||
return;
|
||||
}
|
||||
|
||||
// BFS from all trigger nodes
|
||||
const reachable = new Set<string>();
|
||||
const queue: string[] = [...triggerNodes];
|
||||
for (const t of triggerNodes) reachable.add(t);
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
const neighbors = adjacency.get(current);
|
||||
if (neighbors) {
|
||||
for (const neighbor of neighbors) {
|
||||
if (!reachable.has(neighbor)) {
|
||||
reachable.add(neighbor);
|
||||
queue.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flag unreachable nodes
|
||||
for (const node of workflow.nodes) {
|
||||
if (node.disabled || isNonExecutableNode(node.type)) continue;
|
||||
if (isTriggerNode(node.type)) continue;
|
||||
|
||||
if (!reachable.has(node.name)) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'Node is not reachable from any trigger node'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if workflow has cycles
|
||||
* Allow legitimate loops for SplitInBatches and similar loop nodes
|
||||
@@ -1024,23 +1368,13 @@ export class WorkflowValidator {
|
||||
const connections = workflow.connections[nodeName];
|
||||
if (connections) {
|
||||
const allTargets: string[] = [];
|
||||
|
||||
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);
|
||||
|
||||
@@ -311,6 +311,7 @@ export interface WebhookRequest {
|
||||
// MCP Tool Response Type
|
||||
export interface McpToolResponse {
|
||||
success: boolean;
|
||||
saved?: boolean;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
message?: string;
|
||||
@@ -318,6 +319,7 @@ export interface McpToolResponse {
|
||||
details?: Record<string, unknown>;
|
||||
executionId?: string;
|
||||
workflowId?: string;
|
||||
operationsApplied?: number;
|
||||
}
|
||||
|
||||
// Execution Filtering Types
|
||||
|
||||
@@ -190,6 +190,8 @@ export interface WorkflowDiffResult {
|
||||
staleConnectionsRemoved?: Array<{ from: string; to: string }>; // For cleanStaleConnections operation
|
||||
shouldActivate?: boolean; // Flag to activate workflow after update (for activateWorkflow operation)
|
||||
shouldDeactivate?: boolean; // Flag to deactivate workflow after update (for deactivateWorkflow operation)
|
||||
tagsToAdd?: string[];
|
||||
tagsToRemove?: string[];
|
||||
}
|
||||
|
||||
// Helper type for node reference (supports both ID and name)
|
||||
|
||||
@@ -172,14 +172,14 @@ export function isTriggerNode(nodeType: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for specific trigger types that don't have 'trigger' in their name
|
||||
const specificTriggers = [
|
||||
'nodes-base.start',
|
||||
'nodes-base.manualTrigger',
|
||||
'nodes-base.formTrigger'
|
||||
];
|
||||
// Check for polling-based triggers that don't have 'trigger' in their name
|
||||
if (lowerType.includes('emailread') || lowerType.includes('emailreadimap')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return specificTriggers.includes(normalized);
|
||||
// Check for specific trigger types that don't have 'trigger' in their name
|
||||
// (manualTrigger and formTrigger are already caught by the 'trigger' check above)
|
||||
return normalized === 'nodes-base.start';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('Database Performance Tests', () => {
|
||||
// Adjusted based on actual CI performance measurements + type safety overhead
|
||||
// CI environments show ratios of ~7-10 for 1000:100 and ~6-7 for 5000:1000
|
||||
// Increased thresholds to account for community node columns (8 additional fields)
|
||||
expect(ratio1000to100).toBeLessThan(15); // Allow for CI variability + community columns (was 12)
|
||||
expect(ratio1000to100).toBeLessThan(20); // Allow for CI variability + community columns (was 15)
|
||||
expect(ratio5000to1000).toBeLessThan(12); // Allow for type safety overhead + community columns (was 11)
|
||||
});
|
||||
|
||||
|
||||
@@ -105,21 +105,14 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
describe('Message Format Validation', () => {
|
||||
it('should reject messages without method', async () => {
|
||||
// Test by sending raw message through transport
|
||||
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
|
||||
const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
|
||||
|
||||
await mcpServer.connectToTransport(serverTransport);
|
||||
await testClient.connect(clientTransport);
|
||||
|
||||
// MCP SDK 1.27+ enforces single-connection per Server instance,
|
||||
// so use the existing client from beforeEach instead of a new one.
|
||||
try {
|
||||
// This should fail as MCP SDK validates method
|
||||
await (testClient as any).request({ method: '', params: {} });
|
||||
await (client as any).request({ method: '', params: {} });
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
} finally {
|
||||
await testClient.close();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -250,10 +243,15 @@ describe('MCP Protocol Compliance', () => {
|
||||
|
||||
describe('Transport Layer', () => {
|
||||
it('should handle transport disconnection gracefully', async () => {
|
||||
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
|
||||
const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
|
||||
// Use a dedicated server instance so we don't conflict with the
|
||||
// shared mcpServer that beforeEach already connected a transport to.
|
||||
const dedicatedServer = new TestableN8NMCPServer();
|
||||
await dedicatedServer.initialize();
|
||||
|
||||
await mcpServer.connectToTransport(serverTransport);
|
||||
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
|
||||
await dedicatedServer.connectToTransport(serverTransport);
|
||||
|
||||
const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
|
||||
await testClient.connect(clientTransport);
|
||||
|
||||
// Make a request
|
||||
@@ -270,6 +268,8 @@ describe('MCP Protocol Compliance', () => {
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
|
||||
await dedicatedServer.close();
|
||||
});
|
||||
|
||||
it('should handle multiple sequential connections', async () => {
|
||||
|
||||
@@ -73,10 +73,11 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
const serverInfo = await client.getServerVersion();
|
||||
expect(serverInfo).toBeDefined();
|
||||
expect(serverInfo?.name).toBe('n8n-documentation-mcp');
|
||||
|
||||
// Check capabilities if they exist
|
||||
if (serverInfo?.capabilities) {
|
||||
expect(serverInfo.capabilities).toHaveProperty('tools');
|
||||
|
||||
// Check capabilities via the dedicated method
|
||||
const capabilities = client.getServerCapabilities();
|
||||
if (capabilities) {
|
||||
expect(capabilities).toHaveProperty('tools');
|
||||
}
|
||||
|
||||
// Clean up - ensure proper order
|
||||
@@ -340,9 +341,9 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
it('should handle different client versions', async () => {
|
||||
const mcpServer = new TestableN8NMCPServer();
|
||||
await mcpServer.initialize();
|
||||
|
||||
const clients = [];
|
||||
|
||||
// MCP SDK 1.27+ enforces single-connection per Server instance,
|
||||
// so we test each version sequentially rather than concurrently.
|
||||
for (const version of ['1.0.0', '1.1.0', '2.0.0']) {
|
||||
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
|
||||
await mcpServer.connectToTransport(serverTransport);
|
||||
@@ -353,21 +354,14 @@ describe('MCP Session Management', { timeout: 15000 }, () => {
|
||||
}, {});
|
||||
|
||||
await client.connect(clientTransport);
|
||||
clients.push(client);
|
||||
|
||||
const info = await client.getServerVersion();
|
||||
expect(info!.name).toBe('n8n-documentation-mcp');
|
||||
|
||||
await client.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
// All versions should work
|
||||
const responses = await Promise.all(
|
||||
clients.map(client => client.getServerVersion())
|
||||
);
|
||||
|
||||
responses.forEach(info => {
|
||||
expect(info!.name).toBe('n8n-documentation-mcp');
|
||||
});
|
||||
|
||||
// Clean up
|
||||
await Promise.all(clients.map(client => client.close()));
|
||||
await new Promise(resolve => setTimeout(resolve, 100)); // Give time for all clients to fully close
|
||||
await mcpServer.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
InitializeRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
@@ -14,18 +14,30 @@ export class TestableN8NMCPServer {
|
||||
private mcpServer: N8NDocumentationMCPServer;
|
||||
private server: Server;
|
||||
private transports = new Set<Transport>();
|
||||
private connections = new Set<any>();
|
||||
private static instanceCount = 0;
|
||||
private testDbPath: string;
|
||||
|
||||
constructor() {
|
||||
// Use a unique test database for each instance to avoid conflicts
|
||||
// This prevents concurrent test issues with database locking
|
||||
const instanceId = TestableN8NMCPServer.instanceCount++;
|
||||
this.testDbPath = `/tmp/n8n-mcp-test-${process.pid}-${instanceId}.db`;
|
||||
// Use path.resolve to produce a canonical absolute path so the shared
|
||||
// database singleton always sees the exact same string, preventing
|
||||
// "Shared database already initialized with different path" errors.
|
||||
const path = require('path');
|
||||
this.testDbPath = path.resolve(process.cwd(), 'data', 'nodes.db');
|
||||
process.env.NODE_DB_PATH = this.testDbPath;
|
||||
|
||||
this.server = new Server({
|
||||
|
||||
this.server = this.createServer();
|
||||
|
||||
this.mcpServer = new N8NDocumentationMCPServer();
|
||||
this.setupHandlers(this.server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fresh MCP SDK Server instance.
|
||||
* MCP SDK 1.27+ enforces single-connection per Protocol instance,
|
||||
* so we create a new one each time we need to connect to a transport.
|
||||
*/
|
||||
private createServer(): Server {
|
||||
return new Server({
|
||||
name: 'n8n-documentation-mcp',
|
||||
version: '1.0.0'
|
||||
}, {
|
||||
@@ -33,14 +45,11 @@ export class TestableN8NMCPServer {
|
||||
tools: {}
|
||||
}
|
||||
});
|
||||
|
||||
this.mcpServer = new N8NDocumentationMCPServer();
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private setupHandlers() {
|
||||
private setupHandlers(server: Server) {
|
||||
// Initialize handler
|
||||
this.server.setRequestHandler(InitializeRequestSchema, async () => {
|
||||
server.setRequestHandler(InitializeRequestSchema, async () => {
|
||||
return {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
@@ -54,27 +63,27 @@ export class TestableN8NMCPServer {
|
||||
});
|
||||
|
||||
// List tools handler
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
// Import the tools directly from the tools module
|
||||
const { n8nDocumentationToolsFinal } = await import('../../../src/mcp/tools');
|
||||
const { n8nManagementTools } = await import('../../../src/mcp/tools-n8n-manager');
|
||||
const { isN8nApiConfigured } = await import('../../../src/config/n8n-api');
|
||||
|
||||
|
||||
// Combine documentation tools with management tools if API is configured
|
||||
const tools = [...n8nDocumentationToolsFinal];
|
||||
if (isN8nApiConfigured()) {
|
||||
tools.push(...n8nManagementTools);
|
||||
}
|
||||
|
||||
|
||||
return { tools };
|
||||
});
|
||||
|
||||
// Call tool handler
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
// The mcpServer.executeTool returns raw data, we need to wrap it in the MCP response format
|
||||
const result = await this.mcpServer.executeTool(request.params.name, request.params.arguments || {});
|
||||
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@@ -98,21 +107,8 @@ export class TestableN8NMCPServer {
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
// Copy production database to test location for realistic testing
|
||||
try {
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
const prodDbPath = path.join(process.cwd(), 'data', 'nodes.db');
|
||||
|
||||
if (await fs.promises.access(prodDbPath).then(() => true).catch(() => false)) {
|
||||
await fs.promises.copyFile(prodDbPath, this.testDbPath);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore copy errors, database will be created fresh
|
||||
}
|
||||
|
||||
// The MCP server initializes its database lazily
|
||||
// We can trigger initialization by calling executeTool
|
||||
// The MCP server initializes its database lazily via the shared
|
||||
// database singleton. Trigger initialization by calling executeTool.
|
||||
try {
|
||||
await this.mcpServer.executeTool('tools_documentation', {});
|
||||
} catch (error) {
|
||||
@@ -125,20 +121,26 @@ export class TestableN8NMCPServer {
|
||||
if (!transport || typeof transport !== 'object') {
|
||||
throw new Error('Invalid transport provided');
|
||||
}
|
||||
|
||||
// Set up any missing transport handlers to prevent "Cannot set properties of undefined" errors
|
||||
if (transport && typeof transport === 'object') {
|
||||
const transportAny = transport as any;
|
||||
if (transportAny.serverTransport && !transportAny.serverTransport.onclose) {
|
||||
transportAny.serverTransport.onclose = () => {};
|
||||
}
|
||||
|
||||
// MCP SDK 1.27+ enforces single-connection per Protocol instance.
|
||||
// Close the current server and create a fresh one so that _transport
|
||||
// is guaranteed to be undefined. Reusing the same Server after close()
|
||||
// is unreliable because _transport is cleared asynchronously via the
|
||||
// transport onclose callback chain, which can fail in CI.
|
||||
try {
|
||||
await this.server.close();
|
||||
} catch {
|
||||
// Ignore errors during cleanup of previous transport
|
||||
}
|
||||
|
||||
|
||||
// Create a brand-new Server instance for this connection
|
||||
this.server = this.createServer();
|
||||
this.setupHandlers(this.server);
|
||||
|
||||
// Track this transport for cleanup
|
||||
this.transports.add(transport);
|
||||
|
||||
const connection = await this.server.connect(transport);
|
||||
this.connections.add(connection);
|
||||
|
||||
await this.server.connect(transport);
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
@@ -151,78 +153,47 @@ export class TestableN8NMCPServer {
|
||||
});
|
||||
|
||||
const performClose = async () => {
|
||||
// Close all connections first with timeout protection
|
||||
const connectionPromises = Array.from(this.connections).map(async (connection) => {
|
||||
const connTimeout = new Promise<void>((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
try {
|
||||
if (connection && typeof connection.close === 'function') {
|
||||
await Promise.race([connection.close(), connTimeout]);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors during connection cleanup
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(connectionPromises);
|
||||
this.connections.clear();
|
||||
|
||||
// Close the MCP SDK Server (resets _transport via _onclose)
|
||||
try {
|
||||
await this.server.close();
|
||||
} catch {
|
||||
// Ignore errors during server close
|
||||
}
|
||||
|
||||
// Shut down the inner N8NDocumentationMCPServer to release the
|
||||
// shared database reference and prevent resource leaks.
|
||||
try {
|
||||
await this.mcpServer.shutdown();
|
||||
} catch {
|
||||
// Ignore errors during inner server shutdown
|
||||
}
|
||||
|
||||
// Close all tracked transports with timeout protection
|
||||
const transportPromises: Promise<void>[] = [];
|
||||
|
||||
|
||||
for (const transport of this.transports) {
|
||||
const transportTimeout = new Promise<void>((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
|
||||
try {
|
||||
// Force close all transports
|
||||
const transportAny = transport as any;
|
||||
|
||||
// Try different close methods
|
||||
if (transportAny.close && typeof transportAny.close === 'function') {
|
||||
transportPromises.push(
|
||||
Promise.race([transportAny.close(), transportTimeout])
|
||||
);
|
||||
}
|
||||
if (transportAny.serverTransport?.close) {
|
||||
transportPromises.push(
|
||||
Promise.race([transportAny.serverTransport.close(), transportTimeout])
|
||||
);
|
||||
}
|
||||
if (transportAny.clientTransport?.close) {
|
||||
transportPromises.push(
|
||||
Promise.race([transportAny.clientTransport.close(), transportTimeout])
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Ignore errors during transport cleanup
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all transports to close with timeout
|
||||
|
||||
await Promise.allSettled(transportPromises);
|
||||
|
||||
// Clear the transports set
|
||||
this.transports.clear();
|
||||
|
||||
// Don't shut down the shared MCP server instance
|
||||
};
|
||||
|
||||
// Race between actual close and timeout
|
||||
await Promise.race([performClose(), closeTimeout]);
|
||||
|
||||
// Clean up test database
|
||||
if (this.testDbPath) {
|
||||
try {
|
||||
const fs = await import('fs');
|
||||
await fs.promises.unlink(this.testDbPath).catch(() => {});
|
||||
await fs.promises.unlink(`${this.testDbPath}-shm`).catch(() => {});
|
||||
await fs.promises.unlink(`${this.testDbPath}-wal`).catch(() => {});
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static async shutdownShared(): Promise<void> {
|
||||
if (sharedMcpServer) {
|
||||
await sharedMcpServer.shutdown();
|
||||
|
||||
@@ -41,6 +41,7 @@ describe('DocumentationGenerator', () => {
|
||||
apiKey: 'test-key',
|
||||
timeout: 30000,
|
||||
maxTokens: 1000,
|
||||
temperature: 0.3,
|
||||
};
|
||||
|
||||
const validSummary = {
|
||||
@@ -163,7 +164,7 @@ describe('DocumentationGenerator', () => {
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith({
|
||||
model: 'test-model',
|
||||
max_tokens: 1000,
|
||||
max_completion_tokens: 1000,
|
||||
temperature: 0.3,
|
||||
messages: expect.arrayContaining([
|
||||
expect.objectContaining({ role: 'system' }),
|
||||
@@ -680,7 +681,7 @@ describe('DocumentationGenerator', () => {
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
max_tokens: 10,
|
||||
max_completion_tokens: 200,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
|
||||
300
tests/unit/mcp/coerce-stringified-params.test.ts
Normal file
300
tests/unit/mcp/coerce-stringified-params.test.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
|
||||
|
||||
// Mock the database and dependencies
|
||||
vi.mock('../../../src/database/database-adapter');
|
||||
vi.mock('../../../src/database/node-repository');
|
||||
vi.mock('../../../src/templates/template-service');
|
||||
vi.mock('../../../src/utils/logger');
|
||||
|
||||
class TestableN8NMCPServer extends N8NDocumentationMCPServer {
|
||||
public testCoerceStringifiedJsonParams(
|
||||
toolName: string,
|
||||
args: Record<string, any>
|
||||
): Record<string, any> {
|
||||
return (this as any).coerceStringifiedJsonParams(toolName, args);
|
||||
}
|
||||
}
|
||||
|
||||
describe('coerceStringifiedJsonParams', () => {
|
||||
let server: TestableN8NMCPServer;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.NODE_DB_PATH = ':memory:';
|
||||
server = new TestableN8NMCPServer();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.NODE_DB_PATH;
|
||||
});
|
||||
|
||||
describe('Object coercion', () => {
|
||||
it('should coerce stringified object for validate_node config', () => {
|
||||
const args = {
|
||||
nodeType: 'nodes-base.slack',
|
||||
config: '{"resource":"channel","operation":"create"}'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('validate_node', args);
|
||||
expect(result.config).toEqual({ resource: 'channel', operation: 'create' });
|
||||
expect(result.nodeType).toBe('nodes-base.slack');
|
||||
});
|
||||
|
||||
it('should coerce stringified object for n8n_create_workflow connections', () => {
|
||||
const connections = { 'Webhook': { main: [[{ node: 'Slack', type: 'main', index: 0 }]] } };
|
||||
const args = {
|
||||
name: 'Test Workflow',
|
||||
nodes: [{ id: '1', name: 'Webhook', type: 'n8n-nodes-base.webhook' }],
|
||||
connections: JSON.stringify(connections)
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('n8n_create_workflow', args);
|
||||
expect(result.connections).toEqual(connections);
|
||||
});
|
||||
|
||||
it('should coerce stringified object for validate_workflow workflow param', () => {
|
||||
const workflow = { nodes: [], connections: {} };
|
||||
const args = {
|
||||
workflow: JSON.stringify(workflow)
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('validate_workflow', args);
|
||||
expect(result.workflow).toEqual(workflow);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Array coercion', () => {
|
||||
it('should coerce stringified array for n8n_update_partial_workflow operations', () => {
|
||||
const operations = [
|
||||
{ type: 'addNode', node: { id: '1', name: 'Test', type: 'n8n-nodes-base.noOp' } }
|
||||
];
|
||||
const args = {
|
||||
id: '123',
|
||||
operations: JSON.stringify(operations)
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('n8n_update_partial_workflow', args);
|
||||
expect(result.operations).toEqual(operations);
|
||||
expect(result.id).toBe('123');
|
||||
});
|
||||
|
||||
it('should coerce stringified array for n8n_autofix_workflow fixTypes', () => {
|
||||
const fixTypes = ['expression-format', 'typeversion-correction'];
|
||||
const args = {
|
||||
id: '456',
|
||||
fixTypes: JSON.stringify(fixTypes)
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('n8n_autofix_workflow', args);
|
||||
expect(result.fixTypes).toEqual(fixTypes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('No-op cases', () => {
|
||||
it('should not modify object params that are already objects', () => {
|
||||
const config = { resource: 'channel', operation: 'create' };
|
||||
const args = {
|
||||
nodeType: 'nodes-base.slack',
|
||||
config
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('validate_node', args);
|
||||
expect(result.config).toEqual(config);
|
||||
expect(result.config).toBe(config); // same reference
|
||||
});
|
||||
|
||||
it('should not modify string params even if they contain JSON', () => {
|
||||
const args = {
|
||||
query: '{"some":"json"}',
|
||||
limit: 10
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
|
||||
expect(result.query).toBe('{"some":"json"}');
|
||||
});
|
||||
|
||||
it('should not modify args for tools with no object/array params', () => {
|
||||
const args = {
|
||||
query: 'webhook',
|
||||
limit: 20,
|
||||
mode: 'OR'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
|
||||
expect(result).toEqual(args);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Safety cases', () => {
|
||||
it('should keep original string for invalid JSON', () => {
|
||||
const args = {
|
||||
nodeType: 'nodes-base.slack',
|
||||
config: '{invalid json here}'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('validate_node', args);
|
||||
expect(result.config).toBe('{invalid json here}');
|
||||
});
|
||||
|
||||
it('should not attempt parse when object param starts with [', () => {
|
||||
const args = {
|
||||
nodeType: 'nodes-base.slack',
|
||||
config: '[1, 2, 3]'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('validate_node', args);
|
||||
expect(result.config).toBe('[1, 2, 3]');
|
||||
});
|
||||
|
||||
it('should not attempt parse when array param starts with {', () => {
|
||||
const args = {
|
||||
id: '123',
|
||||
operations: '{"not":"an array"}'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('n8n_update_partial_workflow', args);
|
||||
expect(result.operations).toBe('{"not":"an array"}');
|
||||
});
|
||||
|
||||
it('should handle null args gracefully', () => {
|
||||
const result = server.testCoerceStringifiedJsonParams('validate_node', null as any);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle undefined args gracefully', () => {
|
||||
const result = server.testCoerceStringifiedJsonParams('validate_node', undefined as any);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return args unchanged for unknown tool', () => {
|
||||
const args = { config: '{"key":"value"}' };
|
||||
const result = server.testCoerceStringifiedJsonParams('nonexistent_tool', args);
|
||||
expect(result).toEqual(args);
|
||||
expect(result.config).toBe('{"key":"value"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Number coercion', () => {
|
||||
it('should coerce string to number for search_nodes limit', () => {
|
||||
const args = {
|
||||
query: 'webhook',
|
||||
limit: '10'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
|
||||
expect(result.limit).toBe(10);
|
||||
expect(result.query).toBe('webhook');
|
||||
});
|
||||
|
||||
it('should coerce string to number for n8n_executions limit', () => {
|
||||
const args = {
|
||||
action: 'list',
|
||||
limit: '50'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('n8n_executions', args);
|
||||
expect(result.limit).toBe(50);
|
||||
});
|
||||
|
||||
it('should not coerce non-numeric string to number', () => {
|
||||
const args = {
|
||||
query: 'webhook',
|
||||
limit: 'abc'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
|
||||
expect(result.limit).toBe('abc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Boolean coercion', () => {
|
||||
it('should coerce "true" string to boolean', () => {
|
||||
const args = {
|
||||
query: 'webhook',
|
||||
includeExamples: 'true'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
|
||||
expect(result.includeExamples).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce "false" string to boolean', () => {
|
||||
const args = {
|
||||
query: 'webhook',
|
||||
includeExamples: 'false'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
|
||||
expect(result.includeExamples).toBe(false);
|
||||
});
|
||||
|
||||
it('should not coerce non-boolean string to boolean', () => {
|
||||
const args = {
|
||||
query: 'webhook',
|
||||
includeExamples: 'yes'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
|
||||
expect(result.includeExamples).toBe('yes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Number-to-string coercion', () => {
|
||||
it('should coerce number to string for n8n_get_workflow id', () => {
|
||||
const args = {
|
||||
id: 123,
|
||||
mode: 'minimal'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('n8n_get_workflow', args);
|
||||
expect(result.id).toBe('123');
|
||||
expect(result.mode).toBe('minimal');
|
||||
});
|
||||
|
||||
it('should coerce boolean to string when string expected', () => {
|
||||
const args = {
|
||||
id: true
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('n8n_get_workflow', args);
|
||||
expect(result.id).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('End-to-end Claude Desktop scenario', () => {
|
||||
it('should coerce all stringified params for n8n_create_workflow', () => {
|
||||
const nodes = [
|
||||
{
|
||||
id: 'webhook_1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: { httpMethod: 'POST', path: 'slack-notify' }
|
||||
},
|
||||
{
|
||||
id: 'slack_1',
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
typeVersion: 1,
|
||||
position: [450, 300],
|
||||
parameters: { resource: 'message', operation: 'post', channel: '#general' }
|
||||
}
|
||||
];
|
||||
const connections = {
|
||||
'Webhook': { main: [[{ node: 'Slack', type: 'main', index: 0 }]] }
|
||||
};
|
||||
const settings = { executionOrder: 'v1', timezone: 'America/New_York' };
|
||||
|
||||
// Simulate Claude Desktop sending all object/array params as strings
|
||||
const args = {
|
||||
name: 'Webhook to Slack',
|
||||
nodes: JSON.stringify(nodes),
|
||||
connections: JSON.stringify(connections),
|
||||
settings: JSON.stringify(settings)
|
||||
};
|
||||
|
||||
const result = server.testCoerceStringifiedJsonParams('n8n_create_workflow', args);
|
||||
|
||||
expect(result.name).toBe('Webhook to Slack');
|
||||
expect(result.nodes).toEqual(nodes);
|
||||
expect(result.connections).toEqual(connections);
|
||||
expect(result.settings).toEqual(settings);
|
||||
});
|
||||
|
||||
it('should handle mixed type mismatches from Claude Desktop', () => {
|
||||
// Simulate Claude Desktop sending object params as strings
|
||||
const args = {
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
config: '{"method":"GET","url":"https://example.com"}',
|
||||
mode: 'full',
|
||||
profile: 'ai-friendly'
|
||||
};
|
||||
const result = server.testCoerceStringifiedJsonParams('validate_node', args);
|
||||
expect(result.config).toEqual({ method: 'GET', url: 'https://example.com' });
|
||||
expect(result.nodeType).toBe('nodes-base.httpRequest');
|
||||
expect(result.mode).toBe('full');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -73,6 +73,9 @@ describe('handlers-workflow-diff', () => {
|
||||
mockApiClient = {
|
||||
getWorkflow: vi.fn(),
|
||||
updateWorkflow: vi.fn(),
|
||||
listTags: vi.fn().mockResolvedValue({ data: [] }),
|
||||
createTag: vi.fn(),
|
||||
updateWorkflowTags: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
// Setup mock diff engine
|
||||
@@ -150,6 +153,7 @@ describe('handlers-workflow-diff', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
saved: true,
|
||||
data: {
|
||||
id: 'test-workflow-id',
|
||||
name: 'Test Workflow',
|
||||
@@ -309,10 +313,12 @@ describe('handlers-workflow-diff', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
saved: false,
|
||||
operationsApplied: 0,
|
||||
error: 'Failed to apply diff operations',
|
||||
details: {
|
||||
errors: ['Node "non-existent-node" not found'],
|
||||
operationsApplied: 0,
|
||||
warnings: undefined,
|
||||
applied: [],
|
||||
failed: [0],
|
||||
},
|
||||
@@ -630,10 +636,14 @@ describe('handlers-workflow-diff', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
saved: false,
|
||||
operationsApplied: 1,
|
||||
error: 'Failed to apply diff operations',
|
||||
details: {
|
||||
errors: ['Operation 2 failed: Node "invalid-node" not found'],
|
||||
operationsApplied: 1,
|
||||
warnings: undefined,
|
||||
applied: undefined,
|
||||
failed: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -855,5 +865,258 @@ describe('handlers-workflow-diff', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tag Operations via Dedicated API', () => {
|
||||
it('should create a new tag and associate it with the workflow', async () => {
|
||||
const testWorkflow = createTestWorkflow();
|
||||
const updatedWorkflow = { ...testWorkflow };
|
||||
|
||||
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||
success: true,
|
||||
workflow: updatedWorkflow,
|
||||
operationsApplied: 1,
|
||||
message: 'Success',
|
||||
errors: [],
|
||||
tagsToAdd: ['new-tag'],
|
||||
});
|
||||
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||
mockApiClient.listTags.mockResolvedValue({ data: [] });
|
||||
mockApiClient.createTag.mockResolvedValue({ id: 'tag-123', name: 'new-tag' });
|
||||
|
||||
const result = await handleUpdatePartialWorkflow({
|
||||
id: 'test-workflow-id',
|
||||
operations: [{ type: 'addTag', tag: 'new-tag' }],
|
||||
}, mockRepository);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockApiClient.createTag).toHaveBeenCalledWith({ name: 'new-tag' });
|
||||
expect(mockApiClient.updateWorkflowTags).toHaveBeenCalledWith('test-workflow-id', ['tag-123']);
|
||||
});
|
||||
|
||||
it('should use existing tag ID when tag already exists', async () => {
|
||||
const testWorkflow = createTestWorkflow();
|
||||
const updatedWorkflow = { ...testWorkflow };
|
||||
|
||||
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||
success: true,
|
||||
workflow: updatedWorkflow,
|
||||
operationsApplied: 1,
|
||||
message: 'Success',
|
||||
errors: [],
|
||||
tagsToAdd: ['existing-tag'],
|
||||
});
|
||||
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||
mockApiClient.listTags.mockResolvedValue({ data: [{ id: 'tag-456', name: 'existing-tag' }] });
|
||||
|
||||
const result = await handleUpdatePartialWorkflow({
|
||||
id: 'test-workflow-id',
|
||||
operations: [{ type: 'addTag', tag: 'existing-tag' }],
|
||||
}, mockRepository);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockApiClient.createTag).not.toHaveBeenCalled();
|
||||
expect(mockApiClient.updateWorkflowTags).toHaveBeenCalledWith('test-workflow-id', ['tag-456']);
|
||||
});
|
||||
|
||||
it('should remove a tag from the workflow', async () => {
|
||||
const testWorkflow = createTestWorkflow({
|
||||
tags: [{ id: 'tag-789', name: 'old-tag' }],
|
||||
});
|
||||
const updatedWorkflow = { ...testWorkflow };
|
||||
|
||||
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||
success: true,
|
||||
workflow: updatedWorkflow,
|
||||
operationsApplied: 1,
|
||||
message: 'Success',
|
||||
errors: [],
|
||||
tagsToRemove: ['old-tag'],
|
||||
});
|
||||
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||
mockApiClient.listTags.mockResolvedValue({ data: [{ id: 'tag-789', name: 'old-tag' }] });
|
||||
|
||||
const result = await handleUpdatePartialWorkflow({
|
||||
id: 'test-workflow-id',
|
||||
operations: [{ type: 'removeTag', tag: 'old-tag' }],
|
||||
}, mockRepository);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockApiClient.updateWorkflowTags).toHaveBeenCalledWith('test-workflow-id', []);
|
||||
});
|
||||
|
||||
it('should produce warning on tag creation failure without failing the operation', async () => {
|
||||
const testWorkflow = createTestWorkflow();
|
||||
const updatedWorkflow = { ...testWorkflow };
|
||||
|
||||
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||
success: true,
|
||||
workflow: updatedWorkflow,
|
||||
operationsApplied: 1,
|
||||
message: 'Success',
|
||||
errors: [],
|
||||
tagsToAdd: ['fail-tag'],
|
||||
});
|
||||
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||
mockApiClient.listTags.mockResolvedValue({ data: [] });
|
||||
mockApiClient.createTag.mockRejectedValue(new Error('Tag creation failed'));
|
||||
|
||||
const result = await handleUpdatePartialWorkflow({
|
||||
id: 'test-workflow-id',
|
||||
operations: [{ type: 'addTag', tag: 'fail-tag' }],
|
||||
}, mockRepository);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.saved).toBe(true);
|
||||
// Tag creation failure should produce a warning, not block the update
|
||||
const warnings = (result.details as any)?.warnings;
|
||||
expect(warnings).toBeDefined();
|
||||
expect(warnings.some((w: any) => w.message.includes('Failed to create tag'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should not call tag APIs when no tag operations are present', async () => {
|
||||
const testWorkflow = createTestWorkflow();
|
||||
const updatedWorkflow = { ...testWorkflow };
|
||||
|
||||
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||
success: true,
|
||||
workflow: updatedWorkflow,
|
||||
operationsApplied: 1,
|
||||
message: 'Success',
|
||||
errors: [],
|
||||
});
|
||||
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||
|
||||
await handleUpdatePartialWorkflow({
|
||||
id: 'test-workflow-id',
|
||||
operations: [{ type: 'updateName', name: 'New Name' }],
|
||||
}, mockRepository);
|
||||
|
||||
expect(mockApiClient.listTags).not.toHaveBeenCalled();
|
||||
expect(mockApiClient.createTag).not.toHaveBeenCalled();
|
||||
expect(mockApiClient.updateWorkflowTags).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('field name normalization', () => {
|
||||
it('should normalize "name" to "nodeName" for updateNode operations', async () => {
|
||||
const testWorkflow = createTestWorkflow();
|
||||
const updatedWorkflow = { ...testWorkflow };
|
||||
|
||||
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||
success: true,
|
||||
workflow: updatedWorkflow,
|
||||
operationsApplied: 1,
|
||||
message: 'Success',
|
||||
errors: [],
|
||||
});
|
||||
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||
|
||||
await handleUpdatePartialWorkflow({
|
||||
id: 'test-workflow-id',
|
||||
operations: [{
|
||||
type: 'updateNode',
|
||||
name: 'HTTP Request', // LLMs often use "name" instead of "nodeName"
|
||||
updates: { 'parameters.url': 'https://new-url.com' },
|
||||
}],
|
||||
}, mockRepository);
|
||||
|
||||
// Verify the diff engine received nodeName (normalized from name)
|
||||
expect(mockDiffEngine.applyDiff).toHaveBeenCalled();
|
||||
const diffArgs = mockDiffEngine.applyDiff.mock.calls[0][1];
|
||||
expect(diffArgs.operations[0].nodeName).toBe('HTTP Request');
|
||||
});
|
||||
|
||||
it('should normalize "id" to "nodeId" for removeNode operations', async () => {
|
||||
const testWorkflow = createTestWorkflow();
|
||||
const updatedWorkflow = { ...testWorkflow };
|
||||
|
||||
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||
success: true,
|
||||
workflow: updatedWorkflow,
|
||||
operationsApplied: 1,
|
||||
message: 'Success',
|
||||
errors: [],
|
||||
});
|
||||
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||
|
||||
await handleUpdatePartialWorkflow({
|
||||
id: 'test-workflow-id',
|
||||
operations: [{
|
||||
type: 'removeNode',
|
||||
id: 'node2', // LLMs may use "id" instead of "nodeId"
|
||||
}],
|
||||
}, mockRepository);
|
||||
|
||||
// Verify the diff engine received nodeId (normalized from id)
|
||||
expect(mockDiffEngine.applyDiff).toHaveBeenCalled();
|
||||
const diffArgs = mockDiffEngine.applyDiff.mock.calls[0][1];
|
||||
expect(diffArgs.operations[0].nodeId).toBe('node2');
|
||||
});
|
||||
|
||||
it('should NOT normalize "name" for updateName operations', async () => {
|
||||
const testWorkflow = createTestWorkflow();
|
||||
const updatedWorkflow = { ...testWorkflow };
|
||||
|
||||
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||
success: true,
|
||||
workflow: updatedWorkflow,
|
||||
operationsApplied: 1,
|
||||
message: 'Success',
|
||||
errors: [],
|
||||
});
|
||||
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||
|
||||
await handleUpdatePartialWorkflow({
|
||||
id: 'test-workflow-id',
|
||||
operations: [{
|
||||
type: 'updateName',
|
||||
name: 'New Workflow Name', // This is the correct field for updateName
|
||||
}],
|
||||
}, mockRepository);
|
||||
|
||||
// Verify "name" stays as "name" (not moved to nodeName) for updateName
|
||||
expect(mockDiffEngine.applyDiff).toHaveBeenCalled();
|
||||
const diffArgs = mockDiffEngine.applyDiff.mock.calls[0][1];
|
||||
expect(diffArgs.operations[0].name).toBe('New Workflow Name');
|
||||
expect(diffArgs.operations[0].nodeName).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prefer explicit "nodeName" over "name" alias', async () => {
|
||||
const testWorkflow = createTestWorkflow();
|
||||
const updatedWorkflow = { ...testWorkflow };
|
||||
|
||||
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
|
||||
mockDiffEngine.applyDiff.mockResolvedValue({
|
||||
success: true,
|
||||
workflow: updatedWorkflow,
|
||||
operationsApplied: 1,
|
||||
message: 'Success',
|
||||
errors: [],
|
||||
});
|
||||
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
|
||||
|
||||
await handleUpdatePartialWorkflow({
|
||||
id: 'test-workflow-id',
|
||||
operations: [{
|
||||
type: 'updateNode',
|
||||
nodeName: 'HTTP Request', // Explicit nodeName provided
|
||||
name: 'Should Be Ignored', // Should NOT override nodeName
|
||||
updates: { 'parameters.url': 'https://new-url.com' },
|
||||
}],
|
||||
}, mockRepository);
|
||||
|
||||
expect(mockDiffEngine.applyDiff).toHaveBeenCalled();
|
||||
const diffArgs = mockDiffEngine.applyDiff.mock.calls[0][1];
|
||||
expect(diffArgs.operations[0].nodeName).toBe('HTTP Request');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -86,7 +86,7 @@ describe('UI_APP_CONFIGS', () => {
|
||||
expect(config!.toolPatterns).toContain('n8n_update_full_workflow');
|
||||
expect(config!.toolPatterns).toContain('n8n_delete_workflow');
|
||||
expect(config!.toolPatterns).toContain('n8n_test_workflow');
|
||||
expect(config!.toolPatterns).toContain('n8n_deploy_template');
|
||||
expect(config!.toolPatterns).not.toContain('n8n_deploy_template');
|
||||
});
|
||||
|
||||
it('should contain the validation-summary config', () => {
|
||||
@@ -102,6 +102,12 @@ describe('UI_APP_CONFIGS', () => {
|
||||
expect(UI_APP_CONFIGS.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should not contain disabled apps', () => {
|
||||
expect(UI_APP_CONFIGS.find(c => c.id === 'workflow-list')).toBeUndefined();
|
||||
expect(UI_APP_CONFIGS.find(c => c.id === 'execution-history')).toBeUndefined();
|
||||
expect(UI_APP_CONFIGS.find(c => c.id === 'health-dashboard')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should have IDs that are valid URI path segments (no spaces or special chars)', () => {
|
||||
for (const config of UI_APP_CONFIGS) {
|
||||
expect(config.id).toMatch(/^[a-z0-9-]+$/);
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('UI Meta Injection on Tool Definitions', () => {
|
||||
expect(tools[2]._meta.ui.resourceUri).toBe('ui://n8n-mcp/validation-summary');
|
||||
});
|
||||
|
||||
it('should produce _meta with exact shape { ui: { resourceUri: string } }', () => {
|
||||
it('should produce _meta with both nested and flat resourceUri keys', () => {
|
||||
const tools: any[] = [
|
||||
{ name: 'n8n_create_workflow', description: 'Create', inputSchema: { type: 'object', properties: {} } },
|
||||
];
|
||||
@@ -83,10 +83,10 @@ describe('UI Meta Injection on Tool Definitions', () => {
|
||||
ui: {
|
||||
resourceUri: 'ui://n8n-mcp/operation-result',
|
||||
},
|
||||
'ui/resourceUri': 'ui://n8n-mcp/operation-result',
|
||||
});
|
||||
expect(Object.keys(tools[0]._meta)).toEqual(['ui']);
|
||||
expect(Object.keys(tools[0]._meta.ui)).toEqual(['resourceUri']);
|
||||
expect(typeof tools[0]._meta.ui.resourceUri).toBe('string');
|
||||
expect(tools[0]._meta.ui.resourceUri).toBe('ui://n8n-mcp/operation-result');
|
||||
expect(tools[0]._meta['ui/resourceUri']).toBe('ui://n8n-mcp/operation-result');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -187,8 +187,11 @@ describe('UIAppRegistry', () => {
|
||||
expect(UIAppRegistry.getAppForTool('n8n_autofix_workflow')!.config.id).toBe('operation-result');
|
||||
});
|
||||
|
||||
it('should map n8n_deploy_template to operation-result', () => {
|
||||
expect(UIAppRegistry.getAppForTool('n8n_deploy_template')!.config.id).toBe('operation-result');
|
||||
it('should not map disabled tools', () => {
|
||||
expect(UIAppRegistry.getAppForTool('n8n_deploy_template')).toBeNull();
|
||||
expect(UIAppRegistry.getAppForTool('n8n_list_workflows')).toBeNull();
|
||||
expect(UIAppRegistry.getAppForTool('n8n_executions')).toBeNull();
|
||||
expect(UIAppRegistry.getAppForTool('n8n_health_check')).toBeNull();
|
||||
});
|
||||
|
||||
it('should map validate_node to validation-summary', () => {
|
||||
@@ -308,7 +311,7 @@ describe('UIAppRegistry', () => {
|
||||
{ name: 'n8n_create_workflow', description: 'Create', inputSchema: { type: 'object', properties: {} } },
|
||||
];
|
||||
UIAppRegistry.injectToolMeta(tools);
|
||||
expect(tools[0]._meta).toEqual({ ui: { resourceUri: 'ui://n8n-mcp/operation-result' } });
|
||||
expect(tools[0]._meta).toEqual({ ui: { resourceUri: 'ui://n8n-mcp/operation-result' }, 'ui/resourceUri': 'ui://n8n-mcp/operation-result' });
|
||||
});
|
||||
|
||||
it('should set _meta.ui.resourceUri on matching validation tools', () => {
|
||||
@@ -316,7 +319,7 @@ describe('UIAppRegistry', () => {
|
||||
{ name: 'validate_node', description: 'Validate', inputSchema: { type: 'object', properties: {} } },
|
||||
];
|
||||
UIAppRegistry.injectToolMeta(tools);
|
||||
expect(tools[0]._meta).toEqual({ ui: { resourceUri: 'ui://n8n-mcp/validation-summary' } });
|
||||
expect(tools[0]._meta).toEqual({ ui: { resourceUri: 'ui://n8n-mcp/validation-summary' }, 'ui/resourceUri': 'ui://n8n-mcp/validation-summary' });
|
||||
});
|
||||
|
||||
it('should not set _meta on tools without a matching UI app', () => {
|
||||
|
||||
@@ -398,7 +398,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
const result = await client.activateWorkflow('123');
|
||||
|
||||
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/workflows/123/activate');
|
||||
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/workflows/123/activate', {});
|
||||
expect(result).toEqual(activatedWorkflow);
|
||||
expect(result.active).toBe(true);
|
||||
});
|
||||
@@ -484,7 +484,7 @@ describe('N8nApiClient', () => {
|
||||
|
||||
const result = await client.deactivateWorkflow('123');
|
||||
|
||||
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/workflows/123/deactivate');
|
||||
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/workflows/123/deactivate', {});
|
||||
expect(result).toEqual(deactivatedWorkflow);
|
||||
expect(result.active).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1096,6 +1096,83 @@ describe('n8n-validation', () => {
|
||||
expect(disconnectedErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should NOT flag nodes as disconnected when connected via ai_outputParser', () => {
|
||||
const workflow = {
|
||||
name: 'AI Output Parser Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: 'agent-1',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1.6,
|
||||
position: [500, 300] as [number, number],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'parser-1',
|
||||
name: 'Structured Output Parser',
|
||||
type: '@n8n/n8n-nodes-langchain.outputParserStructured',
|
||||
typeVersion: 1,
|
||||
position: [300, 400] as [number, number],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'Structured Output Parser': {
|
||||
ai_outputParser: [[{ node: 'AI Agent', type: 'ai_outputParser', index: 0 }]],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const errors = validateWorkflowStructure(workflow);
|
||||
const disconnectedErrors = errors.filter(e => e.includes('Disconnected'));
|
||||
expect(disconnectedErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should NOT flag nodes as disconnected when connected via ai_document or ai_textSplitter', () => {
|
||||
const workflow = {
|
||||
name: 'Document Processing Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: 'vs-1',
|
||||
name: 'Pinecone Vector Store',
|
||||
type: '@n8n/n8n-nodes-langchain.vectorStorePinecone',
|
||||
typeVersion: 1,
|
||||
position: [500, 300] as [number, number],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'doc-1',
|
||||
name: 'Default Data Loader',
|
||||
type: '@n8n/n8n-nodes-langchain.documentDefaultDataLoader',
|
||||
typeVersion: 1,
|
||||
position: [300, 400] as [number, number],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'splitter-1',
|
||||
name: 'Text Splitter',
|
||||
type: '@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter',
|
||||
typeVersion: 1,
|
||||
position: [100, 400] as [number, number],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'Default Data Loader': {
|
||||
ai_document: [[{ node: 'Pinecone Vector Store', type: 'ai_document', index: 0 }]],
|
||||
},
|
||||
'Text Splitter': {
|
||||
ai_textSplitter: [[{ node: 'Default Data Loader', type: 'ai_textSplitter', index: 0 }]],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const errors = validateWorkflowStructure(workflow);
|
||||
const disconnectedErrors = errors.filter(e => e.includes('Disconnected'));
|
||||
expect(disconnectedErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should still flag truly disconnected nodes in AI workflows', () => {
|
||||
const workflow = {
|
||||
name: 'AI Workflow with Disconnected Node',
|
||||
|
||||
@@ -1912,6 +1912,55 @@ return [{"json": {"result": result}}]
|
||||
});
|
||||
});
|
||||
|
||||
it('should not error on primitive return inside helper functions', () => {
|
||||
context.config = {
|
||||
language: 'javaScript',
|
||||
jsCode: 'function isValid(item) { return false; }\nconst items = $input.all();\nreturn items.filter(isValid).map(i => ({json: i.json}));'
|
||||
};
|
||||
|
||||
NodeSpecificValidators.validateCode(context);
|
||||
|
||||
const primitiveErrors = context.errors.filter(e => e.message === 'Cannot return primitive values directly');
|
||||
expect(primitiveErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not error on primitive return inside arrow function helpers', () => {
|
||||
context.config = {
|
||||
language: 'javaScript',
|
||||
jsCode: 'const isValid = (item) => { return false; };\nreturn $input.all().filter(isValid).map(i => ({json: i.json}));'
|
||||
};
|
||||
|
||||
NodeSpecificValidators.validateCode(context);
|
||||
|
||||
const primitiveErrors = context.errors.filter(e => e.message === 'Cannot return primitive values directly');
|
||||
expect(primitiveErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not error on primitive return inside async function helpers', () => {
|
||||
context.config = {
|
||||
language: 'javaScript',
|
||||
jsCode: 'async function fetchData(url) { return null; }\nconst data = await fetchData("https://api.example.com");\nreturn [{json: {data}}];'
|
||||
};
|
||||
|
||||
NodeSpecificValidators.validateCode(context);
|
||||
|
||||
const primitiveErrors = context.errors.filter(e => e.message === 'Cannot return primitive values directly');
|
||||
expect(primitiveErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should still error on primitive return without helper functions', () => {
|
||||
context.config = {
|
||||
language: 'javaScript',
|
||||
jsCode: 'return "success";'
|
||||
};
|
||||
|
||||
NodeSpecificValidators.validateCode(context);
|
||||
|
||||
expect(context.errors).toContainEqual(expect.objectContaining({
|
||||
message: 'Cannot return primitive values directly'
|
||||
}));
|
||||
});
|
||||
|
||||
it('should error on Python primitive return', () => {
|
||||
context.config = {
|
||||
language: 'python',
|
||||
@@ -2038,6 +2087,30 @@ return [{"json": {"result": result}}]
|
||||
});
|
||||
});
|
||||
|
||||
it('should not warn about $() node reference syntax', () => {
|
||||
context.config = {
|
||||
language: 'javaScript',
|
||||
jsCode: 'const data = $("Previous Node").first().json;\nreturn [{json: data}];'
|
||||
};
|
||||
|
||||
NodeSpecificValidators.validateCode(context);
|
||||
|
||||
const dollarWarnings = context.warnings.filter(w => w.message === 'Invalid $ usage detected');
|
||||
expect(dollarWarnings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not warn about $_ variables', () => {
|
||||
context.config = {
|
||||
language: 'javaScript',
|
||||
jsCode: 'const $_temp = 1;\nreturn [{json: {value: $_temp}}];'
|
||||
};
|
||||
|
||||
NodeSpecificValidators.validateCode(context);
|
||||
|
||||
const dollarWarnings = context.warnings.filter(w => w.message === 'Invalid $ usage detected');
|
||||
expect(dollarWarnings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should correct helpers usage', () => {
|
||||
context.config = {
|
||||
language: 'javaScript',
|
||||
|
||||
566
tests/unit/services/workflow-auto-fixer-connections.test.ts
Normal file
566
tests/unit/services/workflow-auto-fixer-connections.test.ts
Normal file
@@ -0,0 +1,566 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { WorkflowAutoFixer } from '@/services/workflow-auto-fixer';
|
||||
import { NodeRepository } from '@/database/node-repository';
|
||||
import type { WorkflowValidationResult } from '@/services/workflow-validator';
|
||||
import type { Workflow, WorkflowNode } from '@/types/n8n-api';
|
||||
|
||||
vi.mock('@/database/node-repository');
|
||||
vi.mock('@/services/node-similarity-service');
|
||||
|
||||
describe('WorkflowAutoFixer - Connection Fixes', () => {
|
||||
let autoFixer: WorkflowAutoFixer;
|
||||
let mockRepository: NodeRepository;
|
||||
|
||||
const createMockWorkflow = (
|
||||
nodes: WorkflowNode[],
|
||||
connections: any = {}
|
||||
): Workflow => ({
|
||||
id: 'test-workflow',
|
||||
name: 'Test Workflow',
|
||||
active: false,
|
||||
nodes,
|
||||
connections,
|
||||
settings: {},
|
||||
createdAt: '',
|
||||
updatedAt: ''
|
||||
});
|
||||
|
||||
const createMockNode = (id: string, name: string, type: string = 'n8n-nodes-base.noOp'): WorkflowNode => ({
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
});
|
||||
|
||||
const emptyValidation: WorkflowValidationResult = {
|
||||
valid: true,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
statistics: {
|
||||
totalNodes: 0,
|
||||
enabledNodes: 0,
|
||||
triggerNodes: 0,
|
||||
validConnections: 0,
|
||||
invalidConnections: 0,
|
||||
expressionsValidated: 0
|
||||
},
|
||||
suggestions: []
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockRepository = new NodeRepository({} as any);
|
||||
vi.spyOn(mockRepository, 'getNodeVersions').mockReturnValue([]);
|
||||
autoFixer = new WorkflowAutoFixer(mockRepository);
|
||||
});
|
||||
|
||||
describe('Numeric Keys', () => {
|
||||
it('should convert single numeric key to main[index]', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-numeric-keys');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].before).toBe('0');
|
||||
expect(connFixes[0].after).toBe('main[0]');
|
||||
|
||||
// Verify replaceConnections operation
|
||||
const replaceOp = result.operations.find(op => op.type === 'replaceConnections');
|
||||
expect(replaceOp).toBeDefined();
|
||||
const connOp = replaceOp as any;
|
||||
expect(connOp.connections.Node1['main']).toBeDefined();
|
||||
expect(connOp.connections.Node1['0']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should convert multiple numeric keys', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2'), createMockNode('id3', 'Node3')],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[{ node: 'Node2', type: 'main', index: 0 }]],
|
||||
'1': [[{ node: 'Node3', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-numeric-keys');
|
||||
expect(connFixes).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should merge with existing main entries', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2'), createMockNode('id3', 'Node3')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]],
|
||||
'1': [[{ node: 'Node3', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const replaceOp = result.operations.find(op => op.type === 'replaceConnections') as any;
|
||||
expect(replaceOp.connections.Node1['main']).toHaveLength(2);
|
||||
expect(replaceOp.connections.Node1['main'][0]).toEqual([{ node: 'Node2', type: 'main', index: 0 }]);
|
||||
expect(replaceOp.connections.Node1['main'][1]).toEqual([{ node: 'Node3', type: 'main', index: 0 }]);
|
||||
});
|
||||
|
||||
it('should handle sparse numeric keys with gap filling', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2'), createMockNode('id3', 'Node3')],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[{ node: 'Node2', type: 'main', index: 0 }]],
|
||||
'3': [[{ node: 'Node3', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const replaceOp = result.operations.find(op => op.type === 'replaceConnections') as any;
|
||||
expect(replaceOp.connections.Node1['main']).toHaveLength(4);
|
||||
expect(replaceOp.connections.Node1['main'][1]).toEqual([]);
|
||||
expect(replaceOp.connections.Node1['main'][2]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid Type', () => {
|
||||
it('should fix numeric type to "main"', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: '0', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-invalid-type');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].before).toBe('0');
|
||||
expect(connFixes[0].after).toBe('main');
|
||||
});
|
||||
|
||||
it('should use parent output key for AI connection types', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
ai_tool: [[{ node: 'Node2', type: '0', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-invalid-type');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].after).toBe('ai_tool');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ID-to-Name', () => {
|
||||
it('should replace source key when it matches a node ID', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('abc-123', 'Node1'), createMockNode('def-456', 'Node2')],
|
||||
{
|
||||
'abc-123': {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-id-to-name');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].before).toBe('abc-123');
|
||||
expect(connFixes[0].after).toBe('Node1');
|
||||
|
||||
const replaceOp = result.operations.find(op => op.type === 'replaceConnections') as any;
|
||||
expect(replaceOp.connections['Node1']).toBeDefined();
|
||||
expect(replaceOp.connections['abc-123']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should replace target node value when it matches a node ID', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('abc-123', 'Node1'), createMockNode('def-456', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'def-456', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-id-to-name');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].before).toBe('def-456');
|
||||
expect(connFixes[0].after).toBe('Node2');
|
||||
});
|
||||
|
||||
it('should NOT fix when key matches both an ID and a name', async () => {
|
||||
// Node with name that looks like an ID of another node
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('abc-123', 'abc-123'), createMockNode('def-456', 'Node2')],
|
||||
{
|
||||
'abc-123': {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-id-to-name');
|
||||
expect(connFixes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dedup', () => {
|
||||
it('should remove exact duplicate connections', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[
|
||||
{ node: 'Node2', type: 'main', index: 0 },
|
||||
{ node: 'Node2', type: 'main', index: 0 },
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-duplicate-removal');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
|
||||
const replaceOp = result.operations.find(op => op.type === 'replaceConnections') as any;
|
||||
expect(replaceOp.connections.Node1.main[0]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should keep near-duplicates with different index', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[
|
||||
{ node: 'Node2', type: 'main', index: 0 },
|
||||
{ node: 'Node2', type: 'main', index: 1 },
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-duplicate-removal');
|
||||
expect(connFixes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input Index', () => {
|
||||
it('should reset to 0 for single-input nodes', async () => {
|
||||
const validation: WorkflowValidationResult = {
|
||||
...emptyValidation,
|
||||
errors: [{
|
||||
type: 'error',
|
||||
nodeName: 'Node2',
|
||||
message: 'Input index 3 on node "Node2" exceeds its input count (1). Connection from "Node1" targets input 3, but this node has 1 main input(s) (indices 0-0).',
|
||||
code: 'INPUT_INDEX_OUT_OF_BOUNDS'
|
||||
}]
|
||||
};
|
||||
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2', 'n8n-nodes-base.httpRequest')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 3 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, validation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-input-index');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].before).toBe(3);
|
||||
expect(connFixes[0].after).toBe(0);
|
||||
expect(connFixes[0].confidence).toBe('medium');
|
||||
});
|
||||
|
||||
it('should clamp for Merge nodes', async () => {
|
||||
const validation: WorkflowValidationResult = {
|
||||
...emptyValidation,
|
||||
errors: [{
|
||||
type: 'error',
|
||||
nodeName: 'MergeNode',
|
||||
message: 'Input index 5 on node "MergeNode" exceeds its input count (2). Connection from "Node1" targets input 5, but this node has 2 main input(s) (indices 0-1).',
|
||||
code: 'INPUT_INDEX_OUT_OF_BOUNDS'
|
||||
}]
|
||||
};
|
||||
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'MergeNode', 'n8n-nodes-base.merge')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'MergeNode', type: 'main', index: 5 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, validation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-input-index');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].before).toBe(5);
|
||||
expect(connFixes[0].after).toBe(1); // clamped to max valid index
|
||||
});
|
||||
|
||||
it('should not fix valid indices', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-input-index');
|
||||
expect(connFixes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Combined', () => {
|
||||
it('should fix multiple issues in one workflow', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[
|
||||
createMockNode('id1', 'Node1'),
|
||||
createMockNode('id2', 'Node2'),
|
||||
createMockNode('id3', 'Node3')
|
||||
],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[
|
||||
{ node: 'Node2', type: '0', index: 0 },
|
||||
{ node: 'Node2', type: '0', index: 0 }, // duplicate
|
||||
]]
|
||||
},
|
||||
'id3': { // ID instead of name
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
expect(result.fixes.length).toBeGreaterThan(0);
|
||||
expect(result.operations.find(op => op.type === 'replaceConnections')).toBeDefined();
|
||||
|
||||
// Should have numeric key, invalid type, dedup, and id-to-name fixes
|
||||
const types = new Set(result.fixes.map(f => f.type));
|
||||
expect(types.has('connection-numeric-keys')).toBe(true);
|
||||
expect(types.has('connection-id-to-name')).toBe(true);
|
||||
});
|
||||
|
||||
it('should be idempotent (no fixes on valid connections)', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connectionFixTypes = [
|
||||
'connection-numeric-keys',
|
||||
'connection-invalid-type',
|
||||
'connection-id-to-name',
|
||||
'connection-duplicate-removal',
|
||||
'connection-input-index'
|
||||
];
|
||||
const connFixes = result.fixes.filter(f => connectionFixTypes.includes(f.type));
|
||||
expect(connFixes).toHaveLength(0);
|
||||
expect(result.operations.find(op => op.type === 'replaceConnections')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty connections', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1')],
|
||||
{}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
expect(result.operations.find(op => op.type === 'replaceConnections')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should respect fixTypes filtering', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[{ node: 'Node2', type: '0', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Only allow numeric key fixes, not invalid type fixes
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, [], {
|
||||
fixTypes: ['connection-numeric-keys']
|
||||
});
|
||||
|
||||
const numericFixes = result.fixes.filter(f => f.type === 'connection-numeric-keys');
|
||||
const typeFixes = result.fixes.filter(f => f.type === 'connection-invalid-type');
|
||||
expect(numericFixes.length).toBeGreaterThan(0);
|
||||
expect(typeFixes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should filter replaceConnections from operations when confidence threshold filters all connection fixes', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 5 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const validation: WorkflowValidationResult = {
|
||||
...emptyValidation,
|
||||
errors: [{
|
||||
type: 'error',
|
||||
nodeName: 'Node2',
|
||||
message: 'Input index 5 on node "Node2" exceeds its input count (1). Connection from "Node1" targets input 5, but this node has 1 main input(s) (indices 0-0).',
|
||||
code: 'INPUT_INDEX_OUT_OF_BOUNDS'
|
||||
}]
|
||||
};
|
||||
|
||||
// Input index fixes are medium confidence. Filter to high only.
|
||||
const result = await autoFixer.generateFixes(workflow, validation, [], {
|
||||
confidenceThreshold: 'high'
|
||||
});
|
||||
|
||||
// Medium confidence fixes should be filtered out
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-input-index');
|
||||
expect(connFixes).toHaveLength(0);
|
||||
expect(result.operations.find(op => op.type === 'replaceConnections')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should include connection issues in summary', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
expect(result.summary).toContain('connection');
|
||||
});
|
||||
|
||||
it('should handle non-existent target nodes gracefully', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1')],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[{ node: 'NonExistent', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Should not throw
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
expect(result.fixes.some(f => f.type === 'connection-numeric-keys')).toBe(true);
|
||||
});
|
||||
|
||||
it('should skip unparseable INPUT_INDEX_OUT_OF_BOUNDS errors gracefully', async () => {
|
||||
const validation: WorkflowValidationResult = {
|
||||
...emptyValidation,
|
||||
errors: [{
|
||||
type: 'error',
|
||||
nodeName: 'Node2',
|
||||
message: 'Something unexpected about input indices',
|
||||
code: 'INPUT_INDEX_OUT_OF_BOUNDS'
|
||||
}]
|
||||
};
|
||||
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 5 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, validation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-input-index');
|
||||
expect(connFixes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should fix both source keys and target .node values as IDs in the same workflow', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[
|
||||
createMockNode('abc-123', 'Node1'),
|
||||
createMockNode('def-456', 'Node2'),
|
||||
createMockNode('ghi-789', 'Node3')
|
||||
],
|
||||
{
|
||||
'abc-123': { // source key is ID
|
||||
main: [[{ node: 'def-456', type: 'main', index: 0 }]] // target .node is also ID
|
||||
},
|
||||
Node2: {
|
||||
main: [[{ node: 'ghi-789', type: 'main', index: 0 }]] // another target ID
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-id-to-name');
|
||||
|
||||
// Should fix: source key abc-123 → Node1, target def-456 → Node2, target ghi-789 → Node3
|
||||
expect(connFixes).toHaveLength(3);
|
||||
|
||||
const replaceOp = result.operations.find(op => op.type === 'replaceConnections') as any;
|
||||
expect(replaceOp.connections['Node1']).toBeDefined();
|
||||
expect(replaceOp.connections['abc-123']).toBeUndefined();
|
||||
|
||||
// Verify target .node values were also replaced
|
||||
const node1Conns = replaceOp.connections['Node1'].main[0];
|
||||
expect(node1Conns[0].node).toBe('Node2');
|
||||
|
||||
const node2Conns = replaceOp.connections['Node2'].main[0];
|
||||
expect(node2Conns[0].node).toBe('Node3');
|
||||
});
|
||||
|
||||
it('should lower confidence to medium when merging numeric key into non-empty main slot', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2'), createMockNode('id3', 'Node3')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]],
|
||||
'0': [[{ node: 'Node3', type: 'main', index: 0 }]] // conflicts with existing main[0]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const numericFixes = result.fixes.filter(f => f.type === 'connection-numeric-keys');
|
||||
expect(numericFixes).toHaveLength(1);
|
||||
expect(numericFixes[0].confidence).toBe('medium');
|
||||
expect(numericFixes[0].description).toContain('Merged');
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user