Compare commits

..

3 Commits

Author SHA1 Message Date
czlonkowski
4d4ef64bf6 fix: data tables available on self-hosted too, not just enterprise/cloud
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:26:24 +01:00
czlonkowski
984a49d4ec fix: address code review - add spec-compliance comment and mixed-batch test
- Document design choice: !('id' in msg) is strict per JSON-RPC 2.0 §4.1
- Add test: mixed batch (notification + request) with stale session → 400

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:45:57 +01:00
czlonkowski
e52a04a291 fix: return 202 for notifications with stale sessions, warn on updateTable columns (#654)
**Issue #654 (critical):** handleRequest() returned HTTP 400 for JSON-RPC
notifications with stale/expired session IDs. Per JSON-RPC 2.0 spec,
notifications don't expect responses. Returning 400 caused Claude's proxy
to interpret the connection as broken, triggering reconnection storms
(930 errors/day, 216 users affected).

Fix: Added isJsonRpcNotification() helper that detects single and batch
notifications (messages with method but no id field). Path C in
handleRequest() now returns 202 Accepted for notifications with stale
sessions instead of 400. Also added TOCTOU null guard in Path B.

**Issue 1 (medium):** updateTable silently accepted columns parameter
via the shared MCP tool schema. Zod stripped it but gave no feedback.

Fix: handleUpdateTable now detects columns in raw args and appends a
warning to the response. Tool schema descriptions clarified that columns
are for createTable only and schema is immutable after creation.

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 19:21:29 +01:00
9 changed files with 8 additions and 149 deletions

View File

@@ -7,23 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [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
### Fixed

View File

@@ -1 +1 @@
{"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"}
{"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"}

View File

@@ -1,7 +1,4 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultWorkflowSettings = exports.workflowSettingsSchema = exports.workflowConnectionSchema = exports.workflowNodeSchema = void 0;
exports.validateWorkflowNode = validateWorkflowNode;
@@ -16,7 +13,6 @@ exports.validateOperatorStructure = validateOperatorStructure;
exports.getWebhookUrl = getWebhookUrl;
exports.getWorkflowStructureExample = getWorkflowStructureExample;
exports.getWorkflowFixSuggestions = getWorkflowFixSuggestions;
const crypto_1 = __importDefault(require("crypto"));
const zod_1 = require("zod");
const node_type_utils_1 = require("../utils/node-type-utils");
const node_classification_1 = require("../utils/node-classification");
@@ -80,27 +76,11 @@ function validateWorkflowConnections(connections) {
function validateWorkflowSettings(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) {
const { id, createdAt, updatedAt, versionId, meta, active, tags, ...cleanedWorkflow } = workflow;
if (!cleanedWorkflow.settings || Object.keys(cleanedWorkflow.settings).length === 0) {
cleanedWorkflow.settings = exports.defaultWorkflowSettings;
}
ensureWebhookIds(cleanedWorkflow.nodes);
return cleanedWorkflow;
}
function cleanWorkflowForUpdate(workflow) {
@@ -136,7 +116,6 @@ function cleanWorkflowForUpdate(workflow) {
else {
cleanedWorkflow.settings = { executionOrder: 'v1' };
}
ensureWebhookIds(cleanedWorkflow.nodes);
return cleanedWorkflow;
}
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",
"version": "2.40.5",
"version": "2.37.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "n8n-mcp",
"version": "2.40.5",
"version": "2.37.3",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1",

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp",
"version": "2.40.5",
"version": "2.40.3",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"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',
'Filter supports: eq, neq, like, ilike, gt, gte, lt, lte conditions',
'Use returnData: true to get affected rows back from update/upsert/delete',
'Requires N8N_API_URL and N8N_API_KEY configured'
'Requires n8n instance with data tables feature enabled'
]
},
full: {
@@ -96,6 +96,8 @@ export const n8nManageDatatableDoc: ToolDocumentation = {
'Use sortBy for deterministic row ordering',
],
pitfalls: [
'Requires N8N_API_URL and N8N_API_KEY configured',
'Requires n8n instance with data tables feature enabled (available on cloud, enterprise, and self-hosted)',
'deleteTable permanently deletes all rows — cannot be undone',
'deleteRows requires a filter — cannot delete all rows without one',
'Column types cannot be changed after table creation via API',

View File

@@ -1,4 +1,3 @@
import crypto from 'crypto';
import { z } from 'zod';
import { WorkflowNode, WorkflowConnection, Workflow } from '../types/n8n-api';
import { isTriggerNode, isActivatableTrigger } from '../utils/node-type-utils';
@@ -88,22 +87,6 @@ export function validateWorkflowSettings(settings: unknown): z.infer<typeof work
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
export function cleanWorkflowForCreate(workflow: Partial<Workflow>): Partial<Workflow> {
const {
@@ -126,8 +109,6 @@ export function cleanWorkflowForCreate(workflow: Partial<Workflow>): Partial<Wor
cleanedWorkflow.settings = defaultWorkflowSettings;
}
ensureWebhookIds(cleanedWorkflow.nodes);
return cleanedWorkflow;
}
@@ -213,8 +194,6 @@ export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> {
cleanedWorkflow.settings = { executionOrder: 'v1' as const };
}
ensureWebhookIds(cleanedWorkflow.nodes);
return cleanedWorkflow;
}

View File

@@ -19,14 +19,6 @@ import { WorkflowBuilder } from '../../utils/builders/workflow.builder';
import { z } from 'zod';
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('Zod Schemas', () => {
describe('workflowNodeSchema', () => {
@@ -309,44 +301,6 @@ describe('n8n-validation', () => {
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
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', () => {
@@ -579,44 +533,6 @@ describe('n8n-validation', () => {
});
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();
});
});
});