mcp-adr-analysis-server 2.0.13 → 2.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1096 @@
1
+ /**
2
+ * MCP Project Planning Tool - Enhanced Project Planning and Workflow Management
3
+ *
4
+ * Bridges gaps between architectural decisions and implementation workflows by providing:
5
+ * - Phase-based project management with milestone tracking
6
+ * - Team resource allocation and capacity planning
7
+ * - Visual progress tracking with Gantt-style views
8
+ * - Risk analysis and automated blocker detection
9
+ * - Executive reporting and status dashboards
10
+ *
11
+ * Integrates with existing ADR Analysis tools:
12
+ * - Links with ADR tools for architectural decision tracking
13
+ * - Syncs with TODO management for execution tracking
14
+ * - Connects with development guidance for roadmap conversion
15
+ * - Updates project health scoring system
16
+ *
17
+ * Cache Dependencies:
18
+ * - Requires: .mcp-adr-cache/project-planning.json (main planning data)
19
+ * - Uses: .mcp-adr-cache/todo-data.json (for task integration)
20
+ * - Updates: .mcp-adr-cache/project-health-scores.json (project metrics)
21
+ */
22
+ import { z } from 'zod';
23
+ import { promises as fs } from 'fs';
24
+ import { join } from 'path';
25
+ import { McpAdrError } from '../types/index.js';
26
+ import { TodoJsonManager } from '../utils/todo-json-manager.js';
27
+ // Project phase schema
28
+ const ProjectPhaseSchema = z.object({
29
+ id: z.string(),
30
+ name: z.string(),
31
+ description: z.string().optional(),
32
+ status: z.enum(['planning', 'active', 'completed', 'blocked', 'cancelled']),
33
+ startDate: z.string().optional(),
34
+ endDate: z.string().optional(),
35
+ estimatedDuration: z.string(),
36
+ actualDuration: z.string().optional(),
37
+ dependencies: z.array(z.string()).default([]), // Other phase IDs
38
+ blockers: z.array(z.string()).default([]), // Blocker descriptions
39
+ milestones: z.array(z.string()).default([]),
40
+ linkedAdrs: z.array(z.string()).default([]), // ADR files
41
+ tasks: z.array(z.string()).default([]), // Task IDs from TODO system
42
+ completion: z.number().min(0).max(100).default(0),
43
+ riskLevel: z.enum(['low', 'medium', 'high', 'critical']).default('medium'),
44
+ riskFactors: z.array(z.string()).default([]),
45
+ resources: z.array(z.object({
46
+ role: z.string(),
47
+ capacity: z.string(), // e.g., "40h/week"
48
+ allocation: z.number().min(0).max(100) // percentage allocated to this phase
49
+ })).default([]),
50
+ createdAt: z.string(),
51
+ updatedAt: z.string()
52
+ });
53
+ // Team member schema
54
+ const TeamMemberSchema = z.object({
55
+ id: z.string(),
56
+ name: z.string(),
57
+ role: z.string(),
58
+ skills: z.array(z.string()).default([]),
59
+ capacity: z.string(), // e.g., "40h/week"
60
+ currentWorkload: z.number().min(0).max(100).default(0), // percentage utilization
61
+ availability: z.object({
62
+ startDate: z.string().optional(),
63
+ endDate: z.string().optional(),
64
+ unavailableDates: z.array(z.string()).default([])
65
+ }).optional()
66
+ });
67
+ // Project schema
68
+ const ProjectPlanSchema = z.object({
69
+ id: z.string(),
70
+ name: z.string(),
71
+ description: z.string().optional(),
72
+ status: z.enum(['planning', 'active', 'completed', 'on_hold', 'cancelled']),
73
+ phases: z.array(ProjectPhaseSchema).default([]),
74
+ team: z.array(TeamMemberSchema).default([]),
75
+ milestones: z.array(z.object({
76
+ id: z.string(),
77
+ name: z.string(),
78
+ description: z.string().optional(),
79
+ dueDate: z.string(),
80
+ status: z.enum(['pending', 'achieved', 'delayed', 'cancelled']),
81
+ criteria: z.array(z.string()).default([]),
82
+ linkedPhases: z.array(z.string()).default([])
83
+ })).default([]),
84
+ risks: z.array(z.object({
85
+ id: z.string(),
86
+ description: z.string(),
87
+ impact: z.enum(['low', 'medium', 'high', 'critical']),
88
+ probability: z.enum(['low', 'medium', 'high']),
89
+ mitigation: z.string().optional(),
90
+ owner: z.string().optional(),
91
+ status: z.enum(['open', 'mitigated', 'closed'])
92
+ })).default([]),
93
+ metadata: z.object({
94
+ createdAt: z.string(),
95
+ updatedAt: z.string(),
96
+ createdBy: z.string(),
97
+ projectPath: z.string(),
98
+ adrDirectory: z.string().default('docs/adrs'),
99
+ linkedAdrs: z.array(z.string()).default([])
100
+ }),
101
+ settings: z.object({
102
+ autoPhaseTransition: z.boolean().default(false),
103
+ riskAssessmentInterval: z.string().default('weekly'),
104
+ progressUpdateFrequency: z.string().default('daily')
105
+ }).default({})
106
+ });
107
+ // Operation schemas
108
+ const CreateProjectSchema = z.object({
109
+ operation: z.literal('create_project'),
110
+ projectPath: z.string().describe('Project root path'),
111
+ projectName: z.string().describe('Project name'),
112
+ description: z.string().optional().describe('Project description'),
113
+ phases: z.array(z.object({
114
+ name: z.string(),
115
+ duration: z.string(),
116
+ dependencies: z.array(z.string()).default([]),
117
+ milestones: z.array(z.string()).default([])
118
+ })).describe('Initial project phases'),
119
+ team: z.array(z.object({
120
+ name: z.string(),
121
+ role: z.string(),
122
+ skills: z.array(z.string()).default([]),
123
+ capacity: z.string()
124
+ })).default([]).describe('Team structure'),
125
+ importFromAdrs: z.boolean().default(true).describe('Import phases from existing ADRs'),
126
+ importFromTodos: z.boolean().default(true).describe('Import tasks from TODO system')
127
+ });
128
+ const ManagePhasesSchema = z.object({
129
+ operation: z.literal('manage_phases'),
130
+ projectPath: z.string().describe('Project root path'),
131
+ action: z.enum(['list', 'create', 'update', 'delete', 'transition']).describe('Phase management action'),
132
+ phaseId: z.string().optional().describe('Phase ID for update/delete/transition'),
133
+ phaseData: z.object({
134
+ name: z.string().optional(),
135
+ description: z.string().optional(),
136
+ estimatedDuration: z.string().optional(),
137
+ dependencies: z.array(z.string()).optional(),
138
+ milestones: z.array(z.string()).optional(),
139
+ linkedAdrs: z.array(z.string()).optional()
140
+ }).optional().describe('Phase data for create/update'),
141
+ targetStatus: z.enum(['planning', 'active', 'completed', 'blocked', 'cancelled']).optional().describe('Target status for transition')
142
+ });
143
+ const TrackProgressSchema = z.object({
144
+ operation: z.literal('track_progress'),
145
+ projectPath: z.string().describe('Project root path'),
146
+ reportType: z.enum(['summary', 'detailed', 'gantt', 'milestones', 'risks']).default('summary').describe('Type of progress report'),
147
+ timeframe: z.enum(['current', 'weekly', 'monthly', 'quarterly']).default('current').describe('Time frame for progress tracking'),
148
+ includeVisuals: z.boolean().default(true).describe('Include visual progress indicators'),
149
+ updateTaskProgress: z.boolean().default(true).describe('Sync progress from TODO system')
150
+ });
151
+ const ManageResourcesSchema = z.object({
152
+ operation: z.literal('manage_resources'),
153
+ projectPath: z.string().describe('Project root path'),
154
+ action: z.enum(['list', 'add', 'update', 'remove', 'allocate', 'optimize']).describe('Resource management action'),
155
+ memberId: z.string().optional().describe('Team member ID for individual actions'),
156
+ memberData: z.object({
157
+ name: z.string().optional(),
158
+ role: z.string().optional(),
159
+ skills: z.array(z.string()).optional(),
160
+ capacity: z.string().optional()
161
+ }).optional().describe('Team member data'),
162
+ allocationData: z.object({
163
+ phaseId: z.string(),
164
+ allocation: z.number().min(0).max(100)
165
+ }).optional().describe('Resource allocation data')
166
+ });
167
+ const RiskAnalysisSchema = z.object({
168
+ operation: z.literal('risk_analysis'),
169
+ projectPath: z.string().describe('Project root path'),
170
+ analysisType: z.enum(['automated', 'manual', 'comprehensive']).default('comprehensive').describe('Type of risk analysis'),
171
+ includeAdrRisks: z.boolean().default(true).describe('Analyze risks from ADR complexity'),
172
+ includeDependencyRisks: z.boolean().default(true).describe('Analyze dependency chain risks'),
173
+ includeResourceRisks: z.boolean().default(true).describe('Analyze resource allocation risks'),
174
+ generateMitigation: z.boolean().default(true).describe('Generate mitigation strategies')
175
+ });
176
+ const GenerateReportsSchema = z.object({
177
+ operation: z.literal('generate_reports'),
178
+ projectPath: z.string().describe('Project root path'),
179
+ reportType: z.enum(['executive', 'status', 'health', 'team_performance', 'milestone_tracking']).describe('Type of report to generate'),
180
+ format: z.enum(['markdown', 'json', 'html']).default('markdown').describe('Report output format'),
181
+ includeCharts: z.boolean().default(true).describe('Include progress charts and graphs'),
182
+ timeframe: z.enum(['week', 'month', 'quarter', 'project']).default('month').describe('Report time frame')
183
+ });
184
+ // Main operation schema
185
+ const McpPlanningSchema = z.union([
186
+ CreateProjectSchema,
187
+ ManagePhasesSchema,
188
+ TrackProgressSchema,
189
+ ManageResourcesSchema,
190
+ RiskAnalysisSchema,
191
+ GenerateReportsSchema
192
+ ]);
193
+ /**
194
+ * Get planning cache directory
195
+ */
196
+ function getPlanningCacheDir(projectPath) {
197
+ return join(projectPath, '.mcp-adr-cache');
198
+ }
199
+ /**
200
+ * Get planning data file path
201
+ */
202
+ function getPlanningDataPath(projectPath) {
203
+ return join(getPlanningCacheDir(projectPath), 'project-planning.json');
204
+ }
205
+ /**
206
+ * Ensure cache directory exists
207
+ */
208
+ async function ensureCacheDir(projectPath) {
209
+ const cacheDir = getPlanningCacheDir(projectPath);
210
+ try {
211
+ await fs.mkdir(cacheDir, { recursive: true });
212
+ }
213
+ catch (error) {
214
+ // Directory might already exist, that's ok
215
+ }
216
+ }
217
+ /**
218
+ * Load existing project planning data
219
+ */
220
+ async function loadPlanningData(projectPath) {
221
+ const planningPath = getPlanningDataPath(projectPath);
222
+ try {
223
+ const data = await fs.readFile(planningPath, 'utf-8');
224
+ return ProjectPlanSchema.parse(JSON.parse(data));
225
+ }
226
+ catch (error) {
227
+ return null;
228
+ }
229
+ }
230
+ /**
231
+ * Save project planning data
232
+ */
233
+ async function savePlanningData(projectPath, data) {
234
+ await ensureCacheDir(projectPath);
235
+ const planningPath = getPlanningDataPath(projectPath);
236
+ // Update timestamps
237
+ data.metadata.updatedAt = new Date().toISOString();
238
+ await fs.writeFile(planningPath, JSON.stringify(data, null, 2));
239
+ }
240
+ /**
241
+ * Generate unique ID
242
+ */
243
+ function generateId() {
244
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
245
+ }
246
+ /**
247
+ * Create new project operation
248
+ */
249
+ async function createProject(args) {
250
+ const { projectPath, projectName, description, phases, team, importFromAdrs, importFromTodos } = args;
251
+ // Check if project already exists
252
+ const existing = await loadPlanningData(projectPath);
253
+ if (existing) {
254
+ throw new McpAdrError(`Project already exists at ${projectPath}. Use manage_phases to modify existing project.`, 'PROJECT_EXISTS');
255
+ }
256
+ const now = new Date().toISOString();
257
+ const projectId = generateId();
258
+ // Create project phases
259
+ const projectPhases = phases.map(phase => ({
260
+ id: generateId(),
261
+ name: phase.name,
262
+ description: `Auto-generated phase: ${phase.name}`,
263
+ status: 'planning',
264
+ estimatedDuration: phase.duration,
265
+ dependencies: phase.dependencies,
266
+ blockers: [], // Add missing field
267
+ milestones: phase.milestones,
268
+ linkedAdrs: [],
269
+ tasks: [],
270
+ completion: 0,
271
+ riskLevel: 'medium',
272
+ riskFactors: [],
273
+ resources: [],
274
+ createdAt: now,
275
+ updatedAt: now
276
+ }));
277
+ // Create team members
278
+ const teamMembers = team.map(member => ({
279
+ id: generateId(),
280
+ name: member.name,
281
+ role: member.role,
282
+ skills: member.skills,
283
+ capacity: member.capacity,
284
+ currentWorkload: 0
285
+ }));
286
+ // Import from ADRs if requested
287
+ if (importFromAdrs) {
288
+ try {
289
+ const { discoverAdrsInDirectory } = await import('../utils/adr-discovery.js');
290
+ const discoveryResult = await discoverAdrsInDirectory(join(projectPath, 'docs/adrs'), true, projectPath);
291
+ // Link relevant ADRs to phases
292
+ for (const phase of projectPhases) {
293
+ const relatedAdrs = discoveryResult.adrs.filter((adr) => adr.title.toLowerCase().includes(phase.name.toLowerCase()) ||
294
+ adr.content.toLowerCase().includes(phase.name.toLowerCase()));
295
+ phase.linkedAdrs = relatedAdrs.map((adr) => adr.file);
296
+ }
297
+ }
298
+ catch (error) {
299
+ console.warn('Failed to import ADRs:', error);
300
+ }
301
+ }
302
+ // Import from TODO system if requested
303
+ if (importFromTodos) {
304
+ try {
305
+ const todoManager = new TodoJsonManager();
306
+ const todoData = await todoManager.loadTodoData();
307
+ if (todoData && todoData.tasks && Object.values(todoData.tasks).length > 0) {
308
+ // Distribute tasks across phases based on categories or ADR links
309
+ const tasksArray = Object.values(todoData.tasks);
310
+ for (const phase of projectPhases) {
311
+ const relatedTasks = tasksArray.filter((task) => task.linkedAdrs.some((adr) => phase.linkedAdrs.includes(adr)) ||
312
+ task.category?.toLowerCase().includes(phase.name.toLowerCase()));
313
+ phase.tasks = relatedTasks.map((task) => task.id);
314
+ }
315
+ }
316
+ }
317
+ catch (error) {
318
+ console.warn('Failed to import TODO tasks:', error);
319
+ }
320
+ }
321
+ // Create project plan
322
+ const projectPlan = {
323
+ id: projectId,
324
+ name: projectName,
325
+ description,
326
+ status: 'planning',
327
+ phases: projectPhases,
328
+ team: teamMembers,
329
+ milestones: [],
330
+ risks: [],
331
+ metadata: {
332
+ createdAt: now,
333
+ updatedAt: now,
334
+ createdBy: 'mcp-planning-tool',
335
+ projectPath,
336
+ adrDirectory: 'docs/adrs',
337
+ linkedAdrs: projectPhases.flatMap(p => p.linkedAdrs)
338
+ },
339
+ settings: {
340
+ autoPhaseTransition: false,
341
+ riskAssessmentInterval: 'weekly',
342
+ progressUpdateFrequency: 'daily'
343
+ }
344
+ };
345
+ await savePlanningData(projectPath, projectPlan);
346
+ return {
347
+ content: [{
348
+ type: 'text',
349
+ text: `# Project Created Successfully
350
+
351
+ ## Project Overview
352
+ - **Name**: ${projectName}
353
+ - **ID**: ${projectId}
354
+ - **Status**: Planning
355
+ - **Phases**: ${projectPhases.length}
356
+ - **Team Members**: ${teamMembers.length}
357
+
358
+ ## Project Phases
359
+ ${projectPhases.map((phase, index) => `
360
+ ### Phase ${index + 1}: ${phase.name}
361
+ - **Duration**: ${phase.estimatedDuration}
362
+ - **Dependencies**: ${phase.dependencies.length > 0 ? phase.dependencies.join(', ') : 'None'}
363
+ - **Milestones**: ${phase.milestones.length > 0 ? phase.milestones.join(', ') : 'None'}
364
+ - **Linked ADRs**: ${phase.linkedAdrs.length}
365
+ - **Tasks**: ${phase.tasks.length}
366
+ `).join('')}
367
+
368
+ ## Team Structure
369
+ ${teamMembers.map(member => `
370
+ ### ${member.name} - ${member.role}
371
+ - **Capacity**: ${member.capacity}
372
+ - **Skills**: ${member.skills.join(', ')}
373
+ - **Current Workload**: ${member.currentWorkload}%
374
+ `).join('')}
375
+
376
+ ## Next Steps
377
+ 1. Use \`manage_phases\` to refine phase details
378
+ 2. Use \`manage_resources\` to allocate team members to phases
379
+ 3. Use \`track_progress\` to monitor project advancement
380
+ 4. Use \`risk_analysis\` to identify potential issues
381
+
382
+ ## Integration Status
383
+ - **ADR Import**: ${importFromAdrs ? `✅ Linked ${projectPlan.metadata.linkedAdrs.length} ADRs` : '❌ Skipped'}
384
+ - **TODO Import**: ${importFromTodos ? `✅ Imported tasks from TODO system` : '❌ Skipped'}
385
+
386
+ Project planning data saved to: \`.mcp-adr-cache/project-planning.json\`
387
+ `
388
+ }]
389
+ };
390
+ }
391
+ /**
392
+ * Manage phases operation
393
+ */
394
+ async function managePhases(args) {
395
+ const { projectPath, action, phaseId, phaseData, targetStatus } = args;
396
+ const projectPlan = await loadPlanningData(projectPath);
397
+ if (!projectPlan) {
398
+ throw new McpAdrError(`No project found at ${projectPath}. Use create_project first.`, 'PROJECT_NOT_FOUND');
399
+ }
400
+ const now = new Date().toISOString();
401
+ switch (action) {
402
+ case 'list':
403
+ return {
404
+ content: [{
405
+ type: 'text',
406
+ text: `# Project Phases
407
+
408
+ ## ${projectPlan.name}
409
+
410
+ ${projectPlan.phases.map((phase, index) => `
411
+ ### Phase ${index + 1}: ${phase.name}
412
+ - **Status**: ${phase.status}
413
+ - **Progress**: ${phase.completion}%
414
+ - **Duration**: ${phase.estimatedDuration} ${phase.actualDuration ? `(Actual: ${phase.actualDuration})` : ''}
415
+ - **Dependencies**: ${phase.dependencies.length > 0 ? phase.dependencies.join(', ') : 'None'}
416
+ - **Blockers**: ${phase.blockers.length > 0 ? phase.blockers.join(', ') : 'None'}
417
+ - **Risk Level**: ${phase.riskLevel}
418
+ - **Tasks**: ${phase.tasks.length}
419
+ - **Linked ADRs**: ${phase.linkedAdrs.length}
420
+ `).join('')}
421
+
422
+ Total phases: ${projectPlan.phases.length}
423
+ `
424
+ }]
425
+ };
426
+ case 'create':
427
+ if (!phaseData?.name) {
428
+ throw new McpAdrError('Phase name is required for creation', 'INVALID_INPUT');
429
+ }
430
+ const newPhase = {
431
+ id: generateId(),
432
+ name: phaseData.name,
433
+ description: phaseData.description || `New phase: ${phaseData.name}`,
434
+ status: 'planning',
435
+ estimatedDuration: phaseData.estimatedDuration || 'TBD',
436
+ dependencies: phaseData.dependencies || [],
437
+ blockers: [], // Add missing field
438
+ milestones: phaseData.milestones || [],
439
+ linkedAdrs: phaseData.linkedAdrs || [],
440
+ tasks: [],
441
+ completion: 0,
442
+ riskLevel: 'medium',
443
+ riskFactors: [],
444
+ resources: [],
445
+ createdAt: now,
446
+ updatedAt: now
447
+ };
448
+ projectPlan.phases.push(newPhase);
449
+ await savePlanningData(projectPath, projectPlan);
450
+ return {
451
+ content: [{
452
+ type: 'text',
453
+ text: `# Phase Created
454
+
455
+ **${newPhase.name}** has been added to the project.
456
+
457
+ - **ID**: ${newPhase.id}
458
+ - **Status**: ${newPhase.status}
459
+ - **Duration**: ${newPhase.estimatedDuration}
460
+ - **Dependencies**: ${newPhase.dependencies.join(', ') || 'None'}
461
+
462
+ Use \`manage_resources\` to allocate team members to this phase.
463
+ `
464
+ }]
465
+ };
466
+ case 'update':
467
+ if (!phaseId) {
468
+ throw new McpAdrError('Phase ID is required for update', 'INVALID_INPUT');
469
+ }
470
+ const phaseIndex = projectPlan.phases.findIndex(p => p.id === phaseId);
471
+ if (phaseIndex === -1) {
472
+ throw new McpAdrError(`Phase with ID ${phaseId} not found`, 'PHASE_NOT_FOUND');
473
+ }
474
+ const phase = projectPlan.phases[phaseIndex];
475
+ if (!phase) {
476
+ throw new McpAdrError(`Phase with ID ${phaseId} not found`, 'PHASE_NOT_FOUND');
477
+ }
478
+ if (phaseData) {
479
+ Object.assign(phase, {
480
+ ...phaseData,
481
+ updatedAt: now
482
+ });
483
+ }
484
+ await savePlanningData(projectPath, projectPlan);
485
+ return {
486
+ content: [{
487
+ type: 'text',
488
+ text: `# Phase Updated
489
+
490
+ **${phase.name}** has been updated successfully.
491
+
492
+ Updated fields: ${Object.keys(phaseData || {}).join(', ')}
493
+ `
494
+ }]
495
+ };
496
+ case 'transition':
497
+ if (!phaseId || !targetStatus) {
498
+ throw new McpAdrError('Phase ID and target status are required for transition', 'INVALID_INPUT');
499
+ }
500
+ const transitionPhaseIndex = projectPlan.phases.findIndex(p => p.id === phaseId);
501
+ if (transitionPhaseIndex === -1) {
502
+ throw new McpAdrError(`Phase with ID ${phaseId} not found`, 'PHASE_NOT_FOUND');
503
+ }
504
+ const transitionPhase = projectPlan.phases[transitionPhaseIndex];
505
+ if (!transitionPhase) {
506
+ throw new McpAdrError(`Phase with ID ${phaseId} not found`, 'PHASE_NOT_FOUND');
507
+ }
508
+ const oldStatus = transitionPhase.status;
509
+ transitionPhase.status = targetStatus;
510
+ transitionPhase.updatedAt = now;
511
+ // Auto-calculate completion based on status
512
+ if (targetStatus === 'completed') {
513
+ transitionPhase.completion = 100;
514
+ }
515
+ else if (targetStatus === 'active' && transitionPhase.completion === 0) {
516
+ transitionPhase.completion = 10; // Started
517
+ }
518
+ await savePlanningData(projectPath, projectPlan);
519
+ return {
520
+ content: [{
521
+ type: 'text',
522
+ text: `# Phase Transition Complete
523
+
524
+ **${transitionPhase.name}** status changed from **${oldStatus}** to **${targetStatus}**.
525
+
526
+ Current completion: ${transitionPhase.completion}%
527
+ `
528
+ }]
529
+ };
530
+ case 'delete':
531
+ if (!phaseId) {
532
+ throw new McpAdrError('Phase ID is required for deletion', 'INVALID_INPUT');
533
+ }
534
+ const deleteIndex = projectPlan.phases.findIndex(p => p.id === phaseId);
535
+ if (deleteIndex === -1) {
536
+ throw new McpAdrError(`Phase with ID ${phaseId} not found`, 'PHASE_NOT_FOUND');
537
+ }
538
+ const deletedPhase = projectPlan.phases.splice(deleteIndex, 1)[0];
539
+ if (!deletedPhase) {
540
+ throw new McpAdrError(`Phase with ID ${phaseId} not found`, 'PHASE_NOT_FOUND');
541
+ }
542
+ await savePlanningData(projectPath, projectPlan);
543
+ return {
544
+ content: [{
545
+ type: 'text',
546
+ text: `# Phase Deleted
547
+
548
+ **${deletedPhase.name}** has been removed from the project.
549
+
550
+ Remaining phases: ${projectPlan.phases.length}
551
+ `
552
+ }]
553
+ };
554
+ default:
555
+ throw new McpAdrError(`Unknown phase action: ${action}`, 'INVALID_INPUT');
556
+ }
557
+ }
558
+ /**
559
+ * Track progress operation
560
+ */
561
+ async function trackProgress(args) {
562
+ const { projectPath, reportType, includeVisuals, updateTaskProgress } = args;
563
+ const projectPlan = await loadPlanningData(projectPath);
564
+ if (!projectPlan) {
565
+ throw new McpAdrError(`No project found at ${projectPath}. Use create_project first.`, 'PROJECT_NOT_FOUND');
566
+ }
567
+ // Update task progress from TODO system if requested
568
+ if (updateTaskProgress) {
569
+ try {
570
+ const todoManager = new TodoJsonManager();
571
+ const todoData = await todoManager.loadTodoData();
572
+ if (todoData && todoData.tasks) {
573
+ const tasksArray = Object.values(todoData.tasks);
574
+ for (const phase of projectPlan.phases) {
575
+ const phaseTasks = tasksArray.filter((task) => phase.tasks.includes(task.id));
576
+ if (phaseTasks.length > 0) {
577
+ const completedTasks = phaseTasks.filter((task) => task.status === 'completed').length;
578
+ phase.completion = Math.round((completedTasks / phaseTasks.length) * 100);
579
+ }
580
+ }
581
+ await savePlanningData(projectPath, projectPlan);
582
+ }
583
+ }
584
+ catch (error) {
585
+ console.warn('Failed to update task progress:', error);
586
+ }
587
+ }
588
+ // Calculate overall project progress
589
+ const totalPhases = projectPlan.phases.length;
590
+ const completedPhases = projectPlan.phases.filter(p => p.status === 'completed').length;
591
+ const activePhases = projectPlan.phases.filter(p => p.status === 'active').length;
592
+ const blockedPhases = projectPlan.phases.filter(p => p.status === 'blocked').length;
593
+ const overallProgress = totalPhases > 0 ? Math.round(projectPlan.phases.reduce((sum, phase) => sum + phase.completion, 0) / totalPhases) : 0;
594
+ switch (reportType) {
595
+ case 'summary':
596
+ return {
597
+ content: [{
598
+ type: 'text',
599
+ text: `# Project Progress Summary
600
+
601
+ ## ${projectPlan.name}
602
+
603
+ ### Overall Status
604
+ - **Progress**: ${overallProgress}% ${includeVisuals ? getProgressBar(overallProgress) : ''}
605
+ - **Status**: ${projectPlan.status}
606
+ - **Total Phases**: ${totalPhases}
607
+ - **Completed**: ${completedPhases}
608
+ - **Active**: ${activePhases}
609
+ - **Blocked**: ${blockedPhases}
610
+
611
+ ### Phase Progress
612
+ ${projectPlan.phases.map(phase => `
613
+ #### ${phase.name}
614
+ - **Status**: ${phase.status}
615
+ - **Progress**: ${phase.completion}% ${includeVisuals ? getProgressBar(phase.completion) : ''}
616
+ - **Tasks**: ${phase.tasks.length}
617
+ - **Blockers**: ${phase.blockers.length}
618
+ `).join('')}
619
+
620
+ ### Current Focus
621
+ ${activePhases > 0 ?
622
+ `Currently working on ${activePhases} phase(s):\n${projectPlan.phases.filter(p => p.status === 'active').map(p => `- ${p.name}`).join('\n')}` :
623
+ 'No active phases'}
624
+
625
+ ${blockedPhases > 0 ?
626
+ `\n⚠️ **Blocked Phases**: ${projectPlan.phases.filter(p => p.status === 'blocked').map(p => p.name).join(', ')}` :
627
+ ''}
628
+ `
629
+ }]
630
+ };
631
+ case 'detailed':
632
+ return {
633
+ content: [{
634
+ type: 'text',
635
+ text: `# Detailed Progress Report
636
+
637
+ ## ${projectPlan.name}
638
+
639
+ ### Project Metrics
640
+ - **Overall Progress**: ${overallProgress}%
641
+ - **Team Utilization**: ${calculateTeamUtilization(projectPlan)}%
642
+ - **Risk Level**: ${calculateProjectRisk(projectPlan)}
643
+ - **Timeline Status**: ${calculateTimelineStatus(projectPlan)}
644
+
645
+ ${projectPlan.phases.map(phase => `
646
+ ### ${phase.name}
647
+ - **Status**: ${phase.status}
648
+ - **Progress**: ${phase.completion}% ${includeVisuals ? getProgressBar(phase.completion) : ''}
649
+ - **Duration**: ${phase.estimatedDuration} ${phase.actualDuration ? `(Actual: ${phase.actualDuration})` : ''}
650
+ - **Risk Level**: ${phase.riskLevel}
651
+ - **Dependencies**: ${phase.dependencies.length > 0 ? phase.dependencies.join(', ') : 'None'}
652
+ - **Blockers**: ${phase.blockers.length > 0 ? phase.blockers.map(b => `⚠️ ${b}`).join('\n ') : 'None'}
653
+ - **Tasks**: ${phase.tasks.length} tasks
654
+ - **Linked ADRs**: ${phase.linkedAdrs.length} ADRs
655
+ - **Resources**: ${phase.resources.length} team members allocated
656
+
657
+ ${phase.riskFactors.length > 0 ? `**Risk Factors**:\n${phase.riskFactors.map(r => `- ${r}`).join('\n')}` : ''}
658
+ `).join('')}
659
+ `
660
+ }]
661
+ };
662
+ case 'milestones':
663
+ return {
664
+ content: [{
665
+ type: 'text',
666
+ text: `# Milestone Tracking
667
+
668
+ ## ${projectPlan.name}
669
+
670
+ ### Project Milestones
671
+ ${projectPlan.milestones.length > 0 ?
672
+ projectPlan.milestones.map(milestone => `
673
+ #### ${milestone.name}
674
+ - **Status**: ${milestone.status}
675
+ - **Due Date**: ${milestone.dueDate}
676
+ - **Linked Phases**: ${milestone.linkedPhases.join(', ') || 'None'}
677
+ - **Criteria**: ${milestone.criteria.join(', ') || 'None'}
678
+ `).join('') :
679
+ 'No project milestones defined'}
680
+
681
+ ### Phase Milestones
682
+ ${projectPlan.phases.map(phase => `
683
+ #### ${phase.name}
684
+ ${phase.milestones.length > 0 ?
685
+ phase.milestones.map(m => `- ${m}`).join('\n') :
686
+ '- No milestones defined'}
687
+ `).join('')}
688
+ `
689
+ }]
690
+ };
691
+ default:
692
+ return {
693
+ content: [{
694
+ type: 'text',
695
+ text: `Progress tracking for report type "${reportType}" is not yet implemented.`
696
+ }]
697
+ };
698
+ }
699
+ }
700
+ /**
701
+ * Manage resources operation
702
+ */
703
+ async function manageResources(args) {
704
+ const { projectPath, action, memberData } = args;
705
+ const projectPlan = await loadPlanningData(projectPath);
706
+ if (!projectPlan) {
707
+ throw new McpAdrError(`No project found at ${projectPath}. Use create_project first.`, 'PROJECT_NOT_FOUND');
708
+ }
709
+ switch (action) {
710
+ case 'list':
711
+ return {
712
+ content: [{
713
+ type: 'text',
714
+ text: `# Team Resources
715
+
716
+ ## ${projectPlan.name}
717
+
718
+ ### Team Members
719
+ ${projectPlan.team.map(member => `
720
+ #### ${member.name} - ${member.role}
721
+ - **Capacity**: ${member.capacity}
722
+ - **Current Workload**: ${member.currentWorkload}%
723
+ - **Skills**: ${member.skills.join(', ') || 'None specified'}
724
+ - **Availability**: ${member.availability ? 'Configured' : 'Not set'}
725
+ `).join('')}
726
+
727
+ ### Resource Allocation
728
+ ${projectPlan.phases.map(phase => `
729
+ #### ${phase.name}
730
+ ${phase.resources.length > 0 ?
731
+ phase.resources.map(resource => `- ${resource.role}: ${resource.allocation}% (${resource.capacity})`).join('\n') :
732
+ '- No resources allocated'}
733
+ `).join('')}
734
+
735
+ Total team members: ${projectPlan.team.length}
736
+ `
737
+ }]
738
+ };
739
+ case 'add':
740
+ if (!memberData?.name || !memberData?.role) {
741
+ throw new McpAdrError('Member name and role are required', 'INVALID_INPUT');
742
+ }
743
+ const newMember = {
744
+ id: generateId(),
745
+ name: memberData.name,
746
+ role: memberData.role,
747
+ skills: memberData.skills || [],
748
+ capacity: memberData.capacity || '40h/week',
749
+ currentWorkload: 0
750
+ };
751
+ projectPlan.team.push(newMember);
752
+ await savePlanningData(projectPath, projectPlan);
753
+ return {
754
+ content: [{
755
+ type: 'text',
756
+ text: `# Team Member Added
757
+
758
+ **${newMember.name}** has been added to the team.
759
+
760
+ - **Role**: ${newMember.role}
761
+ - **Capacity**: ${newMember.capacity}
762
+ - **Skills**: ${newMember.skills.join(', ') || 'None specified'}
763
+
764
+ Use \`allocate\` action to assign them to project phases.
765
+ `
766
+ }]
767
+ };
768
+ default:
769
+ return {
770
+ content: [{
771
+ type: 'text',
772
+ text: `Resource management for action "${action}" is not yet implemented.`
773
+ }]
774
+ };
775
+ }
776
+ }
777
+ /**
778
+ * Risk analysis operation
779
+ */
780
+ async function riskAnalysis(args) {
781
+ const { projectPath, includeAdrRisks, includeDependencyRisks, includeResourceRisks, generateMitigation } = args;
782
+ const projectPlan = await loadPlanningData(projectPath);
783
+ if (!projectPlan) {
784
+ throw new McpAdrError(`No project found at ${projectPath}. Use create_project first.`, 'PROJECT_NOT_FOUND');
785
+ }
786
+ const risks = [];
787
+ // Analyze ADR-based risks
788
+ if (includeAdrRisks) {
789
+ for (const phase of projectPlan.phases) {
790
+ if (phase.linkedAdrs.length === 0) {
791
+ const riskObj = {
792
+ category: 'Architecture',
793
+ description: `Phase "${phase.name}" has no linked ADRs - architectural decisions may be unclear`,
794
+ impact: 'medium',
795
+ probability: 'high'
796
+ };
797
+ if (generateMitigation) {
798
+ riskObj.mitigation = 'Review and create ADRs for architectural decisions in this phase';
799
+ }
800
+ risks.push(riskObj);
801
+ }
802
+ if (phase.linkedAdrs.length > 5) {
803
+ const riskObj = {
804
+ category: 'Complexity',
805
+ description: `Phase "${phase.name}" has many ADRs (${phase.linkedAdrs.length}) - high complexity risk`,
806
+ impact: 'high',
807
+ probability: 'medium'
808
+ };
809
+ if (generateMitigation) {
810
+ riskObj.mitigation = 'Consider breaking this phase into smaller, more manageable sub-phases';
811
+ }
812
+ risks.push(riskObj);
813
+ }
814
+ }
815
+ }
816
+ // Analyze dependency risks
817
+ if (includeDependencyRisks) {
818
+ for (const phase of projectPlan.phases) {
819
+ if (phase.dependencies.length > 3) {
820
+ const riskObj = {
821
+ category: 'Dependencies',
822
+ description: `Phase "${phase.name}" has multiple dependencies (${phase.dependencies.length}) - high coordination risk`,
823
+ impact: 'high',
824
+ probability: 'medium'
825
+ };
826
+ if (generateMitigation) {
827
+ riskObj.mitigation = 'Review and simplify dependencies, create detailed coordination plan';
828
+ }
829
+ risks.push(riskObj);
830
+ }
831
+ // Check for circular dependencies (simplified check)
832
+ const dependentPhases = projectPlan.phases.filter(p => p.dependencies.includes(phase.id) && phase.dependencies.includes(p.id));
833
+ if (dependentPhases.length > 0) {
834
+ const riskObj = {
835
+ category: 'Dependencies',
836
+ description: `Potential circular dependency detected between "${phase.name}" and other phases`,
837
+ impact: 'critical',
838
+ probability: 'high'
839
+ };
840
+ if (generateMitigation) {
841
+ riskObj.mitigation = 'Review and restructure phase dependencies to eliminate circular references';
842
+ }
843
+ risks.push(riskObj);
844
+ }
845
+ }
846
+ }
847
+ // Analyze resource risks
848
+ if (includeResourceRisks) {
849
+ const totalWorkload = projectPlan.team.reduce((sum, member) => sum + member.currentWorkload, 0);
850
+ const avgWorkload = projectPlan.team.length > 0 ? totalWorkload / projectPlan.team.length : 0;
851
+ if (avgWorkload > 80) {
852
+ const riskObj = {
853
+ category: 'Resources',
854
+ description: `Team is overallocated (${avgWorkload.toFixed(1)}% average workload)`,
855
+ impact: 'high',
856
+ probability: 'high'
857
+ };
858
+ if (generateMitigation) {
859
+ riskObj.mitigation = 'Rebalance workload or consider adding team members';
860
+ }
861
+ risks.push(riskObj);
862
+ }
863
+ if (projectPlan.team.length < projectPlan.phases.length / 2) {
864
+ const riskObj = {
865
+ category: 'Resources',
866
+ description: `Limited team size (${projectPlan.team.length} members) for ${projectPlan.phases.length} phases`,
867
+ impact: 'medium',
868
+ probability: 'medium'
869
+ };
870
+ if (generateMitigation) {
871
+ riskObj.mitigation = 'Consider increasing team size or extending timeline';
872
+ }
873
+ risks.push(riskObj);
874
+ }
875
+ }
876
+ // Calculate overall risk score
877
+ const criticalRisks = risks.filter(r => r.impact === 'critical').length;
878
+ const highRisks = risks.filter(r => r.impact === 'high').length;
879
+ const mediumRisks = risks.filter(r => r.impact === 'medium').length;
880
+ const riskScore = (criticalRisks * 10) + (highRisks * 5) + (mediumRisks * 2);
881
+ let riskLevel = 'low';
882
+ if (riskScore > 20)
883
+ riskLevel = 'critical';
884
+ else if (riskScore > 10)
885
+ riskLevel = 'high';
886
+ else if (riskScore > 5)
887
+ riskLevel = 'medium';
888
+ return {
889
+ content: [{
890
+ type: 'text',
891
+ text: `# Risk Analysis Report
892
+
893
+ ## ${projectPlan.name}
894
+
895
+ ### Overall Risk Assessment
896
+ - **Risk Level**: ${riskLevel.toUpperCase()}
897
+ - **Risk Score**: ${riskScore}
898
+ - **Total Risks**: ${risks.length}
899
+ - **Critical**: ${criticalRisks}
900
+ - **High**: ${highRisks}
901
+ - **Medium**: ${mediumRisks}
902
+
903
+ ### Identified Risks
904
+
905
+ ${risks.map((risk, index) => `
906
+ #### Risk ${index + 1}: ${risk.category}
907
+ - **Description**: ${risk.description}
908
+ - **Impact**: ${risk.impact}
909
+ - **Probability**: ${risk.probability}
910
+ ${risk.mitigation ? `- **Mitigation**: ${risk.mitigation}` : ''}
911
+ `).join('')}
912
+
913
+ ### Recommendations
914
+
915
+ ${riskLevel === 'critical' ?
916
+ '🚨 **Critical risks detected** - Immediate action required before proceeding' :
917
+ riskLevel === 'high' ?
918
+ '⚠️ **High risks present** - Review and mitigate before major milestones' :
919
+ '✅ **Manageable risk level** - Continue with normal monitoring'}
920
+
921
+ ### Next Steps
922
+ 1. Review and prioritize risk mitigation strategies
923
+ 2. Assign risk owners for high-impact items
924
+ 3. Schedule regular risk assessment reviews
925
+ 4. Update project timeline if necessary
926
+ `
927
+ }]
928
+ };
929
+ }
930
+ /**
931
+ * Generate reports operation
932
+ */
933
+ async function generateReports(args) {
934
+ const { projectPath, reportType, includeCharts, timeframe } = args;
935
+ const projectPlan = await loadPlanningData(projectPath);
936
+ if (!projectPlan) {
937
+ throw new McpAdrError(`No project found at ${projectPath}. Use create_project first.`, 'PROJECT_NOT_FOUND');
938
+ }
939
+ switch (reportType) {
940
+ case 'executive':
941
+ const overallProgress = calculateOverallProgress(projectPlan);
942
+ const teamUtilization = calculateTeamUtilization(projectPlan);
943
+ const riskLevel = calculateProjectRisk(projectPlan);
944
+ return {
945
+ content: [{
946
+ type: 'text',
947
+ text: `# Executive Summary
948
+
949
+ ## ${projectPlan.name}
950
+
951
+ ### Key Metrics
952
+ - **Overall Progress**: ${overallProgress}% ${includeCharts ? getProgressBar(overallProgress) : ''}
953
+ - **Team Utilization**: ${teamUtilization}%
954
+ - **Risk Level**: ${riskLevel}
955
+ - **Active Phases**: ${projectPlan.phases.filter(p => p.status === 'active').length}
956
+ - **Completed Phases**: ${projectPlan.phases.filter(p => p.status === 'completed').length}
957
+
958
+ ### Current Status
959
+ ${projectPlan.status === 'active' ?
960
+ '🟢 Project is actively progressing' :
961
+ projectPlan.status === 'completed' ?
962
+ '✅ Project completed successfully' :
963
+ '🟡 Project in planning phase'}
964
+
965
+ ### Key Highlights
966
+ - Total phases: ${projectPlan.phases.length}
967
+ - Team size: ${projectPlan.team.length} members
968
+ - Linked ADRs: ${projectPlan.metadata.linkedAdrs.length}
969
+ - Risk level: ${riskLevel}
970
+
971
+ ### Next Milestones
972
+ ${projectPlan.milestones.filter(m => m.status === 'pending').slice(0, 3).map(milestone => `- ${milestone.name} (Due: ${milestone.dueDate})`).join('\n') || 'No upcoming milestones'}
973
+
974
+ ### Blockers & Risks
975
+ ${projectPlan.phases.filter(p => p.blockers.length > 0).length > 0 ?
976
+ `⚠️ ${projectPlan.phases.filter(p => p.blockers.length > 0).length} phases have blockers` :
977
+ '✅ No major blockers reported'}
978
+
979
+ *Report generated: ${new Date().toISOString()}*
980
+ `
981
+ }]
982
+ };
983
+ case 'status':
984
+ return {
985
+ content: [{
986
+ type: 'text',
987
+ text: `# Status Report - ${timeframe}
988
+
989
+ ## ${projectPlan.name}
990
+
991
+ ### Phase Status
992
+ ${projectPlan.phases.map(phase => `
993
+ #### ${phase.name}
994
+ - **Status**: ${phase.status}
995
+ - **Progress**: ${phase.completion}%
996
+ - **Timeline**: ${phase.estimatedDuration}
997
+ - **Team**: ${phase.resources.length} members
998
+ - **Blockers**: ${phase.blockers.length}
999
+ `).join('')}
1000
+
1001
+ ### Team Status
1002
+ ${projectPlan.team.map(member => `
1003
+ #### ${member.name}
1004
+ - **Role**: ${member.role}
1005
+ - **Workload**: ${member.currentWorkload}%
1006
+ - **Capacity**: ${member.capacity}
1007
+ `).join('')}
1008
+
1009
+ ### Recent Updates
1010
+ - Last updated: ${projectPlan.metadata.updatedAt}
1011
+ - Total phases: ${projectPlan.phases.length}
1012
+ - Active work streams: ${projectPlan.phases.filter(p => p.status === 'active').length}
1013
+ `
1014
+ }]
1015
+ };
1016
+ default:
1017
+ return {
1018
+ content: [{
1019
+ type: 'text',
1020
+ text: `Report generation for type "${reportType}" is not yet implemented.`
1021
+ }]
1022
+ };
1023
+ }
1024
+ }
1025
+ /**
1026
+ * Helper functions
1027
+ */
1028
+ function getProgressBar(percentage, width = 20) {
1029
+ const filled = Math.round((percentage / 100) * width);
1030
+ const empty = width - filled;
1031
+ return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
1032
+ }
1033
+ function calculateOverallProgress(projectPlan) {
1034
+ if (projectPlan.phases.length === 0)
1035
+ return 0;
1036
+ return Math.round(projectPlan.phases.reduce((sum, phase) => sum + phase.completion, 0) / projectPlan.phases.length);
1037
+ }
1038
+ function calculateTeamUtilization(projectPlan) {
1039
+ if (projectPlan.team.length === 0)
1040
+ return 0;
1041
+ return Math.round(projectPlan.team.reduce((sum, member) => sum + member.currentWorkload, 0) / projectPlan.team.length);
1042
+ }
1043
+ function calculateProjectRisk(projectPlan) {
1044
+ const blockedPhases = projectPlan.phases.filter(p => p.status === 'blocked').length;
1045
+ const highRiskPhases = projectPlan.phases.filter(p => p.riskLevel === 'high' || p.riskLevel === 'critical').length;
1046
+ const overloadedTeam = projectPlan.team.filter(m => m.currentWorkload > 90).length;
1047
+ if (blockedPhases > 0 || highRiskPhases > projectPlan.phases.length * 0.3 || overloadedTeam > 0) {
1048
+ return 'HIGH';
1049
+ }
1050
+ else if (highRiskPhases > 0 || overloadedTeam > projectPlan.team.length * 0.5) {
1051
+ return 'MEDIUM';
1052
+ }
1053
+ else {
1054
+ return 'LOW';
1055
+ }
1056
+ }
1057
+ function calculateTimelineStatus(projectPlan) {
1058
+ const blockedPhases = projectPlan.phases.filter(p => p.status === 'blocked').length;
1059
+ const completedOnTime = projectPlan.phases.filter(p => p.status === 'completed').length;
1060
+ if (blockedPhases > 0)
1061
+ return 'AT RISK';
1062
+ if (completedOnTime > projectPlan.phases.length * 0.5)
1063
+ return 'ON TRACK';
1064
+ return 'MONITORING';
1065
+ }
1066
+ /**
1067
+ * Main export function
1068
+ */
1069
+ export async function mcpPlanning(args) {
1070
+ try {
1071
+ const validatedArgs = McpPlanningSchema.parse(args);
1072
+ switch (validatedArgs.operation) {
1073
+ case 'create_project':
1074
+ return await createProject(validatedArgs);
1075
+ case 'manage_phases':
1076
+ return await managePhases(validatedArgs);
1077
+ case 'track_progress':
1078
+ return await trackProgress(validatedArgs);
1079
+ case 'manage_resources':
1080
+ return await manageResources(validatedArgs);
1081
+ case 'risk_analysis':
1082
+ return await riskAnalysis(validatedArgs);
1083
+ case 'generate_reports':
1084
+ return await generateReports(validatedArgs);
1085
+ default:
1086
+ throw new McpAdrError(`Unknown operation: ${validatedArgs.operation}`, 'UNKNOWN_OPERATION');
1087
+ }
1088
+ }
1089
+ catch (error) {
1090
+ if (error instanceof z.ZodError) {
1091
+ throw new McpAdrError(`Validation error: ${error.errors.map(e => e.message).join(', ')}`, 'VALIDATION_ERROR');
1092
+ }
1093
+ throw error;
1094
+ }
1095
+ }
1096
+ //# sourceMappingURL=mcp-planning-tool.js.map