@pagelines/n8n-mcp 0.1.0 → 0.2.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/CHANGELOG.md +52 -0
- package/README.md +37 -106
- package/dist/autofix.d.ts +28 -0
- package/dist/autofix.js +222 -0
- package/dist/expressions.d.ts +25 -0
- package/dist/expressions.js +209 -0
- package/dist/index.js +124 -1
- package/dist/tools.js +147 -4
- package/dist/validators.js +67 -0
- package/dist/validators.test.js +83 -0
- package/dist/versions.d.ts +71 -0
- package/dist/versions.js +239 -0
- package/docs/best-practices.md +241 -0
- package/package.json +1 -1
- package/server.json +10 -2
- package/src/autofix.ts +275 -0
- package/src/expressions.ts +254 -0
- package/src/index.ts +169 -1
- package/src/tools.ts +155 -4
- package/src/validators.test.ts +97 -0
- package/src/validators.ts +77 -0
- package/src/versions.ts +320 -0
package/src/index.ts
CHANGED
|
@@ -14,6 +14,16 @@ import {
|
|
|
14
14
|
import { N8nClient } from './n8n-client.js';
|
|
15
15
|
import { tools } from './tools.js';
|
|
16
16
|
import { validateWorkflow } from './validators.js';
|
|
17
|
+
import { validateExpressions, checkCircularReferences } from './expressions.js';
|
|
18
|
+
import { autofixWorkflow, formatWorkflow } from './autofix.js';
|
|
19
|
+
import {
|
|
20
|
+
initVersionControl,
|
|
21
|
+
saveVersion,
|
|
22
|
+
listVersions,
|
|
23
|
+
getVersion,
|
|
24
|
+
diffWorkflows,
|
|
25
|
+
getVersionStats,
|
|
26
|
+
} from './versions.js';
|
|
17
27
|
import type { PatchOperation, N8nConnections } from './types.js';
|
|
18
28
|
|
|
19
29
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -34,6 +44,12 @@ const client = new N8nClient({
|
|
|
34
44
|
apiKey: N8N_API_KEY,
|
|
35
45
|
});
|
|
36
46
|
|
|
47
|
+
// Initialize version control
|
|
48
|
+
initVersionControl({
|
|
49
|
+
enabled: process.env.N8N_MCP_VERSIONS !== 'false',
|
|
50
|
+
maxVersions: parseInt(process.env.N8N_MCP_MAX_VERSIONS || '20', 10),
|
|
51
|
+
});
|
|
52
|
+
|
|
37
53
|
// ─────────────────────────────────────────────────────────────
|
|
38
54
|
// MCP Server
|
|
39
55
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -146,6 +162,10 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
146
162
|
}
|
|
147
163
|
|
|
148
164
|
case 'workflow_update': {
|
|
165
|
+
// Save version before updating
|
|
166
|
+
const currentWorkflow = await client.getWorkflow(args.id as string);
|
|
167
|
+
const versionSaved = await saveVersion(currentWorkflow, 'before_update');
|
|
168
|
+
|
|
149
169
|
const operations = args.operations as PatchOperation[];
|
|
150
170
|
const { workflow, warnings } = await client.patchWorkflow(
|
|
151
171
|
args.id as string,
|
|
@@ -159,6 +179,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
159
179
|
workflow,
|
|
160
180
|
patchWarnings: warnings,
|
|
161
181
|
validation,
|
|
182
|
+
versionSaved: versionSaved ? versionSaved.id : null,
|
|
162
183
|
};
|
|
163
184
|
}
|
|
164
185
|
|
|
@@ -211,17 +232,164 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
211
232
|
return execution;
|
|
212
233
|
}
|
|
213
234
|
|
|
214
|
-
// Validation
|
|
235
|
+
// Validation & Quality
|
|
215
236
|
case 'workflow_validate': {
|
|
216
237
|
const workflow = await client.getWorkflow(args.id as string);
|
|
217
238
|
const validation = validateWorkflow(workflow);
|
|
239
|
+
const expressionIssues = validateExpressions(workflow);
|
|
240
|
+
const circularRefs = checkCircularReferences(workflow);
|
|
241
|
+
|
|
218
242
|
return {
|
|
219
243
|
workflowId: workflow.id,
|
|
220
244
|
workflowName: workflow.name,
|
|
221
245
|
...validation,
|
|
246
|
+
expressionIssues,
|
|
247
|
+
circularReferences: circularRefs.length > 0 ? circularRefs : null,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case 'workflow_autofix': {
|
|
252
|
+
const workflow = await client.getWorkflow(args.id as string);
|
|
253
|
+
const validation = validateWorkflow(workflow);
|
|
254
|
+
const result = autofixWorkflow(workflow, validation.warnings);
|
|
255
|
+
|
|
256
|
+
if (args.apply && result.fixes.length > 0) {
|
|
257
|
+
// Save version before applying fixes
|
|
258
|
+
await saveVersion(workflow, 'before_autofix');
|
|
259
|
+
|
|
260
|
+
// Apply the fixed workflow
|
|
261
|
+
await client.updateWorkflow(args.id as string, result.workflow);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
applied: true,
|
|
265
|
+
fixes: result.fixes,
|
|
266
|
+
unfixable: result.unfixable,
|
|
267
|
+
workflow: result.workflow,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
applied: false,
|
|
273
|
+
fixes: result.fixes,
|
|
274
|
+
unfixable: result.unfixable,
|
|
275
|
+
previewWorkflow: result.workflow,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
case 'workflow_format': {
|
|
280
|
+
const workflow = await client.getWorkflow(args.id as string);
|
|
281
|
+
const formatted = formatWorkflow(workflow);
|
|
282
|
+
|
|
283
|
+
if (args.apply) {
|
|
284
|
+
await saveVersion(workflow, 'before_format');
|
|
285
|
+
await client.updateWorkflow(args.id as string, formatted);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
applied: true,
|
|
289
|
+
workflow: formatted,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
applied: false,
|
|
295
|
+
previewWorkflow: formatted,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Version Control
|
|
300
|
+
case 'version_list': {
|
|
301
|
+
const versions = await listVersions(args.workflowId as string);
|
|
302
|
+
return {
|
|
303
|
+
workflowId: args.workflowId,
|
|
304
|
+
versions,
|
|
305
|
+
total: versions.length,
|
|
222
306
|
};
|
|
223
307
|
}
|
|
224
308
|
|
|
309
|
+
case 'version_get': {
|
|
310
|
+
const version = await getVersion(
|
|
311
|
+
args.workflowId as string,
|
|
312
|
+
args.versionId as string
|
|
313
|
+
);
|
|
314
|
+
if (!version) {
|
|
315
|
+
throw new Error(`Version ${args.versionId} not found`);
|
|
316
|
+
}
|
|
317
|
+
return version;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
case 'version_save': {
|
|
321
|
+
const workflow = await client.getWorkflow(args.workflowId as string);
|
|
322
|
+
const version = await saveVersion(
|
|
323
|
+
workflow,
|
|
324
|
+
(args.reason as string) || 'manual'
|
|
325
|
+
);
|
|
326
|
+
if (!version) {
|
|
327
|
+
return { saved: false, message: 'No changes detected since last version' };
|
|
328
|
+
}
|
|
329
|
+
return { saved: true, version };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
case 'version_rollback': {
|
|
333
|
+
const version = await getVersion(
|
|
334
|
+
args.workflowId as string,
|
|
335
|
+
args.versionId as string
|
|
336
|
+
);
|
|
337
|
+
if (!version) {
|
|
338
|
+
throw new Error(`Version ${args.versionId} not found`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Save current state before rollback
|
|
342
|
+
const currentWorkflow = await client.getWorkflow(args.workflowId as string);
|
|
343
|
+
await saveVersion(currentWorkflow, 'before_rollback');
|
|
344
|
+
|
|
345
|
+
// Apply the old version
|
|
346
|
+
await client.updateWorkflow(args.workflowId as string, version.workflow);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
success: true,
|
|
350
|
+
restoredVersion: version.meta,
|
|
351
|
+
workflow: version.workflow,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
case 'version_diff': {
|
|
356
|
+
const toVersion = await getVersion(
|
|
357
|
+
args.workflowId as string,
|
|
358
|
+
args.toVersionId as string
|
|
359
|
+
);
|
|
360
|
+
if (!toVersion) {
|
|
361
|
+
throw new Error(`Version ${args.toVersionId} not found`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let fromWorkflow;
|
|
365
|
+
if (args.fromVersionId) {
|
|
366
|
+
const fromVersion = await getVersion(
|
|
367
|
+
args.workflowId as string,
|
|
368
|
+
args.fromVersionId as string
|
|
369
|
+
);
|
|
370
|
+
if (!fromVersion) {
|
|
371
|
+
throw new Error(`Version ${args.fromVersionId} not found`);
|
|
372
|
+
}
|
|
373
|
+
fromWorkflow = fromVersion.workflow;
|
|
374
|
+
} else {
|
|
375
|
+
// Compare against current workflow state
|
|
376
|
+
fromWorkflow = await client.getWorkflow(args.workflowId as string);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const diff = diffWorkflows(fromWorkflow, toVersion.workflow);
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
from: args.fromVersionId || 'current',
|
|
383
|
+
to: args.toVersionId,
|
|
384
|
+
diff,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
case 'version_stats': {
|
|
389
|
+
const stats = await getVersionStats();
|
|
390
|
+
return stats;
|
|
391
|
+
}
|
|
392
|
+
|
|
225
393
|
default:
|
|
226
394
|
throw new Error(`Unknown tool: ${name}`);
|
|
227
395
|
}
|
package/src/tools.ts
CHANGED
|
@@ -249,16 +249,17 @@ Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "proper
|
|
|
249
249
|
},
|
|
250
250
|
|
|
251
251
|
// ─────────────────────────────────────────────────────────────
|
|
252
|
-
// Validation
|
|
252
|
+
// Validation & Quality
|
|
253
253
|
// ─────────────────────────────────────────────────────────────
|
|
254
254
|
{
|
|
255
255
|
name: 'workflow_validate',
|
|
256
256
|
description: `Validate a workflow against best practices:
|
|
257
257
|
- snake_case naming
|
|
258
258
|
- Explicit node references (no $json)
|
|
259
|
-
- No hardcoded IDs
|
|
260
|
-
- No
|
|
261
|
-
-
|
|
259
|
+
- No hardcoded IDs or secrets
|
|
260
|
+
- No orphan nodes
|
|
261
|
+
- AI node structured output
|
|
262
|
+
- Expression syntax validation`,
|
|
262
263
|
inputSchema: {
|
|
263
264
|
type: 'object',
|
|
264
265
|
properties: {
|
|
@@ -270,4 +271,154 @@ Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "proper
|
|
|
270
271
|
required: ['id'],
|
|
271
272
|
},
|
|
272
273
|
},
|
|
274
|
+
|
|
275
|
+
{
|
|
276
|
+
name: 'workflow_autofix',
|
|
277
|
+
description: `Auto-fix common validation issues:
|
|
278
|
+
- Convert names to snake_case
|
|
279
|
+
- Replace $json with explicit node references
|
|
280
|
+
- Add AI structured output settings
|
|
281
|
+
|
|
282
|
+
Returns the fixed workflow and list of changes made.`,
|
|
283
|
+
inputSchema: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: {
|
|
286
|
+
id: {
|
|
287
|
+
type: 'string',
|
|
288
|
+
description: 'Workflow ID to fix',
|
|
289
|
+
},
|
|
290
|
+
apply: {
|
|
291
|
+
type: 'boolean',
|
|
292
|
+
description: 'Apply fixes to n8n (default: false, dry-run)',
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
required: ['id'],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
{
|
|
300
|
+
name: 'workflow_format',
|
|
301
|
+
description: 'Format a workflow: sort nodes by position, clean up null values.',
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
properties: {
|
|
305
|
+
id: {
|
|
306
|
+
type: 'string',
|
|
307
|
+
description: 'Workflow ID to format',
|
|
308
|
+
},
|
|
309
|
+
apply: {
|
|
310
|
+
type: 'boolean',
|
|
311
|
+
description: 'Apply formatting to n8n (default: false)',
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
required: ['id'],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
// ─────────────────────────────────────────────────────────────
|
|
319
|
+
// Version Control
|
|
320
|
+
// ─────────────────────────────────────────────────────────────
|
|
321
|
+
{
|
|
322
|
+
name: 'version_list',
|
|
323
|
+
description: 'List saved versions of a workflow (local snapshots).',
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: 'object',
|
|
326
|
+
properties: {
|
|
327
|
+
workflowId: {
|
|
328
|
+
type: 'string',
|
|
329
|
+
description: 'Workflow ID',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
required: ['workflowId'],
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
{
|
|
337
|
+
name: 'version_get',
|
|
338
|
+
description: 'Get a specific saved version of a workflow.',
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: 'object',
|
|
341
|
+
properties: {
|
|
342
|
+
workflowId: {
|
|
343
|
+
type: 'string',
|
|
344
|
+
description: 'Workflow ID',
|
|
345
|
+
},
|
|
346
|
+
versionId: {
|
|
347
|
+
type: 'string',
|
|
348
|
+
description: 'Version ID (from version_list)',
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
required: ['workflowId', 'versionId'],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
{
|
|
356
|
+
name: 'version_save',
|
|
357
|
+
description: 'Manually save a version snapshot of a workflow.',
|
|
358
|
+
inputSchema: {
|
|
359
|
+
type: 'object',
|
|
360
|
+
properties: {
|
|
361
|
+
workflowId: {
|
|
362
|
+
type: 'string',
|
|
363
|
+
description: 'Workflow ID',
|
|
364
|
+
},
|
|
365
|
+
reason: {
|
|
366
|
+
type: 'string',
|
|
367
|
+
description: 'Reason for saving (default: "manual")',
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
required: ['workflowId'],
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
{
|
|
375
|
+
name: 'version_rollback',
|
|
376
|
+
description: 'Restore a workflow to a previous version.',
|
|
377
|
+
inputSchema: {
|
|
378
|
+
type: 'object',
|
|
379
|
+
properties: {
|
|
380
|
+
workflowId: {
|
|
381
|
+
type: 'string',
|
|
382
|
+
description: 'Workflow ID',
|
|
383
|
+
},
|
|
384
|
+
versionId: {
|
|
385
|
+
type: 'string',
|
|
386
|
+
description: 'Version ID to restore',
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
required: ['workflowId', 'versionId'],
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
{
|
|
394
|
+
name: 'version_diff',
|
|
395
|
+
description: 'Compare two versions of a workflow or current state vs a version.',
|
|
396
|
+
inputSchema: {
|
|
397
|
+
type: 'object',
|
|
398
|
+
properties: {
|
|
399
|
+
workflowId: {
|
|
400
|
+
type: 'string',
|
|
401
|
+
description: 'Workflow ID',
|
|
402
|
+
},
|
|
403
|
+
fromVersionId: {
|
|
404
|
+
type: 'string',
|
|
405
|
+
description: 'First version ID (omit for current workflow state)',
|
|
406
|
+
},
|
|
407
|
+
toVersionId: {
|
|
408
|
+
type: 'string',
|
|
409
|
+
description: 'Second version ID',
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
required: ['workflowId', 'toVersionId'],
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
{
|
|
417
|
+
name: 'version_stats',
|
|
418
|
+
description: 'Get version control statistics.',
|
|
419
|
+
inputSchema: {
|
|
420
|
+
type: 'object',
|
|
421
|
+
properties: {},
|
|
422
|
+
},
|
|
423
|
+
},
|
|
273
424
|
];
|
package/src/validators.test.ts
CHANGED
|
@@ -115,6 +115,103 @@ describe('validateWorkflow', () => {
|
|
|
115
115
|
})
|
|
116
116
|
);
|
|
117
117
|
});
|
|
118
|
+
|
|
119
|
+
it('info on code node usage', () => {
|
|
120
|
+
const workflow = createWorkflow({
|
|
121
|
+
nodes: [
|
|
122
|
+
{
|
|
123
|
+
id: '1',
|
|
124
|
+
name: 'my_code',
|
|
125
|
+
type: 'n8n-nodes-base.code',
|
|
126
|
+
typeVersion: 1,
|
|
127
|
+
position: [0, 0],
|
|
128
|
+
parameters: { jsCode: 'return items;' },
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = validateWorkflow(workflow);
|
|
134
|
+
expect(result.warnings).toContainEqual(
|
|
135
|
+
expect.objectContaining({
|
|
136
|
+
rule: 'code_node_usage',
|
|
137
|
+
severity: 'info',
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('warns on AI node without structured output settings', () => {
|
|
143
|
+
const workflow = createWorkflow({
|
|
144
|
+
nodes: [
|
|
145
|
+
{
|
|
146
|
+
id: '1',
|
|
147
|
+
name: 'ai_agent',
|
|
148
|
+
type: '@n8n/n8n-nodes-langchain.agent',
|
|
149
|
+
typeVersion: 1,
|
|
150
|
+
position: [0, 0],
|
|
151
|
+
parameters: {
|
|
152
|
+
outputParser: true,
|
|
153
|
+
schemaType: 'manual',
|
|
154
|
+
// Missing promptType: 'define' and hasOutputParser: true
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const result = validateWorkflow(workflow);
|
|
161
|
+
expect(result.warnings).toContainEqual(
|
|
162
|
+
expect.objectContaining({
|
|
163
|
+
rule: 'ai_structured_output',
|
|
164
|
+
severity: 'warning',
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('passes AI node with correct structured output settings', () => {
|
|
170
|
+
const workflow = createWorkflow({
|
|
171
|
+
nodes: [
|
|
172
|
+
{
|
|
173
|
+
id: '1',
|
|
174
|
+
name: 'ai_agent',
|
|
175
|
+
type: '@n8n/n8n-nodes-langchain.agent',
|
|
176
|
+
typeVersion: 1,
|
|
177
|
+
position: [0, 0],
|
|
178
|
+
parameters: {
|
|
179
|
+
outputParser: true,
|
|
180
|
+
schemaType: 'manual',
|
|
181
|
+
promptType: 'define',
|
|
182
|
+
hasOutputParser: true,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const result = validateWorkflow(workflow);
|
|
189
|
+
const aiWarnings = result.warnings.filter((w) => w.rule === 'ai_structured_output');
|
|
190
|
+
expect(aiWarnings).toHaveLength(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('warns on in-memory storage nodes', () => {
|
|
194
|
+
const workflow = createWorkflow({
|
|
195
|
+
nodes: [
|
|
196
|
+
{
|
|
197
|
+
id: '1',
|
|
198
|
+
name: 'memory_buffer',
|
|
199
|
+
type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
|
|
200
|
+
typeVersion: 1,
|
|
201
|
+
position: [0, 0],
|
|
202
|
+
parameters: {},
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const result = validateWorkflow(workflow);
|
|
208
|
+
expect(result.warnings).toContainEqual(
|
|
209
|
+
expect.objectContaining({
|
|
210
|
+
rule: 'in_memory_storage',
|
|
211
|
+
severity: 'warning',
|
|
212
|
+
})
|
|
213
|
+
);
|
|
214
|
+
});
|
|
118
215
|
});
|
|
119
216
|
|
|
120
217
|
describe('validatePartialUpdate', () => {
|
package/src/validators.ts
CHANGED
|
@@ -37,6 +37,15 @@ function validateNode(node: N8nNode, warnings: ValidationWarning[]): void {
|
|
|
37
37
|
|
|
38
38
|
// Check for hardcoded secrets
|
|
39
39
|
checkForHardcodedSecrets(node, warnings);
|
|
40
|
+
|
|
41
|
+
// Check for code node usage (should be last resort)
|
|
42
|
+
checkForCodeNodeUsage(node, warnings);
|
|
43
|
+
|
|
44
|
+
// Check for AI node structured output settings
|
|
45
|
+
checkForAIStructuredOutput(node, warnings);
|
|
46
|
+
|
|
47
|
+
// Check for in-memory storage (non-persistent)
|
|
48
|
+
checkForInMemoryStorage(node, warnings);
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
function validateSnakeCase(name: string, context: string, warnings: ValidationWarning[]): void {
|
|
@@ -132,6 +141,74 @@ function checkForHardcodedSecrets(node: N8nNode, warnings: ValidationWarning[]):
|
|
|
132
141
|
}
|
|
133
142
|
}
|
|
134
143
|
|
|
144
|
+
function checkForCodeNodeUsage(node: N8nNode, warnings: ValidationWarning[]): void {
|
|
145
|
+
// Detect code nodes - they should be last resort
|
|
146
|
+
const codeNodeTypes = [
|
|
147
|
+
'n8n-nodes-base.code',
|
|
148
|
+
'n8n-nodes-base.function',
|
|
149
|
+
'n8n-nodes-base.functionItem',
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
if (codeNodeTypes.some((t) => node.type.includes(t))) {
|
|
153
|
+
warnings.push({
|
|
154
|
+
node: node.name,
|
|
155
|
+
rule: 'code_node_usage',
|
|
156
|
+
message: `Node "${node.name}" is a code node - ensure built-in nodes can't achieve this`,
|
|
157
|
+
severity: 'info',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function checkForAIStructuredOutput(node: N8nNode, warnings: ValidationWarning[]): void {
|
|
163
|
+
// Check AI/LLM nodes for structured output settings
|
|
164
|
+
const aiNodeTypes = [
|
|
165
|
+
'langchain.agent',
|
|
166
|
+
'langchain.chainLlm',
|
|
167
|
+
'langchain.lmChatOpenAi',
|
|
168
|
+
'langchain.lmChatAnthropic',
|
|
169
|
+
'langchain.lmChatGoogleGemini',
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const isAINode = aiNodeTypes.some((t) => node.type.toLowerCase().includes(t.toLowerCase()));
|
|
173
|
+
if (!isAINode) return;
|
|
174
|
+
|
|
175
|
+
// Check for structured output settings
|
|
176
|
+
const params = node.parameters as Record<string, unknown>;
|
|
177
|
+
const hasPromptType = params.promptType === 'define';
|
|
178
|
+
const hasOutputParser = params.hasOutputParser === true;
|
|
179
|
+
|
|
180
|
+
// Only warn if it looks like they want structured output but missed settings
|
|
181
|
+
if (params.outputParser || params.schemaType) {
|
|
182
|
+
if (!hasPromptType || !hasOutputParser) {
|
|
183
|
+
warnings.push({
|
|
184
|
+
node: node.name,
|
|
185
|
+
rule: 'ai_structured_output',
|
|
186
|
+
message: `Node "${node.name}" may need promptType: "define" and hasOutputParser: true for reliable structured output`,
|
|
187
|
+
severity: 'warning',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function checkForInMemoryStorage(node: N8nNode, warnings: ValidationWarning[]): void {
|
|
194
|
+
// Detect in-memory storage nodes that don't persist across restarts
|
|
195
|
+
const inMemoryTypes = [
|
|
196
|
+
'memoryBufferWindow',
|
|
197
|
+
'memoryVectorStore',
|
|
198
|
+
'vectorStoreInMemory',
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const isInMemory = inMemoryTypes.some((t) => node.type.toLowerCase().includes(t.toLowerCase()));
|
|
202
|
+
if (isInMemory) {
|
|
203
|
+
warnings.push({
|
|
204
|
+
node: node.name,
|
|
205
|
+
rule: 'in_memory_storage',
|
|
206
|
+
message: `Node "${node.name}" uses in-memory storage - consider Postgres for production persistence`,
|
|
207
|
+
severity: 'warning',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
135
212
|
function validateConnections(workflow: N8nWorkflow, warnings: ValidationWarning[]): void {
|
|
136
213
|
const connectedNodes = new Set<string>();
|
|
137
214
|
|