@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
1,648 lines (1,534 loc) • 64.1 kB
text/typescript
import { JSONSchema7 } from 'json-schema';
import { randomUUID } from 'crypto';
import { createTool, createSuccessResult, createErrorResult } from '../../core/tool-framework.js';
import { ToolRegistration, RequestContext } from '../../core/types.js';
/**
* Product Roadmap Tools - 12-Factor MCP Implementation
*
* Implements Factor 2: Deterministic Execution with structured outputs
* Implements Factor 3: Stateless Processes with RequestContext
* Implements Factor 4: Structured Outputs for LLM consumption
*/
// Input type interfaces
interface CreateRoadmapInput {
name: string;
vision: string;
timeHorizon: 'quarterly' | 'annual' | 'multi-year';
owner: string;
stakeholders?: string[];
}
interface AddRoadmapThemeInput {
roadmapId: string;
name: string;
description: string;
objectives: string[];
priority: 'must-have' | 'should-have' | 'nice-to-have';
startQuarter: string;
endQuarter: string;
}
interface CreateInitiativeInput {
roadmapId: string;
themeId: string;
title: string;
description: string;
estimatedValue: {
userImpact: 'low' | 'medium' | 'high' | 'critical';
revenueImpact: number;
costSavings: number;
strategicValue: number;
customerSatisfaction: number;
};
estimatedEffort: {
developmentWeeks: number;
designWeeks: number;
qaWeeks: number;
confidence: 'low' | 'medium' | 'high';
};
risks?: Array<{
description: string;
likelihood: 'low' | 'medium' | 'high';
impact: 'low' | 'medium' | 'high';
mitigation: string;
}>;
}
interface AddFeatureInput {
roadmapId: string;
initiativeId: string;
name: string;
description: string;
businessValue: {
score: number;
rationale: string;
metrics: string[];
};
technicalComplexity: 'low' | 'medium' | 'high' | 'very-high';
targetRelease?: string;
}
interface UpdateFeatureStatusInput {
featureId: string;
status: 'proposed' | 'approved' | 'in-progress' | 'completed' | 'cancelled';
}
interface UpdateInitiativeStatusInput {
initiativeId: string;
status: 'ideation' | 'validated' | 'scheduled' | 'in-development' | 'launched';
}
interface CreateMilestoneInput {
roadmapId: string;
name: string;
date: string;
type: 'release' | 'business' | 'technical' | 'regulatory';
description: string;
deliverables: string[];
dependencies?: string[];
}
interface PlanReleaseInput {
roadmapId: string;
version: string;
name: string;
date: string;
features: string[];
themes?: string[];
goals: string[];
notes?: string;
}
interface PrioritizeFeaturesInput {
roadmapId: string;
method: 'rice' | 'value-effort' | 'moscow' | 'kano' | 'custom';
weights?: {
businessValue: number;
userImpact: number;
strategicAlignment: number;
technicalFeasibility: number;
risk: number;
};
scope?: 'all' | 'theme' | 'initiative' | 'unscheduled';
scopeId?: string;
}
interface GenerateTimelineInput {
roadmapId: string;
viewType: 'quarterly' | 'monthly' | 'release' | 'now-next-later';
startPeriod?: string;
endPeriod?: string;
months?: number;
}
/**
* Create a new product roadmap
*/
const createRoadmapTool = createTool<CreateRoadmapInput, any>({
name: 'create_roadmap',
description: 'Create a new product roadmap with vision and strategic timeline',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name of the product roadmap',
minLength: 1,
maxLength: 200
},
vision: {
type: 'string',
description: 'Product vision statement',
minLength: 1,
maxLength: 1000
},
timeHorizon: {
type: 'string',
enum: ['quarterly', 'annual', 'multi-year'],
description: 'Planning time horizon'
},
owner: {
type: 'string',
description: 'Product owner name',
minLength: 1,
maxLength: 100
},
stakeholders: {
type: 'array',
items: { type: 'string', maxLength: 100 },
description: 'List of stakeholder names',
maxItems: 20
}
},
required: ['name', 'vision', 'timeHorizon', 'owner'],
additionalProperties: false
} as JSONSchema7,
async execute(input: CreateRoadmapInput, context: RequestContext) {
try {
const roadmapId = `roadmap-${randomUUID()}`;
const now = Date.now();
// Check for duplicate roadmap names
const existingCheck = await context.db.get(
'SELECT id FROM product_roadmaps WHERE name = ? AND project_id = ?',
[input.name, context.projectId || 'default']
);
if (existingCheck.success && existingCheck.data) {
return createErrorResult({
code: 'DUPLICATE_RESOURCE',
message: 'A roadmap with this name already exists',
category: 'validation'
});
}
// Insert roadmap into database
const result = await context.db.run(
`INSERT INTO product_roadmaps
(id, project_id, name, vision, time_horizon, status, owner, stakeholders, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
roadmapId,
context.projectId || 'default',
input.name,
input.vision,
input.timeHorizon,
'draft',
input.owner,
JSON.stringify(input.stakeholders || []),
now,
now
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create roadmap',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
roadmap: {
id: roadmapId,
name: input.name,
vision: input.vision,
timeHorizon: input.timeHorizon,
status: 'draft',
owner: input.owner,
stakeholders: input.stakeholders || []
},
message: `Product roadmap "${input.name}" created successfully`,
nextSteps: [
'Add strategic themes using add_roadmap_theme',
'Create initiatives within themes',
'Add features to initiatives',
'Set up milestones and releases',
'Prioritize features using prioritize_features'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to create roadmap: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Add a strategic theme to a roadmap
*/
const addRoadmapThemeTool = createTool<AddRoadmapThemeInput, any>({
name: 'add_roadmap_theme',
description: 'Add a strategic theme to a product roadmap',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
name: {
type: 'string',
description: 'Theme name',
minLength: 1,
maxLength: 200
},
description: {
type: 'string',
description: 'Theme description',
minLength: 1,
maxLength: 1000
},
objectives: {
type: 'array',
items: { type: 'string', maxLength: 500 },
description: 'Strategic objectives',
minItems: 1,
maxItems: 10
},
priority: {
type: 'string',
enum: ['must-have', 'should-have', 'nice-to-have'],
description: 'Theme priority'
},
startQuarter: {
type: 'string',
description: 'Start quarter (e.g., Q1 2024)',
pattern: '^Q[1-4] \\d{4}$'
},
endQuarter: {
type: 'string',
description: 'End quarter',
pattern: '^Q[1-4] \\d{4}$'
}
},
required: ['roadmapId', 'name', 'description', 'objectives', 'priority', 'startQuarter', 'endQuarter'],
additionalProperties: false
} as JSONSchema7,
async execute(input: AddRoadmapThemeInput, context: RequestContext) {
try {
// Verify roadmap exists
const roadmapCheck = await context.db.get(
'SELECT id FROM product_roadmaps WHERE id = ? AND project_id = ?',
[input.roadmapId, context.projectId || 'default']
);
if (!roadmapCheck.success || !roadmapCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Roadmap not found',
details: { roadmapId: input.roadmapId },
category: 'validation'
});
}
const themeId = `theme-${randomUUID()}`;
const now = Date.now();
// Insert theme
const result = await context.db.run(
`INSERT INTO roadmap_themes
(id, roadmap_id, name, description, objectives, priority, start_quarter, end_quarter, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
themeId,
input.roadmapId,
input.name,
input.description,
JSON.stringify(input.objectives),
input.priority,
input.startQuarter,
input.endQuarter,
'planned',
now,
now
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create theme',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
theme: {
id: themeId,
name: input.name,
description: input.description,
objectives: input.objectives,
priority: input.priority,
timeframe: {
startQuarter: input.startQuarter,
endQuarter: input.endQuarter
},
status: 'planned'
},
message: `Theme "${input.name}" added to roadmap`,
nextSteps: [
'Create initiatives within this theme using create_initiative',
'Track progress through the theme lifecycle'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to add theme: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Create a product initiative
*/
const createInitiativeTool = createTool<CreateInitiativeInput, any>({
name: 'create_initiative',
description: 'Create a product initiative within a theme',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
themeId: {
type: 'string',
description: 'ID of the theme',
pattern: '^theme-[a-f0-9-]+$'
},
title: {
type: 'string',
description: 'Initiative title',
minLength: 1,
maxLength: 200
},
description: {
type: 'string',
description: 'Initiative description',
minLength: 1,
maxLength: 2000
},
estimatedValue: {
type: 'object',
properties: {
userImpact: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical']
},
revenueImpact: {
type: 'number',
description: 'Estimated revenue impact',
minimum: 0
},
costSavings: {
type: 'number',
description: 'Estimated cost savings',
minimum: 0
},
strategicValue: {
type: 'integer',
description: 'Strategic value (1-10)',
minimum: 1,
maximum: 10
},
customerSatisfaction: {
type: 'integer',
description: 'Projected NPS impact',
minimum: -100,
maximum: 100
}
},
required: ['userImpact', 'revenueImpact', 'costSavings', 'strategicValue', 'customerSatisfaction'],
additionalProperties: false
},
estimatedEffort: {
type: 'object',
properties: {
developmentWeeks: {
type: 'number',
minimum: 0
},
designWeeks: {
type: 'number',
minimum: 0
},
qaWeeks: {
type: 'number',
minimum: 0
},
confidence: {
type: 'string',
enum: ['low', 'medium', 'high']
}
},
required: ['developmentWeeks', 'designWeeks', 'qaWeeks', 'confidence'],
additionalProperties: false
},
risks: {
type: 'array',
items: {
type: 'object',
properties: {
description: {
type: 'string',
maxLength: 500
},
likelihood: {
type: 'string',
enum: ['low', 'medium', 'high']
},
impact: {
type: 'string',
enum: ['low', 'medium', 'high']
},
mitigation: {
type: 'string',
maxLength: 500
}
},
required: ['description', 'likelihood', 'impact', 'mitigation'],
additionalProperties: false
},
maxItems: 10
}
},
required: ['roadmapId', 'themeId', 'title', 'description', 'estimatedValue', 'estimatedEffort'],
additionalProperties: false
} as JSONSchema7,
async execute(input: CreateInitiativeInput, context: RequestContext) {
try {
// Verify theme exists
const themeCheck = await context.db.get(
`SELECT t.id FROM roadmap_themes t
JOIN product_roadmaps r ON t.roadmap_id = r.id
WHERE t.id = ? AND t.roadmap_id = ? AND r.project_id = ?`,
[input.themeId, input.roadmapId, context.projectId || 'default']
);
if (!themeCheck.success || !themeCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Theme not found in specified roadmap',
details: { themeId: input.themeId, roadmapId: input.roadmapId },
category: 'validation'
});
}
const initiativeId = `initiative-${randomUUID()}`;
const now = Date.now();
// Insert initiative
const result = await context.db.run(
`INSERT INTO roadmap_initiatives
(id, theme_id, roadmap_id, title, description, status,
user_impact, revenue_impact, cost_savings, strategic_value, customer_satisfaction,
development_weeks, design_weeks, qa_weeks, effort_confidence,
risks, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
initiativeId,
input.themeId,
input.roadmapId,
input.title,
input.description,
'ideation',
input.estimatedValue.userImpact,
input.estimatedValue.revenueImpact,
input.estimatedValue.costSavings,
input.estimatedValue.strategicValue,
input.estimatedValue.customerSatisfaction,
input.estimatedEffort.developmentWeeks,
input.estimatedEffort.designWeeks,
input.estimatedEffort.qaWeeks,
input.estimatedEffort.confidence,
JSON.stringify(input.risks || []),
now,
now
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create initiative',
details: { error: result.error },
category: 'system'
});
}
const totalEffort = input.estimatedEffort.developmentWeeks +
input.estimatedEffort.designWeeks +
input.estimatedEffort.qaWeeks;
return createSuccessResult({
initiative: {
id: initiativeId,
title: input.title,
description: input.description,
status: 'ideation',
value: input.estimatedValue,
effort: input.estimatedEffort,
totalEffortWeeks: totalEffort,
risks: input.risks || []
},
message: `Initiative "${input.title}" created`,
nextSteps: [
'Add features to this initiative using add_feature',
'Validate the initiative with stakeholders',
'Link to agile epics for implementation'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to create initiative: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Add a feature to an initiative
*/
const addFeatureTool = createTool<AddFeatureInput, any>({
name: 'add_feature',
description: 'Add a feature to an initiative',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
initiativeId: {
type: 'string',
description: 'ID of the initiative',
pattern: '^initiative-[a-f0-9-]+$'
},
name: {
type: 'string',
description: 'Feature name',
minLength: 1,
maxLength: 200
},
description: {
type: 'string',
description: 'Feature description',
minLength: 1,
maxLength: 1000
},
businessValue: {
type: 'object',
properties: {
score: {
type: 'integer',
description: 'Business value score (1-100)',
minimum: 1,
maximum: 100
},
rationale: {
type: 'string',
description: 'Value rationale',
maxLength: 1000
},
metrics: {
type: 'array',
items: { type: 'string', maxLength: 200 },
description: 'Success metrics',
minItems: 1,
maxItems: 10
}
},
required: ['score', 'rationale', 'metrics'],
additionalProperties: false
},
technicalComplexity: {
type: 'string',
enum: ['low', 'medium', 'high', 'very-high'],
description: 'Technical complexity'
},
targetRelease: {
type: 'string',
description: 'Target release ID (optional)',
pattern: '^release-[a-f0-9-]+$'
}
},
required: ['roadmapId', 'initiativeId', 'name', 'description', 'businessValue', 'technicalComplexity'],
additionalProperties: false
} as JSONSchema7,
async execute(input: AddFeatureInput, context: RequestContext) {
try {
// Verify initiative exists
const initiativeCheck = await context.db.get(
`SELECT i.id FROM roadmap_initiatives i
JOIN product_roadmaps r ON i.roadmap_id = r.id
WHERE i.id = ? AND i.roadmap_id = ? AND r.project_id = ?`,
[input.initiativeId, input.roadmapId, context.projectId || 'default']
);
if (!initiativeCheck.success || !initiativeCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Initiative not found in specified roadmap',
details: { initiativeId: input.initiativeId, roadmapId: input.roadmapId },
category: 'validation'
});
}
const featureId = `feature-${randomUUID()}`;
const now = Date.now();
// Insert feature
const result = await context.db.run(
`INSERT INTO roadmap_features
(id, initiative_id, roadmap_id, name, description, status, priority,
business_value_score, business_value_rationale, business_value_metrics,
technical_complexity, target_release, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
featureId,
input.initiativeId,
input.roadmapId,
input.name,
input.description,
'proposed',
input.businessValue.score, // Using score as priority
input.businessValue.score,
input.businessValue.rationale,
JSON.stringify(input.businessValue.metrics),
input.technicalComplexity,
input.targetRelease || null,
now,
now
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to add feature',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
feature: {
id: featureId,
name: input.name,
description: input.description,
status: 'proposed',
businessValue: input.businessValue,
technicalComplexity: input.technicalComplexity,
targetRelease: input.targetRelease
},
message: `Feature "${input.name}" added`,
nextSteps: [
'Prioritize features using prioritize_features',
'Assign to a release using plan_release',
'Link to user stories for implementation'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to add feature: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Update feature status
*/
const updateFeatureStatusTool = createTool<UpdateFeatureStatusInput, any>({
name: 'update_feature_status',
description: 'Update the status of a feature',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
featureId: {
type: 'string',
description: 'ID of the feature',
pattern: '^feature-[a-f0-9-]+$'
},
status: {
type: 'string',
enum: ['proposed', 'approved', 'in-progress', 'completed', 'cancelled'],
description: 'New status'
}
},
required: ['featureId', 'status'],
additionalProperties: false
} as JSONSchema7,
async execute(input: UpdateFeatureStatusInput, context: RequestContext) {
try {
const result = await context.db.run(
`UPDATE roadmap_features
SET status = ?, updated_at = ?
WHERE id = ? AND roadmap_id IN (
SELECT id FROM product_roadmaps WHERE project_id = ?
)`,
[input.status, Date.now(), input.featureId, context.projectId || 'default']
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to update feature status',
details: { error: result.error },
category: 'system'
});
}
if (!result.data || result.data.changes === 0) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Feature not found',
details: { featureId: input.featureId },
category: 'validation'
});
}
// Get feature details
const feature = await context.db.get(
'SELECT name FROM roadmap_features WHERE id = ?',
[input.featureId]
);
return createSuccessResult({
featureId: input.featureId,
featureName: feature.data?.name || 'Unknown',
status: input.status,
message: `Feature status updated to: ${input.status}`
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to update feature status: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Update initiative status
*/
const updateInitiativeStatusTool = createTool<UpdateInitiativeStatusInput, any>({
name: 'update_initiative_status',
description: 'Update the status of an initiative',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
initiativeId: {
type: 'string',
description: 'ID of the initiative',
pattern: '^initiative-[a-f0-9-]+$'
},
status: {
type: 'string',
enum: ['ideation', 'validated', 'scheduled', 'in-development', 'launched'],
description: 'New status'
}
},
required: ['initiativeId', 'status'],
additionalProperties: false
} as JSONSchema7,
async execute(input: UpdateInitiativeStatusInput, context: RequestContext) {
try {
const result = await context.db.run(
`UPDATE roadmap_initiatives
SET status = ?, updated_at = ?
WHERE id = ? AND roadmap_id IN (
SELECT id FROM product_roadmaps WHERE project_id = ?
)`,
[input.status, Date.now(), input.initiativeId, context.projectId || 'default']
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to update initiative status',
details: { error: result.error },
category: 'system'
});
}
if (!result.data || result.data.changes === 0) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Initiative not found',
details: { initiativeId: input.initiativeId },
category: 'validation'
});
}
// Get initiative details
const initiative = await context.db.get(
'SELECT title FROM roadmap_initiatives WHERE id = ?',
[input.initiativeId]
);
return createSuccessResult({
initiativeId: input.initiativeId,
initiativeTitle: initiative.data?.title || 'Unknown',
status: input.status,
message: `Initiative status updated to: ${input.status}`
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to update initiative status: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Create a milestone
*/
const createMilestoneTool = createTool<CreateMilestoneInput, any>({
name: 'create_milestone',
description: 'Create a milestone in the roadmap',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
name: {
type: 'string',
description: 'Milestone name',
minLength: 1,
maxLength: 200
},
date: {
type: 'string',
format: 'date',
description: 'Milestone date (ISO format)'
},
type: {
type: 'string',
enum: ['release', 'business', 'technical', 'regulatory'],
description: 'Type of milestone'
},
description: {
type: 'string',
description: 'Milestone description',
minLength: 1,
maxLength: 1000
},
deliverables: {
type: 'array',
items: { type: 'string', maxLength: 200 },
description: 'List of deliverables',
minItems: 1,
maxItems: 20
},
dependencies: {
type: 'array',
items: {
type: 'string',
pattern: '^milestone-[a-f0-9-]+$'
},
description: 'List of dependency IDs',
maxItems: 10
}
},
required: ['roadmapId', 'name', 'date', 'type', 'description', 'deliverables'],
additionalProperties: false
} as JSONSchema7,
async execute(input: CreateMilestoneInput, context: RequestContext) {
try {
// Verify roadmap exists
const roadmapCheck = await context.db.get(
'SELECT id FROM product_roadmaps WHERE id = ? AND project_id = ?',
[input.roadmapId, context.projectId || 'default']
);
if (!roadmapCheck.success || !roadmapCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Roadmap not found',
details: { roadmapId: input.roadmapId },
category: 'validation'
});
}
const milestoneId = `milestone-${randomUUID()}`;
const milestoneDate = new Date(input.date);
const now = Date.now();
// Insert milestone
const result = await context.db.run(
`INSERT INTO roadmap_milestones
(id, roadmap_id, name, date, type, description, deliverables, dependencies, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
milestoneId,
input.roadmapId,
input.name,
milestoneDate.getTime(),
input.type,
input.description,
JSON.stringify(input.deliverables),
JSON.stringify(input.dependencies || []),
'upcoming',
now,
now
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create milestone',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
milestone: {
id: milestoneId,
name: input.name,
date: input.date,
type: input.type,
description: input.description,
deliverables: input.deliverables,
dependencies: input.dependencies || [],
status: 'upcoming'
},
message: `Milestone "${input.name}" created`,
nextSteps: [
'Track milestone progress',
'Update status as work progresses',
'Link features to milestone deliverables'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to create milestone: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Plan a release
*/
const planReleaseTool = createTool<PlanReleaseInput, any>({
name: 'plan_release',
description: 'Plan a product release',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
version: {
type: 'string',
description: 'Release version',
minLength: 1,
maxLength: 50
},
name: {
type: 'string',
description: 'Release name',
minLength: 1,
maxLength: 200
},
date: {
type: 'string',
format: 'date',
description: 'Release date (ISO format)'
},
features: {
type: 'array',
items: {
type: 'string',
pattern: '^feature-[a-f0-9-]+$'
},
description: 'Feature IDs to include',
minItems: 1,
maxItems: 100
},
themes: {
type: 'array',
items: {
type: 'string',
pattern: '^theme-[a-f0-9-]+$'
},
description: 'Theme IDs addressed',
maxItems: 20
},
goals: {
type: 'array',
items: { type: 'string', maxLength: 500 },
description: 'Release goals',
minItems: 1,
maxItems: 10
},
notes: {
type: 'string',
description: 'Release notes',
maxLength: 5000
}
},
required: ['roadmapId', 'version', 'name', 'date', 'features', 'goals'],
additionalProperties: false
} as JSONSchema7,
async execute(input: PlanReleaseInput, context: RequestContext) {
try {
// Verify roadmap exists
const roadmapCheck = await context.db.get(
'SELECT id FROM product_roadmaps WHERE id = ? AND project_id = ?',
[input.roadmapId, context.projectId || 'default']
);
if (!roadmapCheck.success || !roadmapCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Roadmap not found',
details: { roadmapId: input.roadmapId },
category: 'validation'
});
}
// Verify all features exist
const featurePlaceholders = input.features.map(() => '?').join(',');
const featureCheck = await context.db.all(
`SELECT id FROM roadmap_features
WHERE id IN (${featurePlaceholders}) AND roadmap_id = ?`,
[...input.features, input.roadmapId]
);
if (!featureCheck.success || featureCheck.data.length !== input.features.length) {
return createErrorResult({
code: 'VALIDATION_ERROR',
message: 'One or more features not found in this roadmap',
category: 'validation'
});
}
const releaseId = `release-${randomUUID()}`;
const releaseDate = new Date(input.date);
const now = Date.now();
// Insert release
const result = await context.db.run(
`INSERT INTO roadmap_releases
(id, roadmap_id, version, name, date, features, themes, goals, status, notes, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
releaseId,
input.roadmapId,
input.version,
input.name,
releaseDate.getTime(),
JSON.stringify(input.features),
JSON.stringify(input.themes || []),
JSON.stringify(input.goals),
'planning',
input.notes || null,
now,
now
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to plan release',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
release: {
id: releaseId,
version: input.version,
name: input.name,
date: input.date,
featureCount: input.features.length,
themeCount: (input.themes || []).length,
goals: input.goals,
status: 'planning'
},
message: `Release v${input.version} planned`,
nextSteps: [
'Review release scope with stakeholders',
'Update feature statuses as development progresses',
'Prepare release notes and documentation'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to plan release: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Prioritize features
*/
const prioritizeFeaturesTool = createTool<PrioritizeFeaturesInput, any>({
name: 'prioritize_features',
description: 'Prioritize features using various methods (RICE, MoSCoW, Value-Effort, etc.)',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
method: {
type: 'string',
enum: ['rice', 'value-effort', 'moscow', 'kano', 'custom'],
description: 'Prioritization method'
},
weights: {
type: 'object',
properties: {
businessValue: { type: 'number', minimum: 0, maximum: 1 },
userImpact: { type: 'number', minimum: 0, maximum: 1 },
strategicAlignment: { type: 'number', minimum: 0, maximum: 1 },
technicalFeasibility: { type: 'number', minimum: 0, maximum: 1 },
risk: { type: 'number', minimum: 0, maximum: 1 }
},
description: 'Custom weights (only for custom method)',
additionalProperties: false
},
scope: {
type: 'string',
enum: ['all', 'theme', 'initiative', 'unscheduled'],
description: 'Scope of prioritization',
default: 'all'
},
scopeId: {
type: 'string',
description: 'ID of theme/initiative if scope is specific'
}
},
required: ['roadmapId', 'method'],
additionalProperties: false
} as JSONSchema7,
async execute(input: PrioritizeFeaturesInput, context: RequestContext) {
try {
// Build query based on scope
let query = `
SELECT f.*, i.user_impact, i.strategic_value, i.development_weeks
FROM roadmap_features f
JOIN roadmap_initiatives i ON f.initiative_id = i.id
WHERE f.roadmap_id = ?
`;
const params: any[] = [input.roadmapId];
if (input.scope === 'theme' && input.scopeId) {
query += ' AND i.theme_id = ?';
params.push(input.scopeId);
} else if (input.scope === 'initiative' && input.scopeId) {
query += ' AND f.initiative_id = ?';
params.push(input.scopeId);
} else if (input.scope === 'unscheduled') {
query += ' AND f.target_release IS NULL';
}
const features = await context.db.all(query, params);
if (!features.success || features.data.length === 0) {
return createErrorResult({
code: 'NO_DATA',
message: 'No features found for prioritization',
category: 'validation'
});
}
// Calculate scores based on method
const scoredFeatures = features.data.map((feature: any) => {
let score = 0;
let rationale = '';
switch (input.method) {
case 'rice':
// RICE = (Reach * Impact * Confidence) / Effort
const reach = 100; // Default reach
const impact = feature.user_impact === 'critical' ? 3 :
feature.user_impact === 'high' ? 2 :
feature.user_impact === 'medium' ? 1 : 0.5;
const confidence = 0.8; // Default confidence
const effort = feature.development_weeks || 1;
score = (reach * impact * confidence) / effort;
rationale = `RICE Score: Reach(${reach}) × Impact(${impact}) × Confidence(${confidence}) ÷ Effort(${effort}w)`;
break;
case 'value-effort':
const value = feature.business_value_score || 50;
const effortScore = feature.technical_complexity === 'very-high' ? 4 :
feature.technical_complexity === 'high' ? 3 :
feature.technical_complexity === 'medium' ? 2 : 1;
score = value / effortScore;
rationale = `Value(${value}) / Effort(${effortScore})`;
break;
case 'moscow':
// Assign scores based on business value ranges
if (feature.business_value_score >= 80) {
score = 4;
rationale = 'Must Have (80+ value)';
} else if (feature.business_value_score >= 60) {
score = 3;
rationale = 'Should Have (60-79 value)';
} else if (feature.business_value_score >= 40) {
score = 2;
rationale = 'Could Have (40-59 value)';
} else {
score = 1;
rationale = 'Won\'t Have (<40 value)';
}
break;
case 'custom':
if (!input.weights) {
score = feature.priority || 50;
rationale = 'Using default priority';
} else {
// Apply custom weights
const bv = (feature.business_value_score || 50) * (input.weights.businessValue || 0);
const ui = (feature.user_impact === 'critical' ? 100 :
feature.user_impact === 'high' ? 75 :
feature.user_impact === 'medium' ? 50 : 25) * (input.weights.userImpact || 0);
const sa = (feature.strategic_value || 5) * 10 * (input.weights.strategicAlignment || 0);
const tf = (feature.technical_complexity === 'low' ? 100 :
feature.technical_complexity === 'medium' ? 75 :
feature.technical_complexity === 'high' ? 50 : 25) * (input.weights.technicalFeasibility || 0);
score = bv + ui + sa + tf;
rationale = `Custom: BV(${bv.toFixed(1)}) + UI(${ui.toFixed(1)}) + SA(${sa.toFixed(1)}) + TF(${tf.toFixed(1)})`;
}
break;
default:
score = feature.priority || 50;
rationale = 'Default priority';
}
return {
featureId: feature.id,
name: feature.name,
score,
rationale,
currentStatus: feature.status,
complexity: feature.technical_complexity
};
});
// Sort by score descending
scoredFeatures.sort((a: any, b: any) => b.score - a.score);
// Add rank
scoredFeatures.forEach((f: any, index: number) => {
f.rank = index + 1;
});
// Return top 10 for display
const topFeatures = scoredFeatures.slice(0, 10);
return createSuccessResult({
method: input.method,
scope: input.scope,
totalFeatures: scoredFeatures.length,
topFeatures,
message: `Prioritized ${scoredFeatures.length} features using ${input.method.toUpperCase()} method`,
insights: [
`Highest priority: "${topFeatures[0].name}" (score: ${topFeatures[0].score.toFixed(2)})`,
`${scoredFeatures.filter((f: any) => f.currentStatus === 'proposed').length} features awaiting approval`,
`Consider reviewing low-complexity, high-value features for quick wins`
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to prioritize features: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Generate roadmap timeline
*/
const generateTimelineTool = createTool<GenerateTimelineInput, any>({
name: 'generate_roadmap_timeline',
description: 'Generate timeline views of the roadmap',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
viewType: {
type: 'string',
enum: ['quarterly', 'monthly', 'release', 'now-next-later'],
description: 'Type of timeline view'
},
startPeriod: {
type: 'string',
description: 'Start quarter/month (for quarterly/monthly views)'
},
endPeriod: {
type: 'string',
description: 'End quarter/month (for quarterly view)'
},
months: {
type: 'integer',
description: 'Number of months (for monthly view)',
minimum: 1,
maximum: 24,
default: 6
}
},
required: ['roadmapId', 'viewType'],
additionalProperties: false
} as JSONSchema7,
async execute(input: GenerateTimelineInput, context: RequestContext) {
try {
// Verify roadmap exists
const roadmapCheck = await context.db.get(
'SELECT * FROM product_roadmaps WHERE id = ? AND project_id = ?',
[input.roadmapId, context.projectId || 'default']
);
if (!roadmapCheck.success || !roadmapCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Roadmap not found',
details: { roadmapId: input.roadmapId },
category: 'validation'
});
}
const roadmap = roadmapCheck.data;
let timelineData: any = {};
switch (input.viewType) {
case 'quarterly':
// Get themes for quarterly view
const themes = await context.db.all(
`SELECT * FROM roadmap_themes
WHERE roadmap_id = ?
ORDER BY start_quarter`,
[input.roadmapId]
);
if (themes.success && themes.data) {
const quarters = new Set<string>();
themes.data.forEach((theme: any) => {
quarters.add(theme.start_quarter);
quarters.add(theme.end_quarter);
});
timelineData = {
type: 'quarterly',
quarters: Array.from(quarters).sort(),
items: themes.data.map((theme: any) => ({
id: theme.id,
type: 'theme',
name: theme.name,
startQuarter: theme.start_quarter,
endQuarter: theme.end_quarter,
priority: theme.priority,
status: theme.status
}))
};
}
break;
case 'release':
// Get releases
const releases = await context.db.all(
`SELECT * FROM roadmap_releases
WHERE roadmap_id = ?
ORDER BY date`,
[input.roadmapId]
);
if (releases.success && releases.data) {
// Get feature counts for each release
const releaseFeatures = await Promise.all(
releases.data.map(async (release: any) => {
const features = JSON.parse(release.features);
return {
...release,
featureCount: features.length,
date: new Date(release.date).toISOString()
};
})
);
timelineData = {
type: 'release',
releases: releaseFeatures.map((r: any) => ({
id: r.id,
version: r.version,
name: r.name,
date: r.date,
status: r.status,
featureCount: r.featureCount,
goals: JSON.parse(r.goals)
}))
};
}
break;
case 'now-next-later':
// Get all initiatives with their status
const initiatives = await context.db.all(
`SELECT i.*, t.name as theme_name, t.priority as theme_priority
FROM roadmap_initiatives i
JOIN roadmap_themes t ON i.theme_id = t.id
WHERE i.roadmap_id = ?`,
[input.roadmapId]
);
if (initiatives.success && initiatives.data) {
const now = initiatives.data.filter((i: any) =>
i.status === 'in-development' || i.status === 'validated'
);
const next = initiatives.data.filter((i: any) =>
i.status === 'scheduled'
);
const later = initiatives.data.filter((i: any) =>
i.status === 'ideation'
);
timelineData = {
type: 'now-next-later',
now: now.map((i: any) => ({
id: i.id,
title: i.title,
theme: i.theme_name,
priority: i.theme_priority,
status: i.status,
userImpact: i.user_impact
})),
next: next.map((i: any) => ({
id: i.id,
title: i.title,
theme: i.theme_name,
priority: i.theme_priority,
status: i.status,
userImpact: i.user_impact
})),
later: later.map((i: any) => ({
id: i.id,
title: i.title,
theme: i.theme_name,
priority: i.theme_priority,
status: i.status,
userImpact: i.user_impact
}))
};
}
break;
default:
return createErrorResult({
code: 'NOT_IMPLEMENTED',
message: `Timeline view '${input.viewType}' not implemented`,
category: 'validation'
});
}
return createSuccessResult({
roadmap: {
id: roadmap.id,
name: roadmap.name,
timeHorizon: roadmap.time_horizon
},
timeline: timelineData,
message: `Generated ${input.viewType} timeline view`,
visualization: `Timeline view ready for visualization`
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to generate timeline: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* List roadmaps
*/
const listRoadmapsTool = createTool<{}, any>({
name: 'list_roadmaps',
description: 'List all product roadmaps',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false
} as JSONSchema7,
async execute(input: {}, context: RequestContext) {
try {
const roadmaps = await context.db.all(
`SELECT r.*,
(SELECT COUNT(*) FROM roadmap_themes WHERE roadmap_id = r.id) as theme_count,
(SELECT COUNT(*) FROM roadmap_milestones WHERE roadmap_id = r.id) as milestone_count,
(SELECT COUNT(*) FROM roadmap_releases WHERE roadmap_id = r.id) as release_count
FROM product_roadmaps r
WHERE r.project_id = ?
ORDER BY r.updated_at DESC`,
[context.projectId || 'default']
);
if (!roadmaps.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to fetch roadmaps',
details: { error: roadmaps.error },
category: 'system'
});
}
if (roadmaps.data.length === 0) {
return createSuccessResult({
roadmaps: [],
message: 'No product roadmaps found. Create one using create_roadmap.'
});
}
const roadmapList = roadmaps.data.map((r: any) => ({
id: