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 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
- | **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') |
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
- active: false,
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
- // Sanitize workflowName for filename or ensure it's safe.
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, `${workflow_name.replace(/[^a-z0-9_.-]/gi, '_')}.json`));
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: {}, active: false, pinData: {}, settings: { executionOrder: 'v1' }, versionId: (0, id_1.generateUUID)(), meta: { instanceId: (0, id_1.generateUUID)() }, tags: [] };
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
- for (const t of plan.tools)
1534
- extraTools.push(await addNode(t.node_type, t.node_name || t.node_type.split('.').pop() || 'Tool', t.parameters));
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 = [...(vtool ? [vtool.id] : []), ...extraTools.map(t => t.id)];
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', vtool, 'ai_vectorStore');
1599
- await connect(model, 'ai_languageModel', vtool, 'ai_languageModel');
1600
- await connect(vtool, 'ai_tool', agent, 'ai_tool');
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
- // Always strict now; multiple chains are not allowed
2127
- const strictMainOnly = true;
2128
- const targetSet = strictMainOnly ? reachableMain : reachableExtended;
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
- active: false,
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
- active: false,
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
- active: false,
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: {}, active: false, pinData: {}, settings: { executionOrder: "v1" }, versionId: (0, id_1.generateUUID)(), meta: { instanceId: (0, id_1.generateInstanceId)() }, tags: []
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.resolvePath)(path_1.default.join(workspace_1.WORKFLOW_DATA_DIR_NAME, `${workflow_name.replace(/[^a-z0-9_.-]/gi, '_')}.json`));
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: {}, active: false, pinData: {}, settings: { executionOrder: 'v1' }, versionId: (0, id_1.generateUUID)(), meta: { instanceId: (0, id_1.generateUUID)() }, tags: [] };
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
- active: false,
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
- // Sanitize workflowName for filename or ensure it's safe.
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
- // Always strict now; multiple chains are not allowed
183
- const strictMainOnly = true;
184
- const targetSet = strictMainOnly ? reachableMain : reachableExtended;
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)
@@ -63,7 +63,9 @@ function normalizeNodeTypeAndVersion(inputType, inputVersion) {
63
63
  return { finalNodeType, finalTypeVersion };
64
64
  }
65
65
  async function loadKnownNodeBaseTypes() {
66
- const workflowNodesDir = path_1.default.resolve(__dirname, '../workflow_nodes');
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 {
@@ -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
  }
@@ -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.104.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',
@@ -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 (!home)
26
- return null;
27
- const defaultDir = path_1.default.join(home, '.cache', 'n8n-nodes');
28
- return path_1.default.join(defaultDir, 'catalog.sqlite');
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
- const stmt = db.prepare('SELECT id, version, nodeType, baseName, typeVersion, filename, raw FROM nodes WHERE version = ?');
53
- for (const row of stmt.iterate(version)) {
54
- yield row;
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) {
@@ -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": "1.0.2",
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 ./node_definitions",
29
- "rebuild:nodes-db": "node scripts/build-nodes-db.js --source ./node_definitions --full-rebuild"
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",