@pagelines/n8n-mcp 0.2.1 → 0.3.0

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/src/tools.ts CHANGED
@@ -29,7 +29,7 @@ export const tools: Tool[] = [
29
29
 
30
30
  {
31
31
  name: 'workflow_get',
32
- description: 'Get a workflow by ID. Returns full workflow with nodes, connections, settings.',
32
+ description: 'Get a workflow by ID. Use format=summary for minimal response, compact (default) for nodes without parameters, full for everything.',
33
33
  inputSchema: {
34
34
  type: 'object',
35
35
  properties: {
@@ -37,6 +37,11 @@ export const tools: Tool[] = [
37
37
  type: 'string',
38
38
  description: 'Workflow ID',
39
39
  },
40
+ format: {
41
+ type: 'string',
42
+ enum: ['summary', 'compact', 'full'],
43
+ description: 'Response detail level. summary=minimal, compact=nodes without params (default), full=everything',
44
+ },
40
45
  },
41
46
  required: ['id'],
42
47
  },
@@ -44,7 +49,7 @@ export const tools: Tool[] = [
44
49
 
45
50
  {
46
51
  name: 'workflow_create',
47
- description: 'Create a new workflow. Returns the created workflow.',
52
+ description: 'Create a new workflow. Returns the created workflow with validation.',
48
53
  inputSchema: {
49
54
  type: 'object',
50
55
  properties: {
@@ -81,6 +86,11 @@ export const tools: Tool[] = [
81
86
  type: 'object',
82
87
  description: 'Workflow settings',
83
88
  },
89
+ format: {
90
+ type: 'string',
91
+ enum: ['summary', 'compact', 'full'],
92
+ description: 'Response detail level (default: compact)',
93
+ },
84
94
  },
85
95
  required: ['name', 'nodes', 'connections'],
86
96
  },
@@ -138,6 +148,11 @@ Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "proper
138
148
  required: ['type'],
139
149
  },
140
150
  },
151
+ format: {
152
+ type: 'string',
153
+ enum: ['summary', 'compact', 'full'],
154
+ description: 'Response detail level (default: compact)',
155
+ },
141
156
  },
142
157
  required: ['id', 'operations'],
143
158
  },
@@ -229,13 +244,18 @@ Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "proper
229
244
  type: 'number',
230
245
  description: 'Max results (default 20)',
231
246
  },
247
+ format: {
248
+ type: 'string',
249
+ enum: ['summary', 'compact', 'full'],
250
+ description: 'Response detail level (default: compact)',
251
+ },
232
252
  },
233
253
  },
234
254
  },
235
255
 
236
256
  {
237
257
  name: 'execution_get',
238
- description: 'Get execution details including run data and errors.',
258
+ description: 'Get execution details. Use format=summary for status only, compact (default) omits runData, full for everything including runData.',
239
259
  inputSchema: {
240
260
  type: 'object',
241
261
  properties: {
@@ -243,6 +263,11 @@ Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "proper
243
263
  type: 'string',
244
264
  description: 'Execution ID',
245
265
  },
266
+ format: {
267
+ type: 'string',
268
+ enum: ['summary', 'compact', 'full'],
269
+ description: 'Response detail level. summary=status only, compact=no runData (default), full=everything',
270
+ },
246
271
  },
247
272
  required: ['id'],
248
273
  },
@@ -315,6 +340,36 @@ Returns the fixed workflow and list of changes made.`,
315
340
  },
316
341
  },
317
342
 
343
+ // ─────────────────────────────────────────────────────────────
344
+ // Node Discovery
345
+ // ─────────────────────────────────────────────────────────────
346
+ {
347
+ name: 'node_types_list',
348
+ description: `List available n8n node types. Use this to discover valid node types BEFORE creating workflows.
349
+
350
+ Returns: type name, display name, description, category, and version for each node.
351
+ Use the search parameter to filter by keyword (searches type name, display name, and description).
352
+
353
+ IMPORTANT: Always check node types exist before using them in workflow_create or workflow_update.`,
354
+ inputSchema: {
355
+ type: 'object',
356
+ properties: {
357
+ search: {
358
+ type: 'string',
359
+ description: 'Filter nodes by keyword (searches name, type, description)',
360
+ },
361
+ category: {
362
+ type: 'string',
363
+ description: 'Filter by category (e.g., "Core Nodes", "Flow", "AI")',
364
+ },
365
+ limit: {
366
+ type: 'number',
367
+ description: 'Max results (default 50)',
368
+ },
369
+ },
370
+ },
371
+ },
372
+
318
373
  // ─────────────────────────────────────────────────────────────
319
374
  // Version Control
320
375
  // ─────────────────────────────────────────────────────────────
@@ -347,6 +402,11 @@ Returns the fixed workflow and list of changes made.`,
347
402
  type: 'string',
