@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/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
- }