Backup Agent commited on
Commit
271338b
·
1 Parent(s): bb096d9

Implement Super-MCP Aggregator Gateway and Auto-Parsing Registry

Browse files
Files changed (1) hide show
  1. index.js +188 -6
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
- return {
39
- tools: [calculateTool]
40
- };
 
 
 
 
 
 
 
 
41
  });
42
 
43
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
44
- if (request.params.name === "calculate") {
45
- const expr = request.params.arguments?.expression;
 
 
 
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
  });