vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
208 lines (195 loc) • 13.4 kB
JavaScript
import fs from 'fs-extra';
import path from 'path';
import { z } from 'zod';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { performResearchQuery } from '../../utils/researchHelper.js';
import { performFormatAwareLlmCallWithCentralizedConfig } from '../../utils/llmHelper.js';
import logger from '../../logger.js';
import { registerTool, isToolRegistered } from '../../services/routing/toolRegistry.js';
import { AppError, ToolExecutionError } from '../../utils/errors.js';
import { jobManager, JobStatus } from '../../services/job-manager/index.js';
import { sseNotifier } from '../../services/sse-notifier/index.js';
import { formatBackgroundJobInitiationResponse } from '../../services/job-response-formatter/index.js';
import { getToolOutputDirectory, ensureToolOutputDirectory, getUnifiedSecurityConfig } from '../vibe-task-manager/security/unified-security-config.js';
function getBaseOutputDir() {
try {
return getToolOutputDirectory();
}
catch {
return process.env.VIBE_CODER_OUTPUT_DIR
? path.resolve(process.env.VIBE_CODER_OUTPUT_DIR)
: path.join(process.cwd(), 'VibeCoderOutput');
}
}
const RESEARCH_DIR = path.join(getBaseOutputDir(), 'research-manager');
export async function initDirectories() {
try {
const securityConfig = getUnifiedSecurityConfig();
if (!securityConfig.isInitialized()) {
logger.warn('UnifiedSecurityConfigManager not initialized, using fallback directory creation');
throw new Error('Security config not initialized');
}
const toolDir = await ensureToolOutputDirectory('research-manager');
logger.debug(`Ensured research directory exists: ${toolDir}`);
}
catch (error) {
logger.error({ err: error }, `Failed to ensure base output directory exists for research-manager.`);
const baseOutputDir = getBaseOutputDir();
try {
await fs.ensureDir(baseOutputDir);
const toolDir = path.join(baseOutputDir, 'research-manager');
await fs.ensureDir(toolDir);
logger.debug(`Ensured research directory exists (fallback): ${toolDir}`);
}
catch (fallbackError) {
logger.error({ err: fallbackError, path: baseOutputDir }, `Fallback directory creation also failed.`);
}
}
}
const RESEARCH_SYSTEM_PROMPT = `
# ROLE & GOAL
You are an expert AI Research Specialist. Your goal is to synthesize initial research findings and the original user query into a comprehensive, well-structured, and insightful research report in Markdown format.
# CORE TASK
Process the initial research findings (provided as context) related to the user's original 'query'. Enhance, structure, and synthesize this information into a high-quality research report.
# INPUT HANDLING
- The user prompt will contain the original 'query' and the initial research findings (likely from Perplexity) under a heading like 'Incorporate this information:'.
- Your task is *not* to perform new research, but to *refine, structure, and deepen* the provided information based on the original query.
# RESEARCH CONTEXT INTEGRATION (Your Input IS the Context)
- Treat the provided research findings as your primary source material.
- Analyze the findings for key themes, data points, conflicting information, and gaps.
- Synthesize the information logically, adding depth and interpretation where possible. Do not simply reformat the input.
- If the initial research seems incomplete based on the original query, explicitly state the limitations or areas needing further investigation in the 'Limitations' section.
# OUTPUT FORMAT & STRUCTURE (Strict Markdown)
- Your entire response **MUST** be valid Markdown.
- Start **directly** with the main title: '# Research Report: [Topic from Original Query]'
- Use the following sections with the specified Markdown heading levels. Include all sections, even if brief.
## 1. Executive Summary
- Provide a brief (2-4 sentence) overview of the key findings and conclusions based *only* on the provided research content.
## 2. Key Findings
- List the most important discoveries or data points from the research as bullet points.
- Directly synthesize information from the provided research context.
## 3. Detailed Analysis
- Elaborate on the key findings.
- Organize the information logically using subheadings (###).
- Discuss different facets of the topic, incorporating various points from the research.
- Compare and contrast different viewpoints or data points if present in the research.
## 4. Practical Applications / Implications
- Discuss the real-world relevance or potential uses of the researched information.
- How can this information be applied? What are the consequences?
## 5. Limitations and Caveats
- Acknowledge any limitations mentioned in the research findings.
- Identify potential gaps or areas where the provided research seems incomplete relative to the original query.
- Mention any conflicting information found in the research.
## 6. Conclusion & Recommendations (Optional)
- Summarize the main takeaways.
- If appropriate based *only* on the provided research, suggest potential next steps or areas for further investigation.
# QUALITY ATTRIBUTES
- **Synthesized:** Do not just regurgitate the input; organize, connect, and add analytical value.
- **Structured:** Strictly adhere to the specified Markdown format and sections.
- **Accurate:** Faithfully represent the information provided in the research context.
- **Comprehensive (within context):** Cover the key aspects present in the provided research relative to the query.
- **Clear & Concise:** Use precise language.
- **Objective:** Present the information neutrally, clearly separating findings from interpretation.
# CONSTRAINTS (Do NOT Do the Following)
- **NO Conversational Filler:** Start directly with the '# Research Report: ...' title.
- **NO New Research:** Do not attempt to access external websites or knowledge beyond the provided research context. Your task is synthesis and structuring.
- **NO Hallucination:** Do not invent findings or data not present in the input.
- **NO Process Commentary:** Do not mention Perplexity, Gemini, or the synthesis process itself.
- **Strict Formatting:** Use \`##\` for main sections and \`###\` for subheadings within the Detailed Analysis. Use bullet points for Key Findings.
`;
export const performResearch = async (params, config, context) => {
const sessionId = context?.sessionId || 'unknown-session';
if (sessionId === 'unknown-session') {
logger.warn({ tool: 'performResearch' }, 'Executing tool without a valid sessionId. SSE progress updates will not be sent.');
}
const query = params.query;
const jobId = jobManager.createJob('research', params);
logger.info({ jobId, tool: 'research', sessionId }, 'Starting background job.');
const initialResponse = formatBackgroundJobInitiationResponse(jobId, 'Research', `Your research request for query "${query.substring(0, 50)}..." has been submitted. You can retrieve the result using the job ID.`);
setImmediate(async () => {
const logs = [];
let filePath = '';
try {
jobManager.updateJobStatus(jobId, JobStatus.RUNNING, 'Starting research process...');
sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Starting research process...');
logs.push(`[${new Date().toISOString()}] Starting research for: ${query.substring(0, 50)}...`);
await initDirectories();
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const sanitizedQuery = query.substring(0, 30).toLowerCase().replace(/[^a-z0-9]+/g, '-');
const filename = `${timestamp}-${sanitizedQuery}-research.md`;
filePath = path.join(RESEARCH_DIR, filename);
logger.info({ jobId }, `Performing initial research query via Perplexity: ${query.substring(0, 50)}...`);
jobManager.updateJobStatus(jobId, JobStatus.RUNNING, 'Performing initial research query via Perplexity...');
sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Performing initial research query via Perplexity...');
logs.push(`[${new Date().toISOString()}] Calling Perplexity for initial research.`);
const researchResult = await performResearchQuery(query, config);
logger.info({ jobId }, "Research Manager: Initial research complete. Enhancing results using direct LLM call...");
jobManager.updateJobStatus(jobId, JobStatus.RUNNING, 'Initial research complete. Enhancing results via LLM...');
sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Initial research complete. Enhancing results via LLM...');
logs.push(`[${new Date().toISOString()}] Perplexity research complete. Calling LLM for enhancement.`);
const enhancementPrompt = `Synthesize and structure the following initial research findings based on the original query.\n\nOriginal Query: ${query}\n\nInitial Research Findings:\n${researchResult}`;
const enhancedResearch = await performFormatAwareLlmCallWithCentralizedConfig(enhancementPrompt, RESEARCH_SYSTEM_PROMPT, 'research_enhancement', 'markdown', undefined, 0.4);
logger.info({ jobId }, "Research Manager: Enhancement completed.");
jobManager.updateJobStatus(jobId, JobStatus.RUNNING, 'Processing enhanced research...');
sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, 'Processing enhanced research...');
logs.push(`[${new Date().toISOString()}] LLM enhancement complete.`);
if (!enhancedResearch || typeof enhancedResearch !== 'string' || !enhancedResearch.trim().startsWith('# Research Report:')) {
logger.warn({ jobId, markdown: enhancedResearch?.substring(0, 100) }, 'Research enhancement returned empty or potentially invalid Markdown format.');
logs.push(`[${new Date().toISOString()}] Validation Error: LLM output invalid format.`);
throw new ToolExecutionError('Research enhancement returned empty or invalid Markdown content.');
}
const formattedResult = `${enhancedResearch}\n\n_Generated: ${new Date().toLocaleString()}_`;
logger.info({ jobId }, `Saving research to ${filePath}...`);
jobManager.updateJobStatus(jobId, JobStatus.RUNNING, `Saving research to file...`);
sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, `Saving research to file...`);
logs.push(`[${new Date().toISOString()}] Saving research to ${filePath}.`);
await fs.writeFile(filePath, formattedResult, 'utf8');
logger.info({ jobId }, `Research result saved to ${filePath}`);
logs.push(`[${new Date().toISOString()}] Research saved successfully.`);
sseNotifier.sendProgress(sessionId, jobId, JobStatus.RUNNING, `Research saved successfully.`);
const finalResult = {
content: [{ type: "text", text: `Research completed successfully and saved to: ${filePath}\n\n${formattedResult}` }],
isError: false
};
jobManager.setJobResult(jobId, finalResult);
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
logger.error({ err: error, jobId, tool: 'research', query }, `Research Manager Error: ${errorMsg}`);
logs.push(`[${new Date().toISOString()}] Error: ${errorMsg}`);
let appError;
const cause = error instanceof Error ? error : undefined;
if (error instanceof AppError) {
appError = error;
}
else {
appError = new ToolExecutionError(`Failed to perform research for query "${query}": ${errorMsg}`, { query, filePath }, cause);
}
const mcpError = new McpError(ErrorCode.InternalError, appError.message, appError.context);
const errorResult = {
content: [{ type: 'text', text: `Error during background job ${jobId}: ${mcpError.message}\n\nLogs:\n${logs.join('\n')}` }],
isError: true,
errorDetails: mcpError
};
jobManager.setJobResult(jobId, errorResult);
sseNotifier.sendProgress(sessionId, jobId, JobStatus.FAILED, `Job failed: ${mcpError.message}`);
}
});
return initialResponse;
};
const researchInputSchemaShape = {
query: z.string().min(3, { message: "Query must be at least 3 characters long." }).describe("The research query or topic to investigate")
};
const researchToolDefinition = {
name: "research-manager",
description: "Performs comprehensive research on any technical topic including frameworks, libraries, packages, tools, and best practices using Perplexity Sonar.",
inputSchema: researchInputSchemaShape,
executor: performResearch
};
if (!isToolRegistered(researchToolDefinition.name)) {
registerTool(researchToolDefinition);
logger.debug(`Tool "${researchToolDefinition.name}" registered successfully`);
}
else {
logger.debug(`Tool "${researchToolDefinition.name}" already registered, skipping`);
}