create-multicast 0.3.0 → 0.4.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/cli.js CHANGED
@@ -7,6 +7,8 @@ import { existsSync } from "node:fs";
7
7
  import { resolve, join, dirname } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { homedir } from "node:os";
10
+ import { createServer } from "node:http";
11
+ import { randomBytes } from "node:crypto";
10
12
  const __filename = fileURLToPath(import.meta.url);
11
13
  const __dirname = dirname(__filename);
12
14
  const TEMPLATES_DIR = resolve(__dirname, "..", "templates", "default");
@@ -70,6 +72,189 @@ async function processTemplates(targetDir, replacements) {
70
72
  await unlink(tmplPath);
71
73
  }
72
74
  }
75
+ // ── OAuth Flow ───────────────────────────────────────────────
76
+ // Discovers OAuth metadata, registers client, opens browser for auth,
77
+ // receives callback with code, exchanges for tokens.
78
+ async function discoverOAuthMetadata(serverUrl) {
79
+ const baseUrl = new URL(serverUrl);
80
+ const wellKnownPaths = [
81
+ `${baseUrl.origin}/.well-known/oauth-authorization-server`,
82
+ `${baseUrl.origin}/.well-known/openid-configuration`,
83
+ ];
84
+ for (const path of wellKnownPaths) {
85
+ try {
86
+ const res = await fetch(path, { signal: AbortSignal.timeout(5000) });
87
+ if (res.ok) {
88
+ const metadata = (await res.json());
89
+ if (metadata.authorization_endpoint && metadata.token_endpoint) {
90
+ return metadata;
91
+ }
92
+ }
93
+ }
94
+ catch {
95
+ // Try next
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ async function registerOAuthClient(registrationEndpoint, redirectUri) {
101
+ try {
102
+ const res = await fetch(registrationEndpoint, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({
106
+ client_name: "Multicast MCP Gateway",
107
+ redirect_uris: [redirectUri],
108
+ grant_types: ["authorization_code", "refresh_token"],
109
+ response_types: ["code"],
110
+ token_endpoint_auth_method: "none",
111
+ }),
112
+ signal: AbortSignal.timeout(10000),
113
+ });
114
+ if (!res.ok)
115
+ return null;
116
+ return (await res.json());
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ function generateCodeVerifier() {
123
+ return randomBytes(32).toString("base64url");
124
+ }
125
+ async function generateCodeChallenge(verifier) {
126
+ const { createHash } = await import("node:crypto");
127
+ return createHash("sha256").update(verifier).digest("base64url");
128
+ }
129
+ function openBrowser(url) {
130
+ const cmd = process.platform === "darwin"
131
+ ? "open"
132
+ : process.platform === "win32"
133
+ ? "cmd"
134
+ : "xdg-open";
135
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
136
+ try {
137
+ spawnSync(cmd, args, { stdio: "ignore" });
138
+ }
139
+ catch {
140
+ // Browser open failed silently — URL is shown in terminal
141
+ }
142
+ }
143
+ async function runOAuthFlow(serverUrl, metadata) {
144
+ const port = 9876 + Math.floor(Math.random() * 100);
145
+ const redirectUri = `http://localhost:${port}/oauth/callback`;
146
+ // Dynamic Client Registration
147
+ let clientId = "multicast-mcp-gateway";
148
+ let clientSecret;
149
+ if (metadata.registration_endpoint) {
150
+ const reg = await registerOAuthClient(metadata.registration_endpoint, redirectUri);
151
+ if (reg) {
152
+ clientId = reg.client_id;
153
+ clientSecret = reg.client_secret;
154
+ }
155
+ }
156
+ // PKCE
157
+ const codeVerifier = generateCodeVerifier();
158
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
159
+ const state = randomBytes(16).toString("hex");
160
+ // Build authorization URL
161
+ const authUrl = new URL(metadata.authorization_endpoint);
162
+ authUrl.searchParams.set("response_type", "code");
163
+ authUrl.searchParams.set("client_id", clientId);
164
+ authUrl.searchParams.set("redirect_uri", redirectUri);
165
+ authUrl.searchParams.set("code_challenge", codeChallenge);
166
+ authUrl.searchParams.set("code_challenge_method", "S256");
167
+ authUrl.searchParams.set("state", state);
168
+ authUrl.searchParams.set("scope", "mcp");
169
+ authUrl.searchParams.set("resource", serverUrl);
170
+ return new Promise((resolve) => {
171
+ let resolved = false;
172
+ const timer = setTimeout(() => {
173
+ if (!resolved) {
174
+ resolved = true;
175
+ httpServer.close();
176
+ resolve(null);
177
+ }
178
+ }, 120000);
179
+ const httpServer = createServer(async (req, res) => {
180
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
181
+ if (url.pathname !== "/oauth/callback") {
182
+ res.writeHead(404);
183
+ res.end("Not found");
184
+ return;
185
+ }
186
+ const code = url.searchParams.get("code");
187
+ const returnedState = url.searchParams.get("state");
188
+ const error = url.searchParams.get("error");
189
+ if (error || !code || returnedState !== state) {
190
+ res.writeHead(400, { "Content-Type": "text/html" });
191
+ res.end("<html><body><h2>Authorization failed</h2><p>You can close this window.</p></body></html>");
192
+ if (!resolved) {
193
+ resolved = true;
194
+ clearTimeout(timer);
195
+ httpServer.close();
196
+ resolve(null);
197
+ }
198
+ return;
199
+ }
200
+ try {
201
+ const tokenParams = new URLSearchParams({
202
+ grant_type: "authorization_code",
203
+ code,
204
+ redirect_uri: redirectUri,
205
+ client_id: clientId,
206
+ code_verifier: codeVerifier,
207
+ });
208
+ if (clientSecret)
209
+ tokenParams.set("client_secret", clientSecret);
210
+ const tokenRes = await fetch(metadata.token_endpoint, {
211
+ method: "POST",
212
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
213
+ body: tokenParams.toString(),
214
+ });
215
+ if (!tokenRes.ok) {
216
+ res.writeHead(500, { "Content-Type": "text/html" });
217
+ res.end("<html><body><h2>Token exchange failed</h2><p>You can close this window.</p></body></html>");
218
+ if (!resolved) {
219
+ resolved = true;
220
+ clearTimeout(timer);
221
+ httpServer.close();
222
+ resolve(null);
223
+ }
224
+ return;
225
+ }
226
+ const tokenData = (await tokenRes.json());
227
+ res.writeHead(200, { "Content-Type": "text/html" });
228
+ res.end("<html><body><h2>Authorized!</h2><p>You can close this window and return to the terminal.</p></body></html>");
229
+ if (!resolved) {
230
+ resolved = true;
231
+ clearTimeout(timer);
232
+ httpServer.close();
233
+ resolve({
234
+ access_token: tokenData.access_token,
235
+ refresh_token: tokenData.refresh_token,
236
+ token_endpoint: metadata.token_endpoint,
237
+ client_id: clientId,
238
+ expires_at: Math.floor(Date.now() / 1000) + (tokenData.expires_in || 3600),
239
+ });
240
+ }
241
+ }
242
+ catch {
243
+ res.writeHead(500, { "Content-Type": "text/html" });
244
+ res.end("<html><body><h2>Error</h2><p>You can close this window.</p></body></html>");
245
+ if (!resolved) {
246
+ resolved = true;
247
+ clearTimeout(timer);
248
+ httpServer.close();
249
+ resolve(null);
250
+ }
251
+ }
252
+ });
253
+ httpServer.listen(port, () => {
254
+ openBrowser(authUrl.toString());
255
+ });
256
+ });
257
+ }
73
258
  // ── MCP Config Discovery ─────────────────────────────────────
74
259
  // Scans known locations for existing MCP server configurations.
75
260
  function getMcpConfigPaths() {
@@ -270,13 +455,17 @@ async function main() {
270
455
  if (auth) {
271
456
  // Auth found in existing config — use it
272
457
  authStatus = "credentials found in config";
458
+ selectedServers.push({ name, url: server.url, auth });
273
459
  }
274
460
  else {
275
461
  // No auth in config — probe the server
276
462
  try {
277
463
  const probe = await fetch(server.url, {
278
464
  method: "POST",
279
- headers: { "Content-Type": "application/json" },
465
+ headers: {
466
+ "Content-Type": "application/json",
467
+ "Accept": "application/json, text/event-stream",
468
+ },
280
469
  body: JSON.stringify({
281
470
  jsonrpc: "2.0",
282
471
  method: "initialize",
@@ -290,43 +479,61 @@ async function main() {
290
479
  signal: AbortSignal.timeout(5000),
291
480
  });
292
481
  if (probe.status === 401 || probe.status === 403) {
293
- // Server requires auth but we don't have it — need to ask
482
+ // Server requires auth check if it's OAuth
294
483
  authStatus = "needs-auth";
295
484
  }
296
485
  else {
297
486
  // Server responded without auth — no auth needed
298
487
  authStatus = "no auth required";
488
+ selectedServers.push({ name, url: server.url });
299
489
  }
300
490
  }
301
491
  catch {
302
- // Can't reach server — assume no auth (will fail later with clear error)
303
492
  authStatus = "unreachable (skipping auth)";
493
+ selectedServers.push({ name, url: server.url });
304
494
  }
305
495
  }
306
496
  if (authStatus === "needs-auth") {
307
- // Only ask when we KNOW the server needs auth but we don't have it
308
- authSpinner.stop(`${pc.yellow("!")} ${name} requires authentication.`);
309
- const authResult = await p.text({
310
- message: `Enter auth header for ${pc.bold(name)}:`,
311
- placeholder: "Bearer your-token-here",
312
- validate: (v) => {
313
- if (!v.trim())
314
- return "Auth header is required — server returned 401";
315
- return undefined;
316
- },
317
- });
318
- if (p.isCancel(authResult)) {
319
- p.cancel("Setup cancelled.");
320
- process.exit(0);
497
+ // Check if server supports OAuth
498
+ authSpinner.stop(`${pc.yellow("!")} ${name} requires authentication. Checking for OAuth...`);
499
+ const oauthMetadata = await discoverOAuthMetadata(server.url);
500
+ if (oauthMetadata) {
501
+ // OAuth server — run the authorization flow
502
+ p.log.info(`${pc.bold(name)} uses OAuth. Opening browser for authorization...`);
503
+ p.log.info(pc.dim(`Authorization URL: ${oauthMetadata.authorization_endpoint}`));
504
+ const oauthResult = await runOAuthFlow(server.url, oauthMetadata);
505
+ if (oauthResult) {
506
+ p.log.success(`${name} authorized via OAuth.`);
507
+ selectedServers.push({
508
+ name,
509
+ url: server.url,
510
+ oauth: oauthResult,
511
+ });
512
+ }
513
+ else {
514
+ p.log.warn(`OAuth authorization failed for ${name}. Skipping.\n` +
515
+ ` ${pc.dim("You can add it manually later or re-run the installer.")}`);
516
+ }
517
+ }
518
+ else {
519
+ // Not OAuth — ask for static auth header
520
+ const authResult = await p.text({
521
+ message: `Enter auth header for ${pc.bold(name)}:`,
522
+ placeholder: "Bearer your-token-here",
523
+ validate: (v) => {
524
+ if (!v.trim())
525
+ return "Auth header is required — server returned 401";
526
+ return undefined;
527
+ },
528
+ });
529
+ if (p.isCancel(authResult)) {
530
+ p.cancel("Setup cancelled.");
531
+ process.exit(0);
532
+ }
533
+ selectedServers.push({ name, url: server.url, auth: authResult });
321
534
  }
322
- auth = authResult;
323
535
  authSpinner.start("Detecting authentication requirements...");
324
536
  }
325
- selectedServers.push({
326
- name,
327
- url: server.url,
328
- auth: auth || undefined,
329
- });
330
537
  }
331
538
  authSpinner.stop("Authentication configured.");
332
539
  // Show auth summary
@@ -509,8 +716,35 @@ async function main() {
509
716
  else {
510
717
  p.log.success(`${server.name} URL set.`);
511
718
  }
512
- // Set auth if present
513
- if (server.auth) {
719
+ // Set auth: static header OR OAuth tokens
720
+ if (server.oauth) {
721
+ // OAuth server — store tokens in D1 and set MCP_OAUTH_ flag
722
+ const oauthFlag = spawnSync("npx", ["wrangler", "secret", "put", `MCP_OAUTH_${envName}`], {
723
+ cwd: targetDir,
724
+ input: "true\n",
725
+ stdio: ["pipe", "inherit", "inherit"],
726
+ encoding: "utf-8",
727
+ });
728
+ if (oauthFlag.status !== 0) {
729
+ p.log.warn(`Failed to set OAuth flag for ${server.name}.`);
730
+ }
731
+ // Insert tokens into D1
732
+ const sql = `INSERT OR REPLACE INTO oauth_tokens (server_name, access_token, refresh_token, token_endpoint, client_id, expires_at) VALUES ('${server.name}', '${server.oauth.access_token}', ${server.oauth.refresh_token ? `'${server.oauth.refresh_token}'` : "NULL"}, '${server.oauth.token_endpoint}', '${server.oauth.client_id}', ${server.oauth.expires_at})`;
733
+ const dbInsert = spawnSync("npx", ["wrangler", "d1", "execute", `${projectName}-db`, "--remote", `--command=${sql}`], {
734
+ cwd: targetDir,
735
+ stdio: ["inherit", "inherit", "inherit"],
736
+ encoding: "utf-8",
737
+ });
738
+ if (dbInsert.status !== 0) {
739
+ p.log.warn(`Failed to store OAuth tokens for ${server.name} in D1.\n` +
740
+ ` ${pc.dim("You may need to re-run the installer to authorize again.")}`);
741
+ }
742
+ else {
743
+ p.log.success(`${server.name} OAuth tokens stored.`);
744
+ }
745
+ }
746
+ else if (server.auth) {
747
+ // Static auth header
514
748
  const authResult = spawnSync("npx", ["wrangler", "secret", "put", `MCP_AUTH_${envName}`], {
515
749
  cwd: targetDir,
516
750
  input: server.auth + "\n",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-multicast",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Create a Multicast MCP gateway — one command to scaffold, configure, and deploy your parallel MCP server.",
5
5
  "type": "module",
6
6
  "bin": "./dist/cli.js",
@@ -39,6 +39,19 @@ CREATE TABLE IF NOT EXISTS result_cache (
39
39
  created_at TEXT DEFAULT (datetime('now'))
40
40
  );
41
41
 
42
+ -- OAuth tokens: cached access/refresh tokens for OAuth-based MCP servers
43
+ -- Installer stores initial tokens during setup. Worker refreshes automatically
44
+ -- when access_token expires using the refresh_token + token_endpoint.
45
+ CREATE TABLE IF NOT EXISTS oauth_tokens (
46
+ server_name TEXT PRIMARY KEY,
47
+ access_token TEXT NOT NULL,
48
+ refresh_token TEXT,
49
+ token_endpoint TEXT NOT NULL,
50
+ client_id TEXT NOT NULL,
51
+ expires_at INTEGER NOT NULL, -- unix timestamp (seconds)
52
+ updated_at TEXT DEFAULT (datetime('now'))
53
+ );
54
+
42
55
  -- Indexes for common queries
43
56
  CREATE INDEX IF NOT EXISTS idx_tools_server ON tools(server_name);
44
57
  CREATE INDEX IF NOT EXISTS idx_servers_status ON servers(status);
@@ -14,6 +14,16 @@ interface RegisteredServer {
14
14
  name: string;
15
15
  url: string;
16
16
  auth?: string;
17
+ isOAuth?: boolean; // true if server uses OAuth (tokens in D1)
18
+ }
19
+
20
+ interface OAuthTokenRow {
21
+ server_name: string;
22
+ access_token: string;
23
+ refresh_token: string | null;
24
+ token_endpoint: string;
25
+ client_id: string;
26
+ expires_at: number;
17
27
  }
18
28
 
19
29
  interface CallResult {
@@ -39,9 +49,10 @@ interface CachedTool {
39
49
  const INLINE_RESULT_MAX_CHARS = 5000;
40
50
 
41
51
  // ── Server Registry ──────────────────────────────────────────
42
- // Parses MCP_SERVER_* and MCP_AUTH_* env vars at request time.
52
+ // Parses MCP_SERVER_*, MCP_AUTH_*, and MCP_OAUTH_* env vars at request time.
43
53
  // MCP_SERVER_CONTEXT_HUB=https://... → server name: "context-hub"
44
- // MCP_AUTH_CONTEXT_HUB=Bearer key... → auth header for "context-hub"
54
+ // MCP_AUTH_CONTEXT_HUB=Bearer key... → static auth header
55
+ // MCP_OAUTH_NEOSAPIEN=true → OAuth server (tokens managed in D1)
45
56
 
46
57
  function getRegisteredServers(env: Env): Map<string, RegisteredServer> {
47
58
  const servers = new Map<string, RegisteredServer>();
@@ -51,15 +62,118 @@ function getRegisteredServers(env: Env): Map<string, RegisteredServer> {
51
62
  const rawName = key.replace("MCP_SERVER_", "");
52
63
  const name = rawName.toLowerCase().replace(/_/g, "-");
53
64
  const authKey = `MCP_AUTH_${rawName}`;
65
+ const oauthKey = `MCP_OAUTH_${rawName}`;
54
66
  const auth = typeof env[authKey] === "string" ? (env[authKey] as string) : undefined;
67
+ const isOAuth = typeof env[oauthKey] === "string" && env[oauthKey] === "true";
55
68
 
56
- servers.set(name, { name, url: value, auth });
69
+ servers.set(name, { name, url: value, auth, isOAuth });
57
70
  }
58
71
  }
59
72
 
60
73
  return servers;
61
74
  }
62
75
 
76
+ // ── SSE Response Parser ──────────────────────────────────────
77
+ // MCP Streamable HTTP servers may respond with either application/json
78
+ // or text/event-stream. This helper parses both formats.
79
+
80
+ async function parseJsonOrSse(response: Response): Promise<unknown> {
81
+ const contentType = response.headers.get("content-type") || "";
82
+
83
+ if (contentType.includes("text/event-stream")) {
84
+ // Parse SSE: extract the last JSON data line
85
+ const text = await response.text();
86
+ const lines = text.split("\n");
87
+ let lastData = "";
88
+ for (const line of lines) {
89
+ if (line.startsWith("data: ")) {
90
+ lastData = line.slice(6);
91
+ }
92
+ }
93
+ if (lastData) {
94
+ return JSON.parse(lastData);
95
+ }
96
+ throw new Error("No data in SSE response");
97
+ }
98
+
99
+ // Plain JSON
100
+ return response.json();
101
+ }
102
+
103
+ // ── OAuth Token Management ───────────────────────────────────
104
+ // Resolves auth for OAuth servers: checks D1 for cached token,
105
+ // refreshes if expired, returns a valid Bearer header.
106
+
107
+ async function resolveOAuthToken(
108
+ serverName: string,
109
+ db: D1Database
110
+ ): Promise<{ auth: string; error?: undefined } | { auth?: undefined; error: string }> {
111
+ const row = await db
112
+ .prepare("SELECT * FROM oauth_tokens WHERE server_name = ?")
113
+ .bind(serverName)
114
+ .first<OAuthTokenRow>();
115
+
116
+ if (!row) {
117
+ return { error: `no OAuth tokens found for "${serverName}". Re-run npx create-multicast to authorize.` };
118
+ }
119
+
120
+ const nowSeconds = Math.floor(Date.now() / 1000);
121
+
122
+ // Token still valid (with 60s buffer)
123
+ if (row.expires_at > nowSeconds + 60) {
124
+ return { auth: `Bearer ${row.access_token}` };
125
+ }
126
+
127
+ // Token expired — try to refresh
128
+ if (!row.refresh_token) {
129
+ return { error: `OAuth token expired for "${serverName}" and no refresh token available. Re-run npx create-multicast to re-authorize.` };
130
+ }
131
+
132
+ try {
133
+ const refreshResponse = await fetch(row.token_endpoint, {
134
+ method: "POST",
135
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
136
+ body: new URLSearchParams({
137
+ grant_type: "refresh_token",
138
+ refresh_token: row.refresh_token,
139
+ client_id: row.client_id,
140
+ }).toString(),
141
+ });
142
+
143
+ if (!refreshResponse.ok) {
144
+ return { error: `OAuth token refresh failed for "${serverName}": HTTP ${refreshResponse.status}. Re-run npx create-multicast to re-authorize.` };
145
+ }
146
+
147
+ const tokenData = (await refreshResponse.json()) as {
148
+ access_token: string;
149
+ refresh_token?: string;
150
+ expires_in?: number;
151
+ };
152
+
153
+ const newExpiresAt = nowSeconds + (tokenData.expires_in || 3600);
154
+
155
+ // Update D1 with new tokens
156
+ await db
157
+ .prepare(
158
+ `UPDATE oauth_tokens
159
+ SET access_token = ?, refresh_token = ?, expires_at = ?, updated_at = datetime('now')
160
+ WHERE server_name = ?`
161
+ )
162
+ .bind(
163
+ tokenData.access_token,
164
+ tokenData.refresh_token || row.refresh_token,
165
+ newExpiresAt,
166
+ serverName
167
+ )
168
+ .run();
169
+
170
+ return { auth: `Bearer ${tokenData.access_token}` };
171
+ } catch (err: unknown) {
172
+ const message = err instanceof Error ? err.message : "unknown error";
173
+ return { error: `OAuth token refresh failed for "${serverName}": ${message}` };
174
+ }
175
+ }
176
+
63
177
  // ── Downstream MCP Client ────────────────────────────────────
64
178
  // Calls a downstream MCP server via JSON-RPC over HTTP.
65
179
  // Supports optional "setup" steps that run sequentially on the same
@@ -75,7 +189,8 @@ async function callMcpServer(
75
189
  tool: string,
76
190
  args: Record<string, unknown>,
77
191
  timeoutMs: number,
78
- setup?: ToolStep[]
192
+ setup?: ToolStep[],
193
+ db?: D1Database
79
194
  ): Promise<CallResult> {
80
195
  const start = Date.now();
81
196
  const controller = new AbortController();
@@ -86,7 +201,21 @@ async function callMcpServer(
86
201
  "Content-Type": "application/json",
87
202
  "Accept": "application/json, text/event-stream",
88
203
  };
89
- if (server.auth) {
204
+
205
+ // Resolve auth: static header OR OAuth token from D1
206
+ if (server.isOAuth && db) {
207
+ const oauthResult = await resolveOAuthToken(server.name, db);
208
+ if (oauthResult.error) {
209
+ return {
210
+ server: server.name,
211
+ tool,
212
+ success: false,
213
+ error: oauthResult.error,
214
+ duration_ms: Date.now() - start,
215
+ };
216
+ }
217
+ baseHeaders["Authorization"] = oauthResult.auth!;
218
+ } else if (server.auth) {
90
219
  baseHeaders["Authorization"] = server.auth;
91
220
  }
92
221
 
@@ -151,7 +280,7 @@ async function callMcpServer(
151
280
  };
152
281
  }
153
282
 
154
- const setupData = (await setupResponse.json()) as {
283
+ const setupData = (await parseJsonOrSse(setupResponse)) as {
155
284
  result?: { content?: Array<{ text?: string }>; isError?: boolean };
156
285
  error?: { message?: string };
157
286
  };
@@ -195,7 +324,7 @@ async function callMcpServer(
195
324
  };
196
325
  }
197
326
 
198
- const data = (await response.json()) as {
327
+ const data = (await parseJsonOrSse(response)) as {
199
328
  result?: unknown;
200
329
  error?: { message?: string; code?: number };
201
330
  };
@@ -289,7 +418,15 @@ async function discoverServerTools(
289
418
  "Content-Type": "application/json",
290
419
  "Accept": "application/json, text/event-stream",
291
420
  };
292
- if (server.auth) {
421
+
422
+ // Resolve auth: static header OR OAuth token from D1
423
+ if (server.isOAuth) {
424
+ const oauthResult = await resolveOAuthToken(server.name, db);
425
+ if (oauthResult.error) {
426
+ return { tool_count: 0, error: oauthResult.error };
427
+ }
428
+ headers["Authorization"] = oauthResult.auth!;
429
+ } else if (server.auth) {
293
430
  headers["Authorization"] = server.auth;
294
431
  }
295
432
 
@@ -343,7 +480,7 @@ async function discoverServerTools(
343
480
  return { tool_count: 0, error: errText };
344
481
  }
345
482
 
346
- const data = (await response.json()) as {
483
+ const data = (await parseJsonOrSse(response)) as {
347
484
  result?: { tools?: Array<{ name: string; description?: string; inputSchema?: unknown }> };
348
485
  error?: { message?: string };
349
486
  };
@@ -640,14 +777,14 @@ Different servers still run in parallel with each other.`,
640
777
 
641
778
  // Execute all valid calls in parallel
642
779
  // Each call gets its own session; setup steps run sequentially within that session
780
+ // Pass db for OAuth token resolution
781
+ const db = this.env.DB;
643
782
  const promises = validCalls.map((call) =>
644
- callMcpServer(call.server, call.tool, call.args, timeout, call.setup)
783
+ callMcpServer(call.server, call.tool, call.args, timeout, call.setup, db)
645
784
  );
646
785
 
647
786
  const settled = await Promise.allSettled(promises);
648
787
 
649
- const db = this.env.DB;
650
-
651
788
  // Clean expired cache entries (older than 1 hour)
652
789
  await db
653
790
  .prepare(