Spaces:
Sleeping
Sleeping
Backup Agent commited on
Commit ·
271338b
1
Parent(s): bb096d9
Implement Super-MCP Aggregator Gateway and Auto-Parsing Registry
Browse files
index.js
CHANGED
|
@@ -2,9 +2,17 @@ import express from "express";
|
|
| 2 |
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
| 3 |
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
| 4 |
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
const app = express();
|
| 7 |
const PORT = process.env.PORT || 7860;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
const server = new Server(
|
| 10 |
{
|
|
@@ -18,7 +26,7 @@ const server = new Server(
|
|
| 18 |
}
|
| 19 |
);
|
| 20 |
|
| 21 |
-
// Define tools
|
| 22 |
const calculateTool = {
|
| 23 |
name: "calculate",
|
| 24 |
description: "Evaluate a mathematical expression in JS safely",
|
|
@@ -34,15 +42,86 @@ const calculateTool = {
|
|
| 34 |
}
|
| 35 |
};
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
});
|
| 42 |
|
| 43 |
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
| 46 |
try {
|
| 47 |
if (/[^0-9\+\-\*\/\(\)\s\.\,Mathsqrtpow]/i.test(expr)) {
|
| 48 |
return {
|
|
@@ -61,9 +140,70 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
| 61 |
};
|
| 62 |
}
|
| 63 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
throw new Error("Tool not found");
|
| 65 |
});
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
let transport = null;
|
| 68 |
|
| 69 |
app.get("/sse", (req, res) => {
|
|
@@ -80,6 +220,45 @@ app.post("/messages", express.json(), async (req, res) => {
|
|
| 80 |
}
|
| 81 |
});
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
app.post("/api/research", express.json(), async (req, res) => {
|
| 84 |
const { query } = req.body;
|
| 85 |
console.log(`[Library] Received research request for: '${query}'`);
|
|
@@ -137,6 +316,9 @@ app.post("/api/research", express.json(), async (req, res) => {
|
|
| 137 |
return res.json({ status: "success", brief: fallbackBrief });
|
| 138 |
});
|
| 139 |
|
|
|
|
|
|
|
|
|
|
| 140 |
app.listen(PORT, () => {
|
| 141 |
console.log(`SSE MCP Host listening on port ${PORT}`);
|
| 142 |
});
|
|
|
|
| 2 |
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
| 3 |
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
| 4 |
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
| 5 |
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
| 6 |
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
| 7 |
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
| 8 |
+
import fs from "fs";
|
| 9 |
|
| 10 |
const app = express();
|
| 11 |
const PORT = process.env.PORT || 7860;
|
| 12 |
+
const REGISTRY_PATH = "/tmp/mcp_registry.json";
|
| 13 |
+
|
| 14 |
+
// In-memory active sub-clients map
|
| 15 |
+
const activeClients = new Map(); // serverName -> { client, tools }
|
| 16 |
|
| 17 |
const server = new Server(
|
| 18 |
{
|
|
|
|
| 26 |
}
|
| 27 |
);
|
| 28 |
|
| 29 |
+
// Define local tools
|
| 30 |
const calculateTool = {
|
| 31 |
name: "calculate",
|
| 32 |
description: "Evaluate a mathematical expression in JS safely",
|
|
|
|
| 42 |
}
|
| 43 |
};
|
| 44 |
|
| 45 |
+
// ---------------------------------------------------------------------------
|
| 46 |
+
// Registry Loading & Sub-client Lifecycle
|
| 47 |
+
// ---------------------------------------------------------------------------
|
| 48 |
+
|
| 49 |
+
async function loadAndConnectServers() {
|
| 50 |
+
let registry = {};
|
| 51 |
+
if (fs.existsSync(REGISTRY_PATH)) {
|
| 52 |
+
try {
|
| 53 |
+
registry = JSON.parse(fs.readFileSync(REGISTRY_PATH, "utf8"));
|
| 54 |
+
} catch (e) {
|
| 55 |
+
console.error("Error reading registry:", e);
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// Close existing active clients
|
| 60 |
+
for (const [name, entry] of activeClients.entries()) {
|
| 61 |
+
try {
|
| 62 |
+
await entry.client.close();
|
| 63 |
+
} catch (e) {}
|
| 64 |
+
}
|
| 65 |
+
activeClients.clear();
|
| 66 |
+
|
| 67 |
+
// Connect to each registered server
|
| 68 |
+
for (const [name, serverConfig] of Object.entries(registry)) {
|
| 69 |
+
try {
|
| 70 |
+
console.log(`[Registry] Connecting to sub-server: ${name}...`);
|
| 71 |
+
let transport;
|
| 72 |
+
if (serverConfig.type === "sse") {
|
| 73 |
+
transport = new SSEClientTransport(new URL(serverConfig.data.url));
|
| 74 |
+
} else if (serverConfig.type === "stdio") {
|
| 75 |
+
transport = new StdioClientTransport({
|
| 76 |
+
command: serverConfig.data.command,
|
| 77 |
+
args: serverConfig.data.args || [],
|
| 78 |
+
env: { ...process.env, ...serverConfig.data.env }
|
| 79 |
+
});
|
| 80 |
+
} else {
|
| 81 |
+
continue;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
const client = new Client(
|
| 85 |
+
{ name: `proxy-${name}`, version: "1.0.0" },
|
| 86 |
+
{ capabilities: {} }
|
| 87 |
+
);
|
| 88 |
+
|
| 89 |
+
await client.connect(transport);
|
| 90 |
+
const toolsResult = await client.listTools();
|
| 91 |
+
const tools = toolsResult.tools || [];
|
| 92 |
+
|
| 93 |
+
activeClients.set(name, { client, tools });
|
| 94 |
+
console.log(`[Registry] Successfully registered '${name}' with ${tools.length} tools.`);
|
| 95 |
+
} catch (e) {
|
| 96 |
+
console.error(`[Registry Error] Failed to connect to '${name}':`, e.message);
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// ---------------------------------------------------------------------------
|
| 102 |
+
// Server Request Handlers
|
| 103 |
+
// ---------------------------------------------------------------------------
|
| 104 |
+
|
| 105 |
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
| 106 |
+
const list = [calculateTool];
|
| 107 |
+
for (const [serverName, entry] of activeClients.entries()) {
|
| 108 |
+
entry.tools.forEach(t => {
|
| 109 |
+
list.push({
|
| 110 |
+
name: `${serverName}__${t.name}`,
|
| 111 |
+
description: `[from ${serverName}] ${t.description}`,
|
| 112 |
+
inputSchema: t.inputSchema
|
| 113 |
+
});
|
| 114 |
+
});
|
| 115 |
+
}
|
| 116 |
+
return { tools: list };
|
| 117 |
});
|
| 118 |
|
| 119 |
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
| 120 |
+
const name = request.params.name;
|
| 121 |
+
const args = request.params.arguments;
|
| 122 |
+
|
| 123 |
+
if (name === "calculate") {
|
| 124 |
+
const expr = args?.expression;
|
| 125 |
try {
|
| 126 |
if (/[^0-9\+\-\*\/\(\)\s\.\,Mathsqrtpow]/i.test(expr)) {
|
| 127 |
return {
|
|
|
|
| 140 |
};
|
| 141 |
}
|
| 142 |
}
|
| 143 |
+
|
| 144 |
+
if (name.includes("__")) {
|
| 145 |
+
const idx = name.indexOf("__");
|
| 146 |
+
const serverName = name.substring(0, idx);
|
| 147 |
+
const actualToolName = name.substring(idx + 2);
|
| 148 |
+
|
| 149 |
+
const entry = activeClients.get(serverName);
|
| 150 |
+
if (!entry) {
|
| 151 |
+
throw new Error(`MCP sub-server '${serverName}' is not connected or active.`);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
const result = await entry.client.callTool({
|
| 155 |
+
name: actualToolName,
|
| 156 |
+
arguments: args
|
| 157 |
+
});
|
| 158 |
+
return result;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
throw new Error("Tool not found");
|
| 162 |
});
|
| 163 |
|
| 164 |
+
// ---------------------------------------------------------------------------
|
| 165 |
+
// Configuration Parser
|
| 166 |
+
// ---------------------------------------------------------------------------
|
| 167 |
+
|
| 168 |
+
function parseMcpConfig(input) {
|
| 169 |
+
// 1. Try parsing as JSON
|
| 170 |
+
try {
|
| 171 |
+
const json = JSON.parse(input);
|
| 172 |
+
if (json.mcpServers) {
|
| 173 |
+
return { type: "claude_desktop", data: json.mcpServers };
|
| 174 |
+
}
|
| 175 |
+
if (json.command) {
|
| 176 |
+
return { type: "stdio", data: { command: json.command, args: json.args || [], env: json.env || {} } };
|
| 177 |
+
}
|
| 178 |
+
if (json.url) {
|
| 179 |
+
return { type: "sse", data: { url: json.url } };
|
| 180 |
+
}
|
| 181 |
+
} catch (e) {
|
| 182 |
+
// Continue to raw string parsing
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
const str = input.trim();
|
| 186 |
+
|
| 187 |
+
// 2. Try parsing as SSE URL
|
| 188 |
+
if (str.startsWith("http://") || str.startsWith("https://")) {
|
| 189 |
+
return { type: "sse", data: { url: str } };
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// 3. Try parsing as stdio Command String
|
| 193 |
+
const parts = str.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g);
|
| 194 |
+
if (parts && parts.length > 0) {
|
| 195 |
+
const command = parts[0].replace(/^["']|["']$/g, "");
|
| 196 |
+
const args = parts.slice(1).map(arg => arg.replace(/^["']|["']$/g, ""));
|
| 197 |
+
return { type: "stdio", data: { command, args, env: {} } };
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
throw new Error("Invalid MCP configuration format");
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// ---------------------------------------------------------------------------
|
| 204 |
+
// Express Web Routes
|
| 205 |
+
// ---------------------------------------------------------------------------
|
| 206 |
+
|
| 207 |
let transport = null;
|
| 208 |
|
| 209 |
app.get("/sse", (req, res) => {
|
|
|
|
| 220 |
}
|
| 221 |
});
|
| 222 |
|
| 223 |
+
app.post("/api/mcp/register", express.json(), async (req, res) => {
|
| 224 |
+
const { name, config } = req.body;
|
| 225 |
+
if (!name || !config) {
|
| 226 |
+
return res.status(400).json({ error: "Missing name or config payload." });
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
try {
|
| 230 |
+
const parsed = parseMcpConfig(config);
|
| 231 |
+
let registry = {};
|
| 232 |
+
if (fs.existsSync(REGISTRY_PATH)) {
|
| 233 |
+
try {
|
| 234 |
+
registry = JSON.parse(fs.readFileSync(REGISTRY_PATH, "utf8"));
|
| 235 |
+
} catch (e) {}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
if (parsed.type === "claude_desktop") {
|
| 239 |
+
for (const [subName, subConfig] of Object.entries(parsed.data)) {
|
| 240 |
+
registry[subName] = {
|
| 241 |
+
type: "stdio",
|
| 242 |
+
data: {
|
| 243 |
+
command: subConfig.command,
|
| 244 |
+
args: subConfig.args || [],
|
| 245 |
+
env: subConfig.env || {}
|
| 246 |
+
}
|
| 247 |
+
};
|
| 248 |
+
}
|
| 249 |
+
} else {
|
| 250 |
+
registry[name] = parsed;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2), "utf8");
|
| 254 |
+
await loadAndConnectServers();
|
| 255 |
+
|
| 256 |
+
return res.json({ status: "success", parsed: parsed });
|
| 257 |
+
} catch (e) {
|
| 258 |
+
return res.status(500).json({ error: e.message });
|
| 259 |
+
}
|
| 260 |
+
});
|
| 261 |
+
|
| 262 |
app.post("/api/research", express.json(), async (req, res) => {
|
| 263 |
const { query } = req.body;
|
| 264 |
console.log(`[Library] Received research request for: '${query}'`);
|
|
|
|
| 316 |
return res.json({ status: "success", brief: fallbackBrief });
|
| 317 |
});
|
| 318 |
|
| 319 |
+
// Load existing registry on startup
|
| 320 |
+
loadAndConnectServers().catch(console.error);
|
| 321 |
+
|
| 322 |
app.listen(PORT, () => {
|
| 323 |
console.log(`SSE MCP Host listening on port ${PORT}`);
|
| 324 |
});
|