• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

get2knowio / n8n-mcp / 28765216212

06 Jul 2026 03:09AM UTC coverage: 64.317% (+4.1%) from 60.256%
28765216212

push

github

web-flow
Merge pull request #99 from get2knowio/feat/n8n-mcp-high-leverage-tools

feat: workflow validation, templates, execution debugging, capability discovery tools

446 of 747 branches covered (59.71%)

Branch coverage included in aggregate %.

188 of 225 new or added lines in 4 files covered. (83.56%)

713 of 1055 relevant lines covered (67.58%)

19.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.11
/src/workflow-validator.ts
1
import { N8nNode, N8nConnections, WorkflowValidationIssue, WorkflowValidationResult } from './types.js';
2
import { getNodeType } from './node-registry.js';
12✔
3
import { validateNodeConfig } from './node-validator.js';
12✔
4

5
export interface WorkflowLike {
6
  name?: string;
7
  nodes: N8nNode[];
8
  connections: N8nConnections;
9
}
10

11
const TRIGGER_TYPE_PATTERN = /trigger$/i;
12✔
12
const TRIGGER_TYPE_ALLOWLIST = new Set([
12✔
13
  'n8n-nodes-base.webhook',
14
  'n8n-nodes-base.cron',
15
  'n8n-nodes-base.scheduleTrigger',
16
]);
17

18
function isTriggerNode(node: N8nNode): boolean {
19
  return TRIGGER_TYPE_ALLOWLIST.has(node.type) || TRIGGER_TYPE_PATTERN.test(node.type);
15✔
20
}
21

22
function issue(
23
  severity: WorkflowValidationIssue['severity'],
24
  code: string,
25
  message: string,
26
  extra?: Partial<WorkflowValidationIssue>,
27
): WorkflowValidationIssue {
28
  return { severity, code, message, ...extra };
48✔
29
}
30

31
/**
32
 * Validates the connections graph: dangling node references and the
33
 * array-of-arrays nesting n8n expects (`main[outputIndex][] = {node,type,index}`).
34
 * This is the single most common structural mistake when hand-authoring workflow JSON.
35
 */
36
export function validateConnections(workflow: WorkflowLike): WorkflowValidationIssue[] {
12✔
37
  const issues: WorkflowValidationIssue[] = [];
30✔
38
  const nodeNames = new Set(workflow.nodes.map((n) => n.name));
51✔
39
  const connections = workflow.connections || {};
30!
40

41
  for (const [sourceName, outputsByType] of Object.entries(connections)) {
30✔
42
    if (!nodeNames.has(sourceName)) {
18✔
43
      issues.push(
3✔
44
        issue('error', 'UNKNOWN_SOURCE_NODE', `Connections reference unknown source node '${sourceName}'`, {
45
          node: sourceName,
46
        }),
47
      );
48
      continue;
3✔
49
    }
50
    if (outputsByType == null || typeof outputsByType !== 'object') continue;
15!
51

52
    for (const [connectionType, outputs] of Object.entries(outputsByType)) {
15✔
53
      if (!Array.isArray(outputs)) {
15✔
54
        issues.push(
3✔
55
          issue(
56
            'error',
57
            'INVALID_CONNECTION_NESTING',
58
            `Connection '${sourceName}.${connectionType}' must be an array of output ports (one per output index), got ${typeof outputs}`,
59
            { node: sourceName, path: `${sourceName}.${connectionType}` },
60
          ),
61
        );
62
        continue;
3✔
63
      }
64
      outputs.forEach((port, outputIndex) => {
12✔
65
        if (port == null) return; // sparse output arrays are valid (unused output)
12!
66
        if (!Array.isArray(port)) {
12✔
67
          issues.push(
3✔
68
            issue(
69
              'error',
70
              'INVALID_CONNECTION_NESTING',
71
              `Connection '${sourceName}.${connectionType}[${outputIndex}]' must itself be an array of connections (array-of-arrays), got ${typeof port}`,
72
              { node: sourceName, path: `${sourceName}.${connectionType}[${outputIndex}]` },
73
            ),
74
          );
75
          return;
3✔
76
        }
77
        port.forEach((edge: any, edgeIndex: number) => {
9✔
78
          if (!edge || typeof edge !== 'object') {
9!
NEW
79
            issues.push(
×
80
              issue(
81
                'error',
82
                'INVALID_CONNECTION_EDGE',
83
                `Connection '${sourceName}.${connectionType}[${outputIndex}][${edgeIndex}]' is not a valid edge object`,
84
                { node: sourceName, path: `${sourceName}.${connectionType}[${outputIndex}][${edgeIndex}]` },
85
              ),
86
            );
NEW
87
            return;
×
88
          }
89
          if (!nodeNames.has(edge.node)) {
9✔
90
            issues.push(
3✔
91
              issue(
92
                'error',
93
                'UNKNOWN_TARGET_NODE',
94
                `Connection from '${sourceName}' targets unknown node '${edge.node}'`,
95
                { node: sourceName, path: `${sourceName}.${connectionType}[${outputIndex}][${edgeIndex}]` },
96
              ),
97
            );
98
          }
99
          if (edge.type && edge.type !== connectionType) {
9!
NEW
100
            issues.push(
×
101
              issue(
102
                'warning',
103
                'CONNECTION_TYPE_MISMATCH',
104
                `Connection edge type '${edge.type}' does not match its container key '${connectionType}' for '${sourceName}' -> '${edge.node}'`,
105
                { node: sourceName, path: `${sourceName}.${connectionType}[${outputIndex}][${edgeIndex}]` },
106
              ),
107
            );
108
          }
109
        });
110
      });
111
    }
112
  }
113

114
  return issues;
30✔
115
}
116

