@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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Format Transformers
|
|
3
|
+
* Reduces response size for MCP to prevent context overflow
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Format a workflow based on the requested format level
|
|
7
|
+
*/
|
|
8
|
+
export function formatWorkflowResponse(workflow, format = 'compact') {
|
|
9
|
+
switch (format) {
|
|
10
|
+
case 'summary':
|
|
11
|
+
return toWorkflowSummary(workflow);
|
|
12
|
+
case 'compact':
|
|
13
|
+
return toWorkflowCompact(workflow);
|
|
14
|
+
case 'full':
|
|
15
|
+
default:
|
|
16
|
+
return workflow;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function toWorkflowSummary(workflow) {
|
|
20
|
+
const connectionCount = Object.values(workflow.connections).reduce((acc, outputs) => {
|
|
21
|
+
return acc + Object.values(outputs).reduce((a, conns) => {
|
|
22
|
+
return a + conns.reduce((b, c) => b + c.length, 0);
|
|
23
|
+
}, 0);
|
|
24
|
+
}, 0);
|
|
25
|
+
return {
|
|
26
|
+
id: workflow.id,
|
|
27
|
+
name: workflow.name,
|
|
28
|
+
active: workflow.active,
|
|
29
|
+
nodeCount: workflow.nodes.length,
|
|
30
|
+
connectionCount,
|
|
31
|
+
updatedAt: workflow.updatedAt,
|
|
32
|
+
nodeTypes: [...new Set(workflow.nodes.map((n) => n.type))],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function toWorkflowCompact(workflow) {
|
|
36
|
+
// Simplify connections to just show flow: nodeName -> [targetNodeNames]
|
|
37
|
+
const simpleConnections = {};
|
|
38
|
+
for (const [nodeName, outputs] of Object.entries(workflow.connections)) {
|
|
39
|
+
const targets = [];
|
|
40
|
+
for (const conns of Object.values(outputs)) {
|
|
41
|
+
for (const connList of conns) {
|
|
42
|
+
for (const conn of connList) {
|
|
43
|
+
if (!targets.includes(conn.node)) {
|
|
44
|
+
targets.push(conn.node);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (targets.length > 0) {
|
|
50
|
+
simpleConnections[nodeName] = targets;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
id: workflow.id,
|
|
55
|
+
name: workflow.name,
|
|
56
|
+
active: workflow.active,
|
|
57
|
+
updatedAt: workflow.updatedAt,
|
|
58
|
+
nodes: workflow.nodes.map(toCompactNode),
|
|
59
|
+
connections: simpleConnections,
|
|
60
|
+
...(workflow.settings && Object.keys(workflow.settings).length > 0 && { settings: workflow.settings }),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function toCompactNode(node) {
|
|
64
|
+
return {
|
|
65
|
+
name: node.name,
|
|
66
|
+
type: node.type,
|
|
67
|
+
position: node.position,
|
|
68
|
+
hasCredentials: !!node.credentials && Object.keys(node.credentials).length > 0,
|
|
69
|
+
...(node.disabled && { disabled: true }),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Format an execution based on the requested format level
|
|
74
|
+
*/
|
|
75
|
+
export function formatExecutionResponse(execution, format = 'compact') {
|
|
76
|
+
switch (format) {
|
|
77
|
+
case 'summary':
|
|
78
|
+
return toExecutionSummary(execution);
|
|
79
|
+
case 'compact':
|
|
80
|
+
return toExecutionCompact(execution);
|
|
81
|
+
case 'full':
|
|
82
|
+
default:
|
|
83
|
+
return execution;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function toExecutionSummary(execution) {
|
|
87
|
+
const durationMs = execution.stoppedAt && execution.startedAt
|
|
88
|
+
? new Date(execution.stoppedAt).getTime() - new Date(execution.startedAt).getTime()
|
|
89
|
+
: undefined;
|
|
90
|
+
return {
|
|
91
|
+
id: execution.id,
|
|
92
|
+
workflowId: execution.workflowId,
|
|
93
|
+
status: execution.status,
|
|
94
|
+
mode: execution.mode,
|
|
95
|
+
startedAt: execution.startedAt,
|
|
96
|
+
stoppedAt: execution.stoppedAt,
|
|
97
|
+
durationMs,
|
|
98
|
+
hasError: execution.status === 'error',
|
|
99
|
+
errorMessage: execution.data?.resultData?.error?.message,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function toExecutionCompact(execution) {
|
|
103
|
+
const nodeResults = [];
|
|
104
|
+
if (execution.data?.resultData?.runData) {
|
|
105
|
+
for (const [nodeName, runs] of Object.entries(execution.data.resultData.runData)) {
|
|
106
|
+
// runData is Record<string, unknown>, so we need to handle it carefully
|
|
107
|
+
const runArray = runs;
|
|
108
|
+
if (Array.isArray(runArray) && runArray.length > 0) {
|
|
109
|
+
const lastRun = runArray[runArray.length - 1];
|
|
110
|
+
const itemCount = lastRun?.data?.main?.[0]?.length ?? 0;
|
|
111
|
+
nodeResults.push({
|
|
112
|
+
nodeName,
|
|
113
|
+
itemCount,
|
|
114
|
+
success: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
id: execution.id,
|
|
121
|
+
workflowId: execution.workflowId,
|
|
122
|
+
status: execution.status,
|
|
123
|
+
mode: execution.mode,
|
|
124
|
+
startedAt: execution.startedAt,
|
|
125
|
+
stoppedAt: execution.stoppedAt,
|
|
126
|
+
finished: execution.finished,
|
|
127
|
+
...(execution.data?.resultData?.error && { error: { message: execution.data.resultData.error.message } }),
|
|
128
|
+
...(nodeResults.length > 0 && { nodeResults }),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Format execution list items
|
|
133
|
+
*/
|
|
134
|
+
export function formatExecutionListResponse(executions, format = 'compact') {
|
|
135
|
+
if (format === 'summary') {
|
|
136
|
+
return executions.map((e) => ({
|
|
137
|
+
id: e.id,
|
|
138
|
+
status: e.status,
|
|
139
|
+
startedAt: e.startedAt,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
// compact and full are the same for list items (already minimal)
|
|
143
|
+
return executions;
|
|
144
|
+
}
|
|
145
|
+
// ─────────────────────────────────────────────────────────────
|
|
146
|
+
// Generic Response Utilities
|
|
147
|
+
// ─────────────────────────────────────────────────────────────
|
|
148
|
+
/**
|
|
149
|
+
* Remove null/undefined values and empty objects to reduce size
|
|
150
|
+
*/
|
|
151
|
+
export function cleanResponse(obj) {
|
|
152
|
+
if (obj === null || obj === undefined) {
|
|
153
|
+
return obj;
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(obj)) {
|
|
156
|
+
return obj.map(cleanResponse);
|
|
157
|
+
}
|
|
158
|
+
if (typeof obj === 'object') {
|
|
159
|
+
const cleaned = {};
|
|
160
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
161
|
+
if (value !== null && value !== undefined) {
|
|
162
|
+
const cleanedValue = cleanResponse(value);
|
|
163
|
+
// Skip empty objects and arrays
|
|
164
|
+
if (typeof cleanedValue === 'object' && cleanedValue !== null) {
|
|
165
|
+
if (Array.isArray(cleanedValue) && cleanedValue.length === 0)
|
|
166
|
+
continue;
|
|
167
|
+
if (!Array.isArray(cleanedValue) && Object.keys(cleanedValue).length === 0)
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
cleaned[key] = cleanedValue;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return cleaned;
|
|
174
|
+
}
|
|
175
|
+
return obj;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Stringify with optional minification
|
|
179
|
+
*/
|
|
180
|
+
export function stringifyResponse(obj, minify = true) {
|
|
181
|
+
const cleaned = cleanResponse(obj);
|
|
182
|
+
return minify ? JSON.stringify(cleaned) : JSON.stringify(cleaned, null, 2);
|
|
183
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { formatWorkflowResponse, formatExecutionResponse, formatExecutionListResponse, cleanResponse, stringifyResponse, } from './response-format.js';
|
|
3
|
+
const createWorkflow = (overrides = {}) => ({
|
|
4
|
+
id: '1',
|
|
5
|
+
name: 'test_workflow',
|
|
6
|
+
active: false,
|
|
7
|
+
nodes: [
|
|
8
|
+
{
|
|
9
|
+
id: 'node1',
|
|
10
|
+
name: 'webhook_trigger',
|
|
11
|
+
type: 'n8n-nodes-base.webhook',
|
|
12
|
+
typeVersion: 1,
|
|
13
|
+
position: [0, 0],
|
|
14
|
+
parameters: { path: 'test', httpMethod: 'POST' },
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'node2',
|
|
18
|
+
name: 'set_data',
|
|
19
|
+
type: 'n8n-nodes-base.set',
|
|
20
|
+
typeVersion: 1,
|
|
21
|
+
position: [200, 0],
|
|
22
|
+
parameters: { values: { string: [{ name: 'key', value: 'value' }] } },
|
|
23
|
+
credentials: { httpBasicAuth: { id: '1', name: 'My Auth' } },
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
connections: {
|
|
27
|
+
webhook_trigger: {
|
|
28
|
+
main: [[{ node: 'set_data', type: 'main', index: 0 }]],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
32
|
+
updatedAt: '2024-01-02T00:00:00.000Z',
|
|
33
|
+
...overrides,
|
|
34
|
+
});
|
|
35
|
+
const createExecution = (overrides = {}) => ({
|
|
36
|
+
id: 'exec1',
|
|
37
|
+
workflowId: '1',
|
|
38
|
+
finished: true,
|
|
39
|
+
mode: 'manual',
|
|
40
|
+
startedAt: '2024-01-01T00:00:00.000Z',
|
|
41
|
+
stoppedAt: '2024-01-01T00:00:05.000Z',
|
|
42
|
+
status: 'success',
|
|
43
|
+
data: {
|
|
44
|
+
resultData: {
|
|
45
|
+
runData: {
|
|
46
|
+
webhook_trigger: [{ data: { main: [[{ json: { test: 'data' } }]] } }],
|
|
47
|
+
set_data: [{ data: { main: [[{ json: { key: 'value' } }]] } }],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
...overrides,
|
|
52
|
+
});
|
|
53
|
+
describe('formatWorkflowResponse', () => {
|
|
54
|
+
describe('summary format', () => {
|
|
55
|
+
it('returns minimal workflow info', () => {
|
|
56
|
+
const workflow = createWorkflow();
|
|
57
|
+
const result = formatWorkflowResponse(workflow, 'summary');
|
|
58
|
+
expect(result.id).toBe('1');
|
|
59
|
+
expect(result.name).toBe('test_workflow');
|
|
60
|
+
expect(result.active).toBe(false);
|
|
61
|
+
expect(result.nodeCount).toBe(2);
|
|
62
|
+
expect(result.connectionCount).toBe(1);
|
|
63
|
+
expect(result.updatedAt).toBe('2024-01-02T00:00:00.000Z');
|
|
64
|
+
expect(result.nodeTypes).toContain('n8n-nodes-base.webhook');
|
|
65
|
+
expect(result.nodeTypes).toContain('n8n-nodes-base.set');
|
|
66
|
+
// Should not have full nodes or connections
|
|
67
|
+
expect(result.nodes).toBeUndefined();
|
|
68
|
+
expect(result.connections).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('compact format', () => {
|
|
72
|
+
it('returns nodes without parameters', () => {
|
|
73
|
+
const workflow = createWorkflow();
|
|
74
|
+
const result = formatWorkflowResponse(workflow, 'compact');
|
|
75
|
+
expect(result.id).toBe('1');
|
|
76
|
+
expect(result.name).toBe('test_workflow');
|
|
77
|
+
expect(result.nodes).toHaveLength(2);
|
|
78
|
+
// Nodes should have name, type, position but no parameters
|
|
79
|
+
expect(result.nodes[0].name).toBe('webhook_trigger');
|
|
80
|
+
expect(result.nodes[0].type).toBe('n8n-nodes-base.webhook');
|
|
81
|
+
expect(result.nodes[0].position).toEqual([0, 0]);
|
|
82
|
+
expect(result.nodes[0].hasCredentials).toBe(false);
|
|
83
|
+
expect(result.nodes[0].parameters).toBeUndefined();
|
|
84
|
+
// Second node has credentials
|
|
85
|
+
expect(result.nodes[1].hasCredentials).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
it('simplifies connections to node -> [targets] map', () => {
|
|
88
|
+
const workflow = createWorkflow();
|
|
89
|
+
const result = formatWorkflowResponse(workflow, 'compact');
|
|
90
|
+
expect(result.connections).toEqual({
|
|
91
|
+
webhook_trigger: ['set_data'],
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
it('marks disabled nodes', () => {
|
|
95
|
+
const workflow = createWorkflow({
|
|
96
|
+
nodes: [
|
|
97
|
+
{
|
|
98
|
+
id: 'node1',
|
|
99
|
+
name: 'disabled_node',
|
|
100
|
+
type: 'n8n-nodes-base.set',
|
|
101
|
+
typeVersion: 1,
|
|
102
|
+
position: [0, 0],
|
|
103
|
+
parameters: {},
|
|
104
|
+
disabled: true,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
const result = formatWorkflowResponse(workflow, 'compact');
|
|
109
|
+
expect(result.nodes[0].disabled).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('full format', () => {
|
|
113
|
+
it('returns complete workflow', () => {
|
|
114
|
+
const workflow = createWorkflow();
|
|
115
|
+
const result = formatWorkflowResponse(workflow, 'full');
|
|
116
|
+
expect(result).toEqual(workflow);
|
|
117
|
+
expect(result.nodes[0].parameters).toEqual({ path: 'test', httpMethod: 'POST' });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('default format', () => {
|
|
121
|
+
it('defaults to compact', () => {
|
|
122
|
+
const workflow = createWorkflow();
|
|
123
|
+
const result = formatWorkflowResponse(workflow);
|
|
124
|
+
// Should be compact (no parameters)
|
|
125
|
+
expect(result.nodes[0].parameters).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe('formatExecutionResponse', () => {
|
|
130
|
+
describe('summary format', () => {
|
|
131
|
+
it('returns minimal execution info', () => {
|
|
132
|
+
const execution = createExecution();
|
|
133
|
+
const result = formatExecutionResponse(execution, 'summary');
|
|
134
|
+
expect(result.id).toBe('exec1');
|
|
135
|
+
expect(result.workflowId).toBe('1');
|
|
136
|
+
expect(result.status).toBe('success');
|
|
137
|
+
expect(result.mode).toBe('manual');
|
|
138
|
+
expect(result.durationMs).toBe(5000);
|
|
139
|
+
expect(result.hasError).toBe(false);
|
|
140
|
+
// Should not have runData
|
|
141
|
+
expect(result.data).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
it('includes error message when present', () => {
|
|
144
|
+
const execution = createExecution({
|
|
145
|
+
status: 'error',
|
|
146
|
+
data: {
|
|
147
|
+
resultData: {
|
|
148
|
+
error: { message: 'Something went wrong' },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
const result = formatExecutionResponse(execution, 'summary');
|
|
153
|
+
expect(result.hasError).toBe(true);
|
|
154
|
+
expect(result.errorMessage).toBe('Something went wrong');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('compact format', () => {
|
|
158
|
+
it('returns execution without runData but with node summaries', () => {
|
|
159
|
+
const execution = createExecution();
|
|
160
|
+
const result = formatExecutionResponse(execution, 'compact');
|
|
161
|
+
expect(result.id).toBe('exec1');
|
|
162
|
+
expect(result.status).toBe('success');
|
|
163
|
+
expect(result.finished).toBe(true);
|
|
164
|
+
// Should have node result summaries
|
|
165
|
+
expect(result.nodeResults).toHaveLength(2);
|
|
166
|
+
expect(result.nodeResults[0].nodeName).toBe('webhook_trigger');
|
|
167
|
+
expect(result.nodeResults[0].itemCount).toBe(1);
|
|
168
|
+
// Should not have full runData
|
|
169
|
+
expect(result.data).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
it('includes error in compact format', () => {
|
|
172
|
+
const execution = createExecution({
|
|
173
|
+
status: 'error',
|
|
174
|
+
data: {
|
|
175
|
+
resultData: {
|
|
176
|
+
error: { message: 'Failed' },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const result = formatExecutionResponse(execution, 'compact');
|
|
181
|
+
expect(result.error).toEqual({ message: 'Failed' });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe('full format', () => {
|
|
185
|
+
it('returns complete execution with runData', () => {
|
|
186
|
+
const execution = createExecution();
|
|
187
|
+
const result = formatExecutionResponse(execution, 'full');
|
|
188
|
+
expect(result).toEqual(execution);
|
|
189
|
+
expect(result.data?.resultData?.runData).toBeDefined();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe('formatExecutionListResponse', () => {
|
|
194
|
+
const executions = [
|
|
195
|
+
{ id: '1', workflowId: 'w1', status: 'success', startedAt: '2024-01-01', mode: 'manual' },
|
|
196
|
+
{ id: '2', workflowId: 'w1', status: 'error', startedAt: '2024-01-02', mode: 'webhook' },
|
|
197
|
+
];
|
|
198
|
+
it('summary returns id, status, startedAt only', () => {
|
|
199
|
+
const result = formatExecutionListResponse(executions, 'summary');
|
|
200
|
+
expect(result).toHaveLength(2);
|
|
201
|
+
expect(result[0]).toEqual({ id: '1', status: 'success', startedAt: '2024-01-01' });
|
|
202
|
+
expect(result[0].workflowId).toBeUndefined();
|
|
203
|
+
expect(result[0].mode).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
it('compact and full return same as input', () => {
|
|
206
|
+
const compactResult = formatExecutionListResponse(executions, 'compact');
|
|
207
|
+
const fullResult = formatExecutionListResponse(executions, 'full');
|
|
208
|
+
expect(compactResult).toEqual(executions);
|
|
209
|
+
expect(fullResult).toEqual(executions);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe('cleanResponse', () => {
|
|
213
|
+
it('removes null values', () => {
|
|
214
|
+
const obj = { a: 1, b: null, c: 'test' };
|
|
215
|
+
const result = cleanResponse(obj);
|
|
216
|
+
expect(result).toEqual({ a: 1, c: 'test' });
|
|
217
|
+
});
|
|
218
|
+
it('removes undefined values', () => {
|
|
219
|
+
const obj = { a: 1, b: undefined, c: 'test' };
|
|
220
|
+
const result = cleanResponse(obj);
|
|
221
|
+
expect(result).toEqual({ a: 1, c: 'test' });
|
|
222
|
+
});
|
|
223
|
+
it('removes empty objects', () => {
|
|
224
|
+
const obj = { a: 1, b: {}, c: 'test' };
|
|
225
|
+
const result = cleanResponse(obj);
|
|
226
|
+
expect(result).toEqual({ a: 1, c: 'test' });
|
|
227
|
+
});
|
|
228
|
+
it('removes empty arrays', () => {
|
|
229
|
+
const obj = { a: 1, b: [], c: 'test' };
|
|
230
|
+
const result = cleanResponse(obj);
|
|
231
|
+
expect(result).toEqual({ a: 1, c: 'test' });
|
|
232
|
+
});
|
|
233
|
+
it('handles nested objects', () => {
|
|
234
|
+
const obj = { a: { b: null, c: 1 }, d: { e: {} } };
|
|
235
|
+
const result = cleanResponse(obj);
|
|
236
|
+
expect(result).toEqual({ a: { c: 1 } });
|
|
237
|
+
});
|
|
238
|
+
it('handles arrays', () => {
|
|
239
|
+
const arr = [{ a: 1, b: null }, { c: 2 }];
|
|
240
|
+
const result = cleanResponse(arr);
|
|
241
|
+
expect(result).toEqual([{ a: 1 }, { c: 2 }]);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('stringifyResponse', () => {
|
|
245
|
+
it('minifies by default', () => {
|
|
246
|
+
const obj = { a: 1, b: 'test' };
|
|
247
|
+
const result = stringifyResponse(obj);
|
|
248
|
+
expect(result).toBe('{"a":1,"b":"test"}');
|
|
249
|
+
expect(result).not.toContain('\n');
|
|
250
|
+
});
|
|
251
|
+
it('can pretty print when minify=false', () => {
|
|
252
|
+
const obj = { a: 1 };
|
|
253
|
+
const result = stringifyResponse(obj, false);
|
|
254
|
+
expect(result).toContain('\n');
|
|
255
|
+
});
|
|
256
|
+
it('cleans null values before stringifying', () => {
|
|
257
|
+
const obj = { a: 1, b: null };
|
|
258
|
+
const result = stringifyResponse(obj);
|
|
259
|
+
expect(result).toBe('{"a":1}');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
describe('token reduction estimates', () => {
|
|
263
|
+
it('compact format significantly reduces workflow size', () => {
|
|
264
|
+
const workflow = createWorkflow({
|
|
265
|
+
nodes: Array.from({ length: 10 }, (_, i) => ({
|
|
266
|
+
id: `node${i}`,
|
|
267
|
+
name: `node_${i}`,
|
|
268
|
+
type: 'n8n-nodes-base.set',
|
|
269
|
+
typeVersion: 1,
|
|
270
|
+
position: [i * 200, 0],
|
|
271
|
+
parameters: {
|
|
272
|
+
values: {
|
|
273
|
+
string: Array.from({ length: 10 }, (_, j) => ({
|
|
274
|
+
name: `param_${j}`,
|
|
275
|
+
value: `This is a long value that takes up tokens ${j}`,
|
|
276
|
+
})),
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
})),
|
|
280
|
+
});
|
|
281
|
+
const fullJson = JSON.stringify(workflow);
|
|
282
|
+
const compactJson = stringifyResponse(formatWorkflowResponse(workflow, 'compact'));
|
|
283
|
+
const summaryJson = stringifyResponse(formatWorkflowResponse(workflow, 'summary'));
|
|
284
|
+
// Compact should be significantly smaller than full
|
|
285
|
+
expect(compactJson.length).toBeLessThan(fullJson.length * 0.5);
|
|
286
|
+
// Summary should be smallest
|
|
287
|
+
expect(summaryJson.length).toBeLessThan(compactJson.length);
|
|
288
|
+
console.log(`Full: ${fullJson.length} chars, Compact: ${compactJson.length} chars, Summary: ${summaryJson.length} chars`);
|
|
289
|
+
console.log(`Reduction: ${Math.round((1 - compactJson.length / fullJson.length) * 100)}% (compact), ${Math.round((1 - summaryJson.length / fullJson.length) * 100)}% (summary)`);
|
|
290
|
+
});
|
|
291
|
+
});
|
package/dist/tools.js
CHANGED
|
@@ -25,7 +25,7 @@ export const tools = [
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
name: 'workflow_get',
|
|
28
|
-
description: 'Get a workflow by ID.
|
|
28
|
+
description: 'Get a workflow by ID. Use format=summary for minimal response, compact (default) for nodes without parameters, full for everything.',
|
|
29
29
|
inputSchema: {
|
|
30
30
|
type: 'object',
|
|
31
31
|
properties: {
|
|
@@ -33,13 +33,18 @@ export const tools = [
|
|
|
33
33
|
type: 'string',
|
|
34
34
|
description: 'Workflow ID',
|
|
35
35
|
},
|
|
36
|
+
format: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
enum: ['summary', 'compact', 'full'],
|
|
39
|
+
description: 'Response detail level. summary=minimal, compact=nodes without params (default), full=everything',
|
|
40
|
+
},
|
|
36
41
|
},
|
|
37
42
|
required: ['id'],
|
|
38
43
|
},
|
|
39
44
|
},
|
|
40
45
|
{
|
|
41
46
|
name: 'workflow_create',
|
|
42
|
-
description: 'Create a new workflow. Returns the created workflow.',
|
|
47
|
+
description: 'Create a new workflow. Returns the created workflow with validation.',
|
|
43
48
|
inputSchema: {
|
|
44
49
|
type: 'object',
|
|
45
50
|
properties: {
|
|
@@ -76,6 +81,11 @@ export const tools = [
|
|
|
76
81
|
type: 'object',
|
|
77
82
|
description: 'Workflow settings',
|
|
78
83
|
},
|
|
84
|
+
format: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
enum: ['summary', 'compact', 'full'],
|
|
87
|
+
description: 'Response detail level (default: compact)',
|
|
88
|
+
},
|
|
79
89
|
},
|
|
80
90
|
required: ['name', 'nodes', 'connections'],
|
|
81
91
|
},
|
|
@@ -132,6 +142,11 @@ Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "proper
|
|
|
132
142
|
required: ['type'],
|
|
133
143
|
},
|
|
134
144
|
},
|
|
145
|
+
format: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
enum: ['summary', 'compact', 'full'],
|
|
148
|
+
description: 'Response detail level (default: compact)',
|
|
149
|
+
},
|
|
135
150
|
},
|
|
136
151
|
required: ['id', 'operations'],
|
|
137
152
|
},
|
|
@@ -218,12 +233,17 @@ Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "proper
|
|
|
218
233
|
type: 'number',
|
|
219
234
|
description: 'Max results (default 20)',
|
|
220
235
|
},
|
|
236
|
+
format: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
enum: ['summary', 'compact', 'full'],
|
|
239
|
+
description: 'Response detail level (default: compact)',
|
|
240
|
+
},
|
|
221
241
|
},
|
|
222
242
|
},
|
|
223
243
|
},
|
|
224
244
|
{
|
|
225
245
|
name: 'execution_get',
|
|
226
|
-
description: 'Get execution details
|
|
246
|
+
description: 'Get execution details. Use format=summary for status only, compact (default) omits runData, full for everything including runData.',
|
|
227
247
|
inputSchema: {
|
|
228
248
|
type: 'object',
|
|
229
249
|
properties: {
|
|
@@ -231,6 +251,11 @@ Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "proper
|
|
|
231
251
|
type: 'string',
|
|
232
252
|
description: 'Execution ID',
|
|
233
253
|
},
|
|
254
|
+
format: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
enum: ['summary', 'compact', 'full'],
|
|
257
|
+
description: 'Response detail level. summary=status only, compact=no runData (default), full=everything',
|
|
258
|
+
},
|
|
234
259
|
},
|
|
235
260
|
required: ['id'],
|
|
236
261
|
},
|
|
@@ -300,6 +325,35 @@ Returns the fixed workflow and list of changes made.`,
|
|
|
300
325
|
},
|
|
301
326
|
},
|
|
302
327
|
// ─────────────────────────────────────────────────────────────
|
|
328
|
+
// Node Discovery
|
|
329
|
+
// ─────────────────────────────────────────────────────────────
|
|
330
|
+
{
|
|
331
|
+
name: 'node_types_list',
|
|
332
|
+
description: `List available n8n node types. Use this to discover valid node types BEFORE creating workflows.
|
|
333
|
+
|
|
334
|
+
Returns: type name, display name, description, category, and version for each node.
|
|
335
|
+
Use the search parameter to filter by keyword (searches type name, display name, and description).
|
|
336
|
+
|
|
337
|
+
IMPORTANT: Always check node types exist before using them in workflow_create or workflow_update.`,
|
|
338
|
+
inputSchema: {
|
|
339
|
+
type: 'object',
|
|
340
|
+
properties: {
|
|
341
|
+
search: {
|
|
342
|
+
type: 'string',
|
|
343
|
+
description: 'Filter nodes by keyword (searches name, type, description)',
|
|
344
|
+
},
|
|
345
|
+
category: {
|
|
346
|
+
type: 'string',
|
|
347
|
+
description: 'Filter by category (e.g., "Core Nodes", "Flow", "AI")',
|
|
348
|
+
},
|
|
349
|
+
limit: {
|
|
350
|
+
type: 'number',
|
|
351
|
+
description: 'Max results (default 50)',
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
// ─────────────────────────────────────────────────────────────
|
|
303
357
|
// Version Control
|
|
304
358
|
// ─────────────────────────────────────────────────────────────
|
|
305
359
|
{
|
|
@@ -330,6 +384,11 @@ Returns the fixed workflow and list of changes made.`,
|
|
|
330
384
|
type: 'string',
|
|
331
385
|
description: 'Version ID (from version_list)',
|
|
332
386
|
},
|
|
387
|
+
format: {
|
|
388
|
+
type: 'string',
|
|
389
|
+
enum: ['summary', 'compact', 'full'],
|
|
390
|
+
description: 'Response detail level (default: compact)',
|
|
391
|
+
},
|
|
333
392
|
},
|
|
334
393
|
required: ['workflowId', 'versionId'],
|
|
335
394
|
},
|
|
@@ -366,6 +425,11 @@ Returns the fixed workflow and list of changes made.`,
|
|
|
366
425
|
type: 'string',
|
|
367
426
|
description: 'Version ID to restore',
|
|
368
427
|
},
|
|
428
|
+
format: {
|
|
429
|
+
type: 'string',
|
|
430
|
+
enum: ['summary', 'compact', 'full'],
|
|
431
|
+
description: 'Response detail level (default: compact)',
|
|
432
|
+
},
|
|
369
433
|
},
|
|
370
434
|
required: ['workflowId', 'versionId'],
|
|
371
435
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -130,3 +130,30 @@ export interface N8nListResponse<T> {
|
|
|
130
130
|
data: T[];
|
|
131
131
|
nextCursor?: string;
|
|
132
132
|
}
|
|
133
|
+
export interface N8nNodeType {
|
|
134
|
+
name: string;
|
|
135
|
+
displayName: string;
|
|
136
|
+
description: string;
|
|
137
|
+
group: string[];
|
|
138
|
+
version: number;
|
|
139
|
+
defaults?: {
|
|
140
|
+
name: string;
|
|
141
|
+
};
|
|
142
|
+
codex?: {
|
|
143
|
+
categories?: string[];
|
|
144
|
+
alias?: string[];
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export interface N8nNodeTypeSummary {
|
|
148
|
+
type: string;
|
|
149
|
+
name: string;
|
|
150
|
+
description: string;
|
|
151
|
+
category: string;
|
|
152
|
+
version: number;
|
|
153
|
+
}
|
|
154
|
+
export interface NodeTypeValidationError {
|
|
155
|
+
nodeType: string;
|
|
156
|
+
nodeName: string;
|
|
157
|
+
message: string;
|
|
158
|
+
suggestions?: string[];
|
|
159
|
+
}
|
package/dist/validators.d.ts
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
* Opinion-based workflow validation
|
|
3
3
|
* Enforces best practices from n8n-best-practices.md
|
|
4
4
|
*/
|
|
5
|
-
import type { N8nWorkflow, ValidationResult, ValidationWarning } from './types.js';
|
|
5
|
+
import type { N8nWorkflow, ValidationResult, ValidationWarning, NodeTypeValidationError } from './types.js';
|
|
6
6
|
export declare function validateWorkflow(workflow: N8nWorkflow): ValidationResult;
|
|
7
7
|
/**
|
|
8
8
|
* Validate a partial update to ensure it won't cause issues
|
|
9
9
|
*/
|
|
10
10
|
export declare function validatePartialUpdate(currentWorkflow: N8nWorkflow, nodeName: string, newParameters: Record<string, unknown>): ValidationWarning[];
|
|
11
|
+
/**
|
|
12
|
+
* Validate that all node types in an array exist in the available types
|
|
13
|
+
* Returns errors for any invalid node types with suggestions
|
|
14
|
+
*/
|
|
15
|
+
export declare function validateNodeTypes(nodes: Array<{
|
|
16
|
+
name: string;
|
|
17
|
+
type: string;
|
|
18
|
+
}>, availableTypes: Set<string>): NodeTypeValidationError[];
|