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.

208 lines (195 loc) 13.4 kB
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`); }