Skip to main content
Glama
server.ts25 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { ConfigManager } from './config/config.js'; import { ProviderManager } from './providers/manager.js'; import { EnhancedProviderManager } from './providers/enhanced-manager.js'; import { ConversationManager } from './services/conversation.js'; import { ResponseCache } from './services/cache.js'; import { HealthMonitor } from './services/health.js'; import { MCPClientManager } from './services/mcp-client-manager.js'; import { DuckResponse } from './config/types.js'; import { ApprovalService } from './services/approval.js'; import { FunctionBridge } from './services/function-bridge.js'; import { logger } from './utils/logger.js'; import { duckArt, getRandomDuckMessage } from './utils/ascii-art.js'; // Import tools import { askDuckTool } from './tools/ask-duck.js'; import { chatDuckTool } from './tools/chat-duck.js'; import { clearConversationsTool } from './tools/clear-conversations.js'; import { listDucksTool } from './tools/list-ducks.js'; import { listModelsTool } from './tools/list-models.js'; import { compareDucksTool } from './tools/compare-ducks.js'; import { duckCouncilTool } from './tools/duck-council.js'; import { duckVoteTool } from './tools/duck-vote.js'; import { duckJudgeTool } from './tools/duck-judge.js'; import { duckIterateTool } from './tools/duck-iterate.js'; import { duckDebateTool } from './tools/duck-debate.js'; // Import MCP tools import { getPendingApprovalsTool } from './tools/get-pending-approvals.js'; import { approveMCPRequestTool } from './tools/approve-mcp-request.js'; import { mcpStatusTool } from './tools/mcp-status.js'; export class RubberDuckServer { private server: Server; private configManager: ConfigManager; private providerManager: ProviderManager; private enhancedProviderManager?: EnhancedProviderManager; private conversationManager: ConversationManager; private cache: ResponseCache; private healthMonitor: HealthMonitor; // MCP Bridge components private mcpClientManager?: MCPClientManager; private approvalService?: ApprovalService; private functionBridge?: FunctionBridge; private mcpEnabled: boolean = false; constructor() { this.server = new Server( { name: 'mcp-rubber-duck', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // Initialize managers this.configManager = new ConfigManager(); this.providerManager = new ProviderManager(this.configManager); this.conversationManager = new ConversationManager(); this.cache = new ResponseCache(this.configManager.getConfig().cache_ttl); this.healthMonitor = new HealthMonitor(this.providerManager); // Initialize MCP bridge if enabled this.initializeMCPBridge(); this.setupHandlers(); } private initializeMCPBridge(): void { const config = this.configManager.getConfig(); const mcpConfig = config.mcp_bridge; if (!mcpConfig?.enabled) { logger.info('MCP bridge disabled in configuration'); return; } try { logger.info('Initializing MCP bridge...'); // Initialize MCP client manager this.mcpClientManager = new MCPClientManager(mcpConfig.mcp_servers); // Initialize approval service this.approvalService = new ApprovalService(mcpConfig.approval_timeout); // Initialize function bridge this.functionBridge = new FunctionBridge( this.mcpClientManager, this.approvalService, mcpConfig.trusted_tools, mcpConfig.approval_mode, mcpConfig.trusted_tools_by_server || {} ); // Initialize enhanced provider manager this.enhancedProviderManager = new EnhancedProviderManager( this.configManager, this.functionBridge ); this.mcpEnabled = true; logger.info('MCP bridge initialized successfully'); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error('Failed to initialize MCP bridge:', errorMessage); logger.warn('Falling back to regular duck provider functionality'); this.mcpEnabled = false; } } private setupHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, () => { return { tools: this.getTools() }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'ask_duck': // Use enhanced provider manager if MCP is enabled if (this.mcpEnabled && this.enhancedProviderManager) { return await this.handleAskDuckWithMCP(args || {}); } return await askDuckTool(this.providerManager, this.cache, args || {}); case 'chat_with_duck': return await chatDuckTool(this.providerManager, this.conversationManager, args || {}); case 'clear_conversations': return clearConversationsTool(this.conversationManager, args || {}); case 'list_ducks': return await listDucksTool(this.providerManager, this.healthMonitor, args || {}); case 'list_models': return await listModelsTool(this.providerManager, args || {}); case 'compare_ducks': // Use enhanced provider manager if MCP is enabled if (this.mcpEnabled && this.enhancedProviderManager) { return await this.handleCompareDucksWithMCP(args || {}); } return await compareDucksTool(this.providerManager, this.cache, args || {}); case 'duck_council': // Use enhanced provider manager if MCP is enabled if (this.mcpEnabled && this.enhancedProviderManager) { return await this.handleDuckCouncilWithMCP(args || {}); } return await duckCouncilTool(this.providerManager, args || {}); case 'duck_vote': return await duckVoteTool(this.providerManager, args || {}); case 'duck_judge': return await duckJudgeTool(this.providerManager, args || {}); case 'duck_iterate': return await duckIterateTool(this.providerManager, args || {}); case 'duck_debate': return await duckDebateTool(this.providerManager, args || {}); // MCP-specific tools case 'get_pending_approvals': if (!this.approvalService) { throw new Error('MCP bridge not enabled'); } return getPendingApprovalsTool(this.approvalService, args || {}); case 'approve_mcp_request': if (!this.approvalService) { throw new Error('MCP bridge not enabled'); } return approveMCPRequestTool(this.approvalService, args || {}); case 'mcp_status': if (!this.mcpClientManager || !this.approvalService || !this.functionBridge) { throw new Error('MCP bridge not enabled'); } return await mcpStatusTool( this.mcpClientManager, this.approvalService, this.functionBridge, args || {} ); default: throw new Error(`Unknown tool: ${name}`); } } catch (error: unknown) { logger.error(`Tool execution error for ${name}:`, error); const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `${getRandomDuckMessage('error')}\n\nError: ${errorMessage}`, }, ], isError: true, }; } }); // Handle errors this.server.onerror = (error) => { logger.error('Server error:', error); }; } // MCP-enhanced tool handlers private async handleAskDuckWithMCP(args: Record<string, unknown>) { if (!this.enhancedProviderManager || !this.cache) { throw new Error('Enhanced provider manager not available'); } const { prompt, provider, model, temperature } = args as { prompt?: string; provider?: string; model?: string; temperature?: number; }; if (!prompt) { throw new Error('Prompt is required'); } // Generate cache key (same as regular ask_duck) const cacheKey = this.cache.generateKey(provider || 'default', prompt, { model, temperature }); // Try to get cached response const { value: response, cached } = await this.cache.getOrSet(cacheKey, async () => { return await this.enhancedProviderManager!.askDuckWithMCP(provider, prompt, { model, temperature, }); }); // Format the response with MCP information const formattedResponse = this.formatEnhancedDuckResponse(response, cached); return { content: [ { type: 'text', text: formattedResponse, }, ], }; } private async handleCompareDucksWithMCP(args: Record<string, unknown>) { if (!this.enhancedProviderManager) { throw new Error('Enhanced provider manager not available'); } const { prompt, providers, model } = args as { prompt: string; providers?: string[]; model?: string; }; const responses = await this.enhancedProviderManager.compareDucksWithMCP(prompt, providers, { model, }); const formattedResponse = responses .map((response) => this.formatEnhancedDuckResponse(response)) .join('\n\n═══════════════════════════════════════\n\n'); return { content: [ { type: 'text', text: formattedResponse, }, ], }; } private async handleDuckCouncilWithMCP(args: Record<string, unknown>) { if (!this.enhancedProviderManager) { throw new Error('Enhanced provider manager not available'); } const { prompt, model } = args as { prompt: string; model?: string }; const responses = await this.enhancedProviderManager.duckCouncilWithMCP(prompt, { model }); const header = '🦆 Duck Council in Session 🦆\n============================='; const formattedResponse = responses .map((response) => this.formatEnhancedDuckResponse(response)) .join('\n\n═══════════════════════════════════════\n\n'); return { content: [ { type: 'text', text: `${header}\n\n${formattedResponse}`, }, ], }; } private formatEnhancedDuckResponse( response: DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: unknown[]; }, cached?: boolean ): string { let formatted = `🦆 **${response.nickname}** (${response.provider})\n─────────────────────────────────────\n${response.content}`; // Add pending approvals if any if (response.pendingApprovals && response.pendingApprovals.length > 0) { formatted += '\n\n⏳ **Pending Approvals:**'; response.pendingApprovals.forEach((approval) => { formatted += `\n- ${approval.message} (ID: ${approval.id})`; }); formatted += '\n\n💡 Use `get_pending_approvals` and `approve_mcp_request` to manage approvals'; } // Add MCP results indicator if (response.mcpResults && response.mcpResults.length > 0) { formatted += '\n\n🔧 **Used MCP Tools:** ' + response.mcpResults.length + ' tool call(s)'; } // Add model and performance info formatted += `\n\n📍 Model: ${response.model}`; if (response.usage) { formatted += ` | 📊 Tokens: ${response.usage.total_tokens}`; } formatted += ` | ⏱️ ${response.latency}ms`; if (cached) { formatted += ' | 💾 Cached'; } else if (response.mcpResults) { formatted += ' | 🔄 MCP Enhanced'; } else { formatted += ' | 🔄 Fresh'; } return formatted; } private getTools(): Tool[] { const baseTools: Tool[] = [ { name: 'ask_duck', description: this.mcpEnabled ? 'Ask a question to a specific LLM provider (duck) with MCP tool access' : 'Ask a question to a specific LLM provider (duck)', inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'The question or prompt to send to the duck', }, provider: { type: 'string', description: 'The provider name (optional, uses default if not specified)', }, model: { type: 'string', description: 'Specific model to use (optional, uses provider default if not specified)', }, temperature: { type: 'number', description: 'Temperature for response generation (0-2)', minimum: 0, maximum: 2, }, }, required: ['prompt'], }, }, { name: 'chat_with_duck', description: 'Have a conversation with a duck, maintaining context across messages', inputSchema: { type: 'object', properties: { conversation_id: { type: 'string', description: 'Conversation ID (creates new if not exists)', }, message: { type: 'string', description: 'Your message to the duck', }, provider: { type: 'string', description: 'Provider to use (can switch mid-conversation)', }, model: { type: 'string', description: 'Specific model to use (optional)', }, }, required: ['conversation_id', 'message'], }, }, { name: 'clear_conversations', description: 'Clear all conversation history and start fresh', inputSchema: { type: 'object', properties: {}, }, }, { name: 'list_ducks', description: 'List all available LLM providers (ducks) and their status', inputSchema: { type: 'object', properties: { check_health: { type: 'boolean', description: 'Perform health check on all providers', default: false, }, }, }, }, { name: 'list_models', description: 'List available models for LLM providers', inputSchema: { type: 'object', properties: { provider: { type: 'string', description: 'Provider name (optional, lists all if not specified)', }, fetch_latest: { type: 'boolean', description: 'Fetch latest models from API vs using cached/configured', default: false, }, }, }, }, { name: 'compare_ducks', description: 'Ask the same question to multiple ducks simultaneously', inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'The question to ask all ducks', }, providers: { type: 'array', items: { type: 'string', }, description: 'List of provider names to query (optional, uses all if not specified)', }, model: { type: 'string', description: 'Specific model to use for all providers (optional)', }, }, required: ['prompt'], }, }, { name: 'duck_council', description: 'Get responses from all configured ducks (like a panel discussion)', inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'The question for the duck council', }, model: { type: 'string', description: 'Specific model to use for all ducks (optional)', }, }, required: ['prompt'], }, }, { name: 'duck_vote', description: 'Have multiple ducks vote on options with reasoning. Returns vote tally, confidence scores, and consensus level.', inputSchema: { type: 'object', properties: { question: { type: 'string', description: 'The question to vote on (e.g., "Best approach for error handling?")', }, options: { type: 'array', items: { type: 'string' }, minItems: 2, maxItems: 10, description: 'The options to vote on (2-10 options)', }, voters: { type: 'array', items: { type: 'string' }, description: 'List of provider names to vote (optional, uses all if not specified)', }, require_reasoning: { type: 'boolean', default: true, description: 'Require ducks to explain their vote (default: true)', }, }, required: ['question', 'options'], }, }, { name: 'duck_judge', description: 'Have one duck evaluate and rank other ducks\' responses. Use after duck_council to get a comparative evaluation.', inputSchema: { type: 'object', properties: { responses: { type: 'array', items: { type: 'object', properties: { provider: { type: 'string' }, nickname: { type: 'string' }, model: { type: 'string' }, content: { type: 'string' }, }, required: ['provider', 'nickname', 'content'], }, minItems: 2, description: 'Array of duck responses to evaluate (from duck_council output)', }, judge: { type: 'string', description: 'Provider name of the judge duck (optional, uses first available)', }, criteria: { type: 'array', items: { type: 'string' }, description: 'Evaluation criteria (default: ["accuracy", "completeness", "clarity"])', }, persona: { type: 'string', description: 'Judge persona (e.g., "senior engineer", "security expert")', }, }, required: ['responses'], }, }, { name: 'duck_iterate', description: 'Iteratively refine a response between two ducks. One generates, the other critiques/improves, alternating for multiple rounds.', inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'The initial prompt/task to iterate on', }, iterations: { type: 'number', minimum: 1, maximum: 10, default: 3, description: 'Number of iteration rounds (default: 3, max: 10)', }, providers: { type: 'array', items: { type: 'string' }, minItems: 2, maxItems: 2, description: 'Exactly 2 provider names for the ping-pong iteration', }, mode: { type: 'string', enum: ['refine', 'critique-improve'], description: 'refine: each duck improves the previous response. critique-improve: alternates between critiquing and improving.', }, }, required: ['prompt', 'providers', 'mode'], }, }, { name: 'duck_debate', description: 'Structured multi-round debate between ducks. Supports oxford (pro/con), socratic (questioning), and adversarial (attack/defend) formats.', inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'The debate topic or proposition', }, rounds: { type: 'number', minimum: 1, maximum: 10, default: 3, description: 'Number of debate rounds (default: 3)', }, providers: { type: 'array', items: { type: 'string' }, minItems: 2, description: 'Provider names to participate (min 2, uses all if not specified)', }, format: { type: 'string', enum: ['oxford', 'socratic', 'adversarial'], description: 'Debate format: oxford (pro/con), socratic (questioning), adversarial (attack/defend)', }, synthesizer: { type: 'string', description: 'Provider to synthesize the debate (optional, uses first provider)', }, }, required: ['prompt', 'format'], }, }, ]; // Add MCP-specific tools if enabled if (this.mcpEnabled) { baseTools.push( { name: 'get_pending_approvals', description: 'Get list of pending MCP tool approvals from ducks', inputSchema: { type: 'object', properties: { duck: { type: 'string', description: 'Filter by duck name (optional)', }, }, }, }, { name: 'approve_mcp_request', description: "Approve or deny a duck's MCP tool request", inputSchema: { type: 'object', properties: { approval_id: { type: 'string', description: 'The approval request ID', }, decision: { type: 'string', enum: ['approve', 'deny'], description: 'Whether to approve or deny the request', }, reason: { type: 'string', description: 'Reason for denial (optional)', }, }, required: ['approval_id', 'decision'], }, }, { name: 'mcp_status', description: 'Get status of MCP bridge, servers, and pending approvals', inputSchema: { type: 'object', properties: {}, }, } ); } return baseTools; } async start() { // Only show welcome message when not running as MCP server const isMCP = process.env.MCP_SERVER === 'true' || process.argv.includes('--mcp'); if (!isMCP) { // eslint-disable-next-line no-console console.log(duckArt.welcome); // eslint-disable-next-line no-console console.log('\n' + getRandomDuckMessage('startup')); } // Initialize MCP connections if enabled if (this.mcpEnabled && this.mcpClientManager) { try { logger.info('Connecting to MCP servers...'); await this.mcpClientManager.initialize(); logger.info('MCP servers connected successfully'); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error('Failed to connect to some MCP servers:', errorMessage); logger.warn('Some MCP functionality may not be available'); } } // Start the server const transport = new StdioServerTransport(); await this.server.connect(transport); if (this.mcpEnabled) { logger.info('🦆 MCP Rubber Duck server with MCP bridge started successfully!'); } else { logger.info('🦆 MCP Rubber Duck server started successfully!'); } } async stop() { // Cleanup MCP resources if (this.approvalService) { this.approvalService.shutdown(); } if (this.mcpClientManager) { await this.mcpClientManager.disconnectAll(); } // Stop the server await this.server.close(); logger.info('Server stopped'); } }

Latest Blog Posts

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/nesquikm/mcp-rubber-duck'

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