Skip to main content
Glama

mcp-adr-analysis-server

by tosin2013
ai-executor.ts11.8 kB
/** * AI Executor Service for OpenRouter.ai Integration * * This service handles the execution of prompts using OpenRouter.ai, * transforming the MCP server from returning prompts to returning actual results. */ import OpenAI from 'openai'; import { AIConfig, loadAIConfig, validateAIConfig, isAIExecutionEnabled, } from '../config/ai-config.js'; export interface AIExecutionResult { /** The AI-generated response content */ content: string; /** Model used for generation */ model: string; /** Token usage information */ usage?: { promptTokens: number; completionTokens: number; totalTokens: number; }; /** Execution metadata */ metadata: { executionTime: number; cached: boolean; retryCount: number; timestamp: string; }; } export interface AIExecutionError extends Error { code: string; retryable: boolean; originalError?: unknown; } /** * AI Executor Service Class * * @description Core service for executing AI prompts through OpenRouter.ai integration. * Transforms the MCP server from returning prompts to returning actual AI-generated results. * Includes caching, retry logic, and comprehensive error handling. * * @example * ```typescript * // Initialize with custom configuration * const executor = new AIExecutor({ * apiKey: 'your-api-key', * model: 'anthropic/claude-3-sonnet', * maxTokens: 4000 * }); * * // Execute a prompt * const result = await executor.executePrompt({ * prompt: 'Analyze this ADR...', * context: { projectPath: '/path/to/project' } * }); * * console.log(result.content); // AI-generated analysis * ``` * * @example * ```typescript * // Use singleton instance * const executor = getAIExecutor(); * const result = await executor.executePrompt({ * prompt: 'Generate ADR suggestions', * maxTokens: 2000 * }); * ``` * * @since 2.0.0 * @category AI * @category Core */ export class AIExecutor { private client: OpenAI | null = null; private config: AIConfig; private cache: Map<string, { result: AIExecutionResult; expiry: number }> = new Map(); constructor(config?: AIConfig) { this.config = config || loadAIConfig(); this.initializeClient(); } /** * Initialize OpenAI client for OpenRouter */ private initializeClient(): void { if (!isAIExecutionEnabled(this.config)) { console.log('AI execution disabled - running in prompt-only mode'); return; } try { validateAIConfig(this.config); this.client = new OpenAI({ baseURL: this.config.baseURL, apiKey: this.config.apiKey, timeout: this.config.timeout, maxRetries: this.config.maxRetries, defaultHeaders: { 'HTTP-Referer': this.config.siteUrl || '', 'X-Title': this.config.siteName || '', }, }); console.log(`AI Executor initialized with model: ${this.config.defaultModel}`); } catch (error) { console.error('Failed to initialize AI Executor:', error); this.client = null; } } /** * Check if AI execution is available */ public isAvailable(): boolean { // Reload configuration to pick up environment variable changes this.reloadConfigIfNeeded(); return this.client !== null && isAIExecutionEnabled(this.config); } /** * Reload configuration if environment variables have changed */ private reloadConfigIfNeeded(): void { const currentConfig = loadAIConfig(); // Check if key configuration has changed const configChanged = this.config.apiKey !== currentConfig.apiKey || this.config.executionMode !== currentConfig.executionMode || this.config.defaultModel !== currentConfig.defaultModel; if (configChanged) { console.log('AI configuration changed, reinitializing...'); this.config = currentConfig; this.initializeClient(); } } /** * Execute a prompt and return the AI response */ public async executePrompt( prompt: string, options: { model?: string; temperature?: number; maxTokens?: number; systemPrompt?: string; } = {} ): Promise<AIExecutionResult> { // Ensure configuration is up to date before execution this.reloadConfigIfNeeded(); if (!this.isAvailable()) { throw this.createError( 'AI execution not available - check configuration', 'AI_UNAVAILABLE', false ); } const startTime = Date.now(); const model = options.model || this.config.defaultModel; const cacheKey = this.generateCacheKey(prompt, model, options); // Check cache first if (this.config.cacheEnabled) { const cached = this.getCachedResult(cacheKey); if (cached) { return cached; } } let retryCount = 0; const maxRetries = this.config.maxRetries; while (retryCount <= maxRetries) { try { const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = []; if (options.systemPrompt) { messages.push({ role: 'system', content: options.systemPrompt }); } messages.push({ role: 'user', content: prompt }); const completion = await this.client!.chat.completions.create({ model, messages, temperature: options.temperature ?? this.config.temperature, max_tokens: options.maxTokens ?? this.config.maxTokens, }); const result: AIExecutionResult = { content: completion.choices[0]?.message?.content || '', model: completion.model, metadata: { executionTime: Date.now() - startTime, cached: false, retryCount, timestamp: new Date().toISOString(), }, }; if (completion.usage) { result.usage = { promptTokens: completion.usage.prompt_tokens, completionTokens: completion.usage.completion_tokens, totalTokens: completion.usage.total_tokens, }; } // Cache the result if (this.config.cacheEnabled) { this.setCachedResult(cacheKey, result); } return result; } catch (error) { retryCount++; if (retryCount > maxRetries) { throw this.createError( `AI execution failed after ${maxRetries} retries: ${error}`, 'AI_EXECUTION_FAILED', false, error ); } // Wait before retry (exponential backoff) const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 10000); await new Promise(resolve => setTimeout(resolve, delay)); } } throw this.createError('Unexpected error in AI execution', 'AI_UNEXPECTED_ERROR', false); } /** * Execute a structured prompt that expects JSON response */ public async executeStructuredPrompt<T = any>( prompt: string, schema?: any, options: { model?: string; temperature?: number; maxTokens?: number; systemPrompt?: string; } = {} ): Promise<{ data: T; raw: AIExecutionResult }> { const systemPrompt = options.systemPrompt || 'You are a helpful assistant that responds with valid JSON. Always return properly formatted JSON that matches the requested schema. Do not wrap the JSON in markdown code blocks.'; const result = await this.executePrompt(prompt, { ...options, systemPrompt, temperature: options.temperature ?? 0.1, // Lower temperature for structured output }); try { // Extract JSON from response, handling markdown code blocks const jsonContent = this.extractJsonFromResponse(result.content); const data = JSON.parse(jsonContent) as T; // Basic schema validation if provided if (schema && typeof schema.parse === 'function') { schema.parse(data); } return { data, raw: result }; } catch (error) { throw this.createError( `Failed to parse JSON response: ${error}`, 'AI_JSON_PARSE_ERROR', false, error ); } } /** * Extract JSON content from AI response, handling markdown code blocks */ private extractJsonFromResponse(content: string): string { // Remove leading/trailing whitespace content = content.trim(); // Check if content is wrapped in markdown code blocks const codeBlockMatch = content.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/); if (codeBlockMatch && codeBlockMatch[1]) { return codeBlockMatch[1].trim(); } // Check for inline code blocks const inlineCodeMatch = content.match(/^`([\s\S]*?)`$/); if (inlineCodeMatch && inlineCodeMatch[1]) { return inlineCodeMatch[1].trim(); } // Try to find JSON object/array in the content const jsonMatch = content.match(/(\{[\s\S]*\}|\[[\s\S]*\])/); if (jsonMatch && jsonMatch[1]) { return jsonMatch[1].trim(); } // Return as-is if no patterns match return content; } /** * Generate cache key for a prompt execution */ private generateCacheKey(prompt: string, model: string, options: any): string { const key = JSON.stringify({ prompt, model, options }); return Buffer.from(key).toString('base64').slice(0, 32); } /** * Get cached result if available and not expired */ private getCachedResult(cacheKey: string): AIExecutionResult | null { const cached = this.cache.get(cacheKey); if (!cached) return null; if (Date.now() > cached.expiry) { this.cache.delete(cacheKey); return null; } // Mark as cached const result = { ...cached.result }; result.metadata = { ...result.metadata, cached: true }; return result; } /** * Cache a result */ private setCachedResult(cacheKey: string, result: AIExecutionResult): void { const expiry = Date.now() + this.config.cacheTTL * 1000; this.cache.set(cacheKey, { result, expiry }); // Clean up expired entries periodically if (this.cache.size > 100) { this.cleanupCache(); } } /** * Clean up expired cache entries */ private cleanupCache(): void { const now = Date.now(); for (const [key, value] of this.cache.entries()) { if (now > value.expiry) { this.cache.delete(key); } } } /** * Create a standardized AI execution error */ private createError( message: string, code: string, retryable: boolean, originalError?: unknown ): AIExecutionError { const error = new Error(message) as AIExecutionError; error.code = code; error.retryable = retryable; error.originalError = originalError; return error; } /** * Get current configuration */ public getConfig(): AIConfig { return { ...this.config }; } /** * Update configuration */ public updateConfig(newConfig: Partial<AIConfig>): void { this.config = { ...this.config, ...newConfig }; this.initializeClient(); } /** * Clear cache */ public clearCache(): void { this.cache.clear(); } /** * Get cache statistics */ public getCacheStats(): { size: number; hitRate: number } { // This is a simplified implementation return { size: this.cache.size, hitRate: 0, // Would need to track hits/misses for accurate calculation }; } } /** * Global AI executor instance */ let globalExecutor: AIExecutor | null = null; /** * Get or create the global AI executor instance */ export function getAIExecutor(): AIExecutor { if (!globalExecutor) { globalExecutor = new AIExecutor(); } return globalExecutor; } /** * Reset the global AI executor (useful for testing) */ export function resetAIExecutor(): void { globalExecutor = null; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tosin2013/mcp-adr-analysis-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server