@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
386 lines (385 loc) • 18.4 kB
JavaScript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, PingRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { sanitizeName } from "./utils.js";
import { cleanupAllSessions, initSessions } from "./sessions.js";
import { RateLimiter, sanitizeErrorMessage, validateToolName, validateRequestSize, } from "./security-utils.js";
import { debugLog, debugError } from "./debug-log.js";
import { createRequire } from 'module';
// Import refactored modules
import { staticTools } from './tools/static-tools.js';
import { StaticToolHandlers } from './handlers/static-handlers.js';
import { DynamicToolHandlers } from './handlers/dynamic-handlers.js';
import { staticPrompts, getStaticPrompt } from './utils/prompts.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 -> serverUuid
const instructionToServerMap = {};
// Initialize handlers
const staticToolHandlers = new StaticToolHandlers(toolToServerMap, instructionToServerMap);
const dynamicToolHandlers = new DynamicToolHandlers(toolToServerMap, instructionToServerMap);
// Initialize rate limiter (60 requests per minute)
const rateLimiter = new RateLimiter(60000, 60);
export class McpProxy {
server;
constructor() {
this.server = new Server({
name: "pluggedin-mcp",
version: packageJson.version,
}, {
capabilities: {
tools: {},
resources: {},
prompts: {},
},
});
this.setupHandlers();
this.setupErrorHandling();
}
setupErrorHandling() {
this.server.onerror = (error) => {
debugError("[MCP Proxy] Server error:", error);
console.error("[MCP Proxy] Server error:", error);
};
process.on('SIGINT', async () => {
debugLog('[MCP Proxy] Received SIGINT, cleaning up...');
await cleanupAllSessions();
process.exit(0);
});
process.on('SIGTERM', async () => {
debugLog('[MCP Proxy] Received SIGTERM, cleaning up...');
await cleanupAllSessions();
process.exit(0);
});
}
setupHandlers() {
// Handle ping requests
this.server.setRequestHandler(PingRequestSchema, async () => {
debugLog("[Ping Handler] Ping request received");
return {}; // Return empty object as per the protocol
});
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
debugError("[ListTools Handler] Listing available tools...");
try {
const connectedTools = [];
// Get dynamic tools from connected sessions
const sessions = global.sessions || {};
for (const [sessionKey, session] of Object.entries(sessions)) {
const sessionData = session;
if (sessionData?.serverCapabilities?.tools) {
sessionData.serverCapabilities.tools.forEach((tool) => {
const serverName = sessionData.serverName || 'unknown';
const prefixedName = sanitizeName(`${serverName}_${tool.name}`);
const prefixedDescription = `[${serverName}] ${tool.description}`;
connectedTools.push({
name: prefixedName,
description: prefixedDescription,
inputSchema: tool.inputSchema,
});
});
}
}
// Combine static and dynamic tools
const allTools = [...staticTools, ...connectedTools];
debugError(`[ListTools Handler] Returning ${allTools.length} tools (${staticTools.length} static, ${connectedTools.length} dynamic)`);
return {
tools: allTools,
};
}
catch (error) {
debugError("[ListTools Handler] Error listing tools:", error);
return { tools: staticTools };
}
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const requestedToolName = request.params.name;
const args = request.params.arguments;
debugError(`[CallTool Handler] Tool call received: ${requestedToolName}`);
debugLog(`[CallTool Handler] Arguments:`, args);
// Validate tool name
if (!validateToolName(requestedToolName)) {
throw new Error(`Invalid tool name: ${requestedToolName}`);
}
// Validate request size
if (!validateRequestSize(args)) {
throw new Error("Request payload too large");
}
// Apply rate limiting
if (!rateLimiter.checkLimit()) {
throw new Error(`Rate limit exceeded for tool: ${requestedToolName}. Please try again later.`);
}
try {
// Try static tools first
const staticResult = await staticToolHandlers.handleStaticTool(requestedToolName, args);
if (staticResult) {
return {
content: staticResult.content,
isError: staticResult.isError,
};
}
// Try dynamic tools
const dynamicResult = await dynamicToolHandlers.handleDynamicTool(requestedToolName, args);
if (dynamicResult) {
return {
content: dynamicResult.content,
isError: dynamicResult.isError,
};
}
// Try custom instructions
const instructionResult = await dynamicToolHandlers.handleCustomInstruction(requestedToolName, args);
if (instructionResult) {
return {
content: instructionResult.content,
isError: instructionResult.isError,
};
}
// Tool not found
throw new Error(`Unknown tool: ${requestedToolName}. Run 'pluggedin_discover_tools' to see available tools.`);
}
catch (error) {
debugError(`[CallTool Handler] Error executing tool ${requestedToolName}:`, error);
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error: ${sanitizeErrorMessage(error)}`,
},
],
isError: true,
};
}
});
// List available prompts
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
debugError("[ListPrompts Handler] Listing available prompts...");
try {
const allPrompts = [];
// Add static prompts
for (const [name, prompt] of Object.entries(staticPrompts)) {
allPrompts.push({
name: prompt.name,
description: prompt.description,
arguments: prompt.arguments
});
}
// Get dynamic prompts from connected sessions
const sessions = global.sessions || {};
for (const [sessionKey, session] of Object.entries(sessions)) {
const sessionData = session;
if (sessionData?.serverCapabilities?.prompts) {
sessionData.serverCapabilities.prompts.forEach((prompt) => {
const serverName = sessionData.serverName || 'unknown';
const prefixedName = sanitizeName(`${serverName}_${prompt.name}`);
const prefixedDescription = `[${serverName}] ${prompt.description}`;
allPrompts.push({
name: prefixedName,
description: prefixedDescription,
arguments: prompt.arguments || []
});
});
}
}
debugError(`[ListPrompts Handler] Returning ${allPrompts.length} prompts`);
return {
prompts: allPrompts,
};
}
catch (error) {
debugError("[ListPrompts Handler] Error listing prompts:", error);
return {
prompts: Object.values(staticPrompts).map(p => ({
name: p.name,
description: p.description,
arguments: p.arguments
}))
};
}
});
// Get specific prompt
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const promptName = request.params.name;
const promptArgs = request.params.arguments || {};
debugError(`[GetPrompt Handler] Getting prompt: ${promptName}`);
try {
// Check static prompts first
const staticPrompt = getStaticPrompt(promptName);
if (staticPrompt) {
return staticPrompt;
}
// Check dynamic prompts from connected servers
const sessions = global.sessions || {};
for (const [sessionKey, session] of Object.entries(sessions)) {
const sessionData = session;
if (sessionData?.serverCapabilities?.prompts) {
const serverName = sessionData.serverName || 'unknown';
// Check if this is the server that has the prompt
const originalPromptName = promptName.replace(new RegExp(`^${sanitizeName(serverName)}_`), '');
const prompt = sessionData.serverCapabilities.prompts.find((p) => p.name === originalPromptName || sanitizeName(`${serverName}_${p.name}`) === promptName);
if (prompt && sessionData.client) {
try {
const response = await sessionData.client.request({
method: "prompts/get",
params: {
name: originalPromptName,
arguments: promptArgs
}
});
return response;
}
catch (error) {
debugError(`[GetPrompt Handler] Error getting prompt from server:`, error);
throw error;
}
}
}
}
throw new Error(`Prompt not found: ${promptName}`);
}
catch (error) {
debugError(`[GetPrompt Handler] Error:`, error);
throw error;
}
});
// List resources
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
debugError("[ListResources Handler] Listing available resources...");
try {
const allResources = [];
// Get resources from connected sessions
const sessions = global.sessions || {};
for (const [sessionKey, session] of Object.entries(sessions)) {
const sessionData = session;
if (sessionData?.serverCapabilities?.resources) {
sessionData.serverCapabilities.resources.forEach((resource) => {
const serverName = sessionData.serverName || 'unknown';
const prefixedUri = `${serverName}://${resource.uri}`;
const prefixedName = resource.name ? `[${serverName}] ${resource.name}` : prefixedUri;
const prefixedDescription = resource.description ?
`[${serverName}] ${resource.description}` :
`Resource from ${serverName}`;
allResources.push({
uri: prefixedUri,
name: prefixedName,
description: prefixedDescription,
mimeType: resource.mimeType
});
});
}
}
debugError(`[ListResources Handler] Returning ${allResources.length} resources`);
return {
resources: allResources,
};
}
catch (error) {
debugError("[ListResources Handler] Error listing resources:", error);
return { resources: [] };
}
});
// Read resource
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const resourceUri = request.params.uri;
debugError(`[ReadResource Handler] Reading resource: ${resourceUri}`);
try {
// Parse the prefixed URI to extract server name and original URI
const match = resourceUri.match(/^([^:]+):\/\/(.+)$/);
if (!match) {
throw new Error(`Invalid resource URI format: ${resourceUri}`);
}
const serverName = match[1];
const originalUri = match[2];
// Find the session for this server
const sessions = global.sessions || {};
let targetSession = null;
for (const [sessionKey, session] of Object.entries(sessions)) {
const sessionData = session;
if (sessionData.serverName === serverName) {
targetSession = sessionData;
break;
}
}
if (!targetSession || !targetSession.client) {
throw new Error(`No active session found for server: ${serverName}`);
}
// Forward the read request to the actual server
const response = await targetSession.client.request({
method: "resources/read",
params: { uri: originalUri }
});
return response;
}
catch (error) {
debugError(`[ReadResource Handler] Error:`, error);
throw error;
}
});
// List resource templates
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
debugError("[ListResourceTemplates Handler] Listing resource templates...");
try {
const allTemplates = [];
// Get resource templates from connected sessions
const sessions = global.sessions || {};
for (const [sessionKey, session] of Object.entries(sessions)) {
const sessionData = session;
if (sessionData?.serverCapabilities?.resourceTemplates) {
sessionData.serverCapabilities.resourceTemplates.forEach((template) => {
const serverName = sessionData.serverName || 'unknown';
const prefixedUriTemplate = `${serverName}://${template.uriTemplate}`;
const prefixedName = template.name ? `[${serverName}] ${template.name}` : prefixedUriTemplate;
const prefixedDescription = template.description ?
`[${serverName}] ${template.description}` :
`Resource template from ${serverName}`;
allTemplates.push({
uriTemplate: prefixedUriTemplate,
name: prefixedName,
description: prefixedDescription,
mimeType: template.mimeType
});
});
}
}
debugError(`[ListResourceTemplates Handler] Returning ${allTemplates.length} templates`);
return {
resourceTemplates: allTemplates,
};
}
catch (error) {
debugError("[ListResourceTemplates Handler] Error listing resource templates:", error);
return { resourceTemplates: [] };
}
});
}
async run() {
debugLog("[MCP Proxy] Starting server...");
const transport = this.server.transport;
debugLog("[MCP Proxy] Server started successfully, running on stdio");
if (transport) {
await transport.start();
}
}
}
if (!global.sessions) {
global.sessions = {};
}
// Auto-connect to servers on startup
(async () => {
try {
await initSessions();
}
catch (error) {
debugError("[MCP Proxy] Failed to initialize sessions:", error);
}
})();
// Only run if this is the main module
if (import.meta.url === `file://${process.argv[1]}`) {
const proxy = new McpProxy();
proxy.run().catch((error) => {
console.error("Failed to run MCP Proxy:", error);
process.exit(1);
});
}