Compare commits

...

2 Commits

Author SHA1 Message Date
Romuald Członkowski
1f0738e637 fix: auto-inject webhookId on webhook nodes during create/update (#643) (#657)
n8n 2.10+ requires webhookId (UUID) on webhook-type nodes for proper
webhook URL registration. Without it, webhooks silently fail with 404.
The n8n UI always generates webhookId but programmatic creation via
n8n-mcp did not.

Add ensureWebhookIds() helper that injects crypto.randomUUID() on
webhook, webhookTrigger, formTrigger, and chatTrigger nodes when
webhookId is missing. Called from both cleanWorkflowForCreate() and
cleanWorkflowForUpdate(). Existing webhookId values are preserved.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:20:34 +01:00
Romuald Członkowski
93816fce30 fix: data tables available on all n8n plans, remove redundant pitfalls (#656)
Data tables are available on self-hosted n8n too, not just enterprise/cloud.
Removed incorrect availability restriction from tool description and docs.
Removed redundant pitfalls (API key requirement implicit, plan restriction wrong).

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:29:19 +01:00
10 changed files with 150 additions and 9 deletions

View File

@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [2.40.5] - 2026-03-22
### Fixed
- **Webhook workflows created via MCP get 404 errors** (Issue #643): Auto-inject `webhookId` (UUID) on webhook-type nodes (`webhook`, `webhookTrigger`, `formTrigger`, `chatTrigger`) during `cleanWorkflowForCreate()` and `cleanWorkflowForUpdate()`. n8n 2.10+ requires this field for proper webhook URL registration; without it, webhooks silently fail with 404. Existing `webhookId` values are preserved.
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
## [2.40.4] - 2026-03-22
### Fixed
- **Incorrect data tables availability info**: Removed "enterprise/cloud only" restriction from tool description and documentation — data tables are available on all n8n plans including self-hosted
- **Redundant pitfalls removed**: Removed "Requires N8N_API_URL and N8N_API_KEY" and "enterprise or cloud plans" pitfalls — the first is implicit for all n8n management tools, the second was incorrect
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
## [2.40.3] - 2026-03-22 ## [2.40.3] - 2026-03-22
### Fixed ### Fixed

View File

@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"} {"version":3,"file":"n8n-validation.d.ts","sourceRoot":"","sources":["../../src/services/n8n-validation.ts"],"names":[],"mappings":"AACA,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;AAmBD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAwBrF;AAiBD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsE5E;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"}

View File

@@ -1,4 +1,7 @@
"use strict"; "use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultWorkflowSettings = exports.workflowSettingsSchema = exports.workflowConnectionSchema = exports.workflowNodeSchema = void 0; exports.defaultWorkflowSettings = exports.workflowSettingsSchema = exports.workflowConnectionSchema = exports.workflowNodeSchema = void 0;
exports.validateWorkflowNode = validateWorkflowNode; exports.validateWorkflowNode = validateWorkflowNode;
@@ -13,6 +16,7 @@ exports.validateOperatorStructure = validateOperatorStructure;
exports.getWebhookUrl = getWebhookUrl; exports.getWebhookUrl = getWebhookUrl;
exports.getWorkflowStructureExample = getWorkflowStructureExample; exports.getWorkflowStructureExample = getWorkflowStructureExample;
exports.getWorkflowFixSuggestions = getWorkflowFixSuggestions; exports.getWorkflowFixSuggestions = getWorkflowFixSuggestions;
const crypto_1 = __importDefault(require("crypto"));
const zod_1 = require("zod"); const zod_1 = require("zod");
const node_type_utils_1 = require("../utils/node-type-utils"); const node_type_utils_1 = require("../utils/node-type-utils");
const node_classification_1 = require("../utils/node-classification"); const node_classification_1 = require("../utils/node-classification");
@@ -76,11 +80,27 @@ function validateWorkflowConnections(connections) {
function validateWorkflowSettings(settings) { function validateWorkflowSettings(settings) {
return exports.workflowSettingsSchema.parse(settings); return exports.workflowSettingsSchema.parse(settings);
} }
const WEBHOOK_NODE_TYPES = new Set([
'n8n-nodes-base.webhook',
'n8n-nodes-base.webhookTrigger',
'n8n-nodes-base.formTrigger',
'@n8n/n8n-nodes-langchain.chatTrigger',
]);
function ensureWebhookIds(nodes) {
if (!nodes)
return;
for (const node of nodes) {
if (WEBHOOK_NODE_TYPES.has(node.type) && !node.webhookId) {
node.webhookId = crypto_1.default.randomUUID();
}
}
}
function cleanWorkflowForCreate(workflow) { function cleanWorkflowForCreate(workflow) {
const { id, createdAt, updatedAt, versionId, meta, active, tags, ...cleanedWorkflow } = workflow; const { id, createdAt, updatedAt, versionId, meta, active, tags, ...cleanedWorkflow } = workflow;
if (!cleanedWorkflow.settings || Object.keys(cleanedWorkflow.settings).length === 0) { if (!cleanedWorkflow.settings || Object.keys(cleanedWorkflow.settings).length === 0) {
cleanedWorkflow.settings = exports.defaultWorkflowSettings; cleanedWorkflow.settings = exports.defaultWorkflowSettings;
} }
ensureWebhookIds(cleanedWorkflow.nodes);
return cleanedWorkflow; return cleanedWorkflow;
} }
function cleanWorkflowForUpdate(workflow) { function cleanWorkflowForUpdate(workflow) {
@@ -116,6 +136,7 @@ function cleanWorkflowForUpdate(workflow) {
else { else {
cleanedWorkflow.settings = { executionOrder: 'v1' }; cleanedWorkflow.settings = { executionOrder: 'v1' };
} }
ensureWebhookIds(cleanedWorkflow.nodes);
return cleanedWorkflow; return cleanedWorkflow;
} }
function validateWorkflowStructure(workflow) { function validateWorkflowStructure(workflow) {

File diff suppressed because one or more lines are too long

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.37.3", "version": "2.40.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.37.3", "version": "2.40.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1", "@modelcontextprotocol/sdk": "^1.27.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.40.3", "version": "2.40.5",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -14,7 +14,7 @@ export const n8nManageDatatableDoc: ToolDocumentation = {
'Use dryRun: true to preview update/upsert/delete before applying', 'Use dryRun: true to preview update/upsert/delete before applying',
'Filter supports: eq, neq, like, ilike, gt, gte, lt, lte conditions', 'Filter supports: eq, neq, like, ilike, gt, gte, lt, lte conditions',
'Use returnData: true to get affected rows back from update/upsert/delete', 'Use returnData: true to get affected rows back from update/upsert/delete',
'Requires n8n enterprise or cloud with data tables feature' 'Requires N8N_API_URL and N8N_API_KEY configured'
] ]
}, },
full: { full: {
@@ -96,8 +96,6 @@ export const n8nManageDatatableDoc: ToolDocumentation = {
'Use sortBy for deterministic row ordering', 'Use sortBy for deterministic row ordering',
], ],
pitfalls: [ pitfalls: [
'Requires N8N_API_URL and N8N_API_KEY configured',
'Feature only available on n8n enterprise or cloud plans',
'deleteTable permanently deletes all rows — cannot be undone', 'deleteTable permanently deletes all rows — cannot be undone',
'deleteRows requires a filter — cannot delete all rows without one', 'deleteRows requires a filter — cannot delete all rows without one',
'Column types cannot be changed after table creation via API', 'Column types cannot be changed after table creation via API',

View File

@@ -609,7 +609,7 @@ export const n8nManagementTools: ToolDefinition[] = [
}, },
{ {
name: 'n8n_manage_datatable', name: 'n8n_manage_datatable',
description: `Manage n8n data tables and rows. Actions: createTable, listTables, getTable, updateTable, deleteTable, getRows, insertRows, updateRows, upsertRows, deleteRows. Requires n8n enterprise/cloud with data tables feature.`, description: `Manage n8n data tables and rows. Actions: createTable, listTables, getTable, updateTable, deleteTable, getRows, insertRows, updateRows, upsertRows, deleteRows.`,
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {

View File

@@ -1,3 +1,4 @@
import crypto from 'crypto';
import { z } from 'zod'; import { z } from 'zod';
import { WorkflowNode, WorkflowConnection, Workflow } from '../types/n8n-api'; import { WorkflowNode, WorkflowConnection, Workflow } from '../types/n8n-api';
import { isTriggerNode, isActivatableTrigger } from '../utils/node-type-utils'; import { isTriggerNode, isActivatableTrigger } from '../utils/node-type-utils';
@@ -87,6 +88,22 @@ export function validateWorkflowSettings(settings: unknown): z.infer<typeof work
return workflowSettingsSchema.parse(settings); return workflowSettingsSchema.parse(settings);
} }
const WEBHOOK_NODE_TYPES = new Set([
'n8n-nodes-base.webhook',
'n8n-nodes-base.webhookTrigger',
'n8n-nodes-base.formTrigger',
'@n8n/n8n-nodes-langchain.chatTrigger',
]);
function ensureWebhookIds(nodes?: WorkflowNode[]): void {
if (!nodes) return;
for (const node of nodes) {
if (WEBHOOK_NODE_TYPES.has(node.type) && !node.webhookId) {
node.webhookId = crypto.randomUUID();
}
}
}
// Clean workflow data for API operations // Clean workflow data for API operations
export function cleanWorkflowForCreate(workflow: Partial<Workflow>): Partial<Workflow> { export function cleanWorkflowForCreate(workflow: Partial<Workflow>): Partial<Workflow> {
const { const {
@@ -109,6 +126,8 @@ export function cleanWorkflowForCreate(workflow: Partial<Workflow>): Partial<Wor
cleanedWorkflow.settings = defaultWorkflowSettings; cleanedWorkflow.settings = defaultWorkflowSettings;
} }
ensureWebhookIds(cleanedWorkflow.nodes);
return cleanedWorkflow; return cleanedWorkflow;
} }
@@ -194,6 +213,8 @@ export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> {
cleanedWorkflow.settings = { executionOrder: 'v1' as const }; cleanedWorkflow.settings = { executionOrder: 'v1' as const };
} }
ensureWebhookIds(cleanedWorkflow.nodes);
return cleanedWorkflow; return cleanedWorkflow;
} }

View File

@@ -19,6 +19,14 @@ import { WorkflowBuilder } from '../../utils/builders/workflow.builder';
import { z } from 'zod'; import { z } from 'zod';
import { WorkflowNode, WorkflowConnection, Workflow } from '../../../src/types/n8n-api'; import { WorkflowNode, WorkflowConnection, Workflow } from '../../../src/types/n8n-api';
function webhookNode(id: string, name: string, type: string, typeVersion = 2): WorkflowNode {
return { id, name, type, typeVersion, position: [250, 300] as [number, number], parameters: {} };
}
function workflowWithNodes(nodes: WorkflowNode[]): Partial<Workflow> {
return { name: 'Test', nodes, connections: {} };
}
describe('n8n-validation', () => { describe('n8n-validation', () => {
describe('Zod Schemas', () => { describe('Zod Schemas', () => {
describe('workflowNodeSchema', () => { describe('workflowNodeSchema', () => {
@@ -301,6 +309,44 @@ describe('n8n-validation', () => {
const cleaned = cleanWorkflowForCreate(workflow as Workflow); const cleaned = cleanWorkflowForCreate(workflow as Workflow);
expect(cleaned.settings).toEqual(customSettings); expect(cleaned.settings).toEqual(customSettings);
}); });
it('should inject webhookId on webhook nodes missing it', () => {
const workflow = workflowWithNodes([
webhookNode('1', 'Webhook', 'n8n-nodes-base.webhook'),
]);
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
expect(cleaned.nodes![0].webhookId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
});
it('should preserve existing webhookId on webhook nodes', () => {
const workflow = workflowWithNodes([
{ ...webhookNode('1', 'Webhook', 'n8n-nodes-base.webhook'), webhookId: 'existing-id' },
]);
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
expect(cleaned.nodes![0].webhookId).toBe('existing-id');
});
it('should inject webhookId on formTrigger and chatTrigger nodes', () => {
const workflow = workflowWithNodes([
webhookNode('1', 'Form', 'n8n-nodes-base.formTrigger'),
webhookNode('2', 'Chat', '@n8n/n8n-nodes-langchain.chatTrigger'),
]);
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
expect(cleaned.nodes![0].webhookId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
expect(cleaned.nodes![1].webhookId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
});
it('should not inject webhookId on non-webhook nodes', () => {
const workflow = workflowWithNodes([
webhookNode('1', 'Set', 'n8n-nodes-base.set', 3.4),
]);
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
expect(cleaned.nodes![0].webhookId).toBeUndefined();
});
}); });
describe('cleanWorkflowForUpdate', () => { describe('cleanWorkflowForUpdate', () => {
@@ -533,6 +579,44 @@ describe('n8n-validation', () => {
}); });
expect(cleaned.settings).not.toHaveProperty('someOtherProperty'); expect(cleaned.settings).not.toHaveProperty('someOtherProperty');
}); });
it('should inject webhookId on webhook nodes missing it', () => {
const workflow = workflowWithNodes([
webhookNode('1', 'Webhook', 'n8n-nodes-base.webhook'),
]) as any;
const cleaned = cleanWorkflowForUpdate(workflow);
expect(cleaned.nodes![0].webhookId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
});
it('should preserve existing webhookId on webhook nodes', () => {
const workflow = workflowWithNodes([
{ ...webhookNode('1', 'Webhook', 'n8n-nodes-base.webhook'), webhookId: 'existing-id' },
]) as any;
const cleaned = cleanWorkflowForUpdate(workflow);
expect(cleaned.nodes![0].webhookId).toBe('existing-id');
});
it('should inject webhookId on formTrigger and chatTrigger nodes', () => {
const workflow = workflowWithNodes([
webhookNode('1', 'Form', 'n8n-nodes-base.formTrigger'),
webhookNode('2', 'Chat', '@n8n/n8n-nodes-langchain.chatTrigger'),
]) as any;
const cleaned = cleanWorkflowForUpdate(workflow);
expect(cleaned.nodes![0].webhookId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
expect(cleaned.nodes![1].webhookId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
});
it('should not inject webhookId on non-webhook nodes', () => {
const workflow = workflowWithNodes([
webhookNode('1', 'Set', 'n8n-nodes-base.set', 3.4),
]) as any;
const cleaned = cleanWorkflowForUpdate(workflow);
expect(cleaned.nodes![0].webhookId).toBeUndefined();
});
}); });
}); });