@pagelines/n8n-mcp 0.2.0 → 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.
@@ -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 - use $env.VAR_NAME instead`,
110
- severity: 'error',
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
+ }
@@ -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('errors on hardcoded secrets', () => {
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: 'error',
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
+ });
@@ -1,107 +1,88 @@
1
1
  # n8n Best Practices
2
2
 
3
- > Enforced by `@pagelines/n8n-mcp`
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
4
 
5
- ## Guiding Principle
6
-
7
- **What is most stable and easiest to maintain?**
5
+ ## Quick Reference
8
6
 
9
- | Rule | Why |
10
- |------|-----|
11
- | Minimize nodes | Fewer failure points, easier debugging |
12
- | YAGNI | Build only what's needed now |
13
- | Explicit references | `$('node_name')` not `$json` - traceable, stable |
14
- | snake_case | `node_name` not `NodeName` - consistent, readable |
7
+ ```javascript
8
+ // Explicit reference (always use this)
9
+ {{ $('node_name').item.json.field }}
15
10
 
16
- ---
11
+ // Environment variable
12
+ {{ $env.API_KEY }}
17
13
 
18
- ## Naming Convention
14
+ // Config node reference
15
+ {{ $('config').item.json.setting }}
19
16
 
20
- **snake_case everywhere**
17
+ // Fallback
18
+ {{ $('source').item.json.text || 'default' }}
21
19
 
22
- ```
23
- Workflows: content_factory, publish_linkedin, upload_image
24
- Nodes: trigger_webhook, fetch_articles, check_approved
20
+ // Date
21
+ {{ $now.format('yyyy-MM-dd') }}
25
22
  ```
26
23
 
27
- Never `NodeName`. Always `node_name`.
24
+ ## The Rules
28
25
 
29
- ---
26
+ ### 1. snake_case (auto-fixed)
30
27
 
31
- ## Expression References
28
+ ```
29
+ Good: fetch_articles, check_approved, generate_content
30
+ Bad: FetchArticles, Check Approved, generate-content
31
+ ```
32
32
 
33
- **NEVER use `$json`. Always explicit node references.**
33
+ Why: Consistency, readability. **Auto-fixed:** renamed automatically with all references updated.
34
+
35
+ ### 2. Explicit References (auto-fixed)
34
36
 
35
37
  ```javascript
36
38
  // Bad - breaks when flow changes
37
39
  {{ $json.field }}
38
40
 
39
- // Good - traceable and debuggable
41
+ // Good - traceable, stable
40
42
  {{ $('node_name').item.json.field }}
41
-
42
- // Parallel branch (lookup nodes)
43
- {{ $('lookup_node').all().length > 0 }}
44
-
45
- // Environment variable
46
- {{ $env.API_KEY }}
47
-
48
- // Fallback pattern
49
- {{ $('source').item.json.text || $('source').item.json.media_url }}
50
43
  ```
51
44
 
52
- ---
45
+ Why: `$json` references "previous node" implicitly. Reorder nodes, it breaks. **Auto-fixed:** converted to explicit `$('prev_node')` references.
53
46
 
54
- ## Secrets and Configuration
47
+ ### 3. Config Node
55
48
 
56
- **Secrets in environment variables. Always.**
57
-
58
- ```javascript
59
- // Bad
60
- { "apiKey": "sk_live_abc123" }
61
-
62
- // Good
63
- {{ $env.API_KEY }}
64
- ```
65
-
66
- **For workflow-specific settings, use a config node:**
49
+ Single source for workflow settings:
67
50
 
68
51
  ```
69
52
  [trigger] → [config] → [rest of workflow]
70
53
  ```
71
54
 
72
- Config node uses JSON output mode:
55
+ Config node (JSON mode):
73
56
  ```javascript
74
57
  ={
75
- "channel_id": "{{ $json.body.channelId || '1234567890' }}",
58
+ "channel_id": "{{ $json.body.channelId || '123456' }}",
76
59
  "max_items": 10,
77
60
  "ai_model": "gpt-4.1-mini"
78
61
  }
79
62
  ```
