@pagelines/n8n-mcp 0.3.0 → 0.3.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 CHANGED
@@ -2,18 +2,34 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.3.2] - 2025-01-13
6
+
7
+ ### Removed
8
+ - `node_types_list` tool - n8n API doesn't reliably expose node types across versions
9
+ - Node type pre-validation from `workflow_create` and `workflow_update` - without reliable API, validation was causing false 404 errors
10
+
11
+ ### Fixed
12
+ - `workflow_update` failing with "request/body/tags is read-only" error
13
+ - Removed `tags` from `N8N_WORKFLOW_WRITABLE_FIELDS` since it's read-only in some n8n versions
14
+
15
+ ## [0.3.1] - 2025-01-13
16
+
17
+ ### Fixed
18
+ - **Critical bug**: `workflow_update`, `workflow_format`, and `workflow_autofix` failing with "request/body must NOT have additional properties" error
19
+ - Root cause: n8n API returns additional read-only properties that were being sent back on PUT requests
20
+ - Solution: Schema-driven field filtering using `N8N_WORKFLOW_WRITABLE_FIELDS` allowlist (source of truth: n8n OpenAPI spec at `/api/v1/openapi.yml`)
21
+
22
+ ### Changed
23
+ - Refactored `updateWorkflow` to use schema-driven approach instead of property denylist
24
+ - Added `pickFields` generic utility for type-safe field filtering
25
+ - Added comprehensive tests for schema-driven filtering
26
+
5
27
  ## [0.3.0] - 2025-01-13
6
28
 
7
29
  ### Added
8
30
 
9
- #### Node Type Discovery & Validation
10
- - `node_types_list` - Search available node types by name/category
11
- - Pre-validation blocks invalid node types before `workflow_create` and `workflow_update`
12
- - Fuzzy matching suggests correct types when invalid types detected
13
-
14
31
  #### Auto-Cleanup Pipeline
15
32
  - Every `workflow_create` and `workflow_update` now automatically:
16
- - Validates node types (blocks if invalid)
17
33
  - Runs validation rules
18
34
  - Auto-fixes fixable issues (snake_case, $json refs, AI settings)
19
35
  - Formats workflow (sorts nodes, removes nulls)
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
9
9
  import { N8nClient } from './n8n-client.js';
10
10
  import { tools } from './tools.js';
11
- import { validateWorkflow, validateNodeTypes } from './validators.js';
11
+ import { validateWorkflow } from './validators.js';
12
12
  import { validateExpressions, checkCircularReferences } from './expressions.js';
13
13
  import { autofixWorkflow, formatWorkflow } from './autofix.js';
14
14
  import { initVersionControl, saveVersion, listVersions, getVersion, diffWorkflows, getVersionStats, } from './versions.js';
