mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-04-01 23:23:12 +00:00
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:
186
dist/mcp/server.js
vendored
186
dist/mcp/server.js
vendored
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user