117
const EXPRESSION_BRACE_RE = /\{\{|\}\}/g;
12✔
118
// Matches $node["Name"], $node.Name, $('Name'), $items('Name'), $("Name")
119
const NODE_REFERENCE_RE = /\$(?:node\[["']([^"']+)["']\]|node\.([A-Za-z0-9_]+)|\(["']([^"']+)["']\)|items\(["']([^"']+)["']\))/g;
12✔
120

121
function walkParameterStrings(value: any, path: string, visit: (value: string, path: string) => void): void {
122
  if (typeof value === 'string') {
99✔
123
    visit(value, path);
33✔
124
  } else if (Array.isArray(value)) {
66✔
125
    value.forEach((v, i) => walkParameterStrings(v, `${path}[${i}]`, visit));
3✔
126
  } else if (value && typeof value === 'object') {
63!
127
    for (const [key, v] of Object.entries(value)) {
63✔
128
      walkParameterStrings(v, path ? `${path}.${key}` : key, visit);
39!
129
    }
130
  }
131
}
132

133
/**
134
 * Best-effort validation of n8n expressions embedded in node parameters:
135
 * missing the leading '=' that n8n requires to treat a string as an expression,
136
 * unbalanced {{ }}, and references to node names that don't exist in the workflow.
137
 */
138
export function validateExpressions(workflow: WorkflowLike): WorkflowValidationIssue[] {
12✔
139
  const issues: WorkflowValidationIssue[] = [];
33✔
140
  const nodeNames = new Set(workflow.nodes.map((n) => n.name));
57✔
141

142
  for (const node of workflow.nodes) {
33✔
143
    if (!node.parameters) continue;
57!
144
    walkParameterStrings(node.parameters, 'parameters', (value, path) => {
57✔
145
      if (!value.includes('{{')) return;
33✔
146

147
      if (!value.startsWith('=')) {
18✔
148
        issues.push(
6✔
149
          issue(
150
            'error',
151
            'MISSING_EXPRESSION_PREFIX',
152
            `Node '${node.name}' parameter '${path}' contains '{{ }}' but is missing the leading '=' required for n8n to evaluate it as an expression`,
153
            { node: node.name, path },
154
          ),
155
        );
156
      }
157

158
      const braceTokens = value.match(EXPRESSION_BRACE_RE) || [];
18!
159
      const opens = braceTokens.filter((t) => t === '{{').length;
33✔
160
      const closes = braceTokens.filter((t) => t === '}}').length;
33✔
161
      if (opens !== closes) {
18✔
162
        issues.push(
3✔
163
          issue(
164
            'error',
165
            'UNBALANCED_EXPRESSION_BRACES',
166
            `Node '${node.name}' parameter '${path}' has unbalanced '{{'/'}}' braces`,
167
            { node: node.name, path },
168
          ),
169
        );
170
      }
171

172
      let match: RegExpExecArray | null;
173
      NODE_REFERENCE_RE.lastIndex = 0;
18✔
174
      while ((match = NODE_REFERENCE_RE.exec(value)) !== null) {
18✔
175
        const referenced = match[1] || match[2] || match[3] || match[4];
6!
176
        if (referenced && !nodeNames.has(referenced)) {
6✔
177
          issues.push(
3✔
178
            issue(
179
              'warning',
180
              'UNKNOWN_NODE_REFERENCE',
181
              `Node '${node.name}' parameter '${path}' references node '${referenced}' which does not exist in this workflow`,
182
              { node: node.name, path },
183
            ),
184
          );
185
        }
186
      }
187
    });
188
  }
189

190
  return issues;
33✔
191
}
192

