/** * BrainyMCPService * * This class provides a unified service for accessing Brainy data and augmentations * through the Model Control Protocol (MCP). It integrates the BrainyMCPAdapter and * MCPAugmentationToolset classes and provides WebSocket and REST server implementations * for external model access. */ import { v4 as uuidv4 } from '../universal/uuid.js' import { BrainyInterface } from '../types/brainyInterface.js' import { MCPRequest, MCPResponse, MCPDataAccessRequest, MCPToolExecutionRequest, MCPSystemInfoRequest, MCPAuthenticationRequest, MCPRequestType, MCPServiceOptions, MCP_VERSION, MCPTool } from '../types/mcpTypes.js' import { BrainyMCPAdapter } from './brainyMCPAdapter.js' import { MCPAugmentationToolset } from './mcpAugmentationToolset.js' import { isBrowser, isNode } from '../utils/environment.js' export class BrainyMCPService { private dataAdapter: BrainyMCPAdapter private toolset: MCPAugmentationToolset private options: MCPServiceOptions private authTokens: Map private rateLimits: Map /** * Creates a new BrainyMCPService * @param brainyData The Brainy instance to wrap * @param options Configuration options for the service */ constructor( brainyData: BrainyInterface, options: MCPServiceOptions = {} ) { this.dataAdapter = new BrainyMCPAdapter(brainyData) this.toolset = new MCPAugmentationToolset() this.options = options this.authTokens = new Map() this.rateLimits = new Map() } /** * Handles an MCP request * @param request The MCP request * @returns An MCP response */ async handleRequest(request: MCPRequest): Promise { try { switch (request.type) { case MCPRequestType.DATA_ACCESS: return await this.dataAdapter.handleRequest( request as MCPDataAccessRequest ) case MCPRequestType.TOOL_EXECUTION: return await this.toolset.handleRequest( request as MCPToolExecutionRequest ) case MCPRequestType.SYSTEM_INFO: return await this.handleSystemInfoRequest( request as MCPSystemInfoRequest ) case MCPRequestType.AUTHENTICATION: return await this.handleAuthenticationRequest( request as MCPAuthenticationRequest ) default: return this.createErrorResponse( request.requestId, 'UNSUPPORTED_REQUEST_TYPE', `Request type ${request.type} is not supported` ) } } catch (error) { return this.createErrorResponse( request.requestId, 'INTERNAL_ERROR', error instanceof Error ? error.message : String(error) ) } } /** * Handles a system info request * @param request The MCP request * @returns An MCP response */ private async handleSystemInfoRequest( request: MCPSystemInfoRequest ): Promise { try { switch (request.infoType) { case 'status': return this.createSuccessResponse(request.requestId, { status: 'active', version: MCP_VERSION, environment: isBrowser() ? 'browser' : isNode() ? 'node' : 'unknown' }) case 'availableTools': const tools: MCPTool[] = await this.toolset.getAvailableTools() return this.createSuccessResponse(request.requestId, tools) case 'version': return this.createSuccessResponse(request.requestId, { version: MCP_VERSION }) default: return this.createErrorResponse( request.requestId, 'UNSUPPORTED_INFO_TYPE', `Info type ${request.infoType} is not supported` ) } } catch (error) { return this.createErrorResponse( request.requestId, 'INTERNAL_ERROR', error instanceof Error ? error.message : String(error) ) } } /** * Handles an authentication request * @param request The MCP request * @returns An MCP response */ private async handleAuthenticationRequest( request: MCPAuthenticationRequest ): Promise { try { if (!this.options.enableAuth) { return this.createSuccessResponse(request.requestId, { authenticated: true, message: 'Authentication is not enabled' }) } const { credentials } = request // Check API key authentication if ( credentials.apiKey && this.options.apiKeys?.includes(credentials.apiKey) ) { const token = this.generateAuthToken('api-user') return this.createSuccessResponse(request.requestId, { authenticated: true, token }) } // Authentication must be implemented by the user throw new Error( 'Authentication not configured. Please implement custom authentication handler by extending BrainyMCPService and overriding authenticateUser()' ) return this.createErrorResponse( request.requestId, 'INVALID_CREDENTIALS', 'Invalid credentials' ) } catch (error) { return this.createErrorResponse( request.requestId, 'INTERNAL_ERROR', error instanceof Error ? error.message : String(error) ) } } /** * Checks if a request is valid * @param request The request to check * @returns Whether the request is valid */ private isValidRequest(request: any): boolean { return ( request && typeof request === 'object' && request.type && request.requestId && request.version ) } /** * Checks if a request is authenticated * @param request The request to check * @returns Whether the request is authenticated */ private isAuthenticated(request: MCPRequest): boolean { if (!this.options.enableAuth) { return true } return request.authToken ? this.isValidToken(request.authToken) : false } /** * Checks if a token is valid * @param token The token to check * @returns Whether the token is valid */ private isValidToken(token: string): boolean { const tokenInfo = this.authTokens.get(token) if (!tokenInfo) { return false } if (tokenInfo.expires < Date.now()) { this.authTokens.delete(token) return false } return true } /** * Generates an authentication token * @param userId The user ID to associate with the token * @returns The generated token */ private generateAuthToken(userId: string): string { const token = uuidv4() const expires = Date.now() + 24 * 60 * 60 * 1000 // 24 hours this.authTokens.set(token, { userId, expires }) return token } /** * Checks if a client has exceeded the rate limit * @param clientId The client ID to check * @returns Whether the client is within the rate limit */ private checkRateLimit(clientId: string): boolean { if (!this.options.rateLimit) { return true } const now = Date.now() const limit = this.rateLimits.get(clientId) if (!limit) { this.rateLimits.set(clientId, { count: 1, resetTime: now + this.options.rateLimit.windowMs }) return true } if (limit.resetTime < now) { limit.count = 1 limit.resetTime = now + this.options.rateLimit.windowMs return true } if (limit.count >= this.options.rateLimit.maxRequests) { return false } limit.count++ return true } /** * Creates a success response * @param requestId The request ID * @param data The response data * @returns An MCP response */ private createSuccessResponse(requestId: string, data: any): MCPResponse { return { success: true, requestId, version: MCP_VERSION, data } } /** * Creates an error response * @param requestId The request ID * @param code The error code * @param message The error message * @param details Optional error details * @returns An MCP response */ private createErrorResponse( requestId: string, code: string, message: string, details?: any ): MCPResponse { return { success: false, requestId, version: MCP_VERSION, error: { code, message, details } } } /** * Creates a new request ID * @returns A new UUID */ generateRequestId(): string { return uuidv4() } /** * Handles an MCP request directly (for in-process models) * @param request The MCP request * @returns An MCP response */ async handleMCPRequest(request: MCPRequest): Promise { return await this.handleRequest(request) } }