80
63
 
81
- Then reference: `{{ $('config').item.json.channel_id }}`
82
-
83
- ---
64
+ Reference everywhere: `{{ $('config').item.json.channel_id }}`
84
65
 
85
- ## Workflow Editing Safety
66
+ Why: Change once, not in 5 nodes.
86
67
 
87
- ### The Golden Rule
68
+ ### 4. Secrets in Environment (recommended)
88
69
 
89
- **Never edit a workflow without explicit confirmation and backup.**
70
+ ```javascript
71
+ // Hardcoded (works, but less portable)
72
+ { "apiKey": "sk_live_abc123" }
90
73
 
91
- ### Pre-Edit Checklist
74
+ // Environment variable (recommended)
75
+ {{ $env.API_KEY }}
76
+ ```
92
77
 
93
- 1. **Confirm** - Get explicit user approval
94
- 2. **List versions** - Know your rollback point
95
- 3. **Read full state** - Understand current config
96
- 4. **Make targeted change** - Use patch operations only
97
- 5. **Verify** - Confirm expected state
78
+ Why: Env vars make workflows portable across environments and avoid committing secrets.
98
79
 
99
- ### Parameter Preservation
80
+ ## Parameter Preservation
100
81
 
101
- **CRITICAL:** Partial updates REPLACE the entire `parameters` object.
82
+ **Critical:** Partial updates REPLACE the entire `parameters` object.
102
83
 
103
84
  ```javascript
104
- // Bad: Only updates messageId, loses operation and labelIds
85
+ // Bad - loses operation and labelIds
105
86
  {
106
87
  "type": "updateNode",
107
88
  "nodeName": "archive_email",
@@ -112,7 +93,7 @@ Then reference: `{{ $('config').item.json.channel_id }}`
112
93
  }
113
94
  }
114
95
 
115
- // Good: Include ALL required parameters
96
+ // Good - include ALL parameters
116
97
  {
117
98
  "type": "updateNode",
118
99
  "nodeName": "archive_email",
@@ -120,122 +101,65 @@ Then reference: `{{ $('config').item.json.channel_id }}`
120
101
  "parameters": {
121
102
  "operation": "addLabels",
122
103
  "messageId": "={{ $json.message_id }}",
123
- "labelIds": ["Label_123", "Label_456"]
104
+ "labelIds": ["Label_123"]
124
105
  }
125
106
  }
126
107
  }
127
108
  ```
128
109
 
129
- ---
130
-
131
- ## Code Nodes
132
-
133
- **Code nodes are a last resort.** Exhaust built-in options first:
110
+ Before updating: read current state with `workflow_get`.
134
111
 
135
- | Need | Use Instead |
136
- |------|-------------|
137
- | Transform fields | Set node with expressions |
138
- | Filter items | Filter node or If/Switch |
139
- | Merge data | Merge node |
140
- | Loop processing | n8n processes arrays natively |
141
- | Date formatting | `{{ $now.format('yyyy-MM-dd') }}` |
112
+ ## AI Nodes
142
113
 
143
- **When code IS necessary:**
144
- - Re-establishing `pairedItem` after chain breaks
145
- - Complex conditional logic
146
- - API response parsing expressions can't handle
114
+ ### Structured Output (auto-fixed)
147
115
 
148
- **Code node rules:**
149
- - Single responsibility (one clear purpose)
150
- - Name it for what it does: `merge_context`, `parse_response`
151
- - No side effects - pure data transformation
116
+ Always set for predictable JSON. **Auto-fixed:** `promptType: "define"` and `hasOutputParser: true` added automatically.
152
117
 
153
- ---
118
+ | Setting | Value |
119
+ |---------|-------|
120
+ | `promptType` | `"define"` |
121
+ | `hasOutputParser` | `true` |
122
+ | `schemaType` | `"manual"` (for nullable fields) |
154
123
 
