@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,208 @@
1
+ /**
2
+ * Opinion-based workflow validation
3
+ * Enforces best practices from n8n-best-practices.md
4
+ */
5
+
6
+ import type { N8nWorkflow, N8nNode, ValidationResult, ValidationWarning } from './types.js';
7
+
8
+ export function validateWorkflow(workflow: N8nWorkflow): ValidationResult {
9
+ const warnings: ValidationWarning[] = [];
10
+
11
+ // Validate workflow name
12
+ validateSnakeCase(workflow.name, 'workflow', warnings);
13
+
14
+ // Validate each node
15
+ for (const node of workflow.nodes) {
16
+ validateNode(node, warnings);
17
+ }
18
+
19
+ // Check for orphan nodes (no connections)
20
+ validateConnections(workflow, warnings);
21
+
22
+ return {
23
+ valid: warnings.filter((w) => w.severity === 'error').length === 0,
24
+ warnings,
25
+ };
26
+ }
27
+
28
+ function validateNode(node: N8nNode, warnings: ValidationWarning[]): void {
29
+ // Check node name is snake_case
30
+ validateSnakeCase(node.name, `node "${node.name}"`, warnings);
31
+
32
+ // Check for $json usage (should use explicit references)
33
+ checkForJsonUsage(node, warnings);
34
+
35
+ // Check for hardcoded IDs
36
+ checkForHardcodedIds(node, warnings);
37
+
38
+ // Check for hardcoded secrets
39
+ checkForHardcodedSecrets(node, warnings);
40
+ }
41
+
42
+ function validateSnakeCase(name: string, context: string, warnings: ValidationWarning[]): void {
43
+ // Allow spaces for readability, but check the underlying format
44
+ const normalized = name.toLowerCase().replace(/\s+/g, '_');
45
+
46
+ // Check if it matches snake_case pattern (allowing numbers)
47
+ const isSnakeCase = /^[a-z][a-z0-9_]*$/.test(normalized);
48
+
49
+ // Also allow names that are already snake_case
50
+ const isAlreadySnake = /^[a-z][a-z0-9_]*$/.test(name);
51
+
52
+ if (!isSnakeCase && !isAlreadySnake) {
53
+ warnings.push({
54
+ rule: 'snake_case',
55
+ message: `${context} should use snake_case naming: "${name}" → "${normalized}"`,
56
+ severity: 'warning',
57
+ });
58
+ }
59
+ }
60
+
61
+ function checkForJsonUsage(node: N8nNode, warnings: ValidationWarning[]): void {
62
+ const params = JSON.stringify(node.parameters);
63
+
64
+ // Detect $json without explicit node reference
65
+ // Bad: $json.field, {{ $json.field }}
66
+ // Good: $('node_name').item.json.field
67
+ const badPatterns = [
68
+ /\$json\./g,
69
+ /\{\{\s*\$json\./g,
70
+ ];
71
+
72
+ for (const pattern of badPatterns) {
73
+ if (pattern.test(params)) {
74
+ warnings.push({
75
+ node: node.name,
76
+ rule: 'explicit_reference',
77
+ message: `Node "${node.name}" uses $json - use explicit $('node_name').item.json.field instead`,
78
+ severity: 'warning',
79
+ });
80
+ break;
81
+ }
82
+ }
83
+ }
84
+
85
+ function checkForHardcodedIds(node: N8nNode, warnings: ValidationWarning[]): void {
86
+ const params = JSON.stringify(node.parameters);
87
+
88
+ // Detect patterns that look like hardcoded IDs
89
+ const idPatterns = [
90
+ // Discord IDs (17-19 digit numbers)
91
+ /["']\d{17,19}["']/g,
92
+ // UUIDs
93
+ /["'][0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}["']/gi,
94
+ // MongoDB ObjectIds
95
+ /["'][0-9a-f]{24}["']/gi,
96
+ ];
97
+
98
+ for (const pattern of idPatterns) {
99
+ if (pattern.test(params)) {
100
+ warnings.push({
101
+ node: node.name,
102
+ rule: 'no_hardcoded_ids',
103
+ message: `Node "${node.name}" may contain hardcoded IDs - consider using config nodes or environment variables`,
104
+ severity: 'info',
105
+ });
106
+ break;
107
+ }
108
+ }
109
+ }
110
+
111
+ function checkForHardcodedSecrets(node: N8nNode, warnings: ValidationWarning[]): void {
112
+ const params = JSON.stringify(node.parameters).toLowerCase();
113
+
114
+ // Check for common secret patterns
115
+ const secretPatterns = [
116
+ /api[_-]?key["']\s*:\s*["'][^"']+["']/i,
117
+ /secret["']\s*:\s*["'][^"']+["']/i,
118
+ /password["']\s*:\s*["'][^"']+["']/i,
119
+ /token["']\s*:\s*["'][a-z0-9]{20,}["']/i,
120
+ ];
121
+
122
+ for (const pattern of secretPatterns) {
123
+ if (pattern.test(params)) {
124
+ warnings.push({
125
+ node: node.name,
126
+ rule: 'no_hardcoded_secrets',
127
+ message: `Node "${node.name}" may contain hardcoded secrets - use $env.VAR_NAME instead`,
128
+ severity: 'error',
129
+ });
130
+ break;
131
+ }
132
+ }
133
+ }
134
+
135
+ function validateConnections(workflow: N8nWorkflow, warnings: ValidationWarning[]): void {
136
+ const connectedNodes = new Set<string>();
137
+
138
+ // Collect all nodes that have connections (as source or target)
139
+ for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
140
+ connectedNodes.add(sourceName);
141
+ for (const outputType of Object.values(outputs)) {
142
+ for (const connections of outputType) {
143
+ for (const conn of connections) {
144
+ connectedNodes.add(conn.node);
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ // Check for orphan nodes (excluding trigger nodes which don't need incoming)
151
+ const triggerTypes = [
152
+ 'n8n-nodes-base.webhook',
153
+ 'n8n-nodes-base.scheduleTrigger',
154
+ 'n8n-nodes-base.manualTrigger',
155
+ 'n8n-nodes-base.emailTrigger',
156
+ '@n8n/n8n-nodes-langchain.chatTrigger',
157
+ ];
158
+
159
+ for (const node of workflow.nodes) {
160
+ const isTrigger = triggerTypes.some((t) => node.type.includes(t.split('.')[1]));
161
+ if (!isTrigger && !connectedNodes.has(node.name)) {
162
+ warnings.push({
163
+ node: node.name,
164
+ rule: 'orphan_node',
165
+ message: `Node "${node.name}" has no connections - may be orphaned`,
166
+ severity: 'warning',
167
+ });
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Validate a partial update to ensure it won't cause issues
174
+ */
175
+ export function validatePartialUpdate(
176
+ currentWorkflow: N8nWorkflow,
177
+ nodeName: string,
178
+ newParameters: Record<string, unknown>
179
+ ): ValidationWarning[] {
180
+ const warnings: ValidationWarning[] = [];
181
+
182
+ const currentNode = currentWorkflow.nodes.find((n) => n.name === nodeName);
183
+ if (!currentNode) {
184
+ warnings.push({
185
+ node: nodeName,
186
+ rule: 'node_exists',
187
+ message: `Node "${nodeName}" not found in workflow`,
188
+ severity: 'error',
189
+ });
190
+ return warnings;
191
+ }
192
+
193
+ // Check for parameter loss
194
+ const currentKeys = Object.keys(currentNode.parameters);
195
+ const newKeys = Object.keys(newParameters);
196
+ const missingKeys = currentKeys.filter((k) => !newKeys.includes(k));
197
+
198
+ if (missingKeys.length > 0) {
199
+ warnings.push({
200
+ node: nodeName,
201
+ rule: 'parameter_preservation',
202
+ message: `Update will remove parameters: ${missingKeys.join(', ')}. Include all existing parameters to preserve them.`,
203
+ severity: 'error',
204
+ });
205
+ }
206
+
207
+ return warnings;
208
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ },
8
+ });