modality-mcp-kit 1.5.1 → 1.6.1
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/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export { setupAITools, ModalityFastMCP } from "./util_mcp_tools_converter";
|
|
|
3
3
|
export type { AITools, AITool, FastMCPTool, } from "./schemas/schemas_tool_config";
|
|
4
4
|
export type { FastMCPCompatible, BasePrompt, } from "./util_mcp_tools_converter";
|
|
5
5
|
export { FastHonoMcp } from "./FastHonoMcp";
|
|
6
|
-
export { mcpProxyHandler, mcpProxyListHandler, mcpProxyCacheHandler, type McpProxyConfig, } from "./util_mcp_proxy";
|
|
6
|
+
export { mcpProxyHandler, mcpProxyListHandler, mcpProxyCacheHandler, type McpProxyConfig, type OAuthAllowAccessFn, } from "./util_mcp_proxy";
|
|
@@ -12,12 +12,16 @@ interface MCPServerConfig {
|
|
|
12
12
|
description?: string;
|
|
13
13
|
}
|
|
14
14
|
export type McpProxyConfig = Record<string, MCPServerConfig>;
|
|
15
|
+
export type OAuthAllowAccessFn = (serverUrl: string, mcpName: string) => Promise<{
|
|
16
|
+
status: string;
|
|
17
|
+
message?: string;
|
|
18
|
+
}>;
|
|
15
19
|
/**
|
|
16
20
|
* Hono handler for MCP proxy
|
|
17
21
|
* @param c - Hono context with mcpName param
|
|
18
22
|
* @returns Response proxied from MCP server
|
|
19
23
|
*/
|
|
20
|
-
export declare const mcpProxyHandler: (MCP_SERVERS: McpProxyConfig) => (c: any) => Promise<any>;
|
|
24
|
+
export declare const mcpProxyHandler: (MCP_SERVERS: McpProxyConfig, oauthAllowAccess?: OAuthAllowAccessFn) => (c: any) => Promise<any>;
|
|
21
25
|
/**
|
|
22
26
|
* Hono handler for listing available MCP servers
|
|
23
27
|
* @param c - Hono context
|
package/dist/util_mcp_proxy.js
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* Example: /proxy/figma → http://127.0.0.1:3845/mcp
|
|
9
9
|
*/
|
|
10
10
|
import { SimpleCache } from "modality-kit";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
11
15
|
// ============================================
|
|
12
16
|
// CACHE CONFIGURATION
|
|
13
17
|
// ============================================
|
|
@@ -195,14 +199,96 @@ function getAnyCacheForMethod(cache, method) {
|
|
|
195
199
|
}
|
|
196
200
|
}
|
|
197
201
|
// ============================================
|
|
198
|
-
//
|
|
202
|
+
// OAUTH TOKEN HELPERS
|
|
199
203
|
// ============================================
|
|
204
|
+
function getOAuthCachePath(serverUrl) {
|
|
205
|
+
const key = createHash("sha1").update(serverUrl).digest("hex").slice(0, 12);
|
|
206
|
+
return join(homedir(), ".cache", "counter", `${key}.json`);
|
|
207
|
+
}
|
|
208
|
+
function getStoredOAuthToken(serverUrl) {
|
|
209
|
+
try {
|
|
210
|
+
const data = JSON.parse(readFileSync(getOAuthCachePath(serverUrl), "utf8"));
|
|
211
|
+
return data.tokens?.access_token ?? null;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function clearStoredOAuthTokens(serverUrl) {
|
|
218
|
+
const cachePath = getOAuthCachePath(serverUrl);
|
|
219
|
+
try {
|
|
220
|
+
const data = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
221
|
+
delete data.tokens;
|
|
222
|
+
writeFileSync(cachePath, JSON.stringify(data, null, 2));
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ============================================
|
|
230
|
+
// TOOL PREFETCH HELPERS
|
|
231
|
+
// ============================================
|
|
232
|
+
function parseToolsFromBody(body) {
|
|
233
|
+
try {
|
|
234
|
+
const parsed = JSON.parse(body);
|
|
235
|
+
if (parsed?.result?.tools)
|
|
236
|
+
return parsed.result.tools;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// Try SSE format
|
|
240
|
+
const lines = body.split("\n");
|
|
241
|
+
for (const line of lines) {
|
|
242
|
+
if (line.startsWith("data: ")) {
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(line.substring(6));
|
|
245
|
+
if (parsed?.result?.tools)
|
|
246
|
+
return parsed.result.tools;
|
|
247
|
+
}
|
|
248
|
+
catch { }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
async function prefetchAndCacheTools(mcpName, serverUrl, cache, storedToken) {
|
|
255
|
+
const cacheKey = "tools/list";
|
|
256
|
+
const cached = cache.get(cacheKey);
|
|
257
|
+
if (cached) {
|
|
258
|
+
const dataLine = cached.match(/^data: (.+)$/m)?.[1] ?? cached;
|
|
259
|
+
return { tools: parseToolsFromBody(dataLine), fromCache: true };
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const headers = {
|
|
263
|
+
"Content-Type": "application/json",
|
|
264
|
+
Accept: "application/json, text/event-stream",
|
|
265
|
+
};
|
|
266
|
+
if (storedToken) {
|
|
267
|
+
headers.authorization = `Bearer ${storedToken}`;
|
|
268
|
+
}
|
|
269
|
+
const response = await fetch(serverUrl, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers,
|
|
272
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
|
|
273
|
+
});
|
|
274
|
+
const body = await response.text();
|
|
275
|
+
const ttl = METHOD_TTL_MS["tools/list"];
|
|
276
|
+
const cacheValue = body.includes("event:") ? body : `event: message\ndata: ${body}\n\n`;
|
|
277
|
+
cache.set(cacheKey, cacheValue, ttl);
|
|
278
|
+
console.log(`[MCP-PROXY] Prefetched and cached tools/list for ${mcpName}`);
|
|
279
|
+
return { tools: parseToolsFromBody(body), fromCache: false };
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
console.error(`[MCP-PROXY] Failed to prefetch tools for ${mcpName}:`, e);
|
|
283
|
+
return { tools: null, fromCache: false };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
200
286
|
/**
|
|
201
287
|
* Hono handler for MCP proxy
|
|
202
288
|
* @param c - Hono context with mcpName param
|
|
203
289
|
* @returns Response proxied from MCP server
|
|
204
290
|
*/
|
|
205
|
-
export const mcpProxyHandler = (MCP_SERVERS) => async (c) => {
|
|
291
|
+
export const mcpProxyHandler = (MCP_SERVERS, oauthAllowAccess) => async (c) => {
|
|
206
292
|
const mcpName = c.req.param("mcpName");
|
|
207
293
|
if (!mcpName) {
|
|
208
294
|
return c.json({
|
|
@@ -210,6 +296,54 @@ export const mcpProxyHandler = (MCP_SERVERS) => async (c) => {
|
|
|
210
296
|
availableServers: Object.keys(MCP_SERVERS),
|
|
211
297
|
}, 400);
|
|
212
298
|
}
|
|
299
|
+
// Handle /_allow sub-route (OAuth access flow)
|
|
300
|
+
if (c.req.path.endsWith("/_allow")) {
|
|
301
|
+
const serverConfig = MCP_SERVERS[mcpName];
|
|
302
|
+
if (!serverConfig) {
|
|
303
|
+
return c.json({ error: "MCP server not found", availableServers: Object.keys(MCP_SERVERS) }, 404);
|
|
304
|
+
}
|
|
305
|
+
if (!oauthAllowAccess) {
|
|
306
|
+
return c.json({ error: "OAuth not configured for this proxy" }, 501);
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const result = await oauthAllowAccess(serverConfig.url, mcpName);
|
|
310
|
+
return c.json(result);
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
return c.json({ error: "OAuth failed", message: err.message }, 500);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Handle /_clear-auth sub-route
|
|
317
|
+
if (c.req.path.endsWith("/_clear-auth")) {
|
|
318
|
+
const serverConfig = MCP_SERVERS[mcpName];
|
|
319
|
+
if (!serverConfig) {
|
|
320
|
+
return c.json({ error: "MCP server not found", availableServers: Object.keys(MCP_SERVERS) }, 404);
|
|
321
|
+
}
|
|
322
|
+
const cleared = clearStoredOAuthTokens(serverConfig.url);
|
|
323
|
+
return c.json({
|
|
324
|
+
status: cleared ? "cleared" : "no_cache",
|
|
325
|
+
mcpName,
|
|
326
|
+
message: cleared
|
|
327
|
+
? "OAuth tokens cleared"
|
|
328
|
+
: "No cached OAuth state found",
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// Handle /_tools sub-route — prefetch and return cached tool list
|
|
332
|
+
if (c.req.path.endsWith("/_tools")) {
|
|
333
|
+
const serverConfig = MCP_SERVERS[mcpName];
|
|
334
|
+
if (!serverConfig) {
|
|
335
|
+
return c.json({ error: "MCP server not found", availableServers: Object.keys(MCP_SERVERS) }, 404);
|
|
336
|
+
}
|
|
337
|
+
const cache = getServerCache(mcpName);
|
|
338
|
+
const storedToken = getStoredOAuthToken(serverConfig.url);
|
|
339
|
+
const { tools, fromCache } = await prefetchAndCacheTools(mcpName, serverConfig.url, cache, storedToken);
|
|
340
|
+
return c.json({
|
|
341
|
+
mcpName,
|
|
342
|
+
fromCache,
|
|
343
|
+
count: tools?.length ?? 0,
|
|
344
|
+
tools: tools ?? [],
|
|
345
|
+
});
|
|
346
|
+
}
|
|
213
347
|
// Handle /_cache sub-routes (matched via app.use prefix routing)
|
|
214
348
|
const cachePathMatch = c.req.path.match(/\/_cache(?:\/(.+))?$/);
|
|
215
349
|
if (cachePathMatch) {
|
|
@@ -272,10 +406,18 @@ export const mcpProxyHandler = (MCP_SERVERS) => async (c) => {
|
|
|
272
406
|
upstreamHeaders[header] = value;
|
|
273
407
|
}
|
|
274
408
|
}
|
|
409
|
+
// Auto-inject stored OAuth token if no Authorization header present
|
|
410
|
+
if (!upstreamHeaders.authorization) {
|
|
411
|
+
const storedToken = getStoredOAuthToken(serverConfig.url);
|
|
412
|
+
if (storedToken) {
|
|
413
|
+
upstreamHeaders.authorization = `Bearer ${storedToken}`;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
275
416
|
try {
|
|
276
417
|
const upstreamResponse = await fetch(serverConfig.url, {
|
|
277
418
|
method: "GET",
|
|
278
419
|
headers: upstreamHeaders,
|
|
420
|
+
signal: c.req.raw.signal,
|
|
279
421
|
});
|
|
280
422
|
// Forward response headers
|
|
281
423
|
const responseHeaders = new Headers();
|
|
@@ -308,11 +450,23 @@ export const mcpProxyHandler = (MCP_SERVERS) => async (c) => {
|
|
|
308
450
|
}
|
|
309
451
|
// Regular GET - return server info
|
|
310
452
|
const cache = getServerCache(mcpName);
|
|
453
|
+
const basePath = `/proxy/${mcpName}`;
|
|
311
454
|
return c.json({
|
|
312
455
|
mcpName,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
456
|
+
upstream: serverConfig.url,
|
|
457
|
+
description: serverConfig.description,
|
|
458
|
+
endpoints: {
|
|
459
|
+
sse: `${basePath} (GET, Accept: text/event-stream)`,
|
|
460
|
+
rpc: `${basePath} (POST)`,
|
|
461
|
+
tools: `${basePath}/_tools`,
|
|
462
|
+
allow: `${basePath}/_allow`,
|
|
463
|
+
clearAuth: `${basePath}/_clear-auth`,
|
|
464
|
+
cache: `${basePath}/_cache`,
|
|
465
|
+
},
|
|
466
|
+
cache: {
|
|
467
|
+
keys: cache.keys(),
|
|
468
|
+
size: cache.keys().length,
|
|
469
|
+
},
|
|
316
470
|
});
|
|
317
471
|
}
|
|
318
472
|
// POST request - proxy to MCP server with streaming support
|
|
@@ -384,6 +538,14 @@ export const mcpProxyHandler = (MCP_SERVERS) => async (c) => {
|
|
|
384
538
|
console.log(`[MCP-PROXY] Forwarding header: ${header}=${value.substring(0, 50)}`);
|
|
385
539
|
}
|
|
386
540
|
}
|
|
541
|
+
// Auto-inject stored OAuth token if no Authorization header present
|
|
542
|
+
if (!upstreamHeaders.authorization) {
|
|
543
|
+
const storedToken = getStoredOAuthToken(serverConfig.url);
|
|
544
|
+
if (storedToken) {
|
|
545
|
+
upstreamHeaders.authorization = `Bearer ${storedToken}`;
|
|
546
|
+
console.log(`[MCP-PROXY] Using stored OAuth token for ${mcpName}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
387
549
|
console.log(`[MCP-PROXY] Upstream headers:`, Object.keys(upstreamHeaders));
|
|
388
550
|
const upstreamResponse = await fetch(serverConfig.url, {
|
|
389
551
|
method: "POST",
|
package/package.json
CHANGED