modality-mcp-kit 1.2.2 → 1.3.0
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/types/util_mcp_proxy.d.ts +33 -0
- package/dist/util_mcp_proxy.js +521 -0
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Proxy Utility for WebSocket Server
|
|
3
|
+
*
|
|
4
|
+
* Provides HTTP proxy functionality for MCP servers with caching,
|
|
5
|
+
* schema fixing, and fallback support.
|
|
6
|
+
*
|
|
7
|
+
* Usage: /proxy/:mcpName - proxies requests to configured MCP servers
|
|
8
|
+
* Example: /proxy/figma → http://127.0.0.1:3845/mcp
|
|
9
|
+
*/
|
|
10
|
+
interface MCPServerConfig {
|
|
11
|
+
url: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
export type McpProxyConfig = Record<string, MCPServerConfig>;
|
|
15
|
+
/**
|
|
16
|
+
* Hono handler for MCP proxy
|
|
17
|
+
* @param c - Hono context with mcpName param
|
|
18
|
+
* @returns Response proxied from MCP server
|
|
19
|
+
*/
|
|
20
|
+
export declare const mcpProxyHandler: (MCP_SERVERS: McpProxyConfig) => (c: any) => Promise<any>;
|
|
21
|
+
/**
|
|
22
|
+
* Hono handler for listing available MCP servers
|
|
23
|
+
* @param c - Hono context
|
|
24
|
+
* @returns List of available MCP servers
|
|
25
|
+
*/
|
|
26
|
+
export declare const mcpProxyListHandler: (MCP_SERVERS: McpProxyConfig) => (c: any) => any;
|
|
27
|
+
/**
|
|
28
|
+
* Hono handler for MCP proxy cache inspection
|
|
29
|
+
* @param c - Hono context with mcpName param
|
|
30
|
+
* @returns Cache contents for the specified MCP server
|
|
31
|
+
*/
|
|
32
|
+
export declare function mcpProxyCacheHandler(c: any): Promise<any>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Proxy Utility for WebSocket Server
|
|
3
|
+
*
|
|
4
|
+
* Provides HTTP proxy functionality for MCP servers with caching,
|
|
5
|
+
* schema fixing, and fallback support.
|
|
6
|
+
*
|
|
7
|
+
* Usage: /proxy/:mcpName - proxies requests to configured MCP servers
|
|
8
|
+
* Example: /proxy/figma → http://127.0.0.1:3845/mcp
|
|
9
|
+
*/
|
|
10
|
+
import { SimpleCache } from "modality-kit";
|
|
11
|
+
// ============================================
|
|
12
|
+
// CACHE CONFIGURATION
|
|
13
|
+
// ============================================
|
|
14
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
15
|
+
// Method-specific TTL configuration
|
|
16
|
+
const METHOD_TTL_MS = {
|
|
17
|
+
initialize: 30 * 60 * 1000, // 30 min
|
|
18
|
+
"tools/list": 5 * 60 * 1000, // 5 min
|
|
19
|
+
"resources/list": 1 * 60 * 1000, // 1 min
|
|
20
|
+
"prompts/list": 5 * 60 * 1000, // 5 min
|
|
21
|
+
};
|
|
22
|
+
// Cacheable MCP methods (read-only operations)
|
|
23
|
+
const CACHEABLE_METHODS = new Set([
|
|
24
|
+
"tools/list",
|
|
25
|
+
"resources/list",
|
|
26
|
+
"prompts/list",
|
|
27
|
+
"tools/call",
|
|
28
|
+
]);
|
|
29
|
+
// Special methods that should be cached for fallback but NOT served from cache directly
|
|
30
|
+
const FALLBACK_ONLY_METHODS = new Set(["initialize"]);
|
|
31
|
+
// Per-MCP server cache instances
|
|
32
|
+
const serverCaches = new Map();
|
|
33
|
+
function getServerCache(mcpName) {
|
|
34
|
+
let cache = serverCaches.get(mcpName);
|
|
35
|
+
if (!cache) {
|
|
36
|
+
cache = new SimpleCache({
|
|
37
|
+
ttlMs: DEFAULT_TTL_MS,
|
|
38
|
+
enableLru: true,
|
|
39
|
+
maxSize: 500,
|
|
40
|
+
});
|
|
41
|
+
serverCaches.set(mcpName, cache);
|
|
42
|
+
}
|
|
43
|
+
return cache;
|
|
44
|
+
}
|
|
45
|
+
function getMethodsFromRequest(requestData) {
|
|
46
|
+
if (!requestData)
|
|
47
|
+
return [];
|
|
48
|
+
if (Array.isArray(requestData)) {
|
|
49
|
+
return requestData
|
|
50
|
+
.map((r) => (r && typeof r === "object" ? r.method : undefined))
|
|
51
|
+
.filter((m) => typeof m === "string");
|
|
52
|
+
}
|
|
53
|
+
if (requestData?.method && typeof requestData.method === "string")
|
|
54
|
+
return [requestData.method];
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
function isRequestCacheable(requestData) {
|
|
58
|
+
const methods = getMethodsFromRequest(requestData);
|
|
59
|
+
if (methods.length === 0)
|
|
60
|
+
return false;
|
|
61
|
+
return methods.every((m) => CACHEABLE_METHODS.has(m));
|
|
62
|
+
}
|
|
63
|
+
function isRequestFallbackOnly(requestData) {
|
|
64
|
+
const methods = getMethodsFromRequest(requestData);
|
|
65
|
+
if (methods.length === 0)
|
|
66
|
+
return false;
|
|
67
|
+
return methods.some((m) => FALLBACK_ONLY_METHODS.has(m));
|
|
68
|
+
}
|
|
69
|
+
function shouldStoreRequestInCache(requestData) {
|
|
70
|
+
const methods = getMethodsFromRequest(requestData);
|
|
71
|
+
if (methods.length === 0)
|
|
72
|
+
return false;
|
|
73
|
+
return methods.every((m) => CACHEABLE_METHODS.has(m) || FALLBACK_ONLY_METHODS.has(m));
|
|
74
|
+
}
|
|
75
|
+
function getTTLForMethod(method) {
|
|
76
|
+
return METHOD_TTL_MS[method] || DEFAULT_TTL_MS;
|
|
77
|
+
}
|
|
78
|
+
function getTTLForMethods(methods) {
|
|
79
|
+
if (!methods || methods.length === 0)
|
|
80
|
+
return DEFAULT_TTL_MS;
|
|
81
|
+
const ttls = methods.map((m) => getTTLForMethod(m));
|
|
82
|
+
return Math.min(...ttls);
|
|
83
|
+
}
|
|
84
|
+
function stripMetaFromParams(params) {
|
|
85
|
+
if (!params || typeof params !== "object")
|
|
86
|
+
return params;
|
|
87
|
+
const { _meta, ...rest } = params;
|
|
88
|
+
return Object.keys(rest).length > 0 ? rest : null;
|
|
89
|
+
}
|
|
90
|
+
function generateCacheKey(requestData) {
|
|
91
|
+
try {
|
|
92
|
+
if (!requestData)
|
|
93
|
+
return null;
|
|
94
|
+
if (Array.isArray(requestData)) {
|
|
95
|
+
const parts = requestData
|
|
96
|
+
.map((r) => {
|
|
97
|
+
if (!r || typeof r !== "object")
|
|
98
|
+
return null;
|
|
99
|
+
const m = r.method;
|
|
100
|
+
if (!m || typeof m !== "string")
|
|
101
|
+
return null;
|
|
102
|
+
const strippedParams = stripMetaFromParams(r.params);
|
|
103
|
+
const p = strippedParams ? JSON.stringify(strippedParams) : null;
|
|
104
|
+
return p ? `${m}:${p}` : m;
|
|
105
|
+
})
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
if (parts.length === 0)
|
|
108
|
+
return null;
|
|
109
|
+
return parts.join("|");
|
|
110
|
+
}
|
|
111
|
+
const method = requestData?.method;
|
|
112
|
+
if (!method ||
|
|
113
|
+
typeof method !== "string" ||
|
|
114
|
+
method === "notifications/initialized")
|
|
115
|
+
return null;
|
|
116
|
+
const strippedParams = stripMetaFromParams(requestData?.params);
|
|
117
|
+
if (!strippedParams) {
|
|
118
|
+
return method;
|
|
119
|
+
}
|
|
120
|
+
return `${method}:${JSON.stringify(strippedParams)}`;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function replaceCachedResponseId(cachedResponse, requestId) {
|
|
127
|
+
if (requestId === undefined || requestId === null)
|
|
128
|
+
return cachedResponse;
|
|
129
|
+
try {
|
|
130
|
+
if (cachedResponse.includes("data:")) {
|
|
131
|
+
const lines = cachedResponse.split("\n");
|
|
132
|
+
const processedLines = lines.map((line) => {
|
|
133
|
+
if (line.startsWith("data: ")) {
|
|
134
|
+
const jsonStr = line.substring(6);
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(jsonStr);
|
|
137
|
+
if (parsed && typeof parsed === "object" && "jsonrpc" in parsed) {
|
|
138
|
+
parsed.id = requestId;
|
|
139
|
+
return "data: " + JSON.stringify(parsed);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Not valid JSON, return as-is
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return line;
|
|
147
|
+
});
|
|
148
|
+
return processedLines.join("\n");
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const parsed = JSON.parse(cachedResponse);
|
|
152
|
+
if (parsed && typeof parsed === "object" && "jsonrpc" in parsed) {
|
|
153
|
+
parsed.id = requestId;
|
|
154
|
+
return JSON.stringify(parsed);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Parse error, return original
|
|
160
|
+
}
|
|
161
|
+
return cachedResponse;
|
|
162
|
+
}
|
|
163
|
+
function getStaleCache(cache, cacheKey) {
|
|
164
|
+
try {
|
|
165
|
+
const entry = cache.get(cacheKey, true);
|
|
166
|
+
if (!entry)
|
|
167
|
+
return null;
|
|
168
|
+
return typeof entry === "string" ? entry : entry.data;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function getAnyCacheForMethod(cache, method) {
|
|
175
|
+
try {
|
|
176
|
+
let cached = cache.get(method, true);
|
|
177
|
+
if (cached?.data)
|
|
178
|
+
return cached.data;
|
|
179
|
+
if (typeof cached === "string")
|
|
180
|
+
return cached;
|
|
181
|
+
const keys = cache.keys();
|
|
182
|
+
for (const k of keys) {
|
|
183
|
+
if (k === method || k.startsWith(`${method}:`)) {
|
|
184
|
+
const entry = cache.get(k, true);
|
|
185
|
+
if (entry?.data)
|
|
186
|
+
return entry.data;
|
|
187
|
+
if (typeof entry === "string")
|
|
188
|
+
return entry;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// ============================================
|
|
198
|
+
// HONO HANDLERS
|
|
199
|
+
// ============================================
|
|
200
|
+
/**
|
|
201
|
+
* Hono handler for MCP proxy
|
|
202
|
+
* @param c - Hono context with mcpName param
|
|
203
|
+
* @returns Response proxied from MCP server
|
|
204
|
+
*/
|
|
205
|
+
export const mcpProxyHandler = (MCP_SERVERS) => async (c) => {
|
|
206
|
+
const mcpName = c.req.param("mcpName");
|
|
207
|
+
if (!mcpName) {
|
|
208
|
+
return c.json({
|
|
209
|
+
error: "MCP server name is required",
|
|
210
|
+
availableServers: Object.keys(MCP_SERVERS),
|
|
211
|
+
}, 400);
|
|
212
|
+
}
|
|
213
|
+
// Handle CORS preflight
|
|
214
|
+
if (c.req.method === "OPTIONS") {
|
|
215
|
+
return new Response(null, {
|
|
216
|
+
status: 200,
|
|
217
|
+
headers: {
|
|
218
|
+
"Access-Control-Allow-Origin": "*",
|
|
219
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS, DELETE",
|
|
220
|
+
"Access-Control-Allow-Headers": "Content-Type, Accept, mcp-session-id, mcp-protocol-version, Authorization",
|
|
221
|
+
"Access-Control-Expose-Headers": "mcp-session-id, mcp-protocol-version",
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// GET request - either SSE stream or server info
|
|
226
|
+
if (c.req.method === "GET") {
|
|
227
|
+
const serverConfig = MCP_SERVERS[mcpName];
|
|
228
|
+
if (!serverConfig) {
|
|
229
|
+
return c.json({
|
|
230
|
+
error: "MCP server not found",
|
|
231
|
+
availableServers: Object.keys(MCP_SERVERS),
|
|
232
|
+
}, 404);
|
|
233
|
+
}
|
|
234
|
+
const accept = c.req.header("accept") || "";
|
|
235
|
+
// If client wants SSE, proxy the GET request to upstream for SSE connection
|
|
236
|
+
if (accept.includes("text/event-stream")) {
|
|
237
|
+
console.log(`[MCP-PROXY] SSE GET request to ${mcpName}`);
|
|
238
|
+
// Forward headers to upstream
|
|
239
|
+
const incomingHeaders = c.req.raw.headers;
|
|
240
|
+
const upstreamHeaders = {
|
|
241
|
+
Accept: "text/event-stream",
|
|
242
|
+
};
|
|
243
|
+
// Forward MCP-specific headers
|
|
244
|
+
const headersToForward = [
|
|
245
|
+
"mcp-session-id",
|
|
246
|
+
"mcp-protocol-version",
|
|
247
|
+
"authorization",
|
|
248
|
+
"last-event-id",
|
|
249
|
+
];
|
|
250
|
+
for (const header of headersToForward) {
|
|
251
|
+
const value = incomingHeaders.get(header);
|
|
252
|
+
if (value) {
|
|
253
|
+
upstreamHeaders[header] = value;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const upstreamResponse = await fetch(serverConfig.url, {
|
|
258
|
+
method: "GET",
|
|
259
|
+
headers: upstreamHeaders,
|
|
260
|
+
});
|
|
261
|
+
// Forward response headers
|
|
262
|
+
const responseHeaders = new Headers();
|
|
263
|
+
upstreamResponse.headers.forEach((value, key) => {
|
|
264
|
+
if (!["transfer-encoding", "content-length", "connection"].includes(key.toLowerCase())) {
|
|
265
|
+
responseHeaders.set(key, value);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
|
269
|
+
responseHeaders.set("Access-Control-Expose-Headers", "mcp-session-id, mcp-protocol-version");
|
|
270
|
+
// Stream the SSE response
|
|
271
|
+
if (upstreamResponse.body) {
|
|
272
|
+
return new Response(upstreamResponse.body, {
|
|
273
|
+
status: upstreamResponse.status,
|
|
274
|
+
headers: responseHeaders,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return new Response(await upstreamResponse.text(), {
|
|
278
|
+
status: upstreamResponse.status,
|
|
279
|
+
headers: responseHeaders,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
console.error(`[MCP-PROXY] SSE GET error:`, error);
|
|
284
|
+
return c.json({
|
|
285
|
+
error: "Failed to establish SSE connection",
|
|
286
|
+
message: error.message,
|
|
287
|
+
}, 502);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Regular GET - return server info
|
|
291
|
+
const cache = getServerCache(mcpName);
|
|
292
|
+
return c.json({
|
|
293
|
+
mcpName,
|
|
294
|
+
...serverConfig,
|
|
295
|
+
cacheKeys: cache.keys(),
|
|
296
|
+
cacheSize: cache.keys().length,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
// POST request - proxy to MCP server with streaming support
|
|
300
|
+
if (c.req.method === "POST") {
|
|
301
|
+
const serverConfig = MCP_SERVERS[mcpName];
|
|
302
|
+
if (!serverConfig) {
|
|
303
|
+
return c.json({
|
|
304
|
+
error: "MCP server not found",
|
|
305
|
+
availableServers: Object.keys(MCP_SERVERS),
|
|
306
|
+
}, 404);
|
|
307
|
+
}
|
|
308
|
+
const requestBody = await c.req.text();
|
|
309
|
+
console.log(`[MCP-PROXY] Streaming proxy to ${mcpName}, body: ${requestBody.substring(0, 200)}`);
|
|
310
|
+
// Check for cached response first (for cacheable methods)
|
|
311
|
+
let requestData;
|
|
312
|
+
let cacheKey = null;
|
|
313
|
+
const cache = getServerCache(mcpName);
|
|
314
|
+
try {
|
|
315
|
+
requestData = JSON.parse(requestBody);
|
|
316
|
+
// Handle notifications/initialized locally
|
|
317
|
+
if (requestData === "notifications/initialized" ||
|
|
318
|
+
requestData?.method === "notifications/initialized") {
|
|
319
|
+
return c.json({ jsonrpc: "2.0", result: null, id: null });
|
|
320
|
+
}
|
|
321
|
+
cacheKey = generateCacheKey(requestData);
|
|
322
|
+
// Check cache for cacheable methods
|
|
323
|
+
if (cacheKey &&
|
|
324
|
+
isRequestCacheable(requestData) &&
|
|
325
|
+
!isRequestFallbackOnly(requestData)) {
|
|
326
|
+
const cached = cache.get(cacheKey);
|
|
327
|
+
if (cached) {
|
|
328
|
+
const outBody = replaceCachedResponseId(cached, requestData?.id);
|
|
329
|
+
const isSSE = outBody.includes("event:") || outBody.includes("\ndata:");
|
|
330
|
+
return new Response(outBody, {
|
|
331
|
+
status: 200,
|
|
332
|
+
headers: {
|
|
333
|
+
"Content-Type": isSSE
|
|
334
|
+
? "text/event-stream"
|
|
335
|
+
: "application/json",
|
|
336
|
+
"X-Cache-Status": "HIT",
|
|
337
|
+
"X-Cache-Key": cacheKey,
|
|
338
|
+
"Access-Control-Allow-Origin": "*",
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Invalid JSON, proceed without caching
|
|
346
|
+
}
|
|
347
|
+
// Stream the request to upstream and pipe response back
|
|
348
|
+
try {
|
|
349
|
+
// Forward relevant headers from incoming request to upstream
|
|
350
|
+
const incomingHeaders = c.req.raw.headers;
|
|
351
|
+
const upstreamHeaders = {
|
|
352
|
+
"Content-Type": "application/json",
|
|
353
|
+
Accept: "application/json, text/event-stream",
|
|
354
|
+
};
|
|
355
|
+
// Forward MCP-specific headers
|
|
356
|
+
const headersToForward = [
|
|
357
|
+
"mcp-session-id",
|
|
358
|
+
"mcp-protocol-version",
|
|
359
|
+
"authorization",
|
|
360
|
+
];
|
|
361
|
+
for (const header of headersToForward) {
|
|
362
|
+
const value = incomingHeaders.get(header);
|
|
363
|
+
if (value) {
|
|
364
|
+
upstreamHeaders[header] = value;
|
|
365
|
+
console.log(`[MCP-PROXY] Forwarding header: ${header}=${value.substring(0, 50)}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
console.log(`[MCP-PROXY] Upstream headers:`, Object.keys(upstreamHeaders));
|
|
369
|
+
const upstreamResponse = await fetch(serverConfig.url, {
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: upstreamHeaders,
|
|
372
|
+
body: requestBody,
|
|
373
|
+
});
|
|
374
|
+
// Get all response headers from upstream
|
|
375
|
+
const responseHeaders = new Headers();
|
|
376
|
+
upstreamResponse.headers.forEach((value, key) => {
|
|
377
|
+
// Skip problematic headers
|
|
378
|
+
if (!["transfer-encoding", "content-length", "connection"].includes(key.toLowerCase())) {
|
|
379
|
+
responseHeaders.set(key, value);
|
|
380
|
+
}
|
|
381
|
+
// Log MCP session header
|
|
382
|
+
if (key.toLowerCase() === "mcp-session-id") {
|
|
383
|
+
console.log(`[MCP-PROXY] Response mcp-session-id: ${value}`);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
|
387
|
+
responseHeaders.set("Access-Control-Expose-Headers", "mcp-session-id, mcp-protocol-version");
|
|
388
|
+
console.log(`[MCP-PROXY] Upstream response status: ${upstreamResponse.status}`);
|
|
389
|
+
// If upstream returns a body stream, pipe it directly
|
|
390
|
+
if (upstreamResponse.body) {
|
|
391
|
+
// For SSE, we need to pass through the stream
|
|
392
|
+
const contentType = upstreamResponse.headers.get("content-type") || "";
|
|
393
|
+
const isSSE = contentType.includes("text/event-stream") ||
|
|
394
|
+
contentType.includes("application/json");
|
|
395
|
+
if (isSSE) {
|
|
396
|
+
// Clone the response to read for caching while also streaming
|
|
397
|
+
const [streamForResponse, streamForCache] = upstreamResponse.body.tee();
|
|
398
|
+
// Cache in background (non-blocking)
|
|
399
|
+
if (cacheKey && shouldStoreRequestInCache(requestData)) {
|
|
400
|
+
(async () => {
|
|
401
|
+
try {
|
|
402
|
+
const reader = streamForCache.getReader();
|
|
403
|
+
const chunks = [];
|
|
404
|
+
let done = false;
|
|
405
|
+
while (!done) {
|
|
406
|
+
const result = await reader.read();
|
|
407
|
+
done = result.done;
|
|
408
|
+
if (result.value) {
|
|
409
|
+
chunks.push(result.value);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const fullBody = new TextDecoder().decode(new Uint8Array(chunks.reduce((acc, chunk) => [...acc, ...chunk], [])));
|
|
413
|
+
// Store in cache with proper SSE format
|
|
414
|
+
const methods = getMethodsFromRequest(requestData);
|
|
415
|
+
const ttl = getTTLForMethods(methods);
|
|
416
|
+
let cacheValue = fullBody;
|
|
417
|
+
if (!fullBody.includes("event:")) {
|
|
418
|
+
cacheValue = `event: message\ndata: ${fullBody}\n\n`;
|
|
419
|
+
}
|
|
420
|
+
cache.set(cacheKey, cacheValue, ttl);
|
|
421
|
+
console.log(`[MCP-PROXY] Cached response for ${cacheKey}`);
|
|
422
|
+
}
|
|
423
|
+
catch (e) {
|
|
424
|
+
console.error(`[MCP-PROXY] Cache error:`, e);
|
|
425
|
+
}
|
|
426
|
+
})();
|
|
427
|
+
}
|
|
428
|
+
return new Response(streamForResponse, {
|
|
429
|
+
status: upstreamResponse.status,
|
|
430
|
+
headers: responseHeaders,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// Fallback: buffer and return
|
|
435
|
+
const body = await upstreamResponse.text();
|
|
436
|
+
return new Response(body, {
|
|
437
|
+
status: upstreamResponse.status,
|
|
438
|
+
headers: responseHeaders,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
console.error(`[MCP-PROXY] Upstream error:`, error);
|
|
443
|
+
// Try fallback cache
|
|
444
|
+
const methods = getMethodsFromRequest(requestData);
|
|
445
|
+
let fallbackCache = cacheKey ? getStaleCache(cache, cacheKey) : null;
|
|
446
|
+
if (!fallbackCache && methods?.length > 0) {
|
|
447
|
+
for (const method of methods) {
|
|
448
|
+
fallbackCache = getAnyCacheForMethod(cache, method);
|
|
449
|
+
if (fallbackCache)
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (fallbackCache) {
|
|
454
|
+
const outBody = replaceCachedResponseId(fallbackCache, requestData?.id);
|
|
455
|
+
const isSSE = outBody.includes("event:") || outBody.includes("\ndata:");
|
|
456
|
+
return new Response(outBody, {
|
|
457
|
+
status: 200,
|
|
458
|
+
headers: {
|
|
459
|
+
"Content-Type": isSSE ? "text/event-stream" : "application/json",
|
|
460
|
+
"X-Cache-Status": "STALE",
|
|
461
|
+
"X-Cache-Reason": "upstream-error",
|
|
462
|
+
"Access-Control-Allow-Origin": "*",
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return c.json({
|
|
467
|
+
error: "Upstream server unavailable",
|
|
468
|
+
code: error.code || error.name || "UNKNOWN",
|
|
469
|
+
message: error.message,
|
|
470
|
+
upstream: serverConfig.url,
|
|
471
|
+
}, 502);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return c.json({ error: "Method not allowed" }, 405);
|
|
475
|
+
};
|
|
476
|
+
/**
|
|
477
|
+
* Hono handler for listing available MCP servers
|
|
478
|
+
* @param c - Hono context
|
|
479
|
+
* @returns List of available MCP servers
|
|
480
|
+
*/
|
|
481
|
+
export const mcpProxyListHandler = (MCP_SERVERS) => (c) => {
|
|
482
|
+
const servers = Object.entries(MCP_SERVERS).map(([name, config]) => ({
|
|
483
|
+
name,
|
|
484
|
+
url: config.url,
|
|
485
|
+
description: config.description,
|
|
486
|
+
proxyUrl: `/proxy/${name}`,
|
|
487
|
+
}));
|
|
488
|
+
return c.json({
|
|
489
|
+
servers,
|
|
490
|
+
count: servers.length,
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
/**
|
|
494
|
+
* Hono handler for MCP proxy cache inspection
|
|
495
|
+
* @param c - Hono context with mcpName param
|
|
496
|
+
* @returns Cache contents for the specified MCP server
|
|
497
|
+
*/
|
|
498
|
+
export async function mcpProxyCacheHandler(c) {
|
|
499
|
+
const mcpName = c.req.param("mcpName");
|
|
500
|
+
if (!mcpName) {
|
|
501
|
+
// Return all caches overview
|
|
502
|
+
const caches = {};
|
|
503
|
+
serverCaches.forEach((cache, name) => {
|
|
504
|
+
caches[name] = cache.keys();
|
|
505
|
+
});
|
|
506
|
+
return c.json({ caches });
|
|
507
|
+
}
|
|
508
|
+
const cache = serverCaches.get(mcpName);
|
|
509
|
+
if (!cache) {
|
|
510
|
+
return c.json({ error: "No cache for this MCP server", keys: [] });
|
|
511
|
+
}
|
|
512
|
+
const cacheKey = c.req.param("cacheKey");
|
|
513
|
+
if (cacheKey) {
|
|
514
|
+
const entry = cache.get(decodeURIComponent(cacheKey), true);
|
|
515
|
+
if (!entry) {
|
|
516
|
+
return c.json({ error: "Cache key not found", cacheKey }, 404);
|
|
517
|
+
}
|
|
518
|
+
return c.json({ cacheKey, value: entry });
|
|
519
|
+
}
|
|
520
|
+
return c.json({ keys: cache.keys() });
|
|
521
|
+
}
|
package/package.json
CHANGED