155
- ## AI Agent Best Practices
156
-
157
- ### Structured Output
158
-
159
- **Always enable "Require Specific Output Format"** for reliable JSON:
160
-
161
- ```javascript
162
- {
163
- "promptType": "define",
164
- "hasOutputParser": true,
165
- "schemaType": "manual" // Required for nullable fields
166
- }
167
- ```
168
-
169
- Without these, AI outputs are unpredictable.
170
-
171
- ### Memory Storage
172
-
173
- **Never use in-memory storage in production:**
124
+ ### Memory
174
125
 
175
126
  | Don't Use | Use Instead |
176
127
  |-----------|-------------|
177
128
  | Windowed Buffer Memory | Postgres Chat Memory |
178
129
  | In-Memory Vector Store | Postgres pgvector |
179
130
 
180
- In-memory dies with restart and doesn't scale.
181
-
182
- ---
183
-
184
- ## Architecture Patterns
185
-
186
- ### Single Responsibility
187
-
188
- Don't build monolith workflows:
189
-
190
- ```
191
- Bad: One workflow doing signup → email → CRM → calendar → reports
192
-
193
- Good: Five focused workflows that communicate via webhooks
194
- ```
195
-
196
- ### Switch > If Node
197
-
198
- Always use Switch instead of If:
199
- - Named outputs (not just true/false)
200
- - Unlimited conditional branches
201
- - Send to all matching option
131
+ In-memory dies on restart, doesn't scale.
202
132
 
203
- ---
133
+ ## Code Nodes: Last Resort
204
134
 
205
- ## Validation Rules Enforced
206
-
207
- | Rule | Severity | Description |
208
- |------|----------|-------------|
209
- | `snake_case` | warning | Names should be snake_case |
210
- | `explicit_reference` | warning | Use `$('node')` not `$json` |
211
- | `no_hardcoded_ids` | info | Avoid hardcoded IDs |
212
- | `no_hardcoded_secrets` | error | Never hardcode secrets |
213
- | `orphan_node` | warning | Node has no connections |
214
- | `parameter_preservation` | error | Update would remove parameters |
215
- | `code_node_usage` | info | Code node detected |
216
- | `ai_structured_output` | warning | AI node missing structured output |
217
- | `in_memory_storage` | warning | Using non-persistent storage |
218
-
219
- ---
220
-
221
- ## Quick Reference
135
+ | Need | Use Instead |
136
+ |------|-------------|
137
+ | Transform fields | Set node with expressions |
138
+ | Filter items | Filter node or Switch |
139
+ | Merge data | Merge node |
140
+ | Loop | n8n processes arrays natively |
141
+ | Date formatting | `{{ $now.format('yyyy-MM-dd') }}` |
222
142
 
223
- ```javascript
224
- // Explicit reference
225
- {{ $('node_name').item.json.field }}
143
+ When code IS necessary:
144
+ - Re-establishing `pairedItem` after chain breaks
145
+ - Complex conditional logic
146
+ - API parsing expressions can't handle
226
147
 
227
- // Environment variable
228
- {{ $env.VAR_NAME }}
148
+ ## Pre-Edit Checklist
229
149
 
230
- // Config node reference
231
- {{ $('config').item.json.setting }}
150
+ | Step | Why |
151
+ |------|-----|
152
+ | 1. Get explicit user approval | Don't surprise |
153
+ | 2. List versions | Know rollback point |
154
+ | 3. Read full workflow | Understand current state |
155
+ | 4. Make targeted change | Minimal surface area |
232
156
 
233
- // Parallel branch query
234
- {{ $('lookup').all().some(i => i.json.id === $json.id) }}
157
+ > **Note:** Validation and cleanup are now automatic. Every create/update validates, auto-fixes, and formats automatically.
235
158
 
236
- // Date formatting
237
- {{ $now.format('yyyy-MM-dd') }}
159
+ ## Node-Specific Settings
238
160
 
239
- // Fallback
240
- {{ $json.text || $json.description || 'default' }}
241
- ```
161
+ See [Node Config](node-config.md) for:
162
+ - Resource locator (`__rl`) format with `cachedResultName`
163
+ - Google Sheets, Gmail, Discord required fields
164
+ - Set node JSON mode vs manual mapping
165
+ - Error handling options