@luutuankiet/mcp-proxy-shim 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core.d.ts +34 -0
- package/dist/core.js +13 -5
- package/dist/core.js.map +1 -1
- package/dist/daemon.d.ts +21 -32
- package/dist/daemon.js +397 -460
- package/dist/daemon.js.map +1 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/daemon.js
CHANGED
|
@@ -1,426 +1,395 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* MCP Proxy Shim — Daemon Mode
|
|
3
|
+
* MCP Proxy Shim — Daemon Mode (REST + MCP Gateway)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* and exposes
|
|
7
|
-
*
|
|
8
|
-
* Pure passthrough — no schema transformation. Designed for cloud agents that
|
|
9
|
-
* can't spawn MCP servers on the fly.
|
|
5
|
+
* Connects to a single upstream mcpproxy-go via MCP_URL (same as stdio/serve modes)
|
|
6
|
+
* and exposes both REST endpoints for curl-based subagents AND a Streamable HTTP
|
|
7
|
+
* /mcp endpoint for backward compatibility.
|
|
10
8
|
*
|
|
11
9
|
* Architecture:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* └── stdio ──▶ another process
|
|
10
|
+
* Subagent ──curl──▶ daemon (:3456) ──HTTP──▶ mcpproxy-go (upstream)
|
|
11
|
+
* MCP client ──HTTP──▶ daemon (:3456/mcp) ──HTTP──▶ mcpproxy-go (upstream)
|
|
15
12
|
*
|
|
16
|
-
*
|
|
13
|
+
* REST endpoints (clean JSON, no MCP wrappers):
|
|
14
|
+
* GET /health Health check + session info
|
|
15
|
+
* POST /retrieve_tools { query, compact?, limit? }
|
|
16
|
+
* POST /describe_tools { names: [...] }
|
|
17
|
+
* POST /call { method, name, args, reason?, sensitivity? }
|
|
18
|
+
* POST /exec { code }
|
|
19
|
+
* POST /reinit Force new upstream session
|
|
17
20
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* "type": "stdio",
|
|
21
|
-
* "command": "npx",
|
|
22
|
-
* "args": ["-y", "@modelcontextprotocol/server-github"],
|
|
23
|
-
* "env": { "GITHUB_TOKEN": "ghp_..." }
|
|
24
|
-
* },
|
|
25
|
-
* "my-api": {
|
|
26
|
-
* "type": "streamableHttp",
|
|
27
|
-
* "url": "https://api.example.com/mcp",
|
|
28
|
-
* "headers": { "Authorization": "Bearer xxx", "X-Custom": "value" }
|
|
29
|
-
* }
|
|
30
|
-
* }
|
|
21
|
+
* MCP endpoint (Streamable HTTP, backward compat):
|
|
22
|
+
* POST/GET/DELETE /mcp Standard MCP Streamable HTTP protocol
|
|
31
23
|
*
|
|
32
24
|
* Environment variables:
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
25
|
+
* MCP_URL (required) Upstream mcpproxy-go StreamableHTTP endpoint
|
|
26
|
+
* MCP_PORT (optional) Port to listen on (default: 3456)
|
|
27
|
+
* MCP_HOST (optional) Host to bind to (default: 0.0.0.0)
|
|
28
|
+
* MCP_APIKEY (optional) Require ?apikey=KEY on requests
|
|
29
|
+
* https_proxy (optional) HTTPS proxy for upstream connection
|
|
38
30
|
*
|
|
39
31
|
* Usage:
|
|
40
|
-
*
|
|
41
|
-
* npx @luutuankiet/mcp-proxy-shim daemon
|
|
42
|
-
*
|
|
43
|
-
* MCP_CONFIG=./mcp-servers.json npx @luutuankiet/mcp-proxy-shim daemon
|
|
32
|
+
* MCP_URL="https://proxy.example.com/mcp/?apikey=KEY" npx @luutuankiet/mcp-proxy-shim daemon
|
|
44
33
|
*/
|
|
45
|
-
import { readFileSync } from "node:fs";
|
|
34
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
35
|
+
import { join } from "node:path";
|
|
46
36
|
import { randomUUID } from "node:crypto";
|
|
47
37
|
import http from "node:http";
|
|
48
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
49
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
50
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
51
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
52
38
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
53
|
-
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
39
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
40
|
+
import { createShimServer, log, maskUrl, UPSTREAM_URL, ensureSession, mcpRequest, deepUnwrapResult, compactRetrieveTools, transformToolCallArgs, reinitOnExpiry, getSessionId, resetSessionId, } from "./core.js";
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Auto-load .cloud.env from CWD if MCP_URL not in environment
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
function autoLoadCloudEnv() {
|
|
45
|
+
if (process.env.MCP_URL)
|
|
46
|
+
return;
|
|
47
|
+
const envPath = join(process.cwd(), ".cloud.env");
|
|
48
|
+
if (!existsSync(envPath))
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
const content = readFileSync(envPath, "utf-8");
|
|
52
|
+
for (const line of content.split("\n")) {
|
|
53
|
+
const trimmed = line.trim();
|
|
54
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
55
|
+
continue;
|
|
56
|
+
const eqIdx = trimmed.indexOf("=");
|
|
57
|
+
if (eqIdx < 1)
|
|
58
|
+
continue;
|
|
59
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
60
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
61
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
62
|
+
val = val.slice(1, -1);
|
|
63
|
+
}
|
|
64
|
+
if (!process.env[key]) {
|
|
65
|
+
process.env[key] = val;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
log("Loaded environment from .cloud.env");
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
log("Warning: failed to load .cloud.env:", err.message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
60
74
|
// ---------------------------------------------------------------------------
|
|
61
75
|
// Config
|
|
62
76
|
// ---------------------------------------------------------------------------
|
|
63
77
|
const PORT = parseInt(process.env.MCP_PORT || "3456", 10);
|
|
64
78
|
const HOST = process.env.MCP_HOST || "0.0.0.0";
|
|
65
79
|
const APIKEY = process.env.MCP_APIKEY || null;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Also support .mcp.json format: { mcpServers: {...} }
|
|
87
|
-
return parsed.mcpServers || parsed.servers || parsed;
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
log("Fatal: Cannot read MCP_CONFIG:", err.message);
|
|
91
|
-
process.exit(1);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
log("Fatal: No server configuration provided.");
|
|
95
|
-
log("Set MCP_SERVERS (JSON) or MCP_CONFIG (file path).");
|
|
96
|
-
log("");
|
|
97
|
-
log("Example:");
|
|
98
|
-
log(` MCP_SERVERS='{"my-server":{"type":"stdio","command":"npx","args":["-y","some-mcp-server"]}}' \\`);
|
|
99
|
-
log(" npx @luutuankiet/mcp-proxy-shim daemon");
|
|
100
|
-
process.exit(1);
|
|
101
|
-
return {};
|
|
80
|
+
const startTime = Date.now();
|
|
81
|
+
let callCount = 0;
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// JSON body parser
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
function parseBody(req) {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const chunks = [];
|
|
88
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
89
|
+
req.on("end", () => {
|
|
90
|
+
try {
|
|
91
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
92
|
+
resolve(raw ? JSON.parse(raw) : {});
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
resolve({});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
req.on("error", () => resolve({}));
|
|
99
|
+
});
|
|
102
100
|
}
|
|
103
101
|
// ---------------------------------------------------------------------------
|
|
104
|
-
//
|
|
102
|
+
// REST response helpers
|
|
105
103
|
// ---------------------------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
function jsonResponse(res, status, body) {
|
|
105
|
+
res.writeHead(status, {
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
"Access-Control-Allow-Origin": "*",
|
|
108
|
+
});
|
|
109
|
+
res.end(JSON.stringify(body));
|
|
110
110
|
}
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return null;
|
|
111
|
+
/**
|
|
112
|
+
* Unwrap an MCP tools/call response to clean JSON for REST consumers.
|
|
113
|
+
* Parses content[0].text and JSON.parse if possible.
|
|
114
|
+
*/
|
|
115
|
+
function unwrapForRest(result) {
|
|
116
|
+
return deepUnwrapResult(result);
|
|
120
117
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// REST endpoint handlers
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
async function handleHealth(_req, res) {
|
|
122
|
+
const sid = getSessionId();
|
|
123
|
+
jsonResponse(res, 200, {
|
|
124
|
+
ok: !!sid,
|
|
125
|
+
sessionId: sid ? sid.slice(0, 12) + "..." : null,
|
|
126
|
+
uptime: Math.round((Date.now() - startTime) / 1000),
|
|
127
|
+
callCount,
|
|
129
128
|
});
|
|
130
|
-
await client.connect(transport);
|
|
131
|
-
log(`[${name}] Connected (pid: ${transport.pid ?? "unknown"})`);
|
|
132
|
-
// Fetch tools
|
|
133
|
-
const toolsResult = await client.listTools();
|
|
134
|
-
const tools = (toolsResult.tools || []);
|
|
135
|
-
log(`[${name}] ${tools.length} tools available`);
|
|
136
|
-
return { name, config, client, tools, connected: true };
|
|
137
129
|
}
|
|
138
|
-
async function
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
log(`[${name}] Custom headers: ${headerNames.join(", ")}`);
|
|
144
|
-
}
|
|
145
|
-
const client = new Client({ name: `mcp-daemon/${name}`, version: "1.0.0" }, { capabilities: {} });
|
|
146
|
-
const url = new URL(config.url);
|
|
147
|
-
// Build requestInit with custom headers and proxy support
|
|
148
|
-
const requestInit = {};
|
|
149
|
-
if (config.headers && Object.keys(config.headers).length > 0) {
|
|
150
|
-
requestInit.headers = config.headers;
|
|
130
|
+
async function handleRetrieveTools(req, res) {
|
|
131
|
+
const body = await parseBody(req);
|
|
132
|
+
const query = body.query;
|
|
133
|
+
if (!query) {
|
|
134
|
+
return jsonResponse(res, 400, { error: "query is required" });
|
|
151
135
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
136
|
+
try {
|
|
137
|
+
await ensureSession();
|
|
138
|
+
callCount++;
|
|
139
|
+
const resp = await mcpRequest("tools/call", {
|
|
140
|
+
name: "retrieve_tools",
|
|
141
|
+
arguments: body,
|
|
142
|
+
});
|
|
143
|
+
if (!resp || resp.error) {
|
|
144
|
+
return jsonResponse(res, 502, {
|
|
145
|
+
error: "upstream error",
|
|
146
|
+
detail: resp?.error || "no response",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const compacted = compactRetrieveTools(deepUnwrapResult(resp.result), body);
|
|
150
|
+
const unwrapped = unwrapForRest(compacted);
|
|
151
|
+
return jsonResponse(res, 200, unwrapped);
|
|
155
152
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
opts.requestInit = requestInit;
|
|
153
|
+
catch (err) {
|
|
154
|
+
return jsonResponse(res, 500, { error: err.message });
|
|
159
155
|
}
|
|
160
|
-
const transport = new StreamableHTTPClientTransport(url, opts);
|
|
161
|
-
await client.connect(transport);
|
|
162
|
-
log(`[${name}] Connected`);
|
|
163
|
-
// Fetch tools
|
|
164
|
-
const toolsResult = await client.listTools();
|
|
165
|
-
const tools = (toolsResult.tools || []);
|
|
166
|
-
log(`[${name}] ${tools.length} tools available`);
|
|
167
|
-
return { name, config, client, tools, connected: true };
|
|
168
156
|
}
|
|
169
|
-
|
|
157
|
+
/**
|
|
158
|
+
* describe_tools — batch-hydrate tool schemas.
|
|
159
|
+
*
|
|
160
|
+
* Strategy (mirrors core.ts shim-local describe_tools):
|
|
161
|
+
* 1. Derive multiple BM25 search queries from each requested name
|
|
162
|
+
* 2. Query upstream retrieve_tools for each (gets FULL schemas, not compacted)
|
|
163
|
+
* 3. Build an index keyed by raw name + server:name composite
|
|
164
|
+
* 4. Resolve each requested name with flexible matching
|
|
165
|
+
*/
|
|
166
|
+
async function handleDescribeTools(req, res) {
|
|
167
|
+
const body = await parseBody(req);
|
|
168
|
+
const names = body.names;
|
|
169
|
+
if (!Array.isArray(names) || names.length === 0) {
|
|
170
|
+
return jsonResponse(res, 400, { error: "names array is required" });
|
|
171
|
+
}
|
|
170
172
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
await ensureSession();
|
|
174
|
+
callCount++;
|
|
175
|
+
// 1. Derive search queries from tool names (same strategy as core.ts)
|
|
176
|
+
const queries = new Set();
|
|
177
|
+
for (const n of names) {
|
|
178
|
+
const raw = n.includes(":") ? n.split(":").slice(1).join(":") : n;
|
|
179
|
+
// Raw name as-is — BM25 often matches exact tool names well
|
|
180
|
+
queries.add(raw);
|
|
181
|
+
// Full name with separators → spaces
|
|
182
|
+
queries.add(raw.replace(/__/g, " ").replace(/[-_]/g, " "));
|
|
183
|
+
// Segment-based queries for compound names
|
|
184
|
+
const parts = raw.split("__");
|
|
185
|
+
if (parts.length > 1) {
|
|
186
|
+
const toolPart = parts[parts.length - 1] || raw;
|
|
187
|
+
queries.add(toolPart.replace(/[-_]/g, " "));
|
|
188
|
+
const prefixPart = parts[0];
|
|
189
|
+
if (prefixPart && prefixPart !== toolPart) {
|
|
190
|
+
queries.add(prefixPart.replace(/[-_]/g, " ") + " " + toolPart.replace(/[-_]/g, " "));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
176
193
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
194
|
+
// 2. Query upstream retrieve_tools for each (full schemas, no compaction)
|
|
195
|
+
const index = new Map();
|
|
196
|
+
for (const query of queries) {
|
|
197
|
+
try {
|
|
198
|
+
const resp = await mcpRequest("tools/call", {
|
|
199
|
+
name: "retrieve_tools",
|
|
200
|
+
arguments: { query },
|
|
201
|
+
});
|
|
202
|
+
if (resp?.result) {
|
|
203
|
+
const unwrapped = deepUnwrapResult(resp.result);
|
|
204
|
+
const tools = Array.isArray(unwrapped)
|
|
205
|
+
? unwrapped
|
|
206
|
+
: (Array.isArray(unwrapped?.tools) ? unwrapped.tools : []);
|
|
207
|
+
for (const t of tools) {
|
|
208
|
+
const tool = t;
|
|
209
|
+
const tName = tool.name;
|
|
210
|
+
if (tName && !index.has(tName)) {
|
|
211
|
+
index.set(tName, tool);
|
|
212
|
+
}
|
|
213
|
+
// Also key by server:name composite for flexible lookup
|
|
214
|
+
if (tool.server) {
|
|
215
|
+
const composite = `${tool.server}:${tName}`;
|
|
216
|
+
if (!index.has(composite))
|
|
217
|
+
index.set(composite, tool);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
log("describe_tools query failed:", query, err.message);
|
|
224
|
+
}
|
|
180
225
|
}
|
|
226
|
+
// 3. Resolve each requested name with flexible matching
|
|
227
|
+
const results = names.map((n) => {
|
|
228
|
+
// Exact match
|
|
229
|
+
let tool = index.get(n);
|
|
230
|
+
if (tool)
|
|
231
|
+
return tool;
|
|
232
|
+
// Without server: prefix (e.g., "utils:read_files" → "read_files")
|
|
233
|
+
if (n.includes(":")) {
|
|
234
|
+
const withoutPrefix = n.split(":").slice(1).join(":");
|
|
235
|
+
tool = index.get(withoutPrefix);
|
|
236
|
+
if (tool)
|
|
237
|
+
return tool;
|
|
238
|
+
}
|
|
239
|
+
// Suffix/prefix match
|
|
240
|
+
for (const [key, candidate] of index) {
|
|
241
|
+
if (key.endsWith(n) || n.endsWith(key))
|
|
242
|
+
return candidate;
|
|
243
|
+
}
|
|
244
|
+
// Fuzzy suffix match — handle mount-path variations
|
|
245
|
+
const nParts = n.includes(":") ? n.split(":").slice(1).join(":").split("__") : n.split("__");
|
|
246
|
+
const nSuffix = nParts[nParts.length - 1];
|
|
247
|
+
if (nSuffix) {
|
|
248
|
+
for (const [, candidate] of index) {
|
|
249
|
+
const cName = candidate.name;
|
|
250
|
+
const cParts = cName.split("__");
|
|
251
|
+
const cSuffix = cParts[cParts.length - 1];
|
|
252
|
+
if (cSuffix === nSuffix && cName.includes(nParts[0]))
|
|
253
|
+
return candidate;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return { name: n, error: "not found" };
|
|
257
|
+
});
|
|
258
|
+
return jsonResponse(res, 200, results);
|
|
181
259
|
}
|
|
182
260
|
catch (err) {
|
|
183
|
-
|
|
184
|
-
return null;
|
|
261
|
+
return jsonResponse(res, 500, { error: err.message });
|
|
185
262
|
}
|
|
186
263
|
}
|
|
187
|
-
|
|
188
|
-
|
|
264
|
+
async function handleCall(req, res) {
|
|
265
|
+
const body = await parseBody(req);
|
|
266
|
+
const method = body.method;
|
|
267
|
+
const name = body.name;
|
|
268
|
+
const args = (body.args || {});
|
|
269
|
+
if (!name) {
|
|
270
|
+
return jsonResponse(res, 400, { error: "name is required" });
|
|
271
|
+
}
|
|
272
|
+
const validMethods = [
|
|
273
|
+
"call_tool_read",
|
|
274
|
+
"call_tool_write",
|
|
275
|
+
"call_tool_destructive",
|
|
276
|
+
];
|
|
277
|
+
if (method && !validMethods.includes(method)) {
|
|
278
|
+
return jsonResponse(res, 400, {
|
|
279
|
+
error: `Invalid method: ${method}. Must be one of: ${validMethods.join(", ")}`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
189
282
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
283
|
+
await ensureSession();
|
|
284
|
+
callCount++;
|
|
285
|
+
const toolName = method || "call_tool_read";
|
|
286
|
+
const callArgs = { name, args };
|
|
287
|
+
if (body.reason)
|
|
288
|
+
callArgs.intent_reason = body.reason;
|
|
289
|
+
if (body.sensitivity)
|
|
290
|
+
callArgs.intent_data_sensitivity = body.sensitivity;
|
|
291
|
+
const forwardArgs = transformToolCallArgs(toolName, callArgs);
|
|
292
|
+
const resp = await mcpRequest("tools/call", {
|
|
293
|
+
name: toolName,
|
|
294
|
+
arguments: forwardArgs,
|
|
295
|
+
});
|
|
296
|
+
if (!resp) {
|
|
297
|
+
return jsonResponse(res, 502, { error: "No response from upstream" });
|
|
298
|
+
}
|
|
299
|
+
if (resp.error) {
|
|
300
|
+
if (resp.error.message?.includes("session") ||
|
|
301
|
+
resp.error.message?.includes("Session") ||
|
|
302
|
+
resp.error.code === -32001) {
|
|
303
|
+
const ok = await reinitOnExpiry();
|
|
304
|
+
if (ok) {
|
|
305
|
+
const retry = await mcpRequest("tools/call", {
|
|
306
|
+
name: toolName,
|
|
307
|
+
arguments: forwardArgs,
|
|
308
|
+
});
|
|
309
|
+
if (retry && !retry.error) {
|
|
310
|
+
return jsonResponse(res, 200, unwrapForRest(retry.result));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return jsonResponse(res, 502, {
|
|
315
|
+
error: "upstream error",
|
|
316
|
+
detail: resp.error,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return jsonResponse(res, 200, unwrapForRest(resp.result));
|
|
193
320
|
}
|
|
194
321
|
catch (err) {
|
|
195
|
-
|
|
322
|
+
return jsonResponse(res, 500, { error: err.message });
|
|
196
323
|
}
|
|
197
324
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const desc = tool.description ? ` — ${tool.description.slice(0, 80)}${tool.description.length > 80 ? "..." : ""}` : "";
|
|
220
|
-
lines.push(`- \`${namespaceTool(server.name, tool.name)}\`${desc}`);
|
|
325
|
+
async function handleExec(req, res) {
|
|
326
|
+
const body = await parseBody(req);
|
|
327
|
+
const code = body.code;
|
|
328
|
+
if (!code) {
|
|
329
|
+
return jsonResponse(res, 400, { error: "code is required" });
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
await ensureSession();
|
|
333
|
+
callCount++;
|
|
334
|
+
const resp = await mcpRequest("tools/call", {
|
|
335
|
+
name: "code_execution",
|
|
336
|
+
arguments: { code },
|
|
337
|
+
});
|
|
338
|
+
if (!resp) {
|
|
339
|
+
return jsonResponse(res, 502, { error: "No response from upstream" });
|
|
340
|
+
}
|
|
341
|
+
if (resp.error) {
|
|
342
|
+
return jsonResponse(res, 502, {
|
|
343
|
+
error: "upstream error",
|
|
344
|
+
detail: resp.error,
|
|
345
|
+
});
|
|
221
346
|
}
|
|
222
|
-
|
|
347
|
+
return jsonResponse(res, 200, unwrapForRest(resp.result));
|
|
223
348
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
lines.push("1. Find the tool you need from the list above");
|
|
227
|
-
lines.push("2. Call it using the full namespaced name (e.g., `github__get_file_contents`)");
|
|
228
|
-
lines.push("3. Pass arguments as a native JSON object — no special serialization needed");
|
|
229
|
-
lines.push("");
|
|
230
|
-
lines.push("### Example");
|
|
231
|
-
lines.push("");
|
|
232
|
-
// Generate a real example from the first server's first tool
|
|
233
|
-
if (servers.length > 0 && servers[0].tools.length > 0) {
|
|
234
|
-
const example = servers[0].tools[0];
|
|
235
|
-
const exampleName = namespaceTool(servers[0].name, example.name);
|
|
236
|
-
const exampleArgs = example.inputSchema?.properties
|
|
237
|
-
? Object.fromEntries(Object.entries(example.inputSchema.properties)
|
|
238
|
-
.slice(0, 3)
|
|
239
|
-
.map(([k, v]) => [k, v.type === "number" ? 1 : v.type === "boolean" ? true : "..."]))
|
|
240
|
-
: {};
|
|
241
|
-
lines.push("```json");
|
|
242
|
-
lines.push(`{ "name": "${exampleName}", "arguments": ${JSON.stringify(exampleArgs)} }`);
|
|
243
|
-
lines.push("```");
|
|
349
|
+
catch (err) {
|
|
350
|
+
return jsonResponse(res, 500, { error: err.message });
|
|
244
351
|
}
|
|
245
|
-
lines.push("");
|
|
246
|
-
return lines.join("\n");
|
|
247
352
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
};
|
|
264
|
-
function getAggregatedTools() {
|
|
265
|
-
const allTools = [DAEMON_HELP_TOOL];
|
|
266
|
-
for (const [, server] of upstreams) {
|
|
267
|
-
if (!server.connected)
|
|
268
|
-
continue;
|
|
269
|
-
for (const tool of server.tools) {
|
|
270
|
-
allTools.push({
|
|
271
|
-
...tool,
|
|
272
|
-
name: namespaceTool(server.name, tool.name),
|
|
273
|
-
description: `[${server.name}] ${tool.description || ""}`.trim(),
|
|
274
|
-
});
|
|
275
|
-
}
|
|
353
|
+
async function handleReinit(_req, res) {
|
|
354
|
+
try {
|
|
355
|
+
resetSessionId();
|
|
356
|
+
await ensureSession();
|
|
357
|
+
return jsonResponse(res, 200, {
|
|
358
|
+
ok: true,
|
|
359
|
+
sessionId: (getSessionId() || "").slice(0, 12) + "...",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
return jsonResponse(res, 500, {
|
|
364
|
+
ok: false,
|
|
365
|
+
error: err.message,
|
|
366
|
+
});
|
|
276
367
|
}
|
|
277
|
-
return allTools;
|
|
278
368
|
}
|
|
279
369
|
// ---------------------------------------------------------------------------
|
|
280
|
-
//
|
|
370
|
+
// Streamable HTTP /mcp endpoint (backward compat)
|
|
281
371
|
// ---------------------------------------------------------------------------
|
|
282
|
-
const
|
|
283
|
-
async function
|
|
372
|
+
const mcpTransports = new Map();
|
|
373
|
+
async function createMcpSessionTransport() {
|
|
284
374
|
const transport = new StreamableHTTPServerTransport({
|
|
285
375
|
sessionIdGenerator: () => randomUUID(),
|
|
286
376
|
onsessioninitialized: (sessionId) => {
|
|
287
|
-
log(`
|
|
288
|
-
|
|
377
|
+
log(`MCP session initialized: ${sessionId.slice(0, 12)}...`);
|
|
378
|
+
mcpTransports.set(sessionId, transport);
|
|
289
379
|
},
|
|
290
380
|
});
|
|
291
381
|
transport.onclose = () => {
|
|
292
382
|
const sid = transport.sessionId;
|
|
293
|
-
if (sid &&
|
|
294
|
-
log(`
|
|
295
|
-
|
|
383
|
+
if (sid && mcpTransports.has(sid)) {
|
|
384
|
+
log(`MCP session closed: ${sid.slice(0, 12)}...`);
|
|
385
|
+
mcpTransports.delete(sid);
|
|
296
386
|
}
|
|
297
387
|
};
|
|
298
|
-
|
|
299
|
-
const connectedServers = [...upstreams.values()].filter((s) => s.connected);
|
|
300
|
-
const serverList = connectedServers.map((s) => `${s.name} (${s.tools.length} tools)`).join(", ");
|
|
301
|
-
const instructions = [
|
|
302
|
-
"MCP Daemon — Multi-server gateway.",
|
|
303
|
-
`Connected servers: ${serverList}.`,
|
|
304
|
-
"All tools are namespaced as <server>__<tool> (double underscore).",
|
|
305
|
-
"Call daemon_help for a full usage guide with all available tools.",
|
|
306
|
-
].join(" ");
|
|
307
|
-
// Create MCP server for this session
|
|
308
|
-
const server = new Server({ name: "mcp-daemon", version: "1.0.0" }, {
|
|
309
|
-
capabilities: { tools: {} },
|
|
310
|
-
instructions,
|
|
311
|
-
});
|
|
312
|
-
// Handle tools/list — aggregate from all upstreams
|
|
313
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
314
|
-
// Refresh tools from all connected upstreams
|
|
315
|
-
await Promise.allSettled([...upstreams.values()]
|
|
316
|
-
.filter((s) => s.connected)
|
|
317
|
-
.map((s) => refreshTools(s)));
|
|
318
|
-
return { tools: getAggregatedTools() };
|
|
319
|
-
});
|
|
320
|
-
// Handle tools/call — route to correct upstream
|
|
321
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
322
|
-
const { name, arguments: args } = request.params;
|
|
323
|
-
// --- Shim-local: daemon_help ---
|
|
324
|
-
if (name === "daemon_help") {
|
|
325
|
-
const filterServer = args?.server || "";
|
|
326
|
-
if (filterServer) {
|
|
327
|
-
const upstream = upstreams.get(filterServer);
|
|
328
|
-
if (!upstream || !upstream.connected) {
|
|
329
|
-
return {
|
|
330
|
-
content: [{ type: "text", text: `Server "${filterServer}" not found. Connected servers: ${[...upstreams.keys()].join(", ")}` }],
|
|
331
|
-
isError: true,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
// Server-specific help
|
|
335
|
-
const lines = [
|
|
336
|
-
`# Server: ${filterServer} (${upstream.config.type})`,
|
|
337
|
-
"",
|
|
338
|
-
`## Tools (${upstream.tools.length})`,
|
|
339
|
-
"",
|
|
340
|
-
];
|
|
341
|
-
for (const tool of upstream.tools) {
|
|
342
|
-
lines.push(`### \`${namespaceTool(filterServer, tool.name)}\``);
|
|
343
|
-
if (tool.description)
|
|
344
|
-
lines.push(tool.description);
|
|
345
|
-
if (tool.inputSchema) {
|
|
346
|
-
lines.push("");
|
|
347
|
-
lines.push("```json");
|
|
348
|
-
lines.push(JSON.stringify(tool.inputSchema, null, 2));
|
|
349
|
-
lines.push("```");
|
|
350
|
-
}
|
|
351
|
-
lines.push("");
|
|
352
|
-
}
|
|
353
|
-
return {
|
|
354
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
return {
|
|
358
|
-
content: [{ type: "text", text: buildUsageGuide() }],
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
const parsed = parseNamespacedTool(name);
|
|
362
|
-
if (!parsed) {
|
|
363
|
-
return {
|
|
364
|
-
content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${name}. Tool names are prefixed with server name (e.g., "myserver__toolname"). Call daemon_help for a full list.` }) }],
|
|
365
|
-
isError: true,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
const server = upstreams.get(parsed.serverName);
|
|
369
|
-
if (!server || !server.connected) {
|
|
370
|
-
return {
|
|
371
|
-
content: [{ type: "text", text: JSON.stringify({ error: `Server "${parsed.serverName}" is not connected` }) }],
|
|
372
|
-
isError: true,
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
try {
|
|
376
|
-
const result = await server.client.callTool({
|
|
377
|
-
name: parsed.toolName,
|
|
378
|
-
arguments: args || {},
|
|
379
|
-
});
|
|
380
|
-
// Passthrough the result as-is
|
|
381
|
-
if (result.content && Array.isArray(result.content)) {
|
|
382
|
-
return {
|
|
383
|
-
content: result.content,
|
|
384
|
-
isError: result.isError === true ? true : undefined,
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
// Wrap non-standard results
|
|
388
|
-
const text = typeof result === "string" ? result : JSON.stringify(result);
|
|
389
|
-
return { content: [{ type: "text", text }] };
|
|
390
|
-
}
|
|
391
|
-
catch (err) {
|
|
392
|
-
const msg = err.message;
|
|
393
|
-
log(`[${parsed.serverName}] Tool call error (${parsed.toolName}):`, msg);
|
|
394
|
-
return {
|
|
395
|
-
content: [{ type: "text", text: `Error calling ${parsed.serverName}/${parsed.toolName}: ${msg}` }],
|
|
396
|
-
isError: true,
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
});
|
|
388
|
+
const server = await createShimServer({ lazyInit: true });
|
|
400
389
|
await server.connect(transport);
|
|
401
390
|
return transport;
|
|
402
391
|
}
|
|
403
|
-
// ---------------------------------------------------------------------------
|
|
404
|
-
// HTTP Server
|
|
405
|
-
// ---------------------------------------------------------------------------
|
|
406
|
-
function parseBody(req) {
|
|
407
|
-
return new Promise((resolve) => {
|
|
408
|
-
const chunks = [];
|
|
409
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
410
|
-
req.on("end", () => {
|
|
411
|
-
try {
|
|
412
|
-
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
413
|
-
resolve(raw ? JSON.parse(raw) : null);
|
|
414
|
-
}
|
|
415
|
-
catch {
|
|
416
|
-
resolve(null);
|
|
417
|
-
}
|
|
418
|
-
});
|
|
419
|
-
req.on("error", () => resolve(null));
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
392
|
async function handleMcpRequest(req, res) {
|
|
423
|
-
// CORS
|
|
424
393
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
425
394
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
426
395
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Last-Event-ID");
|
|
@@ -434,39 +403,33 @@ async function handleMcpRequest(req, res) {
|
|
|
434
403
|
try {
|
|
435
404
|
if (req.method === "POST") {
|
|
436
405
|
const body = await parseBody(req);
|
|
437
|
-
if (sessionId &&
|
|
438
|
-
const transport =
|
|
406
|
+
if (sessionId && mcpTransports.has(sessionId)) {
|
|
407
|
+
const transport = mcpTransports.get(sessionId);
|
|
439
408
|
await transport.handleRequest(req, res, body);
|
|
440
409
|
}
|
|
441
410
|
else if (!sessionId && isInitializeRequest(body)) {
|
|
442
|
-
const transport = await
|
|
411
|
+
const transport = await createMcpSessionTransport();
|
|
443
412
|
await transport.handleRequest(req, res, body);
|
|
444
413
|
}
|
|
445
414
|
else {
|
|
446
415
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
447
416
|
res.end(JSON.stringify({
|
|
448
417
|
jsonrpc: "2.0",
|
|
449
|
-
error: {
|
|
418
|
+
error: {
|
|
419
|
+
code: -32000,
|
|
420
|
+
message: "Bad Request: No valid session ID provided",
|
|
421
|
+
},
|
|
450
422
|
id: null,
|
|
451
423
|
}));
|
|
452
424
|
}
|
|
453
425
|
}
|
|
454
|
-
else if (req.method === "GET") {
|
|
455
|
-
if (!sessionId || !
|
|
456
|
-
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
457
|
-
res.end("Invalid or missing session ID");
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
const transport = downstreamTransports.get(sessionId);
|
|
461
|
-
await transport.handleRequest(req, res);
|
|
462
|
-
}
|
|
463
|
-
else if (req.method === "DELETE") {
|
|
464
|
-
if (!sessionId || !downstreamTransports.has(sessionId)) {
|
|
426
|
+
else if (req.method === "GET" || req.method === "DELETE") {
|
|
427
|
+
if (!sessionId || !mcpTransports.has(sessionId)) {
|
|
465
428
|
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
466
429
|
res.end("Invalid or missing session ID");
|
|
467
430
|
return;
|
|
468
431
|
}
|
|
469
|
-
const transport =
|
|
432
|
+
const transport = mcpTransports.get(sessionId);
|
|
470
433
|
await transport.handleRequest(req, res);
|
|
471
434
|
}
|
|
472
435
|
else {
|
|
@@ -475,7 +438,7 @@ async function handleMcpRequest(req, res) {
|
|
|
475
438
|
}
|
|
476
439
|
}
|
|
477
440
|
catch (error) {
|
|
478
|
-
log("
|
|
441
|
+
log("MCP handler error:", error.message);
|
|
479
442
|
if (!res.headersSent) {
|
|
480
443
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
481
444
|
res.end(JSON.stringify({
|
|
@@ -487,111 +450,85 @@ async function handleMcpRequest(req, res) {
|
|
|
487
450
|
}
|
|
488
451
|
}
|
|
489
452
|
// ---------------------------------------------------------------------------
|
|
490
|
-
//
|
|
453
|
+
// HTTP Server + routing
|
|
491
454
|
// ---------------------------------------------------------------------------
|
|
492
455
|
async function main() {
|
|
493
|
-
log("Starting MCP
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
log("
|
|
498
|
-
process.exit(1);
|
|
499
|
-
}
|
|
500
|
-
log(`Configured servers: ${serverNames.join(", ")}`);
|
|
501
|
-
// Connect to all upstream servers in parallel
|
|
502
|
-
const results = await Promise.allSettled(serverNames.map(async (name) => {
|
|
503
|
-
const server = await connectServer(name, configs[name]);
|
|
504
|
-
if (server) {
|
|
505
|
-
upstreams.set(name, server);
|
|
506
|
-
}
|
|
507
|
-
return server;
|
|
508
|
-
}));
|
|
509
|
-
const connected = [...upstreams.values()].filter((s) => s.connected);
|
|
510
|
-
const totalTools = connected.reduce((sum, s) => sum + s.tools.length, 0);
|
|
511
|
-
if (connected.length === 0) {
|
|
512
|
-
log("Fatal: No upstream servers connected successfully.");
|
|
513
|
-
process.exit(1);
|
|
456
|
+
log("Starting daemon (REST + MCP gateway)...");
|
|
457
|
+
log(`Upstream: ${maskUrl(UPSTREAM_URL)}`);
|
|
458
|
+
try {
|
|
459
|
+
await ensureSession();
|
|
460
|
+
log("Upstream session established");
|
|
514
461
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
log(` [${server.name}] ${server.tools.length} tools: ${server.tools.slice(0, 5).map((t) => t.name).join(", ")}${server.tools.length > 5 ? "..." : ""}`);
|
|
462
|
+
catch (err) {
|
|
463
|
+
log("Warning: initial upstream connection failed:", err.message);
|
|
464
|
+
log("Will retry on first request");
|
|
519
465
|
}
|
|
520
|
-
// Start HTTP server
|
|
521
466
|
const httpServer = http.createServer((req, res) => {
|
|
522
467
|
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
jsonrpc: "2.0",
|
|
529
|
-
error: { code: -32001, message: "Unauthorized: invalid or missing apikey" },
|
|
530
|
-
id: null,
|
|
531
|
-
}));
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
handleMcpRequest(req, res).catch((err) => {
|
|
535
|
-
log("Unhandled error:", err.message);
|
|
536
|
-
if (!res.headersSent) {
|
|
537
|
-
res.writeHead(500);
|
|
538
|
-
res.end("Internal Server Error");
|
|
539
|
-
}
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
else if (url.pathname === "/health" || url.pathname === "/healthz") {
|
|
543
|
-
const serverStatus = {};
|
|
544
|
-
for (const [name, server] of upstreams) {
|
|
545
|
-
serverStatus[name] = {
|
|
546
|
-
connected: server.connected,
|
|
547
|
-
tools: server.tools.length,
|
|
548
|
-
type: server.config.type,
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
552
|
-
res.end(JSON.stringify({
|
|
553
|
-
status: "ok",
|
|
554
|
-
sessions: downstreamTransports.size,
|
|
555
|
-
uptime: process.uptime(),
|
|
556
|
-
servers: serverStatus,
|
|
557
|
-
totalTools,
|
|
558
|
-
}));
|
|
468
|
+
const pathname = url.pathname;
|
|
469
|
+
if (APIKEY && url.searchParams.get("apikey") !== APIKEY) {
|
|
470
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
471
|
+
res.end(JSON.stringify({ error: "Unauthorized: invalid or missing apikey" }));
|
|
472
|
+
return;
|
|
559
473
|
}
|
|
560
|
-
|
|
561
|
-
res.
|
|
562
|
-
res.
|
|
474
|
+
if (req.method === "OPTIONS") {
|
|
475
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
476
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
477
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Last-Event-ID");
|
|
478
|
+
res.writeHead(204);
|
|
479
|
+
res.end();
|
|
480
|
+
return;
|
|
563
481
|
}
|
|
482
|
+
const handler = async () => {
|
|
483
|
+
if (pathname === "/health" || pathname === "/healthz") {
|
|
484
|
+
return handleHealth(req, res);
|
|
485
|
+
}
|
|
486
|
+
if (pathname === "/retrieve_tools" && req.method === "POST") {
|
|
487
|
+
return handleRetrieveTools(req, res);
|
|
488
|
+
}
|
|
489
|
+
if (pathname === "/describe_tools" && req.method === "POST") {
|
|
490
|
+
return handleDescribeTools(req, res);
|
|
491
|
+
}
|
|
492
|
+
if (pathname === "/call" && req.method === "POST") {
|
|
493
|
+
return handleCall(req, res);
|
|
494
|
+
}
|
|
495
|
+
if (pathname === "/exec" && req.method === "POST") {
|
|
496
|
+
return handleExec(req, res);
|
|
497
|
+
}
|
|
498
|
+
if (pathname === "/reinit" && req.method === "POST") {
|
|
499
|
+
return handleReinit(req, res);
|
|
500
|
+
}
|
|
501
|
+
if (pathname === "/mcp" || pathname === "/mcp/") {
|
|
502
|
+
return handleMcpRequest(req, res);
|
|
503
|
+
}
|
|
504
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
505
|
+
res.end(JSON.stringify({ error: "Not Found" }));
|
|
506
|
+
};
|
|
507
|
+
handler().catch((err) => {
|
|
508
|
+
log("Unhandled error:", err.message);
|
|
509
|
+
if (!res.headersSent) {
|
|
510
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
511
|
+
res.end(JSON.stringify({ error: "Internal Server Error" }));
|
|
512
|
+
}
|
|
513
|
+
});
|
|
564
514
|
});
|
|
565
515
|
httpServer.listen(PORT, HOST, () => {
|
|
566
|
-
log(`Daemon listening on http://${HOST}:${PORT}
|
|
567
|
-
log(
|
|
516
|
+
log(`Daemon listening on http://${HOST}:${PORT}`);
|
|
517
|
+
log("REST endpoints: /health, /retrieve_tools, /describe_tools, /call, /exec, /reinit");
|
|
518
|
+
log("MCP endpoint: /mcp");
|
|
568
519
|
log(`Auth: ${APIKEY ? "apikey required (?apikey=...)" : "OPEN (no MCP_APIKEY set)"}`);
|
|
569
|
-
log("Waiting for MCP client connections...");
|
|
570
520
|
});
|
|
571
|
-
// Graceful shutdown
|
|
572
521
|
const shutdown = async () => {
|
|
573
522
|
log("Shutting down daemon...");
|
|
574
|
-
|
|
575
|
-
for (const [sid, transport] of downstreamTransports) {
|
|
523
|
+
for (const [sid, transport] of mcpTransports) {
|
|
576
524
|
try {
|
|
577
525
|
await transport.close();
|
|
578
526
|
}
|
|
579
527
|
catch (err) {
|
|
580
|
-
log(`Error closing session ${sid.slice(0, 12)}:`, err.message);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
downstreamTransports.clear();
|
|
584
|
-
// Disconnect upstream servers
|
|
585
|
-
for (const [name, server] of upstreams) {
|
|
586
|
-
try {
|
|
587
|
-
log(`Disconnecting [${name}]...`);
|
|
588
|
-
await server.client.close();
|
|
589
|
-
}
|
|
590
|
-
catch (err) {
|
|
591
|
-
log(`Error disconnecting [${name}]:`, err.message);
|
|
528
|
+
log(`Error closing MCP session ${sid.slice(0, 12)}:`, err.message);
|
|
592
529
|
}
|
|
593
530
|
}
|
|
594
|
-
|
|
531
|
+
mcpTransports.clear();
|
|
595
532
|
httpServer.close(() => {
|
|
596
533
|
log("Daemon stopped");
|
|
597
534
|
process.exit(0);
|