Skip to main content
Glama
mlaurel

Structured Workflow Engine MCP Server

by mlaurel
workflow-loader.ts7.98 kB
import fs from 'fs'; import path from 'path'; import yaml from 'js-yaml'; import { WorkflowConfig, PhaseConfig, StepConfig } from '../types/workflow-types'; export interface WorkflowMetadata { id: string; title: string; description: string; category: string; complexity: 'Simple' | 'Standard' | 'Complex'; tags: string[]; estimatedDuration: string; teamSize?: string; skillLevel?: string; } export class WorkflowLoader { private workflowsPath: string; private cache: Map<string, WorkflowConfig> = new Map(); private metadataCache: Map<string, WorkflowMetadata> = new Map(); constructor(workflowsPath?: string) { // Always use public/playbook/workflows for consistency this.workflowsPath = workflowsPath || path.join(process.cwd(), 'public', 'playbook', 'workflows'); console.log(`[WorkflowLoader] Using workflows path: ${this.workflowsPath}`); } /** * Load all available workflows metadata for browsing */ async loadWorkflowsMetadata(): Promise<WorkflowMetadata[]> { if (this.metadataCache.size === 0) { await this.buildMetadataCache(); } return Array.from(this.metadataCache.values()); } /** * Load specific workflow configuration */ async loadWorkflowConfig(workflowId: string): Promise<WorkflowConfig | null> { const cacheKey = workflowId; // Check cache first if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey)!; } // Load from file const workflow = await this.loadWorkflowFromFile(workflowId); if (workflow) { this.cache.set(cacheKey, workflow); } return workflow; } /** * Search workflows by description and category */ async searchWorkflows(query: string, limit: number = 5): Promise<WorkflowMetadata[]> { const allWorkflows = await this.loadWorkflowsMetadata(); // Simple text-based search for now const searchTerms = query.toLowerCase().split(' '); const scored = allWorkflows.map(workflow => { let score = 0; const searchableText = `${workflow.title} ${workflow.description} ${workflow.category} ${workflow.tags.join(' ')}`.toLowerCase(); searchTerms.forEach(term => { if (searchableText.includes(term)) { score += 1; // Boost score for title matches if (workflow.title.toLowerCase().includes(term)) { score += 2; } // Boost score for category matches if (workflow.category.toLowerCase().includes(term)) { score += 1.5; } } }); return { workflow, score }; }); return scored .filter(item => item.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit) .map(item => item.workflow); } /** * Build metadata cache from all YAML files */ private async buildMetadataCache(): Promise<void> { if (!fs.existsSync(this.workflowsPath)) { console.warn(`[WorkflowLoader] Workflows directory not found: ${this.workflowsPath}`); console.log(`[WorkflowLoader] Current working directory: ${process.cwd()}`); console.log(`[WorkflowLoader] Available directories:`, fs.readdirSync(process.cwd())); return; } const files = fs.readdirSync(this.workflowsPath) .filter(file => file.endsWith('.yml') || file.endsWith('.yaml')); console.log(`[WorkflowLoader] Found ${files.length} workflow files:`, files); for (const file of files) { const workflowId = path.basename(file, path.extname(file)); const filePath = path.join(this.workflowsPath, file); try { const yamlContent = fs.readFileSync(filePath, 'utf-8'); const workflowData = yaml.load(yamlContent) as any; const metadata: WorkflowMetadata = { id: workflowId, title: workflowData.name || workflowId, description: workflowData.description || '', category: workflowData.category || 'general', complexity: workflowData.metadata?.complexity || 'Standard', tags: workflowData.tags || [], estimatedDuration: workflowData.metadata?.estimatedDuration || 'Unknown', teamSize: workflowData.metadata?.teamSize, skillLevel: workflowData.metadata?.skillLevel }; this.metadataCache.set(workflowId, metadata); } catch (error) { console.error(`[WorkflowLoader] Error loading workflow metadata from ${file}:`, error); } } } /** * Load workflow from YAML file */ private async loadWorkflowFromFile(workflowId: string): Promise<WorkflowConfig | null> { const filePath = path.join(this.workflowsPath, `${workflowId}.yml`); if (!fs.existsSync(filePath)) { // Try .yaml extension const altPath = path.join(this.workflowsPath, `${workflowId}.yaml`); if (!fs.existsSync(altPath)) { console.warn(`[WorkflowLoader] Workflow file not found: ${workflowId}`); console.log(`[WorkflowLoader] Tried paths:`, filePath, altPath); console.log(`[WorkflowLoader] Directory contents:`, fs.existsSync(this.workflowsPath) ? fs.readdirSync(this.workflowsPath) : 'Directory does not exist'); return null; } } try { const yamlContent = fs.readFileSync(filePath, 'utf-8'); const workflowData = yaml.load(yamlContent) as any; // Transform YAML structure to WorkflowConfig const workflowConfig: WorkflowConfig = { name: workflowData.name || workflowId, description: workflowData.description || '', complexity: workflowData.metadata?.complexity || 'Standard', category: workflowData.category || 'general', phases: this.transformPhases(workflowData.phases || []), execution_strategy: workflowData.execution?.allowSkipping ? 'smart_skip' : 'linear', estimated_duration: workflowData.metadata?.estimatedDuration || 'Unknown' }; console.log(`[WorkflowLoader] Successfully loaded workflow: ${workflowId}`); return workflowConfig; } catch (error) { console.error(`[WorkflowLoader] Error loading workflow ${workflowId}:`, error); return null; } } /** * Transform YAML phases to PhaseConfig format */ private transformPhases(yamlPhases: any[]): PhaseConfig[] { return yamlPhases.map(yamlPhase => { const phaseConfig: PhaseConfig = { name: yamlPhase.phase || yamlPhase.name, required: yamlPhase.required !== false, steps: this.transformSteps(yamlPhase.steps || []) }; return phaseConfig; }); } /** * Transform YAML steps to StepConfig format */ private transformSteps(yamlSteps: any[]): StepConfig[] { return yamlSteps.map(yamlStep => { const stepConfig: StepConfig = { id: yamlStep.id, mini_prompt: yamlStep.miniPrompt || yamlStep.mini_prompt || yamlStep.id, required: yamlStep.required !== false, context: yamlStep.context, prerequisites: { mcp_servers: yamlStep.prerequisites?.mcpServers || [], context: yamlStep.prerequisites?.requiredContext || [], optional: yamlStep.prerequisites?.optionalContext || [] }, skip_conditions: yamlStep.prerequisites?.skipConditions || [] }; return stepConfig; }); } /** * Clear all caches */ clearCache(): void { this.cache.clear(); this.metadataCache.clear(); } /** * Get available workflow categories */ async getWorkflowCategories(): Promise<string[]> { const workflows = await this.loadWorkflowsMetadata(); const categories = new Set(workflows.map(w => w.category)); return Array.from(categories).sort(); } /** * Get workflows by category */ async getWorkflowsByCategory(category: string): Promise<WorkflowMetadata[]> { const workflows = await this.loadWorkflowsMetadata(); return workflows.filter(w => w.category === category); } }

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