UNPKG

mcp-adr-analysis-server

Version:

MCP server for analyzing Architectural Decision Records and project architecture

449 lines (447 loc) 16.7 kB
/** * MCP Tasks Integration for Research Orchestrator * * This module provides standardized task tracking for research operations, * implementing ADR-020: MCP Tasks Integration Strategy. * * Key features: * - Creates MCP Tasks for research operations * - Returns research plans for LLM delegation (non-blocking) * - Tracks progress through research phases * - Supports cancellation between phases * * @see ADR-020: MCP Tasks Integration Strategy * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks */ import { getTaskManager } from './task-manager.js'; import { createComponentLogger } from './enhanced-logging.js'; const logger = createComponentLogger('ResearchTaskIntegration'); /** * Research phases that map to MCP Task phases */ export const RESEARCH_PHASES = [ 'initialization', 'project_files_search', 'knowledge_graph_query', 'environment_analysis', 'web_search', 'synthesis', ]; /** * Research Task Manager - Provides MCP Tasks integration for research operations * * This class wraps the TaskManager to provide research-specific functionality: * - Creates tasks with research phases * - Returns research plans for LLM delegation (non-blocking) * - Tracks research progress through multiple phases * - Supports cancellation between phases */ export class ResearchTaskManager { taskManager; activeContexts = new Map(); constructor(taskManager) { this.taskManager = taskManager ?? getTaskManager(); } /** * Initialize the research task manager */ async initialize() { await this.taskManager.initialize(); logger.info('ResearchTaskManager initialized'); } /** * Create a new research task and return a research plan for LLM delegation * * This method does NOT execute research - it returns a plan that the calling * LLM should execute using atomic tools. * * @returns The created task, context, and research plan for LLM delegation */ async createResearchTask(options) { const { question, projectPath, includeWebSearch = true, confidenceThreshold = 0.6 } = options; const task = await this.taskManager.createTask({ type: 'research', tool: 'research_orchestrator', phases: [...RESEARCH_PHASES], projectPath, ttl: 300000, // 5 minute TTL for research pollInterval: 1000, }); const context = { taskId: task.taskId, currentPhase: 'initialization', question, projectPath, cancelled: false, }; this.activeContexts.set(task.taskId, context); // Generate research plan for LLM delegation const plan = this.generateResearchPlan(task.taskId, question, projectPath, includeWebSearch, confidenceThreshold); logger.info('Research task created with LLM delegation plan', { taskId: task.taskId, question, projectPath, phasesCount: plan.phases.length, }); return { task, context, plan }; } /** * Generate a research plan for LLM delegation * * This creates a structured plan that tells the calling LLM what tools * to use and in what order to complete the research. */ generateResearchPlan(taskId, question, projectPath, includeWebSearch, confidenceThreshold) { const phases = [ { phase: 'project_files_search', tool: 'searchCodebase', params: { query: question, projectPath, includeAdr: true, maxResults: 20, }, purpose: 'Search project files for relevant code, documentation, and ADRs', expectedOutput: 'List of relevant files with content snippets and relevance scores', }, { phase: 'knowledge_graph_query', tool: 'query_knowledge_graph', params: { question, projectPath, }, purpose: 'Query the knowledge graph for architectural decisions and relationships', expectedOutput: 'Nodes and relationships relevant to the question', }, { phase: 'environment_analysis', tool: 'environment_analysis', params: { projectPath, analysisType: 'research', }, purpose: 'Analyze the runtime environment for relevant capabilities and context', expectedOutput: 'Environment capabilities and configuration relevant to the question', }, ]; if (includeWebSearch) { phases.push({ phase: 'web_search', tool: 'web_search', params: { query: question, maxResults: 5, }, purpose: 'Search the web for external information if internal sources are insufficient', condition: `Only execute if overall confidence from previous phases is below ${confidenceThreshold}`, expectedOutput: 'Web search results with relevant external information', }); } return { taskId, question, phases, synthesisInstructions: ` After executing the applicable phases, synthesize the results into a comprehensive answer: 1. **Combine Sources**: Merge information from all phases that returned useful data 2. **Calculate Confidence**: Weight each source by its confidence score: - project_files: 0.9 weight - knowledge_graph: 0.85 weight - environment: 0.95 weight - web_search: 0.7 weight 3. **Identify Conflicts**: Note any conflicting information between sources 4. **Provide Answer**: Generate a clear, actionable answer to the original question 5. **Cite Sources**: Reference which phase(s) provided each piece of information Report progress by calling updateResearchProgress() after each phase completes. `, expectedResultFormat: `{ "answer": "The synthesized answer to the question", "confidence": 0.0-1.0, "sources": [ { "type": "project_files|knowledge_graph|environment|web_search", "confidence": 0.0-1.0, "data": {...} } ], "phasesCompleted": ["phase1", "phase2", ...] }`, }; } /** * Get research task context */ getContext(taskId) { return this.activeContexts.get(taskId); } /** * Start a research phase */ async startPhase(taskId, phase, message) { const context = this.activeContexts.get(taskId); if (!context) { logger.warn('No context found for task', { taskId }); return; } if (context.cancelled) { throw new Error('Task was cancelled'); } context.currentPhase = phase; await this.taskManager.startPhase(taskId, phase); const phaseIndex = RESEARCH_PHASES.indexOf(phase); const phaseProgress = phaseIndex >= 0 ? Math.floor((phaseIndex / RESEARCH_PHASES.length) * 100) : 0; await this.taskManager.updateProgress({ taskId, progress: phaseProgress, phase, phaseProgress: 0, ...(message !== undefined && { message }), }); logger.info('Research phase started', { taskId, phase, progress: phaseProgress }); } /** * Update phase progress */ async updatePhaseProgress(taskId, phase, phaseProgress, message) { const context = this.activeContexts.get(taskId); if (!context) { return; } const phaseIndex = RESEARCH_PHASES.indexOf(phase); const phaseFraction = 100 / RESEARCH_PHASES.length; const baseProgress = phaseIndex * phaseFraction; const overallProgress = Math.floor(baseProgress + (phaseProgress / 100) * phaseFraction); await this.taskManager.updateProgress({ taskId, progress: overallProgress, phase, phaseProgress, ...(message !== undefined && { message }), }); } /** * Complete a research phase */ async completePhase(taskId, phase, _message) { const context = this.activeContexts.get(taskId); if (!context) { return; } await this.taskManager.completePhase(taskId, phase); logger.info('Research phase completed', { taskId, phase }); } /** * Fail a research phase */ async failPhase(taskId, phase, error) { const context = this.activeContexts.get(taskId); if (!context) { return; } await this.taskManager.failPhase(taskId, phase, error); logger.warn('Research phase failed', { taskId, phase, error }); } /** * Store project files search result */ async storeProjectFilesResult(taskId, result) { const context = this.activeContexts.get(taskId); if (!context) { return; } context.projectFilesResult = result; await this.taskManager.updateProgress({ taskId, progress: 20, message: `Found ${result.filesFound} files, ${result.relevantFiles.length} relevant (${Math.round(result.confidence * 100)}% confidence)`, }); logger.info('Project files result stored', { taskId, ...result }); } /** * Store knowledge graph query result */ async storeKnowledgeGraphResult(taskId, result) { const context = this.activeContexts.get(taskId); if (!context) { return; } context.knowledgeGraphResult = result; await this.taskManager.updateProgress({ taskId, progress: 40, message: `Found ${result.nodesFound} nodes, ${result.relationshipsFound} relationships (${Math.round(result.confidence * 100)}% confidence)`, }); logger.info('Knowledge graph result stored', { taskId, ...result }); } /** * Store environment analysis result */ async storeEnvironmentResult(taskId, result) { const context = this.activeContexts.get(taskId); if (!context) { return; } context.environmentResult = result; await this.taskManager.updateProgress({ taskId, progress: 60, message: `Found ${result.capabilitiesFound} capabilities (${Math.round(result.confidence * 100)}% confidence)`, }); logger.info('Environment result stored', { taskId, capabilitiesFound: result.capabilitiesFound, }); } /** * Store web search result */ async storeWebSearchResult(taskId, result) { const context = this.activeContexts.get(taskId); if (!context) { return; } context.webSearchResult = result; await this.taskManager.updateProgress({ taskId, progress: 80, message: `Found ${result.resultsFound} web results from ${result.sources.length} sources (${Math.round(result.confidence * 100)}% confidence)`, }); logger.info('Web search result stored', { taskId, ...result }); } /** * Store synthesized answer */ async storeSynthesizedAnswer(taskId, answer, confidence) { const context = this.activeContexts.get(taskId); if (!context) { return; } context.synthesizedAnswer = answer; context.overallConfidence = confidence; await this.taskManager.updateProgress({ taskId, progress: 95, message: `Answer synthesized (${Math.round(confidence * 100)}% confidence)`, }); logger.info('Synthesized answer stored', { taskId, confidence }); } /** * Check if task is cancelled */ async isCancelled(taskId) { const context = this.activeContexts.get(taskId); if (!context) { return false; } const task = await this.taskManager.getTask(taskId); if (task?.status === 'cancelled') { context.cancelled = true; } return context.cancelled; } /** * Cancel a research task */ async cancelTask(taskId, reason) { const context = this.activeContexts.get(taskId); if (context) { context.cancelled = true; } await this.taskManager.cancelTask(taskId, reason ?? 'Research cancelled by user'); this.activeContexts.delete(taskId); logger.info('Research task cancelled', { taskId, reason }); } /** * Complete a research task successfully */ async completeTask(taskId, result) { await this.taskManager.completeTask(taskId, result); this.activeContexts.delete(taskId); logger.info('Research task completed', { taskId, success: result.success }); } /** * Fail a research task */ async failTask(taskId, error) { await this.taskManager.failTask(taskId, error); this.activeContexts.delete(taskId); logger.error('Research task failed', undefined, { taskId, error }); } /** * Get task status */ async getTaskStatus(taskId) { const task = await this.taskManager.getTask(taskId); const context = this.activeContexts.get(taskId); return { task, context }; } } /** * Get the global ResearchTaskManager instance */ let globalResearchTaskManager = null; export function getResearchTaskManager() { if (!globalResearchTaskManager) { globalResearchTaskManager = new ResearchTaskManager(); } return globalResearchTaskManager; } /** * Reset the global ResearchTaskManager (for testing) */ export async function resetResearchTaskManager() { globalResearchTaskManager = null; } /** * Helper function to create a research task and get the LLM delegation plan * * This is the primary entry point for research operations. It: * 1. Creates an MCP Task for tracking * 2. Returns a research plan for the calling LLM to execute * 3. The LLM executes each phase using atomic tools * 4. The LLM reports progress back via the tracker interface * * @example * ```typescript * const { taskId, plan, tracker } = await createResearchWithDelegation({ * question: 'How does authentication work in this codebase?', * projectPath: '/path/to/project', * }); * * // LLM receives the plan and executes each phase: * // Phase 1: searchCodebase({ query: question, ... }) * // Phase 2: query_knowledge_graph({ question, ... }) * // etc. * * // After each phase, LLM reports progress: * await tracker.storeProjectFilesResult({ filesFound: 10, ... }); * await tracker.completePhase('project_files_search'); * * // Finally, LLM synthesizes and completes: * await tracker.storeSynthesizedAnswer(answer, confidence); * await tracker.complete({ success: true, data: { answer, ... } }); * ``` */ export async function createResearchWithDelegation(options) { const rtm = getResearchTaskManager(); await rtm.initialize(); const { task, context, plan } = await rtm.createResearchTask(options); const taskId = task.taskId; const tracker = { taskId, startPhase: (phase, message) => rtm.startPhase(taskId, phase, message), updatePhaseProgress: (phase, progress, message) => rtm.updatePhaseProgress(taskId, phase, progress, message), completePhase: (phase, message) => rtm.completePhase(taskId, phase, message), failPhase: (phase, error) => rtm.failPhase(taskId, phase, error), storeProjectFilesResult: result => rtm.storeProjectFilesResult(taskId, result), storeKnowledgeGraphResult: result => rtm.storeKnowledgeGraphResult(taskId, result), storeEnvironmentResult: result => rtm.storeEnvironmentResult(taskId, result), storeWebSearchResult: result => rtm.storeWebSearchResult(taskId, result), storeSynthesizedAnswer: (answer, confidence) => rtm.storeSynthesizedAnswer(taskId, answer, confidence), isCancelled: () => rtm.isCancelled(taskId), cancel: reason => rtm.cancelTask(taskId, reason), complete: result => rtm.completeTask(taskId, result), fail: error => rtm.failTask(taskId, error), getContext: () => context, }; return { taskId, plan, tracker }; } //# sourceMappingURL=research-task-integration.js.map