Skip to main content
Glama
mlaurel

Structured Workflow Engine MCP Server

by mlaurel
workflow-parser.ts7.53 kB
import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; export interface ParsedWorkflow { id: string; title: string; description: string; complexity: 'Simple' | 'Standard' | 'Complex'; keywords: string[]; file_path: string; category: string; use_case: string; output: string; steps: WorkflowStep[]; next_workflows?: string[]; } export interface WorkflowStep { step: number; title: string; description: string; estimated_time?: string; content: string; } export class WorkflowParser { private agentsPlaybookPath: string; constructor(agentsPlaybookPath: string = './playbook') { this.agentsPlaybookPath = agentsPlaybookPath; } /** * Parse all MD files in agents-playbook directory structure */ async parseAllWorkflows(): Promise<ParsedWorkflow[]> { const workflows: ParsedWorkflow[] = []; try { // Parse playbook/prompt-playbook.md for workflow metadata const mainPlaybook = await this.parseMainPlaybook(); // Parse individual prompt files const directories = ['planning', 'kickoff', 'qa']; for (const dir of directories) { const dirPath = path.join(this.agentsPlaybookPath, dir); if (fs.existsSync(dirPath)) { const files = fs.readdirSync(dirPath).filter(file => file.endsWith('.md')); for (const file of files) { try { const workflow = await this.parseWorkflowFile(path.join(dirPath, file), dir); if (workflow) { workflows.push(workflow); } } catch (error) { console.warn(`[Parser] Failed to parse ${file}:`, error); } } } } console.log(`[Parser] Successfully parsed ${workflows.length} workflows`); return workflows; } catch (error) { console.error('[Parser] Error parsing workflows:', error); return []; } } /** * Parse playbook/prompt-playbook.md to extract workflow metadata */ private async parseMainPlaybook(): Promise<any> { const playbookPath = path.join(this.agentsPlaybookPath, 'prompt-playbook.md'); if (!fs.existsSync(playbookPath)) { console.warn('[Parser] playbook/prompt-playbook.md not found'); return {}; } const content = fs.readFileSync(playbookPath, 'utf-8'); // Extract workflow table information // This helps map prompt files to their metadata return { content }; } /** * Parse individual workflow/prompt file */ private async parseWorkflowFile(filePath: string, category: string): Promise<ParsedWorkflow | null> { if (!fs.existsSync(filePath)) { return null; } const fileContent = fs.readFileSync(filePath, 'utf-8'); const parsed = matter(fileContent); const content = parsed.content; const frontmatter = parsed.data; // Extract title from filename or content const filename = path.basename(filePath, '.md'); const title = frontmatter.title || this.extractTitleFromContent(content) || filename; // Generate ID from filename const id = filename.replace(/-prompt$/, '').replace(/-/g, '-'); // Extract description and other metadata const description = frontmatter.description || this.extractDescriptionFromContent(content); const complexity = frontmatter.complexity || this.inferComplexity(content); const keywords = frontmatter.keywords || this.extractKeywords(title, description, content); // Parse workflow steps const steps = this.parseSteps(content); // Extract use case and output from content const { use_case, output } = this.extractUseCaseAndOutput(content); return { id, title, description, complexity, keywords, file_path: path.relative(process.cwd(), filePath), category, use_case, output, steps, next_workflows: frontmatter.next_workflows }; } /** * Extract title from markdown content */ private extractTitleFromContent(content: string): string { const titleMatch = content.match(/^#\s+(.+)$/m); return titleMatch ? titleMatch[1].replace(/^(Prompt\s*[•·]\s*)/i, '').trim() : ''; } /** * Extract description from content */ private extractDescriptionFromContent(content: string): string { // Look for use case or description patterns const patterns = [ /use case[:\s]+(.+?)(?:\n|\|)/i, /description[:\s]+(.+?)(?:\n|\|)/i, /##\s*overview\s*\n(.+?)(?:\n#)/is ]; for (const pattern of patterns) { const match = content.match(pattern); if (match) { return match[1].trim(); } } // Fallback: get first meaningful paragraph const lines = content.split('\n').filter(line => line.trim() && !line.startsWith('#')); return lines[0]?.substring(0, 100) + '...' || ''; } /** * Infer complexity from content length and structure */ private inferComplexity(content: string): 'Simple' | 'Standard' | 'Complex' { const length = content.length; const sectionCount = (content.match(/^##/gm) || []).length; if (length > 3000 || sectionCount > 8) return 'Complex'; if (length > 1500 || sectionCount > 4) return 'Standard'; return 'Simple'; } /** * Extract keywords from title and content */ private extractKeywords(title: string, description: string, content: string): string[] { const text = `${title} ${description}`.toLowerCase(); const keywords = new Set<string>(); // Common workflow keywords const commonKeywords = [ 'planning', 'development', 'kickoff', 'qa', 'testing', 'validation', 'feature', 'bug', 'fix', 'implementation', 'architecture', 'refactoring', 'product', 'trd', 'documentation', 'research' ]; commonKeywords.forEach(keyword => { if (text.includes(keyword)) { keywords.add(keyword); } }); // Extract from title words title.toLowerCase().split(/[\s-]+/).forEach(word => { if (word.length > 3) { keywords.add(word); } }); return Array.from(keywords); } /** * Parse workflow steps from content */ private parseSteps(content: string): WorkflowStep[] { const steps: WorkflowStep[] = []; // Look for numbered workflow sections const workflowMatch = content.match(/## Workflow\s*\n([\s\S]+?)(?:\n##|$)/i); if (!workflowMatch) return steps; const workflowContent = workflowMatch[1]; const stepMatches = workflowContent.match(/(\d+)\.\s*\*\*(.+?)\*\*[:\s]*(.+?)(?=\n\d+\.|$)/gs); if (stepMatches) { stepMatches.forEach((stepMatch, index) => { const match = stepMatch.match(/(\d+)\.\s*\*\*(.+?)\*\*[:\s]*([\s\S]+)/); if (match) { steps.push({ step: parseInt(match[1]), title: match[2].trim(), description: match[3].trim().split('\n')[0], content: match[3].trim() }); } }); } return steps; } /** * Extract use case and output information */ private extractUseCaseAndOutput(content: string): { use_case: string; output: string } { const useCaseMatch = content.match(/use case[:\s]+(.+?)(?:\n|\|)/i); const outputMatch = content.match(/output[:\s]+(.+?)(?:\n|\|)/i); return { use_case: useCaseMatch ? useCaseMatch[1].trim() : '', output: outputMatch ? outputMatch[1].trim() : '' }; } }

Latest Blog Posts

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/mlaurel/mcp-workflow-engine'

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