@pagelines/n8n-mcp 0.1.0 → 0.2.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/CHANGELOG.md +52 -0
- package/README.md +37 -106
- package/dist/autofix.d.ts +28 -0
- package/dist/autofix.js +222 -0
- package/dist/expressions.d.ts +25 -0
- package/dist/expressions.js +209 -0
- package/dist/index.js +124 -1
- package/dist/tools.js +147 -4
- package/dist/validators.js +67 -0
- package/dist/validators.test.js +83 -0
- package/dist/versions.d.ts +71 -0
- package/dist/versions.js +239 -0
- package/docs/best-practices.md +241 -0
- package/package.json +1 -1
- package/server.json +10 -2
- package/src/autofix.ts +275 -0
- package/src/expressions.ts +254 -0
- package/src/index.ts +169 -1
- package/src/tools.ts +155 -4
- package/src/validators.test.ts +97 -0
- package/src/validators.ts +77 -0
- package/src/versions.ts +320 -0
package/dist/validators.test.js
CHANGED
|
@@ -95,6 +95,89 @@ describe('validateWorkflow', () => {
|
|
|
95
95
|
severity: 'warning',
|
|
96
96
|
}));
|
|
97
97
|
});
|
|
98
|
+
it('info on code node usage', () => {
|
|
99
|
+
const workflow = createWorkflow({
|
|
100
|
+
nodes: [
|
|
101
|
+
{
|
|
102
|
+
id: '1',
|
|
103
|
+
name: 'my_code',
|
|
104
|
+
type: 'n8n-nodes-base.code',
|
|
105
|
+
typeVersion: 1,
|
|
106
|
+
position: [0, 0],
|
|
107
|
+
parameters: { jsCode: 'return items;' },
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
const result = validateWorkflow(workflow);
|
|
112
|
+
expect(result.warnings).toContainEqual(expect.objectContaining({
|
|
113
|
+
rule: 'code_node_usage',
|
|
114
|
+
severity: 'info',
|
|
115
|
+
}));
|
|
116
|
+
});
|
|
117
|
+
it('warns on AI node without structured output settings', () => {
|
|
118
|
+
const workflow = createWorkflow({
|
|
119
|
+
nodes: [
|
|
120
|
+
{
|
|
121
|
+
id: '1',
|
|
122
|
+
name: 'ai_agent',
|
|
123
|
+
type: '@n8n/n8n-nodes-langchain.agent',
|
|
124
|
+
typeVersion: 1,
|
|
125
|
+
position: [0, 0],
|
|
126
|
+
parameters: {
|
|
127
|
+
outputParser: true,
|
|
128
|
+
schemaType: 'manual',
|
|
129
|
+
// Missing promptType: 'define' and hasOutputParser: true
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
const result = validateWorkflow(workflow);
|
|
135
|
+
expect(result.warnings).toContainEqual(expect.objectContaining({
|
|
136
|
+
rule: 'ai_structured_output',
|
|
137
|
+
severity: 'warning',
|
|
138
|
+
}));
|
|
139
|
+
});
|
|
140
|
+
it('passes AI node with correct structured output settings', () => {
|
|
141
|
+
const workflow = createWorkflow({
|
|
142
|
+
nodes: [
|
|
143
|
+
{
|
|
144
|
+
id: '1',
|
|
145
|
+
name: 'ai_agent',
|
|
146
|
+
type: '@n8n/n8n-nodes-langchain.agent',
|
|
147
|
+
typeVersion: 1,
|
|
148
|
+
position: [0, 0],
|
|
149
|
+
parameters: {
|
|
150
|
+
outputParser: true,
|
|
151
|
+
schemaType: 'manual',
|
|
152
|
+
promptType: 'define',
|
|
153
|
+
hasOutputParser: true,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
const result = validateWorkflow(workflow);
|
|
159
|
+
const aiWarnings = result.warnings.filter((w) => w.rule === 'ai_structured_output');
|
|
160
|
+
expect(aiWarnings).toHaveLength(0);
|
|
161
|
+
});
|
|
162
|
+
it('warns on in-memory storage nodes', () => {
|
|
163
|
+
const workflow = createWorkflow({
|
|
164
|
+
nodes: [
|
|
165
|
+
{
|
|
166
|
+
id: '1',
|
|
167
|
+
name: 'memory_buffer',
|
|
168
|
+
type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
|
|
169
|
+
typeVersion: 1,
|
|
170
|
+
position: [0, 0],
|
|
171
|
+
parameters: {},
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
const result = validateWorkflow(workflow);
|
|
176
|
+
expect(result.warnings).toContainEqual(expect.objectContaining({
|
|
177
|
+
rule: 'in_memory_storage',
|
|
178
|
+
severity: 'warning',
|
|
179
|
+
}));
|
|
180
|
+
});
|
|
98
181
|
});
|
|
99
182
|
describe('validatePartialUpdate', () => {
|
|
100
183
|
it('errors when node not found', () => {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local version control for n8n workflows
|
|
3
|
+
* Stores workflow snapshots in ~/.n8n-mcp/versions/
|
|
4
|
+
*/
|
|
5
|
+
import type { N8nWorkflow } from './types.js';
|
|
6
|
+
export interface WorkflowVersion {
|
|
7
|
+
id: string;
|
|
8
|
+
workflowId: string;
|
|
9
|
+
workflowName: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
nodeCount: number;
|
|
13
|
+
hash: string;
|
|
14
|
+
}
|
|
15
|
+
export interface VersionConfig {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
maxVersions: number;
|
|
18
|
+
storageDir: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Initialize version control with custom config
|
|
22
|
+
*/
|
|
23
|
+
export declare function initVersionControl(customConfig?: Partial<VersionConfig>): void;
|
|
24
|
+
/**
|
|
25
|
+
* Save a workflow version
|
|
26
|
+
*/
|
|
27
|
+
export declare function saveVersion(workflow: N8nWorkflow, reason?: string): Promise<WorkflowVersion | null>;
|
|
28
|
+
/**
|
|
29
|
+
* List all versions for a workflow
|
|
30
|
+
*/
|
|
31
|
+
export declare function listVersions(workflowId: string): Promise<WorkflowVersion[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Get a specific version's full workflow data
|
|
34
|
+
*/
|
|
35
|
+
export declare function getVersion(workflowId: string, versionId: string): Promise<{
|
|
36
|
+
meta: WorkflowVersion;
|
|
37
|
+
workflow: N8nWorkflow;
|
|
38
|
+
} | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Get the most recent version
|
|
41
|
+
*/
|
|
42
|
+
export declare function getLatestVersion(workflowId: string): Promise<{
|
|
43
|
+
meta: WorkflowVersion;
|
|
44
|
+
workflow: N8nWorkflow;
|
|
45
|
+
} | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Compare two workflow versions
|
|
48
|
+
*/
|
|
49
|
+
export interface VersionDiff {
|
|
50
|
+
nodesAdded: string[];
|
|
51
|
+
nodesRemoved: string[];
|
|
52
|
+
nodesModified: string[];
|
|
53
|
+
connectionsChanged: boolean;
|
|
54
|
+
settingsChanged: boolean;
|
|
55
|
+
summary: string;
|
|
56
|
+
}
|
|
57
|
+
export declare function diffWorkflows(oldWorkflow: N8nWorkflow, newWorkflow: N8nWorkflow): VersionDiff;
|
|
58
|
+
/**
|
|
59
|
+
* Delete all versions for a workflow
|
|
60
|
+
*/
|
|
61
|
+
export declare function deleteAllVersions(workflowId: string): Promise<number>;
|
|
62
|
+
/**
|
|
63
|
+
* Get version control status/stats
|
|
64
|
+
*/
|
|
65
|
+
export declare function getVersionStats(): Promise<{
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
storageDir: string;
|
|
68
|
+
maxVersions: number;
|
|
69
|
+
workflowCount: number;
|
|
70
|
+
totalVersions: number;
|
|
71
|
+
}>;
|
package/dist/versions.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local version control for n8n workflows
|
|
3
|
+
* Stores workflow snapshots in ~/.n8n-mcp/versions/
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
enabled: true,
|
|
10
|
+
maxVersions: 20,
|
|
11
|
+
storageDir: path.join(os.homedir(), '.n8n-mcp', 'versions'),
|
|
12
|
+
};
|
|
13
|
+
let config = { ...DEFAULT_CONFIG };
|
|
14
|
+
/**
|
|
15
|
+
* Initialize version control with custom config
|
|
16
|
+
*/
|
|
17
|
+
export function initVersionControl(customConfig = {}) {
|
|
18
|
+
config = { ...DEFAULT_CONFIG, ...customConfig };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get the storage directory for a workflow
|
|
22
|
+
*/
|
|
23
|
+
function getWorkflowDir(workflowId) {
|
|
24
|
+
return path.join(config.storageDir, workflowId);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Generate a simple hash for workflow content
|
|
28
|
+
*/
|
|
29
|
+
function hashWorkflow(workflow) {
|
|
30
|
+
const content = JSON.stringify({
|
|
31
|
+
nodes: workflow.nodes,
|
|
32
|
+
connections: workflow.connections,
|
|
33
|
+
settings: workflow.settings,
|
|
34
|
+
});
|
|
35
|
+
// Simple hash - good enough for comparison
|
|
36
|
+
let hash = 0;
|
|
37
|
+
for (let i = 0; i < content.length; i++) {
|
|
38
|
+
const char = content.charCodeAt(i);
|
|
39
|
+
hash = ((hash << 5) - hash) + char;
|
|
40
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
41
|
+
}
|
|
42
|
+
return Math.abs(hash).toString(16).padStart(8, '0');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Save a workflow version
|
|
46
|
+
*/
|
|
47
|
+
export async function saveVersion(workflow, reason = 'manual') {
|
|
48
|
+
if (!config.enabled)
|
|
49
|
+
return null;
|
|
50
|
+
const workflowDir = getWorkflowDir(workflow.id);
|
|
51
|
+
await fs.mkdir(workflowDir, { recursive: true });
|
|
52
|
+
const timestamp = new Date().toISOString();
|
|
53
|
+
const hash = hashWorkflow(workflow);
|
|
54
|
+
// Check if this exact version already exists (avoid duplicates)
|
|
55
|
+
const existing = await listVersions(workflow.id);
|
|
56
|
+
if (existing.length > 0 && existing[0].hash === hash) {
|
|
57
|
+
return null; // No changes, skip
|
|
58
|
+
}
|
|
59
|
+
const versionId = `${timestamp.replace(/[:.]/g, '-')}_${hash.slice(0, 6)}`;
|
|
60
|
+
const versionFile = path.join(workflowDir, `${versionId}.json`);
|
|
61
|
+
const versionMeta = {
|
|
62
|
+
id: versionId,
|
|
63
|
+
workflowId: workflow.id,
|
|
64
|
+
workflowName: workflow.name,
|
|
65
|
+
timestamp,
|
|
66
|
+
reason,
|
|
67
|
+
nodeCount: workflow.nodes.length,
|
|
68
|
+
hash,
|
|
69
|
+
};
|
|
70
|
+
const versionData = {
|
|
71
|
+
meta: versionMeta,
|
|
72
|
+
workflow,
|
|
73
|
+
};
|
|
74
|
+
await fs.writeFile(versionFile, JSON.stringify(versionData, null, 2));
|
|
75
|
+
// Prune old versions
|
|
76
|
+
await pruneVersions(workflow.id);
|
|
77
|
+
return versionMeta;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* List all versions for a workflow
|
|
81
|
+
*/
|
|
82
|
+
export async function listVersions(workflowId) {
|
|
83
|
+
const workflowDir = getWorkflowDir(workflowId);
|
|
84
|
+
try {
|
|
85
|
+
const files = await fs.readdir(workflowDir);
|
|
86
|
+
const versions = [];
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
if (!file.endsWith('.json'))
|
|
89
|
+
continue;
|
|
90
|
+
const filePath = path.join(workflowDir, file);
|
|
91
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
92
|
+
const data = JSON.parse(content);
|
|
93
|
+
if (data.meta) {
|
|
94
|
+
versions.push(data.meta);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Sort by timestamp descending (newest first)
|
|
98
|
+
return versions.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error.code === 'ENOENT') {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get a specific version's full workflow data
|
|
109
|
+
*/
|
|
110
|
+
export async function getVersion(workflowId, versionId) {
|
|
111
|
+
const workflowDir = getWorkflowDir(workflowId);
|
|
112
|
+
const versionFile = path.join(workflowDir, `${versionId}.json`);
|
|
113
|
+
try {
|
|
114
|
+
const content = await fs.readFile(versionFile, 'utf-8');
|
|
115
|
+
return JSON.parse(content);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code === 'ENOENT') {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the most recent version
|
|
126
|
+
*/
|
|
127
|
+
export async function getLatestVersion(workflowId) {
|
|
128
|
+
const versions = await listVersions(workflowId);
|
|
129
|
+
if (versions.length === 0)
|
|
130
|
+
return null;
|
|
131
|
+
return getVersion(workflowId, versions[0].id);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Prune old versions beyond maxVersions
|
|
135
|
+
*/
|
|
136
|
+
async function pruneVersions(workflowId) {
|
|
137
|
+
const versions = await listVersions(workflowId);
|
|
138
|
+
if (versions.length <= config.maxVersions) {
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
const toDelete = versions.slice(config.maxVersions);
|
|
142
|
+
const workflowDir = getWorkflowDir(workflowId);
|
|
143
|
+
for (const version of toDelete) {
|
|
144
|
+
const versionFile = path.join(workflowDir, `${version.id}.json`);
|
|
145
|
+
await fs.unlink(versionFile);
|
|
146
|
+
}
|
|
147
|
+
return toDelete.length;
|
|
148
|
+
}
|
|
149
|
+
export function diffWorkflows(oldWorkflow, newWorkflow) {
|
|
150
|
+
const oldNodes = new Map(oldWorkflow.nodes.map((n) => [n.name, n]));
|
|
151
|
+
const newNodes = new Map(newWorkflow.nodes.map((n) => [n.name, n]));
|
|
152
|
+
const nodesAdded = [];
|
|
153
|
+
const nodesRemoved = [];
|
|
154
|
+
const nodesModified = [];
|
|
155
|
+
// Find added and modified nodes
|
|
156
|
+
for (const [name, node] of newNodes) {
|
|
157
|
+
if (!oldNodes.has(name)) {
|
|
158
|
+
nodesAdded.push(name);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const oldNode = oldNodes.get(name);
|
|
162
|
+
if (JSON.stringify(oldNode.parameters) !== JSON.stringify(node.parameters)) {
|
|
163
|
+
nodesModified.push(name);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Find removed nodes
|
|
168
|
+
for (const name of oldNodes.keys()) {
|
|
169
|
+
if (!newNodes.has(name)) {
|
|
170
|
+
nodesRemoved.push(name);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const connectionsChanged = JSON.stringify(oldWorkflow.connections) !== JSON.stringify(newWorkflow.connections);
|
|
174
|
+
const settingsChanged = JSON.stringify(oldWorkflow.settings) !== JSON.stringify(newWorkflow.settings);
|
|
175
|
+
// Generate summary
|
|
176
|
+
const parts = [];
|
|
177
|
+
if (nodesAdded.length)
|
|
178
|
+
parts.push(`+${nodesAdded.length} nodes`);
|
|
179
|
+
if (nodesRemoved.length)
|
|
180
|
+
parts.push(`-${nodesRemoved.length} nodes`);
|
|
181
|
+
if (nodesModified.length)
|
|
182
|
+
parts.push(`~${nodesModified.length} modified`);
|
|
183
|
+
if (connectionsChanged)
|
|
184
|
+
parts.push('connections changed');
|
|
185
|
+
if (settingsChanged)
|
|
186
|
+
parts.push('settings changed');
|
|
187
|
+
return {
|
|
188
|
+
nodesAdded,
|
|
189
|
+
nodesRemoved,
|
|
190
|
+
nodesModified,
|
|
191
|
+
connectionsChanged,
|
|
192
|
+
settingsChanged,
|
|
193
|
+
summary: parts.length ? parts.join(', ') : 'no changes',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Delete all versions for a workflow
|
|
198
|
+
*/
|
|
199
|
+
export async function deleteAllVersions(workflowId) {
|
|
200
|
+
const versions = await listVersions(workflowId);
|
|
201
|
+
const workflowDir = getWorkflowDir(workflowId);
|
|
202
|
+
for (const version of versions) {
|
|
203
|
+
const versionFile = path.join(workflowDir, `${version.id}.json`);
|
|
204
|
+
await fs.unlink(versionFile);
|
|
205
|
+
}
|
|
206
|
+
// Remove the directory if empty
|
|
207
|
+
try {
|
|
208
|
+
await fs.rmdir(workflowDir);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Ignore if not empty
|
|
212
|
+
}
|
|
213
|
+
return versions.length;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get version control status/stats
|
|
217
|
+
*/
|
|
218
|
+
export async function getVersionStats() {
|
|
219
|
+
let workflowCount = 0;
|
|
220
|
+
let totalVersions = 0;
|
|
221
|
+
try {
|
|
222
|
+
const workflows = await fs.readdir(config.storageDir);
|
|
223
|
+
workflowCount = workflows.length;
|
|
224
|
+
for (const workflowId of workflows) {
|
|
225
|
+
const versions = await listVersions(workflowId);
|
|
226
|
+
totalVersions += versions.length;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Storage dir doesn't exist yet
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
enabled: config.enabled,
|
|
234
|
+
storageDir: config.storageDir,
|
|
235
|
+
maxVersions: config.maxVersions,
|
|
236
|
+
workflowCount,
|
|
237
|
+
totalVersions,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# n8n Best Practices
|
|
2
|
+
|
|
3
|
+
> Enforced by `@pagelines/n8n-mcp`
|
|
4
|
+
|
|
5
|
+
## Guiding Principle
|
|
6
|
+
|
|
7
|
+
**What is most stable and easiest to maintain?**
|
|
8
|
+
|
|
9
|
+
| Rule | Why |
|
|
10
|
+
|------|-----|
|
|
11
|
+
| Minimize nodes | Fewer failure points, easier debugging |
|
|
12
|
+
| YAGNI | Build only what's needed now |
|
|
13
|
+
| Explicit references | `$('node_name')` not `$json` - traceable, stable |
|
|
14
|
+
| snake_case | `node_name` not `NodeName` - consistent, readable |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Naming Convention
|
|
19
|
+
|
|
20
|
+
**snake_case everywhere**
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Workflows: content_factory, publish_linkedin, upload_image
|
|
24
|
+
Nodes: trigger_webhook, fetch_articles, check_approved
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Never `NodeName`. Always `node_name`.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Expression References
|
|
32
|
+
|
|
33
|
+
**NEVER use `$json`. Always explicit node references.**
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
// Bad - breaks when flow changes
|
|
37
|
+
{{ $json.field }}
|
|
38
|
+
|
|
39
|
+
// Good - traceable and debuggable
|
|
40
|
+
{{ $('node_name').item.json.field }}
|
|
41
|
+
|
|
42
|
+
// Parallel branch (lookup nodes)
|
|
43
|
+
{{ $('lookup_node').all().length > 0 }}
|
|
44
|
+
|
|
45
|
+
// Environment variable
|
|
46
|
+
{{ $env.API_KEY }}
|
|
47
|
+
|
|
48
|
+
// Fallback pattern
|
|
49
|
+
{{ $('source').item.json.text || $('source').item.json.media_url }}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Secrets and Configuration
|
|
55
|
+
|
|
56
|
+
**Secrets in environment variables. Always.**
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
// Bad
|
|
60
|
+
{ "apiKey": "sk_live_abc123" }
|
|
61
|
+
|
|
62
|
+
// Good
|
|
63
|
+
{{ $env.API_KEY }}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**For workflow-specific settings, use a config node:**
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
[trigger] → [config] → [rest of workflow]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Config node uses JSON output mode:
|
|
73
|
+
```javascript
|
|
74
|
+
={
|
|
75
|
+
"channel_id": "{{ $json.body.channelId || '1234567890' }}",
|
|
76
|
+
"max_items": 10,
|
|
77
|
+
"ai_model": "gpt-4.1-mini"
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then reference: `{{ $('config').item.json.channel_id }}`
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Workflow Editing Safety
|
|
86
|
+
|
|
87
|
+
### The Golden Rule
|
|
88
|
+
|
|
89
|
+
**Never edit a workflow without explicit confirmation and backup.**
|
|
90
|
+
|
|
91
|
+
### Pre-Edit Checklist
|
|
92
|
+
|
|
93
|
+
1. **Confirm** - Get explicit user approval
|
|
94
|
+
2. **List versions** - Know your rollback point
|
|
95
|
+
3. **Read full state** - Understand current config
|
|
96
|
+
4. **Make targeted change** - Use patch operations only
|
|
97
|
+
5. **Verify** - Confirm expected state
|
|
98
|
+
|
|
99
|
+
### Parameter Preservation
|
|
100
|
+
|
|
101
|
+
**CRITICAL:** Partial updates REPLACE the entire `parameters` object.
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
// Bad: Only updates messageId, loses operation and labelIds
|
|
105
|
+
{
|
|
106
|
+
"type": "updateNode",
|
|
107
|
+
"nodeName": "archive_email",
|
|
108
|
+
"properties": {
|
|
109
|
+
"parameters": {
|
|
110
|
+
"messageId": "={{ $json.message_id }}"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Good: Include ALL required parameters
|
|
116
|
+
{
|
|
117
|
+
"type": "updateNode",
|
|
118
|
+
"nodeName": "archive_email",
|
|
119
|
+
"properties": {
|
|
120
|
+
"parameters": {
|
|
121
|
+
"operation": "addLabels",
|
|
122
|
+
"messageId": "={{ $json.message_id }}",
|
|
123
|
+
"labelIds": ["Label_123", "Label_456"]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Code Nodes
|
|
132
|
+
|
|
133
|
+
**Code nodes are a last resort.** Exhaust built-in options first:
|
|
134
|
+
|
|
135
|
+
| Need | Use Instead |
|
|
136
|
+
|------|-------------|
|
|
137
|
+
| Transform fields | Set node with expressions |
|
|
138
|
+
| Filter items | Filter node or If/Switch |
|
|
139
|
+
| Merge data | Merge node |
|
|
140
|
+
| Loop processing | n8n processes arrays natively |
|
|
141
|
+
| Date formatting | `{{ $now.format('yyyy-MM-dd') }}` |
|
|
142
|
+
|
|
143
|
+
**When code IS necessary:**
|
|
144
|
+
- Re-establishing `pairedItem` after chain breaks
|
|
145
|
+
- Complex conditional logic
|
|
146
|
+
- API response parsing expressions can't handle
|
|
147
|
+
|
|
148
|
+
**Code node rules:**
|
|
149
|
+
- Single responsibility (one clear purpose)
|
|
150
|
+
- Name it for what it does: `merge_context`, `parse_response`
|
|
151
|
+
- No side effects - pure data transformation
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## AI Agent Best Practices
|
|
156
|
+
|
|
157
|
+
### Structured Output
|
|
158
|
+
|
|
159
|
+
**Always enable "Require Specific Output Format"** for reliable JSON:
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
{
|
|
163
|
+
"promptType": "define",
|
|
164
|
+
"hasOutputParser": true,
|
|
165
|
+
"schemaType": "manual" // Required for nullable fields
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Without these, AI outputs are unpredictable.
|
|
170
|
+
|
|
171
|
+
### Memory Storage
|
|
172
|
+
|
|
173
|
+
**Never use in-memory storage in production:**
|
|
174
|
+
|
|
175
|
+
| Don't Use | Use Instead |
|
|
176
|
+
|-----------|-------------|
|
|
177
|
+
| Windowed Buffer Memory | Postgres Chat Memory |
|
|
178
|
+
| In-Memory Vector Store | Postgres pgvector |
|
|
179
|
+
|
|
180
|
+
In-memory dies with restart and doesn't scale.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Architecture Patterns
|
|
185
|
+
|
|
186
|
+
### Single Responsibility
|
|
187
|
+
|
|
188
|
+
Don't build monolith workflows:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
Bad: One workflow doing signup → email → CRM → calendar → reports
|
|
192
|
+
|
|
193
|
+
Good: Five focused workflows that communicate via webhooks
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Switch > If Node
|
|
197
|
+
|
|
198
|
+
Always use Switch instead of If:
|
|
199
|
+
- Named outputs (not just true/false)
|
|
200
|
+
- Unlimited conditional branches
|
|
201
|
+
- Send to all matching option
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Validation Rules Enforced
|
|
206
|
+
|
|
207
|
+
| Rule | Severity | Description |
|
|
208
|
+
|------|----------|-------------|
|
|
209
|
+
| `snake_case` | warning | Names should be snake_case |
|
|
210
|
+
| `explicit_reference` | warning | Use `$('node')` not `$json` |
|
|
211
|
+
| `no_hardcoded_ids` | info | Avoid hardcoded IDs |
|
|
212
|
+
| `no_hardcoded_secrets` | error | Never hardcode secrets |
|
|
213
|
+
| `orphan_node` | warning | Node has no connections |
|
|
214
|
+
| `parameter_preservation` | error | Update would remove parameters |
|
|
215
|
+
| `code_node_usage` | info | Code node detected |
|
|
216
|
+
| `ai_structured_output` | warning | AI node missing structured output |
|
|
217
|
+
| `in_memory_storage` | warning | Using non-persistent storage |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Quick Reference
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
// Explicit reference
|
|
225
|
+
{{ $('node_name').item.json.field }}
|
|
226
|
+
|
|
227
|
+
// Environment variable
|
|
228
|
+
{{ $env.VAR_NAME }}
|
|
229
|
+
|
|
230
|
+
// Config node reference
|
|
231
|
+
{{ $('config').item.json.setting }}
|
|
232
|
+
|
|
233
|
+
// Parallel branch query
|
|
234
|
+
{{ $('lookup').all().some(i => i.json.id === $json.id) }}
|
|
235
|
+
|
|
236
|
+
// Date formatting
|
|
237
|
+
{{ $now.format('yyyy-MM-dd') }}
|
|
238
|
+
|
|
239
|
+
// Fallback
|
|
240
|
+
{{ $json.text || $json.description || 'default' }}
|
|
241
|
+
```
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.pagelines/n8n-mcp",
|
|
4
4
|
"displayName": "pl-n8n-mcp",
|
|
5
5
|
"description": "Opinionated MCP server for n8n workflow automation with built-in validation and safety features",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.2.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PageLines",
|
|
9
9
|
"url": "https://github.com/pagelines"
|
|
@@ -46,8 +46,16 @@
|
|
|
46
46
|
"workflow_deactivate",
|
|
47
47
|
"workflow_execute",
|
|
48
48
|
"workflow_validate",
|
|
49
|
+
"workflow_autofix",
|
|
50
|
+
"workflow_format",
|
|
49
51
|
"execution_list",
|
|
50
|
-
"execution_get"
|
|
52
|
+
"execution_get",
|
|
53
|
+
"version_list",
|
|
54
|
+
"version_get",
|
|
55
|
+
"version_save",
|
|
56
|
+
"version_rollback",
|
|
57
|
+
"version_diff",
|
|
58
|
+
"version_stats"
|
|
51
59
|
],
|
|
52
60
|
"keywords": [
|
|
53
61
|
"n8n",
|