Skip to main content
Glama

McFlow

handler.ts16.8 kB
import path from 'path'; import fs from 'fs/promises'; import { WorkflowManager } from '../workflows/manager.js'; import { N8nManager } from '../n8n/manager.js'; import { CredentialHelper } from '../credentials/helper.js'; import { WorkflowCompiler } from '../workflows/compiler.js'; import { NodeManager } from '../nodes/manager.js'; import { ChangeTracker } from '../utils/change-tracker.js'; import { analyzeWorkflow } from '../workflows/analyzer.js'; import { validateWorkflow, autofixWorkflow } from '../workflows/validator.js'; import { addNodeToWorkflow, connectNodes } from '../workflows/operations.js'; import { generateWorkflowFromTemplate } from '../workflows/templates.js'; import { TrackingInjector } from '../workflows/tracking-injector.js'; import { TrackingConfig } from '../workflows/tracking.js'; import { AppGenerator } from '../app/generator.js'; export class ToolHandler { private trackingConfig: TrackingConfig = { enabled: false }; constructor( private workflowsPath: string, private workflowManager: WorkflowManager, private n8nManager: N8nManager, private credentialHelper: CredentialHelper ) {} async handleTool(toolName: string, args: any): Promise<any> { switch (toolName) { case 'list': return await this.workflowManager.listWorkflows(args?.project as string); case 'read': return await this.workflowManager.readWorkflow(args?.path as string); case 'create': return await this.workflowManager.createWorkflow( args?.name as string, args?.workflow as any, args?.project as string ); case 'update': return await this.workflowManager.updateWorkflow( args?.path as string, args?.workflow as any ); case 'analyze': return await analyzeWorkflow(this.workflowsPath, args?.path as string); case 'info': return await this.workflowManager.getProjectInfo(args?.project as string); case 'validate': const validatePath = args?.path as string; const validateWorkflow = args?.workflow as any; const autofix = args?.autofix as boolean; if (validatePath) { const fullPath = path.join(this.workflowsPath, validatePath); const content = await fs.readFile(fullPath, 'utf-8'); const workflow = JSON.parse(content); if (autofix) { const fixed = await autofixWorkflow(workflow); if (fixed.changed) { await fs.writeFile(fullPath, JSON.stringify(fixed.workflow, null, 2)); return { content: [{ type: 'text', text: `✅ Fixed ${fixed.fixes.length} issues:\n${fixed.fixes.join('\n')}\n\nWorkflow saved!` }] }; } } return await validateWorkflow(workflow); } else { return await validateWorkflow(validateWorkflow); } case 'add_node': return await addNodeToWorkflow( this.workflowsPath, args?.path as string, args?.node as any, args?.position as any ); case 'connect': return await connectNodes( this.workflowsPath, args?.path as string, args?.sourceNode as string, args?.targetNode as string, args?.sourceOutput as string, args?.targetInput as string ); case 'generate': return await generateWorkflowFromTemplate( this.workflowManager, args?.template as string, args?.project as string, args?.name as string, args?.config as any ); case 'compile': const outputToFiles = args?.output !== false; const compiler = new WorkflowCompiler(this.workflowsPath); try { const compiledWorkflows = await compiler.compileAll(outputToFiles); return { content: [{ type: 'text', text: `✅ Successfully compiled ${compiledWorkflows.size} workflows${outputToFiles ? ' to dist/' : ' (in memory)'}\n\n` + Array.from(compiledWorkflows.keys()).map(name => `• ${name}`).join('\n') }] }; } catch (error: any) { return { content: [{ type: 'text', text: `❌ Compilation failed: ${error.message}` }] }; } case 'deploy': const deployPath = args?.path as string; const deployAll = args?.all as boolean; const deployOptions = { activate: args?.activate as boolean, separate: args?.separate as boolean, }; if (deployPath) { return await this.n8nManager.importWorkflow(deployPath, deployOptions); } else if (deployAll) { return await this.n8nManager.deployAllWorkflows(deployOptions); } else { return await this.n8nManager.deployChangedWorkflows(deployOptions); } case 'export': return await this.n8nManager.exportWorkflow({ id: args?.id as string, all: args?.all as boolean, outputPath: args?.outputPath as string, pretty: args?.pretty as boolean, }); case 'execute': return await this.n8nManager.executeWorkflow({ id: args?.id as string, file: args?.file as string, data: args?.data as any, }); case 'deployed': return await this.n8nManager.listDeployedWorkflows(); case 'list_credentials': const credentials = await this.n8nManager.listCredentials(); return { content: [{ type: 'text', text: credentials.length > 0 ? `📋 Found ${credentials.length} credentials in n8n:\n\n` + credentials.map((c: any) => `• ID: ${c.id}\n Name: ${c.name}\n Type: ${c.type}`).join('\n\n') + '\n\n💡 Use these IDs in your workflow nodes to reference credentials' : '📭 No credentials found in n8n\n\n' + '💡 Add credentials in the n8n UI first, then use this command to get their IDs' }] }; case 'activate': return await this.n8nManager.updateWorkflowStatus( args?.id as string, args?.active as boolean ); case 'start': return await this.n8nManager.startN8n({ port: args?.port as number, tunnel: args?.tunnel as boolean, }); case 'status': const changeTracker = new ChangeTracker(this.workflowsPath); await changeTracker.initialize(); const statusDetails = await changeTracker.getChangeDetails(); return { content: [{ type: 'text', text: statusDetails }] }; case 'extract_code': const codeManager = new NodeManager(this.workflowsPath); await codeManager.initialize(); const workflowToExtract = args?.workflow as string | undefined; if (workflowToExtract) { const workflowPath = path.join(this.workflowsPath, 'flows', `${workflowToExtract}.json`); const result = await codeManager.extractNodes(workflowPath); return { content: [{ type: 'text', text: result.extracted.length > 0 ? `✅ Extracted ${result.extracted.length} code nodes from ${workflowToExtract}\n\n` + result.extracted.map((n: any) => `• ${n.nodeName} → ${n.filePath}`).join('\n') : `📭 No code nodes found in ${workflowToExtract}` }] }; } else { return await codeManager.extractAllNodes(); } case 'list_code': const codeListManager = new NodeManager(this.workflowsPath); await codeListManager.initialize(); return await codeListManager.listNodes(); case 'create_module': const moduleManager = new NodeManager(this.workflowsPath); await moduleManager.initialize(); return await moduleManager.createSharedModule( args?.name as string, args?.language as 'javascript' | 'python' ); case 'credentials': const credAction = args?.action as string; switch (credAction) { case 'analyze': return await this.credentialHelper.analyzeCredentialRequirements(); case 'instructions': return await this.credentialHelper.getCredentialSetupInstructions(); case 'generate-env': return await this.credentialHelper.generateSecureEnvExample(); default: throw new Error(`Unknown credential action: ${credAction}`); } case 'add_tracking': const trackingPath = args?.path as string; const storageUrl = args?.storageUrl || process.env.WORKFLOW_STORAGE_URL; const trackingOptions = args?.options || {}; if (!storageUrl) { return { content: [{ type: 'text', text: '❌ Storage URL is required. Please provide storageUrl parameter or set WORKFLOW_STORAGE_URL environment variable.' }] }; } // Read workflow const fullTrackingPath = path.join(this.workflowsPath, trackingPath); const workflowContent = await fs.readFile(fullTrackingPath, 'utf-8'); const workflow = JSON.parse(workflowContent); // Create injector with configuration const injector = new TrackingInjector({ enabled: true, storageUrl, enableCheckpoints: trackingOptions.checkpoints?.length > 0, enableErrorTracking: trackingOptions.addErrorTracking }); // Inject tracking const trackedWorkflow = await injector.injectTracking(workflow, trackingOptions); // Save modified workflow await fs.writeFile(fullTrackingPath, JSON.stringify(trackedWorkflow, null, 2)); // Count added nodes const addedNodes = trackedWorkflow.nodes.length - workflow.nodes.length; return { content: [{ type: 'text', text: `✅ Added ${addedNodes} tracking nodes to workflow\\n\\n` + `Storage URL: ${storageUrl}\\n` + `Start tracking: ${trackingOptions.addStartTracking !== false ? 'Yes' : 'No'}\\n` + `End tracking: ${trackingOptions.addEndTracking !== false ? 'Yes' : 'No'}\\n` + `Error tracking: ${trackingOptions.addErrorTracking ? 'Yes' : 'No'}\\n` + `Checkpoints: ${trackingOptions.checkpoints?.length || 0}\\n` + `Stored outputs: ${trackingOptions.storeOutputNodes?.length || 0}` }] }; case 'configure_tracking': // Update global tracking configuration this.trackingConfig = { enabled: args?.enabled ?? this.trackingConfig.enabled, storageUrl: args?.storageUrl || this.trackingConfig.storageUrl, trackAllNodes: args?.trackAllNodes ?? this.trackingConfig.trackAllNodes, enableCheckpoints: args?.enableCheckpoints ?? this.trackingConfig.enableCheckpoints, enableErrorTracking: args?.enableErrorTracking ?? this.trackingConfig.enableErrorTracking }; // Save configuration to file for persistence const configPath = path.join(this.workflowsPath, '.tracking-config.json'); await fs.writeFile(configPath, JSON.stringify(this.trackingConfig, null, 2)); return { content: [{ type: 'text', text: `✅ Tracking configuration updated:\\n\\n` + `Enabled: ${this.trackingConfig.enabled}\\n` + `Storage URL: ${this.trackingConfig.storageUrl || 'Not set'}\\n` + `Track all nodes: ${this.trackingConfig.trackAllNodes || false}\\n` + `Enable checkpoints: ${this.trackingConfig.enableCheckpoints || false}\\n` + `Enable error tracking: ${this.trackingConfig.enableErrorTracking || false}\\n\\n` + `Configuration saved to ${configPath}` }] }; case 'add_checkpoint': const checkpointPath = args?.path as string; const checkpointName = args?.checkpointName as string; const afterNode = args?.afterNode as string; const addRestore = args?.addRestore as boolean; // Read workflow const fullCheckpointPath = path.join(this.workflowsPath, checkpointPath); const checkpointWorkflowContent = await fs.readFile(fullCheckpointPath, 'utf-8'); const checkpointWorkflow = JSON.parse(checkpointWorkflowContent); // Use configured storage URL or environment variable const checkpointStorageUrl = this.trackingConfig.storageUrl || process.env.WORKFLOW_STORAGE_URL; if (!checkpointStorageUrl) { return { content: [{ type: 'text', text: '❌ Storage URL not configured. Use configure_tracking to set storageUrl or set WORKFLOW_STORAGE_URL environment variable.' }] }; } // Create injector const checkpointInjector = new TrackingInjector({ enabled: true, storageUrl: checkpointStorageUrl, enableCheckpoints: true }); // Add checkpoint let modifiedCheckpointWorkflow = checkpointWorkflow; if (afterNode) { // Add save checkpoint after specified node modifiedCheckpointWorkflow = await checkpointInjector.injectTracking(checkpointWorkflow, { checkpoints: [{ afterNode, checkpointName }] }); } if (addRestore) { // Add restore checkpoint at workflow start modifiedCheckpointWorkflow = await checkpointInjector.addCheckpointRestore( modifiedCheckpointWorkflow, checkpointName ); } // Save modified workflow await fs.writeFile(fullCheckpointPath, JSON.stringify(modifiedCheckpointWorkflow, null, 2)); return { content: [{ type: 'text', text: `✅ Added checkpoint "${checkpointName}" to workflow\\n\\n` + (afterNode ? `Save checkpoint after: ${afterNode}\\n` : '') + (addRestore ? `Restore checkpoint at workflow start\\n` : '') + `Storage URL: ${checkpointStorageUrl}` }] }; case 'generate_app': const appName = args?.name as string; const stages = args?.stages as string[] || ['created', 'processing', 'review', 'completed']; const features = args?.features || { dashboard: true, api: true, database: true, webhooks: true, approvals: false }; // Get project path (parent of workflows directory) const projectPath = path.dirname(this.workflowsPath); // Create app generator const appGenerator = new AppGenerator(projectPath); try { await appGenerator.generateApp({ name: appName, projectPath, features, stages }); return { content: [{ type: 'text', text: `✅ Successfully generated Next.js app: ${appName}\\n\\n` + `📁 Location: ${path.join(projectPath, appName)}\\n\\n` + `Features included:\\n` + `• Dashboard: ${features.dashboard ? 'Yes' : 'No'}\\n` + `• API Endpoints: ${features.api ? 'Yes' : 'No'}\\n` + `• Database (SQLite): ${features.database ? 'Yes' : 'No'}\\n` + `• Webhook Receivers: ${features.webhooks ? 'Yes' : 'No'}\\n` + `• Approval System: ${features.approvals ? 'Yes' : 'No'}\\n\\n` + `Pipeline Stages: ${stages.join(' → ')}\\n\\n` + `Next steps:\\n` + `1. cd ${appName}\\n` + `2. npm install\\n` + `3. npm run dev\\n\\n` + `The app will be available at http://localhost:3000\\n\\n` + `To integrate with n8n workflows:\\n` + `• Use the tracking system: mcflow add_tracking --storageUrl http://localhost:3000\\n` + `• Or add HTTP Request nodes manually to your workflows` }] }; } catch (error: any) { return { content: [{ type: 'text', text: `❌ Failed to generate app: ${error.message}` }] }; } default: throw new Error(`Unknown tool: ${toolName}`); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mckinleymedia/mcflow-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server