UNPKG

@pluggedin/pluggedin-mcp-proxy

Version:

Unified MCP proxy that aggregates all your MCP servers (STDIO, SSE, Streamable HTTP) into one powerful interface. Access any tool through a single connection, search across unified documents with built-in RAG, and receive notifications from any model. Tes

409 lines (383 loc) 17.9 kB
import { getPluggedinMCPApiKey, getPluggedinMCPApiBaseUrl, sanitizeName } from "../utils.js"; import { logMcpActivity, createExecutionTimer } from "../notification-logger.js"; import { debugLog, debugError } from "../debug-log.js"; import { getApiKeySetupMessage } from "./static-handlers-helpers.js"; import { makeApiRequest } from "../http-client.js"; import { DiscoverToolsInputSchema, RagQueryInputSchema } from '../schemas/index.js'; import { discoverToolsStaticTool, ragQueryStaticTool } from '../tools/static-tools.js'; import { getMcpServers } from "../fetch-pluggedinmcp.js"; import { initSessions } from "../sessions.js"; /** * Handles setup, discovery, and RAG operations */ export class SetupHandlers { toolToServerMap; instructionToServerMap; constructor(toolToServerMap, instructionToServerMap) { this.toolToServerMap = toolToServerMap; this.instructionToServerMap = instructionToServerMap; } async handleSetup(args) { const topic = args?.topic || 'getting_started'; let content = ''; switch (topic) { case 'getting_started': content = `# Welcome to Plugged.in MCP! 🚀 ## What is Plugged.in? Plugged.in is a unified interface for Model Context Protocol (MCP) servers, allowing you to: - Connect multiple MCP servers through a single proxy - Manage AI-generated documents - Use RAG capabilities for semantic search - Send notifications and track activities ## Getting Started 1. **Get your API key**: Visit https://plugged.in/settings to create an account and get your API key 2. **Configure your environment**: Set the PLUGGEDIN_API_KEY environment variable 3. **Discover tools**: Run \`pluggedin_discover_tools\` to see available MCP servers 4. **Start using tools**: Access any tool from connected servers ## Available Commands - \`pluggedin_setup\` - This help system (no API key required) - \`pluggedin_discover_tools\` - List available MCP servers - Other tools require an API key - see descriptions for details For more help, try: \`pluggedin_setup\` with topic: "api_key", "configuration", or "troubleshooting"`; break; case 'api_key': content = `# Setting up your Plugged.in API Key 🔑 ## Getting an API Key 1. Visit https://plugged.in 2. Sign up or log in to your account 3. Navigate to Settings → API Keys 4. Create a new API key 5. Copy the key (starts with \`pg_in_\`) ## Configuring the API Key Set the environment variable before running your MCP client: ### macOS/Linux: \`\`\`bash export PLUGGEDIN_API_KEY="pg_in_your_key_here" export PLUGGEDIN_API_BASE_URL="https://plugged.in" # Optional, defaults to this \`\`\` ### Windows: \`\`\`cmd set PLUGGEDIN_API_KEY=pg_in_your_key_here set PLUGGEDIN_API_BASE_URL=https://plugged.in \`\`\` ### In your application: Add to your \`.env\` file or configuration. ## Verifying Setup Run \`pluggedin_discover_tools\` - if configured correctly, you'll see your connected MCP servers.`; break; case 'configuration': content = `# Plugged.in Configuration Guide ⚙️ ## Environment Variables - **PLUGGEDIN_API_KEY** (required): Your API key from https://plugged.in/settings - **PLUGGEDIN_API_BASE_URL** (optional): API endpoint (defaults to https://plugged.in) - **PLUGGEDIN_DEBUG** (optional): Set to "true" for verbose logging ## MCP Server Configuration 1. Log in to https://plugged.in 2. Navigate to MCP Servers 3. Add your MCP servers with their connection details 4. Servers are automatically available through the proxy ## Docker Configuration If using Docker, pass environment variables: \`\`\`bash docker run -e PLUGGEDIN_API_KEY="your_key" pluggedin-mcp \`\`\` ## Testing Configuration - \`pluggedin_discover_tools\` - Lists connected servers - \`pluggedin_rag_query\` - Tests RAG functionality - \`pluggedin_list_documents\` - Tests document access`; break; case 'troubleshooting': content = `# Troubleshooting Guide 🔧 ## Common Issues ### "API Key not configured" - Check if PLUGGEDIN_API_KEY environment variable is set - Verify the key starts with \`pg_in_\` - Ensure no extra spaces or quotes in the key ### No servers found with discover_tools - Verify your API key is valid - Check if you have MCP servers configured at https://plugged.in - Try with \`force_refresh: true\` parameter ### Connection timeouts - Check your internet connection - Verify PLUGGEDIN_API_BASE_URL if using custom endpoint - Check if behind a firewall or proxy ### Tools not working - Most tools require an API key (check tool descriptions) - Ensure your account has appropriate permissions - Check server logs for detailed error messages ## Debug Mode Enable debug logging: \`\`\`bash export PLUGGEDIN_DEBUG=true \`\`\` ## Getting Help - Documentation: https://plugged.in/docs - Support: support@plugged.in - GitHub: https://github.com/pluggedin/mcp-proxy ## Platform-Specific Notes ### Claude Desktop Add to your Claude Desktop config: \`\`\`json { "mcpServers": { "pluggedin": { "command": "npx", "args": ["@pluggedin/mcp-proxy"], "env": { "PLUGGEDIN_API_KEY": "pg_in_your_key_here" } } } } \`\`\` ### VS Code / Cursor Set environment variables in your terminal before launching the editor. ### Common Error Codes - 401: Invalid API key - 403: Permission denied (check account status) - 429: Rate limit exceeded - 500: Server error (try again later)`; break; } return { content: [{ type: "text", text: content }], isError: false, }; } async handleDiscoverTools(args) { debugError(`[CallTool Handler] Executing static tool: ${discoverToolsStaticTool.name}`); const validatedArgs = DiscoverToolsInputSchema.parse(args ?? {}); const timer = createExecutionTimer(); try { const apiKey = getPluggedinMCPApiKey(); const baseUrl = getPluggedinMCPApiBaseUrl(); if (!apiKey || !baseUrl) { // No API key - show only static tools let dataContent = '# Available MCP Tools (No API Key)\n\n'; dataContent += 'To see your configured MCP servers, please set up your API key.\n'; dataContent += 'Run `pluggedin_setup` for help getting started.\n\n'; dataContent += '## Available Static Tools:\n'; dataContent += '- **pluggedin_setup** - Get help and setup instructions\n'; dataContent += '- **pluggedin_discover_tools** - Discover available tools (limited without API key)\n'; return { content: [{ type: "text", text: dataContent }], isError: false, }; } // Reinitialize sessions if force refresh requested if (validatedArgs.force_refresh) { debugLog('[CallTool Handler] Force refresh requested, reinitializing sessions...'); await initSessions(); } // Log discovery attempt logMcpActivity({ action: 'tool_call', serverName: 'Pluggedin Discovery', serverUuid: 'pluggedin_discovery', itemName: discoverToolsStaticTool.name, success: true, executionTime: 0, // Will update after }).catch(() => { }); // Ignore notification errors // Wipe stored servers and instructions maps Object.keys(this.toolToServerMap).forEach(key => delete this.toolToServerMap[key]); Object.keys(this.instructionToServerMap).forEach(key => delete this.instructionToServerMap[key]); // First, fetch servers to get basic info let servers = []; let serverMap = {}; try { // Call getMcpServers which returns Record<string, ServerParameters> const mcpServersDict = await getMcpServers(validatedArgs.force_refresh); // Convert the dictionary to an array for processing servers = Object.entries(mcpServersDict).map(([uuid, params]) => ({ ...params, uuid // Ensure uuid is included in the server object })); // Create a map for easy lookup servers.forEach(server => { serverMap[server.uuid] = server; }); } catch (fetchError) { const errorMsg = `Could not fetch MCP servers: ${fetchError.message}. Please ensure your Pluggedin API key and URL are correctly configured.`; throw new Error(errorMsg); } let dataContent = '# Available MCP Servers\n\n'; // Then fetch tools from the tools endpoint let tools = []; try { const response = await makeApiRequest({ method: 'GET', url: '/api/tools', requiresAuth: true }); // The API returns { tools: Tool[] } if (response && response.tools && Array.isArray(response.tools)) { tools = response.tools; debugLog(`[Discover Tools] Found ${tools.length} tools`); if (tools.length > 0) { debugLog('[Discover Tools] First tool example:', tools[0]); } } else { debugLog('[Discover Tools] Unexpected response structure:', response); } } catch (toolsError) { debugError('Failed to fetch tools:', toolsError); debugLog('[Discover Tools] Error details:', toolsError.message); } // Group tools by server const toolsByServer = {}; tools.forEach(tool => { // Try multiple possible field names for server UUID const serverUuid = tool.mcp_server_uuid || tool.server_uuid || tool.serverUuid || tool.mcpServerUuid || tool._serverUuid; const toolName = tool.name || tool.tool_name || tool.toolName; if (serverUuid) { if (!toolsByServer[serverUuid]) { toolsByServer[serverUuid] = []; } toolsByServer[serverUuid].push(tool); } }); debugLog('[Discover Tools] Tools grouped by server:', Object.keys(toolsByServer)); debugLog('[Discover Tools] Server UUIDs from servers:', servers.map(s => s.uuid)); // Process each server and its tools servers.forEach((server) => { const serverTools = toolsByServer[server.uuid] || []; if (serverTools.length > 0) { dataContent += `## ${server.name}\n`; dataContent += `### Tools (${serverTools.length}):\n`; serverTools.forEach((tool) => { const toolName = tool.name || tool.tool_name || tool.toolName; const prefixedName = sanitizeName(`${server.name}_${toolName}`); this.toolToServerMap[prefixedName] = { originalName: toolName, serverUuid: server.uuid }; dataContent += `- **${prefixedName}** - ${tool.description || 'No description available'}\n`; }); dataContent += '\n'; } else { // Only show server name if it has no tools dataContent += `## ${server.name}\n`; dataContent += `*No tools available*\n\n`; } // Process custom instructions if any if (server.customInstructions?.length > 0) { dataContent += `### Custom Instructions (${server.customInstructions.length}):\n`; server.customInstructions.forEach((instruction) => { const name = instruction.name || `instruction_${Math.random().toString(36).substring(7)}`; this.instructionToServerMap[name] = server.uuid; dataContent += `- **${name}** - ${instruction.instruction}\n`; }); dataContent += '\n'; } }); // Check if we have any servers but no custom instructions shown const hasServers = servers.length > 0; const hasCustomInstructions = servers.some(s => s.customInstructions?.length > 0); if (hasServers && !hasCustomInstructions) { dataContent += '\n*Note: Custom instructions/prompts are available through the MCP prompts interface.*\n'; } // Always show static tools for better discoverability dataContent += '\n## Plugged.in Built-in Tools\n'; dataContent += '### Static Tools:\n'; dataContent += '- **pluggedin_setup** - Get started with Plugged.in MCP (no API key required)\n'; dataContent += '- **pluggedin_discover_tools** - Triggers discovery of tools for configured MCP servers\n'; dataContent += '- **pluggedin_rag_query** - Performs a RAG query against documents\n'; dataContent += '- **pluggedin_send_notification** - Send custom notifications\n'; dataContent += '- **pluggedin_list_notifications** - List notifications with filters\n'; dataContent += '- **pluggedin_mark_notification_read** - Mark a notification as read\n'; dataContent += '- **pluggedin_delete_notification** - Delete a notification\n'; dataContent += '- **pluggedin_create_document** - Create and save AI-generated documents to the user\'s library\n'; dataContent += '- **pluggedin_list_documents** - List documents with filtering options\n'; dataContent += '- **pluggedin_search_documents** - Search documents semantically\n'; dataContent += '- **pluggedin_get_document** - Retrieve a specific document by ID\n'; dataContent += '- **pluggedin_update_document** - Update or append to an existing document\n'; // Update activity log with success logMcpActivity({ action: 'tool_call', serverName: 'Pluggedin Discovery', serverUuid: 'pluggedin_discovery', itemName: discoverToolsStaticTool.name, success: true, executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors return { content: [{ type: "text", text: dataContent }], isError: false, }; } catch (toolError) { // Log discovery failure logMcpActivity({ action: 'tool_call', serverName: 'Pluggedin Discovery', serverUuid: 'pluggedin_discovery', itemName: discoverToolsStaticTool.name, success: false, errorMessage: toolError instanceof Error ? toolError.message : String(toolError), executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors throw toolError; } } async handleRagQuery(args) { debugError(`[CallTool Handler] Executing static tool: ${ragQueryStaticTool.name}`); const validatedArgs = RagQueryInputSchema.parse(args ?? {}); const apiKey = getPluggedinMCPApiKey(); if (!apiKey) { return { content: [{ type: "text", text: getApiKeySetupMessage("pluggedin_rag_query") }], isError: false }; } const timer = createExecutionTimer(); try { const response = await makeApiRequest({ method: 'POST', url: '/api/rag/query', data: validatedArgs }); // Log successful RAG query logMcpActivity({ action: 'tool_call', serverName: 'Pluggedin RAG', serverUuid: 'pluggedin_rag', itemName: ragQueryStaticTool.name, success: true, executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors const results = response.results || []; let responseText = `RAG Query Results for: "${validatedArgs.query}"\n\n`; if (results.length === 0) { responseText += 'No relevant documents found.\n'; } else { results.forEach((result, index) => { responseText += `${index + 1}. **${result.title}** (Score: ${(result.score * 100).toFixed(1)}%)\n`; responseText += ` ${result.snippet}\n`; responseText += ` [Document ID: ${result.documentId}]\n\n`; }); } return { content: [{ type: "text", text: responseText }], isError: false, }; } catch (error) { // Log failed RAG query logMcpActivity({ action: 'tool_call', serverName: 'Pluggedin RAG', serverUuid: 'pluggedin_rag', itemName: ragQueryStaticTool.name, success: false, errorMessage: error instanceof Error ? error.message : String(error), executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors throw error; } } }