Skip to main content
Glama

mcp-adr-analysis-server

by tosin2013
mcp-planning-tool.ts42.1 kB
/** * MCP Project Planning Tool - Enhanced Project Planning and Workflow Management * * Bridges gaps between architectural decisions and implementation workflows by providing: * - Phase-based project management with milestone tracking * - Team resource allocation and capacity planning * - Visual progress tracking with Gantt-style views * - Risk analysis and automated blocker detection * - Executive reporting and status dashboards * * Integrates with existing ADR Analysis tools: * - Links with ADR tools for architectural decision tracking * - Syncs with TODO management for execution tracking * - Connects with development guidance for roadmap conversion * - Updates project health scoring system * * Cache Dependencies: * - Requires: .mcp-adr-cache/project-planning.json (main planning data) * - Uses: .mcp-adr-cache/todo-data.json (for task integration) * - Updates: .mcp-adr-cache/project-health-scores.json (project metrics) */ import { z } from 'zod'; import { promises as fs } from 'fs'; import { join, basename } from 'path'; import * as os from 'os'; import { McpAdrError } from '../types/index.js'; // TodoJsonManager removed - use mcp-shrimp-task-manager for task management // Project phase schema const ProjectPhaseSchema = z.object({ id: z.string(), name: z.string(), description: z.string().optional(), status: z.enum(['planning', 'active', 'completed', 'blocked', 'cancelled']), startDate: z.string().optional(), endDate: z.string().optional(), estimatedDuration: z.string(), actualDuration: z.string().optional(), dependencies: z.array(z.string()).default([]), // Other phase IDs blockers: z.array(z.string()).default([]), // Blocker descriptions milestones: z.array(z.string()).default([]), linkedAdrs: z.array(z.string()).default([]), // ADR files tasks: z.array(z.string()).default([]), // Task IDs from TODO system completion: z.number().min(0).max(100).default(0), riskLevel: z.enum(['low', 'medium', 'high', 'critical']).default('medium'), riskFactors: z.array(z.string()).default([]), resources: z .array( z.object({ role: z.string(), capacity: z.string(), // e.g., "40h/week" allocation: z.number().min(0).max(100), // percentage allocated to this phase }) ) .default([]), createdAt: z.string(), updatedAt: z.string(), }); // Team member schema const TeamMemberSchema = z.object({ id: z.string(), name: z.string(), role: z.string(), skills: z.array(z.string()).default([]), capacity: z.string(), // e.g., "40h/week" currentWorkload: z.number().min(0).max(100).default(0), // percentage utilization availability: z .object({ startDate: z.string().optional(), endDate: z.string().optional(), unavailableDates: z.array(z.string()).default([]), }) .optional(), }); // Project schema const ProjectPlanSchema = z.object({ id: z.string(), name: z.string(), description: z.string().optional(), status: z.enum(['planning', 'active', 'completed', 'on_hold', 'cancelled']), phases: z.array(ProjectPhaseSchema).default([]), team: z.array(TeamMemberSchema).default([]), milestones: z .array( z.object({ id: z.string(), name: z.string(), description: z.string().optional(), dueDate: z.string(), status: z.enum(['pending', 'achieved', 'delayed', 'cancelled']), criteria: z.array(z.string()).default([]), linkedPhases: z.array(z.string()).default([]), }) ) .default([]), risks: z .array( z.object({ id: z.string(), description: z.string(), impact: z.enum(['low', 'medium', 'high', 'critical']), probability: z.enum(['low', 'medium', 'high']), mitigation: z.string().optional(), owner: z.string().optional(), status: z.enum(['open', 'mitigated', 'closed']), }) ) .default([]), metadata: z.object({ createdAt: z.string(), updatedAt: z.string(), createdBy: z.string(), projectPath: z.string(), adrDirectory: z.string().default('docs/adrs'), linkedAdrs: z.array(z.string()).default([]), }), settings: z .object({ autoPhaseTransition: z.boolean().default(false), riskAssessmentInterval: z.string().default('weekly'), progressUpdateFrequency: z.string().default('daily'), }) .default({}), }); // Operation schemas const CreateProjectSchema = z.object({ operation: z.literal('create_project'), projectPath: z.string().describe('Project root path'), projectName: z.string().describe('Project name'), description: z.string().optional().describe('Project description'), phases: z .array( z.object({ name: z.string(), duration: z.string(), dependencies: z.array(z.string()).default([]), milestones: z.array(z.string()).default([]), }) ) .describe('Initial project phases'), team: z .array( z.object({ name: z.string(), role: z.string(), skills: z.array(z.string()).default([]), capacity: z.string(), }) ) .default([]) .describe('Team structure'), importFromAdrs: z.boolean().default(true).describe('Import phases from existing ADRs'), importFromTodos: z.boolean().default(true).describe('Import tasks from TODO system'), }); const ManagePhasesSchema = z.object({ operation: z.literal('manage_phases'), projectPath: z.string().describe('Project root path'), action: z .enum(['list', 'create', 'update', 'delete', 'transition']) .describe('Phase management action'), phaseId: z.string().optional().describe('Phase ID for update/delete/transition'), phaseData: z .object({ name: z.string().optional(), description: z.string().optional(), estimatedDuration: z.string().optional(), dependencies: z.array(z.string()).optional(), milestones: z.array(z.string()).optional(), linkedAdrs: z.array(z.string()).optional(), }) .optional() .describe('Phase data for create/update'), targetStatus: z .enum(['planning', 'active', 'completed', 'blocked', 'cancelled']) .optional() .describe('Target status for transition'), }); const TrackProgressSchema = z.object({ operation: z.literal('track_progress'), projectPath: z.string().describe('Project root path'), reportType: z .enum(['summary', 'detailed', 'gantt', 'milestones', 'risks']) .default('summary') .describe('Type of progress report'), timeframe: z .enum(['current', 'weekly', 'monthly', 'quarterly']) .default('current') .describe('Time frame for progress tracking'), includeVisuals: z.boolean().default(true).describe('Include visual progress indicators'), updateTaskProgress: z.boolean().default(true).describe('Sync progress from TODO system'), }); const ManageResourcesSchema = z.object({ operation: z.literal('manage_resources'), projectPath: z.string().describe('Project root path'), action: z .enum(['list', 'add', 'update', 'remove', 'allocate', 'optimize']) .describe('Resource management action'), memberId: z.string().optional().describe('Team member ID for individual actions'), memberData: z .object({ name: z.string().optional(), role: z.string().optional(), skills: z.array(z.string()).optional(), capacity: z.string().optional(), }) .optional() .describe('Team member data'), allocationData: z .object({ phaseId: z.string(), allocation: z.number().min(0).max(100), }) .optional() .describe('Resource allocation data'), }); const RiskAnalysisSchema = z.object({ operation: z.literal('risk_analysis'), projectPath: z.string().describe('Project root path'), analysisType: z .enum(['automated', 'manual', 'comprehensive']) .default('comprehensive') .describe('Type of risk analysis'), includeAdrRisks: z.boolean().default(true).describe('Analyze risks from ADR complexity'), includeDependencyRisks: z.boolean().default(true).describe('Analyze dependency chain risks'), includeResourceRisks: z.boolean().default(true).describe('Analyze resource allocation risks'), generateMitigation: z.boolean().default(true).describe('Generate mitigation strategies'), }); const GenerateReportsSchema = z.object({ operation: z.literal('generate_reports'), projectPath: z.string().describe('Project root path'), reportType: z .enum(['executive', 'status', 'health', 'team_performance', 'milestone_tracking']) .describe('Type of report to generate'), format: z.enum(['markdown', 'json', 'html']).default('markdown').describe('Report output format'), includeCharts: z.boolean().default(true).describe('Include progress charts and graphs'), timeframe: z .enum(['week', 'month', 'quarter', 'project']) .default('month') .describe('Report time frame'), }); // Main operation schema const McpPlanningSchema = z.union([ CreateProjectSchema, ManagePhasesSchema, TrackProgressSchema, ManageResourcesSchema, RiskAnalysisSchema, GenerateReportsSchema, ]); /** * Get planning cache directory */ function getPlanningCacheDir(projectPath: string): string { const projectName = basename(projectPath); return join(os.tmpdir(), projectName, 'cache'); } /** * Get planning data file path */ function getPlanningDataPath(projectPath: string): string { return join(getPlanningCacheDir(projectPath), 'project-planning.json'); } /** * Ensure cache directory exists */ async function ensureCacheDir(projectPath: string): Promise<void> { const cacheDir = getPlanningCacheDir(projectPath); try { await fs.mkdir(cacheDir, { recursive: true }); } catch { // Directory might already exist, that's ok } } /** * Load existing project planning data */ async function loadPlanningData( projectPath: string ): Promise<z.infer<typeof ProjectPlanSchema> | null> { const planningPath = getPlanningDataPath(projectPath); try { const data = await fs.readFile(planningPath, 'utf-8'); return ProjectPlanSchema.parse(JSON.parse(data)); } catch { return null; } } /** * Save project planning data */ async function savePlanningData( projectPath: string, data: z.infer<typeof ProjectPlanSchema> ): Promise<void> { await ensureCacheDir(projectPath); const planningPath = getPlanningDataPath(projectPath); // Update timestamps data.metadata.updatedAt = new Date().toISOString(); await fs.writeFile(planningPath, JSON.stringify(data, null, 2)); } /** * Generate unique ID */ function generateId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Create new project operation */ async function createProject(args: z.infer<typeof CreateProjectSchema>): Promise<any> { const { projectPath, projectName, description, phases, team, importFromAdrs, importFromTodos } = args; // Check if project already exists const existing = await loadPlanningData(projectPath); if (existing) { throw new McpAdrError( `Project already exists at ${projectPath}. Use manage_phases to modify existing project.`, 'PROJECT_EXISTS' ); } const now = new Date().toISOString(); const projectId = generateId(); // Create project phases const projectPhases: z.infer<typeof ProjectPhaseSchema>[] = phases.map(phase => ({ id: generateId(), name: phase.name, description: `Auto-generated phase: ${phase.name}`, status: 'planning' as const, estimatedDuration: phase.duration, dependencies: phase.dependencies, blockers: [], // Add missing field milestones: phase.milestones, linkedAdrs: [], tasks: [], completion: 0, riskLevel: 'medium' as const, riskFactors: [], resources: [], createdAt: now, updatedAt: now, })); // Create team members const teamMembers: z.infer<typeof TeamMemberSchema>[] = team.map(member => ({ id: generateId(), name: member.name, role: member.role, skills: member.skills, capacity: member.capacity, currentWorkload: 0, })); // Import from ADRs if requested if (importFromAdrs) { try { const { discoverAdrsInDirectory } = await import('../utils/adr-discovery.js'); const discoveryResult = await discoverAdrsInDirectory( join(projectPath, 'docs/adrs'), true, projectPath ); // Link relevant ADRs to phases for (const phase of projectPhases) { const relatedAdrs = discoveryResult.adrs.filter( (adr: any) => adr.title.toLowerCase().includes(phase.name.toLowerCase()) || adr.content.toLowerCase().includes(phase.name.toLowerCase()) ); phase.linkedAdrs = relatedAdrs.map((adr: any) => adr.file); } } catch (error) { console.warn('Failed to import ADRs:', error); } } // Import from TODO system if requested if (importFromTodos) { try { // TodoJsonManager removed - use mcp-shrimp-task-manager for task management console.warn( '⚠️ TodoJsonManager is deprecated and was removed in memory-centric transformation' ); // todoManager removed - skip loading todo data const todoData: any = null; if (todoData && todoData.tasks && Object.values(todoData.tasks).length > 0) { // Distribute tasks across phases based on categories or ADR links const tasksArray = Object.values(todoData.tasks); for (const phase of projectPhases) { const relatedTasks = tasksArray.filter( (task: any) => task.linkedAdrs.some((adr: string) => phase.linkedAdrs.includes(adr)) || task.category?.toLowerCase().includes(phase.name.toLowerCase()) ); phase.tasks = relatedTasks.map((task: any) => task.id); } } } catch (error) { console.warn('Failed to import TODO tasks:', error); } } // Create project plan const projectPlan: z.infer<typeof ProjectPlanSchema> = { id: projectId, name: projectName, description, status: 'planning', phases: projectPhases, team: teamMembers, milestones: [], risks: [], metadata: { createdAt: now, updatedAt: now, createdBy: 'mcp-planning-tool', projectPath, adrDirectory: 'docs/adrs', linkedAdrs: projectPhases.flatMap(p => p.linkedAdrs), }, settings: { autoPhaseTransition: false, riskAssessmentInterval: 'weekly', progressUpdateFrequency: 'daily', }, }; await savePlanningData(projectPath, projectPlan); return { content: [ { type: 'text', text: `# Project Created Successfully ## Project Overview - **Name**: ${projectName} - **ID**: ${projectId} - **Status**: Planning - **Phases**: ${projectPhases.length} - **Team Members**: ${teamMembers.length} ## Project Phases ${projectPhases .map( (phase, index) => ` ### Phase ${index + 1}: ${phase.name} - **Duration**: ${phase.estimatedDuration} - **Dependencies**: ${phase.dependencies.length > 0 ? phase.dependencies.join(', ') : 'None'} - **Milestones**: ${phase.milestones.length > 0 ? phase.milestones.join(', ') : 'None'} - **Linked ADRs**: ${phase.linkedAdrs.length} - **Tasks**: ${phase.tasks.length} ` ) .join('')} ## Team Structure ${teamMembers .map( member => ` ### ${member.name} - ${member.role} - **Capacity**: ${member.capacity} - **Skills**: ${member.skills.join(', ')} - **Current Workload**: ${member.currentWorkload}% ` ) .join('')} ## Next Steps 1. Use \`manage_phases\` to refine phase details 2. Use \`manage_resources\` to allocate team members to phases 3. Use \`track_progress\` to monitor project advancement 4. Use \`risk_analysis\` to identify potential issues ## Integration Status - **ADR Import**: ${importFromAdrs ? `✅ Linked ${projectPlan.metadata.linkedAdrs.length} ADRs` : '❌ Skipped'} - **TODO Import**: ${importFromTodos ? `✅ Imported tasks from TODO system` : '❌ Skipped'} Project planning data saved to: \`.mcp-adr-cache/project-planning.json\` `, }, ], }; } /** * Manage phases operation */ async function managePhases(args: z.infer<typeof ManagePhasesSchema>): Promise<any> { const { projectPath, action, phaseId, phaseData, targetStatus } = args; const projectPlan = await loadPlanningData(projectPath); if (!projectPlan) { throw new McpAdrError( `No project found at ${projectPath}. Use create_project first.`, 'PROJECT_NOT_FOUND' ); } const now = new Date().toISOString(); switch (action) { case 'list': return { content: [ { type: 'text', text: `# Project Phases ## ${projectPlan.name} ${projectPlan.phases .map( (phase, index) => ` ### Phase ${index + 1}: ${phase.name} - **Status**: ${phase.status} - **Progress**: ${phase.completion}% - **Duration**: ${phase.estimatedDuration} ${phase.actualDuration ? `(Actual: ${phase.actualDuration})` : ''} - **Dependencies**: ${phase.dependencies.length > 0 ? phase.dependencies.join(', ') : 'None'} - **Blockers**: ${phase.blockers.length > 0 ? phase.blockers.join(', ') : 'None'} - **Risk Level**: ${phase.riskLevel} - **Tasks**: ${phase.tasks.length} - **Linked ADRs**: ${phase.linkedAdrs.length} ` ) .join('')} Total phases: ${projectPlan.phases.length} `, }, ], }; case 'create': { if (!phaseData?.name) { throw new McpAdrError('Phase name is required for creation', 'INVALID_INPUT'); } const newPhase: z.infer<typeof ProjectPhaseSchema> = { id: generateId(), name: phaseData.name, description: phaseData.description || `New phase: ${phaseData.name}`, status: 'planning', estimatedDuration: phaseData.estimatedDuration || 'TBD', dependencies: phaseData.dependencies || [], blockers: [], // Add missing field milestones: phaseData.milestones || [], linkedAdrs: phaseData.linkedAdrs || [], tasks: [], completion: 0, riskLevel: 'medium', riskFactors: [], resources: [], createdAt: now, updatedAt: now, }; projectPlan.phases.push(newPhase); await savePlanningData(projectPath, projectPlan); return { content: [ { type: 'text', text: `# Phase Created **${newPhase.name}** has been added to the project. - **ID**: ${newPhase.id} - **Status**: ${newPhase.status} - **Duration**: ${newPhase.estimatedDuration} - **Dependencies**: ${newPhase.dependencies.join(', ') || 'None'} Use \`manage_resources\` to allocate team members to this phase. `, }, ], }; } case 'update': { if (!phaseId) { throw new McpAdrError('Phase ID is required for update', 'INVALID_INPUT'); } const phaseIndex = projectPlan.phases.findIndex(p => p.id === phaseId); if (phaseIndex === -1) { throw new McpAdrError(`Phase with ID ${phaseId} not found`, 'PHASE_NOT_FOUND'); } const phase = projectPlan.phases[phaseIndex]; if (!phase) { throw new McpAdrError(`Phase with ID ${phaseId} not found`, 'PHASE_NOT_FOUND'); } if (phaseData) { Object.assign(phase, { ...phaseData, updatedAt: now, }); } await savePlanningData(projectPath, projectPlan); return { content: [ { type: 'text', text: `# Phase Updated **${phase.name}** has been updated successfully. Updated fields: ${Object.keys(phaseData || {}).join(', ')} `, }, ], }; } case 'transition': { if (!phaseId || !targetStatus) { throw new McpAdrError( 'Phase ID and target status are required for transition', 'INVALID_INPUT' ); } const transitionPhaseIndex = projectPlan.phases.findIndex(p => p.id === phaseId); if (transitionPhaseIndex === -1) { throw new McpAdrError(`Phase with ID ${phaseId} not found`, 'PHASE_NOT_FOUND'); } const transitionPhase = projectPlan.phases[transitionPhaseIndex]; if (!transitionPhase) { throw new McpAdrError(`Phase with ID ${phaseId} not found`, 'PHASE_NOT_FOUND'); } const oldStatus = transitionPhase.status; transitionPhase.status = targetStatus; transitionPhase.updatedAt = now; // Auto-calculate completion based on status if (targetStatus === 'completed') { transitionPhase.completion = 100; } else if (targetStatus === 'active' && transitionPhase.completion === 0) { transitionPhase.completion = 10; // Started } await savePlanningData(projectPath, projectPlan); return { content: [ { type: 'text', text: `# Phase Transition Complete **${transitionPhase.name}** status changed from **${oldStatus}** to **${targetStatus}**. Current completion: ${transitionPhase.completion}% `, }, ], }; } case 'delete': { if (!phaseId) { throw new McpAdrError('Phase ID is required for deletion', 'INVALID_INPUT'); } const deleteIndex = projectPlan.phases.findIndex(p => p.id === phaseId); if (deleteIndex === -1) { throw new McpAdrError(`Phase with ID ${phaseId} not found`, 'PHASE_NOT_FOUND'); } const deletedPhase = projectPlan.phases.splice(deleteIndex, 1)[0]; if (!deletedPhase) { throw new McpAdrError(`Phase with ID ${phaseId} not found`, 'PHASE_NOT_FOUND'); } await savePlanningData(projectPath, projectPlan); return { content: [ { type: 'text', text: `# Phase Deleted **${deletedPhase.name}** has been removed from the project. Remaining phases: ${projectPlan.phases.length} `, }, ], }; } default: throw new McpAdrError(`Unknown phase action: ${action}`, 'INVALID_INPUT'); } } /** * Track progress operation */ async function trackProgress(args: z.infer<typeof TrackProgressSchema>): Promise<any> { const { projectPath, reportType, includeVisuals, updateTaskProgress } = args; const projectPlan = await loadPlanningData(projectPath); if (!projectPlan) { throw new McpAdrError( `No project found at ${projectPath}. Use create_project first.`, 'PROJECT_NOT_FOUND' ); } // Update task progress from TODO system if requested if (updateTaskProgress) { try { // TodoJsonManager removed - use mcp-shrimp-task-manager for task management console.warn( '⚠️ TodoJsonManager is deprecated and was removed in memory-centric transformation' ); // todoManager removed - skip loading todo data const todoData: any = null; if (todoData && todoData.tasks) { const tasksArray = Object.values(todoData.tasks); for (const phase of projectPlan.phases) { const phaseTasks = tasksArray.filter((task: any) => phase.tasks.includes(task.id)); if (phaseTasks.length > 0) { const completedTasks = phaseTasks.filter( (task: any) => task.status === 'completed' ).length; phase.completion = Math.round((completedTasks / phaseTasks.length) * 100); } } await savePlanningData(projectPath, projectPlan); } } catch (error) { console.warn('Failed to update task progress:', error); } } // Calculate overall project progress const totalPhases = projectPlan.phases.length; const completedPhases = projectPlan.phases.filter(p => p.status === 'completed').length; const activePhases = projectPlan.phases.filter(p => p.status === 'active').length; const blockedPhases = projectPlan.phases.filter(p => p.status === 'blocked').length; const overallProgress = totalPhases > 0 ? Math.round( projectPlan.phases.reduce((sum, phase) => sum + phase.completion, 0) / totalPhases ) : 0; switch (reportType) { case 'summary': return { content: [ { type: 'text', text: `# Project Progress Summary ## ${projectPlan.name} ### Overall Status - **Progress**: ${overallProgress}% ${includeVisuals ? getProgressBar(overallProgress) : ''} - **Status**: ${projectPlan.status} - **Total Phases**: ${totalPhases} - **Completed**: ${completedPhases} - **Active**: ${activePhases} - **Blocked**: ${blockedPhases} ### Phase Progress ${projectPlan.phases .map( phase => ` #### ${phase.name} - **Status**: ${phase.status} - **Progress**: ${phase.completion}% ${includeVisuals ? getProgressBar(phase.completion) : ''} - **Tasks**: ${phase.tasks.length} - **Blockers**: ${phase.blockers.length} ` ) .join('')} ### Current Focus ${ activePhases > 0 ? `Currently working on ${activePhases} phase(s):\n${projectPlan.phases .filter(p => p.status === 'active') .map(p => `- ${p.name}`) .join('\n')}` : 'No active phases' } ${ blockedPhases > 0 ? `\n⚠️ **Blocked Phases**: ${projectPlan.phases .filter(p => p.status === 'blocked') .map(p => p.name) .join(', ')}` : '' } `, }, ], }; case 'detailed': return { content: [ { type: 'text', text: `# Detailed Progress Report ## ${projectPlan.name} ### Project Metrics - **Overall Progress**: ${overallProgress}% - **Team Utilization**: ${calculateTeamUtilization(projectPlan)}% - **Risk Level**: ${calculateProjectRisk(projectPlan)} - **Timeline Status**: ${calculateTimelineStatus(projectPlan)} ${projectPlan.phases .map( phase => ` ### ${phase.name} - **Status**: ${phase.status} - **Progress**: ${phase.completion}% ${includeVisuals ? getProgressBar(phase.completion) : ''} - **Duration**: ${phase.estimatedDuration} ${phase.actualDuration ? `(Actual: ${phase.actualDuration})` : ''} - **Risk Level**: ${phase.riskLevel} - **Dependencies**: ${phase.dependencies.length > 0 ? phase.dependencies.join(', ') : 'None'} - **Blockers**: ${phase.blockers.length > 0 ? phase.blockers.map(b => `⚠️ ${b}`).join('\n ') : 'None'} - **Tasks**: ${phase.tasks.length} tasks - **Linked ADRs**: ${phase.linkedAdrs.length} ADRs - **Resources**: ${phase.resources.length} team members allocated ${phase.riskFactors.length > 0 ? `**Risk Factors**:\n${phase.riskFactors.map(r => `- ${r}`).join('\n')}` : ''} ` ) .join('')} `, }, ], }; case 'milestones': return { content: [ { type: 'text', text: `# Milestone Tracking ## ${projectPlan.name} ### Project Milestones ${ projectPlan.milestones.length > 0 ? projectPlan.milestones .map( milestone => ` #### ${milestone.name} - **Status**: ${milestone.status} - **Due Date**: ${milestone.dueDate} - **Linked Phases**: ${milestone.linkedPhases.join(', ') || 'None'} - **Criteria**: ${milestone.criteria.join(', ') || 'None'} ` ) .join('') : 'No project milestones defined' } ### Phase Milestones ${projectPlan.phases .map( phase => ` #### ${phase.name} ${ phase.milestones.length > 0 ? phase.milestones.map(m => `- ${m}`).join('\n') : '- No milestones defined' } ` ) .join('')} `, }, ], }; default: return { content: [ { type: 'text', text: `Progress tracking for report type "${reportType}" is not yet implemented.`, }, ], }; } } /** * Manage resources operation */ async function manageResources(args: z.infer<typeof ManageResourcesSchema>): Promise<any> { const { projectPath, action, memberData } = args; const projectPlan = await loadPlanningData(projectPath); if (!projectPlan) { throw new McpAdrError( `No project found at ${projectPath}. Use create_project first.`, 'PROJECT_NOT_FOUND' ); } switch (action) { case 'list': return { content: [ { type: 'text', text: `# Team Resources ## ${projectPlan.name} ### Team Members ${projectPlan.team .map( member => ` #### ${member.name} - ${member.role} - **Capacity**: ${member.capacity} - **Current Workload**: ${member.currentWorkload}% - **Skills**: ${member.skills.join(', ') || 'None specified'} - **Availability**: ${member.availability ? 'Configured' : 'Not set'} ` ) .join('')} ### Resource Allocation ${projectPlan.phases .map( phase => ` #### ${phase.name} ${ phase.resources.length > 0 ? phase.resources .map(resource => `- ${resource.role}: ${resource.allocation}% (${resource.capacity})`) .join('\n') : '- No resources allocated' } ` ) .join('')} Total team members: ${projectPlan.team.length} `, }, ], }; case 'add': { if (!memberData?.name || !memberData?.role) { throw new McpAdrError('Member name and role are required', 'INVALID_INPUT'); } const newMember: z.infer<typeof TeamMemberSchema> = { id: generateId(), name: memberData.name, role: memberData.role, skills: memberData.skills || [], capacity: memberData.capacity || '40h/week', currentWorkload: 0, }; projectPlan.team.push(newMember); await savePlanningData(projectPath, projectPlan); return { content: [ { type: 'text', text: `# Team Member Added **${newMember.name}** has been added to the team. - **Role**: ${newMember.role} - **Capacity**: ${newMember.capacity} - **Skills**: ${newMember.skills.join(', ') || 'None specified'} Use \`allocate\` action to assign them to project phases. `, }, ], }; } default: return { content: [ { type: 'text', text: `Resource management for action "${action}" is not yet implemented.`, }, ], }; } } /** * Risk analysis operation */ async function riskAnalysis(args: z.infer<typeof RiskAnalysisSchema>): Promise<any> { const { projectPath, includeAdrRisks, includeDependencyRisks, includeResourceRisks, generateMitigation, } = args; const projectPlan = await loadPlanningData(projectPath); if (!projectPlan) { throw new McpAdrError( `No project found at ${projectPath}. Use create_project first.`, 'PROJECT_NOT_FOUND' ); } const risks: Array<{ category: string; description: string; impact: string; probability: string; mitigation?: string; }> = []; // Analyze ADR-based risks if (includeAdrRisks) { for (const phase of projectPlan.phases) { if (phase.linkedAdrs.length === 0) { const riskObj: any = { category: 'Architecture', description: `Phase "${phase.name}" has no linked ADRs - architectural decisions may be unclear`, impact: 'medium', probability: 'high', }; if (generateMitigation) { riskObj.mitigation = 'Review and create ADRs for architectural decisions in this phase'; } risks.push(riskObj); } if (phase.linkedAdrs.length > 5) { const riskObj: any = { category: 'Complexity', description: `Phase "${phase.name}" has many ADRs (${phase.linkedAdrs.length}) - high complexity risk`, impact: 'high', probability: 'medium', }; if (generateMitigation) { riskObj.mitigation = 'Consider breaking this phase into smaller, more manageable sub-phases'; } risks.push(riskObj); } } } // Analyze dependency risks if (includeDependencyRisks) { for (const phase of projectPlan.phases) { if (phase.dependencies.length > 3) { const riskObj: any = { category: 'Dependencies', description: `Phase "${phase.name}" has multiple dependencies (${phase.dependencies.length}) - high coordination risk`, impact: 'high', probability: 'medium', }; if (generateMitigation) { riskObj.mitigation = 'Review and simplify dependencies, create detailed coordination plan'; } risks.push(riskObj); } // Check for circular dependencies (simplified check) const dependentPhases = projectPlan.phases.filter( p => p.dependencies.includes(phase.id) && phase.dependencies.includes(p.id) ); if (dependentPhases.length > 0) { const riskObj: any = { category: 'Dependencies', description: `Potential circular dependency detected between "${phase.name}" and other phases`, impact: 'critical', probability: 'high', }; if (generateMitigation) { riskObj.mitigation = 'Review and restructure phase dependencies to eliminate circular references'; } risks.push(riskObj); } } } // Analyze resource risks if (includeResourceRisks) { const totalWorkload = projectPlan.team.reduce((sum, member) => sum + member.currentWorkload, 0); const avgWorkload = projectPlan.team.length > 0 ? totalWorkload / projectPlan.team.length : 0; if (avgWorkload > 80) { const riskObj: any = { category: 'Resources', description: `Team is overallocated (${avgWorkload.toFixed(1)}% average workload)`, impact: 'high', probability: 'high', }; if (generateMitigation) { riskObj.mitigation = 'Rebalance workload or consider adding team members'; } risks.push(riskObj); } if (projectPlan.team.length < projectPlan.phases.length / 2) { const riskObj: any = { category: 'Resources', description: `Limited team size (${projectPlan.team.length} members) for ${projectPlan.phases.length} phases`, impact: 'medium', probability: 'medium', }; if (generateMitigation) { riskObj.mitigation = 'Consider increasing team size or extending timeline'; } risks.push(riskObj); } } // Calculate overall risk score const criticalRisks = risks.filter(r => r.impact === 'critical').length; const highRisks = risks.filter(r => r.impact === 'high').length; const mediumRisks = risks.filter(r => r.impact === 'medium').length; const riskScore = criticalRisks * 10 + highRisks * 5 + mediumRisks * 2; let riskLevel = 'low'; if (riskScore > 20) riskLevel = 'critical'; else if (riskScore > 10) riskLevel = 'high'; else if (riskScore > 5) riskLevel = 'medium'; return { content: [ { type: 'text', text: `# Risk Analysis Report ## ${projectPlan.name} ### Overall Risk Assessment - **Risk Level**: ${riskLevel.toUpperCase()} - **Risk Score**: ${riskScore} - **Total Risks**: ${risks.length} - **Critical**: ${criticalRisks} - **High**: ${highRisks} - **Medium**: ${mediumRisks} ### Identified Risks ${risks .map( (risk, index) => ` #### Risk ${index + 1}: ${risk.category} - **Description**: ${risk.description} - **Impact**: ${risk.impact} - **Probability**: ${risk.probability} ${risk.mitigation ? `- **Mitigation**: ${risk.mitigation}` : ''} ` ) .join('')} ### Recommendations ${ riskLevel === 'critical' ? '🚨 **Critical risks detected** - Immediate action required before proceeding' : riskLevel === 'high' ? '⚠️ **High risks present** - Review and mitigate before major milestones' : '✅ **Manageable risk level** - Continue with normal monitoring' } ### Next Steps 1. Review and prioritize risk mitigation strategies 2. Assign risk owners for high-impact items 3. Schedule regular risk assessment reviews 4. Update project timeline if necessary `, }, ], }; } /** * Generate reports operation */ async function generateReports(args: z.infer<typeof GenerateReportsSchema>): Promise<any> { const { projectPath, reportType, includeCharts, timeframe } = args; const projectPlan = await loadPlanningData(projectPath); if (!projectPlan) { throw new McpAdrError( `No project found at ${projectPath}. Use create_project first.`, 'PROJECT_NOT_FOUND' ); } switch (reportType) { case 'executive': { const overallProgress = calculateOverallProgress(projectPlan); const teamUtilization = calculateTeamUtilization(projectPlan); const riskLevel = calculateProjectRisk(projectPlan); return { content: [ { type: 'text', text: `# Executive Summary ## ${projectPlan.name} ### Key Metrics - **Overall Progress**: ${overallProgress}% ${includeCharts ? getProgressBar(overallProgress) : ''} - **Team Utilization**: ${teamUtilization}% - **Risk Level**: ${riskLevel} - **Active Phases**: ${projectPlan.phases.filter(p => p.status === 'active').length} - **Completed Phases**: ${projectPlan.phases.filter(p => p.status === 'completed').length} ### Current Status ${ projectPlan.status === 'active' ? '🟢 Project is actively progressing' : projectPlan.status === 'completed' ? '✅ Project completed successfully' : '🟡 Project in planning phase' } ### Key Highlights - Total phases: ${projectPlan.phases.length} - Team size: ${projectPlan.team.length} members - Linked ADRs: ${projectPlan.metadata.linkedAdrs.length} - Risk level: ${riskLevel} ### Next Milestones ${ projectPlan.milestones .filter(m => m.status === 'pending') .slice(0, 3) .map(milestone => `- ${milestone.name} (Due: ${milestone.dueDate})`) .join('\n') || 'No upcoming milestones' } ### Blockers & Risks ${ projectPlan.phases.filter(p => p.blockers.length > 0).length > 0 ? `⚠️ ${projectPlan.phases.filter(p => p.blockers.length > 0).length} phases have blockers` : '✅ No major blockers reported' } *Report generated: ${new Date().toISOString()}* `, }, ], }; } case 'status': return { content: [ { type: 'text', text: `# Status Report - ${timeframe} ## ${projectPlan.name} ### Phase Status ${projectPlan.phases .map( phase => ` #### ${phase.name} - **Status**: ${phase.status} - **Progress**: ${phase.completion}% - **Timeline**: ${phase.estimatedDuration} - **Team**: ${phase.resources.length} members - **Blockers**: ${phase.blockers.length} ` ) .join('')} ### Team Status ${projectPlan.team .map( member => ` #### ${member.name} - **Role**: ${member.role} - **Workload**: ${member.currentWorkload}% - **Capacity**: ${member.capacity} ` ) .join('')} ### Recent Updates - Last updated: ${projectPlan.metadata.updatedAt} - Total phases: ${projectPlan.phases.length} - Active work streams: ${projectPlan.phases.filter(p => p.status === 'active').length} `, }, ], }; default: return { content: [ { type: 'text', text: `Report generation for type "${reportType}" is not yet implemented.`, }, ], }; } } /** * Helper functions */ function getProgressBar(percentage: number, width: number = 20): string { const filled = Math.round((percentage / 100) * width); const empty = width - filled; return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`; } function calculateOverallProgress(projectPlan: z.infer<typeof ProjectPlanSchema>): number { if (projectPlan.phases.length === 0) return 0; return Math.round( projectPlan.phases.reduce((sum, phase) => sum + phase.completion, 0) / projectPlan.phases.length ); } function calculateTeamUtilization(projectPlan: z.infer<typeof ProjectPlanSchema>): number { if (projectPlan.team.length === 0) return 0; return Math.round( projectPlan.team.reduce((sum, member) => sum + member.currentWorkload, 0) / projectPlan.team.length ); } function calculateProjectRisk(projectPlan: z.infer<typeof ProjectPlanSchema>): string { const blockedPhases = projectPlan.phases.filter(p => p.status === 'blocked').length; const highRiskPhases = projectPlan.phases.filter( p => p.riskLevel === 'high' || p.riskLevel === 'critical' ).length; const overloadedTeam = projectPlan.team.filter(m => m.currentWorkload > 90).length; if (blockedPhases > 0 || highRiskPhases > projectPlan.phases.length * 0.3 || overloadedTeam > 0) { return 'HIGH'; } else if (highRiskPhases > 0 || overloadedTeam > projectPlan.team.length * 0.5) { return 'MEDIUM'; } else { return 'LOW'; } } function calculateTimelineStatus(projectPlan: z.infer<typeof ProjectPlanSchema>): string { const blockedPhases = projectPlan.phases.filter(p => p.status === 'blocked').length; const completedOnTime = projectPlan.phases.filter(p => p.status === 'completed').length; if (blockedPhases > 0) return 'AT RISK'; if (completedOnTime > projectPlan.phases.length * 0.5) return 'ON TRACK'; return 'MONITORING'; } /** * Main export function */ export async function mcpPlanning(args: any): Promise<any> { try { const validatedArgs = McpPlanningSchema.parse(args); switch (validatedArgs.operation) { case 'create_project': return await createProject(validatedArgs); case 'manage_phases': return await managePhases(validatedArgs); case 'track_progress': return await trackProgress(validatedArgs); case 'manage_resources': return await manageResources(validatedArgs); case 'risk_analysis': return await riskAnalysis(validatedArgs); case 'generate_reports': return await generateReports(validatedArgs); default: throw new McpAdrError( `Unknown operation: ${(validatedArgs as any).operation}`, 'UNKNOWN_OPERATION' ); } } catch (error) { if (error instanceof z.ZodError) { throw new McpAdrError( `Validation error: ${error.errors.map(e => e.message).join(', ')}`, 'VALIDATION_ERROR' ); } throw error; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tosin2013/mcp-adr-analysis-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server