@pagelines/n8n-mcp 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -6
- package/dist/index.js +2 -71
- package/dist/n8n-client.d.ts +1 -2
- package/dist/n8n-client.js +3 -8
- package/dist/tools.js +0 -29
- package/dist/types.d.ts +17 -27
- package/dist/types.js +34 -1
- package/dist/validators.d.ts +1 -9
- package/dist/validators.js +0 -85
- package/package.json +2 -2
- package/.github/workflows/ci.yml +0 -38
- package/dist/n8n-client.test.d.ts +0 -1
- package/dist/n8n-client.test.js +0 -295
- package/dist/response-format.test.d.ts +0 -1
- package/dist/response-format.test.js +0 -291
- package/dist/validators.test.d.ts +0 -1
- package/dist/validators.test.js +0 -310
- package/docs/best-practices.md +0 -165
- package/docs/node-config.md +0 -205
- package/plans/ai-guidelines.md +0 -233
- package/plans/architecture.md +0 -220
- package/server.json +0 -66
- package/src/autofix.ts +0 -275
- package/src/expressions.ts +0 -254
- package/src/index.ts +0 -550
- package/src/n8n-client.test.ts +0 -362
- package/src/n8n-client.ts +0 -372
- package/src/response-format.test.ts +0 -355
- package/src/response-format.ts +0 -278
- package/src/tools.ts +0 -489
- package/src/types.ts +0 -140
- package/src/validators.test.ts +0 -374
- package/src/validators.ts +0 -394
- package/src/versions.ts +0 -320
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -8
package/src/n8n-client.ts
DELETED
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* n8n REST API Client
|
|
3
|
-
* Clean, minimal implementation with built-in safety checks
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
N8nWorkflow,
|
|
8
|
-
N8nWorkflowListItem,
|
|
9
|
-
N8nExecution,
|
|
10
|
-
N8nExecutionListItem,
|
|
11
|
-
N8nListResponse,
|
|
12
|
-
N8nNode,
|
|
13
|
-
N8nNodeType,
|
|
14
|
-
PatchOperation,
|
|
15
|
-
} from './types.js';
|
|
16
|
-
|
|
17
|
-
export interface N8nClientConfig {
|
|
18
|
-
apiUrl: string;
|
|
19
|
-
apiKey: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class N8nClient {
|
|
23
|
-
private baseUrl: string;
|
|
24
|
-
private headers: Record<string, string>;
|
|
25
|
-
|
|
26
|
-
constructor(config: N8nClientConfig) {
|
|
27
|
-
// Normalize URL (remove trailing slash)
|
|
28
|
-
this.baseUrl = config.apiUrl.replace(/\/$/, '');
|
|
29
|
-
this.headers = {
|
|
30
|
-
'X-N8N-API-KEY': config.apiKey,
|
|
31
|
-
'Content-Type': 'application/json',
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ─────────────────────────────────────────────────────────────
|
|
36
|
-
// HTTP helpers
|
|
37
|
-
// ─────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
private async request<T>(
|
|
40
|
-
method: string,
|
|
41
|
-
path: string,
|
|
42
|
-
body?: unknown
|
|
43
|
-
): Promise<T> {
|
|
44
|
-
const url = `${this.baseUrl}${path}`;
|
|
45
|
-
|
|
46
|
-
const response = await fetch(url, {
|
|
47
|
-
method,
|
|
48
|
-
headers: this.headers,
|
|
49
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
const text = await response.text();
|
|
54
|
-
throw new Error(`n8n API error (${response.status}): ${text}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Handle empty responses
|
|
58
|
-
const text = await response.text();
|
|
59
|
-
if (!text) return {} as T;
|
|
60
|
-
|
|
61
|
-
return JSON.parse(text) as T;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ─────────────────────────────────────────────────────────────
|
|
65
|
-
// Workflow operations
|
|
66
|
-
// ─────────────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
async listWorkflows(options?: {
|
|
69
|
-
limit?: number;
|
|
70
|
-
cursor?: string;
|
|
71
|
-
active?: boolean;
|
|
72
|
-
tags?: string[];
|
|
73
|
-
}): Promise<N8nListResponse<N8nWorkflowListItem>> {
|
|
74
|
-
const params = new URLSearchParams();
|
|
75
|
-
if (options?.limit) params.set('limit', String(options.limit));
|
|
76
|
-
if (options?.cursor) params.set('cursor', options.cursor);
|
|
77
|
-
if (options?.active !== undefined) params.set('active', String(options.active));
|
|
78
|
-
if (options?.tags?.length) params.set('tags', options.tags.join(','));
|
|
79
|
-
|
|
80
|
-
const query = params.toString();
|
|
81
|
-
return this.request('GET', `/api/v1/workflows${query ? `?${query}` : ''}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async getWorkflow(id: string): Promise<N8nWorkflow> {
|
|
85
|
-
return this.request('GET', `/api/v1/workflows/${id}`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async createWorkflow(workflow: {
|
|
89
|
-
name: string;
|
|
90
|
-
nodes: N8nNode[];
|
|
91
|
-
connections: N8nWorkflow['connections'];
|
|
92
|
-
settings?: Record<string, unknown>;
|
|
93
|
-
}): Promise<N8nWorkflow> {
|
|
94
|
-
return this.request('POST', '/api/v1/workflows', workflow);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async updateWorkflow(
|
|
98
|
-
id: string,
|
|
99
|
-
workflow: Partial<N8nWorkflow>
|
|
100
|
-
): Promise<N8nWorkflow> {
|
|
101
|
-
// Strip properties that n8n API doesn't accept on PUT
|
|
102
|
-
const { id: _id, createdAt, updatedAt, active, versionId, ...allowed } = workflow as any;
|
|
103
|
-
return this.request('PUT', `/api/v1/workflows/${id}`, allowed);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async deleteWorkflow(id: string): Promise<void> {
|
|
107
|
-
await this.request('DELETE', `/api/v1/workflows/${id}`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async activateWorkflow(id: string): Promise<N8nWorkflow> {
|
|
111
|
-
return this.request('POST', `/api/v1/workflows/${id}/activate`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async deactivateWorkflow(id: string): Promise<N8nWorkflow> {
|
|
115
|
-
return this.request('POST', `/api/v1/workflows/${id}/deactivate`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ─────────────────────────────────────────────────────────────
|
|
119
|
-
// Partial update with safety checks
|
|
120
|
-
// ─────────────────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
async patchWorkflow(
|
|
123
|
-
id: string,
|
|
124
|
-
operations: PatchOperation[]
|
|
125
|
-
): Promise<{ workflow: N8nWorkflow; warnings: string[] }> {
|
|
126
|
-
const warnings: string[] = [];
|
|
127
|
-
|
|
128
|
-
// Fetch current state
|
|
129
|
-
const current = await this.getWorkflow(id);
|
|
130
|
-
|
|
131
|
-
// Apply operations
|
|
132
|
-
const updated = this.applyOperations(current, operations, warnings);
|
|
133
|
-
|
|
134
|
-
// Save
|
|
135
|
-
const result = await this.updateWorkflow(id, {
|
|
136
|
-
name: updated.name,
|
|
137
|
-
nodes: updated.nodes,
|
|
138
|
-
connections: updated.connections,
|
|
139
|
-
settings: updated.settings,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
return { workflow: result, warnings };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
private applyOperations(
|
|
146
|
-
workflow: N8nWorkflow,
|
|
147
|
-
operations: PatchOperation[],
|
|
148
|
-
warnings: string[]
|
|
149
|
-
): N8nWorkflow {
|
|
150
|
-
// Deep clone
|
|
151
|
-
const result: N8nWorkflow = JSON.parse(JSON.stringify(workflow));
|
|
152
|
-
|
|
153
|
-
for (const op of operations) {
|
|
154
|
-
switch (op.type) {
|
|
155
|
-
case 'addNode': {
|
|
156
|
-
const node: N8nNode = {
|
|
157
|
-
id: op.node.id || crypto.randomUUID(),
|
|
158
|
-
name: op.node.name,
|
|
159
|
-
type: op.node.type,
|
|
160
|
-
typeVersion: op.node.typeVersion,
|
|
161
|
-
position: op.node.position,
|
|
162
|
-
parameters: op.node.parameters,
|
|
163
|
-
...(op.node.credentials && { credentials: op.node.credentials }),
|
|
164
|
-
...(op.node.disabled && { disabled: op.node.disabled }),
|
|
165
|
-
};
|
|
166
|
-
result.nodes.push(node);
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
case 'removeNode': {
|
|
171
|
-
const idx = result.nodes.findIndex((n) => n.name === op.nodeName);
|
|
172
|
-
if (idx === -1) {
|
|
173
|
-
warnings.push(`Node not found: ${op.nodeName}`);
|
|
174
|
-
} else {
|
|
175
|
-
result.nodes.splice(idx, 1);
|
|
176
|
-
// Clean up connections
|
|
177
|
-
delete result.connections[op.nodeName];
|
|
178
|
-
for (const [, outputs] of Object.entries(result.connections)) {
|
|
179
|
-
for (const [, connections] of Object.entries(outputs)) {
|
|
180
|
-
for (const connArray of connections) {
|
|
181
|
-
const toRemove = connArray.filter((c) => c.node === op.nodeName);
|
|
182
|
-
for (const conn of toRemove) {
|
|
183
|
-
const i = connArray.indexOf(conn);
|
|
184
|
-
if (i !== -1) connArray.splice(i, 1);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
case 'updateNode': {
|
|
194
|
-
const node = result.nodes.find((n) => n.name === op.nodeName);
|
|
195
|
-
if (!node) {
|
|
196
|
-
warnings.push(`Node not found: ${op.nodeName}`);
|
|
197
|
-
} else {
|
|
198
|
-
// CRITICAL: Warn about parameter replacement
|
|
199
|
-
if (op.properties.parameters) {
|
|
200
|
-
const currentKeys = Object.keys(node.parameters);
|
|
201
|
-
const newKeys = Object.keys(op.properties.parameters);
|
|
202
|
-
const missingKeys = currentKeys.filter((k) => !newKeys.includes(k));
|
|
203
|
-
if (missingKeys.length > 0) {
|
|
204
|
-
warnings.push(
|
|
205
|
-
`WARNING: Updating "${op.nodeName}" will remove parameters: ${missingKeys.join(', ')}. ` +
|
|
206
|
-
`Include all existing parameters to preserve them.`
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
Object.assign(node, op.properties);
|
|
211
|
-
}
|
|
212
|
-
break;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
case 'addConnection': {
|
|
216
|
-
const outputType = op.outputType || 'main';
|
|
217
|
-
const fromOutput = op.fromOutput || 0;
|
|
218
|
-
const toInput = op.toInput || 0;
|
|
219
|
-
const inputType = op.inputType || 'main';
|
|
220
|
-
|
|
221
|
-
if (!result.connections[op.from]) {
|
|
222
|
-
result.connections[op.from] = {};
|
|
223
|
-
}
|
|
224
|
-
if (!result.connections[op.from][outputType]) {
|
|
225
|
-
result.connections[op.from][outputType] = [];
|
|
226
|
-
}
|
|
227
|
-
while (result.connections[op.from][outputType].length <= fromOutput) {
|
|
228
|
-
result.connections[op.from][outputType].push([]);
|
|
229
|
-
}
|
|
230
|
-
result.connections[op.from][outputType][fromOutput].push({
|
|
231
|
-
node: op.to,
|
|
232
|
-
type: inputType,
|
|
233
|
-
index: toInput,
|
|
234
|
-
});
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
case 'removeConnection': {
|
|
239
|
-
const outputType = op.outputType || 'main';
|
|
240
|
-
const fromOutput = op.fromOutput || 0;
|
|
241
|
-
const conns = result.connections[op.from]?.[outputType]?.[fromOutput];
|
|
242
|
-
if (conns) {
|
|
243
|
-
const idx = conns.findIndex((c) => c.node === op.to);
|
|
244
|
-
if (idx !== -1) conns.splice(idx, 1);
|
|
245
|
-
}
|
|
246
|
-
break;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
case 'updateSettings': {
|
|
250
|
-
result.settings = { ...result.settings, ...op.settings };
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
case 'updateName': {
|
|
255
|
-
result.name = op.name;
|
|
256
|
-
break;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
case 'activate': {
|
|
260
|
-
result.active = true;
|
|
261
|
-
break;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
case 'deactivate': {
|
|
265
|
-
result.active = false;
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return result;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ─────────────────────────────────────────────────────────────
|
|
275
|
-
// Execution operations
|
|
276
|
-
// ─────────────────────────────────────────────────────────────
|
|
277
|
-
|
|
278
|
-
async listExecutions(options?: {
|
|
279
|
-
workflowId?: string;
|
|
280
|
-
status?: 'success' | 'error' | 'waiting';
|
|
281
|
-
limit?: number;
|
|
282
|
-
cursor?: string;
|
|
283
|
-
}): Promise<N8nListResponse<N8nExecutionListItem>> {
|
|
284
|
-
const params = new URLSearchParams();
|
|
285
|
-
if (options?.workflowId) params.set('workflowId', options.workflowId);
|
|
286
|
-
if (options?.status) params.set('status', options.status);
|
|
287
|
-
if (options?.limit) params.set('limit', String(options.limit));
|
|
288
|
-
if (options?.cursor) params.set('cursor', options.cursor);
|
|
289
|
-
|
|
290
|
-
const query = params.toString();
|
|
291
|
-
return this.request('GET', `/api/v1/executions${query ? `?${query}` : ''}`);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async getExecution(id: string): Promise<N8nExecution> {
|
|
295
|
-
return this.request('GET', `/api/v1/executions/${id}`);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async deleteExecution(id: string): Promise<void> {
|
|
299
|
-
await this.request('DELETE', `/api/v1/executions/${id}`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ─────────────────────────────────────────────────────────────
|
|
303
|
-
// Workflow execution (webhook trigger)
|
|
304
|
-
// ─────────────────────────────────────────────────────────────
|
|
305
|
-
|
|
306
|
-
async executeWorkflow(
|
|
307
|
-
id: string,
|
|
308
|
-
data?: Record<string, unknown>
|
|
309
|
-
): Promise<{ executionId?: string; data?: unknown }> {
|
|
310
|
-
// Get workflow to find webhook path
|
|
311
|
-
const workflow = await this.getWorkflow(id);
|
|
312
|
-
|
|
313
|
-
// Find webhook trigger node
|
|
314
|
-
const webhookNode = workflow.nodes.find(
|
|
315
|
-
(n) => n.type === 'n8n-nodes-base.webhook'
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
if (!webhookNode) {
|
|
319
|
-
throw new Error('Workflow has no webhook trigger. Cannot execute via API.');
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const path = webhookNode.parameters.path as string;
|
|
323
|
-
if (!path) {
|
|
324
|
-
throw new Error('Webhook node has no path configured.');
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Execute via webhook
|
|
328
|
-
const webhookUrl = `${this.baseUrl}/webhook/${path}`;
|
|
329
|
-
const response = await fetch(webhookUrl, {
|
|
330
|
-
method: 'POST',
|
|
331
|
-
headers: { 'Content-Type': 'application/json' },
|
|
332
|
-
body: data ? JSON.stringify(data) : undefined,
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
if (!response.ok) {
|
|
336
|
-
const text = await response.text();
|
|
337
|
-
throw new Error(`Webhook execution failed (${response.status}): ${text}`);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const result = await response.text();
|
|
341
|
-
try {
|
|
342
|
-
return JSON.parse(result);
|
|
343
|
-
} catch {
|
|
344
|
-
return { data: result };
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// ─────────────────────────────────────────────────────────────
|
|
349
|
-
// Health check
|
|
350
|
-
// ─────────────────────────────────────────────────────────────
|
|
351
|
-
|
|
352
|
-
async healthCheck(): Promise<{ healthy: boolean; version?: string; error?: string }> {
|
|
353
|
-
try {
|
|
354
|
-
// Try to list workflows with limit 1
|
|
355
|
-
await this.listWorkflows({ limit: 1 });
|
|
356
|
-
return { healthy: true };
|
|
357
|
-
} catch (error) {
|
|
358
|
-
return {
|
|
359
|
-
healthy: false,
|
|
360
|
-
error: error instanceof Error ? error.message : String(error),
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// ─────────────────────────────────────────────────────────────
|
|
366
|
-
// Node Types
|
|
367
|
-
// ─────────────────────────────────────────────────────────────
|
|
368
|
-
|
|
369
|
-
async listNodeTypes(): Promise<N8nNodeType[]> {
|
|
370
|
-
return this.request<N8nNodeType[]>('GET', '/api/v1/nodes');
|
|
371
|
-
}
|
|
372
|
-
}
|