@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/index.ts DELETED
@@ -1,550 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * @pagelines/n8n-mcp
4
- * Opinionated MCP server for n8n workflow automation
5
- */
6
-
7
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
- import {
10
- CallToolRequestSchema,
11
- ListToolsRequestSchema,
12
- } from '@modelcontextprotocol/sdk/types.js';
13
-
14
- import { N8nClient } from './n8n-client.js';
15
- import { tools } from './tools.js';
16
- import { validateWorkflow, validateNodeTypes } from './validators.js';
17
- import { validateExpressions, checkCircularReferences } from './expressions.js';
18
- import { autofixWorkflow, formatWorkflow } from './autofix.js';
19
- import {
20
- initVersionControl,
21
- saveVersion,
22
- listVersions,
23
- getVersion,
24
- diffWorkflows,
25
- getVersionStats,
26
- } from './versions.js';
27
- import {
28
- formatWorkflowResponse,
29
- formatExecutionResponse,
30
- formatExecutionListResponse,
31
- stringifyResponse,
32
- type ResponseFormat,
33
- } from './response-format.js';
34
- import type { PatchOperation, N8nConnections, N8nNodeTypeSummary } from './types.js';
35
-
36
- // ─────────────────────────────────────────────────────────────
37
- // Configuration
38
- // ─────────────────────────────────────────────────────────────
39
-
40
- const N8N_API_URL = process.env.N8N_API_URL || process.env.N8N_HOST || '';
41
- const N8N_API_KEY = process.env.N8N_API_KEY || '';
42
-
43
- if (!N8N_API_URL || !N8N_API_KEY) {
44
- console.error('Error: N8N_API_URL and N8N_API_KEY environment variables are required');
45
- console.error('Set them in your MCP server configuration or environment');
46
- process.exit(1);
47
- }
48
-
49
- const client = new N8nClient({
50
- apiUrl: N8N_API_URL,
51
- apiKey: N8N_API_KEY,
52
- });
53
-
54
- // Initialize version control
55
- initVersionControl({
56
- enabled: process.env.N8N_MCP_VERSIONS !== 'false',
57
- maxVersions: parseInt(process.env.N8N_MCP_MAX_VERSIONS || '20', 10),
58
- });
59
-
60
- // ─────────────────────────────────────────────────────────────
61
- // MCP Server
62
- // ─────────────────────────────────────────────────────────────
63
-
64
- const server = new Server(
65
- {
66
- name: '@pagelines/n8n-mcp',
67
- version: '0.3.1',
68
- },
69
- {
70
- capabilities: {
71
- tools: {},
72
- },
73
- }
74
- );
75
-
76
- // List available tools
77
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
78
- tools,
79
- }));
80
-
81
- // Handle tool calls
82
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
83
- const { name, arguments: args } = request.params;
84
-
85
- try {
86
- const result = await handleTool(name, args || {});
87
- return {
88
- content: [
89
- {
90
- type: 'text',
91
- // Use minified JSON to reduce token usage
92
- text: typeof result === 'string' ? result : stringifyResponse(result),
93
- },
94
- ],
95
- };
96
- } catch (error) {
97
- const message = error instanceof Error ? error.message : String(error);
98
- return {
99
- content: [
100
- {
101
- type: 'text',
102
- text: `Error: ${message}`,
103
- },
104
- ],
105
- isError: true,
106
- };
107
- }
108
- });
109
-
110
- // ─────────────────────────────────────────────────────────────
111
- // Tool Handlers
112
- // ─────────────────────────────────────────────────────────────
113
-
114
- async function handleTool(name: string, args: Record<string, unknown>): Promise<unknown> {
115
- switch (name) {
116
- // Workflow operations
117
- case 'workflow_list': {
118
- const response = await client.listWorkflows({
119
- active: args.active as boolean | undefined,
120
- limit: (args.limit as number) || 100,
121
- });
122
- return {
123
- workflows: response.data.map((w) => ({
124
- id: w.id,
125
- name: w.name,
126
- active: w.active,
127
- updatedAt: w.updatedAt,
128
- })),
129
- total: response.data.length,
130
- };
131
- }
132
-
133
- case 'workflow_get': {
134
- const workflow = await client.getWorkflow(args.id as string);
135
- const format = (args.format as ResponseFormat) || 'compact';
136
- return formatWorkflowResponse(workflow, format);
137
- }
138
-
139
- case 'workflow_create': {
140
- const inputNodes = args.nodes as Array<{
141
- name: string;
142
- type: string;
143
- typeVersion: number;
144
- position: [number, number];
145
- parameters: Record<string, unknown>;
146
- credentials?: Record<string, { id: string; name: string }>;
147
- }>;
148
-
149
- // Validate node types BEFORE creating workflow
150
- const availableTypes = await client.listNodeTypes();
151
- const validTypeSet = new Set(availableTypes.map((nt) => nt.name));
152
- const typeErrors = validateNodeTypes(inputNodes, validTypeSet);
153
-
154
- if (typeErrors.length > 0) {
155
- const errorMessages = typeErrors.map((e) => {
156
- let msg = e.message;
157
- if (e.suggestions && e.suggestions.length > 0) {
158
- msg += `. Did you mean: ${e.suggestions.join(', ')}?`;
159
- }
160
- return msg;
161
- });
162
- throw new Error(
163
- `Invalid node types detected:\n${errorMessages.join('\n')}\n\n` +
164
- `Use node_types_list to discover available node types.`
165
- );
166
- }
167
-
168
- const nodes = inputNodes.map((n, i) => ({
169
- id: crypto.randomUUID(),
170
- name: n.name,
171
- type: n.type,
172
- typeVersion: n.typeVersion,
173
- position: n.position || [250, 250 + i * 100],
174
- parameters: n.parameters || {},
175
- ...(n.credentials && { credentials: n.credentials }),
176
- }));
177
-
178
- let workflow = await client.createWorkflow({
179
- name: args.name as string,
180
- nodes,
181
- connections: (args.connections as N8nConnections) || {},
182
- settings: args.settings as Record<string, unknown>,
183
- });
184
-
185
- // Validate and auto-cleanup
186
- const validation = validateWorkflow(workflow);
187
- const autofix = autofixWorkflow(workflow, validation.warnings);
188
- let formatted = formatWorkflow(autofix.workflow);
189
-
190
- // Apply cleanup if there were fixes or formatting changes
191
- if (autofix.fixes.length > 0 || JSON.stringify(workflow) !== JSON.stringify(formatted)) {
192
- workflow = await client.updateWorkflow(workflow.id, formatted);
193
- formatted = workflow;
194
- }
195
-
196
- const format = (args.format as ResponseFormat) || 'compact';
197
-
198
- return {
199
- workflow: formatWorkflowResponse(formatted, format),
200
- validation: {
201
- ...validation,
202
- warnings: autofix.unfixable, // Only show unfixable warnings
203
- },
204
- autoFixed: autofix.fixes.length > 0 ? autofix.fixes : undefined,
205
- };
206
- }
207
-
208
- case 'workflow_update': {
209
- const operations = args.operations as PatchOperation[];
210
-
211
- // Extract addNode operations that need validation
212
- const addNodeOps = operations.filter(
213
- (op): op is Extract<PatchOperation, { type: 'addNode' }> =>
214
- op.type === 'addNode'
215
- );
216
-
217
- if (addNodeOps.length > 0) {
218
- // Fetch available types and validate
219
- const availableTypes = await client.listNodeTypes();
220
- const validTypeSet = new Set(availableTypes.map((nt) => nt.name));
221
- const nodesToValidate = addNodeOps.map((op) => ({
222
- name: op.node.name,
223
- type: op.node.type,
224
- }));
225
- const typeErrors = validateNodeTypes(nodesToValidate, validTypeSet);
226
-
227
- if (typeErrors.length > 0) {
228
- const errorMessages = typeErrors.map((e) => {
229
- let msg = e.message;
230
- if (e.suggestions && e.suggestions.length > 0) {
231
- msg += `. Did you mean: ${e.suggestions.join(', ')}?`;
232
- }
233
- return msg;
234
- });
235
- throw new Error(
236
- `Invalid node types in addNode operations:\n${errorMessages.join('\n')}\n\n` +
237
- `Use node_types_list to discover available node types.`
238
- );
239
- }
240
- }
241
-
242
- // Save version before updating
243
- const currentWorkflow = await client.getWorkflow(args.id as string);
244
- const versionSaved = await saveVersion(currentWorkflow, 'before_update');
245
-
246
- let { workflow, warnings } = await client.patchWorkflow(
247
- args.id as string,
248
- operations
249
- );
250
-
251
- // Validate and auto-cleanup
252
- const validation = validateWorkflow(workflow);
253
- const autofix = autofixWorkflow(workflow, validation.warnings);
254
- let formatted = formatWorkflow(autofix.workflow);
255
-
256
- // Apply cleanup if there were fixes or formatting changes
257
- if (autofix.fixes.length > 0 || JSON.stringify(workflow) !== JSON.stringify(formatted)) {
258
- workflow = await client.updateWorkflow(args.id as string, formatted);
259
- formatted = workflow;
260
- }
261
-
262
- const format = (args.format as ResponseFormat) || 'compact';
263
-
264
- return {
265
- workflow: formatWorkflowResponse(formatted, format),
266
- patchWarnings: warnings,
267
- validation: {
268
- ...validation,
269
- warnings: autofix.unfixable, // Only show unfixable warnings
270
- },
271
- autoFixed: autofix.fixes.length > 0 ? autofix.fixes : undefined,
272
- versionSaved: versionSaved ? versionSaved.id : null,
273
- };
274
- }
275
-
276
- case 'workflow_delete': {
277
- await client.deleteWorkflow(args.id as string);
278
- return { success: true, message: `Workflow ${args.id} deleted` };
279
- }
280
-
281
- case 'workflow_activate': {
282
- const workflow = await client.activateWorkflow(args.id as string);
283
- return {
284
- id: workflow.id,
285
- name: workflow.name,
286
- active: workflow.active,
287
- };
288
- }
289
-
290
- case 'workflow_deactivate': {
291
- const workflow = await client.deactivateWorkflow(args.id as string);
292
- return {
293
- id: workflow.id,
294
- name: workflow.name,
295
- active: workflow.active,
296
- };
297
- }
298
-
299
- case 'workflow_execute': {
300
- const result = await client.executeWorkflow(
301
- args.id as string,
302
- args.data as Record<string, unknown>
303
- );
304
- return result;
305
- }
306
-
307
- // Execution operations
308
- case 'execution_list': {
309
- const response = await client.listExecutions({
310
- workflowId: args.workflowId as string | undefined,
311
- status: args.status as 'success' | 'error' | 'waiting' | undefined,
312
- limit: (args.limit as number) || 20,
313
- });
314
- const format = (args.format as ResponseFormat) || 'compact';
315
- return {
316
- executions: formatExecutionListResponse(response.data, format),
317
- total: response.data.length,
318
- };
319
- }
320
-
321
- case 'execution_get': {
322
- const execution = await client.getExecution(args.id as string);
323
- const format = (args.format as ResponseFormat) || 'compact';
324
- return formatExecutionResponse(execution, format);
325
- }
326
-
327
- // Validation & Quality
328
- case 'workflow_validate': {
329
- const workflow = await client.getWorkflow(args.id as string);
330
- const validation = validateWorkflow(workflow);
331
- const expressionIssues = validateExpressions(workflow);
332
- const circularRefs = checkCircularReferences(workflow);
333
-
334
- return {
335
- workflowId: workflow.id,
336
- workflowName: workflow.name,
337
- ...validation,
338
- expressionIssues,
339
- circularReferences: circularRefs.length > 0 ? circularRefs : null,
340
- };
341
- }
342
-
343
- case 'workflow_autofix': {
344
- const workflow = await client.getWorkflow(args.id as string);
345
- const validation = validateWorkflow(workflow);
346
- const result = autofixWorkflow(workflow, validation.warnings);
347
-
348
- if (args.apply && result.fixes.length > 0) {
349
- // Save version before applying fixes
350
- await saveVersion(workflow, 'before_autofix');
351
-
352
- // Apply the fixed workflow
353
- await client.updateWorkflow(args.id as string, result.workflow);
354
-
355
- return {
356
- applied: true,
357
- fixes: result.fixes,
358
- unfixable: result.unfixable,
359
- workflow: result.workflow,
360
- };
361
- }
362
-
363
- return {
364
- applied: false,
365
- fixes: result.fixes,
366
- unfixable: result.unfixable,
367
- previewWorkflow: result.workflow,
368
- };
369
- }
370
-
371
- case 'workflow_format': {
372
- const workflow = await client.getWorkflow(args.id as string);
373
- const formatted = formatWorkflow(workflow);
374
-
375
- if (args.apply) {
376
- await saveVersion(workflow, 'before_format');
377
- await client.updateWorkflow(args.id as string, formatted);
378
-
379
- return {
380
- applied: true,
381
- workflow: formatted,
382
- };
383
- }
384
-
385
- return {
386
- applied: false,
387
- previewWorkflow: formatted,
388
- };
389
- }
390
-
391
- // Node Discovery
392
- case 'node_types_list': {
393
- const nodeTypes = await client.listNodeTypes();
394
- const search = (args.search as string)?.toLowerCase();
395
- const category = args.category as string;
396
- const limit = (args.limit as number) || 50;
397
-
398
- let results: N8nNodeTypeSummary[] = nodeTypes.map((nt) => ({
399
- type: nt.name,
400
- name: nt.displayName,
401
- description: nt.description,
402
- category: nt.codex?.categories?.[0] || nt.group?.[0] || 'Other',
403
- version: nt.version,
404
- }));
405
-
406
- // Apply search filter
407
- if (search) {
408
- results = results.filter(
409
- (nt) =>
410
- nt.type.toLowerCase().includes(search) ||
411
- nt.name.toLowerCase().includes(search) ||
412
- nt.description.toLowerCase().includes(search)
413
- );
414
- }
415
-
416
- // Apply category filter
417
- if (category) {
418
- results = results.filter((nt) =>
419
- nt.category.toLowerCase().includes(category.toLowerCase())
420
- );
421
- }
422
-
423
- // Apply limit
424
- results = results.slice(0, limit);
425
-
426
- return {
427
- nodeTypes: results,
428
- total: results.length,
429
- hint: 'Use the "type" field value when creating nodes (e.g., "n8n-nodes-base.webhook")',
430
- };
431
- }
432
-
433
- // Version Control
434
- case 'version_list': {
435
- const versions = await listVersions(args.workflowId as string);
436
- return {
437
- workflowId: args.workflowId,
438
- versions,
439
- total: versions.length,
440
- };
441
- }
442
-
443
- case 'version_get': {
444
- const version = await getVersion(
445
- args.workflowId as string,
446
- args.versionId as string
447
- );
448
- if (!version) {
449
- throw new Error(`Version ${args.versionId} not found`);
450
- }
451
- const format = (args.format as ResponseFormat) || 'compact';
452
- return {
453
- meta: version.meta,
454
- workflow: formatWorkflowResponse(version.workflow, format),
455
- };
456
- }
457
-
458
- case 'version_save': {
459
- const workflow = await client.getWorkflow(args.workflowId as string);
460
- const version = await saveVersion(
461
- workflow,
462
- (args.reason as string) || 'manual'
463
- );
464
- if (!version) {
465
- return { saved: false, message: 'No changes detected since last version' };
466
- }
467
- return { saved: true, version };
468
- }
469
-
470
- case 'version_rollback': {
471
- const version = await getVersion(
472
- args.workflowId as string,
473
- args.versionId as string
474
- );
475
- if (!version) {
476
- throw new Error(`Version ${args.versionId} not found`);
477
- }
478
-
479
- // Save current state before rollback
480
- const currentWorkflow = await client.getWorkflow(args.workflowId as string);
481
- await saveVersion(currentWorkflow, 'before_rollback');
482
-
483
- // Apply the old version
484
- await client.updateWorkflow(args.workflowId as string, version.workflow);
485
- const format = (args.format as ResponseFormat) || 'compact';
486
-
487
- return {
488
- success: true,
489
- restoredVersion: version.meta,
490
- workflow: formatWorkflowResponse(version.workflow, format),
491
- };
492
- }
493
-
494
- case 'version_diff': {
495
- const toVersion = await getVersion(
496
- args.workflowId as string,
497
- args.toVersionId as string
498
- );
499
- if (!toVersion) {
500
- throw new Error(`Version ${args.toVersionId} not found`);
501
- }
502
-
503
- let fromWorkflow;
504
- if (args.fromVersionId) {
505
- const fromVersion = await getVersion(
506
- args.workflowId as string,
507
- args.fromVersionId as string
508
- );
509
- if (!fromVersion) {
510
- throw new Error(`Version ${args.fromVersionId} not found`);
511
- }
512
- fromWorkflow = fromVersion.workflow;
513
- } else {
514
- // Compare against current workflow state
515
- fromWorkflow = await client.getWorkflow(args.workflowId as string);
516
- }
517
-
518
- const diff = diffWorkflows(fromWorkflow, toVersion.workflow);
519
-
520
- return {
521
- from: args.fromVersionId || 'current',
522
- to: args.toVersionId,
523
- diff,
524
- };
525
- }
526
-
527
- case 'version_stats': {
528
- const stats = await getVersionStats();
529
- return stats;
530
- }
531
-
532
- default:
533
- throw new Error(`Unknown tool: ${name}`);
534
- }
535
- }
536
-
537
- // ─────────────────────────────────────────────────────────────
538
- // Start Server
539
- // ─────────────────────────────────────────────────────────────
540
-
541
- async function main() {
542
- const transport = new StdioServerTransport();
543
- await server.connect(transport);
544
- console.error('@pagelines/n8n-mcp server started');
545
- }
546
-
547
- main().catch((error) => {
548
- console.error('Fatal error:', error);
549
- process.exit(1);
550
- });