Skip to main content
Glama

mcp-n8n-builder

workflow-tools.ts11.7 kB
import { ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { config } from '../config.js'; import { N8nApiClient } from '../n8n-api-client.js'; import { node_validator } from '../node-validator.js'; import { CreateWorkflowInputSchema, WorkflowSchema, } from '../schemas.js'; import { WORKFLOW_COMPOSITION_GUIDE } from '../workflow-composition-guide.js'; /** * Helper function to handle validation errors with helpful guidance * @param error The Zod validation error * @returns Formatted error response with guidance from the workflow composition guide */ function handle_validation_error(error: any) { // Extract the error message const error_message = error.message || 'Validation error'; // Determine which sections of the guide to include based on the error let guidance = ''; // Check for common error patterns if (error_message.includes('nodes')) { guidance += WORKFLOW_COMPOSITION_GUIDE.node_categories; } else if (error_message.includes('connections')) { guidance += WORKFLOW_COMPOSITION_GUIDE.common_patterns; } else if (error_message.includes('trigger')) { guidance += WORKFLOW_COMPOSITION_GUIDE.core_principles; } else { // Default guidance guidance += WORKFLOW_COMPOSITION_GUIDE.workflow_creation_process; } return { content: [ { type: 'text', text: `Validation error: ${error_message}\n\nHere's some guidance that might help:\n\n${guidance}`, }, ], isError: true, }; } /** * Helper function to format output based on verbosity setting * @param summary The human-readable summary text * @param details The full JSON details * @param verbosity The verbosity level (concise or full) * @returns Formatted text based on verbosity setting */ function format_output( summary: string, details: any, verbosity?: string, ): string { // Use the provided verbosity parameter if available, otherwise fall back to config const output_verbosity = verbosity || config.output_verbosity; if (output_verbosity === 'full') { return ( summary + '\n\nFull details:\n' + JSON.stringify(details, null, 2) ); } else { // Default to concise mode return summary; } } /** * Handles the list_workflows tool */ export async function handle_list_workflows( api_client: N8nApiClient, args: any, ) { try { const workflows = await api_client.list_workflows(args); if (!workflows || workflows.length === 0) { return { content: [ { type: 'text', text: 'No workflows found.', }, ], }; } // Create a summary of the workflows const active_count = workflows.filter( (wf: { active: boolean }) => wf.active, ).length; const inactive_count = workflows.length - active_count; const summary = `Found ${workflows.length} workflow${ workflows.length !== 1 ? 's' : '' } (${active_count} active, ${inactive_count} inactive):\n\n`; // Create a list of workflows with their basic info const workflow_list = workflows .map((wf: any, index: number) => { const status = wf.active ? 'Active' : 'Inactive'; const tags = wf.tags ?.map((tag: { name: string }) => tag.name) .join(', ') || 'None'; return `${index + 1}. "${wf.name}" (ID: ${wf.id}) Status: ${status} Created: ${new Date(wf.created_at).toLocaleString()} Tags: ${tags}`; }) .join('\n\n'); return { content: [ { type: 'text', text: format_output( summary + workflow_list, workflows, args.verbosity, ), }, ], }; } catch (error: any) { return { content: [ { type: 'text', text: `Error listing workflows: ${ error.message || String(error) }`, }, ], isError: true, }; } } /** * Handles the create_workflow tool */ export async function handle_create_workflow( api_client: N8nApiClient, args: any, ) { try { // Validate input with Zod const parsed_input = CreateWorkflowInputSchema.parse(args); // Validate that all nodes exist in n8n const invalid_nodes = await node_validator.validate_workflow_nodes( parsed_input.workflow.nodes, ); if (invalid_nodes.length > 0) { // Format error message with suggestions const error_messages = invalid_nodes.map((node) => { const suggestion = node.suggestion ? `Did you mean '${node.suggestion}'?` : 'No similar nodes found.'; return `- '${node.node_type}': Not a valid n8n node. ${suggestion}`; }); // Include relevant sections from the workflow composition guide const node_categories = WORKFLOW_COMPOSITION_GUIDE.node_categories; return { content: [ { type: 'text', text: `Workflow contains invalid node types:\n${error_messages.join( '\n', )}\n\nPlease correct these node types before creating the workflow.\n\n` + `Here are the available node categories for reference:\n${node_categories}`, }, ], isError: true, }; } const workflow = await api_client.create_workflow( parsed_input.workflow, parsed_input.activate, ); const activation_status = workflow.active ? 'activated' : 'created (not activated)'; return { content: [ { type: 'text', text: `Successfully ${activation_status} workflow "${workflow.name}" (ID: ${workflow.id})`, }, ], }; } catch (error: any) { if (error.name === 'ZodError') { return handle_validation_error(error); } return { content: [ { type: 'text', text: `Error creating workflow: ${ error.message || String(error) }`, }, ], isError: true, }; } } /** * Handles the get_workflow tool */ export async function handle_get_workflow( api_client: N8nApiClient, args: any, ) { if (!args.id) { throw new McpError( ErrorCode.InvalidParams, 'Workflow ID is required', ); } try { const workflow = await api_client.get_workflow(args.id); // Format a summary of the workflow const activation_status = workflow.active ? 'Active' : 'Inactive'; const node_count = workflow.nodes.length; const trigger_nodes = workflow.nodes.filter( (node: { type: string }) => node.type.toLowerCase().includes('trigger'), ).length; const summary = `Workflow: "${workflow.name}" (ID: ${args.id}) Status: ${activation_status} Created: ${new Date(workflow.created_at).toLocaleString()} Updated: ${new Date(workflow.updated_at).toLocaleString()} Nodes: ${node_count} (including ${trigger_nodes} trigger nodes) Tags: ${ workflow.tags ?.map((tag: { name: string }) => tag.name) .join(', ') || 'None' }`; return { content: [ { type: 'text', text: format_output(summary, workflow, args.verbosity), }, ], }; } catch (error: any) { return { content: [ { type: 'text', text: `Error retrieving workflow: ${ error.message || String(error) }`, }, ], isError: true, }; } } /** * Handles the update_workflow tool */ export async function handle_update_workflow( api_client: N8nApiClient, args: any, ) { if (!args.id || !args.workflow) { throw new McpError( ErrorCode.InvalidParams, 'Workflow ID and updated workflow data are required', ); } try { // Validate workflow with Zod const parsed_workflow = WorkflowSchema.parse(args.workflow); // Validate that all nodes exist in n8n const invalid_nodes = await node_validator.validate_workflow_nodes( parsed_workflow.nodes, ); if (invalid_nodes.length > 0) { // Format error message with suggestions const error_messages = invalid_nodes.map((node) => { const suggestion = node.suggestion ? `Did you mean '${node.suggestion}'?` : 'No similar nodes found.'; return `- '${node.node_type}': Not a valid n8n node. ${suggestion}`; }); // Include relevant sections from the workflow composition guide const node_categories = WORKFLOW_COMPOSITION_GUIDE.node_categories; return { content: [ { type: 'text', text: `Workflow contains invalid node types:\n${error_messages.join( '\n', )}\n\nPlease correct these node types before updating the workflow.\n\n` + `Here are the available node categories for reference:\n${node_categories}`, }, ], isError: true, }; } const workflow = await api_client.update_workflow( args.id, parsed_workflow, ); const activation_status = workflow.active ? 'active' : 'inactive'; return { content: [ { type: 'text', text: `Successfully updated workflow "${workflow.name}" (ID: ${args.id}, Status: ${activation_status})`, }, ], }; } catch (error: any) { if (error.name === 'ZodError') { return handle_validation_error(error); } return { content: [ { type: 'text', text: `Error updating workflow: ${ error.message || String(error) }`, }, ], isError: true, }; } } /** * Handles the delete_workflow tool */ export async function handle_delete_workflow( api_client: N8nApiClient, args: any, ) { if (!args.id) { throw new McpError( ErrorCode.InvalidParams, 'Workflow ID is required', ); } // First get the workflow to show its name try { const workflow = await api_client.get_workflow(args.id); // Now delete the workflow const result = await api_client.delete_workflow(args.id); return { content: [ { type: 'text', text: `Successfully deleted workflow "${workflow.name}" (ID: ${args.id})`, }, ], }; } catch (error: any) { return { content: [ { type: 'text', text: `Error deleting workflow: ${ error.message || String(error) }`, }, ], isError: true, }; } } /** * Handles the activate_workflow tool */ export async function handle_activate_workflow( api_client: N8nApiClient, args: any, ) { if (!args.id) { throw new McpError( ErrorCode.InvalidParams, 'Workflow ID is required', ); } try { const result = await api_client.activate_workflow(args.id); return { content: [ { type: 'text', text: `Successfully activated workflow "${result.name}" (ID: ${args.id})`, }, ], }; } catch (error: any) { // Check for common activation errors if (error.message && error.message.includes('trigger')) { // This is likely an error about missing trigger nodes const core_principles = WORKFLOW_COMPOSITION_GUIDE.core_principles; return { content: [ { type: 'text', text: `Error activating workflow: ${error.message}\n\n` + `Note: Only workflows with automatic trigger nodes (Schedule, Webhook, etc.) can be activated. ` + `Workflows with only manual triggers cannot be automatically activated.\n\n` + `Here are some core principles for workflow composition:\n${core_principles}`, }, ], isError: true, }; } return { content: [ { type: 'text', text: `Error activating workflow: ${ error.message || String(error) }`, }, ], isError: true, }; } } /** * Handles the deactivate_workflow tool */ export async function handle_deactivate_workflow( api_client: N8nApiClient, args: any, ) { if (!args.id) { throw new McpError( ErrorCode.InvalidParams, 'Workflow ID is required', ); } try { const result = await api_client.deactivate_workflow(args.id); return { content: [ { type: 'text', text: `Successfully deactivated workflow "${result.name}" (ID: ${args.id})`, }, ], }; } catch (error: any) { return { content: [ { type: 'text', text: `Error deactivating workflow: ${ error.message || String(error) }`, }, ], isError: true, }; } }

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/spences10/mcp-n8n-builder'

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