@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/src/autofix.ts DELETED
@@ -1,275 +0,0 @@
1
- /**
2
- * Auto-fix common n8n workflow issues
3
- * Transforms workflows to follow best practices
4
- */
5
-
6
- import type { N8nWorkflow, N8nNode, ValidationWarning } from './types.js';
7
-
8
- export interface AutofixResult {
9
- workflow: N8nWorkflow;
10
- fixes: AutofixAction[];
11
- unfixable: ValidationWarning[];
12
- }
13
-
14
- export interface AutofixAction {
15
- type: string;
16
- target: string;
17
- description: string;
18
- before?: string;
19
- after?: string;
20
- }
21
-
22
- /**
23
- * Auto-fix a workflow based on validation warnings
24
- */
25
- export function autofixWorkflow(
26
- workflow: N8nWorkflow,
27
- warnings: ValidationWarning[]
28
- ): AutofixResult {
29
- // Deep clone to avoid mutation
30
- const fixed: N8nWorkflow = JSON.parse(JSON.stringify(workflow));
31
- const fixes: AutofixAction[] = [];
32
- const unfixable: ValidationWarning[] = [];
33
-
34
- for (const warning of warnings) {
35
- const result = attemptFix(fixed, warning);
36
- if (result) {
37
- fixes.push(result);
38
- } else {
39
- unfixable.push(warning);
40
- }
41
- }
42
-
43
- return { workflow: fixed, fixes, unfixable };
44
- }
45
-
46
- function attemptFix(workflow: N8nWorkflow, warning: ValidationWarning): AutofixAction | null {
47
- switch (warning.rule) {
48
- case 'snake_case':
49
- return fixSnakeCase(workflow, warning);
50
-
51
- case 'explicit_reference':
52
- return fixExplicitReference(workflow, warning);
53
-
54
- case 'ai_structured_output':
55
- return fixAIStructuredOutput(workflow, warning);
56
-
57
- default:
58
- // Rules that can't be auto-fixed:
59
- // - no_hardcoded_secrets (need manual review)
60
- // - no_hardcoded_ids (need manual review)
61
- // - orphan_node (need context to know what to connect)
62
- // - code_node_usage (info only)
63
- // - in_memory_storage (architectural decision)
64
- return null;
65
- }
66
- }
67
-
68
- /**
69
- * Fix snake_case naming
70
- */
71
- function fixSnakeCase(workflow: N8nWorkflow, warning: ValidationWarning): AutofixAction | null {
72
- const target = warning.node || 'workflow';
73
-
74
- if (!warning.node) {
75
- // Fix workflow name
76
- const oldName = workflow.name;
77
- const newName = toSnakeCase(oldName);
78
-
79
- if (oldName === newName) return null;
80
-
81
- workflow.name = newName;
82
- return {
83
- type: 'rename',
84
- target: 'workflow',
85
- description: `Renamed workflow to snake_case`,
86
- before: oldName,
87
- after: newName,
88
- };
89
- }
90
-
91
- // Fix node name
92
- const node = workflow.nodes.find((n) => n.name === warning.node);
93
- if (!node) return null;
94
-
95
- const oldName = node.name;
96
- const newName = toSnakeCase(oldName);
97
-
98
- if (oldName === newName) return null;
99
-
100
- // Update node name
101
- node.name = newName;
102
-
103
- // Update connections that reference this node
104
- if (workflow.connections[oldName]) {
105
- workflow.connections[newName] = workflow.connections[oldName];
106
- delete workflow.connections[oldName];
107
- }
108
-
109
- // Update connections that target this node
110
- for (const outputs of Object.values(workflow.connections)) {
111
- for (const outputType of Object.values(outputs)) {
112
- for (const connections of outputType) {
113
- for (const conn of connections) {
114
- if (conn.node === oldName) {
115
- conn.node = newName;
116
- }
117
- }
118
- }
119
- }
120
- }
121
-
122
- return {
123
- type: 'rename',
124
- target: `node:${oldName}`,
125
- description: `Renamed node to snake_case`,
126
- before: oldName,
127
- after: newName,
128
- };
129
- }
130
-
131
- /**
132
- * Convert to snake_case
133
- */
134
- function toSnakeCase(name: string): string {
135
- return name
136
- .replace(/([A-Z])/g, '_$1')
137
- .replace(/[-\s]+/g, '_')
138
- .replace(/^_/, '')
139
- .replace(/_+/g, '_')
140
- .toLowerCase();
141
- }
142
-
143
- /**
144
- * Fix $json to explicit references
145
- * This is a best-effort fix - may need manual review
146
- */
147
- function fixExplicitReference(
148
- workflow: N8nWorkflow,
149
- warning: ValidationWarning
150
- ): AutofixAction | null {
151
- if (!warning.node) return null;
152
-
153
- const node = workflow.nodes.find((n) => n.name === warning.node);
154
- if (!node) return null;
155
-
156
- // Find the previous node in the connection chain
157
- const previousNode = findPreviousNode(workflow, node.name);
158
- if (!previousNode) {
159
- // Can't auto-fix without knowing the source
160
- return null;
161
- }
162
-
163
- // Replace $json with explicit reference
164
- const params = JSON.stringify(node.parameters);
165
- const fixedParams = params
166
- .replace(/\$json\./g, `$('${previousNode}').item.json.`)
167
- .replace(/\{\{\s*\$json\./g, `{{ $('${previousNode}').item.json.`);
168
-
169
- if (params === fixedParams) return null;
170
-
171
- node.parameters = JSON.parse(fixedParams);
172
-
173
- return {
174
- type: 'expression_fix',
175
- target: `node:${node.name}`,
176
- description: `Changed $json to explicit $('${previousNode}') reference`,
177
- before: '$json.',
178
- after: `$('${previousNode}').item.json.`,
179
- };
180
- }
181
-
182
- /**
183
- * Find the previous node in the connection chain
184
- */
185
- function findPreviousNode(workflow: N8nWorkflow, nodeName: string): string | null {
186
- for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
187
- for (const outputType of Object.values(outputs)) {
188
- for (const connections of outputType) {
189
- for (const conn of connections) {
190
- if (conn.node === nodeName) {
191
- return sourceName;
192
- }
193
- }
194
- }
195
- }
196
- }
197
- return null;
198
- }
199
-
200
- /**
201
- * Fix AI structured output settings
202
- */
203
- function fixAIStructuredOutput(
204
- workflow: N8nWorkflow,
205
- warning: ValidationWarning
206
- ): AutofixAction | null {
207
- if (!warning.node) return null;
208
-
209
- const node = workflow.nodes.find((n) => n.name === warning.node);
210
- if (!node) return null;
211
-
212
- const params = node.parameters as Record<string, unknown>;
213
- const changes: string[] = [];
214
-
215
- if (params.promptType !== 'define') {
216
- params.promptType = 'define';
217
- changes.push('promptType: "define"');
218
- }
219
-
220
- if (params.hasOutputParser !== true) {
221
- params.hasOutputParser = true;
222
- changes.push('hasOutputParser: true');
223
- }
224
-
225
- if (changes.length === 0) return null;
226
-
227
- return {
228
- type: 'parameter_fix',
229
- target: `node:${node.name}`,
230
- description: `Added AI structured output settings: ${changes.join(', ')}`,
231
- };
232
- }
233
-
234
- /**
235
- * Format a workflow for consistency
236
- * - Sorts nodes by position
237
- * - Normalizes connection format
238
- * - Removes empty/null values
239
- */
240
- export function formatWorkflow(workflow: N8nWorkflow): N8nWorkflow {
241
- const formatted: N8nWorkflow = JSON.parse(JSON.stringify(workflow));
242
-
243
- // Sort nodes by Y position, then X
244
- formatted.nodes.sort((a, b) => {
245
- const [ax, ay] = a.position;
246
- const [bx, by] = b.position;
247
- if (ay !== by) return ay - by;
248
- return ax - bx;
249
- });
250
-
251
- // Clean up parameters - remove undefined/null
252
- for (const node of formatted.nodes) {
253
- node.parameters = cleanObject(node.parameters);
254
- }
255
-
256
- return formatted;
257
- }
258
-
259
- /**
260
- * Remove null/undefined values from object
261
- */
262
- function cleanObject(obj: Record<string, unknown>): Record<string, unknown> {
263
- const cleaned: Record<string, unknown> = {};
264
-
265
- for (const [key, value] of Object.entries(obj)) {
266
- if (value === null || value === undefined) continue;
267
- if (typeof value === 'object' && !Array.isArray(value)) {
268
- cleaned[key] = cleanObject(value as Record<string, unknown>);
269
- } else {
270
- cleaned[key] = value;
271
- }
272
- }
273
-
274
- return cleaned;
275
- }
@@ -1,254 +0,0 @@
1
- /**
2
- * n8n expression validation
3
- * Parses and validates expressions in workflow parameters
4
- */
5
-
6
- import type { N8nWorkflow, N8nNode, ValidationWarning } from './types.js';
7
-
8
- export interface ExpressionIssue {
9
- node: string;
10
- parameter: string;
11
- expression: string;
12
- issue: string;
13
- severity: 'error' | 'warning' | 'info';
14
- suggestion?: string;
15
- }
16
-
17
- /**
18
- * Validate all expressions in a workflow
19
- */
20
- export function validateExpressions(workflow: N8nWorkflow): ExpressionIssue[] {
21
- const issues: ExpressionIssue[] = [];
22
- const nodeNames = new Set(workflow.nodes.map((n) => n.name));
23
-
24
- for (const node of workflow.nodes) {
25
- const nodeIssues = validateNodeExpressions(node, nodeNames);
26
- issues.push(...nodeIssues);
27
- }
28
-
29
- return issues;
30
- }
31
-
32
- /**
33
- * Validate expressions in a single node
34
- */
35
- function validateNodeExpressions(node: N8nNode, nodeNames: Set<string>): ExpressionIssue[] {
36
- const issues: ExpressionIssue[] = [];
37
- const expressions = extractExpressions(node.parameters);
38
-
39
- for (const { path, expression } of expressions) {
40
- const exprIssues = validateExpression(expression, nodeNames);
41
- for (const issue of exprIssues) {
42
- issues.push({
43
- node: node.name,
44
- parameter: path,
45
- expression,
46
- ...issue,
47
- });
48
- }
49
- }
50
-
51
- return issues;
52
- }
53
-
54
- /**
55
- * Extract all expressions from parameters
56
- */
57
- function extractExpressions(
58
- params: Record<string, unknown>,
59
- basePath: string = ''
60
- ): Array<{ path: string; expression: string }> {
61
- const results: Array<{ path: string; expression: string }> = [];
62
-
63
- for (const [key, value] of Object.entries(params)) {
64
- const path = basePath ? `${basePath}.${key}` : key;
65
-
66
- if (typeof value === 'string') {
67
- // Check for expression patterns
68
- const exprMatches = value.match(/\{\{.*?\}\}/g) || [];
69
- for (const match of exprMatches) {
70
- results.push({ path, expression: match });
71
- }
72
- // Also check for ={{ prefix (common in n8n)
73
- if (value.startsWith('={{')) {
74
- results.push({ path, expression: value });
75
- }
76
- } else if (typeof value === 'object' && value !== null) {
77
- if (Array.isArray(value)) {
78
- for (let i = 0; i < value.length; i++) {
79
- if (typeof value[i] === 'object' && value[i] !== null) {
80
- results.push(...extractExpressions(value[i] as Record<string, unknown>, `${path}[${i}]`));
81
- } else if (typeof value[i] === 'string') {
82
- const strVal = value[i] as string;
83
- const exprMatches = strVal.match(/\{\{.*?\}\}/g) || [];
84
- for (const match of exprMatches) {
85
- results.push({ path: `${path}[${i}]`, expression: match });
86
- }
87
- }
88
- }
89
- } else {
90
- results.push(...extractExpressions(value as Record<string, unknown>, path));
91
- }
92
- }
93
- }
94
-
95
- return results;
96
- }
97
-
98
- /**
99
- * Validate a single expression
100
- */
101
- function validateExpression(
102
- expression: string,
103
- nodeNames: Set<string>
104
- ): Array<Omit<ExpressionIssue, 'node' | 'parameter' | 'expression'>> {
105
- const issues: Array<Omit<ExpressionIssue, 'node' | 'parameter' | 'expression'>> = [];
106
-
107
- // Strip wrapper
108
- let inner = expression;
109
- if (inner.startsWith('={{')) {
110
- inner = inner.slice(3).trim();
111
- } else if (inner.startsWith('{{') && inner.endsWith('}}')) {
112
- inner = inner.slice(2, -2).trim();
113
- }
114
-
115
- // Check for $json usage (should use explicit reference)
116
- if (/\$json\./.test(inner) && !/\$\(['"]/.test(inner)) {
117
- issues.push({
118
- issue: 'Uses $json instead of explicit node reference',
119
- severity: 'warning',
120
- suggestion: "Use $('node_name').item.json.field instead",
121
- });
122
- }
123
-
124
- // Check for $input usage (acceptable but check context)
125
- if (/\$input\./.test(inner)) {
126
- issues.push({
127
- issue: '$input reference found - ensure this is intentional',
128
- severity: 'info',
129
- suggestion: "Consider explicit $('node_name') for clarity",
130
- });
131
- }
132
-
133
- // Check for node references that don't exist
134
- const nodeRefMatches = inner.matchAll(/\$\(['"]([^'"]+)['"]\)/g);
135
- for (const match of nodeRefMatches) {
136
- const refNodeName = match[1];
137
- if (!nodeNames.has(refNodeName)) {
138
- issues.push({
139
- issue: `References non-existent node "${refNodeName}"`,
140
- severity: 'error',
141
- suggestion: `Check if the node exists or was renamed`,
142
- });
143
- }
144
- }
145
-
146
- // Check for common syntax errors
147
- if (expression.includes('{{') && !expression.includes('}}')) {
148
- issues.push({
149
- issue: 'Missing closing }}',
150
- severity: 'error',
151
- });
152
- }
153
-
154
- // Check for unmatched parentheses
155
- const openParens = (inner.match(/\(/g) || []).length;
156
- const closeParens = (inner.match(/\)/g) || []).length;
157
- if (openParens !== closeParens) {
158
- issues.push({
159
- issue: 'Unmatched parentheses',
160
- severity: 'error',
161
- });
162
- }
163
-
164
- // Check for unmatched brackets
165
- const openBrackets = (inner.match(/\[/g) || []).length;
166
- const closeBrackets = (inner.match(/\]/g) || []).length;
167
- if (openBrackets !== closeBrackets) {
168
- issues.push({
169
- issue: 'Unmatched brackets',
170
- severity: 'error',
171
- });
172
- }
173
-
174
- // Check for deprecated patterns
175
- if (/\$node\./.test(inner)) {
176
- issues.push({
177
- issue: '$node is deprecated',
178
- severity: 'warning',
179
- suggestion: "Use $('node_name') instead",
180
- });
181
- }
182
-
183
- // Check for potential undefined access
184
- if (/\.json\.[a-zA-Z_]+\.[a-zA-Z_]+/.test(inner) && !/\?\./g.test(inner)) {
185
- issues.push({
186
- issue: 'Deep property access without optional chaining',
187
- severity: 'info',
188
- suggestion: 'Consider using ?. for safer access: obj?.nested?.field',
189
- });
190
- }
191
-
192
- return issues;
193
- }
194
-
195
- /**
196
- * Get all referenced nodes from expressions
197
- */
198
- export function getReferencedNodes(workflow: N8nWorkflow): Map<string, string[]> {
199
- const references = new Map<string, string[]>();
200
-
201
- for (const node of workflow.nodes) {
202
- const expressions = extractExpressions(node.parameters);
203
- const refsForNode: string[] = [];
204
-
205
- for (const { expression } of expressions) {
206
- const matches = expression.matchAll(/\$\(['"]([^'"]+)['"]\)/g);
207
- for (const match of matches) {
208
- const refName = match[1];
209
- if (!refsForNode.includes(refName)) {
210
- refsForNode.push(refName);
211
- }
212
- }
213
- }
214
-
215
- if (refsForNode.length > 0) {
216
- references.set(node.name, refsForNode);
217
- }
218
- }
219
-
220
- return references;
221
- }
222
-
223
- /**
224
- * Check for circular references in expressions
225
- */
226
- export function checkCircularReferences(workflow: N8nWorkflow): string[][] {
227
- const references = getReferencedNodes(workflow);
228
- const cycles: string[][] = [];
229
-
230
- function findCycle(start: string, path: string[] = []): void {
231
- if (path.includes(start)) {
232
- const cycleStart = path.indexOf(start);
233
- cycles.push([...path.slice(cycleStart), start]);
234
- return;
235
- }
236
-
237
- const refs = references.get(start) || [];
238
- for (const ref of refs) {
239
- findCycle(ref, [...path, start]);
240
- }
241
- }
242
-
243
- for (const nodeName of references.keys()) {
244
- findCycle(nodeName);
245
- }
246
-
247
- // Deduplicate cycles
248
- const uniqueCycles = cycles.filter((cycle, index) => {
249
- const key = [...cycle].sort().join(',');
250
- return cycles.findIndex((c) => [...c].sort().join(',') === key) === index;
251
- });
252
-
253
- return uniqueCycles;
254
- }