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
JavaScript
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();