Skip to main content
Glama

McFlow

formatter.ts12 kB
/** * Workflow Formatter for McFlow * * Provides formatted, readable output for JSON workflows * with syntax highlighting and proper indentation */ interface FormatOptions { colorize?: boolean; indent?: number; maxDepth?: number; compact?: boolean; showNodeDetails?: boolean; } export class WorkflowFormatter { // ANSI color codes for terminal output private colors = { reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m', // Colors red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', // Background bgRed: '\x1b[41m', bgGreen: '\x1b[42m', bgYellow: '\x1b[43m', bgBlue: '\x1b[44m', }; /** * Format a workflow for display */ formatWorkflow(workflow: any, options: FormatOptions = {}): string { const { colorize = true, indent = 2, compact = false, showNodeDetails = true } = options; if (compact) { return this.formatCompact(workflow, colorize); } let output = ''; // Workflow header if (colorize) { output += `${this.colors.bright}${this.colors.cyan}📋 Workflow: ${workflow.name || 'Unnamed'}${this.colors.reset}\n`; output += `${this.colors.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${this.colors.reset}\n\n`; } else { output += `📋 Workflow: ${workflow.name || 'Unnamed'}\n`; output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`; } // Metadata if (workflow.meta) { output += this.formatSection('Metadata', workflow.meta, { colorize, indent }); } // Nodes if (workflow.nodes && showNodeDetails) { output += this.formatNodes(workflow.nodes, { colorize, indent }); } // Connections if (workflow.connections) { output += this.formatConnections(workflow.connections, { colorize, indent }); } // Settings if (workflow.settings) { output += this.formatSection('Settings', workflow.settings, { colorize, indent }); } return output; } /** * Format nodes section */ private formatNodes(nodes: any[], options: any): string { const { colorize, indent } = options; let output = ''; if (colorize) { output += `${this.colors.bright}${this.colors.green}🔧 Nodes (${nodes.length})${this.colors.reset}\n`; } else { output += `🔧 Nodes (${nodes.length})\n`; } for (const node of nodes) { output += this.formatNode(node, { colorize, indent }); } output += '\n'; return output; } /** * Format individual node */ private formatNode(node: any, options: any): string { const { colorize, indent } = options; let output = ''; const ind = ' '.repeat(indent); // Node header if (colorize) { const nodeColor = this.getNodeColor(node.type); output += `${ind}${nodeColor}▸ ${node.name}${this.colors.reset}`; output += ` ${this.colors.dim}(${node.type})${this.colors.reset}\n`; } else { output += `${ind}▸ ${node.name} (${node.type})\n`; } // Node parameters (formatted as JSON) if (node.parameters && Object.keys(node.parameters).length > 0) { const paramStr = this.formatJSON(node.parameters, indent + 2, colorize); output += `${ind} Parameters:\n${paramStr}\n`; } // Credentials if (node.credentials) { if (colorize) { output += `${ind} ${this.colors.yellow}🔑 Credentials: ${Object.keys(node.credentials).join(', ')}${this.colors.reset}\n`; } else { output += `${ind} 🔑 Credentials: ${Object.keys(node.credentials).join(', ')}\n`; } } return output; } /** * Get color for node type */ private getNodeColor(nodeType: string): string { if (nodeType.includes('trigger') || nodeType.includes('webhook')) { return this.colors.magenta; } else if (nodeType.includes('http')) { return this.colors.blue; } else if (nodeType.includes('code')) { return this.colors.yellow; } else if (nodeType.includes('if') || nodeType.includes('switch')) { return this.colors.cyan; } else if (nodeType.includes('merge') || nodeType.includes('split')) { return this.colors.green; } else if (nodeType.includes('openAi') || nodeType.includes('anthropic')) { return this.colors.bright + this.colors.magenta; } return this.colors.white; } /** * Format connections */ private formatConnections(connections: any, options: any): string { const { colorize, indent } = options; let output = ''; const ind = ' '.repeat(indent); if (colorize) { output += `${this.colors.bright}${this.colors.blue}🔗 Connections${this.colors.reset}\n`; } else { output += `🔗 Connections\n`; } for (const [sourceName, outputs] of Object.entries(connections)) { if (colorize) { output += `${ind}${this.colors.cyan}${sourceName}${this.colors.reset} →\n`; } else { output += `${ind}${sourceName} →\n`; } const mainOutputs = (outputs as any).main; if (Array.isArray(mainOutputs)) { mainOutputs.forEach((outputConnections, outputIndex) => { if (Array.isArray(outputConnections)) { outputConnections.forEach(conn => { if (colorize) { output += `${ind} [${outputIndex}] → ${this.colors.green}${conn.node}${this.colors.reset}`; } else { output += `${ind} [${outputIndex}] → ${conn.node}`; } if (conn.type !== 'main') { output += ` (${conn.type})`; } output += '\n'; }); } }); } } output += '\n'; return output; } /** * Format a section as JSON */ private formatSection(title: string, data: any, options: any): string { const { colorize, indent } = options; let output = ''; if (colorize) { output += `${this.colors.bright}${this.colors.blue}${title}${this.colors.reset}\n`; } else { output += `${title}\n`; } output += this.formatJSON(data, indent, colorize) + '\n\n'; return output; } /** * Format JSON with syntax highlighting */ formatJSON(obj: any, indentLevel: number = 2, colorize: boolean = true): string { const json = JSON.stringify(obj, null, 2); if (!colorize) { return json.split('\n').map(line => ' '.repeat(indentLevel) + line).join('\n'); } // Apply syntax highlighting return json.split('\n').map(line => { let colored = line; // Color property names (in quotes followed by colon) colored = colored.replace(/"([^"]+)":/g, `${this.colors.cyan}"$1":${this.colors.reset}`); // Color string values colored = colored.replace(/: "([^"]*)"/g, `: ${this.colors.green}"$1"${this.colors.reset}`); // Color numbers colored = colored.replace(/: (\d+\.?\d*)/g, `: ${this.colors.yellow}$1${this.colors.reset}`); // Color booleans colored = colored.replace(/: (true|false)/g, `: ${this.colors.magenta}$1${this.colors.reset}`); // Color null colored = colored.replace(/: null/g, `: ${this.colors.dim}null${this.colors.reset}`); // Dim brackets and braces colored = colored.replace(/([{}[\],])/g, `${this.colors.dim}$1${this.colors.reset}`); return ' '.repeat(indentLevel) + colored; }).join('\n'); } /** * Format compact view */ private formatCompact(workflow: any, colorize: boolean): string { let output = ''; // Summary line const nodeCount = workflow.nodes?.length || 0; const connectionCount = Object.keys(workflow.connections || {}).length; if (colorize) { output += `${this.colors.bright}${workflow.name || 'Unnamed'}${this.colors.reset} `; output += `${this.colors.dim}(${nodeCount} nodes, ${connectionCount} connections)${this.colors.reset}\n`; } else { output += `${workflow.name || 'Unnamed'} (${nodeCount} nodes, ${connectionCount} connections)\n`; } // Node types summary if (workflow.nodes) { const nodeTypes = new Map<string, number>(); for (const node of workflow.nodes) { const type = node.type.split('.').pop() || node.type; nodeTypes.set(type, (nodeTypes.get(type) || 0) + 1); } output += ' Nodes: '; const types = Array.from(nodeTypes.entries()) .map(([type, count]) => `${type}${count > 1 ? ` (${count})` : ''}`); if (colorize) { output += `${this.colors.cyan}${types.join(', ')}${this.colors.reset}`; } else { output += types.join(', '); } output += '\n'; } return output; } /** * Format node code (for Code nodes) */ formatNodeCode(code: string, language: 'javascript' | 'python' = 'javascript'): string { // Add line numbers and basic syntax highlighting const lines = code.split('\n'); return lines.map((line, index) => { const lineNum = String(index + 1).padStart(3, ' '); let coloredLine = line; // Basic syntax highlighting if (language === 'javascript') { // Keywords coloredLine = coloredLine.replace( /\b(const|let|var|function|return|if|else|for|while|try|catch|throw|new|async|await)\b/g, `${this.colors.magenta}$1${this.colors.reset}` ); // Strings coloredLine = coloredLine.replace( /(["'`])([^"'`]*)\1/g, `${this.colors.green}$1$2$1${this.colors.reset}` ); // Comments coloredLine = coloredLine.replace( /(\/\/.*$|\/\*.*\*\/)/g, `${this.colors.dim}$1${this.colors.reset}` ); } return `${this.colors.dim}${lineNum}│${this.colors.reset} ${coloredLine}`; }).join('\n'); } /** * Format workflow diff (for showing changes) */ formatDiff(oldWorkflow: any, newWorkflow: any): string { let output = `${this.colors.bright}📝 Workflow Changes${this.colors.reset}\n`; output += `${this.colors.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${this.colors.reset}\n\n`; // Compare nodes const oldNodes = new Map(oldWorkflow.nodes?.map((n: any) => [n.name, n]) || []); const newNodes = new Map(newWorkflow.nodes?.map((n: any) => [n.name, n]) || []); // Added nodes for (const [name, node] of newNodes) { if (!oldNodes.has(name)) { const typedNode = node as any; output += `${this.colors.green}+ Added: ${name} (${typedNode.type})${this.colors.reset}\n`; } } // Removed nodes for (const [name, node] of oldNodes) { if (!newNodes.has(name)) { const typedNode = node as any; output += `${this.colors.red}- Removed: ${name} (${typedNode.type})${this.colors.reset}\n`; } } // Modified nodes for (const [name, newNode] of newNodes) { const oldNode = oldNodes.get(name) as any; const typedNewNode = newNode as any; if (oldNode && JSON.stringify(oldNode) !== JSON.stringify(typedNewNode)) { output += `${this.colors.yellow}~ Modified: ${name}${this.colors.reset}\n`; // Show what changed if (JSON.stringify(oldNode.parameters) !== JSON.stringify(typedNewNode.parameters)) { output += ` ${this.colors.dim}Parameters changed${this.colors.reset}\n`; } if (JSON.stringify(oldNode.credentials) !== JSON.stringify(typedNewNode.credentials)) { output += ` ${this.colors.dim}Credentials changed${this.colors.reset}\n`; } } } return output; } }

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