@pagelines/n8n-mcp 0.2.1 → 0.3.1
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 +38 -0
- package/README.md +40 -24
- package/dist/index.js +122 -18
- package/dist/n8n-client.d.ts +3 -2
- package/dist/n8n-client.js +10 -1
- package/dist/n8n-client.test.js +198 -0
- package/dist/response-format.d.ts +84 -0
- package/dist/response-format.js +183 -0
- package/dist/response-format.test.d.ts +1 -0
- package/dist/response-format.test.js +291 -0
- package/dist/tools.js +67 -3
- package/dist/types.d.ts +44 -0
- package/dist/types.js +34 -1
- package/dist/validators.d.ts +9 -1
- package/dist/validators.js +87 -2
- package/dist/validators.test.js +83 -4
- package/docs/best-practices.md +15 -10
- package/docs/node-config.md +3 -1
- package/logo.png +0 -0
- package/package.json +1 -1
- package/plans/architecture.md +69 -26
- package/src/index.ts +159 -20
- package/src/n8n-client.test.ts +240 -0
- package/src/n8n-client.ts +23 -10
- package/src/response-format.test.ts +355 -0
- package/src/response-format.ts +278 -0
- package/src/tools.ts +68 -3
- package/src/types.ts +76 -0
- package/src/validators.test.ts +101 -4
- package/src/validators.ts +112 -3
package/dist/tools.js
CHANGED
|
@@ -25,7 +25,7 @@ export const tools = [
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
name: 'workflow_get',
|
|
28
|
-
description: 'Get a workflow by ID.
|
|
28
|
+
description: 'Get a workflow by ID. Use format=summary for minimal response, compact (default) for nodes without parameters, full for everything.',
|
|
29
29
|
inputSchema: {
|
|
30
30
|
type: 'object',
|
|
31
31
|
properties: {
|
|
@@ -33,13 +33,18 @@ export const tools = [
|
|
|
33
33
|
type: 'string',
|
|
34
34
|
description: 'Workflow ID',
|
|
35
35
|
},
|
|
36
|
+
format: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
enum: ['summary', 'compact', 'full'],
|
|
39
|
+
description: 'Response detail level. summary=minimal, compact=nodes without params (default), full=everything',
|
|
40
|
+
},
|
|
36
41
|
},
|
|
37
42
|
required: ['id'],
|
|
38
43
|
},
|
|
39
44
|
},
|
|
40
45
|
{
|
|
41
46
|
name: 'workflow_create',
|
|
42
|
-
description: 'Create a new workflow. Returns the created workflow.',
|
|
47
|
+
description: 'Create a new workflow. Returns the created workflow with validation.',
|
|
43
48
|
inputSchema: {
|
|
44
49
|
type: 'object',
|
|
45
50
|
properties: {
|
|
@@ -76,6 +81,11 @@ export const tools = [
|
|
|
76
81
|
type: 'object',
|
|
77
82
|
description: 'Workflow settings',
|
|
78
83
|
},
|
|
84
|
+
format: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
enum: ['summary', 'compact', 'full'],
|
|
87
|
+
description: 'Response detail level (default: compact)',
|
|
88
|
+
},
|
|
79
89
|
},
|
|
80
90
|
required: ['name', 'nodes', 'connections'],
|
|
81
91
|
},
|
|
@@ -132,6 +142,11 @@ Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "proper
|
|
|
132
142
|
required: ['type'],
|
|
133
143
|
},
|
|
134
144
|
},
|
|
145
|
+
format: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
enum: ['summary', 'compact', 'full'],
|
|
148
|
+
description: 'Response detail level (default: compact)',
|
|
149
|
+
},
|
|
135
150
|
},
|
|
136
151
|
required: ['id', 'operations'],
|
|
137
152
|
},
|
|
@@ -218,12 +233,17 @@ Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "proper
|
|
|
218
233
|
type: 'number',
|
|
219
234
|
description: 'Max results (default 20)',
|
|
220
235
|
},
|
|
236
|
+
format: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
enum: ['summary', 'compact', 'full'],
|
|
239
|
+
description: 'Response detail level (default: compact)',
|
|
240
|
+
},
|
|
221
241
|
},
|
|
222
242
|
},
|
|
223
243
|
},
|
|
224
244
|
{
|
|
225
245
|
name: 'execution_get',
|
|
226
|
-
description: 'Get execution details
|
|
246
|
+
description: 'Get execution details. Use format=summary for status only, compact (default) omits runData, full for everything including runData.',
|
|
227
247
|
inputSchema: {
|
|
228
248
|
type: 'object',
|
|
229
249
|
properties: {
|
|
@@ -231,6 +251,11 @@ Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "proper
|
|
|
231
251
|
type: 'string',
|
|
232
252
|
description: 'Execution ID',
|
|
233
253
|
},
|
|
254
|
+
format: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
enum: ['summary', 'compact', 'full'],
|
|
257
|
+
description: 'Response detail level. summary=status only, compact=no runData (default), full=everything',
|
|
258
|
+
},
|
|
234
259
|
},
|
|
235
260
|
required: ['id'],
|
|
236
261
|
},
|
|
@@ -300,6 +325,35 @@ Returns the fixed workflow and list of changes made.`,
|
|
|
300
325
|
},
|
|
301
326
|
},
|
|
302
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
|
+
// ─────────────────────────────────────────────────────────────
|
|
303
357
|
// Version Control
|
|
304
358
|
// ─────────────────────────────────────────────────────────────
|
|
305
359
|
{
|
|
@@ -330,6 +384,11 @@ Returns the fixed workflow and list of changes made.`,
|
|
|
330
384
|
type: 'string',
|
|
331
385
|
description: 'Version ID (from version_list)',
|
|
332
386
|
},
|
|
387
|
+
format: {
|
|
388
|
+
type: 'string',
|
|
389
|
+
enum: ['summary', 'compact', 'full'],
|
|
390
|
+
description: 'Response detail level (default: compact)',
|
|
391
|
+
},
|
|
333
392
|
},
|
|
334
393
|
required: ['workflowId', 'versionId'],
|
|
335
394
|
},
|
|
@@ -366,6 +425,11 @@ Returns the fixed workflow and list of changes made.`,
|
|
|
366
425
|
type: 'string',
|
|
367
426
|
description: 'Version ID to restore',
|
|
368
427
|
},
|
|
428
|
+
format: {
|
|
429
|
+
type: 'string',
|
|
430
|
+
enum: ['summary', 'compact', 'full'],
|
|
431
|
+
description: 'Response detail level (default: compact)',
|
|
432
|
+
},
|
|
369
433
|
},
|
|
370
434
|
required: ['workflowId', 'versionId'],
|
|
371
435
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -130,3 +130,47 @@ export interface N8nListResponse<T> {
|
|
|
130
130
|
data: T[];
|
|
131
131
|
nextCursor?: string;
|
|
132
132
|
}
|
|
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", "tags"];
|
|
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>;
|
|
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
|
@@ -2,4 +2,37 @@
|
|
|
2
2
|
* n8n API Types
|
|
3
3
|
* Minimal types for workflow management
|
|
4
4
|
*/
|
|
5
|
-
|
|
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
|
+
'tags',
|
|
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
|
+
}
|
package/dist/validators.d.ts
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
* Opinion-based workflow validation
|
|
3
3
|
* Enforces best practices from n8n-best-practices.md
|
|
4
4
|
*/
|
|
5
|
-
import type { N8nWorkflow, ValidationResult, ValidationWarning } from './types.js';
|
|
5
|
+
import type { N8nWorkflow, ValidationResult, ValidationWarning, NodeTypeValidationError } 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[];
|
package/dist/validators.js
CHANGED
|
@@ -106,8 +106,8 @@ function checkForHardcodedSecrets(node, warnings) {
|
|
|
106
106
|
warnings.push({
|
|
107
107
|
node: node.name,
|
|
108
108
|
rule: 'no_hardcoded_secrets',
|
|
109
|
-
message: `Node "${node.name}" may contain hardcoded secrets -
|
|
110
|
-
severity: '
|
|
109
|
+
message: `Node "${node.name}" may contain hardcoded secrets - consider using $env.VAR_NAME`,
|
|
110
|
+
severity: 'info',
|
|
111
111
|
});
|
|
112
112
|
break;
|
|
113
113
|
}
|
|
@@ -236,3 +236,88 @@ 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/dist/validators.test.js
CHANGED
|
@@ -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
|
const createWorkflow = (overrides = {}) => ({
|
|
4
4
|
id: '1',
|
|
5
5
|
name: 'test_workflow',
|
|
@@ -55,7 +55,7 @@ describe('validateWorkflow', () => {
|
|
|
55
55
|
severity: 'warning',
|
|
56
56
|
}));
|
|
57
57
|
});
|
|
58
|
-
it('
|
|
58
|
+
it('warns on hardcoded secrets', () => {
|
|
59
59
|
const workflow = createWorkflow({
|
|
60
60
|
nodes: [
|
|
61
61
|
{
|
|
@@ -69,10 +69,9 @@ describe('validateWorkflow', () => {
|
|
|
69
69
|
],
|
|
70
70
|
});
|
|
71
71
|
const result = validateWorkflow(workflow);
|
|
72
|
-
expect(result.valid).toBe(false);
|
|
73
72
|
expect(result.warnings).toContainEqual(expect.objectContaining({
|
|
74
73
|
rule: 'no_hardcoded_secrets',
|
|
75
|
-
severity: '
|
|
74
|
+
severity: 'info',
|
|
76
75
|
}));
|
|
77
76
|
});
|
|
78
77
|
it('warns on orphan nodes', () => {
|
|
@@ -229,3 +228,83 @@ describe('validatePartialUpdate', () => {
|
|
|
229
228
|
expect(warnings).toHaveLength(0);
|
|
230
229
|
});
|
|
231
230
|
});
|
|
231
|
+
describe('validateNodeTypes', () => {
|
|
232
|
+
const availableTypes = new Set([
|
|
233
|
+
'n8n-nodes-base.webhook',
|
|
234
|
+
'n8n-nodes-base.set',
|
|
235
|
+
'n8n-nodes-base.code',
|
|
236
|
+
'n8n-nodes-base.httpRequest',
|
|
237
|
+
'@n8n/n8n-nodes-langchain.agent',
|
|
238
|
+
'@n8n/n8n-nodes-langchain.chatTrigger',
|
|
239
|
+
]);
|
|
240
|
+
it('passes when all node types are valid', () => {
|
|
241
|
+
const nodes = [
|
|
242
|
+
{ name: 'webhook_trigger', type: 'n8n-nodes-base.webhook' },
|
|
243
|
+
{ name: 'set_data', type: 'n8n-nodes-base.set' },
|
|
244
|
+
{ name: 'ai_agent', type: '@n8n/n8n-nodes-langchain.agent' },
|
|
245
|
+
];
|
|
246
|
+
const errors = validateNodeTypes(nodes, availableTypes);
|
|
247
|
+
expect(errors).toHaveLength(0);
|
|
248
|
+
});
|
|
249
|
+
it('returns error for invalid node type', () => {
|
|
250
|
+
const nodes = [
|
|
251
|
+
{ name: 'my_node', type: 'n8n-nodes-base.nonexistent' },
|
|
252
|
+
];
|
|
253
|
+
const errors = validateNodeTypes(nodes, availableTypes);
|
|
254
|
+
expect(errors).toHaveLength(1);
|
|
255
|
+
expect(errors[0]).toEqual(expect.objectContaining({
|
|
256
|
+
nodeType: 'n8n-nodes-base.nonexistent',
|
|
257
|
+
nodeName: 'my_node',
|
|
258
|
+
}));
|
|
259
|
+
});
|
|
260
|
+
it('returns errors for multiple invalid node types', () => {
|
|
261
|
+
const nodes = [
|
|
262
|
+
{ name: 'valid_node', type: 'n8n-nodes-base.webhook' },
|
|
263
|
+
{ name: 'invalid_one', type: 'n8n-nodes-base.fake' },
|
|
264
|
+
{ name: 'invalid_two', type: 'n8n-nodes-base.bogus' },
|
|
265
|
+
];
|
|
266
|
+
const errors = validateNodeTypes(nodes, availableTypes);
|
|
267
|
+
expect(errors).toHaveLength(2);
|
|
268
|
+
expect(errors.map((e) => e.nodeName)).toEqual(['invalid_one', 'invalid_two']);
|
|
269
|
+
});
|
|
270
|
+
it('provides suggestions for typos', () => {
|
|
271
|
+
const nodes = [
|
|
272
|
+
{ name: 'trigger', type: 'n8n-nodes-base.webhok' }, // typo: webhok
|
|
273
|
+
];
|
|
274
|
+
const errors = validateNodeTypes(nodes, availableTypes);
|
|
275
|
+
expect(errors).toHaveLength(1);
|
|
276
|
+
expect(errors[0].suggestions).toContain('n8n-nodes-base.webhook');
|
|
277
|
+
});
|
|
278
|
+
it('provides suggestions for partial matches', () => {
|
|
279
|
+
const nodes = [
|
|
280
|
+
{ name: 'code_node', type: 'n8n-nodes-base.cod' }, // partial: cod
|
|
281
|
+
];
|
|
282
|
+
const errors = validateNodeTypes(nodes, availableTypes);
|
|
283
|
+
expect(errors).toHaveLength(1);
|
|
284
|
+
expect(errors[0].suggestions).toContain('n8n-nodes-base.code');
|
|
285
|
+
});
|
|
286
|
+
it('returns empty suggestions when no matches found', () => {
|
|
287
|
+
const nodes = [
|
|
288
|
+
{ name: 'xyz_node', type: 'n8n-nodes-base.xyz123completely_random' },
|
|
289
|
+
];
|
|
290
|
+
const errors = validateNodeTypes(nodes, availableTypes);
|
|
291
|
+
expect(errors).toHaveLength(1);
|
|
292
|
+
expect(errors[0].suggestions).toHaveLength(0);
|
|
293
|
+
});
|
|
294
|
+
it('limits suggestions to 3', () => {
|
|
295
|
+
// Create a set with many similar types
|
|
296
|
+
const manyTypes = new Set([
|
|
297
|
+
'n8n-nodes-base.httpRequest',
|
|
298
|
+
'n8n-nodes-base.httpRequestTool',
|
|
299
|
+
'n8n-nodes-base.httpRequestV1',
|
|
300
|
+
'n8n-nodes-base.httpRequestV2',
|
|
301
|
+
'n8n-nodes-base.httpRequestV3',
|
|
302
|
+
]);
|
|
303
|
+
const nodes = [
|
|
304
|
+
{ name: 'http', type: 'n8n-nodes-base.http' }, // should match multiple
|
|
305
|
+
];
|
|
306
|
+
const errors = validateNodeTypes(nodes, manyTypes);
|
|
307
|
+
expect(errors).toHaveLength(1);
|
|
308
|
+
expect(errors[0].suggestions.length).toBeLessThanOrEqual(3);
|
|
309
|
+
});
|
|
310
|
+
});
|
package/docs/best-practices.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# n8n Best Practices
|
|
2
2
|
|
|
3
|
+
> **These rules are automatically enforced.** The MCP validates, auto-fixes, and formats on every `workflow_create` and `workflow_update`. You'll only see warnings for issues that can't be auto-fixed.
|
|
4
|
+
|
|
3
5
|
## Quick Reference
|
|
4
6
|
|
|
5
7
|
```javascript
|
|
@@ -21,16 +23,16 @@
|
|
|
21
23
|
|
|
22
24
|
## The Rules
|
|
23
25
|
|
|
24
|
-
### 1. snake_case
|
|
26
|
+
### 1. snake_case (auto-fixed)
|
|
25
27
|
|
|
26
28
|
```
|
|
27
29
|
Good: fetch_articles, check_approved, generate_content
|
|
28
30
|
Bad: FetchArticles, Check Approved, generate-content
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
Why: Consistency, readability
|
|
33
|
+
Why: Consistency, readability. **Auto-fixed:** renamed automatically with all references updated.
|
|
32
34
|
|
|
33
|
-
### 2. Explicit References
|
|
35
|
+
### 2. Explicit References (auto-fixed)
|
|
34
36
|
|
|
35
37
|
```javascript
|
|
36
38
|
// Bad - breaks when flow changes
|
|
@@ -40,7 +42,7 @@ Why: Consistency, readability, auto-fixable.
|
|
|
40
42
|
{{ $('node_name').item.json.field }}
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
Why: `$json` references "previous node" implicitly. Reorder nodes, it breaks.
|
|
45
|
+
Why: `$json` references "previous node" implicitly. Reorder nodes, it breaks. **Auto-fixed:** converted to explicit `$('prev_node')` references.
|
|
44
46
|
|
|
45
47
|
### 3. Config Node
|
|
46
48
|
|
|
@@ -63,16 +65,18 @@ Reference everywhere: `{{ $('config').item.json.channel_id }}`
|
|
|
63
65
|
|
|
64
66
|
Why: Change once, not in 5 nodes.
|
|
65
67
|
|
|
66
|
-
### 4. Secrets in Environment
|
|
68
|
+
### 4. Secrets in Environment (recommended)
|
|
67
69
|
|
|
68
70
|
```javascript
|
|
69
|
-
//
|
|
71
|
+
// Hardcoded (works, but less portable)
|
|
70
72
|
{ "apiKey": "sk_live_abc123" }
|
|
71
73
|
|
|
72
|
-
//
|
|
74
|
+
// Environment variable (recommended)
|
|
73
75
|
{{ $env.API_KEY }}
|
|
74
76
|
```
|
|
75
77
|
|
|
78
|
+
Why: Env vars make workflows portable across environments and avoid committing secrets.
|
|
79
|
+
|
|
76
80
|
## Parameter Preservation
|
|
77
81
|
|
|
78
82
|
**Critical:** Partial updates REPLACE the entire `parameters` object.
|
|
@@ -107,9 +111,9 @@ Before updating: read current state with `workflow_get`.
|
|
|
107
111
|
|
|
108
112
|
## AI Nodes
|
|
109
113
|
|
|
110
|
-
### Structured Output
|
|
114
|
+
### Structured Output (auto-fixed)
|
|
111
115
|
|
|
112
|
-
Always set for predictable JSON:
|
|
116
|
+
Always set for predictable JSON. **Auto-fixed:** `promptType: "define"` and `hasOutputParser: true` added automatically.
|
|
113
117
|
|
|
114
118
|
| Setting | Value |
|
|
115
119
|
|---------|-------|
|
|
@@ -149,7 +153,8 @@ When code IS necessary:
|
|
|
149
153
|
| 2. List versions | Know rollback point |
|
|
150
154
|
| 3. Read full workflow | Understand current state |
|
|
151
155
|
| 4. Make targeted change | Minimal surface area |
|
|
152
|
-
|
|
156
|
+
|
|
157
|
+
> **Note:** Validation and cleanup are now automatic. Every create/update validates, auto-fixes, and formats automatically.
|
|
153
158
|
|
|
154
159
|
## Node-Specific Settings
|
|
155
160
|
|
package/docs/node-config.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Node Configuration Guidelines
|
|
2
2
|
|
|
3
|
-
> Settings that make AI-created nodes human-editable
|
|
3
|
+
> Settings that make AI-created nodes human-editable.
|
|
4
|
+
>
|
|
5
|
+
> **Node types are validated.** Invalid types are blocked with suggestions before hitting n8n. Use `node_types_list` to discover available nodes.
|
|
4
6
|
|
|
5
7
|
## Resource Locator Pattern
|
|
6
8
|
|
package/logo.png
ADDED
|
Binary file
|