@pagelines/n8n-mcp 0.1.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.
@@ -0,0 +1,148 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { validateWorkflow, validatePartialUpdate } from './validators.js';
3
+ const createWorkflow = (overrides = {}) => ({
4
+ id: '1',
5
+ name: 'test_workflow',
6
+ active: false,
7
+ nodes: [],
8
+ connections: {},
9
+ createdAt: '2024-01-01',
10
+ updatedAt: '2024-01-01',
11
+ ...overrides,
12
+ });
13
+ describe('validateWorkflow', () => {
14
+ it('passes for valid snake_case workflow', () => {
15
+ const workflow = createWorkflow({
16
+ name: 'my_workflow',
17
+ nodes: [
18
+ {
19
+ id: '1',
20
+ name: 'webhook_trigger',
21
+ type: 'n8n-nodes-base.webhook',
22
+ typeVersion: 1,
23
+ position: [0, 0],
24
+ parameters: { path: 'test' },
25
+ },
26
+ ],
27
+ });
28
+ const result = validateWorkflow(workflow);
29
+ expect(result.valid).toBe(true);
30
+ });
31
+ it('warns on non-snake_case workflow name', () => {
32
+ const workflow = createWorkflow({ name: 'My-Workflow-Name' });
33
+ const result = validateWorkflow(workflow);
34
+ expect(result.warnings).toContainEqual(expect.objectContaining({
35
+ rule: 'snake_case',
36
+ severity: 'warning',
37
+ }));
38
+ });
39
+ it('warns on $json usage', () => {
40
+ const workflow = createWorkflow({
41
+ nodes: [
42
+ {
43
+ id: '1',
44
+ name: 'set_node',
45
+ type: 'n8n-nodes-base.set',
46
+ typeVersion: 1,
47
+ position: [0, 0],
48
+ parameters: { value: '={{ $json.field }}' },
49
+ },
50
+ ],
51
+ });
52
+ const result = validateWorkflow(workflow);
53
+ expect(result.warnings).toContainEqual(expect.objectContaining({
54
+ rule: 'explicit_reference',
55
+ severity: 'warning',
56
+ }));
57
+ });
58
+ it('errors on hardcoded secrets', () => {
59
+ const workflow = createWorkflow({
60
+ nodes: [
61
+ {
62
+ id: '1',
63
+ name: 'http_node',
64
+ type: 'n8n-nodes-base.httpRequest',
65
+ typeVersion: 1,
66
+ position: [0, 0],
67
+ parameters: { apiKey: 'sk_live_abc123def456' },
68
+ },
69
+ ],
70
+ });
71
+ const result = validateWorkflow(workflow);
72
+ expect(result.valid).toBe(false);
73
+ expect(result.warnings).toContainEqual(expect.objectContaining({
74
+ rule: 'no_hardcoded_secrets',
75
+ severity: 'error',
76
+ }));
77
+ });
78
+ it('warns on orphan nodes', () => {
79
+ const workflow = createWorkflow({
80
+ nodes: [
81
+ {
82
+ id: '1',
83
+ name: 'orphan_node',
84
+ type: 'n8n-nodes-base.set',
85
+ typeVersion: 1,
86
+ position: [0, 0],
87
+ parameters: {},
88
+ },
89
+ ],
90
+ connections: {},
91
+ });
92
+ const result = validateWorkflow(workflow);
93
+ expect(result.warnings).toContainEqual(expect.objectContaining({
94
+ rule: 'orphan_node',
95
+ severity: 'warning',
96
+ }));
97
+ });
98
+ });
99
+ describe('validatePartialUpdate', () => {
100
+ it('errors when node not found', () => {
101
+ const workflow = createWorkflow();
102
+ const warnings = validatePartialUpdate(workflow, 'nonexistent', {});
103
+ expect(warnings).toContainEqual(expect.objectContaining({
104
+ rule: 'node_exists',
105
+ severity: 'error',
106
+ }));
107
+ });
108
+ it('errors on parameter loss', () => {
109
+ const workflow = createWorkflow({
110
+ nodes: [
111
+ {
112
+ id: '1',
113
+ name: 'my_node',
114
+ type: 'n8n-nodes-base.set',
115
+ typeVersion: 1,
116
+ position: [0, 0],
117
+ parameters: { existingParam: 'value', anotherParam: 'value2' },
118
+ },
119
+ ],
120
+ });
121
+ const warnings = validatePartialUpdate(workflow, 'my_node', {
122
+ newParam: 'value', // Missing existingParam and anotherParam
123
+ });
124
+ expect(warnings).toContainEqual(expect.objectContaining({
125
+ rule: 'parameter_preservation',
126
+ severity: 'error',
127
+ }));
128
+ });
129
+ it('passes when all parameters preserved', () => {
130
+ const workflow = createWorkflow({
131
+ nodes: [
132
+ {
133
+ id: '1',
134
+ name: 'my_node',
135
+ type: 'n8n-nodes-base.set',
136
+ typeVersion: 1,
137
+ position: [0, 0],
138
+ parameters: { existingParam: 'value' },
139
+ },
140
+ ],
141
+ });
142
+ const warnings = validatePartialUpdate(workflow, 'my_node', {
143
+ existingParam: 'value',
144
+ newParam: 'new',
145
+ });
146
+ expect(warnings).toHaveLength(0);
147
+ });
148
+ });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@pagelines/n8n-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Opinionated MCP server for n8n workflow automation",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "pl-n8n-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js",
14
+ "test": "vitest",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "n8n",
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "workflow",
22
+ "automation",
23
+ "pagelines"
24
+ ],
25
+ "author": "PageLines",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/PageLines/n8n-mcp"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20.0.0",
39
+ "typescript": "^5.0.0",
40
+ "vitest": "^1.0.0"
41
+ }
42
+ }
package/server.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "https://registry.modelcontextprotocol.io/schema/server.json",
3
+ "name": "io.github.pagelines/n8n-mcp",
4
+ "displayName": "pl-n8n-mcp",
5
+ "description": "Opinionated MCP server for n8n workflow automation with built-in validation and safety features",
6
+ "version": "0.1.0",
7
+ "author": {
8
+ "name": "PageLines",
9
+ "url": "https://github.com/pagelines"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/PageLines/n8n-mcp"
14
+ },
15
+ "license": "MIT",
16
+ "packages": {
17
+ "npm": "@pagelines/n8n-mcp"
18
+ },
19
+ "runtime": {
20
+ "command": "npx",
21
+ "args": ["-y", "@pagelines/n8n-mcp"],
22
+ "env": {
23
+ "N8N_API_URL": {
24
+ "description": "n8n instance URL",
25
+ "required": true
26
+ },
27
+ "N8N_API_KEY": {
28
+ "description": "n8n API key",
29
+ "required": true,
30
+ "secret": true
31
+ }
32
+ }
33
+ },
34
+ "capabilities": {
35
+ "tools": true,
36
+ "resources": false,
37
+ "prompts": false
38
+ },
39
+ "tools": [
40
+ "workflow_list",
41
+ "workflow_get",
42
+ "workflow_create",
43
+ "workflow_update",
44
+ "workflow_delete",
45
+ "workflow_activate",
46
+ "workflow_deactivate",
47
+ "workflow_execute",
48
+ "workflow_validate",
49
+ "execution_list",
50
+ "execution_get"
51
+ ],
52
+ "keywords": [
53
+ "n8n",
54
+ "workflow",
55
+ "automation",
56
+ "mcp"
57
+ ]
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,243 @@
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 } from './validators.js';
17
+ import type { PatchOperation, N8nConnections } from './types.js';
18
+
19
+ // ─────────────────────────────────────────────────────────────
20
+ // Configuration
21
+ // ─────────────────────────────────────────────────────────────
22
+
23
+ const N8N_API_URL = process.env.N8N_API_URL || process.env.N8N_HOST || '';
24
+ const N8N_API_KEY = process.env.N8N_API_KEY || '';
25
+
26
+ if (!N8N_API_URL || !N8N_API_KEY) {
27
+ console.error('Error: N8N_API_URL and N8N_API_KEY environment variables are required');
28
+ console.error('Set them in your MCP server configuration or environment');
29
+ process.exit(1);
30
+ }
31
+
32
+ const client = new N8nClient({
33
+ apiUrl: N8N_API_URL,
34
+ apiKey: N8N_API_KEY,
35
+ });
36
+
37
+ // ─────────────────────────────────────────────────────────────
38
+ // MCP Server
39
+ // ─────────────────────────────────────────────────────────────
40
+
41
+ const server = new Server(
42
+ {
43
+ name: '@pagelines/n8n-mcp',
44
+ version: '0.1.0',
45
+ },
46
+ {
47
+ capabilities: {
48
+ tools: {},
49
+ },
50
+ }
51
+ );
52
+
53
+ // List available tools
54
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
55
+ tools,
56
+ }));
57
+
58
+ // Handle tool calls
59
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
60
+ const { name, arguments: args } = request.params;
61
+
62
+ try {
63
+ const result = await handleTool(name, args || {});
64
+ return {
65
+ content: [
66
+ {
67
+ type: 'text',
68
+ text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
69
+ },
70
+ ],
71
+ };
72
+ } catch (error) {
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ return {
75
+ content: [
76
+ {
77
+ type: 'text',
78
+ text: `Error: ${message}`,
79
+ },
80
+ ],
81
+ isError: true,
82
+ };
83
+ }
84
+ });
85
+
86
+ // ─────────────────────────────────────────────────────────────
87
+ // Tool Handlers
88
+ // ─────────────────────────────────────────────────────────────
89
+
90
+ async function handleTool(name: string, args: Record<string, unknown>): Promise<unknown> {
91
+ switch (name) {
92
+ // Workflow operations
93
+ case 'workflow_list': {
94
+ const response = await client.listWorkflows({
95
+ active: args.active as boolean | undefined,
96
+ limit: (args.limit as number) || 100,
97
+ });
98
+ return {
99
+ workflows: response.data.map((w) => ({
100
+ id: w.id,
101
+ name: w.name,
102
+ active: w.active,
103
+ updatedAt: w.updatedAt,
104
+ })),
105
+ total: response.data.length,
106
+ };
107
+ }
108
+
109
+ case 'workflow_get': {
110
+ const workflow = await client.getWorkflow(args.id as string);
111
+ return workflow;
112
+ }
113
+
114
+ case 'workflow_create': {
115
+ const nodes = (args.nodes as Array<{
116
+ name: string;
117
+ type: string;
118
+ typeVersion: number;
119
+ position: [number, number];
120
+ parameters: Record<string, unknown>;
121
+ credentials?: Record<string, { id: string; name: string }>;
122
+ }>).map((n, i) => ({
123
+ id: crypto.randomUUID(),
124
+ name: n.name,
125
+ type: n.type,
126
+ typeVersion: n.typeVersion,
127
+ position: n.position || [250, 250 + i * 100],
128
+ parameters: n.parameters || {},
129
+ ...(n.credentials && { credentials: n.credentials }),
130
+ }));
131
+
132
+ const workflow = await client.createWorkflow({
133
+ name: args.name as string,
134
+ nodes,
135
+ connections: (args.connections as N8nConnections) || {},
136
+ settings: args.settings as Record<string, unknown>,
137
+ });
138
+
139
+ // Validate the new workflow
140
+ const validation = validateWorkflow(workflow);
141
+
142
+ return {
143
+ workflow,
144
+ validation,
145
+ };
146
+ }
147
+
148
+ case 'workflow_update': {
149
+ const operations = args.operations as PatchOperation[];
150
+ const { workflow, warnings } = await client.patchWorkflow(
151
+ args.id as string,
152
+ operations
153
+ );
154
+
155
+ // Also run validation
156
+ const validation = validateWorkflow(workflow);
157
+
158
+ return {
159
+ workflow,
160
+ patchWarnings: warnings,
161
+ validation,
162
+ };
163
+ }
164
+
165
+ case 'workflow_delete': {
166
+ await client.deleteWorkflow(args.id as string);
167
+ return { success: true, message: `Workflow ${args.id} deleted` };
168
+ }
169
+
170
+ case 'workflow_activate': {
171
+ const workflow = await client.activateWorkflow(args.id as string);
172
+ return {
173
+ id: workflow.id,
174
+ name: workflow.name,
175
+ active: workflow.active,
176
+ };
177
+ }
178
+
179
+ case 'workflow_deactivate': {
180
+ const workflow = await client.deactivateWorkflow(args.id as string);
181
+ return {
182
+ id: workflow.id,
183
+ name: workflow.name,
184
+ active: workflow.active,
185
+ };
186
+ }
187
+
188
+ case 'workflow_execute': {
189
+ const result = await client.executeWorkflow(
190
+ args.id as string,
191
+ args.data as Record<string, unknown>
192
+ );
193
+ return result;
194
+ }
195
+
196
+ // Execution operations
197
+ case 'execution_list': {
198
+ const response = await client.listExecutions({
199
+ workflowId: args.workflowId as string | undefined,
200
+ status: args.status as 'success' | 'error' | 'waiting' | undefined,
201
+ limit: (args.limit as number) || 20,
202
+ });
203
+ return {
204
+ executions: response.data,
205
+ total: response.data.length,
206
+ };
207
+ }
208
+
209
+ case 'execution_get': {
210
+ const execution = await client.getExecution(args.id as string);
211
+ return execution;
212
+ }
213
+
214
+ // Validation
215
+ case 'workflow_validate': {
216
+ const workflow = await client.getWorkflow(args.id as string);
217
+ const validation = validateWorkflow(workflow);
218
+ return {
219
+ workflowId: workflow.id,
220
+ workflowName: workflow.name,
221
+ ...validation,
222
+ };
223
+ }
224
+
225
+ default:
226
+ throw new Error(`Unknown tool: ${name}`);
227
+ }
228
+ }
229
+
230
+ // ─────────────────────────────────────────────────────────────
231
+ // Start Server
232
+ // ─────────────────────────────────────────────────────────────
233
+
234
+ async function main() {
235
+ const transport = new StdioServerTransport();
236
+ await server.connect(transport);
237
+ console.error('@pagelines/n8n-mcp server started');
238
+ }
239
+
240
+ main().catch((error) => {
241
+ console.error('Fatal error:', error);
242
+ process.exit(1);
243
+ });