Skip to content

Commit f418be4

Browse files
czlonkowskiclaude
andcommitted
fix: default n8n_executions action and n8n_workflow_versions mode to list, alias retired get_node vocabulary (#1051)
Telemetry for the last week showed roughly 1,800 error_occurred events from three call shapes that carried enough information to act on. n8n_executions: action is optional and defaults to list; get without id lists the executions of the given workflow (or recent executions) and says so in the response message; delete stays strict; unknown actions get a hint when the value belongs to another tool or is a common spelling of list. The dispatch normalises action through resolveRequestedOperation, the same function the DISABLED_TOOL_OPERATIONS gate uses, and the fallback refuses to list when list is disabled. n8n_workflow_versions: mode defaults to list in the JSON schema, the Zod schema and the policy default map; required: ['mode'] is dropped. It and n8n_test_workflow accept id as an alias for workflowId (blank workflowId counts as absent, numeric id is accepted); the n8n_test_workflow missing-parameter error names the parameter and the alias. get_node: retired get_node_essentials / get_node_info vocabulary is mapped onto mode + detail before validation and logged at debug level. Schemas and docs keep advertising only the canonical values. A filtered tool schema now drops a default that names a disabled operation. Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MnKJXcGCxs2Q4P31koUzsP
1 parent a837fe1 commit f418be4

17 files changed

Lines changed: 671 additions & 35 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.79.0] - 2026-09-02
11+
12+
### Changed
13+
14+
- **The most common agent call errors now resolve instead of failing** ([#1051](https://github.com/czlonkowski/n8n-mcp/issues/1051)). Telemetry for the last week showed roughly 1,800 `error_occurred` events from three call shapes that carried enough information to act on. `n8n_executions` no longer requires `action`: an omitted value lists executions, and `action=get` without an `id` lists the executions of the given workflow (or recent executions) and says so in the response `message`; `delete` still requires `id`, and an unknown action gets a suggestion when the value belongs to another tool or is a common misspelling of `list`. `n8n_workflow_versions` defaults `mode` to `list`. `n8n_workflow_versions` and `n8n_test_workflow` accept `id` as an alias for `workflowId`, and the missing-parameter error of `n8n_test_workflow` names the parameter and the alias. `get_node` maps the retired `get_node_essentials` / `get_node_info` vocabulary onto the canonical parameters before validation: `mode` values `essentials`, `minimal`, `standard`, `full` and `operations` become `mode=info` at the matching `detail`, `properties` and `search` become `search_properties`, and `detail` values `essentials`, `summary` and `short` become `standard`, `minimal` and `minimal`. Aliases are logged at debug level; the schemas and documentation advertise only the canonical values. The `DISABLED_TOOL_OPERATIONS` policy treats an omitted `action` or `mode` as the new default, so a rule that disables `list` still applies, the `get`-without-`id` fallback refuses to list when `list` is disabled, and a filtered tool schema drops a `default` that names a disabled operation. Callers that already pass the canonical parameters see no change.
15+
1016
## [2.78.0] - 2026-09-02
1117

1218
### Added

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "n8n-mcp",
3-
"version": "2.78.0",
3+
"version": "2.79.0",
44
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

package.runtime.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "n8n-mcp-runtime",
3-
"version": "2.78.0",
3+
"version": "2.79.0",
44
"description": "n8n MCP Server Runtime Dependencies Only",
55
"private": true,
66
"dependencies": {

src/mcp/handlers-n8n-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ const cancelTestRunSchema = z.object({
608608
const versionIdValue = z.union([z.number().int(), z.string().min(1)]);
609609

610610
const workflowVersionsSchema = z.object({
611-
mode: z.enum(['list', 'get', 'rollback', 'delete', 'prune', 'diff']),
611+
mode: z.preprocess(emptyToUndefined, z.enum(['list', 'get', 'rollback', 'delete', 'prune', 'diff']).default('list')),
612612
source: z.enum(['local', 'native']).optional(),
613613
workflowId: z.string().optional(),
614614
versionId: versionIdValue.optional(),

src/mcp/param-aliases.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Aliases for parameter vocabulary that agents keep sending after a tool was
3+
* renamed or consolidated. Each map is applied before validation so a caller
4+
* using the old spelling gets the canonical behaviour instead of an error.
5+
* The tool schemas and documentation advertise only the canonical values.
6+
*/
7+
8+
import { logger } from '../utils/logger';
9+
10+
/**
11+
* `get_node` grew out of `get_node_essentials` / `get_node_info`, and prompts
12+
* written against those tools still send their vocabulary as `mode` or
13+
* `detail`. Retired values map onto the canonical `mode` + `detail` pair.
14+
*/
15+
const GET_NODE_MODE_ALIASES: Record<string, { mode: string; detail?: string }> = {
16+
essentials: { mode: 'info', detail: 'standard' },
17+
minimal: { mode: 'info', detail: 'minimal' },
18+
standard: { mode: 'info', detail: 'standard' },
19+
full: { mode: 'info', detail: 'full' },
20+
operations: { mode: 'info', detail: 'standard' },
21+
properties: { mode: 'search_properties' },
22+
search: { mode: 'search_properties' },
23+
};
24+
25+
const GET_NODE_DETAIL_ALIASES: Record<string, string> = {
26+
essentials: 'standard',
27+
summary: 'minimal',
28+
short: 'minimal',
29+
};
30+
31+
export interface GetNodeParams {
32+
mode?: string;
33+
detail?: string;
34+
}
35+
36+
/**
37+
* Resolves retired `mode` / `detail` spellings for `get_node` to canonical
38+
* values. Unknown and non-string values pass through unchanged so the
39+
* existing validation still names them in its error. Undefined stays
40+
* undefined so the handler's own defaults apply.
41+
*/
42+
export function resolveGetNodeAliases(mode?: unknown, detail?: unknown): GetNodeParams {
43+
const aliased: string[] = [];
44+
let resolvedMode = mode as string | undefined;
45+
let resolvedDetail = detail as string | undefined;
46+
47+
if (typeof resolvedDetail === 'string') {
48+
const detailAlias = GET_NODE_DETAIL_ALIASES[resolvedDetail.toLowerCase()];
49+
if (detailAlias) {
50+
aliased.push(`detail=${resolvedDetail}${detailAlias}`);
51+
resolvedDetail = detailAlias;
52+
}
53+
}
54+
55+
if (typeof resolvedMode === 'string') {
56+
const modeAlias = GET_NODE_MODE_ALIASES[resolvedMode.toLowerCase()];
57+
if (modeAlias) {
58+
// A retired mode value is the caller's statement of intent for the
59+
// detail level too (mode=full meant "everything"), so it wins over a
60+
// detail value that most clients only send because the schema defaults it.
61+
const target = modeAlias.detail
62+
? `mode=${modeAlias.mode}, detail=${modeAlias.detail}`
63+
: `mode=${modeAlias.mode}`;
64+
aliased.push(`mode=${resolvedMode}${target}`);
65+
resolvedMode = modeAlias.mode;
66+
if (modeAlias.detail) resolvedDetail = modeAlias.detail;
67+
}
68+
}
69+
70+
if (aliased.length > 0) {
71+
logger.debug(`get_node: retired parameter vocabulary aliased (${aliased.join('; ')})`);
72+
}
73+
74+
return { mode: resolvedMode, detail: resolvedDetail };
75+
}
76+
77+
/**
78+
* Suggestions for `n8n_executions` action values seen in telemetry that belong
79+
* to other tools or to no tool. Returned text is appended to the unknown-action
80+
* error so the caller can correct itself in one step.
81+
*/
82+
const EXECUTIONS_ACTION_HINTS: Record<string, string> = {
83+
get_many: "Did you mean action='list'?",
84+
getmany: "Did you mean action='list'?",
85+
getall: "Did you mean action='list'?",
86+
get_all: "Did you mean action='list'?",
87+
list_executions: "Did you mean action='list'?",
88+
listexecutions: "Did you mean action='list'?",
89+
search: "Did you mean action='list'?",
90+
get_execution: "Did you mean action='get'?",
91+
getexecution: "Did you mean action='get'?",
92+
retry: "Retrying an execution is not supported; re-run the workflow with n8n_test_workflow.",
93+
list_runs: 'Evaluation test runs are managed by n8n_evaluations.',
94+
get_run: 'Evaluation test runs are managed by n8n_evaluations.',
95+
getrows: 'Data table rows are managed by n8n_manage_datatable.',
96+
get_rows: 'Data table rows are managed by n8n_manage_datatable.',
97+
};
98+
99+
export function suggestExecutionsAction(action: string): string | undefined {
100+
return EXECUTIONS_ACTION_HINTS[action.toLowerCase()];
101+
}
102+
103+
/**
104+
* Agents send `id` and `workflowId` interchangeably for tools whose only
105+
* identifier is a workflow id. Returns a copy of the arguments with
106+
* `workflowId` filled from `id` when the canonical key is absent or blank.
107+
* `id` is not a schema property, so the server's type coercion never sees it
108+
* and a numeric value has to be accepted here.
109+
*/
110+
export function withWorkflowIdAlias<T extends Record<string, unknown>>(args: T): T {
111+
if (typeof args.workflowId === 'string' && args.workflowId.trim() !== '') {
112+
return args;
113+
}
114+
const id = typeof args.id === 'number' ? String(args.id) : args.id;
115+
if (typeof id !== 'string' || id.trim() === '') {
116+
return args;
117+
}
118+
return { ...args, workflowId: id };
119+
}

src/mcp/server.ts

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '@modelcontextprotocol/sdk/types.js';
1111
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
1212
import type { ToolDefinition } from '../types';
13+
import type { McpToolResponse } from '../types/n8n-api';
1314
import { existsSync, readFileSync, promises as fs } from 'fs';
1415
import path from 'path';
1516
import { n8nDocumentationToolsFinal } from './tools';
@@ -20,11 +21,13 @@ import {
2021
getDisabledTools as getDisabledToolsPolicy,
2122
getDisabledToolOperations as getDisabledToolOperationsPolicy,
2223
getValidOperations,
24+
isOperationDisabled,
2325
resolveRequestedOperation,
2426
} from './tool-policy';
2527
import { makeToolsN8nFriendly } from './tools-n8n-friendly';
2628
import { getWorkflowExampleString } from './workflow-examples';
2729
import { logger } from '../utils/logger';
30+
import { resolveGetNodeAliases, suggestExecutionsAction, withWorkflowIdAlias } from './param-aliases';
2831
import { installStdioGuard } from '../utils/stdio-guard';
2932
import { summarizeToolCallArgs } from '../utils/redaction';
3033
import { NodeRepository } from '../database/node-repository';
@@ -675,6 +678,9 @@ export class N8NDocumentationMCPServer {
675678
const param = cloned.inputSchema?.properties?.[paramName];
676679
if (param?.enum) {
677680
param.enum = (param.enum as string[]).filter(v => !ops.has(v.toLowerCase()));
681+
if (typeof param.default === 'string' && ops.has(param.default.toLowerCase())) {
682+
delete param.default;
683+
}
678684
if (param.enum.length === 0) {
679685
logger.warn(
680686
`DISABLED_TOOL_OPERATIONS: all operations for '${toolName}' are disabled ` +
@@ -1291,10 +1297,19 @@ export class N8NDocumentationMCPServer {
12911297
validationResult = ToolValidation.validateWorkflowId(args);
12921298
break;
12931299
case 'n8n_executions':
1294-
// Requires action parameter, id validation done in handler based on action
1295-
validationResult = args.action
1300+
// action defaults to list; id validation is done in dispatch based on action
1301+
validationResult = { valid: true, errors: [] };
1302+
break;
1303+
case 'n8n_test_workflow':
1304+
validationResult = typeof args.workflowId === 'string' && args.workflowId.trim() !== ''
12961305
? { valid: true, errors: [] }
1297-
: { valid: false, errors: [{ field: 'action', message: 'action is required' }] };
1306+
: {
1307+
valid: false,
1308+
errors: [{
1309+
field: 'workflowId',
1310+
message: 'workflowId is required: the ID of the workflow to run ("id" is accepted as an alias)'
1311+
}]
1312+
};
12981313
break;
12991314
case 'n8n_evaluations': {
13001315
// Every action of this tool requires action and workflowId;
@@ -1576,6 +1591,26 @@ export class N8NDocumentationMCPServer {
15761591
return coerced;
15771592
}
15781593

1594+
/**
1595+
* `n8n_executions` with action=get but no execution id is the most frequent
1596+
* agent call error in telemetry, and the caller wants the listing. Serve it
1597+
* and say so, rather than failing the call.
1598+
*/
1599+
private async listExecutionsInsteadOfGet(args: any): Promise<McpToolResponse> {
1600+
// The policy gate checked this call as `get`; the fallback must not open a
1601+
// listing that a DISABLED_TOOL_OPERATIONS rule has closed.
1602+
if (isOperationDisabled('n8n_executions', 'list')) {
1603+
throw new Error('id is required for action=get');
1604+
}
1605+
const result = await n8nHandlers.handleListExecutions(args, this.instanceContext);
1606+
if (!result.success) return result;
1607+
const scope = args.workflowId ? `executions of workflow ${args.workflowId}` : 'recent executions';
1608+
return {
1609+
...result,
1610+
message: `action=get was called without an execution id, so ${scope} were listed instead. Pass id to get one execution.`
1611+
};
1612+
}
1613+
15791614
async executeTool(name: string, args: any): Promise<any> {
15801615
// Ensure args is an object and validate it
15811616
args = args || {};
@@ -1628,13 +1663,15 @@ export class N8NDocumentationMCPServer {
16281663
includeOperations: args.includeOperations,
16291664
source: args.source
16301665
});
1631-
case 'get_node':
1666+
case 'get_node': {
16321667
this.validateToolParams(name, args, ['nodeType']);
1668+
// Retired get_node_essentials / get_node_info vocabulary maps onto mode + detail
1669+
const { mode: nodeMode, detail: nodeDetail } = resolveGetNodeAliases(args.mode, args.detail);
16331670
// Handle consolidated modes: docs, search_properties
1634-
if (args.mode === 'docs') {
1671+
if (nodeMode === 'docs') {
16351672
return this.getNodeDocumentation(args.nodeType);
16361673
}
1637-
if (args.mode === 'search_properties') {
1674+
if (nodeMode === 'search_properties') {
16381675
if (!args.propertyQuery) {
16391676
throw new Error('propertyQuery is required for mode=search_properties');
16401677
}
@@ -1643,13 +1680,14 @@ export class N8NDocumentationMCPServer {
16431680
}
16441681
return this.getNode(
16451682
args.nodeType,
1646-
args.detail,
1647-
args.mode,
1683+
nodeDetail,
1684+
nodeMode,
16481685
args.includeTypeInfo,
16491686
args.includeExamples,
16501687
args.fromVersion,
16511688
args.toVersion
16521689
);
1690+
}
16531691
case 'validate_node':
16541692
this.validateToolParams(name, args, ['nodeType', 'config']);
16551693
// Ensure config is an object
@@ -1792,16 +1830,21 @@ export class N8NDocumentationMCPServer {
17921830
await this.ensureInitialized();
17931831
if (!this.repository) throw new Error('Repository not initialized');
17941832
return n8nHandlers.handleAutofixWorkflow(args, this.repository, this.instanceContext);
1795-
case 'n8n_test_workflow':
1796-
this.validateToolParams(name, args, ['workflowId']);
1797-
return n8nHandlers.handleTestWorkflow(args, this.instanceContext);
1833+
case 'n8n_test_workflow': {
1834+
const testArgs = withWorkflowIdAlias(args);
1835+
this.validateToolParams(name, testArgs);
1836+
return n8nHandlers.handleTestWorkflow(testArgs, this.instanceContext);
1837+
}
17981838
case 'n8n_executions': {
1799-
this.validateToolParams(name, args, ['action']);
1800-
const execAction = args.action;
1839+
this.validateToolParams(name, args);
1840+
// Agents that only want a listing often omit action or send get without an id.
1841+
// The same normalisation the policy gate uses, so a disabled-operation rule
1842+
// and the dispatch always see the same value.
1843+
const execAction = String(resolveRequestedOperation(name, args));
18011844
switch (execAction) {
18021845
case 'get':
18031846
if (!args.id) {
1804-
throw new Error('id is required for action=get');
1847+
return this.listExecutionsInsteadOfGet(args);
18051848
}
18061849
return n8nHandlers.handleGetExecution(args, this.instanceContext);
18071850
case 'list':
@@ -1811,8 +1854,11 @@ export class N8NDocumentationMCPServer {
18111854
throw new Error('id is required for action=delete');
18121855
}
18131856
return n8nHandlers.handleDeleteExecution(args, this.instanceContext);
1814-
default:
1815-
throw new Error(`Unknown action: ${execAction}. Valid actions: get, list, delete`);
1857+
default: {
1858+
const message = `Unknown action: ${execAction}. Valid actions: get, list, delete.`;
1859+
const hint = suggestExecutionsAction(execAction);
1860+
throw new Error(hint ? `${message} ${hint}` : message);
1861+
}
18161862
}
18171863
}
18181864
case 'n8n_evaluations': {
@@ -1849,8 +1895,8 @@ export class N8NDocumentationMCPServer {
18491895
}
18501896
return n8nHandlers.handleHealthCheck(this.instanceContext);
18511897
case 'n8n_workflow_versions':
1852-
this.validateToolParams(name, args, ['mode']);
1853-
return n8nHandlers.handleWorkflowVersions(args, this.repository!, this.instanceContext);
1898+
// mode defaults to list in the handler schema; workflowId is filled from id
1899+
return n8nHandlers.handleWorkflowVersions(withWorkflowIdAlias(args), this.repository!, this.instanceContext);
18541900

18551901
case 'n8n_deploy_template':
18561902
this.validateToolParams(name, args, ['templateId']);

src/mcp/tool-docs/workflow_management/n8n-executions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const n8nExecutionsDoc: ToolDocumentation = {
3636
- Provides AI-friendly fix suggestions based on error patterns
3737
- Token-efficient (80-90% smaller than full mode)`,
3838
parameters: {
39-
action: { type: 'string', required: true, description: 'Operation: "get", "list", or "delete"' },
39+
action: { type: 'string', required: false, description: 'Operation: "get", "list" (default), or "delete". "get" without an id lists executions instead' },
4040
id: { type: 'string', required: false, description: 'Execution ID (required for action=get or action=delete)' },
4141
mode: { type: 'string', required: false, description: 'For action=get: "preview", "summary" (default), "filtered", "full", "error"' },
4242
nodeNames: { type: 'array', required: false, description: 'For action=get with mode=filtered: Filter to specific nodes by name' },

src/mcp/tool-docs/workflow_management/n8n-test-workflow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Every response states \`method\` and \`backend\` ('public-api' or 'official-mcp'
5252
workflowId: {
5353
type: 'string',
5454
required: true,
55-
description: 'Workflow ID to execute'
55+
description: 'Workflow ID to execute ("id" is accepted as an alias)'
5656
},
5757
method: {
5858
type: 'string',

src/mcp/tool-docs/workflow_management/n8n-workflow-versions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ per workflow, plus an age-based retention window). Native history retention is n
9797
parameters: {
9898
mode: {
9999
type: 'string',
100-
required: true,
101-
description: 'Operation mode: "list", "get", "rollback", "diff", "delete", or "prune"',
100+
required: false,
101+
description: 'Operation mode: "list" (default), "get", "rollback", "diff", "delete", or "prune"',
102102
enum: ['list', 'get', 'rollback', 'delete', 'prune', 'diff']
103103
},
104104
source: {

0 commit comments

Comments
 (0)