mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-27 15:13:08 +00:00
Compare commits
6 Commits
chore/bump
...
fix/605-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4009cee7c | ||
|
|
4bad880f44 | ||
|
|
77048347b3 | ||
|
|
6f695be482 | ||
|
|
34159f4ece | ||
|
|
8217229e2f |
54
CHANGELOG.md
54
CHANGELOG.md
@@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [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
|
||||
|
||||
@@ -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,236 workflow automation nodes (806 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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"}
|
||||
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;IA8VrB,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;IAuGvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YA+Cd,kBAAkB;YAiDlB,wBAAwB;IA0BtC,OAAO,CAAC,kBAAkB;YA6CZ,iBAAiB;IAa/B,OAAO,CAAC,eAAe,CAAkB;YAE3B,sBAAsB;IAgDpC,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,aAAa;IA0XrB,OAAO,CAAC,wBAAwB;IAoFhC,OAAO,CAAC,kBAAkB;IAqE1B,OAAO,CAAC,uBAAuB;IAwB/B,OAAO,CAAC,qBAAqB;IAiF7B,OAAO,CAAC,2BAA2B;YA0UrB,SAAS;YA2DT,WAAW;YAkFX,WAAW;YA0CX,cAAc;YA8Md,gBAAgB;IAqD9B,OAAO,CAAC,mBAAmB;IAwE3B,OAAO,CAAC,eAAe;YAsBT,eAAe;IA2L7B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,uBAAuB;IA0D/B,OAAO,CAAC,iBAAiB;YAqFX,WAAW;YAgCX,oBAAoB;IAuFlC,OAAO,CAAC,aAAa;YAQP,qBAAqB;YAwDrB,iBAAiB;YAiKjB,OAAO;YAgDP,cAAc;YAwFd,iBAAiB;IAqC/B,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,eAAe;IAwCvB,OAAO,CAAC,kBAAkB;IAiC1B,OAAO,CAAC,aAAa;IAoCrB,OAAO,CAAC,0BAA0B;IAgClC,OAAO,CAAC,4BAA4B;YAKtB,oBAAoB;IAsDlC,OAAO,CAAC,gBAAgB;YAiBV,SAAS;YA6CT,kBAAkB;YAqElB,uBAAuB;YAsDvB,iBAAiB;IAqE/B,OAAO,CAAC,qBAAqB;IA8C7B,OAAO,CAAC,uBAAuB;IA4D/B,OAAO,CAAC,wBAAwB;IAkChC,OAAO,CAAC,iBAAiB;YAoDX,mBAAmB;YAoEnB,qBAAqB;IAS7B,OAAO,CAAC,SAAS,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;YAS9B,aAAa;YAcb,iBAAiB;YAoBjB,WAAW;YAwBX,eAAe;YAqBf,mBAAmB;YAwBnB,yBAAyB;IA4CvC,OAAO,CAAC,kBAAkB;YAiBZ,gBAAgB;YA6HhB,2BAA2B;YAiE3B,2BAA2B;IAyEnC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BpB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAgEhC"}
|
||||
107
dist/mcp/server.js
vendored
107
dist/mcp/server.js
vendored
@@ -457,6 +457,18 @@ class N8NDocumentationMCPServer {
|
||||
};
|
||||
}
|
||||
let processedArgs = args;
|
||||
if (typeof args === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(args);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
processedArgs = parsed;
|
||||
logger_1.logger.warn(`Coerced stringified args object for tool "${name}"`);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
logger_1.logger.warn(`Tool "${name}" received string args that are not valid JSON`);
|
||||
}
|
||||
}
|
||||
if (args && typeof args === 'object' && 'output' in args) {
|
||||
try {
|
||||
const possibleNestedData = args.output;
|
||||
@@ -485,6 +497,7 @@ class N8NDocumentationMCPServer {
|
||||
});
|
||||
}
|
||||
}
|
||||
processedArgs = this.coerceStringifiedJsonParams(name, processedArgs);
|
||||
try {
|
||||
logger_1.logger.debug(`Executing tool: ${name}`, { args: processedArgs });
|
||||
const startTime = Date.now();
|
||||
@@ -556,6 +569,13 @@ class N8NDocumentationMCPServer {
|
||||
if (name.startsWith('validate_') && (errorMessage.includes('config') || errorMessage.includes('nodeType'))) {
|
||||
helpfulMessage += '\n\nFor validation tools:\n- nodeType should be a string (e.g., "nodes-base.webhook")\n- config should be an object (e.g., {})';
|
||||
}
|
||||
try {
|
||||
const argDiag = processedArgs && typeof processedArgs === 'object'
|
||||
? Object.entries(processedArgs).map(([k, v]) => `${k}: ${typeof v}`).join(', ')
|
||||
: `args type: ${typeof processedArgs}`;
|
||||
helpfulMessage += `\n\n[Diagnostic] Received arg types: {${argDiag}}`;
|
||||
}
|
||||
catch { }
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@@ -795,6 +815,93 @@ class N8NDocumentationMCPServer {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
coerceStringifiedJsonParams(toolName, args) {
|
||||
if (!args || typeof args !== 'object')
|
||||
return args;
|
||||
const allTools = [...tools_1.n8nDocumentationToolsFinal, ...tools_n8n_manager_1.n8nManagementTools];
|
||||
const tool = allTools.find(t => t.name === toolName);
|
||||
if (!tool?.inputSchema?.properties)
|
||||
return args;
|
||||
const properties = tool.inputSchema.properties;
|
||||
const coerced = { ...args };
|
||||
let coercedAny = false;
|
||||
for (const [key, value] of Object.entries(coerced)) {
|
||||
if (value === undefined || value === null)
|
||||
continue;
|
||||
const propSchema = properties[key];
|
||||
if (!propSchema)
|
||||
continue;
|
||||
const expectedType = propSchema.type;
|
||||
if (!expectedType)
|
||||
continue;
|
||||
const actualType = typeof value;
|
||||
if (expectedType === 'string' && actualType === 'string')
|
||||
continue;
|
||||
if ((expectedType === 'number' || expectedType === 'integer') && actualType === 'number')
|
||||
continue;
|
||||
if (expectedType === 'boolean' && actualType === 'boolean')
|
||||
continue;
|
||||
if (expectedType === 'object' && actualType === 'object' && !Array.isArray(value))
|
||||
continue;
|
||||
if (expectedType === 'array' && Array.isArray(value))
|
||||
continue;
|
||||
if (actualType === 'string') {
|
||||
const trimmed = value.trim();
|
||||
if (expectedType === 'object' && trimmed.startsWith('{')) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
coerced[key] = parsed;
|
||||
coercedAny = true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
continue;
|
||||
}
|
||||
if (expectedType === 'array' && trimmed.startsWith('[')) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (Array.isArray(parsed)) {
|
||||
coerced[key] = parsed;
|
||||
coercedAny = true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
continue;
|
||||
}
|
||||
if (expectedType === 'number' || expectedType === 'integer') {
|
||||
const num = Number(trimmed);
|
||||
if (!isNaN(num) && trimmed !== '') {
|
||||
coerced[key] = expectedType === 'integer' ? Math.trunc(num) : num;
|
||||
coercedAny = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (expectedType === 'boolean') {
|
||||
if (trimmed === 'true') {
|
||||
coerced[key] = true;
|
||||
coercedAny = true;
|
||||
}
|
||||
else if (trimmed === 'false') {
|
||||
coerced[key] = false;
|
||||
coercedAny = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (expectedType === 'string' && (actualType === 'number' || actualType === 'boolean')) {
|
||||
coerced[key] = String(value);
|
||||
coercedAny = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (coercedAny) {
|
||||
logger_1.logger.warn(`Coerced mistyped params for tool "${toolName}"`, {
|
||||
original: Object.fromEntries(Object.entries(args).map(([k, v]) => [k, `${typeof v}: ${typeof v === 'string' ? v.substring(0, 80) : v}`])),
|
||||
});
|
||||
}
|
||||
return coerced;
|
||||
}
|
||||
async executeTool(name, args) {
|
||||
args = args || {};
|
||||
const disabledTools = this.getDisabledTools();
|
||||
|
||||
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
14515
package-lock.json
generated
14515
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.35.0",
|
||||
"version": "2.35.5",
|
||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -153,16 +153,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.20.1",
|
||||
"@n8n/n8n-nodes-langchain": "^2.6.2",
|
||||
"@n8n/n8n-nodes-langchain": "^2.8.1",
|
||||
"@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.8.3",
|
||||
"n8n-core": "^2.8.1",
|
||||
"n8n-workflow": "^2.8.0",
|
||||
"openai": "^4.77.0",
|
||||
"sql.js": "^1.13.0",
|
||||
"tslib": "^2.6.2",
|
||||
|
||||
@@ -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 } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -687,9 +687,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 +734,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 +828,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 +836,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 +1153,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 || {};
|
||||
|
||||
@@ -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,34 +29,8 @@ export const UI_APP_CONFIGS: UIAppConfig[] = [
|
||||
'n8n_validate_workflow',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'workflow-list',
|
||||
displayName: 'Workflow List',
|
||||
description: 'Compact table of workflows with status, tags, and metadata',
|
||||
uri: 'ui://n8n-mcp/workflow-list',
|
||||
mimeType: 'text/html;profile=mcp-app',
|
||||
toolPatterns: [
|
||||
'n8n_list_workflows',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'execution-history',
|
||||
displayName: 'Execution History',
|
||||
description: 'Execution history table with status summary bar',
|
||||
uri: 'ui://n8n-mcp/execution-history',
|
||||
mimeType: 'text/html;profile=mcp-app',
|
||||
toolPatterns: [
|
||||
'n8n_executions',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'health-dashboard',
|
||||
displayName: 'Health Dashboard',
|
||||
description: 'Connection status, versions, and performance metrics',
|
||||
uri: 'ui://n8n-mcp/health-dashboard',
|
||||
mimeType: 'text/html;profile=mcp-app',
|
||||
toolPatterns: [
|
||||
'n8n_health_check',
|
||||
],
|
||||
},
|
||||
// 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)
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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', () => {
|
||||
@@ -98,29 +98,14 @@ describe('UI_APP_CONFIGS', () => {
|
||||
expect(config!.toolPatterns).toContain('n8n_validate_workflow');
|
||||
});
|
||||
|
||||
it('should have exactly 5 configs', () => {
|
||||
expect(UI_APP_CONFIGS.length).toBe(5);
|
||||
it('should have exactly 2 configs', () => {
|
||||
expect(UI_APP_CONFIGS.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should contain the workflow-list config', () => {
|
||||
const config = UI_APP_CONFIGS.find(c => c.id === 'workflow-list');
|
||||
expect(config).toBeDefined();
|
||||
expect(config!.displayName).toBe('Workflow List');
|
||||
expect(config!.toolPatterns).toContain('n8n_list_workflows');
|
||||
});
|
||||
|
||||
it('should contain the execution-history config', () => {
|
||||
const config = UI_APP_CONFIGS.find(c => c.id === 'execution-history');
|
||||
expect(config).toBeDefined();
|
||||
expect(config!.displayName).toBe('Execution History');
|
||||
expect(config!.toolPatterns).toContain('n8n_executions');
|
||||
});
|
||||
|
||||
it('should contain the health-dashboard config', () => {
|
||||
const config = UI_APP_CONFIGS.find(c => c.id === 'health-dashboard');
|
||||
expect(config).toBeDefined();
|
||||
expect(config!.displayName).toBe('Health Dashboard');
|
||||
expect(config!.toolPatterns).toContain('n8n_health_check');
|
||||
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)', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user