UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

391 lines (349 loc) 10.3 kB
import { JSONSchema7 } from 'json-schema'; import { AtlasTool, RequestContext, ToolResult, ToolError, ToolRegistration, ToolExecutionMetadata } from './types.js'; import { SchemaValidator, getValidator, createValidationError } from './validation.js'; import { getSQLiteManager } from '../storage/sqlite-manager.js'; import { randomUUID } from 'crypto'; /** * Tool Registry - manages all registered tools */ export class ToolRegistry { private tools = new Map<string, AtlasTool>(); private modules = new Map<string, ToolRegistration>(); private validator: SchemaValidator; constructor() { this.validator = getValidator(); } /** * Register a module with its tools */ registerModule(registration: ToolRegistration): void { // Validate all tool schemas first for (const tool of registration.tools) { this.validateToolDefinition(tool); } // Register the module this.modules.set(registration.module, registration); // Register individual tools with module prefix for (const tool of registration.tools) { const fullName = `${registration.module}--${tool.name}`; this.tools.set(fullName, { ...tool, name: fullName }); } console.error(`🔧 Registered module '${registration.module}' with ${registration.tools.length} tools`); } /** * Get a tool by name */ getTool(name: string): AtlasTool | undefined { return this.tools.get(name); } /** * Get all registered tools */ getAllTools(): AtlasTool[] { return Array.from(this.tools.values()); } /** * Get tools by module */ getToolsByModule(moduleName: string): AtlasTool[] { const registration = this.modules.get(moduleName); if (!registration) return []; return registration.tools.map(tool => ({ ...tool, name: `${moduleName}.${tool.name}` })); } /** * Get all modules */ getModules(): string[] { return Array.from(this.modules.keys()); } /** * Get tool manifest for MCP */ getToolManifest(): Array<{ name: string; description: string; inputSchema: JSONSchema7; category?: string; readOnly?: boolean; }> { return this.getAllTools().map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, category: tool.category, readOnly: tool.readOnly })); } /** * Get tool count */ getToolCount(): number { return this.tools.size; } /** * Get module count */ getModuleCount(): number { return this.modules.size; } /** * Validate a tool definition */ private validateToolDefinition(tool: AtlasTool): void { if (!tool.name || typeof tool.name !== 'string') { throw new Error('Tool name is required and must be a string'); } if (!tool.description || typeof tool.description !== 'string') { throw new Error('Tool description is required and must be a string'); } if (!tool.inputSchema || typeof tool.inputSchema !== 'object') { throw new Error('Tool inputSchema is required and must be a valid JSON schema'); } if (typeof tool.execute !== 'function') { throw new Error('Tool execute function is required'); } // Validate the schema itself try { this.validator.createValidator(tool.inputSchema); } catch (error) { throw new Error(`Invalid input schema for tool '${tool.name}': ${error instanceof Error ? error.message : 'Unknown error'}`); } } } /** * Tool executor - handles tool execution with validation and error handling */ export class ToolExecutor { private registry: ToolRegistry; private validator: SchemaValidator; constructor(registry: ToolRegistry) { this.registry = registry; this.validator = getValidator(); } /** * Execute a tool with validation and error handling */ async execute( toolName: string, input: any, context: RequestContext ): Promise<ToolResult> { const startTime = Date.now(); try { // Get the tool const tool = this.registry.getTool(toolName); if (!tool) { return this.createErrorResult({ code: 'TOOL_NOT_FOUND', message: `Tool '${toolName}' not found`, details: { toolName, availableTools: this.registry.getAllTools().map(t => t.name) }, suggestions: [ 'Check the tool name for typos', 'Use list_tools to see available tools', 'Verify the tool module is loaded' ], recoverable: false, category: 'resource' }, context.requestId); } // Validate input const validationResult = this.validator.validate(tool.inputSchema, input); if (!validationResult.valid) { return this.createErrorResult( createValidationError(validationResult.errors), context.requestId ); } // Record execution start await this.recordExecution({ toolName, requestId: context.requestId, userId: context.userId, startTime, success: false // Will be updated after execution }); // Execute the tool const result = await tool.execute(input, context); // Calculate execution time const executionTime = Date.now() - startTime; // Update execution record await this.recordExecution({ toolName, requestId: context.requestId, userId: context.userId, startTime, endTime: Date.now(), executionTime, success: result.success, errorCode: result.error?.code }); // Add metadata if (!result.metadata) { result.metadata = {}; } result.metadata.executionTime = executionTime; result.metadata.requestId = context.requestId; return result; } catch (error) { const executionTime = Date.now() - startTime; // Record failed execution await this.recordExecution({ toolName, requestId: context.requestId, userId: context.userId, startTime, endTime: Date.now(), executionTime, success: false, errorCode: 'EXECUTION_ERROR' }); return this.createErrorResult({ code: 'EXECUTION_ERROR', message: `Tool execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`, details: { toolName, error: error instanceof Error ? error.stack : String(error) }, suggestions: [ 'Check tool implementation for bugs', 'Verify input parameters are correct', 'Check system resources and dependencies', 'Review logs for more details' ], recoverable: true, category: 'execution' }, context.requestId, executionTime); } } /** * Create a standardized error result */ private createErrorResult( error: ToolError, requestId: string, executionTime?: number ): ToolResult { return { success: false, error, metadata: { requestId, executionTime } }; } /** * Record tool execution metrics */ private async recordExecution(metadata: ToolExecutionMetadata): Promise<void> { try { const db = getSQLiteManager(); if (metadata.endTime) { // Update existing record await db.run( `UPDATE performance_metrics SET execution_time = ?, success = ?, error_details = ? WHERE request_id = ?`, [ metadata.executionTime || 0, metadata.success ? 1 : 0, metadata.errorCode || null, metadata.requestId ] ); } else { // Insert new record await db.run( `INSERT INTO performance_metrics (id, tool_name, execution_time, success, user_id, request_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [ randomUUID(), metadata.toolName, 0, // Will be updated later 0, // Will be updated later metadata.userId || null, metadata.requestId, metadata.startTime ] ); } } catch (error) { console.error('Failed to record execution metrics:', error); // Don't fail the tool execution for metrics recording errors } } } /** * Global registry instance */ let globalRegistry: ToolRegistry | null = null; let globalExecutor: ToolExecutor | null = null; export function getToolRegistry(): ToolRegistry { if (!globalRegistry) { globalRegistry = new ToolRegistry(); } return globalRegistry; } export function getToolExecutor(): ToolExecutor { if (!globalExecutor) { globalExecutor = new ToolExecutor(getToolRegistry()); } return globalExecutor; } /** * Utility function to create a tool definition with common patterns */ export function createTool<TInput, TOutput>(config: { name: string; description: string; inputSchema: JSONSchema7; outputSchema?: JSONSchema7; category?: string; requiresApproval?: boolean; readOnly?: boolean; execute: (input: TInput, context: RequestContext) => Promise<ToolResult<TOutput>>; }): AtlasTool<TInput, TOutput> { return { name: config.name, description: config.description, inputSchema: config.inputSchema, outputSchema: config.outputSchema, category: config.category, requiresApproval: config.requiresApproval || false, readOnly: config.readOnly || false, execute: config.execute }; } /** * Utility to create success results */ export function createSuccessResult<T>(data: T, metadata?: any): ToolResult<T> { return { success: true, data, metadata }; } /** * Utility to create error results */ export function createErrorResult(error: Partial<ToolError>): ToolResult { return { success: false, error: { code: error.code || 'UNKNOWN_ERROR', message: error.message || 'An unknown error occurred', details: error.details || {}, suggestions: error.suggestions || ['Please try again'], recoverable: error.recoverable !== false, category: error.category || 'execution' } }; }