mcp-smart
Version:
MCP server providing multi-advisor AI consultations via OpenRouter API with advanced caching, rate limiting, and security features
1,161 lines (1,116 loc) • 51.6 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import axios, { AxiosError } from 'axios';
var LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["ERROR"] = 0] = "ERROR";
LogLevel[LogLevel["WARN"] = 1] = "WARN";
LogLevel[LogLevel["INFO"] = 2] = "INFO";
LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG";
})(LogLevel || (LogLevel = {}));
var CircuitBreakerState;
(function (CircuitBreakerState) {
CircuitBreakerState["CLOSED"] = "CLOSED";
CircuitBreakerState["OPEN"] = "OPEN";
CircuitBreakerState["HALF_OPEN"] = "HALF_OPEN";
})(CircuitBreakerState || (CircuitBreakerState = {}));
class Logger {
context;
constructor(context) {
this.context = context;
}
log(level, message, ...args) {
const timestamp = new Date().toISOString();
const levelName = LogLevel[level];
console.error(`[${timestamp}] [${levelName}] [${this.context}] ${message}`, ...args);
}
error(message, ...args) {
this.log(LogLevel.ERROR, message, ...args);
}
warn(message, ...args) {
this.log(LogLevel.WARN, message, ...args);
}
info(message, ...args) {
this.log(LogLevel.INFO, message, ...args);
}
debug(message, ...args) {
this.log(LogLevel.DEBUG, message, ...args);
}
}
const MODELS = {
'deepseek': 'deepseek/deepseek-chat-v3-0324',
'google': 'google/gemini-2.5-flash',
'openai': 'openai/o3',
'xai': 'x-ai/grok-3-beta',
'claude': 'anthropic/claude-sonnet-4',
'moonshot': 'moonshotai/kimi-k2',
'router': 'openai/gpt-4o-mini' // For routing decisions
};
const MODEL_NAMES = {
'deepseek': 'DeepSeek AI',
'google': 'Google Gemini Flash',
'openai': 'OpenAI o3',
'xai': 'xAI Grok',
'claude': 'Anthropic Claude Sonnet 4',
'moonshot': 'Moonshot Kimi-K2',
'router': 'GPT-4.1-Mini Router'
};
// Provider capabilities and cost tiers (ranked by intelligence: Claude > OpenAI > XAI/Google > DeepSeek)
const PROVIDER_SPECS = {
'deepseek': {
cost: 'low', // Very cheap
intelligence: 'high', // Good reasoning
context: 'medium', // Standard context window
speed: 'fast', // Fast responses
strengths: ['coding', 'logic', 'math', 'analysis', 'cost-efficiency']
},
'google': {
cost: 'low', // Low pricing with Flash
intelligence: 'very-high', // Excellent reasoning
context: 'highest', // Largest context window (2M tokens)
speed: 'fast', // Fast responses with Flash
strengths: ['reasoning', 'research', 'long-context', 'multimodal', 'speed']
},
'openai': {
cost: 'very-high', // Very expensive
intelligence: 'highest', // Top-tier reasoning
context: 'medium', // Standard context
speed: 'slow', // Slower but very high quality
strengths: ['complex-reasoning', 'creativity', 'advanced-coding', 'problem-solving']
},
'xai': {
cost: 'medium', // Mid-tier pricing
intelligence: 'very-high', // Strong reasoning
context: 'high', // Large context window
speed: 'fast', // Fast responses
strengths: ['reasoning', 'real-time-data', 'social-context', 'creative-thinking']
},
'claude': {
cost: 'high', // Premium pricing
intelligence: 'ultimate', // Supreme reasoning capability
context: 'very-high', // Very large context window (200k tokens)
speed: 'medium', // Balanced speed
strengths: ['ultimate-reasoning', 'deep-analysis', 'ethical-coding', 'comprehensive-solutions', 'nuanced-understanding']
},
'moonshot': {
cost: 'medium', // Mid-tier pricing
intelligence: 'very-high', // Strong reasoning capability
context: 'highest', // Very large context window (2M tokens)
speed: 'fast', // Fast responses
strengths: ['chinese-language', 'reasoning', 'coding', 'long-context', 'multimodal']
}
};
// Model routing strategies
const ROUTING_STRATEGIES = {
'auto': 'Let GPT-4o-mini choose the best provider for this specific task',
'intelligence': 'Prioritize the most capable model (Claude Sonnet 4)',
'cost': 'Prioritize the most cost-effective model (DeepSeek)',
'balance': 'Balance cost and performance (Google Gemini Flash)',
'speed': 'Prioritize fastest responses (xAI Grok)',
'premium': 'Use premium intelligence (OpenAI o3)',
'random': 'Randomly select from available providers',
'all': 'Consult all providers',
// Original providers still work
'deepseek': 'Force DeepSeek',
'google': 'Force Google Gemini Flash',
'openai': 'Force OpenAI o3',
'xai': 'Force xAI Grok',
'claude': 'Force Claude Sonnet 4',
'moonshot': 'Force Moonshot Kimi-K2'
};
const TOOL_SPECIFIC_ROLES = {
smart_advisor: {
role: "Smart Technical Advisor",
focus: "providing comprehensive technical guidance",
description: "You are a senior technical advisor who provides strategic coding guidance with deep architectural insights."
},
code_review: {
role: "Senior Code Reviewer",
focus: "conducting thorough code reviews",
description: "You are a meticulous senior developer specializing in code quality, security, performance, and best practices."
},
get_advice: {
role: "Coding Mentor",
focus: "providing practical coding advice",
description: "You are an experienced coding mentor who helps developers solve problems with clear, actionable advice."
},
expert_opinion: {
role: "Technical Expert",
focus: "providing expert technical opinions",
description: "You are a distinguished technical expert who provides authoritative opinions on complex technical matters."
},
smart_llm: {
role: "AI Code Analyst",
focus: "intelligent code analysis and optimization",
description: "You are an advanced AI system specialized in deep code analysis, pattern recognition, and intelligent suggestions."
},
ask_expert: {
role: "Industry Expert",
focus: "sharing professional expertise",
description: "You are a seasoned industry professional with years of experience solving real-world coding challenges."
},
review_code: {
role: "Code Quality Specialist",
focus: "comprehensive code evaluation",
description: "You are a code quality specialist who performs detailed code evaluations focusing on maintainability, scalability, and robustness."
}
};
const SMART_ADVISOR_PROMPT = `Split yourself to four personas:
1. Manager: The "brain" of the team. Defines clear, understandable requirements for the CTO in simple yet detailed terms. I need this persona to ensure you understand the task correctly. Manager speaks only to CTO.
2. CTO: Lead developer. Gets tasks from Manager, implementing detailed architecture. Adept at best DX methodologies: DRY, SOLID, KISS, TDD. CTO speaks to Manager, QA and Engineer. See "FULL CTO DESCRIPTION" section below.
3. QA: Gets technical description from CTO and implements unit tests covering common states, edge cases, potential bottlenecks and invalid data.
4. Engineer: Senior L6 Google developer implements code per CTO instructions and QA test files. Can consult CTO and QA to clarify ambiguous information, request test updates if interface changes needed, and must get CTO approval to deviate from provided instructions and tests.
Working flow (MUST FOLLOW):
Manager -> CTO -> QA -> Engineer -> QA -> CTO -> Manager
FULL CTO DESCRIPTION:
~~~~~~
You are an expert coding assistant in languages like Markdown, JavaScript, HTML, CSS, Python, and Node.js. Your goal is to provide concise, clear, readable, efficient, and bug-free code solutions that follow best practices and modern standards.
When debugging, consider 5-7 possible problem sources, identify the 1-2 most likely causes, and add logs to validate your assumptions before implementing fixes.
1. Analyze the code and question:
In <code_analysis> tags:
- Identify the programming language used
- Assess the difficulty level of the task (Easy, Medium, or Hard)
- Identify key components or functions in the existing code
- Quote relevant parts of the existing code that relate to the user's question
- Provide a brief summary of what the existing code does
- Break down the problem into smaller components
- Consider potential best practices and optimizations
- Create a Mermaid diagram to visualize the solution structure
2. Plan your approach:
In <solution_plan> tags:
Write detailed, numbered pseudocode outlining your solution strategy. Include comments explaining the reasoning behind each step. It's OK for this section to be quite long.
3. Confirm your understanding:
Briefly restate the problem and your planned approach to ensure you've correctly interpreted the user's needs.
4. Implement the solution:
Provide your code implementation, adhering to the following principles:
- Write bug-free, secure, and efficient code
- Prioritize readability and maintainability
- Implement all required functionality completely
- Avoid placeholders
- Be concise while maintaining clarity
- Use the latest relevant technologies and best practices
5. Verify the solution:
Explain how your implementation meets the requirements and addresses the user's question.
6. Consider improvements:
Briefly discuss any potential optimizations or alternative approaches, if applicable.
Please format your response as follows:
<difficulty_level>[Easy/Medium/Hard]</difficulty_level>
<code_analysis>
[Your detailed analysis, including the Mermaid diagram]
</code_analysis>
<solution_plan>
[Your detailed, numbered pseudocode with comments]
</solution_plan>
Confirmation: [Your understanding of the problem and approach]
Code:
\`\`\`[language]
// [Filename (if applicable)]
[Your implemented code]
\`\`\`
Verification: [Explanation of how the solution meets the requirements]
Potential Improvements: [Brief discussion of optimizations or alternatives]
~~~~~~`;
function buildToolSpecificPrompt(toolName) {
const toolRole = TOOL_SPECIFIC_ROLES[toolName];
if (!toolRole) {
return SMART_ADVISOR_PROMPT;
}
return `You are acting as a ${toolRole.role}, ${toolRole.focus}.
${toolRole.description}
Split yourself to four personas:
1. Manager: The "brain" of the team. Defines clear, understandable requirements for the ${toolRole.role} in simple yet detailed terms. I need this persona to ensure you understand the task correctly. Manager speaks only to ${toolRole.role}.
2. ${toolRole.role}: Lead developer with specialized expertise in ${toolRole.focus}. Gets tasks from Manager, implementing detailed architecture. Adept at best DX methodologies: DRY, SOLID, KISS, TDD. ${toolRole.role} speaks to Manager, QA and Engineer. See "FULL ${toolRole.role.toUpperCase()} DESCRIPTION" section below.
3. QA: Gets technical description from ${toolRole.role} and implements unit tests covering common states, edge cases, potential bottlenecks and invalid data.
4. Engineer: Senior L6 Google developer implements code per ${toolRole.role} instructions and QA test files. Can consult ${toolRole.role} and QA to clarify ambiguous information, request test updates if interface changes needed, and must get ${toolRole.role} approval to deviate from provided instructions and tests.
Working flow (MUST FOLLOW):
Manager -> ${toolRole.role} -> QA -> Engineer -> QA -> ${toolRole.role} -> Manager
FULL ${toolRole.role.toUpperCase()} DESCRIPTION:
~~~~~~
You are an expert coding assistant in languages like Markdown, JavaScript, HTML, CSS, Python, and Node.js. Your goal is to provide concise, clear, readable, efficient, and bug-free code solutions that follow best practices and modern standards.
As a ${toolRole.role}, you specialize in ${toolRole.focus} and bring that expertise to every solution.
When debugging, consider 5-7 possible problem sources, identify the 1-2 most likely causes, and add logs to validate your assumptions before implementing fixes.
1. Analyze the code and question:
In <code_analysis> tags:
- Identify the programming language used
- Assess the difficulty level of the task (Easy, Medium, or Hard)
- Identify key components or functions in the existing code
- Quote relevant parts of the existing code that relate to the user's question
- Provide a brief summary of what the existing code does
- Break down the problem into smaller components
- Consider potential best practices and optimizations
- Create a Mermaid diagram to visualize the solution structure
2. Plan your approach:
In <solution_plan> tags:
Write detailed, numbered pseudocode outlining your solution strategy. Include comments explaining the reasoning behind each step. It's OK for this section to be quite long.
3. Confirm your understanding:
Briefly restate the problem and your planned approach to ensure you've correctly interpreted the user's needs.
4. Implement the solution:
Provide your code implementation, adhering to the following principles:
- Write bug-free, secure, and efficient code
- Prioritize readability and maintainability
- Implement all required functionality completely
- Avoid placeholders
- Be concise while maintaining clarity
- Use the latest relevant technologies and best practices
5. Verify the solution:
Explain how your implementation meets the requirements and addresses the user's question.
6. Consider improvements:
Briefly discuss any potential optimizations or alternative approaches, if applicable.
Please format your response as follows:
<difficulty_level>[Easy/Medium/Hard]</difficulty_level>
<code_analysis>
[Your detailed analysis, including the Mermaid diagram]
</code_analysis>
<solution_plan>
[Your detailed, numbered pseudocode with comments]
</solution_plan>
Confirmation: [Your understanding of the problem and approach]
Code:
\`\`\`[language]
// [Filename (if applicable)]
[Your implemented code]
\`\`\`
Verification: [Explanation of how the solution meets the requirements]
Potential Improvements: [Brief discussion of optimizations or alternatives]
~~~~~~`;
}
class SmartAdvisorError extends Error {
code;
cause;
constructor(message, code, cause) {
super(message);
this.code = code;
this.cause = cause;
this.name = 'SmartAdvisorError';
}
}
class CircuitBreaker {
metrics;
config;
logger;
halfOpenCalls = 0;
constructor(config, logger, providerName) {
this.config = config;
this.logger = new Logger(`CircuitBreaker-${providerName}`);
this.metrics = {
failures: 0,
successes: 0,
state: CircuitBreakerState.CLOSED,
lastFailureTime: 0,
consecutiveFailures: 0,
totalRequests: 0
};
}
async execute(operation) {
this.metrics.totalRequests++;
if (this.metrics.state === CircuitBreakerState.OPEN) {
if (this.shouldAttemptReset()) {
this.metrics.state = CircuitBreakerState.HALF_OPEN;
this.halfOpenCalls = 0;
this.logger.info('Circuit breaker transitioning to HALF_OPEN state');
}
else {
throw new SmartAdvisorError('Circuit breaker is OPEN - provider temporarily unavailable', 'CIRCUIT_BREAKER_OPEN');
}
}
if (this.metrics.state === CircuitBreakerState.HALF_OPEN) {
if (this.halfOpenCalls >= this.config.halfOpenMaxCalls) {
throw new SmartAdvisorError('Circuit breaker HALF_OPEN call limit exceeded', 'CIRCUIT_BREAKER_HALF_OPEN_LIMIT');
}
this.halfOpenCalls++;
}
try {
const result = await operation();
this.onSuccess();
return result;
}
catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.metrics.successes++;
this.metrics.consecutiveFailures = 0;
if (this.metrics.state === CircuitBreakerState.HALF_OPEN) {
this.metrics.state = CircuitBreakerState.CLOSED;
this.halfOpenCalls = 0;
this.logger.info('Circuit breaker reset to CLOSED state after successful recovery');
}
}
onFailure() {
this.metrics.failures++;
this.metrics.consecutiveFailures++;
this.metrics.lastFailureTime = Date.now();
if (this.metrics.state === CircuitBreakerState.HALF_OPEN) {
this.metrics.state = CircuitBreakerState.OPEN;
this.halfOpenCalls = 0;
this.logger.warn('Circuit breaker opened due to failure in HALF_OPEN state');
}
else if (this.metrics.consecutiveFailures >= this.config.failureThreshold) {
this.metrics.state = CircuitBreakerState.OPEN;
this.logger.warn(`Circuit breaker opened after ${this.metrics.consecutiveFailures} consecutive failures`);
}
}
shouldAttemptReset() {
return Date.now() - this.metrics.lastFailureTime >= this.config.recoveryTimeout;
}
getMetrics() {
return { ...this.metrics };
}
getState() {
return this.metrics.state;
}
reset() {
this.metrics.state = CircuitBreakerState.CLOSED;
this.metrics.consecutiveFailures = 0;
this.metrics.failures = 0;
this.metrics.successes = 0;
this.halfOpenCalls = 0;
this.logger.info('Circuit breaker manually reset');
}
}
export class SmartAdvisorServer {
server;
config;
requestCache = new Map();
cacheMetrics = { hits: 0, misses: 0, evictions: 0, totalRequests: 0, hitRate: 0 };
logger = new Logger('SmartAdvisorServer');
rateLimitTracker = new Map();
startTime = Date.now();
circuitBreakers = new Map();
constructor() {
this.config = this.loadConfig();
this.logger.info('SmartAdvisorServer initializing', {
maxCacheSize: this.config.maxCacheSize,
cacheTtl: this.config.cacheTtl,
circuitBreakerConfig: this.config.circuitBreaker
});
this.server = new Server({
name: 'smart-advisor',
version: '1.5.3',
}, {
capabilities: {
tools: {},
},
});
this.initializeCircuitBreakers();
this.setupToolHandlers();
this.logger.info('SmartAdvisorServer initialized successfully');
}
initializeCircuitBreakers() {
// Initialize circuit breakers for each AI provider
const providers = Object.keys(MODELS).filter(k => k !== 'router');
providers.forEach(provider => {
const circuitBreaker = new CircuitBreaker(this.config.circuitBreaker, this.logger, provider);
this.circuitBreakers.set(provider, circuitBreaker);
});
this.logger.info('Circuit breakers initialized', {
providers: providers,
config: this.config.circuitBreaker
});
}
loadConfig() {
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
throw new SmartAdvisorError('OPENROUTER_API_KEY environment variable is required', 'MISSING_API_KEY');
}
return {
openrouterApiKey: apiKey,
maxRetries: parseInt(process.env.MAX_RETRIES || '3', 10),
requestTimeout: parseInt(process.env.REQUEST_TIMEOUT || '30000', 10),
cacheTtl: parseInt(process.env.CACHE_TTL || '300000', 10), // 5 minutes
maxTokens: parseInt(process.env.MAX_TOKENS || '4000', 10),
maxCacheSize: parseInt(process.env.MAX_CACHE_SIZE || '100', 10),
maxTaskLength: parseInt(process.env.MAX_TASK_LENGTH || '10000', 10),
maxContextLength: parseInt(process.env.MAX_CONTEXT_LENGTH || '20000', 10),
rateLimitRequests: parseInt(process.env.RATE_LIMIT_REQUESTS || '10', 10),
rateLimitWindow: parseInt(process.env.RATE_LIMIT_WINDOW || '60000', 10), // 1 minute
circuitBreaker: {
failureThreshold: parseInt(process.env.CIRCUIT_BREAKER_FAILURE_THRESHOLD || '5', 10),
recoveryTimeout: parseInt(process.env.CIRCUIT_BREAKER_RECOVERY_TIMEOUT || '60000', 10), // 1 minute
monitoringPeriod: parseInt(process.env.CIRCUIT_BREAKER_MONITORING_PERIOD || '300000', 10), // 5 minutes
halfOpenMaxCalls: parseInt(process.env.CIRCUIT_BREAKER_HALF_OPEN_MAX_CALLS || '3', 10)
}
};
}
validateInput(task, context) {
// Input length validation
if (task.length > this.config.maxTaskLength) {
return {
isValid: false,
error: `Task exceeds maximum length of ${this.config.maxTaskLength} characters`
};
}
if (context.length > this.config.maxContextLength) {
return {
isValid: false,
error: `Context exceeds maximum length of ${this.config.maxContextLength} characters`
};
}
// Enhanced security validation - check for injection patterns
const scriptInjectionPatterns = [
/<script[^>]*>/i,
/javascript:/i,
/on\w+\s*=/i,
/data:text\/html/i,
/vbscript:/i
];
// Prompt injection patterns
const promptInjectionPatterns = [
/ignore\s+(previous|above|all|the)\s+(instructions?|prompts?|rules?)/i,
/forget\s+(everything|all|previous)/i,
/system\s*[:]\s*you\s+are\s+now/i,
/act\s+as\s+if\s+you\s+are/i,
/pretend\s+(you\s+are|to\s+be)/i,
/roleplay\s+as/i,
/new\s+(instructions?|rules?|system\s+prompt)/i,
/disregard\s+(previous|all|above)/i,
/override\s+(instructions?|system|previous)/i,
/simulate\s+(being|you\s+are)/i,
/\[SYSTEM\]/i,
/\<\|system\|\>/i,
/```\s*system/i
];
const combinedInput = task + ' ' + context;
// Check for script injection
for (const pattern of scriptInjectionPatterns) {
if (pattern.test(combinedInput)) {
this.logger.warn('Script injection attempt detected', {
pattern: pattern.source,
inputLength: combinedInput.length
});
return {
isValid: false,
error: 'Input contains potentially malicious script content'
};
}
}
// Check for prompt injection
for (const pattern of promptInjectionPatterns) {
if (pattern.test(combinedInput)) {
this.logger.warn('Prompt injection attempt detected', {
pattern: pattern.source,
inputLength: combinedInput.length
});
return {
isValid: false,
error: 'Input contains potential prompt injection patterns'
};
}
}
return { isValid: true };
}
sanitizeInput(input) {
return input
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control characters
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
}
checkRateLimit(clientId = 'default') {
const now = Date.now();
const clientData = this.rateLimitTracker.get(clientId);
if (!clientData) {
this.rateLimitTracker.set(clientId, { count: 1, windowStart: now });
return true;
}
// Reset window if expired
if (now - clientData.windowStart > this.config.rateLimitWindow) {
this.rateLimitTracker.set(clientId, { count: 1, windowStart: now });
return true;
}
// Check if limit exceeded
if (clientData.count >= this.config.rateLimitRequests) {
this.logger.warn('Rate limit exceeded', {
clientId,
count: clientData.count,
limit: this.config.rateLimitRequests
});
return false;
}
// Increment counter
clientData.count++;
this.rateLimitTracker.set(clientId, clientData);
return true;
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return this.listTools();
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
return this.callTool(request.params.name, request.params.arguments);
});
}
async listTools() {
const inputSchema = {
type: 'object',
properties: {
model: {
type: 'string',
enum: [...Object.keys(ROUTING_STRATEGIES)],
description: 'Routing strategy: auto (smart routing), intelligence (claude), premium (o3), cost (deepseek), balance (gemini), speed (grok), random (random provider), all (multi-provider), or specific provider',
},
task: {
type: 'string',
description: 'The coding task or problem you need advice on',
},
context: {
type: 'string',
description: 'Additional context about your project or requirements (optional)',
},
},
required: ['model', 'task'],
};
return {
tools: [
{
name: 'smart_advisor',
description: 'Get coding advice from premium LLMs using the Smart Advisor prompt structure',
inputSchema,
},
{
name: 'code_review',
description: 'Review your code and provide expert feedback from premium AI models',
inputSchema,
},
{
name: 'get_advice',
description: 'Get coding advice and recommendations from AI experts',
inputSchema,
},
{
name: 'expert_opinion',
description: 'Get third-party expert consultation on your coding challenges',
inputSchema,
},
{
name: 'smart_llm',
description: 'Use advanced AI models for intelligent code analysis and suggestions',
inputSchema,
},
{
name: 'ask_expert',
description: 'Ask coding experts for their professional opinion and guidance',
inputSchema,
},
{
name: 'review_code',
description: 'Get comprehensive code review with detailed feedback and improvements',
inputSchema,
},
],
};
}
async callTool(name, args) {
this.logger.info('Tool call received', { tool: name, model: args?.model });
// Rate limiting check
if (!this.checkRateLimit()) {
throw new SmartAdvisorError(`Rate limit exceeded. Maximum ${this.config.rateLimitRequests} requests per ${this.config.rateLimitWindow / 1000} seconds`, 'RATE_LIMIT_EXCEEDED');
}
const validTools = ['smart_advisor', 'code_review', 'get_advice', 'expert_opinion', 'smart_llm', 'ask_expert', 'review_code'];
if (!validTools.includes(name)) {
this.logger.error('Unknown tool requested', { tool: name });
throw new SmartAdvisorError(`Unknown tool: ${name}`, 'UNKNOWN_TOOL');
}
const { model, task, context = '' } = args;
// Validate and sanitize inputs
const validation = this.validateInput(task, context);
if (!validation.isValid) {
this.logger.warn('Input validation failed', { error: validation.error });
throw new SmartAdvisorError(validation.error, 'INVALID_INPUT');
}
const sanitizedTask = this.sanitizeInput(task);
const sanitizedContext = this.sanitizeInput(context);
this.logger.debug('Input sanitized', {
originalTaskLength: task.length,
sanitizedTaskLength: sanitizedTask.length,
originalContextLength: context.length,
sanitizedContextLength: sanitizedContext.length
});
// Validate routing strategy
if (!Object.keys(ROUTING_STRATEGIES).includes(model)) {
throw new SmartAdvisorError(`Unknown routing strategy: ${model}. Available: ${Object.keys(ROUTING_STRATEGIES).join(', ')}`, 'UNKNOWN_STRATEGY');
}
// Route to optimal provider
const selectedProvider = await this.routeToOptimalProvider(sanitizedTask, sanitizedContext, model);
if (selectedProvider === 'all') {
return await this.consultAllAdvisors(sanitizedTask, sanitizedContext, name);
}
// Validate final provider selection (exclude 'router' from main providers)
const mainProviders = Object.keys(MODELS).filter(k => k !== 'router');
if (!mainProviders.includes(selectedProvider)) {
throw new SmartAdvisorError(`Invalid provider selection: ${selectedProvider}`, 'INVALID_PROVIDER');
}
const cacheKey = `${selectedProvider}:${sanitizedTask}:${sanitizedContext}`;
const cached = this.getCachedResponse(cacheKey);
if (cached) {
this.logger.info('Cache hit', {
strategy: model,
selectedProvider,
cacheKey: cacheKey.substring(0, 50) + '...'
});
return {
content: [
{
type: 'text',
text: cached,
},
],
};
}
this.logger.info('Cache miss, making API call', {
strategy: model,
selectedProvider,
reasoning: model === 'auto' ? 'AI-selected optimal provider' : 'Direct strategy selection'
});
try {
const response = await this.callOpenRouterWithRetry(MODELS[selectedProvider], sanitizedTask, sanitizedContext, name);
this.setCachedResponse(cacheKey, response);
return {
content: [
{
type: 'text',
text: response,
},
],
};
}
catch (error) {
if (error instanceof SmartAdvisorError) {
throw error;
}
throw new SmartAdvisorError(`OpenRouter API error: ${error instanceof Error ? error.message : 'Unknown error'}`, 'API_ERROR', error instanceof Error ? error : undefined);
}
}
async consultAllAdvisors(task, context, toolName = 'smart_advisor') {
const cacheKey = `all:${task}:${context}:${toolName}`;
const cached = this.getCachedResponse(cacheKey);
if (cached) {
return {
content: [
{
type: 'text',
text: cached,
},
],
};
}
const modelKeys = Object.keys(MODELS);
// Use Promise.allSettled for better error resilience
const advisorPromises = modelKeys.map(async (modelKey) => {
const startTime = Date.now();
try {
this.logger.debug('Starting advisor query', { model: modelKey, tool: toolName });
const response = await this.callOpenRouterWithRetry(MODELS[modelKey], task, context, toolName);
const duration = Date.now() - startTime;
this.logger.debug('Advisor query completed', {
model: modelKey,
duration: `${duration}ms`,
responseLength: response.length
});
return {
model: modelKey,
response,
success: true,
duration
};
}
catch (error) {
const duration = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.warn('Advisor query failed', {
model: modelKey,
error: errorMessage,
duration: `${duration}ms`
});
return {
model: modelKey,
error: errorMessage,
success: false,
duration
};
}
});
const settledResults = await Promise.allSettled(advisorPromises);
// Extract results from Promise.allSettled
const results = settledResults.map((settledResult, index) => {
if (settledResult.status === 'fulfilled') {
return settledResult.value;
}
else {
// This should rarely happen since we catch errors in the map function
const modelKey = modelKeys[index];
this.logger.error('Unexpected promise rejection', {
model: modelKey,
error: settledResult.reason
});
return {
model: modelKey,
error: 'Promise rejected unexpectedly',
success: false,
duration: 0
};
}
});
const formattedResponse = this.formatMultiAdvisorResponse(results);
this.setCachedResponse(cacheKey, formattedResponse);
return {
content: [
{
type: 'text',
text: formattedResponse,
},
],
};
}
getCachedResponse(key) {
this.cacheMetrics.totalRequests++;
const cached = this.requestCache.get(key);
if (cached && Date.now() - cached.timestamp < this.config.cacheTtl) {
// Cache hit
this.cacheMetrics.hits++;
this.updateCacheHitRate();
// Update access count and timestamp for LRU
cached.accessCount++;
this.requestCache.set(key, cached);
this.logger.debug('Cache hit', {
key: key.substring(0, 50) + '...',
hitRate: this.cacheMetrics.hitRate.toFixed(2) + '%'
});
return cached.response;
}
// Cache miss or expired
this.cacheMetrics.misses++;
this.updateCacheHitRate();
if (cached) {
// Remove expired entry
this.requestCache.delete(key);
this.logger.debug('Cache entry expired and removed', { key: key.substring(0, 50) + '...' });
}
return null;
}
setCachedResponse(key, response) {
// Implement LRU eviction if cache is full
if (this.requestCache.size >= this.config.maxCacheSize) {
this.evictLeastRecentlyUsed();
}
this.requestCache.set(key, {
response,
timestamp: Date.now(),
accessCount: 1,
});
}
evictLeastRecentlyUsed() {
let lruKey = null;
let lruTimestamp = Date.now();
let lruAccessCount = Infinity;
// Find the least recently used entry
for (const [key, entry] of this.requestCache.entries()) {
if (entry.timestamp < lruTimestamp ||
(entry.timestamp === lruTimestamp && entry.accessCount < lruAccessCount)) {
lruKey = key;
lruTimestamp = entry.timestamp;
lruAccessCount = entry.accessCount;
}
}
if (lruKey) {
this.cacheMetrics.evictions++;
this.logger.debug('Evicting LRU cache entry', {
key: lruKey.substring(0, 50) + '...',
accessCount: lruAccessCount,
age: Date.now() - lruTimestamp,
totalEvictions: this.cacheMetrics.evictions
});
this.requestCache.delete(lruKey);
}
}
updateCacheHitRate() {
if (this.cacheMetrics.totalRequests > 0) {
this.cacheMetrics.hitRate = (this.cacheMetrics.hits / this.cacheMetrics.totalRequests) * 100;
}
}
getCacheMetrics() {
return { ...this.cacheMetrics };
}
async routeToOptimalProvider(task, context, strategy) {
// Handle non-auto strategies
if (strategy === 'intelligence')
return 'claude'; // Ultimate intelligence
if (strategy === 'premium')
return 'openai'; // Premium alternative
if (strategy === 'cost')
return 'deepseek'; // Most cost-effective
if (strategy === 'balance')
return 'google'; // Balanced cost/performance
if (strategy === 'speed')
return 'xai'; // Fastest responses
if (strategy === 'all')
return 'all';
// Handle random strategy
if (strategy === 'random') {
const availableProviders = ['claude', 'openai', 'xai', 'google', 'deepseek', 'moonshot'];
const randomIndex = Math.floor(Math.random() * availableProviders.length);
const selectedProvider = availableProviders[randomIndex];
this.logger.debug('Random provider selection', {
selectedProvider,
availableProviders: availableProviders.length,
strategy: 'random'
});
return selectedProvider;
}
// Handle direct provider names
if (strategy === 'deepseek' || strategy === 'google' || strategy === 'openai' || strategy === 'xai' || strategy === 'claude' || strategy === 'moonshot') {
return strategy;
}
// For 'auto' strategy, use GPT-4o-mini to make routing decision
if (strategy === 'auto') {
try {
const routingPrompt = `You are a smart routing system that selects the best AI provider for a given coding task.
Available providers (ranked by intelligence):
1. Claude Sonnet 4: Ultimate intelligence, supreme reasoning, ethical coding, comprehensive solutions
2. OpenAI o3: Very high intelligence, complex reasoning, creativity, advanced coding
3. xAI Grok: Very high intelligence, fast responses, real-time data, creative thinking
4. Google Gemini Flash: Very high intelligence, fast, large context (2M tokens), multimodal
5. DeepSeek: High intelligence, very cost-effective, fast, excellent for coding/logic/math
Task: "${task}"
Context: "${context || 'None'}"
Respond with ONLY the provider name: "claude", "openai", "xai", "google", or "deepseek"
Consider:
- Task complexity (simple = deepseek, moderate = google/xai, complex = openai, ultimate = claude)
- Cost efficiency (prefer cheaper options when quality difference is minimal)
- Provider strengths vs task requirements
- Context length needs (long context = google)`;
const routingDecision = await this.callOpenRouter(MODELS.router, routingPrompt, '', 'auto');
const cleanDecision = routingDecision.toLowerCase().trim();
// Validate the routing decision
if (['claude', 'openai', 'xai', 'google', 'deepseek'].includes(cleanDecision)) {
this.logger.debug('Auto-routing decision', {
task: task.substring(0, 50) + '...',
selectedProvider: cleanDecision,
strategy: 'auto'
});
return cleanDecision;
}
else {
this.logger.warn('Invalid routing decision, falling back to balance', {
decision: routingDecision,
fallback: 'google'
});
return 'google'; // Safe fallback to Gemini Flash
}
}
catch (error) {
this.logger.error('Routing decision failed, falling back to balance', {
error: error instanceof Error ? error.message : 'Unknown error',
fallback: 'google'
});
return 'google'; // Safe fallback
}
}
// Default fallback
return 'google';
}
getHealthCheck() {
const now = Date.now();
const cacheSize = this.requestCache.size;
const hitRate = this.cacheMetrics.hitRate;
// Collect circuit breaker status
const circuitBreakerStatus = {};
let openCircuitBreakers = 0;
for (const [provider, cb] of this.circuitBreakers.entries()) {
const metrics = cb.getMetrics();
const successRate = metrics.totalRequests > 0
? ((metrics.totalRequests - metrics.failures) / metrics.totalRequests) * 100
: 100;
circuitBreakerStatus[provider] = {
state: metrics.state,
failures: metrics.failures,
successRate: Number(successRate.toFixed(2))
};
if (metrics.state === CircuitBreakerState.OPEN) {
openCircuitBreakers++;
}
}
// Determine health status
let status = 'healthy';
// Mark as unhealthy if more than half of circuit breakers are open
if (openCircuitBreakers > this.circuitBreakers.size / 2) {
status = 'unhealthy';
}
else if (openCircuitBreakers > 0) {
status = 'degraded';
}
// Mark as degraded if cache hit rate is very low (might indicate issues)
if (this.cacheMetrics.totalRequests > 10 && hitRate < 10) {
status = 'degraded';
}
// Mark as degraded if cache is at maximum capacity
if (cacheSize >= this.config.maxCacheSize) {
status = 'degraded';
}
return {
status,
timestamp: new Date().toISOString(),
uptime: now - this.startTime,
cache: {
size: cacheSize,
hitRate: Number(hitRate.toFixed(2)),
evictions: this.cacheMetrics.evictions
},
rateLimit: {
activeWindows: this.rateLimitTracker.size
},
circuitBreakers: circuitBreakerStatus,
version: '1.5.3'
};
}
getCircuitBreakerMetrics() {
const metrics = {};
for (const [provider, cb] of this.circuitBreakers.entries()) {
metrics[provider] = cb.getMetrics();
}
return metrics;
}
resetCircuitBreaker(provider) {
const circuitBreaker = this.circuitBreakers.get(provider);
if (circuitBreaker) {
circuitBreaker.reset();
this.logger.info('Circuit breaker manually reset', { provider });
return true;
}
return false;
}
resetAllCircuitBreakers() {
for (const [provider, cb] of this.circuitBreakers.entries()) {
cb.reset();
}
this.logger.info('All circuit breakers manually reset');
}
async callOpenRouterWithRetry(model, task, context, toolName = 'smart_advisor') {
// Extract provider name from model string
const provider = this.getProviderFromModel(model);
const circuitBreaker = this.circuitBreakers.get(provider);
if (!circuitBreaker) {
this.logger.warn('No circuit breaker found for provider, proceeding without circuit breaker', { provider, model });
return this.callOpenRouterWithRetryFallback(model, task, context, toolName);
}
try {
return await circuitBreaker.execute(() => this.callOpenRouterWithRetryFallback(model, task, context, toolName));
}
catch (error) {
if (error instanceof SmartAdvisorError && error.code.startsWith('CIRCUIT_BREAKER')) {
this.logger.warn('Circuit breaker rejected request', {
provider,
model,
state: circuitBreaker.getState(),
error: error.message
});
// Attempt fallback to alternative provider if available
return this.attemptFallbackProvider(task, context, toolName, provider);
}
throw error;
}
}
getProviderFromModel(model) {
// Map model strings to provider keys
for (const [provider, modelString] of Object.entries(MODELS)) {
if (modelString === model) {
return provider;
}
}
// Fallback: try to extract provider from model string
if (model.includes('claude'))
return 'claude';
if (model.includes('openai') || model.includes('gpt') || model.includes('o3'))
return 'openai';
if (model.includes('google') || model.includes('gemini'))
return 'google';
if (model.includes('x-ai') || model.includes('grok'))
return 'xai';
if (model.includes('deepseek'))
return 'deepseek';
return 'unknown';
}
async attemptFallbackProvider(task, context, toolName, failedProvider) {
// Define fallback hierarchy based on provider capabilities
const fallbackOrder = ['google', 'claude', 'xai', 'moonshot', 'deepseek', 'openai'];
const availableProviders = fallbackOrder.filter(p => {
const cb = this.circuitBreakers.get(p);
return p !== failedProvider && cb && cb.getState() !== CircuitBreakerState.OPEN;
});
if (availableProviders.length === 0) {
throw new SmartAdvisorError(`All providers unavailable. Primary provider '${failedProvider}' circuit breaker is open and no fallback providers available.`, 'ALL_PROVIDERS_UNAVAILABLE');
}
const fallbackProvider = availableProviders[0];
this.logger.info('Attempting fallback provider', {
failedProvider,
fallbackProvider,
availableProviders
});
const fallbackModel = MODELS[fallbackProvider];
return this.callOpenRouterWithRetryFallback(fallbackModel, task, context, toolName);
}
async callOpenRouterWithRetryFallback(model, task, context, toolName = 'smart_advisor') {
let lastError = null;
for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
try {
return await this.callOpenRouter(model, task, context, toolName);
}
catch (error) {
lastError = error instanceof Error ? error : new Error('Unknown error');
if (error instanceof AxiosError) {
// Don't retry on client errors (4xx)
if (error.response?.status && error.response.status >= 400 && error.response.status < 500) {
throw new SmartAdvisorError(`OpenRouter client error: ${error.response.status} ${error.response.statusText}`, 'CLIENT_ERROR', error);
}
}
// Wait before retry (exponential backoff)
if (attempt < this.config.maxRetries - 1) {
await this.delay(Math.pow(2, attempt) * 1000);
}
}
}
throw new SmartAdvisorError(`Failed after ${this.config.maxRetries} attempts: ${lastError?.message}`, 'MAX_RETRIES_EXCEEDED', lastError || undefined);
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async callOpenRouter(model, task, context, toolName = 'smart_advisor') {
const userMessage = context
? `Task: ${task}\n\nAdditional Context: ${context}`
: `Task: ${task}`;
const systemPrompt = buildToolSpecificPrompt(toolName);
const response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model,
messages: [
{
role: 'system',
content: systemPrompt,
},
{
role: 'user',
content: userMessage,
},
],
temperature: 0.7,
max_tokens: this.config.maxTokens,
}, {
headers: {
'Authorization': `Bearer ${this.config.openrouterApiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://github.com/user/mcp-smart-advisor',
'X-Title': 'MCP Smart Advisor',
},
timeout: this.config.requestTimeout,
});
return response.data.choices[0]?.message?.content || 'No response received';
}
formatMultiAdvisorResponse(results) {
const successfulResults = results.filter(r => r.success);
const failedResults = results.filter(r => !r.success);
let formatted = `# 🎯 Multi-Advisor Consultation Results
**What you're seeing:** Three experienced AI advisors have independently analyzed your request. Consider their perspectives to find the most practical and efficient solution.
`;
if (failedResults.length > 0) {
formatted += `⚠️ **Note:** ${fail