@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
966 lines (875 loc) • 28.7 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 { CommonSchemas } from '../../core/validation.js';
/**
* Documentation 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 GenerateReadmeInput {
projectName: string;
description: string;
features?: string[];
techStack?: string[];
installInstructions?: string;
usageExamples?: string;
repository?: string;
}
interface GenerateClaudeConfigInput {
tddMode?: 'strict' | 'moderate' | 'off';
testCommand?: string;
lintCommand?: string;
buildCommand?: string;
customInstructions?: string[];
repository?: string;
}
interface CreateDocumentationInput {
type: 'api' | 'architecture' | 'contributing' | 'changelog' | 'custom';
title: string;
content: string;
repository?: string;
}
interface ListDocumentsInput {
projectId?: string;
type?: string;
status?: 'draft' | 'published' | 'archived';
limit?: number;
offset?: number;
}
interface UpdateDocumentInput {
documentId: string;
title?: string;
content?: string;
status?: 'draft' | 'published' | 'archived';
tags?: string[];
}
interface SearchDocumentsInput {
query: string;
projectId?: string;
type?: string;
limit?: number;
}
/**
* Generate README.md document
*/
const generateReadmeTool = createTool<GenerateReadmeInput, any>({
name: 'generate_readme',
description: 'Generate a professional README.md for your project or specific repository',
category: 'documentation',
inputSchema: {
type: 'object',
properties: {
projectName: {
type: 'string',
description: 'Name of the project',
minLength: 1,
maxLength: 200
},
description: {
type: 'string',
description: 'Project description',
minLength: 1,
maxLength: 2000
},
features: {
type: 'array',
items: { type: 'string', maxLength: 500 },
description: 'List of key features',
maxItems: 20
},
techStack: {
type: 'array',
items: { type: 'string', maxLength: 100 },
description: 'Technologies used',
maxItems: 20
},
installInstructions: {
type: 'string',
description: 'How to install the project',
maxLength: 2000
},
usageExamples: {
type: 'string',
description: 'Usage examples',
maxLength: 2000
},
repository: {
type: 'string',
description: 'Target repository name (for multi-repo workspaces)',
maxLength: 200
}
},
required: ['projectName', 'description'],
additionalProperties: false
} as JSONSchema7,
async execute(input: GenerateReadmeInput, context: RequestContext) {
try {
const docId = randomUUID();
const now = Date.now();
const projectId = context.projectId || 'default';
// Generate README content
const readmeContent = generateReadmeContent({
projectName: input.projectName,
description: input.description,
features: input.features || [],
techStack: input.techStack || [],
installInstructions: input.installInstructions,
usageExamples: input.usageExamples,
});
// Check if README already exists
const existingReadme = await context.db.get(
'SELECT id FROM documents WHERE project_id = ? AND type = ? AND title = ?',
[projectId, 'readme', 'README.md']
);
if (existingReadme.success && existingReadme.data) {
// Update existing README
const updateResult = await context.db.run(
`UPDATE documents
SET content = ?, version = version + 1, updated_at = ?
WHERE id = ?`,
[readmeContent, now, existingReadme.data.id]
);
if (!updateResult.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to update README',
details: { error: updateResult.error },
category: 'system'
});
}
return createSuccessResult({
document: {
id: existingReadme.data.id,
title: 'README.md',
type: 'readme',
updated: true
},
message: `README.md updated for project "${input.projectName}"`,
preview: readmeContent.substring(0, 200) + '...',
sections: ['overview', 'features', 'tech-stack', 'installation', 'usage']
});
} else {
// Create new README
const result = await context.db.run(
`INSERT INTO documents
(id, project_id, title, content, type, path, tags, status,
author, version, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
docId,
projectId,
'README.md',
readmeContent,
'readme',
input.repository ? `${input.repository}/README.md` : 'README.md',
JSON.stringify(['readme', 'documentation']),
'published',
context.userId || 'system',
1,
now,
now
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create README',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
document: {
id: docId,
title: 'README.md',
type: 'readme',
created: true
},
message: `README.md created for project "${input.projectName}"`,
preview: readmeContent.substring(0, 200) + '...',
sections: ['overview', 'features', 'tech-stack', 'installation', 'usage']
});
}
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to generate README: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Generate CLAUDE.md configuration
*/
const generateClaudeConfigTool = createTool<GenerateClaudeConfigInput, any>({
name: 'generate_claude_config',
description: 'Generate CLAUDE.md configuration for Claude Code in a specific repository',
category: 'documentation',
inputSchema: {
type: 'object',
properties: {
tddMode: {
type: 'string',
enum: ['strict', 'moderate', 'off'],
default: 'strict',
description: 'TDD enforcement level'
},
testCommand: {
type: 'string',
default: 'npm test',
description: 'Command to run tests',
maxLength: 200
},
lintCommand: {
type: 'string',
default: 'npm run lint',
description: 'Command to run linter',
maxLength: 200
},
buildCommand: {
type: 'string',
default: 'npm run build',
description: 'Command to build project',
maxLength: 200
},
customInstructions: {
type: 'array',
items: { type: 'string', maxLength: 500 },
description: 'Custom instructions for Claude',
maxItems: 10
},
repository: {
type: 'string',
description: 'Target repository name (for multi-repo workspaces)',
maxLength: 200
}
},
additionalProperties: false
} as JSONSchema7,
async execute(input: GenerateClaudeConfigInput, context: RequestContext) {
try {
const docId = randomUUID();
const now = Date.now();
const projectId = context.projectId || 'default';
const claudeContent = generateClaudeConfig({
tddMode: input.tddMode || 'strict',
testCommand: input.testCommand || 'npm test',
lintCommand: input.lintCommand || 'npm run lint',
buildCommand: input.buildCommand || 'npm run build',
customInstructions: input.customInstructions || [],
});
// Check if CLAUDE.md already exists
const existingConfig = await context.db.get(
'SELECT id FROM documents WHERE project_id = ? AND type = ? AND title = ?',
[projectId, 'claude-config', 'CLAUDE.md']
);
if (existingConfig.success && existingConfig.data) {
// Update existing CLAUDE.md
const updateResult = await context.db.run(
`UPDATE documents
SET content = ?, version = version + 1, updated_at = ?
WHERE id = ?`,
[claudeContent, now, existingConfig.data.id]
);
if (!updateResult.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to update CLAUDE.md',
details: { error: updateResult.error },
category: 'system'
});
}
return createSuccessResult({
document: {
id: existingConfig.data.id,
title: 'CLAUDE.md',
type: 'claude-config',
updated: true
},
message: 'CLAUDE.md configuration updated',
config: {
tddMode: input.tddMode || 'strict',
testCommand: input.testCommand || 'npm test',
lintCommand: input.lintCommand || 'npm run lint',
buildCommand: input.buildCommand || 'npm run build',
customInstructions: input.customInstructions || []
}
});
} else {
// Create new CLAUDE.md
const result = await context.db.run(
`INSERT INTO documents
(id, project_id, title, content, type, path, tags, status,
author, version, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
docId,
projectId,
'CLAUDE.md',
claudeContent,
'claude-config',
input.repository ? `${input.repository}/CLAUDE.md` : 'CLAUDE.md',
JSON.stringify(['claude', 'configuration', 'ai']),
'published',
context.userId || 'system',
1,
now,
now
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create CLAUDE.md',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
document: {
id: docId,
title: 'CLAUDE.md',
type: 'claude-config',
created: true
},
message: 'CLAUDE.md configuration created',
config: {
tddMode: input.tddMode || 'strict',
testCommand: input.testCommand || 'npm test',
lintCommand: input.lintCommand || 'npm run lint',
buildCommand: input.buildCommand || 'npm run build',
customInstructions: input.customInstructions || []
}
});
}
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to generate CLAUDE.md: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Create a new documentation file
*/
const createDocumentationTool = createTool<CreateDocumentationInput, any>({
name: 'create_documentation',
description: 'Create documentation files in your project or specific repository',
category: 'documentation',
inputSchema: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['api', 'architecture', 'contributing', 'changelog', 'custom'],
description: 'Type of documentation'
},
title: {
type: 'string',
description: 'Document title',
minLength: 1,
maxLength: 200
},
content: {
type: 'string',
description: 'Document content in markdown',
minLength: 1,
maxLength: 50000
},
repository: {
type: 'string',
description: 'Target repository name (for multi-repo workspaces)',
maxLength: 200
}
},
required: ['type', 'title', 'content'],
additionalProperties: false
} as JSONSchema7,
async execute(input: CreateDocumentationInput, context: RequestContext) {
try {
const docId = randomUUID();
const now = Date.now();
const projectId = context.projectId || 'default';
// Determine file path based on type
const filename = input.type === 'custom'
? `${input.title.toLowerCase().replace(/\s+/g, '-')}.md`
: `${input.type.toUpperCase()}.md`;
const docPath = input.repository
? `${input.repository}/docs/${filename}`
: `docs/${filename}`;
// Format content with proper markdown
const formattedContent = `# ${input.title}\n\n${input.content}`;
// Insert document into database
const result = await context.db.run(
`INSERT INTO documents
(id, project_id, title, content, type, path, tags, status,
author, version, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
docId,
projectId,
input.title,
formattedContent,
input.type,
docPath,
JSON.stringify([input.type, 'documentation']),
'draft',
context.userId || 'system',
1,
now,
now
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create documentation',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
document: {
id: docId,
title: input.title,
type: input.type,
path: docPath,
size: formattedContent.length
},
message: `Documentation created: ${filename}`,
preview: formattedContent.substring(0, 200) + '...'
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to create documentation: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* List documents with filtering
*/
const listDocumentsTool = createTool<ListDocumentsInput, any>({
name: 'list_documents',
description: 'List documentation files with optional filtering',
category: 'documentation',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
projectId: {
type: 'string',
description: 'Filter by project ID',
pattern: '^[a-zA-Z0-9-_]+$'
},
type: {
type: 'string',
description: 'Filter by document type',
maxLength: 50
},
status: {
type: 'string',
enum: ['draft', 'published', 'archived'],
description: 'Filter by document status'
},
limit: {
type: 'integer',
description: 'Maximum number of documents to return',
minimum: 1,
maximum: 100,
default: 20
},
offset: {
type: 'integer',
description: 'Number of documents to skip',
minimum: 0,
default: 0
}
},
additionalProperties: false
} as JSONSchema7,
async execute(input: ListDocumentsInput, context: RequestContext) {
try {
let sql = 'SELECT * FROM documents WHERE 1=1';
const params: any[] = [];
if (input.projectId) {
sql += ' AND project_id = ?';
params.push(input.projectId);
} else {
sql += ' AND project_id = ?';
params.push(context.projectId || 'default');
}
if (input.type) {
sql += ' AND type = ?';
params.push(input.type);
}
if (input.status) {
sql += ' AND status = ?';
params.push(input.status);
}
sql += ' ORDER BY updated_at DESC LIMIT ? OFFSET ?';
params.push(input.limit || 20, input.offset || 0);
const result = await context.db.query(sql, params);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to list documents',
details: { error: result.error },
category: 'system'
});
}
const documents = (result.data || []).map((doc: any) => ({
id: doc.id,
title: doc.title,
type: doc.type,
path: doc.path,
status: doc.status,
author: doc.author,
version: doc.version,
tags: JSON.parse(doc.tags || '[]'),
createdAt: new Date(doc.created_at).toISOString(),
updatedAt: new Date(doc.updated_at).toISOString(),
preview: doc.content.substring(0, 150) + '...'
}));
return createSuccessResult({
documents,
count: documents.length,
hasMore: documents.length === (input.limit || 20),
filters: {
projectId: input.projectId || context.projectId || 'default',
type: input.type || null,
status: input.status || null
}
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to list documents: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Update an existing document
*/
const updateDocumentTool = createTool<UpdateDocumentInput, any>({
name: 'update_document',
description: 'Update an existing documentation file',
category: 'documentation',
inputSchema: {
type: 'object',
properties: {
documentId: {
type: 'string',
description: 'Document ID to update',
pattern: '^[a-zA-Z0-9-]+$'
},
title: {
type: 'string',
description: 'New document title',
minLength: 1,
maxLength: 200
},
content: {
type: 'string',
description: 'New document content',
minLength: 1,
maxLength: 50000
},
status: {
type: 'string',
enum: ['draft', 'published', 'archived'],
description: 'New document status'
},
tags: {
type: 'array',
items: { type: 'string', maxLength: 50 },
description: 'Document tags',
maxItems: 10
}
},
required: ['documentId'],
additionalProperties: false
} as JSONSchema7,
async execute(input: UpdateDocumentInput, context: RequestContext) {
try {
// Verify document exists
const docCheck = await context.db.get(
'SELECT id, title, type FROM documents WHERE id = ?',
[input.documentId]
);
if (!docCheck.success || !docCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Document not found',
details: { documentId: input.documentId },
category: 'validation'
});
}
// Build update query
const updates: string[] = [];
const params: any[] = [];
if (input.title) {
updates.push('title = ?');
params.push(input.title);
}
if (input.content) {
updates.push('content = ?');
params.push(input.content);
}
if (input.status) {
updates.push('status = ?');
params.push(input.status);
}
if (input.tags) {
updates.push('tags = ?');
params.push(JSON.stringify(input.tags));
}
updates.push('version = version + 1');
updates.push('updated_at = ?');
params.push(Date.now());
params.push(input.documentId);
const result = await context.db.run(
`UPDATE documents SET ${updates.join(', ')} WHERE id = ?`,
params
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to update document',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
document: {
id: input.documentId,
title: input.title || docCheck.data.title,
type: docCheck.data.type,
updated: true
},
message: `Document "${input.title || docCheck.data.title}" updated successfully`,
changes: Object.keys(input).filter(k => k !== 'documentId')
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to update document: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Search documents
*/
const searchDocumentsTool = createTool<SearchDocumentsInput, any>({
name: 'search_documents',
description: 'Search documentation by content or title',
category: 'documentation',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
minLength: 1,
maxLength: 200
},
projectId: {
type: 'string',
description: 'Filter by project ID',
pattern: '^[a-zA-Z0-9-_]+$'
},
type: {
type: 'string',
description: 'Filter by document type',
maxLength: 50
},
limit: {
type: 'integer',
description: 'Maximum number of results',
minimum: 1,
maximum: 50,
default: 10
}
},
required: ['query'],
additionalProperties: false
} as JSONSchema7,
async execute(input: SearchDocumentsInput, context: RequestContext) {
try {
let sql = `
SELECT * FROM documents
WHERE (title LIKE ? OR content LIKE ?)
`;
const searchPattern = `%${input.query}%`;
const params: any[] = [searchPattern, searchPattern];
if (input.projectId) {
sql += ' AND project_id = ?';
params.push(input.projectId);
} else {
sql += ' AND project_id = ?';
params.push(context.projectId || 'default');
}
if (input.type) {
sql += ' AND type = ?';
params.push(input.type);
}
sql += ' ORDER BY updated_at DESC LIMIT ?';
params.push(input.limit || 10);
const result = await context.db.query(sql, params);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to search documents',
details: { error: result.error },
category: 'system'
});
}
const documents = (result.data || []).map((doc: any) => {
// Extract relevant snippet around the match
const content = doc.content;
const matchIndex = content.toLowerCase().indexOf(input.query.toLowerCase());
let snippet = '';
if (matchIndex !== -1) {
const start = Math.max(0, matchIndex - 50);
const end = Math.min(content.length, matchIndex + input.query.length + 50);
snippet = (start > 0 ? '...' : '') +
content.substring(start, end) +
(end < content.length ? '...' : '');
} else {
snippet = content.substring(0, 150) + '...';
}
return {
id: doc.id,
title: doc.title,
type: doc.type,
path: doc.path,
snippet,
score: matchIndex !== -1 ? 1 : 0.5,
updatedAt: new Date(doc.updated_at).toISOString()
};
});
// Sort by relevance (title matches first)
documents.sort((a: any, b: any) => {
const aInTitle = a.title.toLowerCase().includes(input.query.toLowerCase());
const bInTitle = b.title.toLowerCase().includes(input.query.toLowerCase());
if (aInTitle && !bInTitle) return -1;
if (!aInTitle && bInTitle) return 1;
return b.score - a.score;
});
return createSuccessResult({
query: input.query,
results: documents,
count: documents.length,
filters: {
projectId: input.projectId || context.projectId || 'default',
type: input.type || null
}
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to search documents: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Setup documentation management tools
*/
export async function setupDocumentationTools(): Promise<ToolRegistration> {
return {
module: 'documentation',
tools: [
generateReadmeTool,
generateClaudeConfigTool,
createDocumentationTool,
listDocumentsTool,
updateDocumentTool,
searchDocumentsTool
]
};
}
// Helper functions
function generateReadmeContent(options: {
projectName: string;
description: string;
features: string[];
techStack: string[];
installInstructions?: string;
usageExamples?: string;
}): string {
const { projectName, description, features, techStack, installInstructions, usageExamples } = options;
let content = `# ${projectName}\n\n${description}\n\n`;
if (features.length > 0) {
content += `## Features\n\n${features.map(f => `- ${f}`).join('\n')}\n\n`;
}
if (techStack.length > 0) {
content += `## Tech Stack\n\n${techStack.map(t => `- ${t}`).join('\n')}\n\n`;
}
content += `## Installation\n\n${installInstructions || '```bash\nnpm install\n```'}\n\n`;
content += `## Usage\n\n${usageExamples || '```bash\nnpm start\n```'}\n\n`;
content += `## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n`;
content += `## License\n\nThis project is licensed under the MIT License.\n`;
return content;
}
function generateClaudeConfig(options: {
tddMode: 'strict' | 'moderate' | 'off';
testCommand: string;
lintCommand: string;
buildCommand: string;
customInstructions: string[];
}): string {
const { tddMode, testCommand, lintCommand, buildCommand, customInstructions } = options;
let content = `# Claude Code Configuration\n\n`;
content += `This file configures Claude Code's behavior for this project.\n\n`;
content += `## Development Workflow\n\n`;
content += `### Test-Driven Development (TDD)\n\n`;
content += `**Mode**: ${tddMode}\n\n`;
if (tddMode === 'strict') {
content += `- Always write tests before implementation\n`;
content += `- Ensure all tests are failing before writing code\n`;
content += `- Follow Red-Green-Refactor cycle strictly\n\n`;
} else if (tddMode === 'moderate') {
content += `- Prefer writing tests first\n`;
content += `- Ensure good test coverage\n`;
content += `- Refactor with confidence\n\n`;
}
content += `### Commands\n\n`;
content += `- **Run tests**: \`${testCommand}\`\n`;
content += `- **Lint code**: \`${lintCommand}\`\n`;
content += `- **Build project**: \`${buildCommand}\`\n\n`;
content += `## Project Guidelines\n\n`;
content += `1. Follow existing code patterns and conventions\n`;
content += `2. Maintain consistent code style\n`;
content += `3. Write meaningful commit messages\n`;
content += `4. Keep documentation up to date\n`;
if (customInstructions.length > 0) {
content += `\n## Custom Instructions\n\n`;
customInstructions.forEach((instruction, index) => {
content += `${index + 1}. ${instruction}\n`;
});
}
content += `\n## Atlas MCP Integration\n\n`;
content += `This project uses Atlas MCP for project management. Available tools:\n\n`;
content += `- Kanban boards for task management\n`;
content += `- TDD enforcement and tracking\n`;
content += `- Code intelligence and analysis\n`;
content += `- Documentation generation\n`;
return content;
}