@pagelines/n8n-mcp 0.3.1 → 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,6 +2,16 @@
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
+
5
15
  ## [0.3.1] - 2025-01-13
6
16
 
7
17
  ### Fixed
@@ -18,14 +28,8 @@ All notable changes to this project will be documented in this file.
18
28
 
19
29
  ### Added
20
30
 
21
- #### Node Type Discovery & Validation
22
- - `node_types_list` - Search available node types by name/category
23
- - Pre-validation blocks invalid node types before `workflow_create` and `workflow_update`
24
- - Fuzzy matching suggests correct types when invalid types detected
25
-
26
31
  #### Auto-Cleanup Pipeline
27
32
  - Every `workflow_create` and `workflow_update` now automatically:
28
- - Validates node types (blocks if invalid)
29
33
  - Runs validation rules
30
34
  - Auto-fixes fixable issues (snake_case, $json refs, AI settings)
31
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.1',
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, type N8nWorkflowListItem, type N8nExecution, type N8nExecutionListItem, type N8nListResponse, type N8nNode, type N8nNodeType, type 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
  }
@@ -275,10 +275,4 @@ export class N8nClient {
275
275
  };
276
276
  }
277
277
  }
278
- // ─────────────────────────────────────────────────────────────
279
- // Node Types
280
- // ─────────────────────────────────────────────────────────────
281
- async listNodeTypes() {
282
- return this.request('GET', '/api/v1/nodes');
283
- }
284
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
@@ -139,7 +139,7 @@ export interface N8nListResponse<T> {
139
139
  *
140
140
  * If n8n adds new writable fields, check the OpenAPI spec and update this array.
141
141
  */
142
- export declare const N8N_WORKFLOW_WRITABLE_FIELDS: readonly ["name", "nodes", "connections", "settings", "staticData", "tags"];
142
+ export declare const N8N_WORKFLOW_WRITABLE_FIELDS: readonly ["name", "nodes", "connections", "settings", "staticData"];
143
143
  export type N8nWorkflowWritableField = (typeof N8N_WORKFLOW_WRITABLE_FIELDS)[number];
144
144
  export type N8nWorkflowUpdate = Pick<N8nWorkflow, N8nWorkflowWritableField>;
145
145
  /**
@@ -147,30 +147,3 @@ export type N8nWorkflowUpdate = Pick<N8nWorkflow, N8nWorkflowWritableField>;
147
147
  * Generic utility for schema-driven field filtering
148
148
  */
149
149
  export declare function pickFields<T, K extends keyof T>(obj: T, fields: readonly K[]): Pick<T, K>;
150
- export interface N8nNodeType {
151
- name: string;
152
- displayName: string;
153
- description: string;
154
- group: string[];
155
- version: number;
156
- defaults?: {
157
- name: string;
158
- };
159
- codex?: {
160
- categories?: string[];
161
- alias?: string[];
162
- };
163
- }
164
- export interface N8nNodeTypeSummary {
165
- type: string;
166
- name: string;
167
- description: string;
168
- category: string;
169
- version: number;
170
- }
171
- export interface NodeTypeValidationError {
172
- nodeType: string;
173
- nodeName: string;
174
- message: string;
175
- suggestions?: string[];
176
- }
package/dist/types.js CHANGED
@@ -21,7 +21,7 @@ export const N8N_WORKFLOW_WRITABLE_FIELDS = [
21
21
  'connections',
22
22
  'settings',
23
23
  'staticData',
24
- 'tags',
24
+ // Note: 'tags' is read-only in some n8n versions
25
25
  ];
26
26
  /**
27
27
  * Pick only specified fields from an object (strips everything else)
@@ -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.1",
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 {};