UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

257 lines (256 loc) 14.5 kB
import fs from 'fs-extra'; import path from 'path'; import { fileURLToPath } from 'url'; import { executeTool } from '../routing/toolRegistry.js'; import logger from '../../logger.js'; import { AppError, ToolExecutionError, ConfigurationError, ParsingError } from '../../utils/errors.js'; import { jobManager, JobStatus } from '../job-manager/index.js'; import { sseNotifier } from '../sse-notifier/index.js'; const INITIAL_POLLING_INTERVAL_MS = 1000; const MAX_POLLING_INTERVAL_MS = 10000; const POLLING_BACKOFF_FACTOR = 1.5; const MAX_POLLING_DURATION_MS = 300000; let loadedWorkflows = new Map(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const defaultWorkflowPath = path.resolve(__dirname, '../../../workflows.json'); export function loadWorkflowDefinitions(filePath = defaultWorkflowPath) { logger.info(`Attempting to load workflow definitions from: ${filePath}`); loadedWorkflows = new Map(); try { if (!fs.existsSync(filePath)) { logger.warn(`Workflow definition file not found: ${filePath}. No workflows loaded.`); return; } const fileContent = fs.readFileSync(filePath, 'utf-8'); const workflowData = JSON.parse(fileContent); if (!workflowData || typeof workflowData.workflows !== 'object' || workflowData.workflows === null) { throw new ConfigurationError('Invalid workflow file format: Root "workflows" object missing or invalid.'); } loadedWorkflows = new Map(Object.entries(workflowData.workflows)); logger.info(`Successfully loaded ${loadedWorkflows.size} workflow definitions.`); } catch (error) { logger.error({ err: error, filePath }, 'Failed to load or parse workflow definitions. Workflows will be unavailable.'); loadedWorkflows = new Map(); } } function resolveParamValue(valueTemplate, workflowInput, stepOutputs) { if (typeof valueTemplate !== 'string') { return valueTemplate; } const match = valueTemplate.match(/^{(workflow\.input\.([\w-]+))|(steps\.([\w-]+)\.output\.(.+))}$/); if (!match) { return valueTemplate; } try { if (match[1]) { const inputKey = match[2]; if (workflowInput && typeof workflowInput === 'object' && inputKey in workflowInput) { logger.debug(`Resolved template '${valueTemplate}' from workflow input key '${inputKey}'.`); return workflowInput[inputKey]; } throw new ParsingError(`Workflow input key "${inputKey}" not found for template '${valueTemplate}'.`); } else if (match[3]) { const stepId = match[4]; const outputPath = match[5]; const stepResult = stepOutputs.get(stepId); if (!stepResult) { throw new ParsingError(`Output from step "${stepId}" not found (required for template '${valueTemplate}'). Ensure step IDs match and the step executed.`); } let currentValue = stepResult; const pathParts = outputPath.match(/([^[.\]]+)|\[(\d+)\]/g); if (!pathParts) { throw new ParsingError(`Invalid output path format '${outputPath}' in template '${valueTemplate}'.`); } for (const part of pathParts) { if (currentValue === null || currentValue === undefined) { throw new ParsingError(`Cannot access path part '${part}' in '${outputPath}' from step '${stepId}' output because parent value is null or undefined. Template: '${valueTemplate}'.`); } const arrayMatch = part.match(/^\[(\d+)\]$/); if (arrayMatch) { const index = parseInt(arrayMatch[1], 10); if (!Array.isArray(currentValue) || index >= currentValue.length) { throw new ParsingError(`Index ${index} out of bounds for array in path '${outputPath}' from step '${stepId}'. Template: '${valueTemplate}'.`); } currentValue = currentValue[index]; } else { if (typeof currentValue !== 'object' || currentValue === null || !(part in currentValue)) { throw new ParsingError(`Key '${part}' not found in object path '${outputPath}' from step '${stepId}'. Template: '${valueTemplate}'.`); } currentValue = currentValue[part]; } } if (currentValue === undefined) { logger.warn(`Resolved path '${outputPath}' resulted in undefined for step '${stepId}'. Template: '${valueTemplate}'`); } logger.debug(`Resolved template '${valueTemplate}' from step '${stepId}' output path '${outputPath}'.`); return currentValue; } } catch (error) { if (error instanceof ParsingError) throw error; logger.error({ err: error, template: valueTemplate }, `Error resolving parameter template`); throw new ParsingError(`Unexpected error resolving template "${valueTemplate}": ${error instanceof Error ? error.message : String(error)}`); } logger.warn(`Template '${valueTemplate}' matched regex but failed to resolve logic.`); return valueTemplate; } function getJobIdFromResult(result) { if (result.isError || !result.content || result.content.length === 0) { return null; } const textContent = result.content[0]?.text; if (typeof textContent === 'string') { const match = textContent.match(/Job ID: (\S+)/); if (match && match[1]) { return match[1]; } } return null; } async function waitForJobCompletion(jobId, stepId, sessionId) { logger.info({ jobId, stepId, sessionId }, `Waiting for background job to complete...`); let currentInterval = INITIAL_POLLING_INTERVAL_MS; let totalWaitTime = 0; let attempts = 0; const startTime = Date.now(); while (totalWaitTime < MAX_POLLING_DURATION_MS) { await new Promise(resolve => setTimeout(resolve, currentInterval)); totalWaitTime = Date.now() - startTime; attempts++; const job = jobManager.getJob(jobId); if (!job) { logger.error({ jobId, stepId, sessionId }, `Job not found during polling.`); throw new ToolExecutionError(`Background job ${jobId} for step ${stepId} was not found.`); } if (job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED) { logger.info({ jobId, stepId, sessionId, status: job.status }, `Job ${job.status.toLowerCase()}.`); if (!job.result) { logger.error({ jobId, stepId, sessionId, status: job.status }, `Job finished but has no result stored.`); throw new ToolExecutionError(`Background job ${jobId} for step ${stepId} finished with status ${job.status} but has no result.`); } sseNotifier.sendProgress(sessionId, jobId, job.status, job.status === JobStatus.COMPLETED ? `Job completed successfully.` : `Job failed: ${job.progressMessage || 'No error message available.'}`); return job.result; } logger.debug({ jobId, stepId, sessionId, status: job.status, attempt: attempts, currentInterval, totalWaitTime, maxWaitTime: MAX_POLLING_DURATION_MS }, `Polling job status: ${job.status}`); const progressMessage = `Job status: ${job.status}... (Polling for ${Math.floor(totalWaitTime / 1000)}s, next check in ${currentInterval / 1000}s)`; sseNotifier.sendProgress(sessionId, jobId, job.status, progressMessage); currentInterval = Math.min(currentInterval * POLLING_BACKOFF_FACTOR, MAX_POLLING_INTERVAL_MS); } logger.error({ jobId, stepId, sessionId, attempts, totalWaitTime }, `Polling timed out for job.`); throw new ToolExecutionError(`Timed out waiting for background job ${jobId} for step ${stepId} to complete after ${Math.floor(totalWaitTime / 1000)} seconds.`); } export async function executeWorkflow(workflowName, workflowInput, config, context) { const workflow = loadedWorkflows.get(workflowName); const sessionId = context?.sessionId || `no-session-${Math.random().toString(36).substring(2)}`; if (!workflow) { logger.error(`Workflow "${workflowName}" not found.`); return { success: false, message: `Workflow "${workflowName}" not found.`, error: { message: `Workflow "${workflowName}" not found.` } }; } logger.info({ workflowName, sessionId }, `Starting workflow execution.`); const stepOutputs = new Map(); let currentStepIndex = 0; let currentStep; try { for (const step of workflow.steps) { currentStep = step; currentStepIndex++; const stepLogContext = { workflowName, sessionId, stepId: step.id, toolName: step.toolName, stepNum: currentStepIndex }; logger.info(stepLogContext, `Executing workflow step ${currentStepIndex}/${workflow.steps.length}`); sseNotifier.sendProgress(sessionId, step.id, JobStatus.RUNNING, `Workflow '${workflowName}': Starting step ${currentStepIndex} ('${step.id}' - ${step.toolName}).`); const resolvedParams = {}; for (const [key, template] of Object.entries(step.params)) { try { resolvedParams[key] = resolveParamValue(template, workflowInput, stepOutputs); logger.debug(`Resolved param '${key}' for step '${step.id}'`); } catch (resolveError) { logger.error({ err: resolveError, ...stepLogContext, paramKey: key, template }, `Failed to resolve parameter`); throw new AppError(`Failed to resolve parameter '${key}' for step '${step.id}': ${resolveError.message}`, { stepId: step.id, paramKey: key }, resolveError instanceof Error ? resolveError : undefined); } } let stepResult = await executeTool(step.toolName, resolvedParams, config, context); const jobId = getJobIdFromResult(stepResult); if (jobId) { logger.info({ ...stepLogContext, jobId }, `Tool returned a background job ID. Waiting for completion...`); sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, `Workflow '${workflowName}' step '${step.id}': Waiting for background job ${jobId}...`); try { stepResult = await waitForJobCompletion(jobId, step.id, sessionId); logger.info({ ...stepLogContext, jobId, finalStatus: stepResult.isError ? 'FAILED' : 'COMPLETED' }, `Background job finished.`); } catch (jobError) { logger.error({ err: jobError, ...stepLogContext, jobId }, `Error waiting for background job.`); throw new ToolExecutionError(`Step '${step.id}' failed while waiting for background job ${jobId}: ${jobError instanceof Error ? jobError.message : String(jobError)}`, { stepId: step.id, toolName: step.toolName, jobId }); } } stepOutputs.set(step.id, stepResult); if (stepResult.isError) { const stepErrorMessage = stepResult.content[0]?.text || 'Unknown tool error'; logger.error({ ...stepLogContext, errorResult: stepResult }, `Workflow step failed.`); sseNotifier.sendProgress(sessionId, jobId || step.id, JobStatus.FAILED, `Workflow '${workflowName}' step '${step.id}' failed: ${stepErrorMessage}`); throw new ToolExecutionError(`Step '${step.id}' (Tool: ${step.toolName}) failed: ${stepErrorMessage}`, { stepId: step.id, toolName: step.toolName, toolResult: stepResult }); } logger.debug(stepLogContext, `Workflow step completed successfully.`); sseNotifier.sendProgress(sessionId, jobId || step.id, JobStatus.COMPLETED, `Workflow '${workflowName}' step '${step.id}' completed successfully.`); } let finalOutputData; let finalMessage = `Workflow "${workflowName}" completed successfully.`; if (workflow.output) { finalOutputData = {}; logger.debug(`Processing final workflow output template for ${workflowName}`); for (const [key, template] of Object.entries(workflow.output)) { try { finalOutputData[key] = resolveParamValue(template, workflowInput, stepOutputs); if (key === 'summary' && typeof finalOutputData[key] === 'string') { finalMessage = finalOutputData[key]; } } catch (resolveError) { logger.warn({ err: resolveError, key, template }, `Could not resolve output template key '${key}' for workflow ${workflowName}. Skipping.`); finalOutputData[key] = `Error: Failed to resolve output template - ${resolveError.message}`; } } } logger.info({ workflowName, sessionId }, `Workflow execution finished successfully.`); return { success: true, message: finalMessage, outputs: finalOutputData, stepResults: stepOutputs, }; } catch (error) { logger.error({ err: error, workflowName, sessionId, failedStepId: currentStep?.id, failedToolName: currentStep?.toolName }, `Workflow execution failed.`); const errDetails = { stepId: currentStep?.id, toolName: currentStep?.toolName, message: error instanceof Error ? error.message : 'Unknown workflow execution error', details: error instanceof AppError ? error.context : undefined, }; if (currentStep) { sseNotifier.sendProgress(sessionId, currentStep.id, JobStatus.FAILED, `Workflow '${workflowName}' failed at step '${currentStep.id}': ${errDetails.message}`); } return { success: false, message: `Workflow "${workflowName}" failed at step ${currentStepIndex} (${currentStep?.toolName || 'N/A'}): ${errDetails.message}`, stepResults: stepOutputs, error: errDetails, }; } } loadWorkflowDefinitions();