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

811 lines • 92.8 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ReadResourceResultSchema, ListResourceTemplatesRequestSchema, CompatibilityCallToolResultSchema, GetPromptResultSchema, PingRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { getMcpServers } from "./fetch-pluggedinmcp.js"; import { getSessionKey, getPluggedinMCPApiKey, getPluggedinMCPApiBaseUrl } from "./utils.js"; import { cleanupAllSessions, getSession, initSessions } from "./sessions.js"; import axios from "axios"; import { zodToJsonSchema } from 'zod-to-json-schema'; import { createRequire } from 'module'; import { logMcpActivity, createExecutionTimer } from "./notification-logger.js"; import { RateLimiter, sanitizeErrorMessage, validateRequestSize, withTimeout } from "./security-utils.js"; import { debugError } from "./debug-log.js"; import { createDocumentStaticTool, listDocumentsStaticTool, searchDocumentsStaticTool, getDocumentStaticTool, updateDocumentStaticTool } from "./tools/static-tools.js"; import { StaticToolHandlers } from "./handlers/static-handlers.js"; const require = createRequire(import.meta.url); const packageJson = require('../package.json'); // Map to store prefixed tool name -> { originalName, serverUuid } const toolToServerMap = {}; // Map to store custom instruction name -> instruction content const instructionToServerMap = {}; // Define the static discovery tool schema using Zod const DiscoverToolsInputSchema = z.object({ server_uuid: z.string().uuid().optional().describe("Optional UUID of a specific server to discover. If omitted, attempts to discover all."), force_refresh: z.boolean().optional().default(false).describe("Set to true to bypass cache and force a fresh discovery. Defaults to false."), }).describe("Triggers tool discovery for configured MCP servers in the Pluggedin App."); // Define the static discovery tool structure const discoverToolsStaticTool = { name: "pluggedin_discover_tools", description: "Triggers discovery of tools (and resources/templates) for configured MCP servers in the Pluggedin App.", inputSchema: zodToJsonSchema(DiscoverToolsInputSchema), }; // Define the static RAG query tool schema using Zod const RagQueryInputSchema = z.object({ query: z.string() .min(1, "Query cannot be empty") .max(1000, "Query too long") .describe("The RAG query to perform."), }).describe("Performs a RAG query against documents in the authenticated user's project."); // Define the static RAG query tool structure const ragQueryStaticTool = { name: "pluggedin_rag_query", description: "Performs a RAG query against documents in the Pluggedin App.", inputSchema: zodToJsonSchema(RagQueryInputSchema), }; // Define the static tool for sending custom notifications const sendNotificationStaticTool = { name: "pluggedin_send_notification", description: "Send custom notifications through the Plugged.in system with optional email delivery. You can provide a custom title or let the system use a localized default.", inputSchema: { type: "object", properties: { title: { type: "string", description: "Optional notification title. If not provided, a localized default will be used. Consider generating a descriptive title based on the message content." }, message: { type: "string", description: "The notification message content" }, severity: { type: "string", enum: ["INFO", "SUCCESS", "WARNING", "ALERT"], description: "The severity level of the notification (defaults to INFO)", default: "INFO" }, sendEmail: { type: "boolean", description: "Whether to also send the notification via email", default: false } }, required: ["message"] } }; // Input schema for validation const SendNotificationInputSchema = z.object({ title: z.string().optional(), message: z.string().min(1, "Message cannot be empty"), severity: z.enum(["INFO", "SUCCESS", "WARNING", "ALERT"]).default("INFO"), sendEmail: z.boolean().optional().default(false), }); // Define the static tool for listing notifications const listNotificationsStaticTool = { name: "pluggedin_list_notifications", description: "List notifications from the Plugged.in system with optional filters for unread only and result limit", inputSchema: { type: "object", properties: { onlyUnread: { type: "boolean", description: "Filter to show only unread notifications", default: false }, limit: { type: "integer", description: "Limit the number of notifications returned (1-100)", minimum: 1, maximum: 100 } } } }; // Input schema for list notifications validation const ListNotificationsInputSchema = z.object({ onlyUnread: z.boolean().optional().default(false), limit: z.number().int().min(1).max(100).optional(), }); // Define the static tool for marking notification as read const markNotificationReadStaticTool = { name: "pluggedin_mark_notification_read", description: "Mark a notification as read in the Plugged.in system", inputSchema: { type: "object", properties: { notificationId: { type: "string", description: "The ID of the notification to mark as read" } }, required: ["notificationId"] } }; // Input schema for mark notification read validation const MarkNotificationReadInputSchema = z.object({ notificationId: z.string().min(1, "Notification ID cannot be empty"), }); // Define the static tool for deleting notification const deleteNotificationStaticTool = { name: "pluggedin_delete_notification", description: "Delete a notification from the Plugged.in system", inputSchema: { type: "object", properties: { notificationId: { type: "string", description: "The ID of the notification to delete" } }, required: ["notificationId"] } }; // Input schema for delete notification validation const DeleteNotificationInputSchema = z.object({ notificationId: z.string().min(1, "Notification ID cannot be empty"), }); // Define the static prompt for proxy capabilities const proxyCapabilitiesStaticPrompt = { name: "pluggedin_proxy_capabilities", description: "Learn about the Plugged.in MCP Proxy capabilities and available tools", arguments: [] }; export const createServer = async () => { // Create rate limiters for different operations const toolCallRateLimiter = new RateLimiter(60000, 60); // 60 calls per minute const apiCallRateLimiter = new RateLimiter(60000, 100); // 100 API calls per minute const server = new Server({ name: "PluggedinMCP", version: packageJson.version, }, { capabilities: { prompts: {}, // Enable prompt support capability resources: {}, tools: {}, }, }); // List Tools Handler - Fetches tools from Pluggedin App API and adds static tool server.setRequestHandler(ListToolsRequestSchema, async (request) => { const apiKey = getPluggedinMCPApiKey(); const baseUrl = getPluggedinMCPApiBaseUrl(); // If no API key, return only static tools (for Smithery compatibility) // This path should be fast and not rate limited for tool discovery if (!apiKey || !baseUrl) { // Don't log to console for STDIO transport as it interferes with protocol return { tools: [ discoverToolsStaticTool, ragQueryStaticTool, sendNotificationStaticTool, listNotificationsStaticTool, markNotificationReadStaticTool, deleteNotificationStaticTool ], nextCursor: undefined }; } // Rate limit check only for authenticated API calls if (!apiCallRateLimiter.checkLimit()) { throw new Error("Rate limit exceeded. Please try again later."); } let fetchedTools = []; try { const apiUrl = `${baseUrl}/api/tools`; // Assuming this is the correct endpoint // Fetch the list of tools (which include original names and server info) // The API returns an object like { tools: [], message?: "..." } const response = await axios.get(apiUrl, { headers: { Authorization: `Bearer ${apiKey}`, }, timeout: 10000, }); // Access the 'tools' array from the response payload const fetchedTools = response.data?.tools || []; // Clear previous mapping and populate with new data Object.keys(toolToServerMap).forEach(key => delete toolToServerMap[key]); // Clear map // Create mappings for each tool to its server fetchedTools.forEach(tool => { // Store mapping with original name as the key if (tool.name && tool._serverUuid) { toolToServerMap[tool.name] = { originalName: tool.name, // No transformation needed anymore serverUuid: tool._serverUuid }; } else { debugError(`[ListTools Handler] Missing tool name or UUID for tool: ${tool.name}`); } }); // Prepare the response payload according to MCP spec { tools: Tool[] } const toolsForClient = fetchedTools.map(({ _serverUuid, _serverName, ...rest }) => rest); // Note: Pagination not handled here, assumes API returns all tools // Always include the static tools const allToolsForClient = [ discoverToolsStaticTool, ragQueryStaticTool, createDocumentStaticTool, listDocumentsStaticTool, searchDocumentsStaticTool, getDocumentStaticTool, updateDocumentStaticTool, sendNotificationStaticTool, listNotificationsStaticTool, markNotificationReadStaticTool, deleteNotificationStaticTool, ...toolsForClient ]; return { tools: allToolsForClient, nextCursor: undefined }; } catch (error) { // Log API fetch error but still return the static tool let sanitizedError = "Failed to list tools"; if (axios.isAxiosError(error) && error.response?.status) { // Only include status code, not full error details sanitizedError = `Failed to list tools (HTTP ${error.response.status})`; } debugError("[ListTools Handler Error]", error); throw new Error(sanitizedError); } }); // Call Tool Handler - Routes tool calls to the appropriate downstream server server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name: requestedToolName, arguments: args } = request.params; const meta = request.params._meta; // Basic input validation if (!requestedToolName || typeof requestedToolName !== 'string') { throw new Error("Invalid tool name provided"); } // Basic request size check (lightweight) if (!validateRequestSize(request.params, 50 * 1024 * 1024)) { // 50MB limit throw new Error("Request payload too large"); } // Rate limit check for tool calls if (!toolCallRateLimiter.checkLimit()) { throw new Error("Rate limit exceeded. Please try again later."); } try { // Handle static discovery tool first if (requestedToolName === discoverToolsStaticTool.name) { const validatedArgs = DiscoverToolsInputSchema.parse(args ?? {}); // Validate args const { server_uuid, force_refresh } = validatedArgs; const apiKey = getPluggedinMCPApiKey(); const baseUrl = getPluggedinMCPApiBaseUrl(); if (!apiKey || !baseUrl) { throw new Error("Pluggedin API Key or Base URL is not configured for discovery trigger."); } const timer = createExecutionTimer(); let shouldRunDiscovery = force_refresh; // If force_refresh is true, always run discovery let existingDataSummary = ""; // Check for existing data if not forcing refresh if (!force_refresh) { try { // Check for existing tools, resources, prompts, and templates with timeout protection const apiRequests = Promise.all([ axios.get(`${baseUrl}/api/tools`, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 10000 }), axios.get(`${baseUrl}/api/resources`, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 10000 }), axios.get(`${baseUrl}/api/prompts`, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 10000 }), axios.get(`${baseUrl}/api/resource-templates`, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 10000 }) ]); const [toolsResponse, resourcesResponse, promptsResponse, templatesResponse] = await withTimeout(apiRequests, 15000); const toolsCount = toolsResponse.data?.tools?.length || (Array.isArray(toolsResponse.data) ? toolsResponse.data.length : 0); const resourcesCount = Array.isArray(resourcesResponse.data) ? resourcesResponse.data.length : 0; const promptsCount = Array.isArray(promptsResponse.data) ? promptsResponse.data.length : 0; const templatesCount = Array.isArray(templatesResponse.data) ? templatesResponse.data.length : 0; const totalItems = toolsCount + resourcesCount + promptsCount + templatesCount; if (totalItems > 0) { // We have existing data, return it without running discovery const staticToolsCount = 3; // Always have 3 static tools const totalToolsCount = toolsCount + staticToolsCount; existingDataSummary = `Found cached data: ${toolsCount} dynamic tools + ${staticToolsCount} static tools = ${totalToolsCount} total tools, ${resourcesCount} resources, ${promptsCount} prompts, ${templatesCount} templates`; const cacheMessage = server_uuid ? `Returning cached discovery data for server ${server_uuid}. ${existingDataSummary}. Use force_refresh=true to update.\n\n` : `Returning cached discovery data for all servers. ${existingDataSummary}. Use force_refresh=true to update.\n\n`; // Format the actual data for the response let dataContent = cacheMessage; // Add static built-in tools section (always available) dataContent += `## šŸ”§ Static Built-in Tools (Always Available):\n`; dataContent += `1. **pluggedin_discover_tools** - Triggers discovery of tools (and resources/templates) for configured MCP servers in the Pluggedin App\n`; dataContent += `2. **pluggedin_rag_query** - Performs a RAG query against documents in the Pluggedin App\n`; dataContent += `3. **pluggedin_send_notification** - Send custom notifications through the Plugged.in system with optional email delivery\n`; dataContent += `\n`; // Add dynamic tools section (from MCP servers) if (toolsCount > 0) { const tools = toolsResponse.data?.tools || toolsResponse.data || []; dataContent += `## ⚔ Dynamic MCP Tools (${toolsCount}) - From Connected Servers:\n`; tools.forEach((tool, index) => { dataContent += `${index + 1}. **${tool.name}**`; if (tool.description) { dataContent += ` - ${tool.description}`; } dataContent += `\n`; }); dataContent += `\n`; } else { dataContent += `## ⚔ Dynamic MCP Tools (0) - From Connected Servers:\n`; dataContent += `No dynamic tools available. Add MCP servers to get more tools.\n\n`; } // Add prompts section if (promptsCount > 0) { dataContent += `## šŸ’¬ Available Prompts (${promptsCount}):\n`; promptsResponse.data.forEach((prompt, index) => { dataContent += `${index + 1}. **${prompt.name}**`; if (prompt.description) { dataContent += ` - ${prompt.description}`; } dataContent += `\n`; }); dataContent += `\n`; } // Add resources section if (resourcesCount > 0) { dataContent += `## šŸ“„ Available Resources (${resourcesCount}):\n`; resourcesResponse.data.forEach((resource, index) => { dataContent += `${index + 1}. **${resource.name || resource.uri}**`; if (resource.description) { dataContent += ` - ${resource.description}`; } if (resource.uri && resource.name !== resource.uri) { dataContent += ` (${resource.uri})`; } dataContent += `\n`; }); dataContent += `\n`; } // Add templates section if (templatesCount > 0) { dataContent += `## šŸ“‹ Available Resource Templates (${templatesCount}):\n`; templatesResponse.data.forEach((template, index) => { dataContent += `${index + 1}. **${template.name || template.uriTemplate}**`; if (template.description) { dataContent += ` - ${template.description}`; } if (template.uriTemplate && template.name !== template.uriTemplate) { dataContent += ` (${template.uriTemplate})`; } dataContent += `\n`; }); } // Log successful cache hit logMcpActivity({ action: 'tool_call', serverName: 'Discovery System (Cache)', serverUuid: 'pluggedin_discovery_cache', itemName: requestedToolName, success: true, executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors return { content: [{ type: "text", text: dataContent }], isError: false, }; } else { // No existing data found, run discovery shouldRunDiscovery = true; existingDataSummary = "No cached dynamic data found"; } } catch (cacheError) { // Error checking cache, show static tools and proceed with discovery // Show static tools even when cache check fails const staticToolsCount = 3; const cacheErrorMessage = `Cache check failed, showing static tools. Will run discovery for dynamic tools.\n\n`; let staticContent = cacheErrorMessage; staticContent += `## šŸ”§ Static Built-in Tools (Always Available):\n`; staticContent += `1. **pluggedin_discover_tools** - Triggers discovery of tools (and resources/templates) for configured MCP servers in the Pluggedin App\n`; staticContent += `2. **pluggedin_rag_query** - Performs a RAG query against documents in the Pluggedin App\n`; staticContent += `3. **pluggedin_send_notification** - Send custom notifications through the Plugged.in system with optional email delivery\n`; staticContent += `\n## ⚔ Dynamic MCP Tools - From Connected Servers:\n`; staticContent += `Cache check failed. Running discovery to find dynamic tools...\n\n`; staticContent += `Note: You can call pluggedin_discover_tools again to see the updated results.`; // Log cache error but static tools shown logMcpActivity({ action: 'tool_call', serverName: 'Discovery System (Cache Error)', serverUuid: 'pluggedin_discovery_cache_error', itemName: requestedToolName, success: true, executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors // Also trigger discovery in background (fire and forget) try { const discoveryApiUrl = server_uuid ? `${baseUrl}/api/discover/${server_uuid}` : `${baseUrl}/api/discover/all`; axios.post(discoveryApiUrl, {}, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 60000, // Background discovery timeout }).catch(() => { }); // Fire and forget } catch { // Ignore discovery trigger errors } return { content: [{ type: "text", text: staticContent }], isError: false, }; } } // Run discovery if needed if (shouldRunDiscovery) { // Define the API endpoint in pluggedin-app to trigger discovery const discoveryApiUrl = server_uuid ? `${baseUrl}/api/discover/${server_uuid}` // Endpoint for specific server : `${baseUrl}/api/discover/all`; // Endpoint for all servers if (force_refresh) { // For force refresh, get cached data first AND trigger discovery in background try { // Fire-and-forget: trigger discovery in background axios.post(discoveryApiUrl, {}, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 60000, // 60s timeout for background discovery }).catch((bgError) => { }); // Get current cached data to show immediately let forceRefreshContent = ""; try { // Fetch current cached data (use shorter timeout since this is just cache check) const [toolsResponse, resourcesResponse, promptsResponse, templatesResponse] = await Promise.all([ axios.get(`${baseUrl}/api/tools`, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 5000 }), axios.get(`${baseUrl}/api/resources`, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 5000 }), axios.get(`${baseUrl}/api/prompts`, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 5000 }), axios.get(`${baseUrl}/api/resource-templates`, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 5000 }) ]); const toolsCount = toolsResponse.data?.tools?.length || (Array.isArray(toolsResponse.data) ? toolsResponse.data.length : 0); const resourcesCount = Array.isArray(resourcesResponse.data) ? resourcesResponse.data.length : 0; const promptsCount = Array.isArray(promptsResponse.data) ? promptsResponse.data.length : 0; const templatesCount = Array.isArray(templatesResponse.data) ? templatesResponse.data.length : 0; const staticToolsCount = 3; const totalToolsCount = toolsCount + staticToolsCount; const refreshMessage = server_uuid ? `šŸ”„ Force refresh initiated for server ${server_uuid}. Discovery is running in background.\n\nShowing current cached data (${toolsCount} dynamic tools + ${staticToolsCount} static tools = ${totalToolsCount} total tools, ${resourcesCount} resources, ${promptsCount} prompts, ${templatesCount} templates):\n\n` : `šŸ”„ Force refresh initiated for all servers. Discovery is running in background.\n\nShowing current cached data (${toolsCount} dynamic tools + ${staticToolsCount} static tools = ${totalToolsCount} total tools, ${resourcesCount} resources, ${promptsCount} prompts, ${templatesCount} templates):\n\n`; forceRefreshContent = refreshMessage; // Add static built-in tools section (always available) forceRefreshContent += `## šŸ”§ Static Built-in Tools (Always Available):\n`; forceRefreshContent += `1. **pluggedin_discover_tools** - Triggers discovery of tools (and resources/templates) for configured MCP servers in the Pluggedin App\n`; forceRefreshContent += `2. **pluggedin_rag_query** - Performs a RAG query against documents in the Pluggedin App\n`; forceRefreshContent += `3. **pluggedin_send_notification** - Send custom notifications through the Plugged.in system with optional email delivery\n`; forceRefreshContent += `\n`; // Add dynamic tools section (from MCP servers) if (toolsCount > 0) { const tools = toolsResponse.data?.tools || toolsResponse.data || []; forceRefreshContent += `## ⚔ Dynamic MCP Tools (${toolsCount}) - From Connected Servers:\n`; tools.forEach((tool, index) => { forceRefreshContent += `${index + 1}. **${tool.name}**`; if (tool.description) { forceRefreshContent += ` - ${tool.description}`; } forceRefreshContent += `\n`; }); forceRefreshContent += `\n`; } else { forceRefreshContent += `## ⚔ Dynamic MCP Tools (0) - From Connected Servers:\n`; forceRefreshContent += `No dynamic tools available. Add MCP servers to get more tools.\n\n`; } // Add prompts section if (promptsCount > 0) { forceRefreshContent += `## šŸ’¬ Available Prompts (${promptsCount}):\n`; promptsResponse.data.forEach((prompt, index) => { forceRefreshContent += `${index + 1}. **${prompt.name}**`; if (prompt.description) { forceRefreshContent += ` - ${prompt.description}`; } forceRefreshContent += `\n`; }); forceRefreshContent += `\n`; } // Add resources section if (resourcesCount > 0) { forceRefreshContent += `## šŸ“„ Available Resources (${resourcesCount}):\n`; resourcesResponse.data.forEach((resource, index) => { forceRefreshContent += `${index + 1}. **${resource.name || resource.uri}**`; if (resource.description) { forceRefreshContent += ` - ${resource.description}`; } if (resource.uri && resource.name !== resource.uri) { forceRefreshContent += ` (${resource.uri})`; } forceRefreshContent += `\n`; }); forceRefreshContent += `\n`; } // Add templates section if (templatesCount > 0) { forceRefreshContent += `## šŸ“‹ Available Resource Templates (${templatesCount}):\n`; templatesResponse.data.forEach((template, index) => { forceRefreshContent += `${index + 1}. **${template.name || template.uriTemplate}**`; if (template.description) { forceRefreshContent += ` - ${template.description}`; } if (template.uriTemplate && template.name !== template.uriTemplate) { forceRefreshContent += ` (${template.uriTemplate})`; } forceRefreshContent += `\n`; }); forceRefreshContent += `\n`; } forceRefreshContent += `šŸ“ **Note**: Fresh discovery is running in background. Call pluggedin_discover_tools() again in 10-30 seconds to see if any new tools were discovered.`; } catch (cacheError) { // If we can't get cached data, just show static tools forceRefreshContent = server_uuid ? `šŸ”„ Force refresh initiated for server ${server_uuid}. Discovery is running in background.\n\nCould not retrieve cached data, showing static tools:\n\n` : `šŸ”„ Force refresh initiated for all servers. Discovery is running in background.\n\nCould not retrieve cached data, showing static tools:\n\n`; forceRefreshContent += `## šŸ”§ Static Built-in Tools (Always Available):\n`; forceRefreshContent += `1. **pluggedin_discover_tools** - Triggers discovery of tools (and resources/templates) for configured MCP servers in the Pluggedin App\n`; forceRefreshContent += `2. **pluggedin_rag_query** - Performs a RAG query against documents in the Pluggedin App\n`; forceRefreshContent += `3. **pluggedin_send_notification** - Send custom notifications through the Plugged.in system with optional email delivery\n`; forceRefreshContent += `\nšŸ“ **Note**: Fresh discovery is running in background. Call pluggedin_discover_tools() again in 10-30 seconds to see updated results.`; } // Log successful trigger logMcpActivity({ action: 'tool_call', serverName: 'Discovery System (Background)', serverUuid: 'pluggedin_discovery_bg', itemName: requestedToolName, success: true, executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors return { content: [{ type: "text", text: forceRefreshContent }], isError: false, }; } catch (triggerError) { // Even trigger failed, return error const errorMsg = `Failed to trigger background discovery: ${triggerError.message}`; // Log failed trigger logMcpActivity({ action: 'tool_call', serverName: 'Discovery System', serverUuid: 'pluggedin_discovery', itemName: requestedToolName, success: false, errorMessage: errorMsg, executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors throw new Error(errorMsg); } } else { // For regular discovery (no force refresh), wait for completion try { const discoveryResponse = await axios.post(discoveryApiUrl, {}, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 30000, // 30s timeout for regular discovery }); // Return success message from the discovery API response const baseMessage = discoveryResponse.data?.message || "Discovery process initiated."; const contextMessage = `${existingDataSummary}. ${baseMessage}\n\nNote: You can call pluggedin_discover_tools again to see the cached results including both static and dynamic tools.`; // Log successful discovery logMcpActivity({ action: 'tool_call', serverName: 'Discovery System', serverUuid: 'pluggedin_discovery', itemName: requestedToolName, success: true, executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors return { content: [{ type: "text", text: contextMessage }], isError: false, }; } catch (apiError) { // Log failed discovery logMcpActivity({ action: 'tool_call', serverName: 'Discovery System', serverUuid: 'pluggedin_discovery', itemName: requestedToolName, success: false, errorMessage: apiError instanceof Error ? apiError.message : String(apiError), executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors const errorMsg = axios.isAxiosError(apiError) ? `API Error (${apiError.response?.status}): ${apiError.response?.data?.error || apiError.message}` : apiError.message; throw new Error(`Failed to trigger discovery via API: ${errorMsg}`); } } } } // Handle static RAG query tool if (requestedToolName === ragQueryStaticTool.name) { const validatedArgs = RagQueryInputSchema.parse(args ?? {}); // Validate args const apiKey = getPluggedinMCPApiKey(); const baseUrl = getPluggedinMCPApiBaseUrl(); if (!apiKey || !baseUrl) { throw new Error("Pluggedin API Key or Base URL is not configured for RAG query."); } // Define the API endpoint in pluggedin-app for RAG queries const ragApiUrl = `${baseUrl}/api/rag/query`; const timer = createExecutionTimer(); try { // Make POST request with RAG query (ragIdentifier removed for security) const ragResponse = await axios.post(ragApiUrl, { query: validatedArgs.query, }, { headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, timeout: 15000, // Reduced timeout to prevent DoS responseType: 'text' // Expect text response, not JSON }); // The API returns plain text response const responseText = ragResponse.data || "No response generated"; // Log successful RAG query logMcpActivity({ action: 'tool_call', serverName: 'RAG System', serverUuid: 'pluggedin_rag', itemName: requestedToolName, success: true, executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors return { content: [{ type: "text", text: responseText }], isError: false, }; // Cast to expected type } catch (apiError) { // Log failed RAG query logMcpActivity({ action: 'tool_call', serverName: 'RAG System', serverUuid: 'pluggedin_rag', itemName: requestedToolName, success: false, errorMessage: apiError instanceof Error ? apiError.message : String(apiError), executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors // Sanitized error message to prevent information disclosure const errorMsg = axios.isAxiosError(apiError) && apiError.response?.status ? `RAG service error (${apiError.response.status})` : "RAG service temporarily unavailable"; throw new Error(errorMsg); } } // Handle static send notification tool if (requestedToolName === sendNotificationStaticTool.name) { const validatedArgs = SendNotificationInputSchema.parse(args ?? {}); // Validate args const apiKey = getPluggedinMCPApiKey(); const baseUrl = getPluggedinMCPApiBaseUrl(); if (!apiKey || !baseUrl) { throw new Error("Pluggedin API Key or Base URL is not configured for custom notifications."); } // Define the API endpoint in pluggedin-app for custom notifications const notificationApiUrl = `${baseUrl}/api/notifications/custom`; const timer = createExecutionTimer(); try { // Make POST request with notification data const notificationResponse = await axios.post(notificationApiUrl, { title: validatedArgs.title, message: validatedArgs.message, severity: validatedArgs.severity, sendEmail: validatedArgs.sendEmail, }, { headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, timeout: 30000, // Increased timeout for notifications }); // The API returns success confirmation const responseData = notificationResponse.data; const responseText = responseData?.message || "Notification sent successfully"; // Log successful notification logMcpActivity({ action: 'tool_call', serverName: 'Notification System', serverUuid: 'pluggedin_notifications', itemName: requestedToolName, success: true, executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors return { content: [{ type: "text", text: responseText }], isError: false, }; // Cast to expected type } catch (apiError) { // Log failed notification logMcpActivity({ action: 'tool_call', serverName: 'Notification System', serverUuid: 'pluggedin_notifications', itemName: requestedToolName, success: false, errorMessage: apiError instanceof Error ? apiError.message : String(apiError), executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors // Sanitized error message const errorMsg = axios.isAxiosError(apiError) && apiError.response?.status ? `Notification service error (${apiError.response.status})` : "Notification service temporarily unavailable"; throw new Error(errorMsg); } } // Handle static list notifications tool if (requestedToolName === listNotificationsStaticTool.name) { const validatedArgs = ListNotificationsInputSchema.parse(args ?? {}); // Validate args const apiKey = getPluggedinMCPApiKey(); const baseUrl = getPluggedinMCPApiBaseUrl(); if (!apiKey || !baseUrl) { throw new Error("Pluggedin API Key or Base URL is not configured for listing notifications."); } // Build query parameters const queryParams = new URLSearchParams(); if (validatedArgs.onlyUnread) { queryParams.append('onlyUnread', 'true'); } if (validatedArgs.limit) { queryParams.append('limit', validatedArgs.limit.toString()); } const notificationApiUrl = `${baseUrl}/api/notifications${queryParams.toString() ? '?' + queryParams.toString() : ''}`; const timer = createExecutionTimer(); try { // Make GET request to list notifications const notificationResponse = await axios.get(notificationApiUrl, { headers: { Authorization: `Bearer ${apiKey}`, }, timeout: 15000, }); const notifications = notificationResponse.data?.notifications || []; // Format the response for better readability let responseText = `Found ${notifications.length} notification${notifications.length !== 1 ? 's' : ''}`; if (validatedArgs.onlyUnread) { responseText += ' (unread only)'; } responseText += ':\n\n'; if (notifications.length === 0) { responseText += 'No notifications found.'; } else { notifications.forEach((notif, index) => { responseText += `${index + 1}. **${notif.title}**\n`; responseText += ` ID: ${notif.id} (use this ID for operations)\n`; responseText += ` Type: ${notif.type} | Severity: ${notif.severity || 'N/A'}\n`; responseText += ` Status: ${notif.read ? 'Read' : 'Unread'}${notif.completed ? ' | Completed' : ''}\n`; responseText += ` Created: ${new Date(notif.created_at).toLocaleString()}\n`; responseText += ` Message: ${notif.message}\n`; if (notif.link) { responseText += ` Link: ${notif.link}\n`; } responseText += '\n'; }); responseText += 'šŸ’” **Tip**: Use the UUID shown in the ID field when marking as read or deleting notifications.'; } // Log successful list logMcpActivity({ action: 'tool_call', serverName: 'Notification System', serverUuid: 'pluggedin_notifications', itemName: requestedToolName, success: true, executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors return { content: [{ type: "text", text: responseText }], isError: false, }; } catch (apiError) { // Log failed list logMcpActivity({ action: 'tool_call', serverName: 'Notification System', serverUuid: 'pluggedin_notifications', itemName: requestedToolName, success: false, errorMessage: apiError instanceof Error ? apiError.message : String(apiError), executionTime: timer.stop(), }).catch(() => { }); // Ignore notification errors // Sanitized error message const errorMsg = axios.isAxiosError(apiError) && apiError.response?.status ? `Notification service error (${apiError.response.status})` : "Notification service tempor