@letta-ai/letta-code 0.33.6 → 0.33.7

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/letta.js CHANGED
@@ -4358,7 +4358,7 @@ function isDuplicateKeychainItemError(error) {
4358
4358
  return message.includes("already exists in the keychain") || message.includes("code: -25299");
4359
4359
  }
4360
4360
  async function getSecretValue(name, label) {
4361
- if (!secretsAvailable && !secretGetOverrideForTests) {
4361
+ if (!secrets && !secretGetOverrideForTests) {
4362
4362
  return null;
4363
4363
  }
4364
4364
  try {
@@ -4381,7 +4381,7 @@ async function getSecretValue(name, label) {
4381
4381
  }
4382
4382
  }
4383
4383
  async function setSecretValue(name, value) {
4384
- if (!secretsAvailable) {
4384
+ if (!secrets) {
4385
4385
  throw new Error("Secrets API unavailable");
4386
4386
  }
4387
4387
  try {
@@ -4409,7 +4409,7 @@ async function setSecretValue(name, value) {
4409
4409
  });
4410
4410
  }
4411
4411
  async function deleteSecretValue(name) {
4412
- if (!secretsAvailable) {
4412
+ if (!secrets) {
4413
4413
  return false;
4414
4414
  }
4415
4415
  try {
@@ -4423,7 +4423,7 @@ async function deleteSecretValue(name) {
4423
4423
  }
4424
4424
  }
4425
4425
  async function setApiKey(apiKey) {
4426
- if (!secretsAvailable) {
4426
+ if (!secrets) {
4427
4427
  throw new Error("Secrets API unavailable");
4428
4428
  }
4429
4429
  await setSecretValue(API_KEY_NAME, apiKey);
@@ -4432,7 +4432,7 @@ async function getApiKey() {
4432
4432
  return getSecretValue(API_KEY_NAME, "API key");
4433
4433
  }
4434
4434
  async function setRefreshToken(refreshToken2) {
4435
- if (!secretsAvailable) {
4435
+ if (!secrets) {
4436
4436
  throw new Error("Secrets API unavailable");
4437
4437
  }
4438
4438
  await setSecretValue(REFRESH_TOKEN_NAME, refreshToken2);
@@ -4463,7 +4463,7 @@ async function setSecureTokens(tokens) {
4463
4463
  }
4464
4464
  }
4465
4465
  async function deleteApiKey() {
4466
- if (secretsAvailable) {
4466
+ if (secrets) {
4467
4467
  try {
4468
4468
  await secrets.delete({
4469
4469
  service: SERVICE_NAME,
@@ -4476,7 +4476,7 @@ async function deleteApiKey() {
4476
4476
  }
4477
4477
  }
4478
4478
  async function deleteRefreshToken() {
4479
- if (secretsAvailable) {
4479
+ if (secrets) {
4480
4480
  try {
4481
4481
  await secrets.delete({
4482
4482
  service: SERVICE_NAME,
@@ -4498,7 +4498,7 @@ async function isKeychainAvailable() {
4498
4498
  if (process.platform === "linux" && !process.env.DBUS_SESSION_BUS_ADDRESS?.trim()) {
4499
4499
  return false;
4500
4500
  }
4501
- if (!secretsAvailable) {
4501
+ if (!secrets) {
4502
4502
  return false;
4503
4503
  }
4504
4504
  try {
@@ -4511,15 +4511,10 @@ async function isKeychainAvailable() {
4511
4511
  return false;
4512
4512
  }
4513
4513
  }
4514
- var secrets, secretsAvailable = false, SERVICE_NAME, API_KEY_NAME = "letta-api-key", REFRESH_TOKEN_NAME = "letta-refresh-token", warnedSecretReadFailures, secretGetOverrideForTests = null;
4514
+ var secrets, SERVICE_NAME, API_KEY_NAME = "letta-api-key", REFRESH_TOKEN_NAME = "letta-refresh-token", warnedSecretReadFailures, secretGetOverrideForTests = null;
4515
4515
  var init_secrets = __esm(() => {
4516
4516
  init_debug();
4517
- try {
4518
- secrets = globalThis.Bun.secrets;
4519
- secretsAvailable = true;
4520
- } catch {
4521
- secretsAvailable = false;
4522
- }
4517
+ secrets = globalThis.Bun?.secrets;
4523
4518
  SERVICE_NAME = scopeServiceName("letta-code");
4524
4519
  warnedSecretReadFailures = new Set;
4525
4520
  });
@@ -4771,8 +4766,8 @@ class SettingsManager {
4771
4766
  const baseSettings = this.getSettings();
4772
4767
  const secureTokens = { ...this.secureTokensCache };
4773
4768
  if (!getRuntimeContext()) {
4774
- const secretsAvailable2 = await this.isKeychainAvailable();
4775
- if (secretsAvailable2) {
4769
+ const secretsAvailable = await this.isKeychainAvailable();
4770
+ if (secretsAvailable) {
4776
4771
  const storedTokens = await this.getSecureTokens();
4777
4772
  secureTokens.apiKey = storedTokens.apiKey ?? secureTokens.apiKey;
4778
4773
  secureTokens.refreshToken = storedTokens.refreshToken ?? secureTokens.refreshToken;
@@ -4861,8 +4856,8 @@ class SettingsManager {
4861
4856
  this.pendingWrites.add(writePromise);
4862
4857
  }
4863
4858
  async persistSettingsAndTokens(secureTokens) {
4864
- const secretsAvailable2 = await this.isKeychainAvailable();
4865
- if (secretsAvailable2 && Object.keys(secureTokens).length > 0) {
4859
+ const secretsAvailable = await this.isKeychainAvailable();
4860
+ if (secretsAvailable && Object.keys(secureTokens).length > 0) {
4866
4861
  try {
4867
4862
  await Promise.all([
4868
4863
  this.persistSettings(),
@@ -5653,7 +5648,7 @@ var package_default;
5653
5648
  var init_package = __esm(() => {
5654
5649
  package_default = {
5655
5650
  name: "@letta-ai/letta-code",
5656
- version: "0.33.6",
5651
+ version: "0.33.7",
5657
5652
  lettaStartupLogProtocol: 1,
5658
5653
  description: "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5659
5654
  type: "module",
@@ -5680,6 +5675,8 @@ var init_package = __esm(() => {
5680
5675
  "dist/memory-constraints.js.map",
5681
5676
  "dist/mcp-client.js",
5682
5677
  "dist/mcp-client.js.map",
5678
+ "dist/mcp-oauth.js",
5679
+ "dist/mcp-oauth.js.map",
5683
5680
  "dist/agent-presets.js",
5684
5681
  "dist/agent-presets.js.map",
5685
5682
  "dist/schedules.js",
@@ -5724,6 +5721,11 @@ var init_package = __esm(() => {
5724
5721
  import: "./dist/mcp-client.js",
5725
5722
  default: "./dist/mcp-client.js"
5726
5723
  },
5724
+ "./mcp-oauth": {
5725
+ types: "./dist/types/mcp-oauth-public.d.ts",
5726
+ import: "./dist/mcp-oauth.js",
5727
+ default: "./dist/mcp-oauth.js"
5728
+ },
5727
5729
  "./protocol": {
5728
5730
  types: "./dist/types/types/protocol.d.ts"
5729
5731
  },
@@ -5881,6 +5883,9 @@ var init_package = __esm(() => {
5881
5883
  "mcp-client": [
5882
5884
  "./dist/types/mcp-client.d.ts"
5883
5885
  ],
5886
+ "mcp-oauth": [
5887
+ "./dist/types/mcp-oauth-public.d.ts"
5888
+ ],
5884
5889
  channels: [
5885
5890
  "./dist/types/channels-public.d.ts"
5886
5891
  ],
@@ -106236,10 +106241,19 @@ class HeadlessBackend {
106236
106241
  if (!run || run.agent_id !== agentId || isTerminalRun(run)) {
106237
106242
  return { [runId]: "failed" };
106238
106243
  }
106244
+ return this.cancelRunById(runId);
106245
+ }
106246
+ async cancelRunById(runId) {
106247
+ const run = this.runs.get(runId);
106248
+ if (!run || isTerminalRun(run)) {
106249
+ return { [runId]: "failed" };
106250
+ }
106239
106251
  if (run.conversation_id) {
106240
- this.store.settleInterruptedToolCalls(run.conversation_id, { agentId });
106252
+ this.store.settleInterruptedToolCalls(run.conversation_id, {
106253
+ agentId: run.agent_id
106254
+ });
106241
106255
  } else {
106242
- this.store.settleInterruptedToolCalls(agentId);
106256
+ this.store.settleInterruptedToolCalls(run.agent_id);
106243
106257
  }
106244
106258
  const controller = this.runControllerByRunId.get(runId);
106245
106259
  this.recordRunChunk(runId, {
@@ -106250,6 +106264,17 @@ class HeadlessBackend {
106250
106264
  controller?.abort();
106251
106265
  return { [runId]: "cancelled" };
106252
106266
  }
106267
+ async cancelConversationRun(...args) {
106268
+ const [conversationId, requestedRunId] = args;
106269
+ const runId = requestedRunId ?? this.activeRunByConversation.get(conversationId);
106270
+ if (!runId)
106271
+ return {};
106272
+ const run = this.runs.get(runId);
106273
+ if (!run || run.conversation_id !== conversationId || isTerminalRun(run)) {
106274
+ return { [runId]: "failed" };
106275
+ }
106276
+ return this.cancelRunById(runId);
106277
+ }
106253
106278
  async retrieveRun(runId) {
106254
106279
  const run = this.runs.get(runId);
106255
106280
  if (!run)
@@ -110931,6 +110956,9 @@ class APIBackend {
110931
110956
  const client = await this.getClient();
110932
110957
  return client.agents.messages.cancel(agentId, { run_ids: [runId] });
110933
110958
  }
110959
+ async cancelConversationRun(_conversationId, _runId) {
110960
+ throw new Error("API backend does not support exact cancellation for conversations with agent_id:null");
110961
+ }
110934
110962
  async retrieveRun(runId, options) {
110935
110963
  const client = await this.getClient();
110936
110964
  return client.runs.retrieve(runId, options);
@@ -213117,7 +213145,7 @@ class CodexAppServerClient {
213117
213145
  await this.request("initialize", {
213118
213146
  clientInfo: {
213119
213147
  name: "letta-code",
213120
- version: "0.33.6"
213148
+ version: "0.33.7"
213121
213149
  },
213122
213150
  capabilities: null
213123
213151
  });
@@ -418452,6 +418480,15 @@ async function handleAbortMessageInput(listener, params, deps = {}) {
418452
418480
  throw new Error(`Backend did not cancel run ${runId}`);
418453
418481
  }
418454
418482
  },
418483
+ cancelConversationRun: async (conversationId, runId) => {
418484
+ const result3 = await getBackend().cancelConversationRun(conversationId, runId);
418485
+ if (runId && result3[runId] !== "cancelled") {
418486
+ throw new Error(`Backend did not cancel run ${runId}`);
418487
+ }
418488
+ if (!runId && !Object.values(result3).includes("cancelled")) {
418489
+ throw new Error(`Backend did not cancel the active run for ${conversationId}`);
418490
+ }
418491
+ },
418455
418492
  ...deps
418456
418493
  };
418457
418494
  if (listener !== resolvedDeps.getActiveRuntime() || listener.intentionallyClosed) {
@@ -418471,7 +418508,7 @@ async function handleAbortMessageInput(listener, params, deps = {}) {
418471
418508
  return false;
418472
418509
  }
418473
418510
  const cancellation = scopedRuntime.turnLifecycle.requestCancellation({
418474
- waitForExternalSettlement: hasActiveTurn && Boolean(scopedRuntime.agentId)
418511
+ waitForExternalSettlement: hasActiveTurn
418475
418512
  });
418476
418513
  scopedRuntime.queueRuntime.pause();
418477
418514
  const interruptedRunId = cancellation.runId;
@@ -418535,19 +418572,17 @@ async function handleAbortMessageInput(listener, params, deps = {}) {
418535
418572
  }
418536
418573
  const cancelConversationId = scopedRuntime.conversationId;
418537
418574
  const cancelAgentId = scopedRuntime.agentId;
418538
- if (cancelAgentId) {
418539
- const cancelRunId = interruptedRunId ?? params.command.run_id ?? null;
418540
- const backendCancellation = cancelRunId ? resolvedDeps.cancelRun(cancelAgentId, cancelRunId).catch(() => resolvedDeps.cancelConversation(cancelAgentId, cancelConversationId)) : resolvedDeps.cancelConversation(cancelAgentId, cancelConversationId);
418541
- backendCancellation.catch(() => {}).finally(() => {
418542
- if (!cancellation.lease) {
418543
- return;
418544
- }
418545
- const settlement = scopedRuntime.turnLifecycle.settleCancellation(cancellation.lease);
418546
- if (settlement.released) {
418547
- resolvedDeps.scheduleQueuePump(scopedRuntime, params.socket, params.opts, params.processQueuedTurn);
418548
- }
418549
- });
418550
- }
418575
+ const cancelRunId = interruptedRunId ?? params.command.run_id ?? null;
418576
+ const backendCancellation = cancelAgentId && cancelRunId ? resolvedDeps.cancelRun(cancelAgentId, cancelRunId).catch(() => resolvedDeps.cancelConversation(cancelAgentId, cancelConversationId)) : cancelAgentId ? resolvedDeps.cancelConversation(cancelAgentId, cancelConversationId) : resolvedDeps.cancelConversationRun(cancelConversationId, cancelRunId);
418577
+ backendCancellation.catch(() => {}).finally(() => {
418578
+ if (!cancellation.lease) {
418579
+ return;
418580
+ }
418581
+ const settlement = scopedRuntime.turnLifecycle.settleCancellation(cancellation.lease);
418582
+ if (settlement.released) {
418583
+ resolvedDeps.scheduleQueuePump(scopedRuntime, params.socket, params.opts, params.processQueuedTurn);
418584
+ }
418585
+ });
418551
418586
  resolvedDeps.scheduleQueuePump(scopedRuntime, params.socket, params.opts, params.processQueuedTurn);
418552
418587
  return true;
418553
418588
  }
@@ -445147,23 +445182,24 @@ var init_streamableHttp = __esm(() => {
445147
445182
  async function connectMcpServer(config2, options = {}) {
445148
445183
  let client = new Client(options.clientInfo ?? DEFAULT_CLIENT_INFO);
445149
445184
  try {
445185
+ options.signal?.throwIfAborted();
445150
445186
  const transport = createTransport2(config2, options);
445151
445187
  try {
445152
- await client.connect(transport);
445188
+ await client.connect(transport, options.signal ? { signal: options.signal } : undefined);
445153
445189
  } catch (error5) {
445154
445190
  if (!(error5 instanceof UnauthorizedError) || !options.oauth?.waitForAuthorizationCode || !supportsOAuthCompletion(transport)) {
445155
445191
  throw error5;
445156
445192
  }
445157
- const authorizationCode = await options.oauth.waitForAuthorizationCode();
445158
- await transport.finishAuth(authorizationCode);
445193
+ const authorizationCode = await withAbort(options.oauth.waitForAuthorizationCode(), options.signal, () => options.oauth?.close());
445194
+ await withAbort(transport.finishAuth(authorizationCode), options.signal, () => transport.close());
445159
445195
  await client.close().catch(() => transport.close().catch(() => {
445160
445196
  return;
445161
445197
  }));
445162
445198
  client = new Client(options.clientInfo ?? DEFAULT_CLIENT_INFO);
445163
- await client.connect(createTransport2(config2, options));
445199
+ await client.connect(createTransport2(config2, options), options.signal ? { signal: options.signal } : undefined);
445164
445200
  }
445165
- await options.oauth?.close();
445166
- const response = await client.listTools();
445201
+ await (options.oauth?.closeCallback?.() ?? options.oauth?.close() ?? Promise.resolve());
445202
+ const response = await client.listTools(undefined, options.signal ? { signal: options.signal } : undefined);
445167
445203
  const tools = response.tools.map((tool) => ({
445168
445204
  ...tool,
445169
445205
  inputSchema: normalizeInputSchema(tool.inputSchema),
@@ -445186,7 +445222,13 @@ async function connectMcpServer(config2, options = {}) {
445186
445222
  if (closed)
445187
445223
  return;
445188
445224
  closed = true;
445189
- await client.close();
445225
+ const results = await Promise.allSettled([
445226
+ client.close(),
445227
+ options.oauth?.close()
445228
+ ]);
445229
+ const failure2 = results.find((result3) => result3.status === "rejected");
445230
+ if (failure2)
445231
+ throw failure2.reason;
445190
445232
  }
445191
445233
  };
445192
445234
  } catch (error5) {
@@ -445197,12 +445239,34 @@ async function connectMcpServer(config2, options = {}) {
445197
445239
  throw error5;
445198
445240
  }
445199
445241
  }
445242
+ async function withAbort(operation, signal, onAbort) {
445243
+ if (!signal)
445244
+ return operation;
445245
+ signal.throwIfAborted();
445246
+ let abort;
445247
+ const cancellation = new Promise((_resolve, reject2) => {
445248
+ abort = () => {
445249
+ Promise.resolve(onAbort?.()).catch(() => {
445250
+ return;
445251
+ });
445252
+ reject2(signal.reason ?? new DOMException("Operation aborted", "AbortError"));
445253
+ };
445254
+ signal.addEventListener("abort", abort, { once: true });
445255
+ });
445256
+ try {
445257
+ return await Promise.race([operation, cancellation]);
445258
+ } finally {
445259
+ if (abort)
445260
+ signal.removeEventListener("abort", abort);
445261
+ }
445262
+ }
445200
445263
  function createTransport2(config2, options) {
445201
445264
  if (config2.transport === "http") {
445202
445265
  const headers = resolveHeaderEnvironment(config2.headers);
445203
445266
  return new StreamableHTTPClientTransport(new URL(config2.url), {
445204
445267
  requestInit: headersRequestInit(headers),
445205
- authProvider: options.oauth?.authProvider
445268
+ authProvider: options.oauth?.authProvider,
445269
+ fetch: abortAwareFetch(options.signal)
445206
445270
  });
445207
445271
  }
445208
445272
  if (config2.transport === "sse") {
@@ -445211,7 +445275,7 @@ function createTransport2(config2, options) {
445211
445275
  return new SSEClientTransport(new URL(config2.url), {
445212
445276
  requestInit,
445213
445277
  authProvider: options.oauth?.authProvider,
445214
- fetch: headers ? (url2, init) => fetch(url2, mergeHeaders4(init, headers)) : undefined
445278
+ fetch: abortAwareFetch(options.signal, headers)
445215
445279
  });
445216
445280
  }
445217
445281
  return new StdioClientTransport({
@@ -445222,6 +445286,18 @@ function createTransport2(config2, options) {
445222
445286
  stderr: options.stderr ?? "inherit"
445223
445287
  });
445224
445288
  }
445289
+ function abortAwareFetch(operationSignal2, headers) {
445290
+ if (!operationSignal2 && !headers)
445291
+ return;
445292
+ return (url2, init) => {
445293
+ const requestInit = headers ? mergeHeaders4(init, headers) : { ...init };
445294
+ const requestSignal3 = requestInit.signal ?? undefined;
445295
+ return fetch(url2, {
445296
+ ...requestInit,
445297
+ signal: operationSignal2 && requestSignal3 ? AbortSignal.any([operationSignal2, requestSignal3]) : operationSignal2 ?? requestSignal3
445298
+ });
445299
+ };
445300
+ }
445225
445301
  function supportsOAuthCompletion(transport) {
445226
445302
  return "finishAuth" in transport && typeof transport.finishAuth === "function";
445227
445303
  }
@@ -445268,60 +445344,281 @@ var init_mcp_client = __esm(() => {
445268
445344
  init_streamableHttp();
445269
445345
  DEFAULT_CLIENT_INFO = {
445270
445346
  name: "letta-code",
445271
- version: "0.33.6"
445347
+ version: "0.33.7"
445272
445348
  };
445273
445349
  });
445274
445350
 
445351
+ // src/mcp-oauth-callback.ts
445352
+ import { createServer as createServer3 } from "node:http";
445353
+ async function startOAuthCallbackServer2(preferredPort) {
445354
+ try {
445355
+ return await startOAuthCallbackServerOnPort(preferredPort ?? 0);
445356
+ } catch (error5) {
445357
+ if (!preferredPort)
445358
+ throw error5;
445359
+ return startOAuthCallbackServerOnPort(0);
445360
+ }
445361
+ }
445362
+ function callbackPort(redirectUrl) {
445363
+ if (!redirectUrl)
445364
+ return;
445365
+ try {
445366
+ const parsed = new URL(redirectUrl);
445367
+ return parsed.hostname === "127.0.0.1" && parsed.port ? Number(parsed.port) : undefined;
445368
+ } catch {
445369
+ return;
445370
+ }
445371
+ }
445372
+ async function startOAuthCallbackServerOnPort(port) {
445373
+ const expectedState = {};
445374
+ let server2;
445375
+ let completed = false;
445376
+ let settle3;
445377
+ let reject2;
445378
+ const codePromise = new Promise((resolve39, rejectPromise) => {
445379
+ settle3 = (code2) => {
445380
+ completed = true;
445381
+ resolve39(code2);
445382
+ };
445383
+ reject2 = (error5) => {
445384
+ completed = true;
445385
+ rejectPromise(error5);
445386
+ };
445387
+ });
445388
+ codePromise.catch(() => {
445389
+ return;
445390
+ });
445391
+ server2 = createServer3((request, response) => {
445392
+ if (request.method !== "GET" || !request.url?.startsWith("/") || request.url.startsWith("//") || request.url.includes("\\")) {
445393
+ response.writeHead(400, { "Content-Type": "text/plain" });
445394
+ response.end("Invalid request target");
445395
+ return;
445396
+ }
445397
+ const address2 = server2.address();
445398
+ const expectedHost = address2 && typeof address2 !== "string" ? `127.0.0.1:${address2.port}` : undefined;
445399
+ if (!expectedHost || request.headers.host !== expectedHost) {
445400
+ response.writeHead(400, { "Content-Type": "text/plain" });
445401
+ response.end("Invalid callback host");
445402
+ return;
445403
+ }
445404
+ let url2;
445405
+ try {
445406
+ url2 = new URL(request.url, `http://${expectedHost}`);
445407
+ } catch {
445408
+ response.writeHead(400, { "Content-Type": "text/plain" });
445409
+ response.end("Invalid request target");
445410
+ return;
445411
+ }
445412
+ if (url2.origin !== `http://${expectedHost}` || url2.pathname !== "/callback") {
445413
+ response.writeHead(404).end("Not found");
445414
+ return;
445415
+ }
445416
+ const error5 = url2.searchParams.get("error");
445417
+ const code2 = url2.searchParams.get("code");
445418
+ const state = url2.searchParams.get("state");
445419
+ if (!state || state !== expectedState.value) {
445420
+ response.writeHead(400, { "Content-Type": "text/html" });
445421
+ response.end(callbackPage("Authorization failed", "Invalid OAuth callback"));
445422
+ return;
445423
+ }
445424
+ if (error5) {
445425
+ response.writeHead(400, { "Content-Type": "text/html" });
445426
+ reject2?.(new Error(`MCP OAuth authorization failed: ${error5}`));
445427
+ response.end(callbackPage("Authorization failed", error5), () => {
445428
+ closeServer(true);
445429
+ });
445430
+ return;
445431
+ }
445432
+ if (!code2) {
445433
+ response.writeHead(400, { "Content-Type": "text/html" });
445434
+ response.end(callbackPage("Authorization failed", "Missing authorization code"));
445435
+ return;
445436
+ }
445437
+ response.writeHead(200, { "Content-Type": "text/html" });
445438
+ settle3?.(code2);
445439
+ response.end(callbackPage("Authorization complete", "You can close this tab and return to Letta Code."), () => {
445440
+ closeServer(true);
445441
+ });
445442
+ });
445443
+ server2.on("clientError", (_error, socket) => {
445444
+ if (!socket.writable)
445445
+ return;
445446
+ socket.end(`HTTP/1.1 400 Bad Request\r
445447
+ Connection: close\r
445448
+ Content-Length: 0\r
445449
+ \r
445450
+ `);
445451
+ });
445452
+ let serverClosing = false;
445453
+ const closeServer = (force = false) => {
445454
+ if (serverClosing)
445455
+ return;
445456
+ serverClosing = true;
445457
+ server2.close();
445458
+ if (force)
445459
+ server2.closeAllConnections();
445460
+ };
445461
+ await new Promise((resolve39, rejectListen) => {
445462
+ server2.once("error", rejectListen);
445463
+ server2.listen(port, "127.0.0.1", resolve39);
445464
+ });
445465
+ server2.unref();
445466
+ const address = server2.address();
445467
+ if (!address || typeof address === "string") {
445468
+ server2.close();
445469
+ throw new Error("Could not start MCP OAuth callback server");
445470
+ }
445471
+ const timeout = setTimeout(() => {
445472
+ reject2?.(new Error("Timed out waiting for MCP OAuth authorization"));
445473
+ closeServer(true);
445474
+ }, CALLBACK_TIMEOUT_MS);
445475
+ timeout.unref();
445476
+ return {
445477
+ redirectUrl: `http://127.0.0.1:${address.port}/callback`,
445478
+ expectedState,
445479
+ waitForCode: () => codePromise.finally(() => clearTimeout(timeout)),
445480
+ close: () => {
445481
+ clearTimeout(timeout);
445482
+ if (!completed)
445483
+ reject2?.(new Error("MCP OAuth flow was cancelled"));
445484
+ closeServer(true);
445485
+ }
445486
+ };
445487
+ }
445488
+ function callbackPage(title, message) {
445489
+ return `<!doctype html><html><body style="font-family:system-ui;padding:2rem"><h1>${escapeHtml2(title)}</h1><p>${escapeHtml2(message)}</p></body></html>`;
445490
+ }
445491
+ function escapeHtml2(value) {
445492
+ return value.replace(/[&<>"']/g, (character) => {
445493
+ const entities = {
445494
+ "&": "&amp;",
445495
+ "<": "&lt;",
445496
+ ">": "&gt;",
445497
+ '"': "&quot;",
445498
+ "'": "&#39;"
445499
+ };
445500
+ return entities[character] ?? character;
445501
+ });
445502
+ }
445503
+ var CALLBACK_TIMEOUT_MS;
445504
+ var init_mcp_oauth_callback = __esm(() => {
445505
+ CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
445506
+ });
445507
+
445275
445508
  // src/mcp-oauth.ts
445276
445509
  import { createHash as createHash11, randomBytes as randomBytes2 } from "node:crypto";
445277
- import { createServer as createServer3 } from "node:http";
445278
445510
  async function clearMcpOAuthCredentials(agentId, serverName, serverUrl) {
445279
- return deleteSecretValue(oauthSecretName(agentId, serverName, serverUrl));
445511
+ const credentialKey = mcpOAuthCredentialKey(agentId, serverName, serverUrl);
445512
+ const coordinationKey = storageCoordinationKey(bunSecretStorage, credentialKey, "bun-secrets");
445513
+ const { result: result3 } = await clearStoredCredentials(bunSecretStorage, credentialKey, coordinationKey);
445514
+ return result3 === true;
445280
445515
  }
445281
445516
  async function createMcpOAuthSession(agentId, serverName, serverUrl, options) {
445282
- const secretName = oauthSecretName(agentId, serverName, serverUrl);
445283
- const persisted = await loadState(secretName);
445284
- if (!options.interactive && !persisted)
445285
- return;
445286
- const callback = options.interactive ? await startOAuthCallbackServer2(callbackPort(persisted?.redirectUrl)) : undefined;
445287
- const redirectUrl = callback?.redirectUrl ?? persisted?.redirectUrl;
445288
- if (!redirectUrl)
445289
- return;
445290
- const provider = new PersistentMcpOAuthProvider({
445291
- secretName,
445292
- redirectUrl,
445293
- persisted,
445517
+ const credentialKey = mcpOAuthCredentialKey(agentId, serverName, serverUrl);
445518
+ return createMcpOAuthSessionWithStorage({
445519
+ credentialKey,
445520
+ storageNamespace: "bun-secrets",
445521
+ storage: bunSecretStorage,
445294
445522
  interactive: options.interactive,
445295
445523
  onStatus: options.onStatus,
445296
- openBrowser: options.openBrowser ?? openSystemBrowser,
445297
- expectedState: callback?.expectedState
445524
+ openBrowser: options.openBrowser ?? openSystemBrowser
445298
445525
  });
445299
- return {
445300
- authProvider: provider,
445301
- ...callback ? { waitForAuthorizationCode: () => callback.waitForCode() } : {},
445302
- close: async () => callback?.close()
445303
- };
445526
+ }
445527
+ async function createMcpOAuthSessionWithStorage(options) {
445528
+ const coordinationKey = storageCoordinationKey(options.storage, options.credentialKey, options.storageNamespace);
445529
+ retainCredentialCoordinator(coordinationKey);
445530
+ let provider;
445531
+ let callback;
445532
+ try {
445533
+ const loaded = await loadState(options.storage, options.credentialKey, coordinationKey);
445534
+ const persisted = loaded.state;
445535
+ if (!options.interactive && !persisted) {
445536
+ await releaseCredentialCoordinator(coordinationKey);
445537
+ return;
445538
+ }
445539
+ callback = options.interactive ? await startOAuthCallbackServer2(callbackPort(persisted?.redirectUrl)) : undefined;
445540
+ const redirectUrl = callback?.redirectUrl ?? persisted?.redirectUrl;
445541
+ if (!redirectUrl) {
445542
+ await releaseCredentialCoordinator(coordinationKey);
445543
+ return;
445544
+ }
445545
+ provider = new PersistentMcpOAuthProvider({
445546
+ credentialKey: options.credentialKey,
445547
+ coordinationKey,
445548
+ storage: options.storage,
445549
+ redirectUrl,
445550
+ persisted,
445551
+ interactive: options.interactive,
445552
+ onStatus: options.onStatus,
445553
+ openBrowser: options.openBrowser ?? openSystemBrowser,
445554
+ expectedState: callback?.expectedState,
445555
+ generation: loaded.generation
445556
+ });
445557
+ const activeProvider = provider;
445558
+ const activeCallback = callback;
445559
+ return {
445560
+ authProvider: activeProvider,
445561
+ ...activeCallback ? { waitForAuthorizationCode: () => activeCallback.waitForCode() } : {},
445562
+ exportCredentials: () => activeProvider.exportCredentials(),
445563
+ closeCallback: async () => activeCallback?.close(),
445564
+ close: async () => {
445565
+ activeCallback?.close();
445566
+ await activeProvider.dispose();
445567
+ }
445568
+ };
445569
+ } catch (error5) {
445570
+ callback?.close();
445571
+ if (provider)
445572
+ await provider.dispose();
445573
+ else
445574
+ await releaseCredentialCoordinator(coordinationKey);
445575
+ throw error5;
445576
+ }
445304
445577
  }
445305
445578
 
445306
445579
  class PersistentMcpOAuthProvider {
445307
445580
  stateData;
445308
- secretName;
445581
+ pendingClientInformation;
445582
+ pendingDiscoveryState;
445583
+ codeVerifierValue;
445584
+ generation;
445585
+ observedClientVersion;
445586
+ observedTokenVersion;
445587
+ observedDiscoveryVersion;
445588
+ tokenOperationGeneration;
445589
+ tokenOperationVersion;
445590
+ coordinatorValue;
445591
+ credentialKey;
445592
+ coordinationKey;
445593
+ storage;
445309
445594
  interactive;
445310
445595
  onStatus;
445311
445596
  openBrowser;
445312
445597
  expectedState;
445598
+ closed = false;
445313
445599
  constructor(options) {
445314
- this.secretName = options.secretName;
445600
+ this.credentialKey = options.credentialKey;
445601
+ this.coordinationKey = options.coordinationKey;
445602
+ this.storage = options.storage;
445315
445603
  this.interactive = options.interactive;
445316
445604
  this.onStatus = options.onStatus;
445317
445605
  this.openBrowser = options.openBrowser;
445318
445606
  this.expectedState = options.expectedState;
445607
+ this.generation = options.generation;
445608
+ const coordinator = getCredentialCoordinator(options.coordinationKey);
445609
+ this.coordinatorValue = coordinator;
445610
+ this.observedClientVersion = coordinator.clientVersion;
445611
+ this.observedTokenVersion = coordinator.tokenVersion;
445612
+ this.observedDiscoveryVersion = coordinator.discoveryVersion;
445613
+ this.tokenOperationGeneration = coordinator.generation;
445614
+ this.tokenOperationVersion = coordinator.tokenVersion;
445319
445615
  this.stateData = {
445320
445616
  ...options.persisted,
445321
445617
  redirectUrl: options.redirectUrl
445322
445618
  };
445323
445619
  }
445324
445620
  get redirectUrl() {
445621
+ this.assertOpen();
445325
445622
  return this.stateData.redirectUrl;
445326
445623
  }
445327
445624
  get clientMetadata() {
@@ -445334,27 +445631,127 @@ class PersistentMcpOAuthProvider {
445334
445631
  };
445335
445632
  }
445336
445633
  state() {
445634
+ this.assertOpen();
445337
445635
  const state = randomBytes2(24).toString("base64url");
445338
445636
  if (this.expectedState)
445339
445637
  this.expectedState.value = state;
445340
445638
  return state;
445341
445639
  }
445342
445640
  clientInformation() {
445343
- const client = this.stateData.clientInformation;
445344
- return client && "redirect_uris" in client && client.redirect_uris.includes(this.redirectUrl) ? client : undefined;
445641
+ this.assertOpen();
445642
+ if (this.generation !== this.coordinator.generation)
445643
+ return;
445644
+ if (this.pendingClientInformation && this.pendingClientInformation.baseVersion !== this.coordinator.clientVersion) {
445645
+ this.pendingClientInformation = undefined;
445646
+ }
445647
+ const client = this.pendingClientInformation?.value ?? this.coordinator.state?.clientInformation;
445648
+ this.observedClientVersion = this.coordinator.clientVersion;
445649
+ return client && "redirect_uris" in client && Array.isArray(client.redirect_uris) && client.redirect_uris.includes(this.redirectUrl) ? client : undefined;
445345
445650
  }
445346
445651
  saveClientInformation(clientInformation) {
445652
+ this.assertOpen();
445653
+ assertCurrentGeneration(this.coordinationKey, this.generation);
445347
445654
  this.stateData.clientInformation = clientInformation;
445655
+ this.pendingClientInformation = {
445656
+ value: clientInformation,
445657
+ baseVersion: this.coordinator.clientVersion
445658
+ };
445659
+ if (!this.coordinator.state?.tokens) {
445660
+ this.tokenOperationGeneration = this.coordinator.generation;
445661
+ this.tokenOperationVersion = this.coordinator.tokenVersion;
445662
+ }
445348
445663
  }
445349
445664
  tokens() {
445350
- return this.stateData.tokens;
445665
+ this.assertOpen();
445666
+ if (this.generation !== this.coordinator.generation)
445667
+ return;
445668
+ this.observedTokenVersion = this.coordinator.tokenVersion;
445669
+ this.tokenOperationGeneration = this.coordinator.generation;
445670
+ this.tokenOperationVersion = this.coordinator.tokenVersion;
445671
+ return this.coordinator.state?.tokens;
445672
+ }
445673
+ async exportCredentials() {
445674
+ this.assertOpen();
445675
+ const expectedGeneration = this.generation;
445676
+ return enqueueStorageMutation(this.coordinationKey, async () => {
445677
+ this.assertOpen();
445678
+ assertCurrentGeneration(this.coordinationKey, expectedGeneration);
445679
+ const latest = await readStoredState(this.storage, this.credentialKey);
445680
+ if (!latest) {
445681
+ throw new Error("MCP OAuth authorization is not complete");
445682
+ }
445683
+ this.assertOpen();
445684
+ this.coordinator.state = latest;
445685
+ this.stateData = latest;
445686
+ return exportCredentialSnapshot(latest);
445687
+ });
445351
445688
  }
445352
445689
  async saveTokens(tokens) {
445353
- this.stateData.tokens = tokens;
445354
- delete this.stateData.codeVerifier;
445355
- await this.persist();
445690
+ this.assertOpen();
445691
+ const expectedGeneration = this.tokenOperationGeneration;
445692
+ const expectedTokenVersion = this.tokenOperationVersion;
445693
+ const tokenExpiresAt = tokens.expires_in === undefined ? undefined : Date.now() + tokens.expires_in * 1000;
445694
+ const pendingClientInformation = this.pendingClientInformation;
445695
+ const pendingDiscoveryState = this.pendingDiscoveryState;
445696
+ const result3 = await enqueueStorageMutation(this.coordinationKey, async () => {
445697
+ this.assertOpen();
445698
+ assertCurrentGeneration(this.coordinationKey, expectedGeneration);
445699
+ if (this.coordinator.tokenVersion !== expectedTokenVersion) {
445700
+ throw new Error("MCP OAuth token response is stale because credentials changed");
445701
+ }
445702
+ if (pendingClientInformation && this.coordinator.clientVersion !== pendingClientInformation.baseVersion || pendingDiscoveryState && this.coordinator.discoveryVersion !== pendingDiscoveryState.baseVersion) {
445703
+ throw new Error("MCP OAuth token response is stale because protocol state changed");
445704
+ }
445705
+ const state = await readStoredState(this.storage, this.credentialKey) ?? { redirectUrl: this.redirectUrl };
445706
+ let clientMerged = false;
445707
+ let discoveryMerged = false;
445708
+ if (pendingClientInformation && this.coordinator.clientVersion === pendingClientInformation.baseVersion) {
445709
+ state.redirectUrl = this.redirectUrl;
445710
+ state.clientInformation = pendingClientInformation.value;
445711
+ clientMerged = true;
445712
+ }
445713
+ if (pendingDiscoveryState && this.coordinator.discoveryVersion === pendingDiscoveryState.baseVersion) {
445714
+ state.discoveryState = pendingDiscoveryState.value;
445715
+ discoveryMerged = true;
445716
+ }
445717
+ state.tokens = {
445718
+ ...tokens,
445719
+ refresh_token: tokens.refresh_token ?? state.tokens?.refresh_token
445720
+ };
445721
+ state.tokenExpiresAt = tokenExpiresAt;
445722
+ await this.storage.set(this.credentialKey, JSON.stringify(state));
445723
+ this.coordinator.state = state;
445724
+ this.coordinator.tokenVersion += 1;
445725
+ if (clientMerged)
445726
+ this.coordinator.clientVersion += 1;
445727
+ if (discoveryMerged)
445728
+ this.coordinator.discoveryVersion += 1;
445729
+ return { state, clientMerged, discoveryMerged };
445730
+ });
445731
+ this.codeVerifierValue = undefined;
445732
+ if (this.closed) {
445733
+ this.scrubLocalState();
445734
+ return;
445735
+ }
445736
+ this.stateData = result3.state;
445737
+ this.observedTokenVersion = this.coordinator.tokenVersion;
445738
+ this.tokenOperationGeneration = this.coordinator.generation;
445739
+ this.tokenOperationVersion = this.coordinator.tokenVersion;
445740
+ this.observedClientVersion = this.coordinator.clientVersion;
445741
+ this.observedDiscoveryVersion = this.coordinator.discoveryVersion;
445742
+ if (this.pendingClientInformation === pendingClientInformation) {
445743
+ this.pendingClientInformation = undefined;
445744
+ } else if (this.pendingClientInformation) {
445745
+ this.pendingClientInformation.baseVersion = this.coordinator.clientVersion;
445746
+ }
445747
+ if (this.pendingDiscoveryState === pendingDiscoveryState) {
445748
+ this.pendingDiscoveryState = undefined;
445749
+ } else if (this.pendingDiscoveryState) {
445750
+ this.pendingDiscoveryState.baseVersion = this.coordinator.discoveryVersion;
445751
+ }
445356
445752
  }
445357
445753
  async redirectToAuthorization(authorizationUrl) {
445754
+ this.assertOpen();
445358
445755
  if (!this.interactive) {
445359
445756
  throw new Error("MCP authentication requires user authorization. Open /mcp and press R to sign in.");
445360
445757
  }
@@ -445364,147 +445761,268 @@ ${authorizationUrl.toString()}`);
445364
445761
  await this.openBrowser(authorizationUrl.toString());
445365
445762
  }
445366
445763
  saveCodeVerifier(codeVerifier) {
445367
- this.stateData.codeVerifier = codeVerifier;
445764
+ this.assertOpen();
445765
+ this.codeVerifierValue = codeVerifier;
445368
445766
  }
445369
445767
  codeVerifier() {
445370
- if (!this.stateData.codeVerifier) {
445768
+ this.assertOpen();
445769
+ if (!this.codeVerifierValue) {
445371
445770
  throw new Error("No MCP OAuth PKCE verifier is available");
445372
445771
  }
445373
- return this.stateData.codeVerifier;
445772
+ return this.codeVerifierValue;
445374
445773
  }
445375
445774
  saveDiscoveryState(state) {
445775
+ this.assertOpen();
445776
+ assertCurrentGeneration(this.coordinationKey, this.generation);
445376
445777
  this.stateData.discoveryState = state;
445778
+ this.pendingDiscoveryState = {
445779
+ value: state,
445780
+ baseVersion: this.coordinator.discoveryVersion
445781
+ };
445782
+ if (!this.coordinator.state?.tokens) {
445783
+ this.tokenOperationGeneration = this.coordinator.generation;
445784
+ this.tokenOperationVersion = this.coordinator.tokenVersion;
445785
+ }
445377
445786
  }
445378
445787
  discoveryState() {
445379
- return this.stateData.discoveryState;
445380
- }
445381
- async invalidateCredentials(scope) {
445382
- if (scope === "all") {
445383
- this.stateData = { redirectUrl: this.redirectUrl };
445384
- await deleteSecretValue(this.secretName);
445788
+ this.assertOpen();
445789
+ if (this.generation !== this.coordinator.generation)
445385
445790
  return;
445791
+ if (this.pendingDiscoveryState && this.pendingDiscoveryState.baseVersion !== this.coordinator.discoveryVersion) {
445792
+ this.pendingDiscoveryState = undefined;
445386
445793
  }
445387
- if (scope === "client")
445388
- delete this.stateData.clientInformation;
445389
- if (scope === "tokens")
445390
- delete this.stateData.tokens;
445391
- if (scope === "verifier")
445392
- delete this.stateData.codeVerifier;
445393
- if (scope === "discovery")
445394
- delete this.stateData.discoveryState;
445395
- await this.persist();
445396
- }
445397
- async persist() {
445398
- await setSecretValue(this.secretName, JSON.stringify(this.stateData));
445399
- }
445400
- }
445401
- async function startOAuthCallbackServer2(preferredPort) {
445402
- try {
445403
- return await startOAuthCallbackServerOnPort(preferredPort ?? 0);
445404
- } catch (error5) {
445405
- if (!preferredPort)
445406
- throw error5;
445407
- return startOAuthCallbackServerOnPort(0);
445794
+ this.observedDiscoveryVersion = this.coordinator.discoveryVersion;
445795
+ return this.pendingDiscoveryState?.value ?? this.coordinator.state?.discoveryState;
445408
445796
  }
445409
- }
445410
- async function startOAuthCallbackServerOnPort(port) {
445411
- const expectedState = {};
445412
- let server2;
445413
- let completed = false;
445414
- let settle3;
445415
- let reject2;
445416
- const codePromise = new Promise((resolve39, rejectPromise) => {
445417
- settle3 = (code2) => {
445418
- completed = true;
445419
- resolve39(code2);
445420
- };
445421
- reject2 = (error5) => {
445422
- completed = true;
445423
- rejectPromise(error5);
445424
- };
445425
- });
445426
- codePromise.catch(() => {
445427
- return;
445428
- });
445429
- server2 = createServer3((request, response) => {
445430
- const url2 = new URL(request.url ?? "/", "http://127.0.0.1");
445431
- if (url2.pathname !== "/callback") {
445432
- response.writeHead(404).end("Not found");
445797
+ async invalidateCredentials(scope) {
445798
+ this.assertOpen();
445799
+ if (scope === "verifier") {
445800
+ this.codeVerifierValue = undefined;
445433
445801
  return;
445434
445802
  }
445435
- const error5 = url2.searchParams.get("error");
445436
- const code2 = url2.searchParams.get("code");
445437
- const state = url2.searchParams.get("state");
445438
- if (error5) {
445439
- response.writeHead(400, { "Content-Type": "text/html" });
445440
- response.end(callbackPage("Authorization failed", error5));
445441
- reject2?.(new Error(`MCP OAuth authorization failed: ${error5}`));
445442
- server2.close();
445803
+ if (scope === "all") {
445804
+ const redirectUrl = this.redirectUrl;
445805
+ const { generation: generation3, cleared } = await clearStoredCredentials(this.storage, this.credentialKey, this.coordinationKey, {
445806
+ generation: this.generation,
445807
+ clientVersion: this.observedClientVersion,
445808
+ tokenVersion: this.observedTokenVersion,
445809
+ discoveryVersion: this.observedDiscoveryVersion
445810
+ });
445811
+ if (this.closed)
445812
+ return;
445813
+ if (!cleared) {
445814
+ this.stateData = this.coordinator.state ?? { redirectUrl };
445815
+ this.pendingClientInformation = undefined;
445816
+ this.pendingDiscoveryState = undefined;
445817
+ this.codeVerifierValue = undefined;
445818
+ this.observedClientVersion = this.coordinator.clientVersion;
445819
+ this.observedTokenVersion = this.coordinator.tokenVersion;
445820
+ this.observedDiscoveryVersion = this.coordinator.discoveryVersion;
445821
+ return;
445822
+ }
445823
+ this.generation = generation3;
445824
+ this.stateData = { redirectUrl };
445825
+ this.pendingClientInformation = undefined;
445826
+ this.pendingDiscoveryState = undefined;
445827
+ this.codeVerifierValue = undefined;
445828
+ this.observedClientVersion = this.coordinator.clientVersion;
445829
+ this.observedTokenVersion = this.coordinator.tokenVersion;
445830
+ this.observedDiscoveryVersion = this.coordinator.discoveryVersion;
445831
+ return;
445832
+ }
445833
+ const expectedGeneration = this.generation;
445834
+ const pendingClientInformation = this.pendingClientInformation;
445835
+ const pendingDiscoveryState = this.pendingDiscoveryState;
445836
+ const expectedVersion = scope === "client" ? this.observedClientVersion : scope === "tokens" ? this.observedTokenVersion : this.observedDiscoveryVersion;
445837
+ const result3 = await enqueueStorageMutation(this.coordinationKey, async () => {
445838
+ this.assertOpen();
445839
+ assertCurrentGeneration(this.coordinationKey, expectedGeneration);
445840
+ const currentVersion = scope === "client" ? this.coordinator.clientVersion : scope === "tokens" ? this.coordinator.tokenVersion : this.coordinator.discoveryVersion;
445841
+ if (currentVersion !== expectedVersion) {
445842
+ return this.coordinator.state;
445843
+ }
445844
+ const state = await readStoredState(this.storage, this.credentialKey);
445845
+ if (state) {
445846
+ if (scope === "client")
445847
+ delete state.clientInformation;
445848
+ if (scope === "tokens") {
445849
+ delete state.tokens;
445850
+ delete state.tokenExpiresAt;
445851
+ }
445852
+ if (scope === "discovery")
445853
+ delete state.discoveryState;
445854
+ await this.storage.set(this.credentialKey, JSON.stringify(state));
445855
+ }
445856
+ this.coordinator.state = state;
445857
+ if (scope === "client")
445858
+ this.coordinator.clientVersion += 1;
445859
+ if (scope === "tokens")
445860
+ this.coordinator.tokenVersion += 1;
445861
+ if (scope === "discovery")
445862
+ this.coordinator.discoveryVersion += 1;
445863
+ return state;
445864
+ });
445865
+ if (this.closed)
445443
445866
  return;
445867
+ this.stateData = result3 ?? { redirectUrl: this.redirectUrl };
445868
+ if (scope === "client") {
445869
+ this.observedClientVersion = this.coordinator.clientVersion;
445870
+ if (this.pendingClientInformation === pendingClientInformation) {
445871
+ this.pendingClientInformation = undefined;
445872
+ } else if (this.pendingClientInformation) {
445873
+ this.pendingClientInformation.baseVersion = this.coordinator.clientVersion;
445874
+ }
445444
445875
  }
445445
- if (!code2 || !state || state !== expectedState.value) {
445446
- response.writeHead(400, { "Content-Type": "text/html" });
445447
- response.end(callbackPage("Authorization failed", "Invalid OAuth callback"));
445448
- reject2?.(new Error("Invalid MCP OAuth callback state"));
445449
- server2.close();
445876
+ if (scope === "tokens") {
445877
+ this.observedTokenVersion = this.coordinator.tokenVersion;
445878
+ }
445879
+ if (scope === "discovery") {
445880
+ this.observedDiscoveryVersion = this.coordinator.discoveryVersion;
445881
+ if (this.pendingDiscoveryState === pendingDiscoveryState) {
445882
+ this.pendingDiscoveryState = undefined;
445883
+ } else if (this.pendingDiscoveryState) {
445884
+ this.pendingDiscoveryState.baseVersion = this.coordinator.discoveryVersion;
445885
+ }
445886
+ }
445887
+ }
445888
+ async dispose() {
445889
+ if (this.closed)
445450
445890
  return;
445891
+ this.closed = true;
445892
+ this.scrubLocalState();
445893
+ try {
445894
+ await releaseCredentialCoordinator(this.coordinationKey);
445895
+ } finally {
445896
+ this.coordinatorValue = undefined;
445897
+ this.scrubLocalState();
445451
445898
  }
445452
- response.writeHead(200, { "Content-Type": "text/html" });
445453
- response.end(callbackPage("Authorization complete", "You can close this tab and return to Letta Code."));
445454
- settle3?.(code2);
445455
- server2.close();
445456
- });
445457
- await new Promise((resolve39, rejectListen) => {
445458
- server2.once("error", rejectListen);
445459
- server2.listen(port, "127.0.0.1", resolve39);
445460
- });
445461
- server2.unref();
445462
- const address = server2.address();
445463
- if (!address || typeof address === "string") {
445464
- server2.close();
445465
- throw new Error("Could not start MCP OAuth callback server");
445466
445899
  }
445467
- const timeout = setTimeout(() => {
445468
- reject2?.(new Error("Timed out waiting for MCP OAuth authorization"));
445469
- server2.close();
445470
- }, CALLBACK_TIMEOUT_MS);
445471
- timeout.unref();
445900
+ get coordinator() {
445901
+ if (!this.coordinatorValue)
445902
+ throw new Error("MCP OAuth session is closed");
445903
+ return this.coordinatorValue;
445904
+ }
445905
+ scrubLocalState() {
445906
+ this.codeVerifierValue = undefined;
445907
+ this.pendingClientInformation = undefined;
445908
+ this.pendingDiscoveryState = undefined;
445909
+ this.stateData = { redirectUrl: this.stateData.redirectUrl };
445910
+ }
445911
+ assertOpen() {
445912
+ if (this.closed)
445913
+ throw new Error("MCP OAuth session is closed");
445914
+ }
445915
+ }
445916
+ function exportCredentialSnapshot(state) {
445917
+ const clientInformation = state.clientInformation;
445918
+ const tokens = state.tokens;
445919
+ const validClient = clientInformation && "redirect_uris" in clientInformation && clientInformation.redirect_uris.includes(state.redirectUrl) ? clientInformation : undefined;
445920
+ if (!validClient?.client_id || !tokens?.access_token) {
445921
+ throw new Error("MCP OAuth authorization is not complete");
445922
+ }
445472
445923
  return {
445473
- redirectUrl: `http://127.0.0.1:${address.port}/callback`,
445474
- expectedState,
445475
- waitForCode: () => codePromise.finally(() => clearTimeout(timeout)),
445476
- close: () => {
445477
- clearTimeout(timeout);
445478
- if (!completed)
445479
- reject2?.(new Error("MCP OAuth flow was cancelled"));
445480
- server2.close();
445481
- }
445924
+ access_token: tokens.access_token,
445925
+ ...tokens.refresh_token ? { refresh_token: tokens.refresh_token } : {},
445926
+ client_id: validClient.client_id,
445927
+ ...validClient.client_secret ? { client_secret: validClient.client_secret } : {},
445928
+ redirect_uri: state.redirectUrl,
445929
+ ...tokens.token_type ? { token_type: tokens.token_type } : {},
445930
+ ...state.tokenExpiresAt !== undefined ? {
445931
+ expires_in: Math.max(0, Math.ceil((state.tokenExpiresAt - Date.now()) / 1000))
445932
+ } : {},
445933
+ ...tokens.scope ? { scope: tokens.scope } : {}
445482
445934
  };
445483
445935
  }
445484
- function callbackPort(redirectUrl) {
445485
- if (!redirectUrl)
445936
+ function enqueueStorageMutation(coordinationKey, mutation) {
445937
+ const coordinator = getCredentialCoordinator(coordinationKey);
445938
+ const pending = coordinator.queue.then(mutation, mutation);
445939
+ coordinator.queue = pending.then(() => {
445486
445940
  return;
445487
- try {
445488
- const parsed = new URL(redirectUrl);
445489
- return parsed.hostname === "127.0.0.1" && parsed.port ? Number(parsed.port) : undefined;
445490
- } catch {
445941
+ }, () => {
445491
445942
  return;
445943
+ });
445944
+ return pending;
445945
+ }
445946
+ function getCredentialCoordinator(coordinationKey) {
445947
+ const existing = credentialCoordinators.get(coordinationKey);
445948
+ if (existing)
445949
+ return existing;
445950
+ const coordinator = {
445951
+ queue: Promise.resolve(),
445952
+ generation: 0,
445953
+ activeProviders: 0,
445954
+ clientVersion: 0,
445955
+ tokenVersion: 0,
445956
+ discoveryVersion: 0
445957
+ };
445958
+ credentialCoordinators.set(coordinationKey, coordinator);
445959
+ return coordinator;
445960
+ }
445961
+ function storageCoordinationKey(storage, credentialKey, storageNamespace) {
445962
+ const namespace = storageNamespace;
445963
+ if (namespace !== undefined && (!namespace.trim() || namespace.includes("\x00"))) {
445964
+ throw new Error("MCP OAuth storage namespace must be non-empty and contain no NUL bytes");
445965
+ }
445966
+ let identity2;
445967
+ if (!namespace) {
445968
+ let objectId = storageObjectIds.get(storage);
445969
+ if (!objectId) {
445970
+ objectId = nextStorageObjectId;
445971
+ nextStorageObjectId += 1;
445972
+ storageObjectIds.set(storage, objectId);
445973
+ }
445974
+ identity2 = `object:${objectId}`;
445975
+ } else {
445976
+ identity2 = `namespace:${namespace}`;
445492
445977
  }
445978
+ return `${identity2}\x00${credentialKey}`;
445493
445979
  }
445494
- function callbackPage(title, message) {
445495
- return `<!doctype html><html><body style="font-family:system-ui;padding:2rem"><h1>${escapeHtml2(title)}</h1><p>${escapeHtml2(message)}</p></body></html>`;
445980
+ function retainCredentialCoordinator(coordinationKey) {
445981
+ getCredentialCoordinator(coordinationKey).activeProviders += 1;
445496
445982
  }
445497
- function escapeHtml2(value) {
445498
- return value.replace(/[&<>"']/g, (character) => {
445499
- const entities = {
445500
- "&": "&amp;",
445501
- "<": "&lt;",
445502
- ">": "&gt;",
445503
- '"': "&quot;",
445504
- "'": "&#39;"
445505
- };
445506
- return entities[character] ?? character;
445983
+ async function releaseCredentialCoordinator(coordinationKey) {
445984
+ const coordinator = getCredentialCoordinator(coordinationKey);
445985
+ await enqueueStorageMutation(coordinationKey, async () => {
445986
+ coordinator.activeProviders = Math.max(0, coordinator.activeProviders - 1);
445987
+ if (coordinator.activeProviders === 0)
445988
+ coordinator.state = undefined;
445507
445989
  });
445990
+ await coordinator.queue;
445991
+ if (coordinator.activeProviders === 0 && credentialCoordinators.get(coordinationKey) === coordinator) {
445992
+ credentialCoordinators.delete(coordinationKey);
445993
+ }
445994
+ }
445995
+ function assertCurrentGeneration(coordinationKey, expectedGeneration) {
445996
+ if (getCredentialCoordinator(coordinationKey).generation !== expectedGeneration) {
445997
+ throw new Error("MCP OAuth session is stale because credentials were cleared");
445998
+ }
445999
+ }
446000
+ async function clearStoredCredentials(storage, credentialKey, coordinationKey, expected) {
446001
+ const coordinator = getCredentialCoordinator(coordinationKey);
446002
+ const outcome = await enqueueStorageMutation(coordinationKey, async () => {
446003
+ if (expected) {
446004
+ assertCurrentGeneration(coordinationKey, expected.generation);
446005
+ if (coordinator.clientVersion !== expected.clientVersion || coordinator.tokenVersion !== expected.tokenVersion || coordinator.discoveryVersion !== expected.discoveryVersion) {
446006
+ return {
446007
+ result: false,
446008
+ generation: coordinator.generation,
446009
+ cleared: false
446010
+ };
446011
+ }
446012
+ }
446013
+ const result3 = await storage.delete(credentialKey);
446014
+ coordinator.generation += 1;
446015
+ coordinator.clientVersion += 1;
446016
+ coordinator.tokenVersion += 1;
446017
+ coordinator.discoveryVersion += 1;
446018
+ coordinator.state = undefined;
446019
+ return { result: result3, generation: coordinator.generation, cleared: true };
446020
+ });
446021
+ await coordinator.queue;
446022
+ if (coordinator.activeProviders === 0 && credentialCoordinators.get(coordinationKey) === coordinator) {
446023
+ credentialCoordinators.delete(coordinationKey);
446024
+ }
446025
+ return outcome;
445508
446026
  }
445509
446027
  async function openSystemBrowser(url2) {
445510
446028
  try {
@@ -445513,25 +446031,73 @@ async function openSystemBrowser(url2) {
445513
446031
  subprocess.on("error", () => {});
445514
446032
  } catch {}
445515
446033
  }
445516
- function oauthSecretName(agentId, serverName, serverUrl) {
446034
+ function mcpOAuthCredentialKey(agentId, serverName, serverUrl) {
445517
446035
  const digest = createHash11("sha256").update(`${agentId}\x00${serverName}\x00${serverUrl}`).digest("hex").slice(0, 32);
445518
446036
  return `mcp-oauth-${digest}`;
445519
446037
  }
445520
- async function loadState(secretName) {
445521
- const value = await getSecretValue(secretName, "MCP OAuth credentials");
446038
+ async function loadState(storage, credentialKey, coordinationKey) {
446039
+ return enqueueStorageMutation(coordinationKey, async () => {
446040
+ const coordinator = getCredentialCoordinator(coordinationKey);
446041
+ const state = await readStoredState(storage, credentialKey);
446042
+ coordinator.state = state;
446043
+ return { state, generation: coordinator.generation };
446044
+ });
446045
+ }
446046
+ async function readStoredState(storage, credentialKey) {
446047
+ const value = await storage.get(credentialKey);
445522
446048
  if (!value)
445523
446049
  return;
445524
446050
  try {
445525
- return JSON.parse(value);
446051
+ const parsed = JSON.parse(value);
446052
+ if (!isPersistedMcpOAuthState(parsed)) {
446053
+ await storage.delete(credentialKey);
446054
+ return;
446055
+ }
446056
+ return parsed;
445526
446057
  } catch {
445527
- await deleteSecretValue(secretName);
446058
+ await storage.delete(credentialKey);
445528
446059
  return;
445529
446060
  }
445530
446061
  }
445531
- var CALLBACK_TIMEOUT_MS;
446062
+ function isPersistedMcpOAuthState(value) {
446063
+ if (!isRecord17(value) || !SafeUrlSchema.safeParse(value.redirectUrl).success || "codeVerifier" in value) {
446064
+ return false;
446065
+ }
446066
+ if (value.clientInformation !== undefined) {
446067
+ if (!isRecord17(value.clientInformation))
446068
+ return false;
446069
+ const schema5 = "redirect_uris" in value.clientInformation ? OAuthClientInformationFullSchema : OAuthClientInformationSchema;
446070
+ if (!schema5.safeParse(value.clientInformation).success)
446071
+ return false;
446072
+ }
446073
+ if (value.tokens !== undefined && !OAuthTokensSchema.safeParse(value.tokens).success) {
446074
+ return false;
446075
+ }
446076
+ if (value.tokenExpiresAt !== undefined && (typeof value.tokenExpiresAt !== "number" || !Number.isFinite(value.tokenExpiresAt))) {
446077
+ return false;
446078
+ }
446079
+ if (value.discoveryState !== undefined) {
446080
+ if (!isRecord17(value.discoveryState) || !SafeUrlSchema.safeParse(value.discoveryState.authorizationServerUrl).success || value.discoveryState.resourceMetadataUrl !== undefined && !SafeUrlSchema.safeParse(value.discoveryState.resourceMetadataUrl).success || value.discoveryState.authorizationServerMetadata !== undefined && !OAuthMetadataSchema.safeParse(value.discoveryState.authorizationServerMetadata).success && !OpenIdProviderDiscoveryMetadataSchema.safeParse(value.discoveryState.authorizationServerMetadata).success || value.discoveryState.resourceMetadata !== undefined && !OAuthProtectedResourceMetadataSchema.safeParse(value.discoveryState.resourceMetadata).success) {
446081
+ return false;
446082
+ }
446083
+ }
446084
+ return true;
446085
+ }
446086
+ function isRecord17(value) {
446087
+ return typeof value === "object" && value !== null && !Array.isArray(value);
446088
+ }
446089
+ var credentialCoordinators, storageObjectIds, nextStorageObjectId = 1, bunSecretStorage;
445532
446090
  var init_mcp_oauth = __esm(() => {
446091
+ init_auth2();
446092
+ init_mcp_oauth_callback();
445533
446093
  init_secrets();
445534
- CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
446094
+ credentialCoordinators = new Map;
446095
+ storageObjectIds = new WeakMap;
446096
+ bunSecretStorage = {
446097
+ get: (credentialKey) => getSecretValue(credentialKey, "MCP OAuth credentials"),
446098
+ set: setSecretValue,
446099
+ delete: deleteSecretValue
446100
+ };
445535
446101
  });
445536
446102
 
445537
446103
  // src/mcp-runtime.ts
@@ -447936,7 +448502,7 @@ import {
447936
448502
  } from "node:fs";
447937
448503
  import { tmpdir as tmpdir12 } from "node:os";
447938
448504
  import path45 from "node:path";
447939
- function isRecord17(value) {
448505
+ function isRecord18(value) {
447940
448506
  return typeof value === "object" && value !== null && !Array.isArray(value);
447941
448507
  }
447942
448508
  function isPathInsideOrEqual2(childPath, parentPath) {
@@ -447952,7 +448518,7 @@ function readPackageJson(packageJsonPath) {
447952
448518
  } catch (error5) {
447953
448519
  throw new Error(`Could not read package.json: ${error5 instanceof Error ? error5.message : String(error5)}`);
447954
448520
  }
447955
- if (!isRecord17(parsed)) {
448521
+ if (!isRecord18(parsed)) {
447956
448522
  throw new Error("package.json must be an object");
447957
448523
  }
447958
448524
  return parsed;
@@ -447962,7 +448528,7 @@ function formatRepository(repository) {
447962
448528
  const trimmed2 = repository.trim();
447963
448529
  return trimmed2 || undefined;
447964
448530
  }
447965
- if (!isRecord17(repository))
448531
+ if (!isRecord18(repository))
447966
448532
  return;
447967
448533
  const url2 = repository.url;
447968
448534
  if (typeof url2 !== "string")
@@ -448492,7 +449058,7 @@ function hasRuntimeDependencies(packageJson) {
448492
449058
  if (!packageJson)
448493
449059
  return false;
448494
449060
  const dependencies4 = packageJson.dependencies;
448495
- return isRecord17(dependencies4) && Object.keys(dependencies4).length > 0;
449061
+ return isRecord18(dependencies4) && Object.keys(dependencies4).length > 0;
448496
449062
  }
448497
449063
  function readPackageJsonIfExists(packageDirectory) {
448498
449064
  const packageJsonPath = path45.join(packageDirectory, "package.json");
@@ -512630,4 +513196,4 @@ function registerBunOAuthFlows() {
512630
513196
  registerBunOAuthFlows();
512631
513197
  await init_src5().then(() => exports_src2);
512632
513198
 
512633
- //# debugId=8FBCBEB92238D00764756E2164756E21
513199
+ //# debugId=3195287CCD45D3B564756E2164756E21