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.
- package/README.md +87 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/src/index.d.ts +18 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +555 -26
- package/dist/src/index.js.map +1 -1
- package/dist/src/tools/mcp-planning-tool.d.ts +26 -0
- package/dist/src/tools/mcp-planning-tool.d.ts.map +1 -0
- package/dist/src/tools/mcp-planning-tool.js +1096 -0
- package/dist/src/tools/mcp-planning-tool.js.map +1 -0
- package/dist/src/tools/todo-management-tool-v2.d.ts.map +1 -1
- package/dist/src/tools/todo-management-tool-v2.js +69 -15
- package/dist/src/tools/todo-management-tool-v2.js.map +1 -1
- package/dist/src/utils/directory-compat.d.ts +8 -0
- package/dist/src/utils/directory-compat.d.ts.map +1 -0
- package/dist/src/utils/directory-compat.js +27 -0
- package/dist/src/utils/directory-compat.js.map +1 -0
- package/dist/src/utils/todo-json-manager.d.ts.map +1 -1
- package/dist/src/utils/todo-json-manager.js +5 -3
- package/dist/src/utils/todo-json-manager.js.map +1 -1
- package/package.json +13 -12
|
@@ -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
|