@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
1,371 lines (1,241 loc) • 41.9 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';
import { renderADRTemplate } from './templates.js';
import type { ADR, ADRStatus } from './types.js';
/**
* ADR Management 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 CreateADRInput {
title: string;
template?: 'nygard' | 'madr' | 'y-statement';
deciders: string[];
context: string;
decision: string;
consequences: string;
tags?: string[];
decisionDrivers?: string[];
consideredOptions?: Array<{
title: string;
description: string;
pros: string[];
cons: string[];
}>;
prosAndCons?: Array<{
option: string;
pros: string[];
cons: string[];
}>;
supersedes?: string[];
relatedTo?: string[];
}
interface UpdateADRInput {
id: string;
status?: 'proposed' | 'accepted' | 'rejected' | 'deprecated' | 'superseded';
statusChangeReason?: string;
statusChangedBy?: string;
title?: string;
consequences?: string;
tags?: string[];
supersedes?: string[];
supersededBy?: string;
relatedTo?: string[];
decisionDrivers?: string[];
consideredOptions?: Array<{
title: string;
description: string;
pros: string[];
cons: string[];
}>;
prosAndCons?: Array<{
option: string;
pros: string[];
cons: string[];
}>;
}
interface GetADRInput {
id: string;
}
interface ListADRsInput {
status?: 'proposed' | 'accepted' | 'rejected' | 'deprecated' | 'superseded';
}
interface SearchADRsInput {
query?: string;
status?: string[];
tags?: string[];
template?: 'nygard' | 'madr' | 'y-statement';
dateRange?: {
from: string;
to: string;
};
deciders?: string[];
}
interface LinkADRsInput {
fromId: string;
toId: string;
relationship?: 'related-to' | 'supersedes';
}
interface DeleteADRInput {
id: string;
}
/**
* Generate next ADR number
*/
async function getNextADRNumber(context: RequestContext): Promise<string> {
const result = await context.db.get(
'SELECT MAX(CAST(SUBSTR(id, 5) AS INTEGER)) as max_num FROM adr_records WHERE project_id = ?',
[context.projectId || 'default']
);
const nextNum = (result.data?.max_num || 0) + 1;
return nextNum.toString().padStart(4, '0');
}
/**
* Create a new Architecture Decision Record
*/
const createADRTool = createTool<CreateADRInput, any>({
name: 'create_adr',
description: 'Create a new Architecture Decision Record',
category: 'adr',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Title of the decision',
minLength: 1,
maxLength: 200
},
template: {
type: 'string',
enum: ['nygard', 'madr', 'y-statement'],
default: 'nygard',
description: 'ADR template to use'
},
deciders: {
type: 'array',
items: { type: 'string', minLength: 1, maxLength: 100 },
description: 'People involved in the decision',
minItems: 1,
maxItems: 20
},
context: {
type: 'string',
description: 'Context and problem statement',
minLength: 1,
maxLength: 5000
},
decision: {
type: 'string',
description: 'The decision that was made',
minLength: 1,
maxLength: 5000
},
consequences: {
type: 'string',
description: 'Positive and negative consequences',
minLength: 1,
maxLength: 5000
},
tags: {
type: 'array',
items: { type: 'string', maxLength: 50 },
description: 'Optional tags for categorization',
maxItems: 20
},
decisionDrivers: {
type: 'array',
items: { type: 'string' },
description: 'Key factors that influenced the decision (MADR template)',
maxItems: 20
},
consideredOptions: {
type: 'array',
items: {
type: 'object',
properties: {
title: { type: 'string', maxLength: 200 },
description: { type: 'string', maxLength: 1000 },
pros: { type: 'array', items: { type: 'string' } },
cons: { type: 'array', items: { type: 'string' } }
},
required: ['title', 'description', 'pros', 'cons']
},
description: 'Alternative options that were considered (MADR template)',
maxItems: 10
},
supersedes: {
type: 'array',
items: { type: 'string', pattern: '^ADR-\\d{4}$' },
description: 'ADR IDs that this decision supersedes',
maxItems: 10
},
relatedTo: {
type: 'array',
items: { type: 'string', pattern: '^ADR-\\d{4}$' },
description: 'ADR IDs that this decision relates to',
maxItems: 20
}
},
required: ['title', 'deciders', 'context', 'decision', 'consequences'],
additionalProperties: false
} as JSONSchema7,
async execute(input: CreateADRInput, context: RequestContext) {
try {
const adrNumber = await getNextADRNumber(context);
const adrId = `ADR-${adrNumber}`;
const now = Date.now();
// Validate superseded ADRs exist
if (input.supersedes) {
for (const supersededId of input.supersedes) {
const existsResult = await context.db.get(
'SELECT id FROM adr_records WHERE id = ? AND project_id = ?',
[supersededId, context.projectId || 'default']
);
if (!existsResult.success || !existsResult.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: `Referenced ADR ${supersededId} not found`,
category: 'validation'
});
}
}
}
// Validate related ADRs exist
if (input.relatedTo) {
for (const relatedId of input.relatedTo) {
const existsResult = await context.db.get(
'SELECT id FROM adr_records WHERE id = ? AND project_id = ?',
[relatedId, context.projectId || 'default']
);
if (!existsResult.success || !existsResult.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: `Referenced ADR ${relatedId} not found`,
category: 'validation'
});
}
}
}
// Create ADR record in database
const result = await context.db.run(
`INSERT INTO adr_records
(id, title, status, date, deciders, template, context, decision, consequences,
tags, decision_drivers, considered_options, pros_and_cons, supersedes,
superseded_by, related_to, project_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
adrId,
input.title,
'proposed',
now,
JSON.stringify(input.deciders),
input.template || 'nygard',
input.context,
input.decision,
input.consequences,
JSON.stringify(input.tags || []),
JSON.stringify(input.decisionDrivers || []),
JSON.stringify(input.consideredOptions || []),
JSON.stringify(input.prosAndCons || []),
JSON.stringify(input.supersedes || []),
null,
JSON.stringify(input.relatedTo || []),
context.projectId || 'default',
now,
now
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create ADR',
details: { error: result.error },
category: 'system'
});
}
// Update superseded ADRs
if (input.supersedes) {
for (const supersededId of input.supersedes) {
await context.db.run(
'UPDATE adr_records SET status = ?, superseded_by = ?, updated_at = ? WHERE id = ? AND project_id = ?',
['superseded', adrId, now, supersededId, context.projectId || 'default']
);
}
}
// Create status history entry
await context.db.run(
`INSERT INTO adr_status_history
(id, adr_id, from_status, to_status, changed_at, changed_by, reason, project_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[randomUUID(), adrId, null, 'proposed', now, context.userId || 'system', 'Initial creation', context.projectId || 'default']
);
// Build ADR object for template rendering
const adr: ADR = {
id: adrId,
title: input.title,
status: 'proposed' as ADRStatus,
date: new Date(now),
deciders: input.deciders,
template: input.template || 'nygard',
context: input.context,
decision: input.decision,
consequences: input.consequences,
tags: input.tags,
decisionDrivers: input.decisionDrivers,
consideredOptions: input.consideredOptions,
prosAndCons: input.prosAndCons,
supersedes: input.supersedes,
relatedTo: input.relatedTo,
createdAt: new Date(now),
updatedAt: new Date(now),
statusHistory: []
};
const markdown = renderADRTemplate(adr);
return createSuccessResult({
adr: {
id: adrId,
title: input.title,
status: 'proposed',
template: input.template || 'nygard',
deciders: input.deciders,
createdAt: new Date(now).toISOString()
},
markdown,
message: `ADR ${adrId} "${input.title}" created successfully`,
nextSteps: [
'Review the decision with stakeholders',
'Update status when decision is finalized',
'Link to related ADRs if needed'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to create ADR: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Update an existing ADR
*/
const updateADRTool = createTool<UpdateADRInput, any>({
name: 'update_adr',
description: 'Update an existing Architecture Decision Record',
category: 'adr',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'ADR ID (e.g., ADR-0001)',
pattern: '^ADR-\\d{4}$'
},
status: {
type: 'string',
enum: ['proposed', 'accepted', 'rejected', 'deprecated', 'superseded'],
description: 'New status'
},
statusChangeReason: {
type: 'string',
description: 'Reason for status change',
maxLength: 1000
},
statusChangedBy: {
type: 'string',
description: 'Person making the status change',
maxLength: 100
},
title: {
type: 'string',
description: 'Updated title',
maxLength: 200
},
consequences: {
type: 'string',
description: 'Updated consequences',
maxLength: 5000
},
tags: {
type: 'array',
items: { type: 'string', maxLength: 50 },
description: 'Updated tags',
maxItems: 20
}
},
required: ['id'],
additionalProperties: false
} as JSONSchema7,
async execute(input: UpdateADRInput, context: RequestContext) {
try {
// Get existing ADR
const adrResult = await context.db.get(
'SELECT * FROM adr_records WHERE id = ? AND project_id = ?',
[input.id, context.projectId || 'default']
);
if (!adrResult.success || !adrResult.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: `ADR ${input.id} not found`,
category: 'validation'
});
}
const existingADR = adrResult.data;
const now = Date.now();
const updates: string[] = [];
const values: any[] = [];
// Build update query dynamically
if (input.title !== undefined) {
updates.push('title = ?');
values.push(input.title);
}
if (input.consequences !== undefined) {
updates.push('consequences = ?');
values.push(input.consequences);
}
if (input.tags !== undefined) {
updates.push('tags = ?');
values.push(JSON.stringify(input.tags));
}
if (input.status !== undefined && input.status !== existingADR.status) {
updates.push('status = ?');
values.push(input.status);
// Record status change
await context.db.run(
`INSERT INTO adr_status_history
(id, adr_id, from_status, to_status, changed_at, changed_by, reason, project_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
randomUUID(),
input.id,
existingADR.status,
input.status,
now,
input.statusChangedBy || context.userId || 'unknown',
input.statusChangeReason || null,
context.projectId || 'default'
]
);
}
if (updates.length > 0) {
updates.push('updated_at = ?');
values.push(now);
values.push(input.id);
values.push(context.projectId || 'default');
const updateResult = await context.db.run(
`UPDATE adr_records SET ${updates.join(', ')} WHERE id = ? AND project_id = ?`,
values
);
if (!updateResult.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to update ADR',
details: { error: updateResult.error },
category: 'system'
});
}
}
// Get updated ADR with status history
const updatedResult = await context.db.get(
'SELECT * FROM adr_records WHERE id = ? AND project_id = ?',
[input.id, context.projectId || 'default']
);
const historyResult = await context.db.query(
'SELECT * FROM adr_status_history WHERE adr_id = ? AND project_id = ? ORDER BY changed_at',
[input.id, context.projectId || 'default']
);
const statusHistory = (historyResult.data || []).map((h: any) => ({
from: h.from_status,
to: h.to_status,
date: new Date(h.changed_at).toISOString(),
changedBy: h.changed_by,
reason: h.reason
}));
return createSuccessResult({
adr: {
id: input.id,
title: updatedResult.data?.title,
status: updatedResult.data?.status,
updatedAt: new Date(updatedResult.data?.updated_at || now).toISOString()
},
statusHistory,
message: `ADR ${input.id} updated successfully`,
changesApplied: updates.length > 0 ? updates.map(u => u.split(' = ')[0]) : ['No changes']
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to update ADR: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Get a specific ADR
*/
const getADRTool = createTool<GetADRInput, any>({
name: 'get_adr',
description: 'Get details of a specific Architecture Decision Record',
category: 'adr',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'ADR ID (e.g., ADR-0001)',
pattern: '^ADR-\\d{4}$'
}
},
required: ['id'],
additionalProperties: false
} as JSONSchema7,
async execute(input: GetADRInput, context: RequestContext) {
try {
// Get ADR details
const adrResult = await context.db.get(
'SELECT * FROM adr_records WHERE id = ? AND project_id = ?',
[input.id, context.projectId || 'default']
);
if (!adrResult.success || !adrResult.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: `ADR ${input.id} not found`,
category: 'validation'
});
}
const adr = adrResult.data;
// Get status history
const historyResult = await context.db.query(
'SELECT * FROM adr_status_history WHERE adr_id = ? AND project_id = ? ORDER BY changed_at',
[input.id, context.projectId || 'default']
);
// Build full ADR object for template rendering
const adrObject = {
id: adr.id,
title: adr.title,
status: adr.status,
date: new Date(adr.date),
deciders: JSON.parse(adr.deciders || '[]'),
template: adr.template,
context: adr.context,
decision: adr.decision,
consequences: adr.consequences,
tags: JSON.parse(adr.tags || '[]'),
decisionDrivers: JSON.parse(adr.decision_drivers || '[]'),
consideredOptions: JSON.parse(adr.considered_options || '[]'),
prosAndCons: JSON.parse(adr.pros_and_cons || '[]'),
supersedes: JSON.parse(adr.supersedes || '[]'),
supersededBy: adr.superseded_by,
relatedTo: JSON.parse(adr.related_to || '[]'),
createdAt: new Date(adr.created_at),
updatedAt: new Date(adr.updated_at),
statusHistory: (historyResult.data || []).map((h: any) => ({
from: h.from_status,
to: h.to_status,
date: new Date(h.changed_at),
changedBy: h.changed_by,
reason: h.reason
}))
};
const markdown = renderADRTemplate(adrObject);
// Build relationships
const relationships = [];
if (adrObject.relatedTo) {
for (const relatedId of adrObject.relatedTo) {
relationships.push({ type: 'related-to', from: input.id, to: relatedId });
}
}
if (adrObject.supersedes) {
for (const supersededId of adrObject.supersedes) {
relationships.push({ type: 'supersedes', from: input.id, to: supersededId });
}
}
if (adrObject.supersededBy) {
relationships.push({ type: 'superseded-by', from: input.id, to: adrObject.supersededBy });
}
return createSuccessResult({
adr: adrObject,
markdown,
relationships,
statusHistory: adrObject.statusHistory
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to get ADR: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* List all ADRs with optional filtering
*/
const listADRsTool = createTool<ListADRsInput, any>({
name: 'list_adrs',
description: 'List all Architecture Decision Records with optional status filter',
category: 'adr',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['proposed', 'accepted', 'rejected', 'deprecated', 'superseded'],
description: 'Filter by status'
}
},
required: [],
additionalProperties: false
} as JSONSchema7,
async execute(input: ListADRsInput, context: RequestContext) {
try {
let query = 'SELECT id, title, status, date, deciders, tags, created_at FROM adr_records WHERE project_id = ?';
const params = [context.projectId || 'default'];
if (input.status) {
query += ' AND status = ?';
params.push(input.status);
}
query += ' ORDER BY date DESC';
const result = await context.db.query(query, params);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to list ADRs',
details: { error: result.error },
category: 'system'
});
}
const adrs = (result.data || []).map((adr: any) => ({
id: adr.id,
title: adr.title,
status: adr.status,
date: new Date(adr.date).toISOString(),
deciders: JSON.parse(adr.deciders || '[]'),
tags: JSON.parse(adr.tags || '[]'),
createdAt: new Date(adr.created_at).toISOString()
}));
return createSuccessResult({
adrs,
count: adrs.length,
filter: input.status ? { status: input.status } : null
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to list ADRs: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Delete an ADR
*/
const deleteADRTool = createTool<DeleteADRInput, any>({
name: 'delete_adr',
description: 'Delete an Architecture Decision Record',
category: 'adr',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'ADR ID to delete',
pattern: '^ADR-\\d{4}$'
}
},
required: ['id'],
additionalProperties: false
} as JSONSchema7,
async execute(input: DeleteADRInput, context: RequestContext) {
try {
// Check if ADR exists
const existsResult = await context.db.get(
'SELECT id, title FROM adr_records WHERE id = ? AND project_id = ?',
[input.id, context.projectId || 'default']
);
if (!existsResult.success || !existsResult.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: `ADR ${input.id} not found`,
category: 'validation'
});
}
const adr = existsResult.data;
// Delete in transaction to ensure consistency
await context.db.transaction(async (tx) => {
// Delete status history
await tx.run(
'DELETE FROM adr_status_history WHERE adr_id = ? AND project_id = ?',
[input.id, context.projectId || 'default']
);
// Delete the ADR
await tx.run(
'DELETE FROM adr_records WHERE id = ? AND project_id = ?',
[input.id, context.projectId || 'default']
);
// Update any ADRs that reference this one
await tx.run(
'UPDATE adr_records SET superseded_by = NULL WHERE superseded_by = ? AND project_id = ?',
[input.id, context.projectId || 'default']
);
});
return createSuccessResult({
message: `ADR ${input.id} "${adr.title}" deleted successfully`,
deletedADR: {
id: input.id,
title: adr.title
}
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to delete ADR: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Get ADR metrics and statistics
*/
const getADRMetricsTool = createTool<{}, any>({
name: 'adr_metrics',
description: 'Get metrics and statistics about Architecture Decision Records',
category: 'adr',
readOnly: true,
inputSchema: {
type: 'object',
properties: {},
required: [],
additionalProperties: false
} as JSONSchema7,
async execute(input: {}, context: RequestContext) {
try {
// Get total count and status breakdown
const statusResult = await context.db.query(
'SELECT status, COUNT(*) as count FROM adr_records WHERE project_id = ? GROUP BY status',
[context.projectId || 'default']
);
// Get template breakdown
const templateResult = await context.db.query(
'SELECT template, COUNT(*) as count FROM adr_records WHERE project_id = ? GROUP BY template',
[context.projectId || 'default']
);
// Get total count
const totalResult = await context.db.get(
'SELECT COUNT(*) as total FROM adr_records WHERE project_id = ?',
[context.projectId || 'default']
);
// Get most active deciders
const decidersResult = await context.db.query(
'SELECT deciders FROM adr_records WHERE project_id = ?',
[context.projectId || 'default']
);
// Process deciders data
const deciderCounts = new Map();
(decidersResult.data || []).forEach((row: any) => {
const deciders = JSON.parse(row.deciders || '[]');
deciders.forEach((decider: string) => {
deciderCounts.set(decider, (deciderCounts.get(decider) || 0) + 1);
});
});
const mostActiveDeciders = Array.from(deciderCounts.entries())
.map(([name, count]) => ({ name, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
// Calculate average decision time for accepted ADRs
const decisionTimeResult = await context.db.query(
`SELECT adr.created_at, h.changed_at
FROM adr_records adr
JOIN adr_status_history h ON adr.id = h.adr_id
WHERE adr.project_id = ? AND h.to_status = 'accepted'`,
[context.projectId || 'default']
);
let averageDecisionTime = 0;
if (decisionTimeResult.data && decisionTimeResult.data.length > 0) {
const decisionTimes = decisionTimeResult.data.map((row: any) => {
return (row.changed_at - row.created_at) / (1000 * 60 * 60 * 24); // Convert to days
});
averageDecisionTime = decisionTimes.reduce((a: number, b: number) => a + b, 0) / decisionTimes.length;
}
// Build status breakdown
const statusBreakdown: Record<string, number> = {
proposed: 0,
accepted: 0,
rejected: 0,
deprecated: 0,
superseded: 0
};
(statusResult.data || []).forEach((row: any) => {
statusBreakdown[row.status] = row.count;
});
// Build template breakdown
const templateBreakdown: Record<string, number> = {
nygard: 0,
madr: 0,
'y-statement': 0
};
(templateResult.data || []).forEach((row: any) => {
templateBreakdown[row.template] = row.count;
});
return createSuccessResult({
metrics: {
total: totalResult.data?.total || 0,
byStatus: statusBreakdown,
byTemplate: templateBreakdown,
averageDecisionTime: Math.round(averageDecisionTime * 100) / 100,
mostActiveDeciders
},
insights: [
`Total ${totalResult.data?.total || 0} ADRs in the system`,
statusBreakdown.accepted > 0 ? `${statusBreakdown.accepted} decisions have been accepted` : 'No accepted decisions yet',
averageDecisionTime > 0 ? `Average decision time: ${Math.round(averageDecisionTime * 100) / 100} days` : 'No decision time data available',
mostActiveDeciders.length > 0 ? `Most active decider: ${mostActiveDeciders[0].name} (${mostActiveDeciders[0].count} decisions)` : 'No decision makers identified'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to get ADR metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Create ADR interactively (placeholder - uses wizard)
*/
const createADRInteractiveTool = createTool<{}, any>({
name: 'create_adr_interactive',
description: 'Start interactive ADR creation process',
category: 'adr',
inputSchema: {
type: 'object',
properties: {},
required: [],
additionalProperties: false
} as JSONSchema7,
async execute(input: {}, context: RequestContext) {
return createSuccessResult({
message: 'Interactive ADR creation started',
instructions: [
'Use create_adr tool with required fields:',
'- title: Brief title of the decision',
'- deciders: Array of decision makers',
'- context: Situation context',
'- decision: The decision made',
'- consequences: Expected outcomes'
],
nextStep: 'call_create_adr'
});
}
});
/**
* Link ADRs with relationships
*/
const linkADRsTool = createTool<{ sourceId: string; targetId: string; relationship: string }, any>({
name: 'link_adrs',
description: 'Create relationships between ADRs',
category: 'adr',
inputSchema: {
type: 'object',
properties: {
sourceId: {
type: 'string',
description: 'Source ADR ID',
pattern: '^[a-zA-Z0-9-]+$'
},
targetId: {
type: 'string',
description: 'Target ADR ID',
pattern: '^[a-zA-Z0-9-]+$'
},
relationship: {
type: 'string',
enum: ['supersedes', 'relates_to', 'amends'],
description: 'Type of relationship'
}
},
required: ['sourceId', 'targetId', 'relationship'],
additionalProperties: false
} as JSONSchema7,
async execute(input: { sourceId: string; targetId: string; relationship: string }, context: RequestContext) {
try {
// Verify both ADRs exist
const sourceResult = await context.db.get(
'SELECT * FROM adr_records WHERE id = ? AND project_id = ?',
[input.sourceId, context.projectId || 'default']
);
const targetResult = await context.db.get(
'SELECT * FROM adr_records WHERE id = ? AND project_id = ?',
[input.targetId, context.projectId || 'default']
);
if (!sourceResult.success || !sourceResult.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Source ADR not found',
details: { sourceId: input.sourceId },
category: 'validation'
});
}
if (!targetResult.success || !targetResult.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Target ADR not found',
details: { targetId: input.targetId },
category: 'validation'
});
}
// Update relationships based on type
const now = new Date().toISOString();
if (input.relationship === 'supersedes') {
// Update source ADR to include supersedes
const sourceSupersedes = JSON.parse(sourceResult.data.supersedes || '[]');
if (!sourceSupersedes.includes(input.targetId)) {
sourceSupersedes.push(input.targetId);
await context.db.run(
'UPDATE adr_records SET supersedes = ?, updated_at = ? WHERE id = ?',
[JSON.stringify(sourceSupersedes), now, input.sourceId]
);
}
// Update target ADR to be superseded by source
await context.db.run(
'UPDATE adr_records SET superseded_by = ?, status = ?, updated_at = ? WHERE id = ?',
[input.sourceId, 'superseded', now, input.targetId]
);
} else {
// For other relationships, update related_to field
const sourceRelated = JSON.parse(sourceResult.data.related_to || '[]');
if (!sourceRelated.includes(input.targetId)) {
sourceRelated.push(input.targetId);
await context.db.run(
'UPDATE adr_records SET related_to = ?, updated_at = ? WHERE id = ?',
[JSON.stringify(sourceRelated), now, input.sourceId]
);
}
}
return createSuccessResult({
linked: true,
relationship: input.relationship,
source: sourceResult.data,
target: targetResult.data
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to link ADRs: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Search ADRs
*/
const searchADRsTool = createTool<{ query?: string; startDate?: string; endDate?: string; status?: string }, any>({
name: 'search_adrs',
description: 'Search ADRs by content, date range, or status',
category: 'adr',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query for title, context, or decision content',
maxLength: 500
},
startDate: {
type: 'string',
format: 'date',
description: 'Start date for search range'
},
endDate: {
type: 'string',
format: 'date',
description: 'End date for search range'
},
status: {
type: 'string',
enum: ['proposed', 'accepted', 'rejected', 'deprecated', 'superseded'],
description: 'Filter by ADR status'
}
},
additionalProperties: false
} as JSONSchema7,
async execute(input: { query?: string; startDate?: string; endDate?: string; status?: string }, context: RequestContext) {
try {
let sql = 'SELECT * FROM adr_records WHERE project_id = ?';
const params: any[] = [context.projectId || 'default'];
// Add text search
if (input.query) {
sql += ' AND (title LIKE ? OR context LIKE ? OR decision LIKE ?)';
const searchTerm = `%${input.query}%`;
params.push(searchTerm, searchTerm, searchTerm);
}
// Add date range
if (input.startDate) {
sql += ' AND created_at >= ?';
params.push(input.startDate);
}
if (input.endDate) {
sql += ' AND created_at <= ?';
params.push(input.endDate + 'T23:59:59Z');
}
// Add status filter
if (input.status) {
sql += ' AND status = ?';
params.push(input.status);
}
sql += ' ORDER BY created_at DESC';
const result = await context.db.query(sql, params);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to search ADRs',
details: { error: result.error },
category: 'system'
});
}
const adrs = (result.data || []).map((row: any) => ({
id: row.id,
number: row.number,
title: row.title,
status: row.status,
deciders: JSON.parse(row.deciders || '[]'),
template: row.template,
context: row.context,
decision: row.decision,
consequences: row.consequences,
tags: JSON.parse(row.tags || '[]'),
createdAt: new Date(row.created_at).toISOString(),
updatedAt: new Date(row.updated_at).toISOString()
}));
return createSuccessResult({
adrs,
count: adrs.length,
searchCriteria: {
query: input.query || null,
startDate: input.startDate || null,
endDate: input.endDate || null,
status: input.status || null
}
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to search ADRs: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Validate ADR references
*/
const validateADRReferencesTool = createTool<{}, any>({
name: 'validate_adr_references',
description: 'Validate all ADR cross-references and relationships',
category: 'adr',
readOnly: true,
inputSchema: {
type: 'object',
properties: {},
required: [],
additionalProperties: false
} as JSONSchema7,
async execute(input: {}, context: RequestContext) {
try {
// Get all ADRs
const result = await context.db.query(
'SELECT * FROM adr_records WHERE project_id = ?',
[context.projectId || 'default']
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to fetch ADRs for validation',
category: 'system'
});
}
const adrs = result.data || [];
const adrIds = new Set(adrs.map((adr: any) => adr.id));
const brokenReferences: string[] = [];
const validReferences: string[] = [];
// Check all references
adrs.forEach((adr: any) => {
const supersedes = JSON.parse(adr.supersedes || '[]');
const relatedTo = JSON.parse(adr.related_to || '[]');
// Check supersedes references
supersedes.forEach((refId: string) => {
if (!adrIds.has(refId)) {
brokenReferences.push(`ADR ${adr.number} (${adr.id}) supersedes non-existent ADR ${refId}`);
} else {
validReferences.push(`ADR ${adr.number} supersedes reference is valid`);
}
});
// Check related_to references
relatedTo.forEach((refId: string) => {
if (!adrIds.has(refId)) {
brokenReferences.push(`ADR ${adr.number} (${adr.id}) relates to non-existent ADR ${refId}`);
} else {
validReferences.push(`ADR ${adr.number} related_to reference is valid`);
}
});
// Check superseded_by references
if (adr.superseded_by && !adrIds.has(adr.superseded_by)) {
brokenReferences.push(`ADR ${adr.number} (${adr.id}) superseded by non-existent ADR ${adr.superseded_by}`);
} else if (adr.superseded_by) {
validReferences.push(`ADR ${adr.number} superseded_by reference is valid`);
}
});
const isValid = brokenReferences.length === 0;
return createSuccessResult({
valid: isValid,
totalReferences: validReferences.length + brokenReferences.length,
validReferences: validReferences.length,
brokenReferences: brokenReferences.length,
issues: brokenReferences,
summary: isValid
? 'All ADR references are valid'
: `Found ${brokenReferences.length} broken reference(s)`
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to validate ADR references: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Generate ADR decision log
*/
const generateADRLogTool = createTool<{ format?: 'markdown' | 'json' | 'csv' }, any>({
name: 'generate_adr_log',
description: 'Generate a comprehensive log of all architectural decisions',
category: 'adr',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
format: {
type: 'string',
enum: ['markdown', 'json', 'csv'],
default: 'markdown',
description: 'Output format for the decision log'
}
},
additionalProperties: false
} as JSONSchema7,
async execute(input: { format?: 'markdown' | 'json' | 'csv' }, context: RequestContext) {
try {
const result = await context.db.query(
'SELECT * FROM adr_records WHERE project_id = ? ORDER BY number ASC',
[context.projectId || 'default']
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to fetch ADRs for log generation',
category: 'system'
});
}
const adrs = result.data || [];
const format = input.format || 'markdown';
if (format === 'json') {
return createSuccessResult({
format: 'json',
log: adrs.map((adr: any) => ({
number: adr.number,
title: adr.title,
status: adr.status,
deciders: JSON.parse(adr.deciders || '[]'),
template: adr.template,
context: adr.context,
decision: adr.decision,
consequences: adr.consequences,
tags: JSON.parse(adr.tags || '[]'),
supersedes: JSON.parse(adr.supersedes || '[]'),
supersededBy: adr.superseded_by,
relatedTo: JSON.parse(adr.related_to || '[]'),
createdAt: adr.created_at,
updatedAt: adr.updated_at
}))
});
}
if (format === 'csv') {
const headers = 'Number,Title,Status,Deciders,Template,Created,Updated';
const rows = adrs.map((adr: any) => {
const deciders = JSON.parse(adr.deciders || '[]').join(';');
return `${adr.number},"${adr.title}",${adr.status},"${deciders}",${adr.template},${adr.created_at},${adr.updated_at}`;
});
return createSuccessResult({
format: 'csv',
log: [headers, ...rows].join('\n')
});
}
// Default markdown format
const markdownLog = [
'# Architecture Decision Log',
'',
`This log lists the architectural decisions for the project.`,
'',
'<!-- adrlog -->',
'',
...adrs.map((adr: any) => {
const deciders = JSON.parse(adr.deciders || '[]').join(', ');
return `* [ADR-${adr.number}](adr-${adr.number}.md) - ${adr.title} - **${adr.status}** (${deciders})`;
}),
'',
'<!-- adrlogstop -->',
'',
`_Generated on ${new Date().toISOString()}_`
].join('\n');
return createSuccessResult({
format: 'markdown',
log: markdownLog,
count: adrs.length
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to generate ADR log: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Setup ADR tools
*/
export async function setupADRTools(): Promise<ToolRegistration> {
return {
module: 'adr-management',
tools: [
createADRTool,
createADRInteractiveTool,
updateADRTool,
linkADRsTool,
getADRTool,
listADRsTool,
searchADRsTool,
deleteADRTool,
getADRMetricsTool,
validateADRReferencesTool,
generateADRLogTool
]
};
}