second-opinion
Version:
Get a second opinion on your code from AI - MCP tool for Cursor IDE
281 lines (241 loc) ⢠9.35 kB
JavaScript
/**
* 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);
});