UNPKG

second-opinion

Version:

Get a second opinion on your code from AI - MCP tool for Cursor IDE

281 lines (241 loc) • 9.35 kB
#!/usr/bin/env node /** * Second Opinion MCP Server * Provides AI second opinions through Cursor/Claude Desktop MCP integration */ const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } = require('@modelcontextprotocol/sdk/types.js'); // Configuration const API_URL = process.env.SECOND_OPINION_API_URL || 'https://secondopinion.codes/api/second-opinion'; const AUTH_TOKEN = process.env.SECOND_OPINION_TOKEN; const TRIAL_TOKEN = process.env.TRIAL_TOKEN; // Determine which token to use const activeToken = TRIAL_TOKEN || AUTH_TOKEN; const isTrialToken = !!TRIAL_TOKEN; console.error(`[Second Opinion] Starting MCP server`); console.error(`[Second Opinion] API URL: ${API_URL}`); console.error(`[Second Opinion] Token type: ${isTrialToken ? 'trial' : 'auth'}`); console.error(`[Second Opinion] Token: ${activeToken ? activeToken.substring(0, 15) + '...' : 'none'}`); if (!activeToken) { console.error('[Second Opinion] ERROR: No authentication token provided'); console.error('[Second Opinion] Please set SECOND_OPINION_TOKEN or TRIAL_TOKEN environment variable'); process.exit(1); } // Create MCP server const server = new Server( { name: 'second-opinion', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // Tool definitions const TOOLS = [ { name: 'ask_second_opinion', description: 'Get a second opinion from an AI assistant on code, architecture, or development questions', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Your question or code that you want a second opinion on', }, context: { type: 'string', description: 'Current conversation context - what you and the user have been discussing', }, task_type: { type: 'string', description: 'Type of task', enum: ['small scoped changes', 'general', 'larger refactors', 'codebase navigation/search', 'planning or problem-solving', 'complex bugs or deep reasoning', 'coding', 'visual reasoning'], default: 'general', }, }, required: ['query', 'context'], }, }, ]; // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: TOOLS, }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'ask_second_opinion') { try { const { query, context, task_type = 'general' } = args; if (!query || typeof query !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Query is required and must be a string'); } if (!context || typeof context !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Context is required and must be a string - please provide the current conversation context'); } // Ensure context is meaningful (not just whitespace) const finalContext = context.trim(); if (!finalContext) { throw new McpError(ErrorCode.InvalidParams, 'Context cannot be empty - please provide what you and the user have been discussing'); } console.error(`[Second Opinion] Processing request: ${query.substring(0, 100)}...`); // Prepare request payload const payload = { user_query: query, current_conversation_context: finalContext, target_task_type: task_type, }; // Set up authentication headers const headers = { 'Content-Type': 'application/json', 'User-Agent': 'Second-Opinion-MCP/1.0.0', }; if (isTrialToken) { headers['X-Trial-Token'] = activeToken; } else { headers['Authorization'] = `Bearer ${activeToken}`; } console.error(`[Second Opinion] Making API request to ${API_URL}`); // Make API request const response = await fetch(API_URL, { method: 'POST', headers, body: JSON.stringify(payload), }); const responseData = await response.json(); if (!response.ok) { console.error(`[Second Opinion] API error: ${response.status} ${response.statusText}`); console.error(`[Second Opinion] Response:`, responseData); // Handle specific error cases if (response.status === 401) { return { content: [ { type: 'text', text: 'āŒ Authentication failed. Please check your Second Opinion token.\n\nIf you\'re using a trial token, it may have expired. Get a new one at: https://secondopinion.codes', }, ], }; } if (response.status === 402) { // Payment required - different handling for trial vs paid users if (responseData.error === 'trial_credits_exhausted') { const conversionUrl = responseData.conversion_info?.signup_url || 'http://localhost:3000/pricing'; // Ensure we have a full URL with trial token const fullUrl = conversionUrl.includes('trial_token') ? conversionUrl : `${conversionUrl}?trial_token=${activeToken}`; return { content: [ { type: 'text', text: `šŸ†“ Your trial credits have been exhausted!\n\n` + `You've used your 5 free second opinions. Ready to upgrade?\n\n` + `šŸ’³ Subscription Plans:\n` + `• Starter: $5.99/month (100 credits)\n` + `• Pro: $9.99/month (200 credits)\n\n` + `šŸ”— **Upgrade now:** ${fullUrl}\n\n` + `Copy and paste the link above into your browser to subscribe.\n` + `Once subscribed, update your MCP config with your permanent token.`, }, ], }; } else { return { content: [ { type: 'text', text: 'šŸ’³ Insufficient credits. Please purchase more credits or upgrade your plan at: https://secondopinion.codes/dashboard', }, ], }; } } if (response.status === 429) { return { content: [ { type: 'text', text: 'ā±ļø Rate limit exceeded. Please wait a moment before making another request.', }, ], }; } // Generic error throw new McpError( ErrorCode.InternalError, `Second Opinion API error: ${response.status} ${responseData.error || response.statusText}` ); } // Success response const { opinion, credits_remaining, user_type, trial_info, trial_warning, trial_engagement } = responseData; let creditInfo = ''; let specialMessage = ''; if (user_type === 'trial') { creditInfo = `\n\nšŸ†“ Trial credits remaining: ${credits_remaining}/5`; // Handle special trial messages if (trial_warning?.type === 'last_credit') { specialMessage = `\n\n${trial_warning.message}`; } else if (trial_engagement?.type === 'reward_unlocked') { specialMessage = `\n\n${trial_engagement.message}\n${trial_engagement.reward_details}\n\nšŸ”— **Claim your reward:** ${trial_engagement.action_url}\n\n${trial_engagement.prompt}`; } else if (credits_remaining <= 1) { const upgradeUrl = trial_info?.conversion_url || 'https://secondopinion.codes/pricing'; creditInfo += `\nšŸ’” Running low on credits! Upgrade at: ${upgradeUrl}`; } } else { creditInfo = `\n\nšŸ’³ Credits remaining: ${credits_remaining}`; } console.error(`[Second Opinion] Success: ${credits_remaining} credits remaining`); return { content: [ { type: 'text', text: `## šŸ¤” Second Opinion\n\n${opinion}${creditInfo}${specialMessage}`, }, ], }; } catch (error) { console.error('[Second Opinion] Error:', error); if (error instanceof McpError) { throw error; } // Network or other errors if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFUSED') { return { content: [ { type: 'text', text: '🌐 Network error: Could not connect to Second Opinion service. Please check your internet connection.', }, ], }; } throw new McpError( ErrorCode.InternalError, `Unexpected error: ${error.message}` ); } } throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('[Second Opinion] MCP server started successfully'); } main().catch((error) => { console.error('[Second Opinion] Fatal error:', error); process.exit(1); });