@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
JavaScript
/**
* 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);
}