348
403
  description: 'Version ID (from version_list)',
349
404
  },
405
+ format: {
406
+ type: 'string',
407
+ enum: ['summary', 'compact', 'full'],
408
+ description: 'Response detail level (default: compact)',
409
+ },
350
410
  },
351
411
  required: ['workflowId', 'versionId'],
352
412
  },
@@ -385,6 +445,11 @@ Returns the fixed workflow and list of changes made.`,
385
445
  type: 'string',
386
446
  description: 'Version ID to restore',
387
447
  },
448
+ format: {
449
+ type: 'string',
450
+ enum: ['summary', 'compact', 'full'],
451
+ description: 'Response detail level (default: compact)',
452
+ },
388
453
  },
389
454
  required: ['workflowId', 'versionId'],
390
455
  },
package/src/types.ts CHANGED
@@ -105,3 +105,36 @@ export interface N8nListResponse<T> {
105
105
  data: T[];
106
106
  nextCursor?: string;
107
107
  }
108
+
109
+ // Node type information from n8n API (GET /api/v1/nodes)
110
+ export interface N8nNodeType {
111
+ name: string; // e.g., "n8n-nodes-base.webhook"
112
+ displayName: string; // e.g., "Webhook"
113
+ description: string;
114
+ group: string[]; // e.g., ["trigger"]
115
+ version: number;
116
+ defaults?: {
117
+ name: string;
118
+ };
119
+ codex?: {
120
+ categories?: string[];
121
+ alias?: string[];
122
+ };
123
+ }
124
+
125
+ // Simplified node type for tool responses (reduced tokens)
126
+ export interface N8nNodeTypeSummary {
127
+ type: string; // Full type name
128
+ name: string; // Display name
129
+ description: string;
130
+ category: string;
131
+ version: number;
132
+ }
133
+
134
+ // Node type validation error
135
+ export interface NodeTypeValidationError {
136
+ nodeType: string;
137
+ nodeName: string;
138
+ message: string;
139
+ suggestions?: string[];
140
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { validateWorkflow, validatePartialUpdate } from './validators.js';
2
+ import { validateWorkflow, validatePartialUpdate, validateNodeTypes } from './validators.js';
3
3
  import type { N8nWorkflow } from './types.js';
4
4
 
5
5
  const createWorkflow = (overrides: Partial<N8nWorkflow> = {}): N8nWorkflow => ({
@@ -68,7 +68,7 @@ describe('validateWorkflow', () => {
68
68
  );
69
69
  });
70
70
 
71
- it('errors on hardcoded secrets', () => {
71
+ it('warns on hardcoded secrets', () => {
72
72
  const workflow = createWorkflow({
73
73
  nodes: [
74
74
  {
@@ -83,11 +83,10 @@ describe('validateWorkflow', () => {
83
83
  });
84
84
 
85
85
  const result = validateWorkflow(workflow);
86
- expect(result.valid).toBe(false);
87
86
  expect(result.warnings).toContainEqual(
88
87
  expect.objectContaining({
89
88
  rule: 'no_hardcoded_secrets',
90
- severity: 'error',
89
+ severity: 'info',
91
90
  })
92
91
  );
93
92
  });
@@ -275,3 +274,101 @@ describe('validatePartialUpdate', () => {
275
274
  expect(warnings).toHaveLength(0);
276
275
  });
277
276
  });
277
+
278
+ describe('validateNodeTypes', () => {
279
+ const availableTypes = new Set([
280
+ 'n8n-nodes-base.webhook',
281
+ 'n8n-nodes-base.set',
282
+ 'n8n-nodes-base.code',
283
+ 'n8n-nodes-base.httpRequest',
284
+ '@n8n/n8n-nodes-langchain.agent',
285
+ '@n8n/n8n-nodes-langchain.chatTrigger',
286
+ ]);
287
+
288
+ it('passes when all node types are valid', () => {
289
+ const nodes = [
290
+ { name: 'webhook_trigger', type: 'n8n-nodes-base.webhook' },
291
+ { name: 'set_data', type: 'n8n-nodes-base.set' },
292
+ { name: 'ai_agent', type: '@n8n/n8n-nodes-langchain.agent' },
293
+ ];
294
+
295
+ const errors = validateNodeTypes(nodes, availableTypes);
296
+ expect(errors).toHaveLength(0);
297
+ });
298
+
299
+ it('returns error for invalid node type', () => {
300
+ const nodes = [
301
+ { name: 'my_node', type: 'n8n-nodes-base.nonexistent' },
302
+ ];
303
+
304
+ const errors = validateNodeTypes(nodes, availableTypes);
305
+ expect(errors).toHaveLength(1);
306
+ expect(errors[0]).toEqual(
307
+ expect.objectContaining({
308
+ nodeType: 'n8n-nodes-base.nonexistent',
309
+ nodeName: 'my_node',
310
+ })
311
+ );
312
+ });
313
+
314
+ it('returns errors for multiple invalid node types', () => {
315
+ const nodes = [
316
+ { name: 'valid_node', type: 'n8n-nodes-base.webhook' },
317
+ { name: 'invalid_one', type: 'n8n-nodes-base.fake' },
318
+ { name: 'invalid_two', type: 'n8n-nodes-base.bogus' },
319
+ ];
320
+
321
+ const errors = validateNodeTypes(nodes, availableTypes);
322
+ expect(errors).toHaveLength(2);
323
+ expect(errors.map((e) => e.nodeName)).toEqual(['invalid_one', 'invalid_two']);
324
+ });
325
+
326
+ it('provides suggestions for typos', () => {
327
+ const nodes = [
328
+ { name: 'trigger', type: 'n8n-nodes-base.webhok' }, // typo: webhok
329
+ ];
330
+
331
+ const errors = validateNodeTypes(nodes, availableTypes);
332
+ expect(errors).toHaveLength(1);
333
+ expect(errors[0].suggestions).toContain('n8n-nodes-base.webhook');
334
+ });
335
+
336
+ it('provides suggestions for partial matches', () => {
337
+ const nodes = [
338
+ { name: 'code_node', type: 'n8n-nodes-base.cod' }, // partial: cod
339
+ ];
340
+
341
+ const errors = validateNodeTypes(nodes, availableTypes);
342
+ expect(errors).toHaveLength(1);
343
+ expect(errors[0].suggestions).toContain('n8n-nodes-base.code');
344
+ });
345
+
346
+ it('returns empty suggestions when no matches found', () => {
347
+ const nodes = [
348
+ { name: 'xyz_node', type: 'n8n-nodes-base.xyz123completely_random' },
349
+ ];
350
+
351
+ const errors = validateNodeTypes(nodes, availableTypes);
352
+ expect(errors).toHaveLength(1);
353
+ expect(errors[0].suggestions).toHaveLength(0);
354
+ });
355
+
356
+ it('limits suggestions to 3', () => {
357
+ // Create a set with many similar types
358
+ const manyTypes = new Set([
359
+ 'n8n-nodes-base.httpRequest',
360
+ 'n8n-nodes-base.httpRequestTool',
361
+ 'n8n-nodes-base.httpRequestV1',
362
+ 'n8n-nodes-base.httpRequestV2',
363
+ 'n8n-nodes-base.httpRequestV3',
364
+ ]);
365
+
366
+ const nodes = [
367
+ { name: 'http', type: 'n8n-nodes-base.http' }, // should match multiple
368
+ ];
369
+
370
+ const errors = validateNodeTypes(nodes, manyTypes);
371
+ expect(errors).toHaveLength(1);
372
+ expect(errors[0].suggestions!.length).toBeLessThanOrEqual(3);
373
+ });
374
+ });
package/src/validators.ts CHANGED
@@ -3,7 +3,13 @@
3
3
  * Enforces best practices from n8n-best-practices.md
4
4
  */
5
5
 
6
- import type { N8nWorkflow, N8nNode, ValidationResult, ValidationWarning } from './types.js';
6
+ import type {
7
+ N8nWorkflow,
8
+ N8nNode,
9
+ ValidationResult,
10
+ ValidationWarning,
11
+ NodeTypeValidationError,
12
+ } from './types.js';
7
13
 
8
14
  export function validateWorkflow(workflow: N8nWorkflow): ValidationResult {
9
15
  const warnings: ValidationWarning[] = [];
@@ -133,8 +139,8 @@ function checkForHardcodedSecrets(node: N8nNode, warnings: ValidationWarning[]):
133
139
  warnings.push({
134
140
  node: node.name,
135
141
  rule: 'no_hardcoded_secrets',
136
- message: `Node "${node.name}" may contain hardcoded secrets - use $env.VAR_NAME instead`,
137
- severity: 'error',
142
+ message: `Node "${node.name}" may contain hardcoded secrets - consider using $env.VAR_NAME`,
143
+ severity: 'info',
138
144
  });
139
145
  break;
140
146
  }
@@ -283,3 +289,106 @@ export function validatePartialUpdate(
283
289
 
284
290
  return warnings;
285
291
  }
292
+
293
+ // ─────────────────────────────────────────────────────────────
294
+ // Node Type Validation
295
+ // ─────────────────────────────────────────────────────────────
296
+
297
+ /**
298
+ * Validate that all node types in an array exist in the available types
299
+ * Returns errors for any invalid node types with suggestions
300
+ */
301
+ export function validateNodeTypes(
302
+ nodes: Array<{ name: string; type: string }>,
303
+ availableTypes: Set<string>
304
+ ): NodeTypeValidationError[] {
305
+ const errors: NodeTypeValidationError[] = [];
306
+
307
+ for (const node of nodes) {
308
+ if (!availableTypes.has(node.type)) {
309
+ errors.push({
310
+ nodeType: node.type,
311
+ nodeName: node.name,
312
+ message: `Invalid node type "${node.type}" for node "${node.name}"`,
313
+ suggestions: findSimilarTypes(node.type, availableTypes),
314
+ });
315
+ }
316
+ }
317
+
318
+ return errors;
319
+ }
320
+
321
+ /**
322
+ * Find similar node types for suggestions (fuzzy matching)
323
+ * Returns up to 3 suggestions
324
+ */
325
+ function findSimilarTypes(invalidType: string, availableTypes: Set<string>): string[] {
326
+ const suggestions: string[] = [];
327
+ const searchTerm = invalidType.toLowerCase();
328
+
329
+ // Extract the last part after the dot (e.g., "webhook" from "n8n-nodes-base.webhook")
330
+ const typeParts = searchTerm.split('.');
331
+ const shortName = typeParts[typeParts.length - 1];
332
+
333
+ for (const validType of availableTypes) {
334
+ const validLower = validType.toLowerCase();
335
+ const validShortName = validLower.split('.').pop() || '';
336
+
337
+ // Check for partial matches (substring)
338
+ if (
339
+ validLower.includes(shortName) ||
340
+ shortName.includes(validShortName) ||
341
+ validShortName.includes(shortName)
342
+ ) {
343
+ suggestions.push(validType);
344
+ if (suggestions.length >= 3) break;
345
+ continue;
346
+ }
347
+
348
+ // Check for typos using Levenshtein distance
349
+ const distance = levenshteinDistance(shortName, validShortName);
350
+ const maxLen = Math.max(shortName.length, validShortName.length);
351
+ // Allow up to 2 character differences for short names, or 20% of length for longer ones
352
+ const threshold = Math.max(2, Math.floor(maxLen * 0.2));
353
+ if (distance <= threshold) {
354
+ suggestions.push(validType);
355
+ if (suggestions.length >= 3) break;
356
+ }
357
+ }
358
+
359
+ return suggestions;
360
+ }
361
+
362
+ /**
363
+ * Calculate Levenshtein distance between two strings
364
+ */
365
+ function levenshteinDistance(a: string, b: string): number {
366
+ if (a.length === 0) return b.length;
367
+ if (b.length === 0) return a.length;
368
+
369
+ const matrix: number[][] = [];
370
+
371
+ // Initialize first column
372
+ for (let i = 0; i <= a.length; i++) {
373
+ matrix[i] = [i];
374
+ }
375
+
376
+ // Initialize first row
377
+ for (let j = 0; j <= b.length; j++) {
378
+ matrix[0][j] = j;
379
+ }
380
+
381
+ // Fill in the rest of the matrix
382
+ for (let i = 1; i <= a.length; i++) {
383
+ for (let j = 1; j <= b.length; j++) {
384
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
385
+ matrix[i][j] = Math.min(
386
+ matrix[i - 1][j] + 1, // deletion
387
+ matrix[i][j - 1] + 1, // insertion
388
+ matrix[i - 1][j - 1] + cost // substitution
389
+ );
390
+ }
391
+ }
392
+
393
+ return matrix[a.length][b.length];
394
+ }