UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

557 lines (493 loc) 15.8 kB
import { JSONSchema7 } from 'json-schema'; import { randomUUID } from 'crypto'; import { promises as fs } from 'fs'; import * as path from 'path'; import { createTool, createSuccessResult, createErrorResult } from '../../core/tool-framework.js'; import { ToolRegistration, RequestContext } from '../../core/types.js'; /** * Project 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 InitProjectInput { projectName?: string; } interface CheckProjectStatusInput { // No parameters needed } interface FindProjectInput { // No parameters needed } interface RemoveProjectInput { confirm?: boolean; } interface UpdateProjectConfigInput { name?: string; description?: string; settings?: Record<string, any>; } interface ProjectMarker { projectId: string; projectName: string; createdAt: string; atlasVersion: string; projectRoot: string; } /** * Initialize Atlas project */ const initAtlasTool = createTool<InitProjectInput, any>({ name: 'init_atlas', description: 'Initialize Atlas MCP in the current project', category: 'project-management', inputSchema: { type: 'object', properties: { projectName: { type: 'string', description: 'Custom project name (defaults to directory name)', maxLength: 200 } }, additionalProperties: false } as JSONSchema7, async execute(input: InitProjectInput, context: RequestContext) { try { const projectId = randomUUID(); const now = Date.now(); const projectRoot = process.cwd(); const projectName = input.projectName || path.basename(projectRoot); // Check if project already exists const existingProject = await context.db.get( 'SELECT * FROM projects WHERE id = ? OR name = ?', [projectId, projectName] ); if (existingProject.data) { return createSuccessResult({ alreadyInitialized: true, project: existingProject.data, message: `Project "${projectName}" already initialized` }); } // Create project in database const result = await context.db.run( `INSERT INTO projects (id, name, description, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [ projectId, projectName, `Atlas MCP project: ${projectName}`, JSON.stringify({ projectRoot, atlasVersion: '1.0.0', initializedAt: now }), now, now ] ); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to create project', details: { error: result.error }, category: 'system' }); } // Create .atlas marker file const markerPath = path.join(projectRoot, '.atlas'); const marker: ProjectMarker = { projectId, projectName, createdAt: new Date(now).toISOString(), atlasVersion: '1.0.0', projectRoot }; await fs.writeFile(markerPath, JSON.stringify(marker, null, 2), 'utf-8'); // Add .atlas to gitignore if git exists try { const gitignorePath = path.join(projectRoot, '.gitignore'); let gitignoreContent = ''; try { gitignoreContent = await fs.readFile(gitignorePath, 'utf-8'); } catch { // .gitignore doesn't exist yet } if (!gitignoreContent.includes('.atlas')) { gitignoreContent += (gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : '') + '# Atlas MCP marker file\n.atlas\n'; await fs.writeFile(gitignorePath, gitignoreContent, 'utf-8'); } } catch { // Ignore gitignore errors } return createSuccessResult({ initialized: true, project: { id: projectId, name: projectName, projectRoot, marker }, message: `Atlas MCP initialized for project "${projectName}"` }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to initialize project: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Check project status */ const checkProjectStatusTool = createTool<CheckProjectStatusInput, any>({ name: 'check_project_status', description: 'Check if current directory is a Atlas project', category: 'project-management', readOnly: true, inputSchema: { type: 'object', properties: {}, additionalProperties: false } as JSONSchema7, async execute(input: CheckProjectStatusInput, context: RequestContext) { try { const projectRoot = process.cwd(); const markerPath = path.join(projectRoot, '.atlas'); // Check for marker file let marker: ProjectMarker | null = null; try { const markerContent = await fs.readFile(markerPath, 'utf-8'); marker = JSON.parse(markerContent); } catch { return createSuccessResult({ initialized: false, message: 'Not a Atlas project - no .atlas marker file found' }); } if (!marker) { return createSuccessResult({ initialized: false, message: 'Invalid .atlas marker file' }); } // Get project from database const projectResult = await context.db.get( 'SELECT * FROM projects WHERE id = ?', [marker.projectId] ); if (!projectResult.data) { return createSuccessResult({ initialized: false, marker, message: 'Project marker exists but project not found in database' }); } // Count related data const stats = await Promise.all([ context.db.get('SELECT COUNT(*) as count FROM agile_stories WHERE project_id = ?', [marker.projectId]), context.db.get('SELECT COUNT(*) as count FROM kanban_boards WHERE project_id = ?', [marker.projectId]), context.db.get('SELECT COUNT(*) as count FROM documents WHERE project_id = ?', [marker.projectId]), context.db.get('SELECT COUNT(*) as count FROM memories WHERE project_id = ?', [marker.projectId]) ]); const dataStats = { stories: stats[0].data?.count || 0, boards: stats[1].data?.count || 0, documents: stats[2].data?.count || 0, memories: stats[3].data?.count || 0 }; return createSuccessResult({ initialized: true, project: projectResult.data, marker, dataStats, message: `Atlas project "${marker.projectName}" is active` }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to check project status: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Find Atlas project */ const findAtlasProjectTool = createTool<FindProjectInput, any>({ name: 'find_atlas_project', description: 'Find the nearest Atlas project in parent directories', category: 'project-management', readOnly: true, inputSchema: { type: 'object', properties: {}, additionalProperties: false } as JSONSchema7, async execute(input: FindProjectInput, context: RequestContext) { try { const currentDir = process.cwd(); let searchDir = currentDir; let marker: ProjectMarker | null = null; let projectRoot: string | null = null; // Search upward for .atlas marker file while (searchDir !== path.dirname(searchDir)) { const markerPath = path.join(searchDir, '.atlas'); try { const markerContent = await fs.readFile(markerPath, 'utf-8'); marker = JSON.parse(markerContent); projectRoot = searchDir; break; } catch { // Continue searching } searchDir = path.dirname(searchDir); } if (!marker || !projectRoot) { return createSuccessResult({ found: false, searchedFrom: currentDir, message: 'No Atlas project found in current or parent directories' }); } // Get project from database const projectResult = await context.db.get( 'SELECT * FROM projects WHERE id = ?', [marker.projectId] ); return createSuccessResult({ found: true, project: projectResult.data, marker, projectRoot, currentDir, isAtRoot: projectRoot === currentDir, message: `Found Atlas project "${marker.projectName}" at ${projectRoot}` }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to find project: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Remove Atlas project */ const removeAtlasTool = createTool<RemoveProjectInput, any>({ name: 'remove_atlas', description: 'Remove Atlas MCP from the current project', category: 'project-management', inputSchema: { type: 'object', properties: { confirm: { type: 'boolean', default: false, description: 'Confirm removal of all Atlas data' } }, additionalProperties: false } as JSONSchema7, async execute(input: RemoveProjectInput, context: RequestContext) { try { if (!input.confirm) { return createSuccessResult({ requiresConfirmation: true, message: 'This will remove Atlas from this project and delete all data. Set confirm=true to proceed.' }); } const projectRoot = process.cwd(); const markerPath = path.join(projectRoot, '.atlas'); // Read marker file let marker: ProjectMarker | null = null; try { const markerContent = await fs.readFile(markerPath, 'utf-8'); marker = JSON.parse(markerContent); } catch { return createSuccessResult({ message: 'Not a Atlas project - nothing to remove' }); } if (!marker) { return createSuccessResult({ message: 'Invalid marker file - nothing to remove' }); } // Remove project and all related data from database const deleteResult = await context.db.run( 'DELETE FROM projects WHERE id = ?', [marker.projectId] ); if (!deleteResult.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to remove project from database', details: { error: deleteResult.error }, category: 'system' }); } // Remove marker file await fs.unlink(markerPath); return createSuccessResult({ removed: true, project: { id: marker.projectId, name: marker.projectName }, message: `Atlas removed from project "${marker.projectName}"` }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to remove project: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Update project configuration */ const updateProjectConfigTool = createTool<UpdateProjectConfigInput, any>({ name: 'update_project_config', description: 'Update project configuration and settings', category: 'project-management', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'New project name', maxLength: 200 }, description: { type: 'string', description: 'Project description', maxLength: 1000 }, settings: { type: 'object', description: 'Additional project settings', additionalProperties: true } }, additionalProperties: false } as JSONSchema7, async execute(input: UpdateProjectConfigInput, context: RequestContext) { try { const projectRoot = process.cwd(); const markerPath = path.join(projectRoot, '.atlas'); // Read marker file let marker: ProjectMarker | null = null; try { const markerContent = await fs.readFile(markerPath, 'utf-8'); marker = JSON.parse(markerContent); } catch { return createErrorResult({ code: 'NOT_FOUND', message: 'Not a Atlas project - no .atlas marker file found', category: 'validation' }); } if (!marker) { return createErrorResult({ code: 'INVALID_DATA', message: 'Invalid .atlas marker file', category: 'validation' }); } // Get current project config const projectResult = await context.db.get( 'SELECT * FROM projects WHERE id = ?', [marker.projectId] ); if (!projectResult.data) { return createErrorResult({ code: 'NOT_FOUND', message: 'Project not found in database', category: 'validation' }); } const currentConfig = JSON.parse(projectResult.data.config || '{}'); const now = Date.now(); // Build update fields const updates: any = { updated_at: now }; if (input.name) { updates.name = input.name; // Update marker file as well marker.projectName = input.name; await fs.writeFile(markerPath, JSON.stringify(marker, null, 2), 'utf-8'); } if (input.description) { updates.description = input.description; } if (input.settings) { const newConfig = { ...currentConfig, ...input.settings }; updates.config = JSON.stringify(newConfig); } // Update database const updateColumns = Object.keys(updates); const updatePlaceholders = updateColumns.map(() => '?').join(', '); const updateSet = updateColumns.map(col => `${col} = ?`).join(', '); const updateResult = await context.db.run( `UPDATE projects SET ${updateSet} WHERE id = ?`, [...Object.values(updates), marker.projectId] ); if (!updateResult.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to update project configuration', details: { error: updateResult.error }, category: 'system' }); } // Get updated project const updatedProjectResult = await context.db.get( 'SELECT * FROM projects WHERE id = ?', [marker.projectId] ); return createSuccessResult({ updated: true, project: updatedProjectResult.data, marker, message: `Project configuration updated successfully` }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to update project config: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Setup project management tools */ export async function setupProjectManagementTools(): Promise<ToolRegistration> { return { module: 'project-management', tools: [ initAtlasTool, checkProjectStatusTool, findAtlasProjectTool, removeAtlasTool, updateProjectConfigTool ] }; }