@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.
- package/.github/workflows/ci.yml +38 -0
- package/README.md +155 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +192 -0
- package/dist/n8n-client.d.ts +54 -0
- package/dist/n8n-client.js +275 -0
- package/dist/n8n-client.test.d.ts +1 -0
- package/dist/n8n-client.test.js +184 -0
- package/dist/tools.d.ts +6 -0
- package/dist/tools.js +260 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.js +5 -0
- package/dist/validators.d.ts +10 -0
- package/dist/validators.js +171 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +148 -0
- package/package.json +42 -0
- package/server.json +58 -0
- package/src/index.ts +243 -0
- package/src/n8n-client.test.ts +227 -0
- package/src/n8n-client.ts +361 -0
- package/src/tools.ts +273 -0
- package/src/types.ts +107 -0
- package/src/validators.test.ts +180 -0
- package/src/validators.ts +208 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +8 -0
|
@@ -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
|
+
});
|