n8n-workflow-builder-mcp 1.0.2 → 2.0.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/README.md +37 -2
- package/dist/catalog.sqlite +0 -0
- package/dist/index.js +45 -22
- package/dist/mcp/tools/composeAiWorkflow.js +9 -3
- package/dist/mcp/tools/createWorkflow.js +3 -4
- package/dist/mcp/tools/validateWorkflow.js +3 -3
- package/dist/nodes/cache.js +19 -1
- package/dist/nodes/versioning.js +22 -0
- package/dist/utils/constants.js +1 -1
- package/dist/utils/nodesDb.js +44 -8
- package/dist/utils/workspace.js +1 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -155,20 +155,55 @@ Alternatively, you can set up the MCP server through Cursor's interface:
|
|
|
155
155
|
|
|
156
156
|
The server provides the following tools for working with n8n workflows:
|
|
157
157
|
|
|
158
|
+
### Core Workflow Management
|
|
159
|
+
|
|
158
160
|
| Tool Name | Description | Key Parameters |
|
|
159
161
|
|-----------|-------------|----------------|
|
|
160
162
|
| **create_workflow** | Create a new n8n workflow | `workflow_name`, `workspace_dir` |
|
|
161
163
|
| **list_workflows** | List workflows in the workspace | `limit` (optional), `cursor` (optional) |
|
|
162
164
|
| **get_workflow_details** | Get detailed information about a specific workflow | `workflow_name`, `workflow_path` (optional) |
|
|
165
|
+
| **validate_workflow** | Validate a workflow file against node schemas and connectivity | `workflow_name`, `workflow_path` (optional) |
|
|
166
|
+
|
|
167
|
+
### Node Management
|
|
168
|
+
|
|
169
|
+
| Tool Name | Description | Key Parameters |
|
|
170
|
+
|-----------|-------------|----------------|
|
|
163
171
|
| **add_node** | Add a new node to a workflow | `workflow_name`, `node_type`, `position` (optional), `parameters` (optional), `node_name` (optional), `typeVersion` (optional), `webhookId` (optional), `workflow_path` (optional), `connect_from` (optional), `connect_to` (optional) |
|
|
164
172
|
| **edit_node** | Edit an existing node in a workflow | `workflow_name`, `node_id`, `node_type` (optional), `node_name` (optional), `position` (optional), `parameters` (optional), `typeVersion` (optional), `webhookId` (optional), `workflow_path` (optional), `connect_from` (optional), `connect_to` (optional) |
|
|
165
173
|
| **delete_node** | Delete a node from a workflow | `workflow_name`, `node_id`, `workflow_path` (optional) |
|
|
174
|
+
| **list_available_nodes** | List available node types with optional filtering. Supports tag-style synonyms and multi-token OR/AND logic | `search_term` (optional), `n8n_version` (optional), `limit` (optional), `cursor` (optional), `tags` (optional, default: true), `token_logic` (optional: 'or' default, or 'and') |
|
|
175
|
+
|
|
176
|
+
### Connection Management
|
|
177
|
+
|
|
178
|
+
| Tool Name | Description | Key Parameters |
|
|
179
|
+
|-----------|-------------|----------------|
|
|
166
180
|
| **add_connection** | Create a connection between two nodes | `workflow_name`, `source_node_id`, `source_node_output_name`, `target_node_id`, `target_node_input_name`, `target_node_input_index` (optional), `workflow_path` (optional) |
|
|
167
181
|
| **add_ai_connections** | Wire AI model, tools, and memory to an agent | `workflow_name`, `agent_node_id`, `model_node_id` (optional), `tool_node_ids` (optional), `memory_node_id` (optional), `embeddings_node_id` (optional), `vector_store_node_id` (optional), `vector_insert_node_id` (optional), `vector_tool_node_id` (optional), `workflow_path` (optional) |
|
|
182
|
+
| **connect_main_chain** | Build a minimal main path through AI workflow nodes (Trigger → Model → Memory → Embeddings → Doc Loader → Vector Store → Vector Tool → Agent) | `workflow_name`, `workflow_path` (optional), `dry_run` (optional), `idempotency_key` (optional) |
|
|
183
|
+
|
|
184
|
+
### Workflow Planning & Composition
|
|
185
|
+
|
|
186
|
+
| Tool Name | Description | Key Parameters |
|
|
187
|
+
|-----------|-------------|----------------|
|
|
188
|
+
| **plan_workflow** | Create a non-destructive plan (nodes and connections) to update a workflow. Does not write files | `workflow_name`, `target` (nodes, connections), `workspace_dir` (optional) |
|
|
189
|
+
| **review_workflow_plan** | Apply a plan in-memory and return validation errors, warnings, and suggested fixes. Does not write files | `workflow_name`, `plan`, `workflow_path` (optional) |
|
|
190
|
+
| **apply_workflow_plan** | Apply a previously reviewed plan to the workflow on disk (atomic write) | `workflow_name`, `plan`, `workflow_path` (optional) |
|
|
168
191
|
| **compose_ai_workflow** | Compose a complex AI workflow (agent + model + memory + embeddings + vector + tools + trigger) in one call, including wiring and basic validation | `workflow_name`, `plan`, `n8n_version` (optional) |
|
|
169
|
-
|
|
192
|
+
|
|
193
|
+
### Parameter Management
|
|
194
|
+
|
|
195
|
+
| Tool Name | Description | Key Parameters |
|
|
196
|
+
|-----------|-------------|----------------|
|
|
197
|
+
| **suggest_node_params** | Suggest minimal valid parameters for a node type using defaults and required fields | `node_type`, `typeVersion` (optional), `existing_parameters` (optional) |
|
|
198
|
+
| **list_missing_parameters** | List required parameters missing for a node considering visibility rules | `node_type`, `typeVersion` (optional), `parameters` |
|
|
199
|
+
| **fix_node_params** | Return parameters with defaults applied for required fields that are missing | `node_type`, `typeVersion` (optional), `parameters` (optional) |
|
|
200
|
+
|
|
201
|
+
### Templates & Discovery
|
|
202
|
+
|
|
203
|
+
| Tool Name | Description | Key Parameters |
|
|
204
|
+
|-----------|-------------|----------------|
|
|
205
|
+
| **list_template_examples** | List node usage examples extracted from free templates. Filter by node_type or template_name | `node_type` (optional), `template_name` (optional), `limit` (optional), `cursor` (optional) |
|
|
170
206
|
| **get_n8n_version_info** | Get current N8N version and capabilities | `random_string` |
|
|
171
|
-
| **validate_workflow** | Validate a workflow file against node schemas and connectivity | `workflow_name`, `workflow_path` (optional) |
|
|
172
207
|
|
|
173
208
|
### Validation behavior
|
|
174
209
|
|
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -209,7 +209,7 @@ server.tool("create_workflow", "Create a new n8n workflow", createWorkflowParams
|
|
|
209
209
|
id: (0, id_1.generateN8nId)(), // e.g., "Y6sBMxxyJQtgCCBQ"
|
|
210
210
|
nodes: [], // Initialize with empty nodes array
|
|
211
211
|
connections: {}, // Initialize with empty connections object
|
|
212
|
-
|
|
212
|
+
...(0, versioning_1.workflowActivationDefaults)(),
|
|
213
213
|
pinData: {},
|
|
214
214
|
settings: {
|
|
215
215
|
executionOrder: "v1"
|
|
@@ -220,9 +220,7 @@ server.tool("create_workflow", "Create a new n8n workflow", createWorkflowParams
|
|
|
220
220
|
},
|
|
221
221
|
tags: []
|
|
222
222
|
};
|
|
223
|
-
|
|
224
|
-
// For now, using directly. Consider a sanitization function for production.
|
|
225
|
-
const filename = `${workflowName.replace(/[^a-z0-9_.-]/gi, '_')}.json`;
|
|
223
|
+
const filename = `${(0, workspace_1.sanitizeFilename)(workflowName)}.json`;
|
|
226
224
|
const filePath = (0, workspace_1.resolvePath)(path_1.default.join(workspace_1.WORKFLOW_DATA_DIR_NAME, filename));
|
|
227
225
|
await promises_1.default.writeFile(filePath, JSON.stringify(newN8nWorkflow, null, 2));
|
|
228
226
|
console.error("[DEBUG] Workflow created and saved to:", filePath);
|
|
@@ -394,6 +392,15 @@ server.tool("add_node", "Add a node to an n8n workflow", addNodeParamsSchema.sha
|
|
|
394
392
|
// Normalize the node type and resolve to a compatible typeVersion automatically
|
|
395
393
|
const { finalNodeType, finalTypeVersion } = (0, cache_1.normalizeNodeTypeAndVersion)(params.node_type, params.typeVersion);
|
|
396
394
|
let resolvedVersion = finalTypeVersion;
|
|
395
|
+
const addNodeWarnings = [];
|
|
396
|
+
// Check if the node type exists in the loaded catalog at all
|
|
397
|
+
const knownInCatalog = (0, cache_1.getNodeInfoCache)().has(params.node_type.toLowerCase())
|
|
398
|
+
|| (0, cache_1.getNodeInfoCache)().has(finalNodeType.toLowerCase())
|
|
399
|
+
|| ((0, versioning_1.getN8nVersionInfo)()?.supportedNodes.has(finalNodeType) ?? false);
|
|
400
|
+
if (!knownInCatalog) {
|
|
401
|
+
addNodeWarnings.push(`Node type '${finalNodeType}' was not found in the n8n ${(0, versioning_1.getCurrentN8nVersion)() || 'unknown'} node catalog. ` +
|
|
402
|
+
`The node will be added but may not be valid. Use 'list_available_nodes' to find valid node types.`);
|
|
403
|
+
}
|
|
397
404
|
// Check if node type is supported in current N8N version
|
|
398
405
|
if (!(0, cache_1.isNodeTypeSupported)(finalNodeType, finalTypeVersion)) {
|
|
399
406
|
// Auto-heal: if the chosen version is not supported, try to downgrade to the highest supported one
|
|
@@ -586,11 +593,11 @@ server.tool("add_node", "Add a node to an n8n workflow", addNodeParamsSchema.sha
|
|
|
586
593
|
return { ok, errors: nodeErrors, warnings: nodeWarnings, nodeIssues };
|
|
587
594
|
};
|
|
588
595
|
const local = buildLocalValidation(report, newNode.name);
|
|
589
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, node: newNode, workflowId: workflow.id, createdConnections, localValidation: local }) }] };
|
|
596
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, node: newNode, workflowId: workflow.id, createdConnections, localValidation: local, ...(addNodeWarnings.length > 0 ? { warnings: addNodeWarnings } : {}) }) }] };
|
|
590
597
|
}
|
|
591
598
|
catch (e) {
|
|
592
599
|
console.warn('[WARN] Validation step errored after add_node:', e?.message || e);
|
|
593
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, node: newNode, workflowId: workflow.id, createdConnections }) }] };
|
|
600
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, node: newNode, workflowId: workflow.id, createdConnections, ...(addNodeWarnings.length > 0 ? { warnings: addNodeWarnings } : {}) }) }] };
|
|
594
601
|
}
|
|
595
602
|
}
|
|
596
603
|
catch (error) {
|
|
@@ -1490,7 +1497,7 @@ server.tool("compose_ai_workflow", "Compose a complex AI workflow (agent + model
|
|
|
1490
1497
|
try {
|
|
1491
1498
|
// Step 1: ensure workflow exists (create if missing)
|
|
1492
1499
|
await (0, workspace_1.ensureWorkflowDir)();
|
|
1493
|
-
const filePath = (0, workspace_1.resolvePath)(path_1.default.join(workspace_1.WORKFLOW_DATA_DIR_NAME, `${
|
|
1500
|
+
const filePath = (0, workspace_1.resolvePath)(path_1.default.join(workspace_1.WORKFLOW_DATA_DIR_NAME, `${(0, workspace_1.sanitizeFilename)(workflow_name)}.json`));
|
|
1494
1501
|
let workflow;
|
|
1495
1502
|
try {
|
|
1496
1503
|
const raw = await promises_1.default.readFile(filePath, 'utf8');
|
|
@@ -1498,10 +1505,14 @@ server.tool("compose_ai_workflow", "Compose a complex AI workflow (agent + model
|
|
|
1498
1505
|
}
|
|
1499
1506
|
catch (e) {
|
|
1500
1507
|
// Create minimal workflow if missing
|
|
1501
|
-
workflow = { name: workflow_name, id: (0, id_1.generateUUID)(), nodes: [], connections: {},
|
|
1508
|
+
workflow = { name: workflow_name, id: (0, id_1.generateUUID)(), nodes: [], connections: {}, ...(0, versioning_1.workflowActivationDefaults)(), pinData: {}, settings: { executionOrder: 'v1' }, versionId: (0, id_1.generateUUID)(), meta: { instanceId: (0, id_1.generateUUID)() }, tags: [] };
|
|
1502
1509
|
}
|
|
1503
|
-
// Helper to add a node with normalization
|
|
1510
|
+
// Helper to add a node with normalization. Idempotent: if a node with
|
|
1511
|
+
// the same name exists, return it instead of duplicating.
|
|
1504
1512
|
const addNode = async (nodeType, nodeName, parameters, position) => {
|
|
1513
|
+
const existing = workflow.nodes.find(n => n.name === nodeName);
|
|
1514
|
+
if (existing)
|
|
1515
|
+
return existing;
|
|
1505
1516
|
const { finalNodeType, finalTypeVersion } = (0, cache_1.normalizeNodeTypeAndVersion)(nodeType);
|
|
1506
1517
|
const node = {
|
|
1507
1518
|
id: (0, id_1.generateN8nId)(),
|
|
@@ -1530,11 +1541,19 @@ server.tool("compose_ai_workflow", "Compose a complex AI workflow (agent + model
|
|
|
1530
1541
|
const vtool = plan.vector_tool ? await addNode(plan.vector_tool.node_type, plan.vector_tool.node_name, plan.vector_tool.parameters, positions.vtool) : null;
|
|
1531
1542
|
const extraTools = [];
|
|
1532
1543
|
if (Array.isArray(plan.tools)) {
|
|
1533
|
-
|
|
1534
|
-
|
|
1544
|
+
let toolX = 640;
|
|
1545
|
+
for (const t of plan.tools) {
|
|
1546
|
+
extraTools.push(await addNode(t.node_type, t.node_name || t.node_type.split('.').pop() || 'Tool', t.parameters, { x: toolX, y: 240 }));
|
|
1547
|
+
toolX += 200;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
// Auto-create toolVectorStore bridge when vector_store exists but vector_tool wasn't provided
|
|
1551
|
+
let effectiveVtool = vtool;
|
|
1552
|
+
if (vstore && !vtool && agent) {
|
|
1553
|
+
effectiveVtool = await addNode('@n8n/n8n-nodes-langchain.toolVectorStore', 'Vector Store Tool', undefined, positions.vtool);
|
|
1535
1554
|
}
|
|
1536
1555
|
// Step 3: wire standard connections
|
|
1537
|
-
const toolIds = [...(
|
|
1556
|
+
const toolIds = [...(effectiveVtool ? [effectiveVtool.id] : []), ...extraTools.map(t => t.id)];
|
|
1538
1557
|
await promises_1.default.writeFile(filePath, JSON.stringify(workflow, null, 2));
|
|
1539
1558
|
// Re-load to use shared connection routine
|
|
1540
1559
|
const res = await (async () => {
|
|
@@ -1595,9 +1614,13 @@ server.tool("compose_ai_workflow", "Compose a complex AI workflow (agent + model
|
|
|
1595
1614
|
await connect(memory, 'ai_memory', agent, 'ai_memory');
|
|
1596
1615
|
await connect(embeddings, 'ai_embeddings', vstore, 'ai_embeddings');
|
|
1597
1616
|
await connect(vstore, 'ai_document', vinsert, 'ai_document');
|
|
1598
|
-
await connect(vstore, 'ai_vectorStore',
|
|
1599
|
-
await connect(model, 'ai_languageModel',
|
|
1600
|
-
await connect(
|
|
1617
|
+
await connect(vstore, 'ai_vectorStore', effectiveVtool, 'ai_vectorStore');
|
|
1618
|
+
await connect(model, 'ai_languageModel', effectiveVtool, 'ai_languageModel');
|
|
1619
|
+
await connect(effectiveVtool, 'ai_tool', agent, 'ai_tool');
|
|
1620
|
+
// Wire extra tools (from plan.tools[]) to agent
|
|
1621
|
+
for (const tool of extraTools) {
|
|
1622
|
+
await connect(tool, 'ai_tool', agent, 'ai_tool');
|
|
1623
|
+
}
|
|
1601
1624
|
await connect(trigger, 'main', agent, 'main');
|
|
1602
1625
|
return { success: true };
|
|
1603
1626
|
})();
|
|
@@ -2123,9 +2146,9 @@ server.tool("validate_workflow", "Validate a workflow file against known node sc
|
|
|
2123
2146
|
}
|
|
2124
2147
|
}
|
|
2125
2148
|
}
|
|
2126
|
-
//
|
|
2127
|
-
|
|
2128
|
-
const targetSet =
|
|
2149
|
+
// Use extended reachability: nodes attached via AI handles (ai_languageModel,
|
|
2150
|
+
// ai_memory, ai_embeddings, etc.) are considered part of the main chain.
|
|
2151
|
+
const targetSet = reachableExtended;
|
|
2129
2152
|
// Any enabled node not in targetSet is disconnected from main chain → error
|
|
2130
2153
|
for (const [name, node] of Object.entries(nodesByName)) {
|
|
2131
2154
|
if (node.disabled === true)
|
|
@@ -2508,7 +2531,7 @@ server.tool("review_workflow_plan", "Apply a plan in-memory and return validatio
|
|
|
2508
2531
|
id: (0, id_1.generateN8nId)(),
|
|
2509
2532
|
nodes: [],
|
|
2510
2533
|
connections: {},
|
|
2511
|
-
|
|
2534
|
+
...(0, versioning_1.workflowActivationDefaults)(),
|
|
2512
2535
|
pinData: {},
|
|
2513
2536
|
settings: { executionOrder: "v1" },
|
|
2514
2537
|
versionId: (0, id_1.generateUUID)(),
|
|
@@ -2605,7 +2628,7 @@ server.tool("apply_workflow_plan", "Apply a previously reviewed plan to the work
|
|
|
2605
2628
|
id: (0, id_1.generateN8nId)(),
|
|
2606
2629
|
nodes: [],
|
|
2607
2630
|
connections: {},
|
|
2608
|
-
|
|
2631
|
+
...(0, versioning_1.workflowActivationDefaults)(),
|
|
2609
2632
|
pinData: {},
|
|
2610
2633
|
settings: { executionOrder: "v1" },
|
|
2611
2634
|
versionId: (0, id_1.generateUUID)(),
|
|
@@ -2707,7 +2730,7 @@ server.tool("list_missing_parameters", "List required parameters missing for a n
|
|
|
2707
2730
|
id: (0, id_1.generateN8nId)(),
|
|
2708
2731
|
nodes: [{ id: nodeId, name: nodeName, type: finalNodeType, typeVersion: finalTypeVersion, position: [200, 200], parameters: params.parameters }],
|
|
2709
2732
|
connections: {},
|
|
2710
|
-
|
|
2733
|
+
...(0, versioning_1.workflowActivationDefaults)(),
|
|
2711
2734
|
pinData: {},
|
|
2712
2735
|
settings: { executionOrder: "v1" },
|
|
2713
2736
|
versionId: (0, id_1.generateUUID)(),
|
|
@@ -2739,7 +2762,7 @@ server.tool("fix_node_params", "Return parameters with defaults applied for requ
|
|
|
2739
2762
|
const nodeName = (finalNodeType.split('.').pop() || 'Node') + ' 1';
|
|
2740
2763
|
const provisional = {
|
|
2741
2764
|
name: 'tmp', id: (0, id_1.generateN8nId)(), nodes: [{ id: nodeId, name: nodeName, type: finalNodeType, typeVersion: finalTypeVersion, position: [200, 200], parameters: merged }],
|
|
2742
|
-
connections: {},
|
|
2765
|
+
connections: {}, ...(0, versioning_1.workflowActivationDefaults)(), pinData: {}, settings: { executionOrder: "v1" }, versionId: (0, id_1.generateUUID)(), meta: { instanceId: (0, id_1.generateInstanceId)() }, tags: []
|
|
2743
2766
|
};
|
|
2744
2767
|
const nodeTypes = await (0, nodeTypesLoader_1.loadNodeTypesForCurrentVersion)(path_1.default.resolve(__dirname, '../workflow_nodes'), (0, versioning_1.getCurrentN8nVersion)());
|
|
2745
2768
|
const report = (0, workflowValidator_1.validateAndNormalizeWorkflow)(provisional, nodeTypes);
|
|
@@ -48,7 +48,7 @@ async function handler(params, _extra) {
|
|
|
48
48
|
try {
|
|
49
49
|
// Step 1: ensure workflow exists (create if missing)
|
|
50
50
|
await (0, workspace_1.ensureWorkflowDir)();
|
|
51
|
-
const filePath = (0, workspace_1.
|
|
51
|
+
const filePath = (0, workspace_1.resolveWorkflowPath)(workflow_name);
|
|
52
52
|
let workflow;
|
|
53
53
|
try {
|
|
54
54
|
const raw = await promises_1.default.readFile(filePath, 'utf8');
|
|
@@ -56,10 +56,16 @@ async function handler(params, _extra) {
|
|
|
56
56
|
}
|
|
57
57
|
catch (e) {
|
|
58
58
|
// Create minimal workflow if missing
|
|
59
|
-
workflow = { name: workflow_name, id: (0, id_1.generateUUID)(), nodes: [], connections: {},
|
|
59
|
+
workflow = { name: workflow_name, id: (0, id_1.generateUUID)(), nodes: [], connections: {}, ...(0, versioning_1.workflowActivationDefaults)(), pinData: {}, settings: { executionOrder: 'v1' }, versionId: (0, id_1.generateUUID)(), meta: { instanceId: (0, id_1.generateUUID)() }, tags: [] };
|
|
60
60
|
}
|
|
61
|
-
// Helper to add a node with normalization
|
|
61
|
+
// Helper to add a node with normalization. Idempotent: if a node with the
|
|
62
|
+
// same name already exists in the workflow, return it instead of creating a
|
|
63
|
+
// duplicate. This prevents triplication when compose is called multiple times
|
|
64
|
+
// on the same workflow (e.g., LLM retries).
|
|
62
65
|
const addNode = async (nodeType, nodeName, parameters, position) => {
|
|
66
|
+
const existing = workflow.nodes.find(n => n.name === nodeName);
|
|
67
|
+
if (existing)
|
|
68
|
+
return existing;
|
|
63
69
|
const { finalNodeType, finalTypeVersion } = (0, cache_1.normalizeNodeTypeAndVersion)(nodeType);
|
|
64
70
|
const node = {
|
|
65
71
|
id: (0, id_1.generateN8nId)(),
|
|
@@ -10,6 +10,7 @@ const promises_1 = __importDefault(require("fs/promises"));
|
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const workspace_1 = require("../../utils/workspace");
|
|
12
12
|
const id_1 = require("../../utils/id");
|
|
13
|
+
const versioning_1 = require("../../nodes/versioning");
|
|
13
14
|
const constants_1 = require("../../utils/constants");
|
|
14
15
|
// Schema definition
|
|
15
16
|
exports.paramsSchema = zod_1.z.object({
|
|
@@ -51,7 +52,7 @@ async function handler(params, _extra) {
|
|
|
51
52
|
id: (0, id_1.generateN8nId)(), // e.g., "Y6sBMxxyJQtgCCBQ"
|
|
52
53
|
nodes: [], // Initialize with empty nodes array
|
|
53
54
|
connections: {}, // Initialize with empty connections object
|
|
54
|
-
|
|
55
|
+
...(0, versioning_1.workflowActivationDefaults)(),
|
|
55
56
|
pinData: {},
|
|
56
57
|
settings: {
|
|
57
58
|
executionOrder: "v1"
|
|
@@ -62,9 +63,7 @@ async function handler(params, _extra) {
|
|
|
62
63
|
},
|
|
63
64
|
tags: []
|
|
64
65
|
};
|
|
65
|
-
|
|
66
|
-
// For now, using directly. Consider a sanitization function for production.
|
|
67
|
-
const filename = `${workflowName.replace(/[^a-z0-9_.-]/gi, '_')}.json`;
|
|
66
|
+
const filename = `${(0, workspace_1.sanitizeFilename)(workflowName)}.json`;
|
|
68
67
|
const filePath = (0, workspace_1.resolvePath)(path_1.default.join(workspace_1.WORKFLOW_DATA_DIR_NAME, filename));
|
|
69
68
|
await promises_1.default.writeFile(filePath, JSON.stringify(newN8nWorkflow, null, 2));
|
|
70
69
|
console.error("[DEBUG] Workflow created and saved to:", filePath);
|
|
@@ -179,9 +179,9 @@ async function handler(params, _extra) {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
const targetSet =
|
|
182
|
+
// Use extended reachability: nodes attached via AI handles (ai_languageModel,
|
|
183
|
+
// ai_memory, ai_embeddings, etc.) are considered part of the main chain.
|
|
184
|
+
const targetSet = reachableExtended;
|
|
185
185
|
// Any enabled node not in targetSet is disconnected from main chain → error
|
|
186
186
|
for (const [name, node] of Object.entries(nodesByName)) {
|
|
187
187
|
if (node.disabled === true)
|
package/dist/nodes/cache.js
CHANGED
|
@@ -63,7 +63,9 @@ function normalizeNodeTypeAndVersion(inputType, inputVersion) {
|
|
|
63
63
|
return { finalNodeType, finalTypeVersion };
|
|
64
64
|
}
|
|
65
65
|
async function loadKnownNodeBaseTypes() {
|
|
66
|
-
|
|
66
|
+
// When compiled, this file lives in dist/nodes/cache.js — go up two levels
|
|
67
|
+
// to reach the project root, matching the path in versioning.ts.
|
|
68
|
+
const workflowNodesDir = path_1.default.resolve(__dirname, '../../workflow_nodes');
|
|
67
69
|
try {
|
|
68
70
|
const entries = await promises_1.default.readdir(workflowNodesDir, { withFileTypes: true });
|
|
69
71
|
const versionDirs = entries.filter(entry => entry.isDirectory()).map(entry => entry.name);
|
|
@@ -143,12 +145,28 @@ async function loadNodesFromDirectory(directory) {
|
|
|
143
145
|
.filter((v) => !Number.isNaN(v))
|
|
144
146
|
: (typeof rawVersion === 'number' ? rawVersion : parseFloat(String(rawVersion)));
|
|
145
147
|
nodeInfoCache.set(officialType.toLowerCase(), { officialType, version: normalizedVersion });
|
|
148
|
+
// Register short-name aliases so users can type just "chatTrigger"
|
|
149
|
+
// instead of the full "@n8n/n8n-nodes-langchain.chatTrigger".
|
|
146
150
|
const prefix = "n8n-nodes-base.";
|
|
151
|
+
const langchainPrefix = "@n8n/n8n-nodes-langchain.";
|
|
147
152
|
if (officialType.startsWith(prefix)) {
|
|
148
153
|
const baseName = officialType.substring(prefix.length);
|
|
149
154
|
if (baseName)
|
|
150
155
|
nodeInfoCache.set(baseName.toLowerCase(), { officialType, version: normalizedVersion });
|
|
151
156
|
}
|
|
157
|
+
else if (officialType.startsWith(langchainPrefix)) {
|
|
158
|
+
const baseName = officialType.substring(langchainPrefix.length);
|
|
159
|
+
if (baseName) {
|
|
160
|
+
nodeInfoCache.set(baseName.toLowerCase(), { officialType, version: normalizedVersion });
|
|
161
|
+
// LLMs often guess "httpRequestTool" instead of "toolHttpRequest".
|
|
162
|
+
// Register reversed alias: toolFoo → fooTool.
|
|
163
|
+
if (baseName.startsWith('tool') && baseName.length > 4) {
|
|
164
|
+
const inner = baseName.substring(4); // e.g. "HttpRequest"
|
|
165
|
+
const reversed = inner.charAt(0).toLowerCase() + inner.slice(1) + 'Tool'; // "httpRequestTool"
|
|
166
|
+
nodeInfoCache.set(reversed.toLowerCase(), { officialType, version: normalizedVersion });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
152
170
|
}
|
|
153
171
|
}
|
|
154
172
|
catch {
|
package/dist/nodes/versioning.js
CHANGED
|
@@ -4,6 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getCurrentN8nVersion = getCurrentN8nVersion;
|
|
7
|
+
exports.getN8nMajorVersion = getN8nMajorVersion;
|
|
8
|
+
exports.isN8n2xOrLater = isN8n2xOrLater;
|
|
9
|
+
exports.workflowActivationDefaults = workflowActivationDefaults;
|
|
7
10
|
exports.getN8nVersionInfo = getN8nVersionInfo;
|
|
8
11
|
exports.getSupportedN8nVersions = getSupportedN8nVersions;
|
|
9
12
|
exports.initializeN8nVersionSupport = initializeN8nVersionSupport;
|
|
@@ -20,6 +23,25 @@ const N8N_API_URL = process.env.N8N_API_URL;
|
|
|
20
23
|
function getCurrentN8nVersion() {
|
|
21
24
|
return currentN8nVersion;
|
|
22
25
|
}
|
|
26
|
+
function getN8nMajorVersion() {
|
|
27
|
+
const v = currentN8nVersion;
|
|
28
|
+
if (!v)
|
|
29
|
+
return null;
|
|
30
|
+
const major = parseInt(v.replace(/^v/i, '').split('.')[0], 10);
|
|
31
|
+
return Number.isFinite(major) ? major : null;
|
|
32
|
+
}
|
|
33
|
+
function isN8n2xOrLater() {
|
|
34
|
+
const major = getN8nMajorVersion();
|
|
35
|
+
return major !== null && major >= 2;
|
|
36
|
+
}
|
|
37
|
+
// n8n 1.x needs `active: false` on new workflow JSON. n8n 2.x replaced this
|
|
38
|
+
// with server-side Draft/Published state and rejects the field. Spread this
|
|
39
|
+
// helper into new workflow objects so the shape adapts to the detected
|
|
40
|
+
// instance. Callers on older versions or with no detection still get 1.x
|
|
41
|
+
// behavior, which is the safe default.
|
|
42
|
+
function workflowActivationDefaults() {
|
|
43
|
+
return isN8n2xOrLater() ? {} : { active: false };
|
|
44
|
+
}
|
|
23
45
|
function getN8nVersionInfo() {
|
|
24
46
|
return n8nVersionInfo;
|
|
25
47
|
}
|
package/dist/utils/constants.js
CHANGED
|
@@ -17,7 +17,7 @@ exports.MCP_SERVER_VERSION = packageVersion;
|
|
|
17
17
|
exports.MCP_PROTOCOL_VERSION = '1.0';
|
|
18
18
|
exports.HEADER_MCP_VERSION = 'X-MCP-Version';
|
|
19
19
|
// Keep a single fallback for n8n versioning logic if ever needed by callers
|
|
20
|
-
exports.DEFAULT_N8N_VERSION_FALLBACK = '1.
|
|
20
|
+
exports.DEFAULT_N8N_VERSION_FALLBACK = '1.123.2';
|
|
21
21
|
// Optional: canonical tool names to avoid typos
|
|
22
22
|
exports.ToolNames = {
|
|
23
23
|
create_workflow: 'create_workflow',
|
package/dist/utils/nodesDb.js
CHANGED
|
@@ -8,24 +8,33 @@ exports.listDbVersions = listDbVersions;
|
|
|
8
8
|
exports.materializeBestVersion = materializeBestVersion;
|
|
9
9
|
exports.materializeIfConfigured = materializeIfConfigured;
|
|
10
10
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
12
|
const path_1 = __importDefault(require("path"));
|
|
12
13
|
const os_1 = __importDefault(require("os"));
|
|
14
|
+
const zlib_1 = __importDefault(require("zlib"));
|
|
13
15
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
14
16
|
const versioning_1 = require("../nodes/versioning");
|
|
15
17
|
/**
|
|
16
18
|
* Environment helpers for nodes DB and cache settings.
|
|
17
19
|
*/
|
|
20
|
+
function getBundledDbPath() {
|
|
21
|
+
// Bundled catalog shipped inside the package next to compiled output (dist/catalog.sqlite).
|
|
22
|
+
const candidate = path_1.default.resolve(__dirname, '..', 'catalog.sqlite');
|
|
23
|
+
return fs_1.default.existsSync(candidate) ? candidate : null;
|
|
24
|
+
}
|
|
18
25
|
function getNodesDbPath() {
|
|
19
26
|
const envPath = process.env.N8N_NODES_DB_PATH?.trim();
|
|
20
27
|
if (envPath && envPath.length > 0) {
|
|
21
28
|
return path_1.default.resolve(envPath);
|
|
22
29
|
}
|
|
23
|
-
// Default under user cache dir
|
|
24
30
|
const home = os_1.default.homedir();
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
if (home) {
|
|
32
|
+
const userDb = path_1.default.join(home, '.cache', 'n8n-nodes', 'catalog.sqlite');
|
|
33
|
+
if (fs_1.default.existsSync(userDb))
|
|
34
|
+
return userDb;
|
|
35
|
+
}
|
|
36
|
+
// Fall back to catalog bundled inside the npm package.
|
|
37
|
+
return getBundledDbPath();
|
|
29
38
|
}
|
|
30
39
|
function getWorkflowNodesRootDir() {
|
|
31
40
|
// Existing code expects workflow_nodes relative to compiled dist path
|
|
@@ -48,10 +57,37 @@ function readVersionMeta(db, version) {
|
|
|
48
57
|
return null;
|
|
49
58
|
}
|
|
50
59
|
}
|
|
60
|
+
function tableHasColumn(db, table, column) {
|
|
61
|
+
try {
|
|
62
|
+
const info = db.prepare(`PRAGMA table_info('${table}')`).all();
|
|
63
|
+
return info.some(r => r.name === column);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
51
69
|
function* iterateNodes(db, version) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
// New schema (dedup+gzip): nodes joins blobs via sha256; blob stores gzipped JSON.
|
|
71
|
+
// Legacy schema: nodes has a raw TEXT column. Support both for forward compatibility.
|
|
72
|
+
const legacy = tableHasColumn(db, 'nodes', 'raw');
|
|
73
|
+
const sql = legacy
|
|
74
|
+
? 'SELECT id, version, nodeType, baseName, typeVersion, filename, raw FROM nodes WHERE version = ?'
|
|
75
|
+
: 'SELECT n.id, n.version, n.nodeType, n.baseName, n.typeVersion, n.filename, b.gz FROM nodes n JOIN blobs b ON b.sha256 = n.sha256 WHERE n.version = ?';
|
|
76
|
+
const stmt = db.prepare(sql);
|
|
77
|
+
for (const raw of stmt.iterate(version)) {
|
|
78
|
+
const row = raw;
|
|
79
|
+
const rawText = legacy
|
|
80
|
+
? (row.raw ?? '')
|
|
81
|
+
: zlib_1.default.gunzipSync(row.gz).toString('utf8');
|
|
82
|
+
yield {
|
|
83
|
+
id: row.id,
|
|
84
|
+
version: row.version,
|
|
85
|
+
nodeType: row.nodeType,
|
|
86
|
+
baseName: row.baseName,
|
|
87
|
+
typeVersion: row.typeVersion,
|
|
88
|
+
filename: row.filename,
|
|
89
|
+
raw: rawText,
|
|
90
|
+
};
|
|
55
91
|
}
|
|
56
92
|
}
|
|
57
93
|
async function directoryExists(dir) {
|
package/dist/utils/workspace.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.PathSecurityError = exports.WORKFLOWS_FILE_NAME = exports.WORKFLOW_DATA_DIR_NAME = void 0;
|
|
7
7
|
exports.getWorkspaceDir = getWorkspaceDir;
|
|
8
8
|
exports.setWorkspaceDir = setWorkspaceDir;
|
|
9
|
+
exports.sanitizeFilename = sanitizeFilename;
|
|
9
10
|
exports.resolvePath = resolvePath;
|
|
10
11
|
exports.resolveWorkflowPath = resolveWorkflowPath;
|
|
11
12
|
exports.ensureWorkflowParentDir = ensureWorkflowParentDir;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-workflow-builder-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "MCP server for building n8n workflows",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/",
|
|
11
|
+
"dist/catalog.sqlite",
|
|
11
12
|
"README.md",
|
|
12
13
|
"LICENSE"
|
|
13
14
|
],
|
|
@@ -24,9 +25,9 @@
|
|
|
24
25
|
"test:langchain": "mocha tests/unit/langchain-llm-format.test.js",
|
|
25
26
|
"mcp": "node n8n-workflow-mcp.js",
|
|
26
27
|
"build": "tsc",
|
|
27
|
-
"postbuild": "chmod +x dist/index.js",
|
|
28
|
-
"build:nodes-db": "node scripts/build-nodes-db.js --source ./
|
|
29
|
-
"rebuild:nodes-db": "node scripts/build-nodes-db.js --source ./
|
|
28
|
+
"postbuild": "chmod +x dist/index.js && node scripts/bundle-nodes-db.js",
|
|
29
|
+
"build:nodes-db": "node scripts/build-nodes-db.js --source ./workflow_nodes",
|
|
30
|
+
"rebuild:nodes-db": "node scripts/build-nodes-db.js --source ./workflow_nodes --full-rebuild"
|
|
30
31
|
},
|
|
31
32
|
"keywords": [
|
|
32
33
|
"n8n",
|