feat: add n8n_generate_workflow tool for hosted workflow generation

Add new MCP tool that enables AI-powered workflow generation from natural
language descriptions. Uses handler delegation pattern — hosting environments
inject a GenerateWorkflowHandler via EngineOptions, self-hosted instances
receive a hosted-only informational response.

Handler flows through N8NMCPEngine → SingleSessionHTTPServer →
N8NDocumentationMCPServer with helpers for createWorkflow, validateWorkflow,
autofixWorkflow, and getWorkflow.

Includes full tool documentation, tests, and corrected tools overview count.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2026-03-31 12:32:31 +02:00
parent 3417c6701c
commit 198e773bb3
81 changed files with 784 additions and 177 deletions

186
dist/mcp/server.js vendored
View File

@@ -72,7 +72,7 @@ const protocol_version_1 = require("../utils/protocol-version");
const telemetry_1 = require("../telemetry");
const startup_checkpoints_1 = require("../telemetry/startup-checkpoints");
class N8NDocumentationMCPServer {
constructor(instanceContext, earlyLogger) {
constructor(instanceContext, earlyLogger, options) {
this.db = null;
this.repository = null;
this.templateService = null;
@@ -86,8 +86,10 @@ class N8NDocumentationMCPServer {
this.sharedDbState = null;
this.isShutdown = false;
this.dbHealthChecked = false;
this.workflowPatternsCache = null;
this.instanceContext = instanceContext;
this.earlyLogger = earlyLogger || null;
this.generateWorkflowHandler = options?.generateWorkflowHandler;
const envDbPath = process.env.NODE_DB_PATH;
let dbPath = null;
let possiblePaths = [];
@@ -499,6 +501,9 @@ class N8NDocumentationMCPServer {
}
}
processedArgs = this.coerceStringifiedJsonParams(name, processedArgs);
if (processedArgs) {
processedArgs = JSON.parse(JSON.stringify(processedArgs));
}
try {
logger_1.logger.debug(`Executing tool: ${name}`, { args: processedArgs });
const startTime = Date.now();
@@ -931,6 +936,7 @@ class N8NDocumentationMCPServer {
return this.searchNodes(args.query, limit, {
mode: args.mode,
includeExamples: args.includeExamples,
includeOperations: args.includeOperations,
source: args.source
});
case 'get_node':
@@ -1022,6 +1028,8 @@ class N8NDocumentationMCPServer {
requiredService: args.requiredService,
targetAudience: args.targetAudience
}, searchLimit, searchOffset);
case 'patterns':
return this.getWorkflowPatterns(args.task, searchLimit);
case 'keyword':
default:
if (!args.query) {
@@ -1132,6 +1140,44 @@ class N8NDocumentationMCPServer {
throw new Error(`Unknown action: ${dtAction}. Valid actions: createTable, listTables, getTable, updateTable, deleteTable, getRows, insertRows, updateRows, upsertRows, deleteRows`);
}
}
case 'n8n_generate_workflow': {
this.validateToolParams(name, args, ['description']);
if (this.generateWorkflowHandler && this.instanceContext) {
await this.ensureInitialized();
if (!this.repository) {
throw new Error('Repository not initialized');
}
const repo = this.repository;
const ctx = this.instanceContext;
const helpers = {
createWorkflow: (wfArgs) => n8nHandlers.handleCreateWorkflow(wfArgs, ctx),
validateWorkflow: (id) => n8nHandlers.handleValidateWorkflow({ id }, repo, ctx),
autofixWorkflow: (id) => n8nHandlers.handleAutofixWorkflow({ id }, repo, ctx),
getWorkflow: (id) => n8nHandlers.handleGetWorkflow({ id }, ctx),
};
try {
const result = await this.generateWorkflowHandler({ description: args.description, skip_cache: args.skip_cache }, ctx, helpers);
return result ?? { success: false, error: 'Handler returned no result' };
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { success: false, error: message };
}
}
return {
hosted_only: true,
message: 'The n8n_generate_workflow tool is available exclusively on the hosted version of n8n-mcp. ' +
'It uses AI to generate complete, validated n8n workflows from natural language descriptions.\n\n' +
'To access this feature:\n' +
'1. Register for free at https://dashboard.n8n-mcp.com\n' +
'2. Connect your n8n instance\n' +
'3. Use your hosted API key in your MCP client\n\n' +
'The hosted service includes:\n' +
'- 73,000+ pre-built workflow templates with instant deployment\n' +
'- AI-powered fresh generation for custom workflows\n' +
'- Automatic validation and error correction'
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
@@ -1270,7 +1316,7 @@ class N8NDocumentationMCPServer {
return { query, results: [], totalCount: 0 };
}
if (mode === 'FUZZY') {
return this.searchNodesFuzzy(cleanedQuery, limit);
return this.searchNodesFuzzy(cleanedQuery, limit, { includeOperations: options?.includeOperations });
}
let ftsQuery;
if (cleanedQuery.startsWith('"') && cleanedQuery.endsWith('"')) {
@@ -1338,7 +1384,7 @@ class N8NDocumentationMCPServer {
const hasHttpRequest = scoredNodes.some(n => n.node_type === 'nodes-base.httpRequest');
if (cleanedQuery.toLowerCase().includes('http') && !hasHttpRequest) {
logger_1.logger.debug('FTS missed HTTP Request node, augmenting with LIKE search');
return this.searchNodesLIKE(query, limit);
return this.searchNodesLIKE(query, limit, options);
}
const result = {
query,
@@ -1362,6 +1408,12 @@ class N8NDocumentationMCPServer {
nodeResult.npmDownloads = node.npm_downloads;
}
}
if (options?.includeOperations) {
const opsTree = this.buildOperationsTree(node.operations);
if (opsTree) {
nodeResult.operationsTree = opsTree;
}
}
return nodeResult;
}),
totalCount: scoredNodes.length
@@ -1412,7 +1464,7 @@ class N8NDocumentationMCPServer {
return this.searchNodesLIKE(query, limit);
}
}
async searchNodesFuzzy(query, limit) {
async searchNodesFuzzy(query, limit, options) {
if (!this.db)
throw new Error('Database not initialized');
const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 0);
@@ -1440,14 +1492,23 @@ class N8NDocumentationMCPServer {
return {
query,
mode: 'FUZZY',
results: matchingNodes.map(node => ({
nodeType: node.node_type,
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
displayName: node.display_name,
description: node.description,
category: node.category,
package: node.package_name
})),
results: matchingNodes.map(node => {
const nodeResult = {
nodeType: node.node_type,
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
displayName: node.display_name,
description: node.description,
category: node.category,
package: node.package_name
};
if (options?.includeOperations) {
const opsTree = this.buildOperationsTree(node.operations);
if (opsTree) {
nodeResult.operationsTree = opsTree;
}
}
return nodeResult;
}),
totalCount: matchingNodes.length
};
}
@@ -1568,6 +1629,12 @@ class N8NDocumentationMCPServer {
nodeResult.npmDownloads = node.npm_downloads;
}
}
if (options?.includeOperations) {
const opsTree = this.buildOperationsTree(node.operations);
if (opsTree) {
nodeResult.operationsTree = opsTree;
}
}
return nodeResult;
}),
totalCount: rankedNodes.length
@@ -1635,6 +1702,12 @@ class N8NDocumentationMCPServer {
nodeResult.npmDownloads = node.npm_downloads;
}
}
if (options?.includeOperations) {
const opsTree = this.buildOperationsTree(node.operations);
if (opsTree) {
nodeResult.operationsTree = opsTree;
}
}
return nodeResult;
}),
totalCount: rankedNodes.length
@@ -1938,6 +2011,47 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
})),
};
}
buildOperationsTree(operationsRaw) {
if (!operationsRaw)
return undefined;
let ops;
if (typeof operationsRaw === 'string') {
try {
ops = JSON.parse(operationsRaw);
}
catch {
return undefined;
}
}
else if (Array.isArray(operationsRaw)) {
ops = operationsRaw;
}
else {
return undefined;
}
if (!Array.isArray(ops) || ops.length === 0)
return undefined;
const byResource = new Map();
for (const op of ops) {
const resource = op.resource || 'default';
const opName = op.name || op.operation;
if (!opName)
continue;
if (!byResource.has(resource)) {
byResource.set(resource, []);
}
const list = byResource.get(resource);
if (!list.includes(opName)) {
list.push(opName);
}
}
if (byResource.size === 0)
return undefined;
return Array.from(byResource.entries()).map(([resource, operations]) => ({
resource,
operations
}));
}
async getNodeEssentials(nodeType, includeExamples) {
await this.ensureInitialized();
if (!this.repository)
@@ -2795,6 +2909,54 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
tip: `Found ${result.total} templates matching "${query}". Showing ${result.items.length}.`
};
}
getWorkflowPatterns(category, limit = 10) {
if (!this.workflowPatternsCache) {
try {
const patternsPath = path_1.default.join(__dirname, '..', '..', 'data', 'workflow-patterns.json');
if ((0, fs_1.existsSync)(patternsPath)) {
this.workflowPatternsCache = JSON.parse((0, fs_1.readFileSync)(patternsPath, 'utf-8'));
}
else {
return { error: 'Workflow patterns not generated yet. Run: npm run mine:patterns' };
}
}
catch (e) {
return { error: `Failed to load workflow patterns: ${e instanceof Error ? e.message : String(e)}` };
}
}
const patterns = this.workflowPatternsCache;
if (category) {
const categoryData = patterns.categories[category];
if (!categoryData) {
const available = Object.keys(patterns.categories);
return { error: `Unknown category "${category}". Available: ${available.join(', ')}` };
}
const MAX_CHAINS = 5;
return {
category,
templateCount: categoryData.templateCount,
pattern: categoryData.pattern,
nodes: categoryData.nodes?.slice(0, limit).map(n => ({
type: n.type, freq: n.frequency, role: n.role
})),
chains: categoryData.commonChains?.slice(0, MAX_CHAINS).map(c => ({
path: c.chain.map(t => t.split('.').pop() ?? t), count: c.count, freq: c.frequency
})),
};
}
const overview = Object.entries(patterns.categories).map(([name, data]) => ({
category: name,
templateCount: data.templateCount,
pattern: data.pattern,
topNodes: data.nodes?.slice(0, 5).map(n => n.displayName || n.type),
}));
return {
templateCount: patterns.templateCount,
generatedAt: patterns.generatedAt,
categories: overview,
tip: 'Use search_templates({searchMode: "patterns", task: "category_name"}) for full pattern data with nodes, chains, and tips.',
};
}
async getTemplatesForTask(task, limit = 10, offset = 0) {
await this.ensureInitialized();
if (!this.templateService)