193
/**
194
 * Structural checks that don't fit connections/expressions: empty graph,
195
 * duplicate node names, missing trigger, and nodes with no edges at all.
196
 */
197
function validateStructure(workflow: WorkflowLike): WorkflowValidationIssue[] {
198
  const issues: WorkflowValidationIssue[] = [];
15✔
199
  const { nodes, connections } = workflow;
15✔
200

201
  if (!nodes || nodes.length === 0) {
15✔
202
    issues.push(issue('error', 'EMPTY_WORKFLOW', 'Workflow has no nodes'));
3✔
203
    return issues;
3✔
204
  }
205

206
  const seenNames = new Map<string, number>();
12✔
207
  for (const node of nodes) {
12✔
208
    seenNames.set(node.name, (seenNames.get(node.name) || 0) + 1);
21✔
209
  }
210
  for (const [name, count] of seenNames) {
12✔
211
    if (count > 1) {
18✔
212
      issues.push(issue('error', 'DUPLICATE_NODE_NAME', `Node name '${name}' is used by ${count} nodes; names must be unique`, { node: name }));
3✔
213
    }
214
  }
215

216
  if (!nodes.some(isTriggerNode)) {
12✔
217
    issues.push(issue('warning', 'MISSING_TRIGGER', 'Workflow has no trigger node (webhook/cron/schedule/*Trigger); it can only be run manually'));
6✔
218
  }
219

220
  const connected = new Set<string>();
12✔
221
  for (const [sourceName, outputsByType] of Object.entries(connections || {})) {
12!
222
    if (outputsByType == null || typeof outputsByType !== 'object') continue;
3!
223
    let hasOutgoingEdge = false;
3✔
224
    for (const outputs of Object.values(outputsByType)) {
3✔
225
      if (!Array.isArray(outputs)) continue;
3!
226
      for (const port of outputs) {
3✔
227
        if (!Array.isArray(port)) continue;
3!
228
        for (const edge of port) {
3✔
229
          if (edge && typeof edge === 'object' && edge.node) {
3!
230
            connected.add(edge.node);
3✔
231
            hasOutgoingEdge = true;
3✔
232
          }
233
        }
234
      }
235
    }
236
    if (hasOutgoingEdge) connected.add(sourceName);
3!
237
  }
238

239
  const isSingleNodeWorkflow = nodes.length === 1;
12✔
240
  if (!isSingleNodeWorkflow) {
12✔
241
    for (const node of nodes) {
9✔
242
      if (!connected.has(node.name)) {
18✔
243
        issues.push(issue('warning', 'ORPHANED_NODE', `Node '${node.name}' has no incoming or outgoing connections`, { node: node.name }));
12✔
244
      }
245
    }
246
  }
247

248
  return issues;
12✔
249
}
250

251
/**
252
 * Aggregate whole-workflow validation: structure + connections + expressions,
253
 * plus per-node config validation where the (currently small) node catalog covers the type.
254
 * Unknown node types are skipped, not flagged — the catalog is intentionally incomplete.
255
 */
256
export function validateWorkflow(workflow: WorkflowLike): WorkflowValidationResult {
12✔
257
  const issues: WorkflowValidationIssue[] = [
15✔
258
    ...validateStructure(workflow),
259
    ...validateConnections(workflow),
260
    ...validateExpressions(workflow),
261
  ];
262

263
  for (const node of workflow.nodes || []) {
15!
264
    if (!getNodeType(node.type)) continue; // degrade gracefully on unknown types
21!
265
    const result = validateNodeConfig(node.type, node.parameters || {});
21!
266
    for (const err of result.errors) {
21✔
NEW
267
      issues.push(
×
268
        issue('error', err.code, `Node '${node.name}': ${err.message}`, { node: node.name, path: `parameters.${err.property}` }),
269
      );
270
    }
271
  }
272

273
  const valid = !issues.some((i) => i.severity === 'error');
15✔
274
  return { valid, issues };
15✔
275
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc