UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

966 lines (875 loc) 28.7 kB
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; }