@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
JavaScript
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