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 (810 loc) • 117 kB
/** * Plugged.in MCP Proxy - UUID-based Tool Prefixing Implementation * * This module implements automatic UUID-based tool prefixing to resolve name collisions * in MCP clients. When multiple MCP servers provide tools with identical names, * this system prefixes each tool with its server's UUID to ensure uniqueness. * * FEATURES: * - Automatic UUID prefixing: {server_uuid}__{original_tool_name} * - Backward compatibility: Supports both prefixed and non-prefixed tool calls * - Configurable: Can be disabled via PLUGGEDIN_UUID_TOOL_PREFIXING=false * - Collision-free: Guarantees unique tool names across all servers * * CONFIGURATION: * - PLUGGEDIN_UUID_TOOL_PREFIXING: Set to 'false' to disable prefixing (default: true) * * USAGE: * 1. Tools are automatically prefixed when retrieved from /api/tools?prefix_tools=true * 2. MCP proxy handles both prefixed and non-prefixed tool calls seamlessly * 3. Existing integrations continue to work without modification * * EXAMPLES: * - Original: "read_file" from server "550e8400-e29b-41d4-a716-446655440000" * - Prefixed: "550e8400-e29b-41d4-a716-446655440000__read_file" * - Both forms are accepted for backward compatibility */ 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 { debugLog, debugError } from "./debug-log.js"; import { withErrorHandling } from "./error-handler.js"; import { setupStaticTool, createDocumentStaticTool, listDocumentsStaticTool, searchDocumentsStaticTool, getDocumentStaticTool, updateDocumentStaticTool, clipboardSetStaticTool, clipboardGetStaticTool, clipboardDeleteStaticTool, clipboardListStaticTool, clipboardPushStaticTool, clipboardPopStaticTool, STATIC_TOOLS_COUNT } from "./tools/static-tools.js"; import { StaticToolHandlers } from "./handlers/static-handlers.js"; import { formatCustomInstructionsForDiscovery } from "./utils/custom-instructions.js"; import { parsePrefixedToolName as parseAnyPrefixedToolName, isValidUuid } from "./slug-utils.js"; const require = createRequire(import.meta.url); const packageJson = require('../package.json'); // Map to store prefixed tool name -> { originalName, serverUuid } const toolToServerMap = {}; // Configuration for UUID-based tool prefixing // Set PLUGGEDIN_UUID_TOOL_PREFIXING=false to disable UUID prefixing (for backward compatibility) // When enabled, tools are returned with format: {server_uuid}__{original_tool_name} // When disabled, tools are returned with their original names const UUID_TOOL_PREFIXING_ENABLED = process.env.PLUGGEDIN_UUID_TOOL_PREFIXING !== 'false'; // Default to true /** * Creates a UUID-prefixed tool name * Format: {server_uuid}__{original_tool_name} */ export function createPrefixedToolName(serverUuid, originalName) { return `${serverUuid}__${originalName}`; } /** * Parses a potentially prefixed tool name (UUID-based for backward compatibility) * Returns { originalName, serverUuid } or null if not prefixed */ export function parsePrefixedToolName(toolName) { const parsed = parseAnyPrefixedToolName(toolName); if (!parsed || parsed.prefixType !== 'uuid') { return null; // Not a UUID-prefixed name } return { originalName: parsed.originalName, serverUuid: parsed.serverIdentifier }; } // 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), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false } }; // Define the schema for asking questions to the knowledge base const AskKnowledgeBaseInputSchema = z.object({ query: z.string() .min(1, "Query cannot be empty") .max(1000, "Query too long") .describe("Your question or query to get AI-generated answers from the knowledge base.") }).describe("Ask questions and get AI-generated answers from your knowledge base. Returns JSON with answer, sources, and metadata."); // Define the static tool for asking questions to the knowledge base const askKnowledgeBaseStaticTool = { name: "pluggedin_ask_knowledge_base", description: "Ask questions and get AI-generated answers from your knowledge base. Returns structured JSON with answer, document sources, and metadata.", inputSchema: zodToJsonSchema(AskKnowledgeBaseInputSchema), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false } }; // 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"] }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false } }; // 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 } } }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true } }; // 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 done const markNotificationDoneStaticTool = { name: "pluggedin_mark_notification_done", description: "Mark a notification as done in the Plugged.in system", inputSchema: { type: "object", properties: { notificationId: { type: "string", description: "The ID of the notification to mark as read" } }, required: ["notificationId"] }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true } }; // Input schema for mark notification done validation const MarkNotificationDoneInputSchema = 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"] }, annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true } }; // 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 all 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: [ setupStaticTool, discoverToolsStaticTool, askKnowledgeBaseStaticTool, createDocumentStaticTool, listDocumentsStaticTool, searchDocumentsStaticTool, getDocumentStaticTool, updateDocumentStaticTool, sendNotificationStaticTool, listNotificationsStaticTool, markNotificationDoneStaticTool, deleteNotificationStaticTool, clipboardSetStaticTool, clipboardGetStaticTool, clipboardDeleteStaticTool, clipboardListStaticTool, clipboardPushStaticTool, clipboardPopStaticTool ], 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 { // Build API URL with prefixing parameter const apiUrl = new URL(`${baseUrl}/api/tools`); if (UUID_TOOL_PREFIXING_ENABLED) { apiUrl.searchParams.set('prefix_tools', 'true'); } // 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.toString(), { headers: { Authorization: `Bearer ${apiKey}`, }, timeout: 10000, }); // Access the 'tools' array from the response payload 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 => { if (tool.name && tool._serverUuid) { // Store mapping with the tool name as returned by API (may be prefixed or not) toolToServerMap[tool.name] = { originalName: tool.name, // Will be updated if prefixed serverUuid: tool._serverUuid }; // If UUID prefixing is enabled and the tool name is not already prefixed, // we need to handle backward compatibility if (UUID_TOOL_PREFIXING_ENABLED) { // Use shared helper for parsing prefixed tool names const parsed = parseAnyPrefixedToolName(tool.name); if (parsed) { // Tool name is prefixed, extract original name toolToServerMap[tool.name].originalName = parsed.originalName; debugLog(`[ListTools Handler] Tool ${tool.name} is ${parsed.prefixType}-prefixed, original: ${parsed.originalName}`); } else { // Tool name is not prefixed, this might be for backward compatibility // In this case, the originalName should remain as tool.name debugLog(`[ListTools Handler] Tool ${tool.name} is not prefixed, using as-is for backward compatibility`); } } } else { debugError(`[ListTools Handler] Missing tool name or UUID for tool: ${tool.name}`); } }); // Fetch server configurations with custom instructions let serverContexts = new Map(); try { const serverParams = await getMcpServers(false); // Build server contexts with parsed constraints const { buildServerContextsMap } = await import('./utils/custom-instructions.js'); serverContexts = buildServerContextsMap(Object.values(serverParams)); } catch (contextError) { // Log error but continue without custom instructions debugError('[ListTools Handler] Failed to fetch server contexts:', contextError); } // Prepare the response payload with custom instructions and constraints in metadata const toolsForClient = fetchedTools.map(({ _serverUuid, _serverName, ...rest }) => { // Add custom instructions and constraints to tool metadata if available if (_serverUuid) { const context = serverContexts.get(_serverUuid); if (context) { const toolWithMetadata = { ...rest, metadata: { server: _serverName || _serverUuid, instructions: context.rawInstructions, constraints: context.constraints, formattedContext: context.formattedContext } }; return toolWithMetadata; } } // Remove internal fields return rest; }); // Note: Pagination not handled here, assumes API returns all tools // Always include the static tools const allToolsForClient = [ discoverToolsStaticTool, askKnowledgeBaseStaticTool, createDocumentStaticTool, listDocumentsStaticTool, searchDocumentsStaticTool, getDocumentStaticTool, updateDocumentStaticTool, sendNotificationStaticTool, listNotificationsStaticTool, markNotificationDoneStaticTool, deleteNotificationStaticTool, clipboardSetStaticTool, clipboardGetStaticTool, clipboardDeleteStaticTool, clipboardListStaticTool, clipboardPushStaticTool, clipboardPopStaticTool, ...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); } }); // List Resources Handler - Returns available resources from the knowledge base server.setRequestHandler(ListResourcesRequestSchema, async () => { const { RESOURCE_REGISTRY } = await import('./resources/registry.js'); const { ensureAuth } = await import('./resources/helpers.js'); // Check auth status - always succeeds for non-auth resources const { key, base } = ensureAuth('pluggedin://setup', false); // Filter resources based on auth status return { resources: RESOURCE_REGISTRY .filter(r => !r.requiresAuth || (key && base)) .map(({ uri, mimeType, name, description }) => ({ uri, mimeType, name, description, })), }; }); // Read Resource Handler - Returns content of a specific resource server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; const { RESOURCE_REGISTRY } = await import('./resources/registry.js'); const { ensureAuth } = await import('./resources/helpers.js'); // Find resource definition const def = RESOURCE_REGISTRY.find(r => r.uri === uri); if (!def) { throw new Error(`Resource not found: ${uri}`); } // Check authentication if required ensureAuth(uri, def.requiresAuth); // Return resource content return { contents: [ { uri, mimeType: def.mimeType, text: def.getContent(), }, ], }; }); // 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 = STATIC_TOOLS_COUNT; 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 += `**Discovery (1):**\n`; dataContent += `1. **pluggedin_discover_tools** - Triggers discovery of tools for configured MCP servers\n`; dataContent += `\n**Knowledge Base (1):**\n`; dataContent += `2. **pluggedin_ask_knowledge_base** - Performs a RAG query against documents\n`; dataContent += `\n**Notifications (3):**\n`; dataContent += `3. **pluggedin_send_notification** - Send custom notifications with optional email\n`; dataContent += `4. **pluggedin_list_notifications** - List notifications with filters\n`; dataContent += `5. **pluggedin_mark_notification_done** - Mark a notification as done\n`; dataContent += `6. **pluggedin_delete_notification** - Delete a notification\n`; dataContent += `\n**Documents (5):**\n`; dataContent += `7. **pluggedin_create_document** - Create AI-generated documents\n`; dataContent += `8. **pluggedin_list_documents** - List documents with filtering\n`; dataContent += `9. **pluggedin_search_documents** - Search documents semantically\n`; dataContent += `10. **pluggedin_get_document** - Retrieve a specific document by ID\n`; dataContent += `11. **pluggedin_update_document** - Update or append to a document\n`; dataContent += `\n**Clipboard (7):**\n`; dataContent += `12. **pluggedin_clipboard_set** - Set a clipboard entry by name or index\n`; dataContent += `13. **pluggedin_clipboard_get** - Get clipboard entries with pagination\n`; dataContent += `14. **pluggedin_clipboard_delete** - Delete clipboard entries\n`; dataContent += `15. **pluggedin_clipboard_list** - List all clipboard entries (metadata only)\n`; dataContent += `16. **pluggedin_clipboard_push** - Push to indexed clipboard (auto-increment)\n`; dataContent += `17. **pluggedin_clipboard_pop** - Pop highest-indexed entry (LIFO)\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`; }); } // Add custom instructions section dataContent += await formatCustomInstructionsForDiscovery(); // 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 = 17; 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 - 17 total):\n`; staticContent += `**Discovery (1):** pluggedin_discover_tools\n`; staticContent += `**Knowledge Base (1):** pluggedin_ask_knowledge_base\n`; staticContent += `**Notifications (4):** pluggedin_send_notification, pluggedin_list_notifications, pluggedin_mark_notification_done, pluggedin_delete_notification\n`; staticContent += `**Documents (5):** pluggedin_create_document, pluggedin_list_documents, pluggedin_search_documents, pluggedin_get_document, pluggedin_update_document\n`; staticContent += `**Clipboard (7):** pluggedin_clipboard_set, pluggedin_clipboard_get, pluggedin_clipboard_delete, pluggedin_clipboard_list, pluggedin_clipboard_push, pluggedin_clipboard_pop\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, { force_refresh: false }, { 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, { force_refresh: true }, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 60000, // 60s timeout for background discovery }).catch(() => { // Ignore background discovery errors }); // 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 = 17; // Discovery, RAG, Notifications (4), Documents (5), Clipboard (7) 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 += `**Discovery (1):**\n`; forceRefreshContent += `1. **pluggedin_discover_tools** - Triggers discovery of tools for configured MCP servers\n`; forceRefreshContent += `\n**Knowledge Base (1):**\n`; forceRefreshContent += `2. **pluggedin_ask_knowledge_base** - Performs a RAG query against documents\n`; forceRefreshContent += `\n**Notifications (4):**\n`; forceRefreshContent += `3. **pluggedin_send_notification** - Send custom notifications with optional email\n`; forceRefreshContent += `4. **pluggedin_list_notifications** - List notifications with filters\n`; forceRefreshContent += `5. **pluggedin_mark_notification_done** - Mark a notification as done\n`; forceRefreshContent += `6. **pluggedin_delete_notification** - Delete a notification\n`; forceRefreshContent += `\n**Documents (5):**\n`; forceRefreshContent += `7. **pluggedin_create_document** - Create AI-generated documents\n`; forceRefreshContent += `8. **pluggedin_list_documents** - List documents with filtering\n`; forceRefreshContent += `9. **pluggedin_search_documents** - Search documents semantically\n`; forceRefreshContent += `10. **pluggedin_get_document** - Retrieve a specific document by ID\n`; forceRefreshContent += `11. **pluggedin_update_document** - Update or append to a document\n`; forceRefreshContent += `\n**Clipboard (7):**\n`; forceRefreshContent += `12. **pluggedin_clipboard_set** - Set a clipboard entry by name or index\n`; forceRefreshContent += `13. **pluggedin_clipboard_get** - Get clipboard entries with pagination\n`; forceRefreshContent += `14. **pluggedin_clipboard_delete** - Delete clipboard entries\n`; forceRefreshContent += `15. **pluggedin_clipboard_list** - List all clipboard entries (metadata only)\n`; forceRefreshContent += `16. **pluggedin_clipboard_push** - Push to indexed clipboard (auto-increment)\n`; forceRefreshContent += `17. **pluggedin_clipboard_pop** - Pop highest-indexed entry (LIFO)\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`; } // Add custom instructions section forceRefreshContent += await formatCustomInstructionsForDiscovery(); 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_ask_knowledge_base** - 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); }