@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 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
- > Version control, validation, and patch-based updates for n8n workflows.
5
+ **Opinionated** workflow automation for n8n. Enforces best practices, auto-fixes issues, and prevents mistakes.
4
6
 
5
- [![npm version](https://img.shields.io/npm/v/@pagelines/n8n-mcp.svg)](https://www.npmjs.com/package/@pagelines/n8n-mcp)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+ ## Why This MCP?
7
8
 
8
- ## Install
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
- ```bash
11
- npx @pagelines/n8n-mcp
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 (`~/.claude/mcp.json`):
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
- ## Tools
32
-
33
- ### Workflow Operations
34
-
35
- | Tool | Description |
36
- |------|-------------|
37
- | `workflow_list` | List all workflows |
38
- | `workflow_get` | Get workflow by ID |
39
- | `workflow_create` | Create new workflow |
40
- | `workflow_update` | Patch-based updates (preserves parameters) |
41
- | `workflow_delete` | Delete workflow |
42
- | `workflow_activate` | Enable triggers |
43
- | `workflow_deactivate` | Disable triggers |
44
- | `workflow_execute` | Execute via webhook |
45
-
46
- ### Quality & Validation
47
-
48
- | Tool | Description |
49
- |------|-------------|
50
- | `workflow_validate` | Check best practices, expressions, circular refs |
51
- | `workflow_autofix` | Auto-fix snake_case, explicit refs, AI settings |
52
- | `workflow_format` | Sort nodes, clean nulls |
53
-
54
- ### Version Control
55
-
56
- | Tool | Description |
57
- |------|-------------|
58
- | `version_list` | List saved versions |
59
- | `version_get` | Get specific version |
60
- | `version_save` | Manual snapshot |
61
- | `version_rollback` | Restore previous version |
62
- | `version_diff` | Compare versions |
63
-
64
- ## Validation Rules
65
-
66
- | Rule | Severity |
67
- |------|----------|
68
- | `snake_case` naming | warning |
69
- | Explicit refs (`$('node')` not `$json`) | warning |
70
- | No hardcoded secrets | error |
71
- | No orphan nodes | warning |
72
- | AI structured output | warning |
73
- | Expression syntax | error |
74
-
75
- ## Environment Variables
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.1.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
- text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
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
- return workflow;
101
+ const format = args.format || 'compact';
102
+ return formatWorkflowResponse(workflow, format);
100
103
  }
101
104
  case 'workflow_create': {
102
- const nodes = args.nodes.map((n, i) => ({
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
- const workflow = await client.createWorkflow({
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 the new workflow
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
- const operations = args.operations;
129
- const { workflow, warnings } = await client.patchWorkflow(args.id, operations);
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
- return execution;
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
- return version;
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': {
@@ -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<Omit<N8nWorkflow, 'id' | 'createdAt' | 'updatedAt'>>): Promise<N8nWorkflow>;
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
  }
@@ -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
- return this.request('PUT', `/api/v1/workflows/${id}`, workflow);
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
  }
@@ -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;