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