n8n-decanter 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/lib/mcp.mjs +63 -5
- package/dist/lib/preflight.mjs +5 -5
- package/dist/lib/testrun.mjs +2 -2
- package/package.json +2 -2
- package/template/AGENTS.md.example +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.2] - 2026-10-21
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`test --scenario` and `test --execution` work again on n8n 2.34 and
|
|
15
|
+
newer** ([#327](https://github.com/buttjer/n8n-decanter/issues/327)). The run
|
|
16
|
+
succeeded on the instance, but decanter then stopped with `Tool get_execution
|
|
17
|
+
not found` and compared nothing. n8n 2.34 renamed that MCP tool to
|
|
18
|
+
`get_workflow_execution`, and renamed two more that decanter uses:
|
|
19
|
+
`search_executions` (behind `preflight`'s `history` check, which quietly
|
|
20
|
+
fell back to the REST API or skipped) and `prepare_test_pin_data` (behind
|
|
21
|
+
`scenario create --scaffold`, which quietly produced fills without schemas).
|
|
22
|
+
decanter now asks for the new name and falls back to the old one, so older n8n
|
|
23
|
+
keeps working too.
|
|
24
|
+
|
|
10
25
|
## [0.11.1] - 2026-09-10
|
|
11
26
|
|
|
12
27
|
### Added
|
package/dist/lib/mcp.mjs
CHANGED
|
@@ -33,6 +33,63 @@ export class McpToolError extends Error {
|
|
|
33
33
|
export function isUnavailableInMcp(err) {
|
|
34
34
|
return err instanceof Error && /not available in MCP/i.test(err.message);
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The tools decanter calls that n8n RENAMED, current name first. n8n 2.34.0
|
|
38
|
+
* renamed them (n8n-io/n8n#34347) and kept no alias, so each name works on
|
|
39
|
+
* one side of that release only: the old one fails "Tool get_execution not
|
|
40
|
+
* found" on 2.34+ (#327), the new one fails the same way on everything older.
|
|
41
|
+
* Source-verified 2026-10-21: old names on n8n@2.33.7, new names from
|
|
42
|
+
* n8n@2.34.0 through n8n@2.44.5. Every other tool decanter calls kept its name.
|
|
43
|
+
*/
|
|
44
|
+
export const MCP_TOOL = {
|
|
45
|
+
getExecution: ["get_workflow_execution", "get_execution"],
|
|
46
|
+
searchExecutions: ["search_workflow_executions", "search_executions"],
|
|
47
|
+
preparePinData: ["prepare_workflow_pin_data", "prepare_test_pin_data"],
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The SDK's unknown-tool answer names the tool, so the match is per name: a
|
|
51
|
+
* real tool's own "… not found" (an execution, a node) never reads as this.
|
|
52
|
+
* Covers both carriers — a JSON-RPC error and an `isError` result.
|
|
53
|
+
*/
|
|
54
|
+
function isToolNotFound(err, name) {
|
|
55
|
+
return err instanceof Error && new RegExp(`\\bTool ${name} not found\\b`).test(err.message);
|
|
56
|
+
}
|
|
57
|
+
/** Per client, the name each renamed tool answered to — one probe per process, not per call. */
|
|
58
|
+
const resolvedToolName = new WeakMap();
|
|
59
|
+
/**
|
|
60
|
+
* Call a renamed tool by whichever of its names this n8n knows. Probing beats
|
|
61
|
+
* `tools/list` here: a server only ever lacks one of the names, and a probe
|
|
62
|
+
* that misses costs exactly the one round-trip a listing would. A name is
|
|
63
|
+
* remembered once the tool itself answered — a result, or its own `isError`
|
|
64
|
+
* (`McpToolError`). A transport failure, a 429 that outlasted the backoff or
|
|
65
|
+
* any other JSON-RPC error proves nothing about the name, so it is not
|
|
66
|
+
* remembered: caching it would send every later call in a multi-ref run to a
|
|
67
|
+
* name an older n8n does not have.
|
|
68
|
+
*/
|
|
69
|
+
export async function callToolByAnyName(mcp, names, args) {
|
|
70
|
+
const cache = resolvedToolName.get(mcp) ?? new Map();
|
|
71
|
+
resolvedToolName.set(mcp, cache);
|
|
72
|
+
const known = cache.get(names[0]);
|
|
73
|
+
if (known !== undefined)
|
|
74
|
+
return mcp.callTool(known, args);
|
|
75
|
+
let lastErr;
|
|
76
|
+
for (const name of names) {
|
|
77
|
+
try {
|
|
78
|
+
const out = await mcp.callTool(name, args);
|
|
79
|
+
cache.set(names[0], name);
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (!isToolNotFound(err, name)) {
|
|
84
|
+
if (err instanceof McpToolError)
|
|
85
|
+
cache.set(names[0], name);
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
lastErr = err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw lastErr;
|
|
92
|
+
}
|
|
36
93
|
/**
|
|
37
94
|
* Strictly local — the path `init` mints into and every rotation rewrites.
|
|
38
95
|
* `readAuthFilePath` is the read-side counterpart, which may resolve elsewhere.
|
|
@@ -739,25 +796,26 @@ export async function updateWorkflow(mcp, id, operations) {
|
|
|
739
796
|
}
|
|
740
797
|
/** Ask n8n for the per-node output schemas of the nodes a test run must pin (read-only). */
|
|
741
798
|
export async function prepareTestPinData(mcp, id) {
|
|
742
|
-
return mcp.
|
|
799
|
+
return callToolByAnyName(mcp, MCP_TOOL.preparePinData, { workflowId: id });
|
|
743
800
|
}
|
|
744
801
|
/**
|
|
745
802
|
* Recent executions of a workflow — **metadata only** (status/timing, no run
|
|
746
|
-
* data; `readOnlyHint`/`idempotentHint`), over MCP `
|
|
747
|
-
*
|
|
803
|
+
* data; `readOnlyHint`/`idempotentHint`), over MCP `search_workflow_executions`
|
|
804
|
+
* (`search_executions` on n8n ~2.30–2.33; shape source-verified 2026-07-22,
|
|
805
|
+
* unchanged through 2.44.5 bar an added `nextCursor`). The health signal behind
|
|
748
806
|
* `preflight`'s `history` check. The tool doesn't guarantee an order — the
|
|
749
807
|
* caller sorts/interprets. Throws (in-band `error` or an MCP `isError`) when the
|
|
750
808
|
* server lacks the tool or refuses access, so the caller can fall back to the
|
|
751
809
|
* REST executions API.
|
|
752
810
|
*/
|
|
753
811
|
export async function searchExecutions(mcp, { workflowId, status, limit }) {
|
|
754
|
-
const res = await mcp.
|
|
812
|
+
const res = await callToolByAnyName(mcp, MCP_TOOL.searchExecutions, {
|
|
755
813
|
...(workflowId !== undefined && { workflowId }),
|
|
756
814
|
...(status !== undefined && { status }),
|
|
757
815
|
...(limit !== undefined && { limit }),
|
|
758
816
|
});
|
|
759
817
|
if (res.error !== undefined)
|
|
760
|
-
throw new McpToolError(
|
|
818
|
+
throw new McpToolError(MCP_TOOL.searchExecutions[0], res.error);
|
|
761
819
|
return res.data ?? [];
|
|
762
820
|
}
|
|
763
821
|
/**
|
package/dist/lib/preflight.mjs
CHANGED
|
@@ -311,9 +311,9 @@ export async function runPreflight(ctx) {
|
|
|
311
311
|
skip(id, "sync", "workflow not available in MCP", ENABLE_MCP_HINT);
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
|
-
// history: production-run health. MCP
|
|
315
|
-
// listExecutions (no run data) fallback; neither → skip.
|
|
316
|
-
// the draft isn't guilty of the live workflow's past.
|
|
314
|
+
// history: production-run health. MCP search_workflow_executions
|
|
315
|
+
// preferred; REST listExecutions (no run data) fallback; neither → skip.
|
|
316
|
+
// Never fails — the draft isn't guilty of the live workflow's past.
|
|
317
317
|
await run("history", "sync", async () => historyCheck(ctx));
|
|
318
318
|
}
|
|
319
319
|
}
|
|
@@ -485,8 +485,8 @@ async function historyCheck(ctx) {
|
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
487
|
if (rows === undefined) {
|
|
488
|
-
const reason = ctx.restAvailable ? "
|
|
489
|
-
return { status: "skip", message: reason, reason, unlock: "upgrade n8n for
|
|
488
|
+
const reason = ctx.restAvailable ? "MCP execution search unavailable and the REST executions probe failed" : "no MCP execution search and no N8N_API_KEY for the REST fallback";
|
|
489
|
+
return { status: "skip", message: reason, reason, unlock: "upgrade n8n for MCP execution search, or set N8N_API_KEY" };
|
|
490
490
|
}
|
|
491
491
|
if (rows.length === 0)
|
|
492
492
|
return { status: "info", message: "no recent production runs" };
|
package/dist/lib/testrun.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { EXECUTIONS_DIR } from "./executions.mjs";
|
|
12
|
-
import { getWorkflowDetails, updateWorkflow } from "./mcp.mjs";
|
|
12
|
+
import { callToolByAnyName, getWorkflowDetails, MCP_TOOL, updateWorkflow } from "./mcp.mjs";
|
|
13
13
|
import { createPrompt } from "./prompt.mjs";
|
|
14
14
|
import { buildNodeCode, pushWorkflow } from "./push.mjs";
|
|
15
15
|
import { readState, writeState } from "./state.mjs";
|
|
@@ -284,7 +284,7 @@ export async function runTest(mcp, config, dir, id, { ref, source, trigger }, lo
|
|
|
284
284
|
const diffs = [];
|
|
285
285
|
let coverage;
|
|
286
286
|
if (result.executionId !== null && result.status === "success") {
|
|
287
|
-
const execution = await mcp.
|
|
287
|
+
const execution = await callToolByAnyName(mcp, MCP_TOOL.getExecution, {
|
|
288
288
|
workflowId: id,
|
|
289
289
|
executionId: result.executionId,
|
|
290
290
|
includeData: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-decanter",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "The toolkit for building code-heavy n8n workflows — agent-first, MCP-native",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"luxon": "^3.7.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@biomejs/biome": "2.5.
|
|
62
|
+
"@biomejs/biome": "2.5.15",
|
|
63
63
|
"@types/jmespath": "^0.15.2",
|
|
64
64
|
"@types/luxon": "^3.7.2",
|
|
65
65
|
"@types/node": "^26.1.1",
|
|
@@ -632,7 +632,7 @@ n8n-decanter preflight "<workflow>" --simulate --scenario <slug> # replay the
|
|
|
632
632
|
```
|
|
633
633
|
|
|
634
634
|
Add `--scaffold` to `scenario create` to annotate each gap with its **output
|
|
635
|
-
schema** (from n8n's read-only `
|
|
635
|
+
schema** (from n8n's read-only `prepare_workflow_pin_data`) — a `--scaffold` with no
|
|
636
636
|
`--execution` builds a from-scratch set where *every* pinnable node is a fill
|
|
637
637
|
entry. Provenance is tracked per node: `capture` (real, diff-asserted),
|
|
638
638
|
`authored` (hand-filled), `scaffolded` (schema-guided). A run on a scenario with
|