@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
557 lines (493 loc) • 15.8 kB
text/typescript
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
]
};
}