@@ -37,7 +37,7 @@ initVersionControl({
37
37
  // ─────────────────────────────────────────────────────────────
38
38
  const server = new Server({
39
39
  name: '@pagelines/n8n-mcp',
40
- version: '0.3.0',
40
+ version: '0.3.2',
41
41
  }, {
42
42
  capabilities: {
43
43
  tools: {},
@@ -103,21 +103,6 @@ async function handleTool(name, args) {
103
103
  }
104
104
  case 'workflow_create': {
105
105
  const inputNodes = args.nodes;
106
- // Validate node types BEFORE creating workflow
107
- const availableTypes = await client.listNodeTypes();
108
- const validTypeSet = new Set(availableTypes.map((nt) => nt.name));
109
- const typeErrors = validateNodeTypes(inputNodes, validTypeSet);
110
- if (typeErrors.length > 0) {
111
- const errorMessages = typeErrors.map((e) => {
112
- let msg = e.message;
113
- if (e.suggestions && e.suggestions.length > 0) {
114
- msg += `. Did you mean: ${e.suggestions.join(', ')}?`;
115
- }
116
- return msg;
117
- });
118
- throw new Error(`Invalid node types detected:\n${errorMessages.join('\n')}\n\n` +
119
- `Use node_types_list to discover available node types.`);
120
- }
121
106
  const nodes = inputNodes.map((n, i) => ({
122
107
  id: crypto.randomUUID(),
123
108
  name: n.name,
@@ -154,29 +139,6 @@ async function handleTool(name, args) {
154
139
  }
155
140
  case 'workflow_update': {
156
141
  const operations = args.operations;
157
- // Extract addNode operations that need validation
158
- const addNodeOps = operations.filter((op) => op.type === 'addNode');
159
- if (addNodeOps.length > 0) {
160
- // Fetch available types and validate
161
- const availableTypes = await client.listNodeTypes();
162
- const validTypeSet = new Set(availableTypes.map((nt) => nt.name));
163
- const nodesToValidate = addNodeOps.map((op) => ({
164
- name: op.node.name,
165
- type: op.node.type,
166
- }));
167
- const typeErrors = validateNodeTypes(nodesToValidate, validTypeSet);
168
- if (typeErrors.length > 0) {
169
- const errorMessages = typeErrors.map((e) => {
170
- let msg = e.message;
171
- if (e.suggestions && e.suggestions.length > 0) {
172
- msg += `. Did you mean: ${e.suggestions.join(', ')}?`;
173
- }
174
- return msg;
175
- });
176
- throw new Error(`Invalid node types in addNode operations:\n${errorMessages.join('\n')}\n\n` +
177
- `Use node_types_list to discover available node types.`);
178
- }
179
- }
180
142
  // Save version before updating
181
143
  const currentWorkflow = await client.getWorkflow(args.id);
182
144
  const versionSaved = await saveVersion(currentWorkflow, 'before_update');
@@ -297,37 +259,6 @@ async function handleTool(name, args) {
297
259
  previewWorkflow: formatted,
298
260
  };
299
261
  }
300
- // Node Discovery
301
- case 'node_types_list': {
302
- const nodeTypes = await client.listNodeTypes();
303
- const search = args.search?.toLowerCase();
304
- const category = args.category;
305
- const limit = args.limit || 50;
306
- let results = nodeTypes.map((nt) => ({
307
- type: nt.name,
308
- name: nt.displayName,
309
- description: nt.description,
310
- category: nt.codex?.categories?.[0] || nt.group?.[0] || 'Other',
311
- version: nt.version,
312
- }));
313
- // Apply search filter
314
- if (search) {
315
- results = results.filter((nt) => nt.type.toLowerCase().includes(search) ||
316
- nt.name.toLowerCase().includes(search) ||
317
- nt.description.toLowerCase().includes(search));
318
- }
319
- // Apply category filter
320
- if (category) {
321
- results = results.filter((nt) => nt.category.toLowerCase().includes(category.toLowerCase()));
322
- }
323
- // Apply limit
324
- results = results.slice(0, limit);
325
- return {
326
- nodeTypes: results,
327
- total: results.length,
328
- hint: 'Use the "type" field value when creating nodes (e.g., "n8n-nodes-base.webhook")',
329
- };
330
- }
331
262
  // Version Control
332
263
  case 'version_list': {
333
264
  const versions = await listVersions(args.workflowId);
@@ -2,7 +2,7 @@
2
2
  * n8n REST API Client
3
3
  * Clean, minimal implementation with built-in safety checks
4
4
  */
5
- import type { N8nWorkflow, N8nWorkflowListItem, N8nExecution, N8nExecutionListItem, N8nListResponse, N8nNode, N8nNodeType, PatchOperation } from './types.js';
5
+ import { type N8nWorkflow, type N8nWorkflowListItem, type N8nExecution, type N8nExecutionListItem, type N8nListResponse, type N8nNode, type PatchOperation } from './types.js';
6
6
  export interface N8nClientConfig {
7
7
  apiUrl: string;
8
8
  apiKey: string;
@@ -51,5 +51,4 @@ export declare class N8nClient {
51
51
  version?: string;
52
52
  error?: string;
53
53
  }>;
54
- listNodeTypes(): Promise<N8nNodeType[]>;
55
54
  }
@@ -2,6 +2,7 @@
2
2
  * n8n REST API Client
3
3
  * Clean, minimal implementation with built-in safety checks
4
4
  */
5
+ import { N8N_WORKFLOW_WRITABLE_FIELDS, pickFields, } from './types.js';
5
6
  export class N8nClient {
6
7
  baseUrl;
7
8
  headers;
@@ -56,8 +57,8 @@ export class N8nClient {
56
57
  return this.request('POST', '/api/v1/workflows', workflow);
57
58
  }
58
59
  async updateWorkflow(id, workflow) {
59
- // Strip properties that n8n API doesn't accept on PUT
60
- const { id: _id, createdAt, updatedAt, active, versionId, ...allowed } = workflow;
60
+ // Schema-driven: only send fields n8n accepts (defined in types.ts)
61
+ const allowed = pickFields(workflow, N8N_WORKFLOW_WRITABLE_FIELDS);
61
62
  return this.request('PUT', `/api/v1/workflows/${id}`, allowed);
62
63
  }
63
64
  async deleteWorkflow(id) {
@@ -274,10 +275,4 @@ export class N8nClient {
274
275
  };
275
276
  }
276
277
  }
277
- // ─────────────────────────────────────────────────────────────
278
- // Node Types
279
- // ─────────────────────────────────────────────────────────────
280
- async listNodeTypes() {
281
- return this.request('GET', '/api/v1/nodes');
282
- }
283
278
  }
package/dist/tools.js CHANGED
@@ -325,35 +325,6 @@ Returns the fixed workflow and list of changes made.`,
325
325
  },
326
326
  },
327
327
  // ─────────────────────────────────────────────────────────────
328
- // Node Discovery
329
- // ─────────────────────────────────────────────────────────────
330
- {
331
- name: 'node_types_list',
332
- description: `List available n8n node types. Use this to discover valid node types BEFORE creating workflows.
333
-
334
- Returns: type name, display name, description, category, and version for each node.
335
- Use the search parameter to filter by keyword (searches type name, display name, and description).
336
-
337
- IMPORTANT: Always check node types exist before using them in workflow_create or workflow_update.`,
338
- inputSchema: {
339
- type: 'object',
340
- properties: {
341
- search: {
342
- type: 'string',
343
- description: 'Filter nodes by keyword (searches name, type, description)',
344
- },
345
- category: {
346
- type: 'string',
347
- description: 'Filter by category (e.g., "Core Nodes", "Flow", "AI")',
348
- },
349
- limit: {
350
- type: 'number',
351
- description: 'Max results (default 50)',
352
- },
353
- },
354
- },
355
- },
356
- // ─────────────────────────────────────────────────────────────
357
328
  // Version Control
358
329
  // ─────────────────────────────────────────────────────────────
359
330
  {
package/dist/types.d.ts CHANGED
@@ -130,30 +130,20 @@ export interface N8nListResponse<T> {
130
130
  data: T[];
131
131
  nextCursor?: string;
132
132
  }
133
- export interface N8nNodeType {
134
- name: string;
135
- displayName: string;
136
- description: string;
137
- group: string[];
138
- version: number;
139
- defaults?: {
140
- name: string;
141
- };
142
- codex?: {
143
- categories?: string[];
144
- alias?: string[];
145
- };
146
- }
147
- export interface N8nNodeTypeSummary {
148
- type: string;
149
- name: string;
150
- description: string;
151
- category: string;
152
- version: number;
153
- }
154
- export interface NodeTypeValidationError {
155
- nodeType: string;
156
- nodeName: string;
157
- message: string;
158
- suggestions?: string[];
159
- }
133
+ /**
134
+ * Fields that n8n API accepts on PUT /workflows/:id
135
+ * All other fields (id, createdAt, homeProject, etc.) are read-only
136
+ *
137
+ * Derived from: n8n OpenAPI spec (GET {n8n-url}/api/v1/openapi.yml)
138
+ * Path: /workflows/{id} → put → requestBody → content → application/json → schema
139
+ *
140
+ * If n8n adds new writable fields, check the OpenAPI spec and update this array.
141
+ */
142
+ export declare const N8N_WORKFLOW_WRITABLE_FIELDS: readonly ["name", "nodes", "connections", "settings", "staticData"];
143
+ export type N8nWorkflowWritableField = (typeof N8N_WORKFLOW_WRITABLE_FIELDS)[number];
144
+ export type N8nWorkflowUpdate = Pick<N8nWorkflow, N8nWorkflowWritableField>;
145
+ /**
146
+ * Pick only specified fields from an object (strips everything else)
147
+ * Generic utility for schema-driven field filtering
148
+ */
149
+ export declare function pickFields<T, K extends keyof T>(obj: T, fields: readonly K[]): Pick<T, K>;
package/dist/types.js CHANGED
@@ -2,4 +2,37 @@
2
2
  * n8n API Types
3
3
  * Minimal types for workflow management
4
4
  */
5
- export {};
5
+ // ─────────────────────────────────────────────────────────────
6
+ // Schema-driven field definitions
7
+ // Source of truth: n8n OpenAPI spec at /api/v1/openapi.yml
8
+ // ─────────────────────────────────────────────────────────────
9
+ /**
10
+ * Fields that n8n API accepts on PUT /workflows/:id
11
+ * All other fields (id, createdAt, homeProject, etc.) are read-only
12
+ *
13
+ * Derived from: n8n OpenAPI spec (GET {n8n-url}/api/v1/openapi.yml)
14
+ * Path: /workflows/{id} → put → requestBody → content → application/json → schema
15
+ *
16
+ * If n8n adds new writable fields, check the OpenAPI spec and update this array.
17
+ */
18
+ export const N8N_WORKFLOW_WRITABLE_FIELDS = [
19
+ 'name',
20
+ 'nodes',
21
+ 'connections',
22
+ 'settings',
23
+ 'staticData',
24
+ // Note: 'tags' is read-only in some n8n versions
25
+ ];
26
+ /**
27
+ * Pick only specified fields from an object (strips everything else)
28
+ * Generic utility for schema-driven field filtering
29
+ */
30
+ export function pickFields(obj, fields) {
31
+ const result = {};
32
+ for (const field of fields) {
33
+ if (field in obj && obj[field] !== undefined) {
34
+ result[field] = obj[field];
35
+ }
36
+ }
37
+ return result;
38
+ }
@@ -2,17 +2,9 @@
2
2
  * Opinion-based workflow validation
3
3
  * Enforces best practices from n8n-best-practices.md
4
4
  */
5
- import type { N8nWorkflow, ValidationResult, ValidationWarning, NodeTypeValidationError } from './types.js';
5
+ import type { N8nWorkflow, ValidationResult, ValidationWarning } from './types.js';
6
6
  export declare function validateWorkflow(workflow: N8nWorkflow): ValidationResult;
7
7
  /**
8
8
  * Validate a partial update to ensure it won't cause issues
9
9
  */
10
10
  export declare function validatePartialUpdate(currentWorkflow: N8nWorkflow, nodeName: string, newParameters: Record<string, unknown>): ValidationWarning[];
11
- /**
12
- * Validate that all node types in an array exist in the available types
13
- * Returns errors for any invalid node types with suggestions
14
- */
15
- export declare function validateNodeTypes(nodes: Array<{
16
- name: string;
17
- type: string;
18
- }>, availableTypes: Set<string>): NodeTypeValidationError[];
@@ -236,88 +236,3 @@ export function validatePartialUpdate(currentWorkflow, nodeName, newParameters)
236
236
  }
237
237
  return warnings;
238
238
  }
239
- // ─────────────────────────────────────────────────────────────
240
- // Node Type Validation
241
- // ─────────────────────────────────────────────────────────────
242
- /**
243
- * Validate that all node types in an array exist in the available types
244
- * Returns errors for any invalid node types with suggestions
245
- */
246
- export function validateNodeTypes(nodes, availableTypes) {
247
- const errors = [];
248
- for (const node of nodes) {
249
- if (!availableTypes.has(node.type)) {
250
- errors.push({
251
- nodeType: node.type,
252
- nodeName: node.name,
253
- message: `Invalid node type "${node.type}" for node "${node.name}"`,
254
- suggestions: findSimilarTypes(node.type, availableTypes),
255
- });
256
- }
257
- }
258
- return errors;
259
- }
260
- /**
261
- * Find similar node types for suggestions (fuzzy matching)
262
- * Returns up to 3 suggestions
263
- */
264
- function findSimilarTypes(invalidType, availableTypes) {
265
- const suggestions = [];
266
- const searchTerm = invalidType.toLowerCase();
267
- // Extract the last part after the dot (e.g., "webhook" from "n8n-nodes-base.webhook")
268
- const typeParts = searchTerm.split('.');
269
- const shortName = typeParts[typeParts.length - 1];
270
- for (const validType of availableTypes) {
271
- const validLower = validType.toLowerCase();
272
- const validShortName = validLower.split('.').pop() || '';
273
- // Check for partial matches (substring)
274
- if (validLower.includes(shortName) ||
275
- shortName.includes(validShortName) ||
276
- validShortName.includes(shortName)) {
277
- suggestions.push(validType);
278
- if (suggestions.length >= 3)
279
- break;
280
- continue;
281
- }
282
- // Check for typos using Levenshtein distance
283
- const distance = levenshteinDistance(shortName, validShortName);
284
- const maxLen = Math.max(shortName.length, validShortName.length);
285
- // Allow up to 2 character differences for short names, or 20% of length for longer ones
286
- const threshold = Math.max(2, Math.floor(maxLen * 0.2));
287
- if (distance <= threshold) {
288
- suggestions.push(validType);
289
- if (suggestions.length >= 3)
290
- break;
291
- }
292
- }
293
- return suggestions;
294
- }
295
- /**
296
- * Calculate Levenshtein distance between two strings
297
- */
298
- function levenshteinDistance(a, b) {
299
- if (a.length === 0)
300
- return b.length;
301
- if (b.length === 0)
302
- return a.length;
303
- const matrix = [];
304
- // Initialize first column
305
- for (let i = 0; i <= a.length; i++) {
306
- matrix[i] = [i];
307
- }
308
- // Initialize first row
309
- for (let j = 0; j <= b.length; j++) {
310
- matrix[0][j] = j;
311
- }
312
- // Fill in the rest of the matrix
313
- for (let i = 1; i <= a.length; i++) {
314
- for (let j = 1; j <= b.length; j++) {
315
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
316
- matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
317
- matrix[i][j - 1] + 1, // insertion
318
- matrix[i - 1][j - 1] + cost // substitution
319
- );
320
- }
321
- }
322
- return matrix[a.length][b.length];
323
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagelines/n8n-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Opinionated MCP server for n8n workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "license": "MIT",
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "https://github.com/PageLines/n8n-mcp"
29
+ "url": "git+https://github.com/PageLines/n8n-mcp.git"
30
30
  },
31
31
  "engines": {
32
32
  "node": ">=18"
@@ -1,38 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/checkout@v4
14
- - uses: actions/setup-node@v4
15
- with:
16
- node-version: '20'
17
- cache: 'npm'
18
- - run: npm ci
19
- - run: npm run build
20
- - run: npm test -- --run
21
-
22
- publish:
23
- needs: test
24
- runs-on: ubuntu-latest
25
- if: github.ref == 'refs/heads/main' && github.event_name == 'push'
26
- steps:
27
- - uses: actions/checkout@v4
28
- - uses: actions/setup-node@v4
29
- with:
30
- node-version: '20'
31
- cache: 'npm'
32
- registry-url: 'https://registry.npmjs.org'
33
- - run: npm ci
34
- - run: npm run build
35
- - run: npm publish --access public
36
- env:
37
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
38
- continue-on-error: true # Won't fail if version already exists
@@ -1 +0,0 @@
1
- export {};