modality-mcp-kit 1.5.1 → 1.6.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.
@@ -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,39 @@ 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
+ }
200
229
  /**
201
230
  * Hono handler for MCP proxy
202
231
  * @param c - Hono context with mcpName param
203
232
  * @returns Response proxied from MCP server
204
233
  */
205
- export const mcpProxyHandler = (MCP_SERVERS) => async (c) => {
234
+ export const mcpProxyHandler = (MCP_SERVERS, oauthAllowAccess) => async (c) => {
206
235
  const mcpName = c.req.param("mcpName");
207
236
  if (!mcpName) {
208
237
  return c.json({
@@ -210,6 +239,38 @@ export const mcpProxyHandler = (MCP_SERVERS) => async (c) => {
210
239
  availableServers: Object.keys(MCP_SERVERS),
211
240
  }, 400);
212
241
  }
242
+ // Handle /_allow sub-route (OAuth access flow)
243
+ if (c.req.path.endsWith("/_allow")) {
244
+ const serverConfig = MCP_SERVERS[mcpName];
245
+ if (!serverConfig) {
246
+ return c.json({ error: "MCP server not found", availableServers: Object.keys(MCP_SERVERS) }, 404);
247
+ }
248
+ if (!oauthAllowAccess) {
249
+ return c.json({ error: "OAuth not configured for this proxy" }, 501);
250
+ }
251
+ try {
252
+ const result = await oauthAllowAccess(serverConfig.url, mcpName);
253
+ return c.json(result);
254
+ }
255
+ catch (err) {
256
+ return c.json({ error: "OAuth failed", message: err.message }, 500);
257
+ }
258
+ }
259
+ // Handle /_clear-auth sub-route
260
+ if (c.req.path.endsWith("/_clear-auth")) {
261
+ const serverConfig = MCP_SERVERS[mcpName];
262
+ if (!serverConfig) {
263
+ return c.json({ error: "MCP server not found", availableServers: Object.keys(MCP_SERVERS) }, 404);
264
+ }
265
+ const cleared = clearStoredOAuthTokens(serverConfig.url);
266
+ return c.json({
267
+ status: cleared ? "cleared" : "no_cache",
268
+ mcpName,
269
+ message: cleared
270
+ ? "OAuth tokens cleared"
271
+ : "No cached OAuth state found",
272
+ });
273
+ }
213
274
  // Handle /_cache sub-routes (matched via app.use prefix routing)
214
275
  const cachePathMatch = c.req.path.match(/\/_cache(?:\/(.+))?$/);
215
276
  if (cachePathMatch) {
@@ -272,10 +333,18 @@ export const mcpProxyHandler = (MCP_SERVERS) => async (c) => {
272
333
  upstreamHeaders[header] = value;
273
334
  }
274
335
  }
336
+ // Auto-inject stored OAuth token if no Authorization header present
337
+ if (!upstreamHeaders.authorization) {
338
+ const storedToken = getStoredOAuthToken(serverConfig.url);
339
+ if (storedToken) {
340
+ upstreamHeaders.authorization = `Bearer ${storedToken}`;
341
+ }
342
+ }
275
343
  try {
276
344
  const upstreamResponse = await fetch(serverConfig.url, {
277
345
  method: "GET",
278
346
  headers: upstreamHeaders,
347
+ signal: c.req.raw.signal,
279
348
  });
280
349
  // Forward response headers
281
350
  const responseHeaders = new Headers();
@@ -308,11 +377,22 @@ export const mcpProxyHandler = (MCP_SERVERS) => async (c) => {
308
377
  }
309
378
  // Regular GET - return server info
310
379
  const cache = getServerCache(mcpName);
380
+ const basePath = `/proxy/${mcpName}`;
311
381
  return c.json({
312
382
  mcpName,
313
- ...serverConfig,
314
- cacheKeys: cache.keys(),
315
- cacheSize: cache.keys().length,
383
+ upstream: serverConfig.url,
384
+ description: serverConfig.description,
385
+ endpoints: {
386
+ sse: `${basePath} (GET, Accept: text/event-stream)`,
387
+ rpc: `${basePath} (POST)`,
388
+ allow: `${basePath}/_allow`,
389
+ clearAuth: `${basePath}/_clear-auth`,
390
+ cache: `${basePath}/_cache`,
391
+ },
392
+ cache: {
393
+ keys: cache.keys(),
394
+ size: cache.keys().length,
395
+ },
316
396
  });
317
397
  }
318
398
  // POST request - proxy to MCP server with streaming support
@@ -384,6 +464,14 @@ export const mcpProxyHandler = (MCP_SERVERS) => async (c) => {
384
464
  console.log(`[MCP-PROXY] Forwarding header: ${header}=${value.substring(0, 50)}`);
385
465
  }
386
466
  }
467
+ // Auto-inject stored OAuth token if no Authorization header present
468
+ if (!upstreamHeaders.authorization) {
469
+ const storedToken = getStoredOAuthToken(serverConfig.url);
470
+ if (storedToken) {
471
+ upstreamHeaders.authorization = `Bearer ${storedToken}`;
472
+ console.log(`[MCP-PROXY] Using stored OAuth token for ${mcpName}`);
473
+ }
474
+ }
387
475
  console.log(`[MCP-PROXY] Upstream headers:`, Object.keys(upstreamHeaders));
388
476
  const upstreamResponse = await fetch(serverConfig.url, {
389
477
  method: "POST",
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.5.1",
2
+ "version": "1.6.0",
3
3
  "name": "modality-mcp-kit",
4
4
  "repository": {
5
5
  "type": "git",