@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
1,536 lines (1,422 loc) • 59.5 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 Requirements 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 CreateRequirementInput {
name: string;
type: 'functional' | 'non-functional' | 'business' | 'technical' | 'user-interface' | 'security' | 'performance';
description: string;
acceptanceCriteria?: string[];
priority?: 'low' | 'medium' | 'high' | 'critical';
status?: 'draft' | 'approved' | 'in_development' | 'implemented' | 'deprecated';
repositories?: Array<{
name: string;
implementationStatus: 'not_started' | 'in_progress' | 'completed' | 'blocked';
branch?: string;
prUrl?: string;
notes?: string;
}>;
relatedStories?: string[];
tags?: string[];
businessValue?: string;
technicalNotes?: string;
testCriteria?: string[];
complianceRequirements?: string[];
estimatedEffort?: string;
targetRelease?: string;
parentRequirementId?: string;
}
interface UpdateRequirementInput {
id: string;
name?: string;
type?: 'functional' | 'non-functional' | 'business' | 'technical' | 'user-interface' | 'security' | 'performance';
description?: string;
acceptanceCriteria?: string[];
priority?: 'low' | 'medium' | 'high' | 'critical';
status?: 'draft' | 'approved' | 'in_development' | 'implemented' | 'deprecated';
repositories?: Array<{
name: string;
implementationStatus: 'not_started' | 'in_progress' | 'completed' | 'blocked';
branch?: string;
prUrl?: string;
notes?: string;
}>;
tags?: string[];
businessValue?: string;
technicalNotes?: string;
testCriteria?: string[];
complianceRequirements?: string[];
estimatedEffort?: string;
actualEffort?: string;
targetRelease?: string;
documentationLinks?: string[];
}
interface ListRequirementsInput {
filterByType?: 'functional' | 'non-functional' | 'business' | 'technical' | 'user-interface' | 'security' | 'performance';
filterByStatus?: 'draft' | 'approved' | 'in_development' | 'implemented' | 'deprecated';
filterByPriority?: 'low' | 'medium' | 'high' | 'critical';
filterByRepository?: string;
filterByTag?: string;
filterByParent?: string;
hasStories?: boolean;
implementationStatus?: 'not_started' | 'in_progress' | 'completed' | 'blocked';
}
interface SearchRequirementsInput {
query: string;
searchIn?: ('name' | 'description' | 'acceptance_criteria' | 'tags')[];
limit?: number;
offset?: number;
}
interface LinkRequirementToStoryInput {
requirementId: string;
storyId: string;
linkType?: 'implements' | 'relates_to' | 'depends_on';
notes?: string;
}
interface UpdateImplementationStatusInput {
requirementId: string;
repositoryName: string;
status: 'not_started' | 'in_progress' | 'completed' | 'blocked';
branch?: string;
prUrl?: string;
notes?: string;
}
interface GenerateReportInput {
format?: 'json' | 'markdown' | 'html';
includeImplementationStatus?: boolean;
groupBy?: 'type' | 'status' | 'priority';
filterType?: 'functional' | 'non-functional' | 'business' | 'technical' | 'user-interface' | 'security' | 'performance';
filterStatus?: 'draft' | 'approved' | 'in_development' | 'implemented' | 'deprecated';
filterPriority?: 'low' | 'medium' | 'high' | 'critical';
}
/**
* Create a new product requirement
*/
const createRequirementTool = createTool<CreateRequirementInput, any>({
name: 'create_requirement',
description: 'Create a new product requirement',
category: 'product-requirements',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name of the requirement',
minLength: 1,
maxLength: 200
},
type: {
type: 'string',
enum: ['functional', 'non-functional', 'business', 'technical', 'user-interface', 'security', 'performance'],
description: 'Type of requirement'
},
description: {
type: 'string',
description: 'Detailed description of the requirement',
minLength: 1,
maxLength: 5000
},
acceptanceCriteria: {
type: 'array',
items: { type: 'string', maxLength: 1000 },
description: 'List of acceptance criteria',
maxItems: 20
},
priority: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical'],
description: 'Priority level',
default: 'medium'
},
status: {
type: 'string',
enum: ['draft', 'approved', 'in_development', 'implemented', 'deprecated'],
description: 'Current status',
default: 'draft'
},
repositories: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', maxLength: 100 },
implementationStatus: {
type: 'string',
enum: ['not_started', 'in_progress', 'completed', 'blocked']
},
branch: { type: 'string', maxLength: 100 },
prUrl: { type: 'string', maxLength: 500 },
notes: { type: 'string', maxLength: 1000 }
},
required: ['name', 'implementationStatus'],
additionalProperties: false
},
description: 'Repositories where this requirement is implemented',
maxItems: 10
},
relatedStories: {
type: 'array',
items: { type: 'string' },
description: 'IDs of related user stories',
maxItems: 50
},
tags: {
type: 'array',
items: { type: 'string', maxLength: 50 },
description: 'Tags for categorization',
maxItems: 20
},
businessValue: {
type: 'string',
description: 'Business value statement',
maxLength: 2000
},
technicalNotes: {
type: 'string',
description: 'Technical implementation notes',
maxLength: 5000
},
testCriteria: {
type: 'array',
items: { type: 'string', maxLength: 1000 },
description: 'Testing criteria',
maxItems: 20
},
complianceRequirements: {
type: 'array',
items: { type: 'string', maxLength: 500 },
description: 'Compliance and regulatory requirements',
maxItems: 10
},
estimatedEffort: {
type: 'string',
description: 'Estimated effort (e.g., "2 weeks", "5 story points")',
maxLength: 100
},
targetRelease: {
type: 'string',
description: 'Target release version',
maxLength: 50
},
parentRequirementId: {
type: 'string',
description: 'ID of parent requirement for hierarchical organization',
pattern: '^req-[a-f0-9-]+$'
}
},
required: ['name', 'type', 'description'],
additionalProperties: false
} as JSONSchema7,
async execute(input: CreateRequirementInput, context: RequestContext) {
try {
const requirementId = `req-${randomUUID()}`;
const now = Date.now();
// Validate parent requirement if provided
if (input.parentRequirementId) {
const parentCheck = await context.db.get(
'SELECT id FROM product_requirements WHERE id = ? AND project_id = ?',
[input.parentRequirementId, context.projectId || 'default']
);
if (!parentCheck.success || !parentCheck.data) {
return createErrorResult({
code: 'VALIDATION_ERROR',
message: 'Parent requirement not found',
details: { parentRequirementId: input.parentRequirementId },
category: 'validation'
});
}
}
// Insert requirement
const result = await context.db.run(
`INSERT INTO product_requirements
(id, project_id, name, type, description, acceptance_criteria, priority, status,
related_stories, tags, created_at, updated_at, created_by, version,
parent_requirement_id, business_value, technical_notes, test_criteria,
compliance_requirements, estimated_effort, target_release)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
requirementId,
context.projectId || 'default',
input.name,
input.type,
input.description,
JSON.stringify(input.acceptanceCriteria || []),
input.priority || 'medium',
input.status || 'draft',
JSON.stringify(input.relatedStories || []),
JSON.stringify(input.tags || []),
now,
now,
context.userId || 'system',
1,
input.parentRequirementId || null,
input.businessValue || null,
input.technicalNotes || null,
JSON.stringify(input.testCriteria || []),
JSON.stringify(input.complianceRequirements || []),
input.estimatedEffort || null,
input.targetRelease || null
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create requirement',
details: { error: result.error },
category: 'system'
});
}
// Insert repositories if provided
if (input.repositories && input.repositories.length > 0) {
for (const repo of input.repositories) {
const repoId = `repo-${randomUUID()}`;
await context.db.run(
`INSERT INTO requirement_repositories
(id, requirement_id, project_id, name, implementation_status, branch, pr_url, notes, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
repoId,
requirementId,
context.projectId || 'default',
repo.name,
repo.implementationStatus,
repo.branch || null,
repo.prUrl || null,
repo.notes || null,
now,
now
]
);
}
}
// Update parent requirement's child_requirement_ids if applicable
if (input.parentRequirementId) {
const parent = await context.db.get(
'SELECT child_requirement_ids FROM product_requirements WHERE id = ?',
[input.parentRequirementId]
);
if (parent.success && parent.data) {
const childIds = JSON.parse(parent.data.child_requirement_ids || '[]');
childIds.push(requirementId);
await context.db.run(
'UPDATE product_requirements SET child_requirement_ids = ?, updated_at = ? WHERE id = ?',
[JSON.stringify(childIds), now, input.parentRequirementId]
);
}
}
return createSuccessResult({
requirement: {
id: requirementId,
name: input.name,
type: input.type,
description: input.description,
status: input.status || 'draft',
priority: input.priority || 'medium',
version: 1
},
message: `Created requirement "${input.name}" with ID ${requirementId}`,
nextSteps: [
'Add acceptance criteria if not already provided',
'Link to user stories using link_requirement_to_story',
'Update implementation status as development progresses',
'Review and approve the requirement when ready'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to create requirement: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* List all product requirements with optional filtering
*/
const listRequirementsTool = createTool<ListRequirementsInput, any>({
name: 'list_requirements',
description: 'List all product requirements with optional filtering',
category: 'product-requirements',
inputSchema: {
type: 'object',
properties: {
filterByType: {
type: 'string',
enum: ['functional', 'non-functional', 'business', 'technical', 'user-interface', 'security', 'performance'],
description: 'Filter by requirement type'
},
filterByStatus: {
type: 'string',
enum: ['draft', 'approved', 'in_development', 'implemented', 'deprecated'],
description: 'Filter by status'
},
filterByPriority: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical'],
description: 'Filter by priority'
},
filterByRepository: {
type: 'string',
description: 'Filter by repository name',
maxLength: 100
},
filterByTag: {
type: 'string',
description: 'Filter by tag',
maxLength: 50
},
filterByParent: {
type: 'string',
description: 'Filter by parent requirement ID',
pattern: '^req-[a-f0-9-]+$'
},
hasStories: {
type: 'boolean',
description: 'Filter requirements with/without related stories'
},
implementationStatus: {
type: 'string',
enum: ['not_started', 'in_progress', 'completed', 'blocked'],
description: 'Filter by implementation status in any repository'
}
},
additionalProperties: false
} as JSONSchema7,
async execute(input: ListRequirementsInput, context: RequestContext) {
try {
let query = `
SELECT DISTINCT r.*,
(SELECT COUNT(*) FROM requirement_repositories WHERE requirement_id = r.id) as repo_count,
(SELECT COUNT(*) FROM requirement_story_links WHERE requirement_id = r.id) as story_count
FROM product_requirements r
WHERE r.project_id = ?
`;
const params: any[] = [context.projectId || 'default'];
// Apply filters
if (input.filterByType) {
query += ' AND r.type = ?';
params.push(input.filterByType);
}
if (input.filterByStatus) {
query += ' AND r.status = ?';
params.push(input.filterByStatus);
}
if (input.filterByPriority) {
query += ' AND r.priority = ?';
params.push(input.filterByPriority);
}
if (input.filterByTag) {
query += ' AND r.tags LIKE ?';
params.push(`%"${input.filterByTag}"%`);
}
if (input.filterByParent !== undefined) {
if (input.filterByParent === null) {
query += ' AND r.parent_requirement_id IS NULL';
} else {
query += ' AND r.parent_requirement_id = ?';
params.push(input.filterByParent);
}
}
if (input.hasStories !== undefined) {
if (input.hasStories) {
query += ' AND EXISTS (SELECT 1 FROM requirement_story_links WHERE requirement_id = r.id)';
} else {
query += ' AND NOT EXISTS (SELECT 1 FROM requirement_story_links WHERE requirement_id = r.id)';
}
}
if (input.filterByRepository || input.implementationStatus) {
query += ' AND EXISTS (SELECT 1 FROM requirement_repositories rr WHERE rr.requirement_id = r.id';
if (input.filterByRepository) {
query += ' AND rr.name = ?';
params.push(input.filterByRepository);
}
if (input.implementationStatus) {
query += ' AND rr.implementation_status = ?';
params.push(input.implementationStatus);
}
query += ')';
}
query += ' ORDER BY r.priority DESC, r.updated_at DESC';
const requirements = await context.db.all(query, params);
if (!requirements.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to fetch requirements',
details: { error: requirements.error },
category: 'system'
});
}
if (requirements.data.length === 0) {
return createSuccessResult({
requirements: [],
totalCount: 0,
message: 'No requirements found matching the criteria'
});
}
// Get repository details for each requirement
const requirementsWithRepos = await Promise.all(
requirements.data.map(async (req: any) => {
const repos = await context.db.all(
'SELECT * FROM requirement_repositories WHERE requirement_id = ?',
[req.id]
);
return {
id: req.id,
name: req.name,
type: req.type,
description: req.description,
priority: req.priority,
status: req.status,
tags: JSON.parse(req.tags || '[]'),
repositories: repos.success ? repos.data.map((r: any) => ({
name: r.name,
implementationStatus: r.implementation_status,
branch: r.branch,
prUrl: r.pr_url
})) : [],
storyCount: req.story_count,
createdAt: new Date(req.created_at).toISOString(),
updatedAt: new Date(req.updated_at).toISOString()
};
})
);
// Generate summary statistics
const stats = {
byType: {} as Record<string, number>,
byStatus: {} as Record<string, number>,
byPriority: {} as Record<string, number>
};
requirementsWithRepos.forEach(req => {
stats.byType[req.type] = (stats.byType[req.type] || 0) + 1;
stats.byStatus[req.status] = (stats.byStatus[req.status] || 0) + 1;
stats.byPriority[req.priority] = (stats.byPriority[req.priority] || 0) + 1;
});
return createSuccessResult({
requirements: requirementsWithRepos,
totalCount: requirementsWithRepos.length,
statistics: stats,
message: `Found ${requirementsWithRepos.length} requirement(s)`
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to list requirements: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Get a specific product requirement by ID
*/
const getRequirementTool = createTool<{ id: string }, any>({
name: 'get_requirement',
description: 'Get a specific product requirement by ID',
category: 'product-requirements',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Requirement ID',
pattern: '^req-[a-f0-9-]+$'
}
},
required: ['id'],
additionalProperties: false
} as JSONSchema7,
async execute(input: { id: string }, context: RequestContext) {
try {
const requirement = await context.db.get(
'SELECT * FROM product_requirements WHERE id = ? AND project_id = ?',
[input.id, context.projectId || 'default']
);
if (!requirement.success || !requirement.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: `Requirement ${input.id} not found`,
details: { requirementId: input.id },
category: 'validation'
});
}
// Get repositories
const repos = await context.db.all(
'SELECT * FROM requirement_repositories WHERE requirement_id = ?',
[input.id]
);
// Get story links
const storyLinks = await context.db.all(
'SELECT * FROM requirement_story_links WHERE requirement_id = ?',
[input.id]
);
// Get child requirements if any
let childRequirements = [];
if (requirement.data.child_requirement_ids) {
const childIds = JSON.parse(requirement.data.child_requirement_ids);
if (childIds.length > 0) {
const placeholders = childIds.map(() => '?').join(',');
const children = await context.db.all(
`SELECT id, name, type, status FROM product_requirements WHERE id IN (${placeholders})`,
childIds
);
childRequirements = children.success ? children.data : [];
}
}
const requirementData = {
id: requirement.data.id,
name: requirement.data.name,
type: requirement.data.type,
description: requirement.data.description,
acceptanceCriteria: JSON.parse(requirement.data.acceptance_criteria || '[]'),
priority: requirement.data.priority,
status: requirement.data.status,
repositories: repos.success ? repos.data.map((r: any) => ({
name: r.name,
implementationStatus: r.implementation_status,
branch: r.branch,
prUrl: r.pr_url,
notes: r.notes
})) : [],
relatedStories: JSON.parse(requirement.data.related_stories || '[]'),
storyLinks: storyLinks.success ? storyLinks.data.map((l: any) => ({
storyId: l.story_id,
linkType: l.link_type,
notes: l.notes
})) : [],
tags: JSON.parse(requirement.data.tags || '[]'),
createdAt: new Date(requirement.data.created_at).toISOString(),
updatedAt: new Date(requirement.data.updated_at).toISOString(),
createdBy: requirement.data.created_by,
approvedBy: requirement.data.approved_by,
approvedAt: requirement.data.approved_at ? new Date(requirement.data.approved_at).toISOString() : null,
version: requirement.data.version,
parentRequirementId: requirement.data.parent_requirement_id,
childRequirements,
dependencies: JSON.parse(requirement.data.dependencies || '[]'),
businessValue: requirement.data.business_value,
technicalNotes: requirement.data.technical_notes,
testCriteria: JSON.parse(requirement.data.test_criteria || '[]'),
complianceRequirements: JSON.parse(requirement.data.compliance_requirements || '[]'),
estimatedEffort: requirement.data.estimated_effort,
actualEffort: requirement.data.actual_effort,
targetRelease: requirement.data.target_release,
documentationLinks: JSON.parse(requirement.data.documentation_links || '[]')
};
return createSuccessResult({
requirement: requirementData,
message: `Retrieved requirement "${requirementData.name}"`
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to get requirement: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Update an existing product requirement
*/
const updateRequirementTool = createTool<UpdateRequirementInput, any>({
name: 'update_requirement',
description: 'Update an existing product requirement',
category: 'product-requirements',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Requirement ID to update',
pattern: '^req-[a-f0-9-]+$'
},
name: {
type: 'string',
description: 'Updated name',
minLength: 1,
maxLength: 200
},
type: {
type: 'string',
enum: ['functional', 'non-functional', 'business', 'technical', 'user-interface', 'security', 'performance'],
description: 'Updated type'
},
description: {
type: 'string',
description: 'Updated description',
minLength: 1,
maxLength: 5000
},
acceptanceCriteria: {
type: 'array',
items: { type: 'string', maxLength: 1000 },
description: 'Updated acceptance criteria',
maxItems: 20
},
priority: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical'],
description: 'Updated priority'
},
status: {
type: 'string',
enum: ['draft', 'approved', 'in_development', 'implemented', 'deprecated'],
description: 'Updated status'
},
repositories: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', maxLength: 100 },
implementationStatus: {
type: 'string',
enum: ['not_started', 'in_progress', 'completed', 'blocked']
},
branch: { type: 'string', maxLength: 100 },
prUrl: { type: 'string', maxLength: 500 },
notes: { type: 'string', maxLength: 1000 }
},
required: ['name', 'implementationStatus'],
additionalProperties: false
},
description: 'Updated repository information',
maxItems: 10
},
tags: {
type: 'array',
items: { type: 'string', maxLength: 50 },
description: 'Updated tags',
maxItems: 20
},
businessValue: {
type: 'string',
description: 'Updated business value',
maxLength: 2000
},
technicalNotes: {
type: 'string',
description: 'Updated technical notes',
maxLength: 5000
},
testCriteria: {
type: 'array',
items: { type: 'string', maxLength: 1000 },
description: 'Updated test criteria',
maxItems: 20
},
complianceRequirements: {
type: 'array',
items: { type: 'string', maxLength: 500 },
description: 'Updated compliance requirements',
maxItems: 10
},
estimatedEffort: {
type: 'string',
description: 'Updated estimated effort',
maxLength: 100
},
actualEffort: {
type: 'string',
description: 'Actual effort spent',
maxLength: 100
},
targetRelease: {
type: 'string',
description: 'Updated target release',
maxLength: 50
},
documentationLinks: {
type: 'array',
items: { type: 'string', maxLength: 500 },
description: 'Links to documentation',
maxItems: 10
}
},
required: ['id'],
additionalProperties: false
} as JSONSchema7,
async execute(input: UpdateRequirementInput, context: RequestContext) {
try {
// Check if requirement exists
const existing = await context.db.get(
'SELECT * FROM product_requirements WHERE id = ? AND project_id = ?',
[input.id, context.projectId || 'default']
);
if (!existing.success || !existing.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: `Requirement ${input.id} not found`,
details: { requirementId: input.id },
category: 'validation'
});
}
const now = Date.now();
const updates: string[] = ['updated_at = ?'];
const values: any[] = [now];
// Build update query dynamically
if (input.name !== undefined) {
updates.push('name = ?');
values.push(input.name);
}
if (input.type !== undefined) {
updates.push('type = ?');
values.push(input.type);
}
if (input.description !== undefined) {
updates.push('description = ?');
values.push(input.description);
}
if (input.acceptanceCriteria !== undefined) {
updates.push('acceptance_criteria = ?');
values.push(JSON.stringify(input.acceptanceCriteria));
}
if (input.priority !== undefined) {
updates.push('priority = ?');
values.push(input.priority);
}
if (input.status !== undefined) {
updates.push('status = ?');
values.push(input.status);
// Set approval info if moving to approved status
if (input.status === 'approved' && existing.data.status !== 'approved') {
updates.push('approved_by = ?', 'approved_at = ?');
values.push(context.userId || 'system', now);
}
}
if (input.tags !== undefined) {
updates.push('tags = ?');
values.push(JSON.stringify(input.tags));
}
if (input.businessValue !== undefined) {
updates.push('business_value = ?');
values.push(input.businessValue);
}
if (input.technicalNotes !== undefined) {
updates.push('technical_notes = ?');
values.push(input.technicalNotes);
}
if (input.testCriteria !== undefined) {
updates.push('test_criteria = ?');
values.push(JSON.stringify(input.testCriteria));
}
if (input.complianceRequirements !== undefined) {
updates.push('compliance_requirements = ?');
values.push(JSON.stringify(input.complianceRequirements));
}
if (input.estimatedEffort !== undefined) {
updates.push('estimated_effort = ?');
values.push(input.estimatedEffort);
}
if (input.actualEffort !== undefined) {
updates.push('actual_effort = ?');
values.push(input.actualEffort);
}
if (input.targetRelease !== undefined) {
updates.push('target_release = ?');
values.push(input.targetRelease);
}
if (input.documentationLinks !== undefined) {
updates.push('documentation_links = ?');
values.push(JSON.stringify(input.documentationLinks));
}
// Increment version
updates.push('version = version + 1');
values.push(input.id, context.projectId || 'default');
const updateQuery = `UPDATE product_requirements SET ${updates.join(', ')} WHERE id = ? AND project_id = ?`;
const result = await context.db.run(updateQuery, values);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to update requirement',
details: { error: result.error },
category: 'system'
});
}
// Update repositories if provided
if (input.repositories !== undefined) {
// Delete existing repositories
await context.db.run(
'DELETE FROM requirement_repositories WHERE requirement_id = ?',
[input.id]
);
// Insert new repositories
for (const repo of input.repositories) {
const repoId = `repo-${randomUUID()}`;
await context.db.run(
`INSERT INTO requirement_repositories
(id, requirement_id, project_id, name, implementation_status, branch, pr_url, notes, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
repoId,
input.id,
context.projectId || 'default',
repo.name,
repo.implementationStatus,
repo.branch || null,
repo.prUrl || null,
repo.notes || null,
now,
now
]
);
}
}
// Track changes
const changedFields = Object.keys(input).filter(k => k !== 'id');
for (const field of changedFields) {
const changeId = `change-${randomUUID()}`;
await context.db.run(
`INSERT INTO requirement_changes
(id, requirement_id, project_id, field, old_value, new_value, changed_by, changed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
changeId,
input.id,
context.projectId || 'default',
field,
JSON.stringify(existing.data[field]),
JSON.stringify((input as any)[field]),
context.userId || 'system',
now
]
);
}
// Get updated requirement
const updated = await context.db.get(
'SELECT * FROM product_requirements WHERE id = ?',
[input.id]
);
return createSuccessResult({
requirement: {
id: updated.data.id,
name: updated.data.name,
type: updated.data.type,
status: updated.data.status,
priority: updated.data.priority,
version: updated.data.version
},
message: `Updated requirement "${updated.data.name}"`,
fieldsUpdated: changedFields
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to update requirement: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Delete a product requirement
*/
const deleteRequirementTool = createTool<{ id: string }, any>({
name: 'delete_requirement',
description: 'Delete a product requirement',
category: 'product-requirements',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Requirement ID to delete',
pattern: '^req-[a-f0-9-]+$'
}
},
required: ['id'],
additionalProperties: false
} as JSONSchema7,
async execute(input: { id: string }, context: RequestContext) {
try {
// Check if requirement exists
const existing = await context.db.get(
'SELECT * FROM product_requirements WHERE id = ? AND project_id = ?',
[input.id, context.projectId || 'default']
);
if (!existing.success || !existing.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: `Requirement ${input.id} not found`,
details: { requirementId: input.id },
category: 'validation'
});
}
// Check for child requirements
const childIds = JSON.parse(existing.data.child_requirement_ids || '[]');
if (childIds.length > 0) {
return createErrorResult({
code: 'VALIDATION_ERROR',
message: 'Cannot delete requirement with child requirements',
details: { childCount: childIds.length },
category: 'validation'
});
}
// Remove from parent's child list if applicable
if (existing.data.parent_requirement_id) {
const parent = await context.db.get(
'SELECT child_requirement_ids FROM product_requirements WHERE id = ?',
[existing.data.parent_requirement_id]
);
if (parent.success && parent.data) {
const parentChildIds = JSON.parse(parent.data.child_requirement_ids || '[]');
const updatedChildIds = parentChildIds.filter((id: string) => id !== input.id);
await context.db.run(
'UPDATE product_requirements SET child_requirement_ids = ?, updated_at = ? WHERE id = ?',
[JSON.stringify(updatedChildIds), Date.now(), existing.data.parent_requirement_id]
);
}
}
// Delete requirement (cascades to repositories, story links, and changes)
const result = await context.db.run(
'DELETE FROM product_requirements WHERE id = ? AND project_id = ?',
[input.id, context.projectId || 'default']
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to delete requirement',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
deletedRequirement: {
id: input.id,
name: existing.data.name
},
message: `Deleted requirement "${existing.data.name}"`
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to delete requirement: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Search product requirements by text query
*/
const searchRequirementsTool = createTool<SearchRequirementsInput, any>({
name: 'search_requirements',
description: 'Search product requirements by text query',
category: 'product-requirements',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
minLength: 2,
maxLength: 200
},
searchIn: {
type: 'array',
items: {
type: 'string',
enum: ['name', 'description', 'acceptance_criteria', 'tags']
},
description: 'Fields to search in (default: name, description)',
maxItems: 4
},
limit: {
type: 'number',
description: 'Maximum results to return',
minimum: 1,
maximum: 100,
default: 20
},
offset: {
type: 'number',
description: 'Offset for pagination',
minimum: 0,
default: 0
}
},
required: ['query'],
additionalProperties: false
} as JSONSchema7,
async execute(input: SearchRequirementsInput, context: RequestContext) {
try {
const searchFields = input.searchIn || ['name', 'description'];
const searchConditions: string[] = [];
const params: any[] = [];
// Build search conditions
if (searchFields.includes('name')) {
searchConditions.push('r.name LIKE ?');
params.push(`%${input.query}%`);
}
if (searchFields.includes('description')) {
searchConditions.push('r.description LIKE ?');
params.push(`%${input.query}%`);
}
if (searchFields.includes('acceptance_criteria')) {
searchConditions.push('r.acceptance_criteria LIKE ?');
params.push(`%${input.query}%`);
}
if (searchFields.includes('tags')) {
searchConditions.push('r.tags LIKE ?');
params.push(`%${input.query}%`);
}
const query = `
SELECT r.*,
(SELECT COUNT(*) FROM requirement_repositories WHERE requirement_id = r.id) as repo_count,
(SELECT COUNT(*) FROM requirement_story_links WHERE requirement_id = r.id) as story_count
FROM product_requirements r
WHERE r.project_id = ? AND (${searchConditions.join(' OR ')})
ORDER BY
CASE
WHEN r.name LIKE ? THEN 1
WHEN r.description LIKE ? THEN 2
ELSE 3
END,
r.updated_at DESC
LIMIT ? OFFSET ?
`;
const allParams = [
context.projectId || 'default',
...params,
`${input.query}%`, // For relevance sorting
`${input.query}%`,
input.limit || 20,
input.offset || 0
];
const results = await context.db.all(query, allParams);
if (!results.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to search requirements',
details: { error: results.error },
category: 'system'
});
}
if (results.data.length === 0) {
return createSuccessResult({
requirements: [],
totalFound: 0,
message: `No requirements found matching "${input.query}"`
});
}
const requirements = results.data.map((req: any) => ({
id: req.id,
name: req.name,
type: req.type,
description: req.description.substring(0, 200) + (req.description.length > 200 ? '...' : ''),
priority: req.priority,
status: req.status,
tags: JSON.parse(req.tags || '[]'),
repositoryCount: req.repo_count,
storyCount: req.story_count,
relevanceScore: req.name.toLowerCase().includes(input.query.toLowerCase()) ? 100 : 50
}));
return createSuccessResult({
requirements,
totalFound: requirements.length,
query: input.query,
searchedIn: searchFields,
message: `Found ${requirements.length} requirement(s) matching "${input.query}"`
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to search requirements: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Link a product requirement to a user story
*/
const linkRequirementToStoryTool = createTool<LinkRequirementToStoryInput, any>({
name: 'link_requirement_to_story',
description: 'Link a product requirement to a user story',
category: 'product-requirements',
inputSchema: {
type: 'object',
properties: {
requirementId: {
type: 'string',
description: 'Product requirement ID',
pattern: '^req-[a-f0-9-]+$'
},
storyId: {
type: 'string',
description: 'User story ID'
},
linkType: {
type: 'string',
enum: ['implements', 'relates_to', 'depends_on'],
description: 'Type of relationship',
default: 'implements'
},
notes: {
type: 'string',
description: 'Additional notes about the link',
maxLength: 1000
}
},
required: ['requirementId', 'storyId'],
additionalProperties: false
} as JSONSchema7,
async execute(input: LinkRequirementToStoryInput, context: RequestContext) {
try {
// Verify requirement exists
const reqCheck = await context.db.get(
'SELECT id, related_stories FROM product_requirements WHERE id = ? AND project_id = ?',
[input.requirementId, context.projectId || 'default']
);
if (!reqCheck.success || !reqCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Requirement not found',
details: { requirementId: input.requirementId },
category: 'validation'
});
}
// Verify story exists
const storyCheck = await context.db.get(
'SELECT id FROM agile_stories WHERE id = ? AND project_id = ?',
[input.storyId, context.projectId || 'default']
);
if (!storyCheck.success || !storyCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Story not found',
details: { storyId: input.storyId },
category: 'validation'
});
}
// Check if link already exists
const existingLink = await context.db.get(
'SELECT id FROM requirement_story_links WHERE requirement_id = ? AND story_id = ?',
[input.requirementId, input.storyId]
);
if (existingLink.success && existingLink.data) {
return createErrorResult({
code: 'DUPLICATE_RESOURCE',
message: 'Link already exists between this requirement and story',
category: 'validation'
});
}
const linkId = `link-${randomUUID()}`;
const now = Date.now();
// Create link
const result = await context.db.run(
`INSERT INTO requirement_story_links
(id, requirement_id, story_id, project_id, link_type, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
linkId,
input.requirementId,
input.storyId,
context.projectId || 'default',
input.linkType || 'implements',
input.notes || null,
now
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create link',
details: { error: result.error },
category: 'system'
});
}
// Update requirement's related_stories array
const relatedStories = JSON.parse(reqCheck.data.related_stories || '[]');
if (!relatedStories.includes(input.storyId)) {
relatedStories.push(input.storyId);
await context.db.run(
'UPDATE product_requirements SET related_stories = ?, updated_at = ? WHERE id = ?',
[JSON.stringify(relatedStories), now, input.requirementId]
);
}
return createSuccessResult({
link: {
id: linkId,
requirementId: input.requirementId,
storyId: input.storyId,
linkType: input.linkType || 'implements',
notes: input.notes
},
message: `Linked requirement ${input.requirementId} to story ${input.storyId}`,
nextSteps: [
'Update story to reflect requirement implementation',
'Track progress through story status updates'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to link requirement to story: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Update implementation status for a requirement in a specific repository
*/
const updateImplementationStatusTool = createTool<UpdateImplementationStatusInput, any>({
name: 'update_implementation_status',
description: 'Update implementation status for a requirement in a specific repository',
category: 'product-requirements',
inputSchema: {
type: 'object',
properties: {
requirementId: {
type: 'string',
description: 'Product requirement ID',
pattern: '^req-[a-f0-9-]+$'
},
repositoryName: {
type: 'string',
description: 'Repository name',
minLength: 1,
maxLength: 100
},
status: {
type: 'string',
enum: ['not_started', 'in_progress', 'completed', 'blocked'],
description: 'New implementation status'
},
branch: {
type: 'string',
description: 'Branch name where implementation is happening',
maxLength: 100
},
prUrl: {
type: 'string',
description: 'Pull request URL',
maxLength: 500
},
notes: {
type: 'string',
description: 'Implementation notes',
maxLength: 1000
}
},
required: ['requirementId', 'repositoryName', 'status'],
additionalProperties: false
} as JSONSchema7,
async execute(input: UpdateImplementationStatusInput, context: RequestContext) {
try {
// Verify requirement exists
const reqCheck = await context.db.get(
'SELECT id FROM product_requirements WHERE id = ? AND project_id = ?',
[input.requirementId, context.projectId || 'default']
);
if (!reqCheck.success || !reqCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Requirement not found',
details: { requirementId: input.requirementId },
category: 'validation'
});
}
// Check if repository exists for this requirement
const repoCheck = await context.db.get(
'SELECT id FROM requirement_repositories WHERE requirement_id = ? AND name = ?',
[input.requirementId, input.repositoryName]
);
const now = Date.now();
if (repoCheck.success && repoCheck.data) {
// Update existing repository
const result = await context.db.run(
`UPDATE requirement_repositories
SET implementation_status = ?, branch = ?, pr_url = ?, notes = ?, updated_at = ?
WHERE id = ?`,
[
input.status,
input.branch || null,
input.prUrl || null,
input.notes || null,
now,
repoCheck.data.id
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to update implementation status',
details: { error: result.error },
category: 'system'
});
}
} else {
// Create new repository entry
const repoId = `repo-${randomUUID()}`;
const result = await context.db.run(
`INSERT INTO requirement_repositories
(id, requirement_id, project_id, name, implementation_status, branch, pr_url, notes, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
repoId,
input.requirementId,
context.projectId || 'default',
input.repositoryName,
input.status,
input.branch || null,
input.prUrl || null,
input.notes || null,
now,
now
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create repository entry',
details: { error: result.error },
category: 'system'
});
}
}
// Update requirement's updated_at timestamp
await context.db.run(
'UPDATE product_requirements SET updated_at = ? WHERE id = ?',
[now, input.requirementId]
);
// Track the change
const changeId = `change-${randomUUID()}`;
await context.db.run(
`INSERT INTO requirement_changes
(id, requirement_id, project_id, field, old_value, new_value, changed_by, changed_at, reason)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
changeId,
input.requirementId,
context.projectId || 'default',
`implementation_status_${input.repositoryName}`,
repoCheck.data ? repoCheck.data.implementation_status : null,
input.status,
context.userId || 'system',
now,
input.notes || null
]
);
return createSuccessResult({
requirementId: input.requirementId,
repository: {
name: input.repositoryName,
status: input.status,
branch: input.branch,
prUrl: input.prUrl
},
message: `Updated implementation status for repository "${input.repositoryName}" to "${input.status}"`,
nextSteps: input.status === 'completed' ? [
'Verify all acceptance criteria are met',
'Update requirement