@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.
- package/CHANGELOG.md +37 -0
- package/README.md +70 -60
- package/dist/index.js +122 -18
- package/dist/n8n-client.d.ts +3 -2
- package/dist/n8n-client.js +9 -1
- package/dist/n8n-client.test.js +111 -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 +27 -0
- 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 +80 -156
- package/docs/node-config.md +205 -0
- package/logo.png +0 -0
- package/package.json +1 -1
- package/plans/ai-guidelines.md +233 -0
- package/plans/architecture.md +220 -0
- package/src/index.ts +159 -20
- package/src/n8n-client.test.ts +135 -0
- package/src/n8n-client.ts +13 -2
- 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 +33 -0
- package/src/validators.test.ts +101 -4
- package/src/validators.ts +112 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2025-01-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
#### Node Type Discovery & Validation
|
|
10
|
+
- `node_types_list` - Search available node types by name/category
|
|
11
|
+
- Pre-validation blocks invalid node types before `workflow_create` and `workflow_update`
|
|
12
|
+
- Fuzzy matching suggests correct types when invalid types detected
|
|
13
|
+
|
|
14
|
+
#### Auto-Cleanup Pipeline
|
|
15
|
+
- Every `workflow_create` and `workflow_update` now automatically:
|
|
16
|
+
- Validates node types (blocks if invalid)
|
|
17
|
+
- Runs validation rules
|
|
18
|
+
- Auto-fixes fixable issues (snake_case, $json refs, AI settings)
|
|
19
|
+
- Formats workflow (sorts nodes, removes nulls)
|
|
20
|
+
- Returns only unfixable warnings
|
|
21
|
+
|
|
22
|
+
#### Response Formatting
|
|
23
|
+
- New `format` parameter on workflow/execution tools: `compact` (default), `summary`, `full`
|
|
24
|
+
- Token-efficient responses (88% reduction with compact, 98% with summary)
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Hardcoded secrets: severity `error` → `info` (recommend env vars, don't block)
|
|
28
|
+
- Documentation updated with "opinionated" messaging throughout
|
|
29
|
+
- Added PageLines logo to README
|
|
30
|
+
|
|
31
|
+
## [0.2.1] - 2025-01-13
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- [Node Config Guide](docs/node-config.md) - Human-editable node settings (`__rl` resource locator, Set node JSON mode, AI structured output)
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- Sharpened documentation (31% line reduction, higher data-ink ratio)
|
|
38
|
+
- README: Lead with differentiation, compact tool/validation tables
|
|
39
|
+
- Best Practices: Quick reference at top, focused on MCP-validated patterns
|
|
40
|
+
- Architecture: Technical reference, removed redundant philosophy sections
|
|
41
|
+
|
|
5
42
|
## [0.2.0] - 2025-01-12
|
|
6
43
|
|
|
7
44
|
### Added
|
package/README.md
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
|
+
<img src="logo.png" width="64" height="64" alt="PageLines">
|
|
2
|
+
|
|
1
3
|
# n8n MCP Server
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
**Opinionated** workflow automation for n8n. Enforces best practices, auto-fixes issues, and prevents mistakes.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
## Why This MCP?
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
**The problem:** Other n8n MCPs replace entire nodes when you update one field. Change a message? Lose your channel ID, auth settings, everything else. No undo. And they bundle 70MB of node docs you can just Google.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
**This MCP is opinionated:**
|
|
12
|
+
- **Patches, not replaces** - Update one field, keep everything else
|
|
13
|
+
- **Auto-cleanup** - Every create/update validates, auto-fixes, and formats automatically
|
|
14
|
+
- **Auto-snapshots** - Every mutation saves a version first. Always have rollback.
|
|
15
|
+
- **Node type validation** - Blocks invalid node types with suggestions before they hit n8n
|
|
16
|
+
- **Expression validation** - Catches `$json` refs that break on reorder, circular deps, missing nodes
|
|
17
|
+
- **Enforces conventions** - snake_case naming, explicit references, recommends env vars
|
|
18
|
+
- **Lightweight** - ~1,500 lines, zero runtime dependencies
|
|
19
|
+
|
|
20
|
+
| | This MCP | Others |
|
|
21
|
+
|--|----------|--------|
|
|
22
|
+
| Update a node | Preserves untouched params | Loses them |
|
|
23
|
+
| After create/update | Auto-validates, auto-fixes, formats | Manual cleanup |
|
|
24
|
+
| Invalid node types | Blocked with suggestions | API error |
|
|
25
|
+
| Before mutations | Auto-saves version | Hope you backed up |
|
|
26
|
+
| Expression validation | Syntax, refs, circular deps | Basic |
|
|
27
|
+
| Size | ~1,500 LOC | 10k+ LOC, 70MB SQLite |
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
13
30
|
|
|
14
|
-
Add to MCP config
|
|
31
|
+
Add to your MCP client config:
|
|
15
32
|
|
|
16
33
|
```json
|
|
17
34
|
{
|
|
@@ -28,58 +45,51 @@ Add to MCP config (`~/.claude/mcp.json`):
|
|
|
28
45
|
}
|
|
29
46
|
```
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
|
40
|
-
|
|
41
|
-
| `
|
|
42
|
-
|
|
|
43
|
-
| `
|
|
44
|
-
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
|
57
|
-
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
|
67
|
-
|
|
68
|
-
| `
|
|
69
|
-
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| Variable | Description |
|
|
78
|
-
|----------|-------------|
|
|
79
|
-
| `N8N_API_URL` | Your n8n instance URL |
|
|
80
|
-
| `N8N_API_KEY` | API key from n8n settings |
|
|
81
|
-
| `N8N_MCP_VERSIONS` | Enable version control (default: true) |
|
|
82
|
-
| `N8N_MCP_MAX_VERSIONS` | Max versions per workflow (default: 20) |
|
|
48
|
+
No install step needed - npx handles it.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Reference
|
|
53
|
+
|
|
54
|
+
### Tools
|
|
55
|
+
|
|
56
|
+
| Category | Tools |
|
|
57
|
+
|----------|-------|
|
|
58
|
+
| Workflow | `list` `get` `create` `update` `delete` `activate` `deactivate` `execute` |
|
|
59
|
+
| Execution | `list` `get` |
|
|
60
|
+
| Validation | `validate` `autofix` `format` |
|
|
61
|
+
| Discovery | `node_types_list` |
|
|
62
|
+
| Versions | `list` `get` `save` `rollback` `diff` `stats` |
|
|
63
|
+
|
|
64
|
+
### Opinions Enforced
|
|
65
|
+
|
|
66
|
+
These rules are checked and auto-fixed on every `workflow_create` and `workflow_update`:
|
|
67
|
+
|
|
68
|
+
| Rule | Severity | Auto-fix |
|
|
69
|
+
|------|----------|----------|
|
|
70
|
+
| snake_case naming | warning | Yes |
|
|
71
|
+
| Explicit refs (`$('node')` not `$json`) | warning | Yes |
|
|
72
|
+
| AI structured output settings | warning | Yes |
|
|
73
|
+
| Invalid node types | error | Blocked |
|
|
74
|
+
| Hardcoded secrets | info | No |
|
|
75
|
+
| Orphan nodes | warning | No |
|
|
76
|
+
| Expression syntax | error | No |
|
|
77
|
+
| Circular references | error | No |
|
|
78
|
+
|
|
79
|
+
### Config
|
|
80
|
+
|
|
81
|
+
| Variable | Default | Description |
|
|
82
|
+
|----------|---------|-------------|
|
|
83
|
+
| `N8N_API_URL` | required | n8n instance URL |
|
|
84
|
+
| `N8N_API_KEY` | required | API key |
|
|
85
|
+
| `N8N_MCP_VERSIONS` | `true` | Enable version control |
|
|
86
|
+
| `N8N_MCP_MAX_VERSIONS` | `20` | Max snapshots per workflow |
|
|
87
|
+
|
|
88
|
+
### Docs
|
|
89
|
+
|
|
90
|
+
- [Best Practices](docs/best-practices.md) - Expression patterns, config nodes, AI settings
|
|
91
|
+
- [Node Config](docs/node-config.md) - Human-editable node settings
|
|
92
|
+
- [Architecture](plans/architecture.md) - Technical reference
|
|
83
93
|
|
|
84
94
|
## License
|
|
85
95
|
|
package/dist/index.js
CHANGED
|
@@ -8,10 +8,11 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import { N8nClient } from './n8n-client.js';
|
|
10
10
|
import { tools } from './tools.js';
|
|
11
|
-
import { validateWorkflow } from './validators.js';
|
|
11
|
+
import { validateWorkflow, validateNodeTypes } from './validators.js';
|
|
12
12
|
import { validateExpressions, checkCircularReferences } from './expressions.js';
|
|
13
13
|
import { autofixWorkflow, formatWorkflow } from './autofix.js';
|
|
14
14
|
import { initVersionControl, saveVersion, listVersions, getVersion, diffWorkflows, getVersionStats, } from './versions.js';
|
|
15
|
+
import { formatWorkflowResponse, formatExecutionResponse, formatExecutionListResponse, stringifyResponse, } from './response-format.js';
|
|
15
16
|
// ─────────────────────────────────────────────────────────────
|
|
16
17
|
// Configuration
|
|
17
18
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -36,7 +37,7 @@ initVersionControl({
|
|
|
36
37
|
// ─────────────────────────────────────────────────────────────
|
|
37
38
|
const server = new Server({
|
|
38
39
|
name: '@pagelines/n8n-mcp',
|
|
39
|
-
version: '0.
|
|
40
|
+
version: '0.3.0',
|
|
40
41
|
}, {
|
|
41
42
|
capabilities: {
|
|
42
43
|
tools: {},
|
|
@@ -55,7 +56,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
55
56
|
content: [
|
|
56
57
|
{
|
|
57
58
|
type: 'text',
|
|
58
|
-
|
|
59
|
+
// Use minified JSON to reduce token usage
|
|
60
|
+
text: typeof result === 'string' ? result : stringifyResponse(result),
|
|
59
61
|
},
|
|
60
62
|
],
|
|
61
63
|
};
|
|
@@ -96,10 +98,27 @@ async function handleTool(name, args) {
|
|
|
96
98
|
}
|
|
97
99
|
case 'workflow_get': {
|
|
98
100
|
const workflow = await client.getWorkflow(args.id);
|
|
99
|
-
|
|
101
|
+
const format = args.format || 'compact';
|
|
102
|
+
return formatWorkflowResponse(workflow, format);
|
|
100
103
|
}
|
|
101
104
|
case 'workflow_create': {
|
|
102
|
-
const
|
|
105
|
+
const inputNodes = args.nodes;
|
|
106
|
+
// Validate node types BEFORE creating workflow
|
|
107
|
+
const availableTypes = await client.listNodeTypes();
|
|
108
|
+
const validTypeSet = new Set(availableTypes.map((nt) => nt.name));
|
|
109
|
+
const typeErrors = validateNodeTypes(inputNodes, validTypeSet);
|
|
110
|
+
if (typeErrors.length > 0) {
|
|
111
|
+
const errorMessages = typeErrors.map((e) => {
|
|
112
|
+
let msg = e.message;
|
|
113
|
+
if (e.suggestions && e.suggestions.length > 0) {
|
|
114
|
+
msg += `. Did you mean: ${e.suggestions.join(', ')}?`;
|
|
115
|
+
}
|
|
116
|
+
return msg;
|
|
117
|
+
});
|
|
118
|
+
throw new Error(`Invalid node types detected:\n${errorMessages.join('\n')}\n\n` +
|
|
119
|
+
`Use node_types_list to discover available node types.`);
|
|
120
|
+
}
|
|
121
|
+
const nodes = inputNodes.map((n, i) => ({
|
|
103
122
|
id: crypto.randomUUID(),
|
|
104
123
|
name: n.name,
|
|
105
124
|
type: n.type,
|
|
@@ -108,31 +127,78 @@ async function handleTool(name, args) {
|
|
|
108
127
|
parameters: n.parameters || {},
|
|
109
128
|
...(n.credentials && { credentials: n.credentials }),
|
|
110
129
|
}));
|
|
111
|
-
|
|
130
|
+
let workflow = await client.createWorkflow({
|
|
112
131
|
name: args.name,
|
|
113
132
|
nodes,
|
|
114
133
|
connections: args.connections || {},
|
|
115
134
|
settings: args.settings,
|
|
116
135
|
});
|
|
117
|
-
// Validate
|
|
136
|
+
// Validate and auto-cleanup
|
|
118
137
|
const validation = validateWorkflow(workflow);
|
|
138
|
+
const autofix = autofixWorkflow(workflow, validation.warnings);
|
|
139
|
+
let formatted = formatWorkflow(autofix.workflow);
|
|
140
|
+
// Apply cleanup if there were fixes or formatting changes
|
|
141
|
+
if (autofix.fixes.length > 0 || JSON.stringify(workflow) !== JSON.stringify(formatted)) {
|
|
142
|
+
workflow = await client.updateWorkflow(workflow.id, formatted);
|
|
143
|
+
formatted = workflow;
|
|
144
|
+
}
|
|
145
|
+
const format = args.format || 'compact';
|
|
119
146
|
return {
|
|
120
|
-
workflow,
|
|
121
|
-
validation
|
|
147
|
+
workflow: formatWorkflowResponse(formatted, format),
|
|
148
|
+
validation: {
|
|
149
|
+
...validation,
|
|
150
|
+
warnings: autofix.unfixable, // Only show unfixable warnings
|
|
151
|
+
},
|
|
152
|
+
autoFixed: autofix.fixes.length > 0 ? autofix.fixes : undefined,
|
|
122
153
|
};
|
|
123
154
|
}
|
|
124
155
|
case 'workflow_update': {
|
|
156
|
+
const operations = args.operations;
|
|
157
|
+
// Extract addNode operations that need validation
|
|
158
|
+
const addNodeOps = operations.filter((op) => op.type === 'addNode');
|
|
159
|
+
if (addNodeOps.length > 0) {
|
|
160
|
+
// Fetch available types and validate
|
|
161
|
+
const availableTypes = await client.listNodeTypes();
|
|
162
|
+
const validTypeSet = new Set(availableTypes.map((nt) => nt.name));
|
|
163
|
+
const nodesToValidate = addNodeOps.map((op) => ({
|
|
164
|
+
name: op.node.name,
|
|
165
|
+
type: op.node.type,
|
|
166
|
+
}));
|
|
167
|
+
const typeErrors = validateNodeTypes(nodesToValidate, validTypeSet);
|
|
168
|
+
if (typeErrors.length > 0) {
|
|
169
|
+
const errorMessages = typeErrors.map((e) => {
|
|
170
|
+
let msg = e.message;
|
|
171
|
+
if (e.suggestions && e.suggestions.length > 0) {
|
|
172
|
+
msg += `. Did you mean: ${e.suggestions.join(', ')}?`;
|
|
173
|
+
}
|
|
174
|
+
return msg;
|
|
175
|
+
});
|
|
176
|
+
throw new Error(`Invalid node types in addNode operations:\n${errorMessages.join('\n')}\n\n` +
|
|
177
|
+
`Use node_types_list to discover available node types.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
125
180
|
// Save version before updating
|
|
126
181
|
const currentWorkflow = await client.getWorkflow(args.id);
|
|
127
182
|
const versionSaved = await saveVersion(currentWorkflow, 'before_update');
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Also run validation
|
|
183
|
+
let { workflow, warnings } = await client.patchWorkflow(args.id, operations);
|
|
184
|
+
// Validate and auto-cleanup
|
|
131
185
|
const validation = validateWorkflow(workflow);
|
|
186
|
+
const autofix = autofixWorkflow(workflow, validation.warnings);
|
|
187
|
+
let formatted = formatWorkflow(autofix.workflow);
|
|
188
|
+
// Apply cleanup if there were fixes or formatting changes
|
|
189
|
+
if (autofix.fixes.length > 0 || JSON.stringify(workflow) !== JSON.stringify(formatted)) {
|
|
190
|
+
workflow = await client.updateWorkflow(args.id, formatted);
|
|
191
|
+
formatted = workflow;
|
|
192
|
+
}
|
|
193
|
+
const format = args.format || 'compact';
|
|
132
194
|
return {
|
|
133
|
-
workflow,
|
|
195
|
+
workflow: formatWorkflowResponse(formatted, format),
|
|
134
196
|
patchWarnings: warnings,
|
|
135
|
-
validation
|
|
197
|
+
validation: {
|
|
198
|
+
...validation,
|
|
199
|
+
warnings: autofix.unfixable, // Only show unfixable warnings
|
|
200
|
+
},
|
|
201
|
+
autoFixed: autofix.fixes.length > 0 ? autofix.fixes : undefined,
|
|
136
202
|
versionSaved: versionSaved ? versionSaved.id : null,
|
|
137
203
|
};
|
|
138
204
|
}
|
|
@@ -167,14 +233,16 @@ async function handleTool(name, args) {
|
|
|
167
233
|
status: args.status,
|
|
168
234
|
limit: args.limit || 20,
|
|
169
235
|
});
|
|
236
|
+
const format = args.format || 'compact';
|
|
170
237
|
return {
|
|
171
|
-
executions: response.data,
|
|
238
|
+
executions: formatExecutionListResponse(response.data, format),
|
|
172
239
|
total: response.data.length,
|
|
173
240
|
};
|
|
174
241
|
}
|
|
175
242
|
case 'execution_get': {
|
|
176
243
|
const execution = await client.getExecution(args.id);
|
|
177
|
-
|
|
244
|
+
const format = args.format || 'compact';
|
|
245
|
+
return formatExecutionResponse(execution, format);
|
|
178
246
|
}
|
|
179
247
|
// Validation & Quality
|
|
180
248
|
case 'workflow_validate': {
|
|
@@ -229,6 +297,37 @@ async function handleTool(name, args) {
|
|
|
229
297
|
previewWorkflow: formatted,
|
|
230
298
|
};
|
|
231
299
|
}
|
|
300
|
+
// Node Discovery
|
|
301
|
+
case 'node_types_list': {
|
|
302
|
+
const nodeTypes = await client.listNodeTypes();
|
|
303
|
+
const search = args.search?.toLowerCase();
|
|
304
|
+
const category = args.category;
|
|
305
|
+
const limit = args.limit || 50;
|
|
306
|
+
let results = nodeTypes.map((nt) => ({
|
|
307
|
+
type: nt.name,
|
|
308
|
+
name: nt.displayName,
|
|
309
|
+
description: nt.description,
|
|
310
|
+
category: nt.codex?.categories?.[0] || nt.group?.[0] || 'Other',
|
|
311
|
+
version: nt.version,
|
|
312
|
+
}));
|
|
313
|
+
// Apply search filter
|
|
314
|
+
if (search) {
|
|
315
|
+
results = results.filter((nt) => nt.type.toLowerCase().includes(search) ||
|
|
316
|
+
nt.name.toLowerCase().includes(search) ||
|
|
317
|
+
nt.description.toLowerCase().includes(search));
|
|
318
|
+
}
|
|
319
|
+
// Apply category filter
|
|
320
|
+
if (category) {
|
|
321
|
+
results = results.filter((nt) => nt.category.toLowerCase().includes(category.toLowerCase()));
|
|
322
|
+
}
|
|
323
|
+
// Apply limit
|
|
324
|
+
results = results.slice(0, limit);
|
|
325
|
+
return {
|
|
326
|
+
nodeTypes: results,
|
|
327
|
+
total: results.length,
|
|
328
|
+
hint: 'Use the "type" field value when creating nodes (e.g., "n8n-nodes-base.webhook")',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
232
331
|
// Version Control
|
|
233
332
|
case 'version_list': {
|
|
234
333
|
const versions = await listVersions(args.workflowId);
|
|
@@ -243,7 +342,11 @@ async function handleTool(name, args) {
|
|
|
243
342
|
if (!version) {
|
|
244
343
|
throw new Error(`Version ${args.versionId} not found`);
|
|
245
344
|
}
|
|
246
|
-
|
|
345
|
+
const format = args.format || 'compact';
|
|
346
|
+
return {
|
|
347
|
+
meta: version.meta,
|
|
348
|
+
workflow: formatWorkflowResponse(version.workflow, format),
|
|
349
|
+
};
|
|
247
350
|
}
|
|
248
351
|
case 'version_save': {
|
|
249
352
|
const workflow = await client.getWorkflow(args.workflowId);
|
|
@@ -263,10 +366,11 @@ async function handleTool(name, args) {
|
|
|
263
366
|
await saveVersion(currentWorkflow, 'before_rollback');
|
|
264
367
|
// Apply the old version
|
|
265
368
|
await client.updateWorkflow(args.workflowId, version.workflow);
|
|
369
|
+
const format = args.format || 'compact';
|
|
266
370
|
return {
|
|
267
371
|
success: true,
|
|
268
372
|
restoredVersion: version.meta,
|
|
269
|
-
workflow: version.workflow,
|
|
373
|
+
workflow: formatWorkflowResponse(version.workflow, format),
|
|
270
374
|
};
|
|
271
375
|
}
|
|
272
376
|
case 'version_diff': {
|
package/dist/n8n-client.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* n8n REST API Client
|
|
3
3
|
* Clean, minimal implementation with built-in safety checks
|
|
4
4
|
*/
|
|
5
|
-
import type { N8nWorkflow, N8nWorkflowListItem, N8nExecution, N8nExecutionListItem, N8nListResponse, N8nNode, PatchOperation } from './types.js';
|
|
5
|
+
import type { N8nWorkflow, N8nWorkflowListItem, N8nExecution, N8nExecutionListItem, N8nListResponse, N8nNode, N8nNodeType, PatchOperation } from './types.js';
|
|
6
6
|
export interface N8nClientConfig {
|
|
7
7
|
apiUrl: string;
|
|
8
8
|
apiKey: string;
|
|
@@ -25,7 +25,7 @@ export declare class N8nClient {
|
|
|
25
25
|
connections: N8nWorkflow['connections'];
|
|
26
26
|
settings?: Record<string, unknown>;
|
|
27
27
|
}): Promise<N8nWorkflow>;
|
|
28
|
-
updateWorkflow(id: string, workflow: Partial<
|
|
28
|
+
updateWorkflow(id: string, workflow: Partial<N8nWorkflow>): Promise<N8nWorkflow>;
|
|
29
29
|
deleteWorkflow(id: string): Promise<void>;
|
|
30
30
|
activateWorkflow(id: string): Promise<N8nWorkflow>;
|
|
31
31
|
deactivateWorkflow(id: string): Promise<N8nWorkflow>;
|
|
@@ -51,4 +51,5 @@ export declare class N8nClient {
|
|
|
51
51
|
version?: string;
|
|
52
52
|
error?: string;
|
|
53
53
|
}>;
|
|
54
|
+
listNodeTypes(): Promise<N8nNodeType[]>;
|
|
54
55
|
}
|
package/dist/n8n-client.js
CHANGED
|
@@ -56,7 +56,9 @@ export class N8nClient {
|
|
|
56
56
|
return this.request('POST', '/api/v1/workflows', workflow);
|
|
57
57
|
}
|
|
58
58
|
async updateWorkflow(id, workflow) {
|
|
59
|
-
|
|
59
|
+
// Strip properties that n8n API doesn't accept on PUT
|
|
60
|
+
const { id: _id, createdAt, updatedAt, active, versionId, ...allowed } = workflow;
|
|
61
|
+
return this.request('PUT', `/api/v1/workflows/${id}`, allowed);
|
|
60
62
|
}
|
|
61
63
|
async deleteWorkflow(id) {
|
|
62
64
|
await this.request('DELETE', `/api/v1/workflows/${id}`);
|
|
@@ -272,4 +274,10 @@ export class N8nClient {
|
|
|
272
274
|
};
|
|
273
275
|
}
|
|
274
276
|
}
|
|
277
|
+
// ─────────────────────────────────────────────────────────────
|
|
278
|
+
// Node Types
|
|
279
|
+
// ─────────────────────────────────────────────────────────────
|
|
280
|
+
async listNodeTypes() {
|
|
281
|
+
return this.request('GET', '/api/v1/nodes');
|
|
282
|
+
}
|
|
275
283
|
}
|
package/dist/n8n-client.test.js
CHANGED
|
@@ -181,4 +181,115 @@ describe('N8nClient', () => {
|
|
|
181
181
|
await expect(client.getWorkflow('999')).rejects.toThrow('n8n API error (404)');
|
|
182
182
|
});
|
|
183
183
|
});
|
|
184
|
+
describe('listNodeTypes', () => {
|
|
185
|
+
it('calls correct endpoint', async () => {
|
|
186
|
+
const mockNodeTypes = [
|
|
187
|
+
{
|
|
188
|
+
name: 'n8n-nodes-base.webhook',
|
|
189
|
+
displayName: 'Webhook',
|
|
190
|
+
description: 'Starts workflow on webhook call',
|
|
191
|
+
group: ['trigger'],
|
|
192
|
+
version: 2,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'n8n-nodes-base.set',
|
|
196
|
+
displayName: 'Set',
|
|
197
|
+
description: 'Set values',
|
|
198
|
+
group: ['transform'],
|
|
199
|
+
version: 3,
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
mockFetch.mockResolvedValueOnce({
|
|
203
|
+
ok: true,
|
|
204
|
+
text: async () => JSON.stringify(mockNodeTypes),
|
|
205
|
+
});
|
|
206
|
+
const result = await client.listNodeTypes();
|
|
207
|
+
expect(mockFetch).toHaveBeenCalledWith('https://n8n.example.com/api/v1/nodes', expect.objectContaining({
|
|
208
|
+
method: 'GET',
|
|
209
|
+
headers: expect.objectContaining({
|
|
210
|
+
'X-N8N-API-KEY': 'test-api-key',
|
|
211
|
+
}),
|
|
212
|
+
}));
|
|
213
|
+
expect(result).toHaveLength(2);
|
|
214
|
+
expect(result[0].name).toBe('n8n-nodes-base.webhook');
|
|
215
|
+
expect(result[1].name).toBe('n8n-nodes-base.set');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('updateWorkflow', () => {
|
|
219
|
+
it('strips disallowed properties before sending to API', async () => {
|
|
220
|
+
const fullWorkflow = {
|
|
221
|
+
id: '123',
|
|
222
|
+
name: 'test_workflow',
|
|
223
|
+
active: true,
|
|
224
|
+
nodes: [{ id: 'n1', name: 'node1', type: 'test', typeVersion: 1, position: [0, 0], parameters: {} }],
|
|
225
|
+
connections: {},
|
|
226
|
+
settings: { timezone: 'UTC' },
|
|
227
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
228
|
+
updatedAt: '2024-01-02T00:00:00.000Z',
|
|
229
|
+
versionId: 'v1',
|
|
230
|
+
staticData: undefined,
|
|
231
|
+
tags: [{ id: 't1', name: 'tag1' }],
|
|
232
|
+
};
|
|
233
|
+
mockFetch.mockResolvedValueOnce({
|
|
234
|
+
ok: true,
|
|
235
|
+
text: async () => JSON.stringify(fullWorkflow),
|
|
236
|
+
});
|
|
237
|
+
await client.updateWorkflow('123', fullWorkflow);
|
|
238
|
+
// Verify the request body does NOT contain disallowed properties
|
|
239
|
+
const putCall = mockFetch.mock.calls[0];
|
|
240
|
+
const putBody = JSON.parse(putCall[1].body);
|
|
241
|
+
// These should be stripped
|
|
242
|
+
expect(putBody.id).toBeUndefined();
|
|
243
|
+
expect(putBody.createdAt).toBeUndefined();
|
|
244
|
+
expect(putBody.updatedAt).toBeUndefined();
|
|
245
|
+
expect(putBody.active).toBeUndefined();
|
|
246
|
+
expect(putBody.versionId).toBeUndefined();
|
|
247
|
+
// These should be preserved
|
|
248
|
+
expect(putBody.name).toBe('test_workflow');
|
|
249
|
+
expect(putBody.nodes).toHaveLength(1);
|
|
250
|
+
expect(putBody.connections).toEqual({});
|
|
251
|
+
expect(putBody.settings).toEqual({ timezone: 'UTC' });
|
|
252
|
+
expect(putBody.staticData).toBeUndefined();
|
|
253
|
+
expect(putBody.tags).toEqual([{ id: 't1', name: 'tag1' }]);
|
|
254
|
+
});
|
|
255
|
+
it('works with partial workflow (only some fields)', async () => {
|
|
256
|
+
mockFetch.mockResolvedValueOnce({
|
|
257
|
+
ok: true,
|
|
258
|
+
text: async () => JSON.stringify({ id: '123', name: 'updated' }),
|
|
259
|
+
});
|
|
260
|
+
await client.updateWorkflow('123', { name: 'updated', nodes: [] });
|
|
261
|
+
const putCall = mockFetch.mock.calls[0];
|
|
262
|
+
const putBody = JSON.parse(putCall[1].body);
|
|
263
|
+
expect(putBody.name).toBe('updated');
|
|
264
|
+
expect(putBody.nodes).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
it('handles workflow from formatWorkflow (simulating workflow_format apply)', async () => {
|
|
267
|
+
// This simulates the exact scenario that caused the bug:
|
|
268
|
+
// workflow_format returns a full N8nWorkflow object with id, createdAt, etc.
|
|
269
|
+
const formattedWorkflow = {
|
|
270
|
+
id: 'zbB1fCxWgZXgpjB1',
|
|
271
|
+
name: 'my_workflow',
|
|
272
|
+
active: false,
|
|
273
|
+
nodes: [],
|
|
274
|
+
connections: {},
|
|
275
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
276
|
+
updatedAt: '2024-01-02T00:00:00.000Z',
|
|
277
|
+
};
|
|
278
|
+
mockFetch.mockResolvedValueOnce({
|
|
279
|
+
ok: true,
|
|
280
|
+
text: async () => JSON.stringify(formattedWorkflow),
|
|
281
|
+
});
|
|
282
|
+
// This should NOT throw "must NOT have additional properties"
|
|
283
|
+
await client.updateWorkflow('zbB1fCxWgZXgpjB1', formattedWorkflow);
|
|
284
|
+
const putCall = mockFetch.mock.calls[0];
|
|
285
|
+
const putBody = JSON.parse(putCall[1].body);
|
|
286
|
+
// Critical: these must NOT be in the request body
|
|
287
|
+
expect(putBody.id).toBeUndefined();
|
|
288
|
+
expect(putBody.createdAt).toBeUndefined();
|
|
289
|
+
expect(putBody.updatedAt).toBeUndefined();
|
|
290
|
+
expect(putBody.active).toBeUndefined();
|
|
291
|
+
// Only allowed properties should be sent
|
|
292
|
+
expect(Object.keys(putBody).sort()).toEqual(['connections', 'name', 'nodes']);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
184
295
|
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Format Transformers
|
|
3
|
+
* Reduces response size for MCP to prevent context overflow
|
|
4
|
+
*/
|
|
5
|
+
import type { N8nWorkflow, N8nExecution, N8nExecutionListItem } from './types.js';
|
|
6
|
+
export type ResponseFormat = 'full' | 'compact' | 'summary';
|
|
7
|
+
export interface WorkflowSummary {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
active: boolean;
|
|
11
|
+
nodeCount: number;
|
|
12
|
+
connectionCount: number;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
nodeTypes: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface WorkflowCompact {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
active: boolean;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
nodes: CompactNode[];
|
|
22
|
+
connections: Record<string, string[]>;
|
|
23
|
+
settings?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface CompactNode {
|
|
26
|
+
name: string;
|
|
27
|
+
type: string;
|
|
28
|
+
position: [number, number];
|
|
29
|
+
hasCredentials: boolean;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Format a workflow based on the requested format level
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatWorkflowResponse(workflow: N8nWorkflow, format?: ResponseFormat): N8nWorkflow | WorkflowCompact | WorkflowSummary;
|
|
36
|
+
export interface ExecutionSummary {
|
|
37
|
+
id: string;
|
|
38
|
+
workflowId: string;
|
|
39
|
+
status: string;
|
|
40
|
+
mode: string;
|
|
41
|
+
startedAt: string;
|
|
42
|
+
stoppedAt?: string;
|
|
43
|
+
durationMs?: number;
|
|
44
|
+
hasError: boolean;
|
|
45
|
+
errorMessage?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ExecutionCompact {
|
|
48
|
+
id: string;
|
|
49
|
+
workflowId: string;
|
|
50
|
+
status: string;
|
|
51
|
+
mode: string;
|
|
52
|
+
startedAt: string;
|
|
53
|
+
stoppedAt?: string;
|
|
54
|
+
finished: boolean;
|
|
55
|
+
error?: {
|
|
56
|
+
message: string;
|
|
57
|
+
};
|
|
58
|
+
nodeResults?: NodeResultSummary[];
|
|
59
|
+
}
|
|
60
|
+
export interface NodeResultSummary {
|
|
61
|
+
nodeName: string;
|
|
62
|
+
itemCount: number;
|
|
63
|
+
success: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Format an execution based on the requested format level
|
|
67
|
+
*/
|
|
68
|
+
export declare function formatExecutionResponse(execution: N8nExecution, format?: ResponseFormat): N8nExecution | ExecutionCompact | ExecutionSummary;
|
|
69
|
+
/**
|
|
70
|
+
* Format execution list items
|
|
71
|
+
*/
|
|
72
|
+
export declare function formatExecutionListResponse(executions: N8nExecutionListItem[], format?: ResponseFormat): N8nExecutionListItem[] | Array<{
|
|
73
|
+
id: string;
|
|
74
|
+
status: string;
|
|
75
|
+
startedAt: string;
|
|
76
|
+
}>;
|
|
77
|
+
/**
|
|
78
|
+
* Remove null/undefined values and empty objects to reduce size
|
|
79
|
+
*/
|
|
80
|
+
export declare function cleanResponse<T>(obj: T): T;
|
|
81
|
+
/**
|
|
82
|
+
* Stringify with optional minification
|
|
83
|
+
*/
|
|
84
|
+
export declare function stringifyResponse(obj: unknown, minify?: boolean): string;
|