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.
@@ -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
@@ -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
- // HONO HANDLERS
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
- ...serverConfig,
314
- cacheKeys: cache.keys(),
315
- cacheSize: cache.keys().length,
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.5.1",
2
+ "version": "1.6.1",
3
3
  "name": "modality-mcp-kit",
4
4
  "repository": {
5
5
  "type": "git",