@chrischall/office-outlook-mcp 0.1.4 → 0.2.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.
package/dist/bundle.js CHANGED
@@ -50,8 +50,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
50
50
 
51
51
  // node_modules/@chrischall/mcp-utils/dist/cancel/index.js
52
52
  import { AsyncLocalStorage } from "node:async_hooks";
53
- function withCallSignal(signal, fn) {
54
- return signal ? storage.run({ signal }, fn) : fn();
53
+ function withCallSignal(signal, fn, request) {
54
+ return signal ? storage.run({ signal, ...request ? { request } : {} }, fn) : fn();
55
55
  }
56
56
  function currentCallSignal() {
57
57
  return storage.getStore()?.signal;
@@ -71,6 +71,18 @@ var init_cancel = __esm({
71
71
  }
72
72
  });
73
73
 
74
+ // node_modules/@chrischall/mcp-utils/dist/caller/index.js
75
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
76
+ function withCallerCapabilities(capabilities, fn) {
77
+ return capabilities ? storage2.run(capabilities, fn) : fn();
78
+ }
79
+ var storage2;
80
+ var init_caller = __esm({
81
+ "node_modules/@chrischall/mcp-utils/dist/caller/index.js"() {
82
+ storage2 = new AsyncLocalStorage2();
83
+ }
84
+ });
85
+
74
86
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
75
87
  var __create2, __defProp2, __getOwnPropDesc2, __getOwnPropNames2, __getProtoOf2, __hasOwnProp2, __commonJSMin, __exportAll, __copyProps2, __toESM2;
76
88
  var init_chunk_Br0eD_fh = __esm({
@@ -35139,7 +35151,7 @@ var init_dist = __esm({
35139
35151
 
35140
35152
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
35141
35153
  function redactSecrets(text) {
35142
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
35154
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(HEADER_SECRET_RE, "$1[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(OAUTH_CODE_RE, "$1[REDACTED]").replace(FORM_PASSWORD_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
35143
35155
  }
35144
35156
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
35145
35157
  const str = text === null || text === void 0 ? "" : String(text);
@@ -35153,7 +35165,7 @@ function messageOf(err) {
35153
35165
  return err.message;
35154
35166
  return String(err);
35155
35167
  }
35156
- var DEFAULT_ERROR_MESSAGE_MAX, McpToolError, BEARER_RE, JWT_RE, BASIC_AUTH_RE, SET_COOKIE_RE, COOKIE_HEADER_RE, API_KEY_RE, QUERY_SECRET_RE, AWS_SIGV4_RE, JSON_SECRET_KEYS, JSON_SECRET_DQ_RE, JSON_SECRET_SQ_RE;
35168
+ var DEFAULT_ERROR_MESSAGE_MAX, McpToolError, BEARER_RE, JWT_RE, BASIC_AUTH_RE, SET_COOKIE_RE, COOKIE_HEADER_RE, API_KEY_RE, QUERY_SECRET_RE, OAUTH_CODE_RE, FORM_PASSWORD_RE, HEADER_SECRET_RE, AWS_SIGV4_RE, JSON_SECRET_KEYS, JSON_SECRET_DQ_RE, JSON_SECRET_SQ_RE;
35157
35169
  var init_errors3 = __esm({
35158
35170
  "node_modules/@chrischall/mcp-utils/dist/errors/index.js"() {
35159
35171
  DEFAULT_ERROR_MESSAGE_MAX = 500;
@@ -35187,11 +35199,26 @@ var init_errors3 = __esm({
35187
35199
  "whsec_[A-Za-z0-9]{16,}\\b"
35188
35200
  // webhook signing secret (Stripe-style)
35189
35201
  ].map((p) => `\\b${p}`).join("|"), "g");
35190
- QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
35202
+ QUERY_SECRET_RE = /([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
35203
+ OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
35204
+ FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
35205
+ HEADER_SECRET_RE = /(\b(?:x[-_][a-z0-9_-]*?(?:api[-_]?key|token|secret|auth)[a-z0-9_-]*|api[-_]?key)\s*:\s*)[^\s,;"'<>`]+/gi;
35191
35206
  AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
35192
- JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
35193
- JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
35194
- JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
35207
+ JSON_SECRET_KEYS = [
35208
+ "(?:access|refresh|id|auth|session|bearer|csrf|xsrf)[_-]?token",
35209
+ "client[_-]?secret",
35210
+ "api[_-]?secret",
35211
+ "(?:x[_-])?api[_-]?key",
35212
+ "private[_-]?key",
35213
+ "x[_-][a-z0-9_-]*?(?:api[_-]?key|token|secret|auth)[a-z0-9_-]*",
35214
+ "(?:proxy-)?authorization",
35215
+ "password",
35216
+ "passwd",
35217
+ "secret",
35218
+ "token"
35219
+ ].join("|");
35220
+ JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")(?:[^"\\\\]|\\\\.)*(")`, "gi");
35221
+ JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')(?:[^'\\\\]|\\\\.)*(')`, "gi");
35195
35222
  }
35196
35223
  });
35197
35224
 
@@ -35280,6 +35307,7 @@ var init_response = __esm({
35280
35307
  var init_confirmation = __esm({
35281
35308
  "node_modules/@chrischall/mcp-utils/dist/server/confirmation.js"() {
35282
35309
  init_dist();
35310
+ init_caller();
35283
35311
  init_response();
35284
35312
  }
35285
35313
  });
@@ -35293,6 +35321,15 @@ Hint: ${err.hint}`);
35293
35321
  }
35294
35322
  throw err;
35295
35323
  }
35324
+ function mcpRequestFrom(args) {
35325
+ const ctx = args.at(-1);
35326
+ if (typeof ctx !== "object" || ctx === null)
35327
+ return void 0;
35328
+ const req = ctx.mcpReq;
35329
+ if (typeof req !== "object" || req === null)
35330
+ return void 0;
35331
+ return typeof req.notify === "function" ? req : void 0;
35332
+ }
35296
35333
  function callSignalFrom(args) {
35297
35334
  const ctx = args.at(-1);
35298
35335
  if (typeof ctx !== "object" || ctx === null)
@@ -35303,6 +35340,20 @@ function callSignalFrom(args) {
35303
35340
  const signal = req.signal;
35304
35341
  return signal instanceof AbortSignal ? signal : void 0;
35305
35342
  }
35343
+ function declaredCapabilitiesFrom(server) {
35344
+ const inner = server.server;
35345
+ if (typeof inner !== "object" || inner === null)
35346
+ return void 0;
35347
+ const read = inner.getClientCapabilities;
35348
+ if (typeof read !== "function")
35349
+ return void 0;
35350
+ try {
35351
+ const declared = read.call(inner);
35352
+ return typeof declared === "object" && declared !== null ? declared : void 0;
35353
+ } catch {
35354
+ return void 0;
35355
+ }
35356
+ }
35306
35357
  function surfaceToolHints(server) {
35307
35358
  const register2 = server.registerTool.bind(server);
35308
35359
  server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => (
@@ -35318,15 +35369,24 @@ function surfaceToolHints(server) {
35318
35369
  // Here because this is the one wrapper every tool already passes
35319
35370
  // through: threading the signal by hand would mean editing several
35320
35371
  // hundred handlers and missing exactly the ones nobody edits.
35321
- withCallSignal(callSignalFrom(args), () => {
35322
- let result;
35323
- try {
35324
- result = cb(...args);
35325
- } catch (err) {
35326
- return hintResultOrRethrow(err);
35327
- }
35328
- return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
35329
- })
35372
+ withCallSignal(callSignalFrom(args), () => (
35373
+ // WHAT THE CALLER CAN DO, made ambient for the same reason and in
35374
+ // the same place (`caller/index.ts`). Only the 2025-era half needs
35375
+ // the wrapper — a 2026-07-28 request answers for itself off its own
35376
+ // envelope — but a guarded tool that cannot tell whether the caller
35377
+ // can be shown a prompt returns one the SDK then refuses to deliver,
35378
+ // and that refusal reaches the caller as an unexplained protocol
35379
+ // error it had no chance to catch.
35380
+ withCallerCapabilities(declaredCapabilitiesFrom(server), () => {
35381
+ let result;
35382
+ try {
35383
+ result = cb(...args);
35384
+ } catch (err) {
35385
+ return hintResultOrRethrow(err);
35386
+ }
35387
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
35388
+ })
35389
+ ), mcpRequestFrom(args))
35330
35390
  ));
35331
35391
  }
35332
35392
  async function createMcpServer(opts) {
@@ -35344,11 +35404,43 @@ async function createMcpServer(opts) {
35344
35404
  }
35345
35405
  function withGracefulShutdown(target, opts = {}) {
35346
35406
  const shouldExit = opts.exit ?? true;
35407
+ if (opts.timeoutMs !== void 0 && !(Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0)) {
35408
+ throw new RangeError("withGracefulShutdown: timeoutMs must be a finite number greater than 0.");
35409
+ }
35410
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
35411
+ if (opts.repeatSignalGraceMs !== void 0 && !(Number.isFinite(opts.repeatSignalGraceMs) && opts.repeatSignalGraceMs >= 0)) {
35412
+ throw new RangeError("withGracefulShutdown: repeatSignalGraceMs must be a finite number >= 0.");
35413
+ }
35414
+ const repeatGraceMs = opts.repeatSignalGraceMs ?? DEFAULT_REPEAT_SIGNAL_GRACE_MS;
35347
35415
  let shuttingDown = false;
35416
+ let firstSignalAt = 0;
35417
+ let exited = false;
35418
+ const exitOnce = () => {
35419
+ if (exited)
35420
+ return;
35421
+ exited = true;
35422
+ process.exit(0);
35423
+ };
35348
35424
  const handler = (signal) => {
35349
- if (shuttingDown)
35425
+ if (shuttingDown) {
35426
+ if (Date.now() - firstSignalAt < repeatGraceMs)
35427
+ return;
35428
+ if (shouldExit) {
35429
+ console.error(`[mcp-utils] second ${signal} during shutdown \u2014 exiting now`);
35430
+ exitOnce();
35431
+ }
35350
35432
  return;
35433
+ }
35351
35434
  shuttingDown = true;
35435
+ firstSignalAt = Date.now();
35436
+ let timer;
35437
+ if (shouldExit) {
35438
+ timer = setTimeout(() => {
35439
+ console.error(`[mcp-utils] graceful shutdown on ${signal} did not finish in ${timeoutMs}ms \u2014 exiting`);
35440
+ exitOnce();
35441
+ }, timeoutMs);
35442
+ timer.unref?.();
35443
+ }
35352
35444
  void (async () => {
35353
35445
  try {
35354
35446
  if (opts.onSignal)
@@ -35357,8 +35449,10 @@ function withGracefulShutdown(target, opts = {}) {
35357
35449
  } catch (err) {
35358
35450
  console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
35359
35451
  } finally {
35452
+ if (timer !== void 0)
35453
+ clearTimeout(timer);
35360
35454
  if (shouldExit)
35361
- process.exit(0);
35455
+ exitOnce();
35362
35456
  }
35363
35457
  })();
35364
35458
  };
@@ -35386,7 +35480,7 @@ function runMcp(opts) {
35386
35480
  }
35387
35481
  return handle;
35388
35482
  }
35389
- var SERVER_PROTOCOL_VERSIONS;
35483
+ var SERVER_PROTOCOL_VERSIONS, DEFAULT_SHUTDOWN_TIMEOUT_MS, DEFAULT_REPEAT_SIGNAL_GRACE_MS;
35390
35484
  var init_server = __esm({
35391
35485
  "node_modules/@chrischall/mcp-utils/dist/server/index.js"() {
35392
35486
  init_stdio();
@@ -35395,10 +35489,13 @@ var init_server = __esm({
35395
35489
  init_response();
35396
35490
  init_confirmation();
35397
35491
  init_cancel();
35492
+ init_caller();
35398
35493
  SERVER_PROTOCOL_VERSIONS = Object.freeze([
35399
35494
  "2026-07-28",
35400
35495
  ...SUPPORTED_PROTOCOL_VERSIONS
35401
35496
  ]);
35497
+ DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
35498
+ DEFAULT_REPEAT_SIGNAL_GRACE_MS = 500;
35402
35499
  }
35403
35500
  });
35404
35501
 
@@ -35482,17 +35579,27 @@ var init_config = __esm({
35482
35579
  }
35483
35580
  });
35484
35581
 
35582
+ // node_modules/@chrischall/mcp-utils/dist/fs/confine.js
35583
+ var init_confine = __esm({
35584
+ "node_modules/@chrischall/mcp-utils/dist/fs/confine.js"() {
35585
+ init_config();
35586
+ }
35587
+ });
35588
+
35485
35589
  // node_modules/@chrischall/mcp-utils/dist/fs/output.js
35486
35590
  var init_output = __esm({
35487
35591
  "node_modules/@chrischall/mcp-utils/dist/fs/output.js"() {
35488
35592
  init_config();
35593
+ init_confine();
35489
35594
  }
35490
35595
  });
35491
35596
 
35492
35597
  // node_modules/@chrischall/mcp-utils/dist/fs/index.js
35493
35598
  var init_fs = __esm({
35494
35599
  "node_modules/@chrischall/mcp-utils/dist/fs/index.js"() {
35600
+ init_confine();
35495
35601
  init_output();
35602
+ init_confine();
35496
35603
  }
35497
35604
  });
35498
35605
 
@@ -35543,24 +35650,78 @@ function createApiClient(opts) {
35543
35650
  const retryStatuses = retry.statuses ?? [429];
35544
35651
  const timeoutMs = opts.timeout;
35545
35652
  const authHeader = (token) => !token ? {} : opts.tokenHeader ? { [opts.tokenHeader]: token } : { Authorization: `Bearer ${token}` };
35546
- function withTimeout(run) {
35547
- if (timeoutMs == null || timeoutMs <= 0)
35548
- return run(withAmbientCancellation(void 0));
35653
+ async function withTimeout(run) {
35654
+ if (timeoutMs == null || timeoutMs <= 0) {
35655
+ const res2 = await run(withAmbientCancellation(void 0));
35656
+ return { res: res2, done: () => {
35657
+ }, expired: void 0 };
35658
+ }
35549
35659
  const controller = new AbortController();
35550
35660
  const timer = setTimeout(() => controller.abort(), timeoutMs);
35551
- return run(withAmbientCancellation(controller.signal)).catch((err) => {
35661
+ let res;
35662
+ try {
35663
+ res = await run(withAmbientCancellation(controller.signal));
35664
+ } catch (err) {
35665
+ clearTimeout(timer);
35552
35666
  if (err instanceof Error && err.name === "AbortError" && controller.signal.aborted) {
35553
35667
  throw new RequestTimeoutError(service, timeoutMs);
35554
35668
  }
35555
35669
  throw err;
35556
- }).finally(() => clearTimeout(timer));
35670
+ }
35671
+ const expired = new Promise((_resolve, reject) => {
35672
+ controller.signal.addEventListener("abort", () => reject(new RequestTimeoutError(service, timeoutMs)), {
35673
+ once: true
35674
+ });
35675
+ });
35676
+ expired.catch(() => {
35677
+ });
35678
+ return { res, done: () => clearTimeout(timer), expired };
35679
+ }
35680
+ async function readBody(attempt, read) {
35681
+ try {
35682
+ if (!attempt.expired)
35683
+ return await read(attempt.res);
35684
+ return await Promise.race([read(attempt.res), attempt.expired]);
35685
+ } catch (err) {
35686
+ if (err instanceof RequestTimeoutError) {
35687
+ attempt.res.body?.cancel().catch(() => {
35688
+ });
35689
+ }
35690
+ throw err;
35691
+ } finally {
35692
+ attempt.done();
35693
+ }
35694
+ }
35695
+ const baseParts = (() => {
35696
+ try {
35697
+ const u = new URL(base);
35698
+ return { origin: u.origin, username: u.username, password: u.password };
35699
+ } catch {
35700
+ return void 0;
35701
+ }
35702
+ })();
35703
+ const baseOrigin = baseParts?.origin;
35704
+ function resolveUrl(path, query) {
35705
+ const url2 = `${base}${path}${query}`;
35706
+ if (baseOrigin === void 0)
35707
+ return url2;
35708
+ let parsed;
35709
+ try {
35710
+ parsed = new URL(url2);
35711
+ } catch {
35712
+ throw new Error(`Refusing request to ${service}: path ${JSON.stringify(path)} does not form a valid URL.`);
35713
+ }
35714
+ if (parsed.origin !== baseOrigin || parsed.username !== baseParts?.username || parsed.password !== baseParts?.password) {
35715
+ throw new Error(`Refusing request to ${service}: path ${JSON.stringify(path)} resolves outside the base origin ${baseOrigin}. Paths must be relative to baseUrl (start them with "/").`);
35716
+ }
35717
+ return url2;
35557
35718
  }
35558
35719
  async function send(method, path, opt) {
35559
35720
  const isMultipart = opt.formData !== void 0;
35560
35721
  const hasJsonBody = !isMultipart && opt.body !== void 0;
35561
35722
  const reqBody = isMultipart ? opt.formData : hasJsonBody ? JSON.stringify(opt.body) : void 0;
35562
35723
  const query = opt.query ? buildQueryString(opt.query) : "";
35563
- const url2 = `${base}${path}${query}`;
35724
+ const url2 = resolveUrl(path, query);
35564
35725
  const bodyInit = reqBody !== void 0 ? { body: reqBody } : {};
35565
35726
  const fetchWith = (token, signal) => doFetch(url2, {
35566
35727
  method,
@@ -35577,8 +35738,10 @@ function createApiClient(opts) {
35577
35738
  const once = () => withTimeout((signal) => opts.tokenManager ? opts.tokenManager.withAuth((token) => fetchWith(token, signal)) : (async () => fetchWith(await opts.getToken?.() || void 0, signal))());
35578
35739
  let attempt = 0;
35579
35740
  for (; ; ) {
35580
- const res = await once();
35741
+ const current = await once();
35742
+ const res = current.res;
35581
35743
  if (retryStatuses.includes(res.status) && attempt < retry.count) {
35744
+ current.done();
35582
35745
  attempt += 1;
35583
35746
  const delay = retry.honorRetryAfter ? parseRetryAfterMs(res.headers.get("retry-after"), {
35584
35747
  defaultMs: retry.delayMs,
@@ -35587,18 +35750,21 @@ function createApiClient(opts) {
35587
35750
  await sleep3(delay);
35588
35751
  continue;
35589
35752
  }
35590
- return res;
35753
+ return current;
35591
35754
  }
35592
35755
  }
35593
35756
  async function fetchJson(method, path, opt = {}) {
35594
- const res = await send(method, path, opt);
35757
+ const attempt = await send(method, path, opt);
35758
+ const res = attempt.res;
35759
+ if (res.status === 401 || res.status === 429 || res.status === 204)
35760
+ attempt.done();
35595
35761
  if (res.status === 401)
35596
35762
  throw unauthorized();
35597
35763
  if (res.status === 429)
35598
35764
  throw rateLimited();
35599
35765
  if (res.status === 204)
35600
35766
  return void 0;
35601
- const text = await res.text();
35767
+ const text = await readBody(attempt, (r) => r.text());
35602
35768
  if (!res.ok) {
35603
35769
  throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
35604
35770
  }
@@ -35608,12 +35774,15 @@ function createApiClient(opts) {
35608
35774
  }
35609
35775
  async function fetchHtml(method, path, opt = {}) {
35610
35776
  const headers = { Accept: "text/html,*/*", ...opt.headers };
35611
- const res = await send(method, path, { ...opt, headers });
35777
+ const attempt = await send(method, path, { ...opt, headers });
35778
+ const res = attempt.res;
35779
+ if (res.status === 401 || res.status === 429)
35780
+ attempt.done();
35612
35781
  if (res.status === 401)
35613
35782
  throw unauthorized();
35614
35783
  if (res.status === 429)
35615
35784
  throw rateLimited();
35616
- const text = await res.text();
35785
+ const text = await readBody(attempt, (r) => r.text());
35617
35786
  if (!res.ok) {
35618
35787
  throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
35619
35788
  }
@@ -35621,16 +35790,23 @@ function createApiClient(opts) {
35621
35790
  }
35622
35791
  async function fetchRaw(method, path, opt = {}) {
35623
35792
  const headers = { Accept: "*/*", ...opt.headers };
35624
- const res = await send(method, path, { ...opt, headers });
35793
+ const attempt = await send(method, path, { ...opt, headers });
35794
+ const res = attempt.res;
35795
+ if (res.status === 401 || res.status === 429)
35796
+ attempt.done();
35625
35797
  if (res.status === 401)
35626
35798
  throw unauthorized();
35627
35799
  if (res.status === 429)
35628
35800
  throw rateLimited();
35629
35801
  if (!res.ok) {
35630
- const text = await res.text().catch(() => "");
35802
+ const text = await readBody(attempt, (r) => r.text()).catch((err) => {
35803
+ if (err instanceof RequestTimeoutError)
35804
+ throw err;
35805
+ return "";
35806
+ });
35631
35807
  throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
35632
35808
  }
35633
- const bytes = new Uint8Array(await res.arrayBuffer());
35809
+ const bytes = new Uint8Array(await readBody(attempt, (r) => r.arrayBuffer()));
35634
35810
  return {
35635
35811
  status: res.status,
35636
35812
  contentType: res.headers.get("content-type"),
@@ -35813,9 +35989,16 @@ var init_auth = __esm({
35813
35989
  }
35814
35990
  });
35815
35991
 
35992
+ // node_modules/@chrischall/mcp-utils/dist/internal/ascii.js
35993
+ var init_ascii = __esm({
35994
+ "node_modules/@chrischall/mcp-utils/dist/internal/ascii.js"() {
35995
+ }
35996
+ });
35997
+
35816
35998
  // node_modules/@chrischall/mcp-utils/dist/scrape/index.js
35817
35999
  var init_scrape = __esm({
35818
36000
  "node_modules/@chrischall/mcp-utils/dist/scrape/index.js"() {
36001
+ init_ascii();
35819
36002
  }
35820
36003
  });
35821
36004
 
@@ -35823,6 +36006,7 @@ var init_scrape = __esm({
35823
36006
  var init_dist2 = __esm({
35824
36007
  "node_modules/@chrischall/mcp-utils/dist/index.js"() {
35825
36008
  init_cancel();
36009
+ init_caller();
35826
36010
  init_server();
35827
36011
  init_response();
35828
36012
  init_errors3();
@@ -35843,7 +36027,7 @@ var init_version = __esm({
35843
36027
  "src/version.ts"() {
35844
36028
  "use strict";
35845
36029
  PACKAGE_NAME = "office-outlook-mcp";
35846
- VERSION = "0.1.4";
36030
+ VERSION = "0.2.0";
35847
36031
  }
35848
36032
  });
35849
36033
 
@@ -36058,6 +36242,7 @@ var init_frames = __esm({
36058
36242
  "capture_redirect",
36059
36243
  "read_indexed_db",
36060
36244
  "read_dom",
36245
+ "read_dom_list",
36061
36246
  "download",
36062
36247
  "graphql",
36063
36248
  "write_cookies"
@@ -36558,6 +36743,79 @@ function assertDomSelectorsArray(value, label) {
36558
36743
  }
36559
36744
  }
36560
36745
  }
36746
+ function assertDomListSelectorsArray(value, label) {
36747
+ if (!Array.isArray(value)) {
36748
+ throw new ProtocolError3(`${label}: expected array, got ${typeof value}`);
36749
+ }
36750
+ const seen = /* @__PURE__ */ new Set();
36751
+ for (let i = 0; i < value.length; i++) {
36752
+ const entry = value[i];
36753
+ assertObject(entry, `${label}[${i}]`);
36754
+ if (entry.name === void 0) {
36755
+ throw new ProtocolError3(`${label}[${i}].name: missing`);
36756
+ }
36757
+ if (entry.itemSelector === void 0) {
36758
+ throw new ProtocolError3(`${label}[${i}].itemSelector: missing`);
36759
+ }
36760
+ if (entry.fields === void 0) {
36761
+ throw new ProtocolError3(`${label}[${i}].fields: missing`);
36762
+ }
36763
+ if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
36764
+ throw new ProtocolError3(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
36765
+ }
36766
+ if (typeof entry.itemSelector !== "string" || !DOM_SELECTOR_RE.test(entry.itemSelector)) {
36767
+ throw new ProtocolError3(`${label}[${i}].itemSelector: invalid ${JSON.stringify(entry.itemSelector)}`);
36768
+ }
36769
+ if (!Array.isArray(entry.fields) || entry.fields.length === 0) {
36770
+ throw new ProtocolError3(`${label}[${i}].fields: expected non-empty array`);
36771
+ }
36772
+ const fieldNames = /* @__PURE__ */ new Set();
36773
+ for (let j = 0; j < entry.fields.length; j++) {
36774
+ const field = entry.fields[j];
36775
+ const fieldLabel = `${label}[${i}].fields[${j}]`;
36776
+ assertObject(field, fieldLabel);
36777
+ if (field.name === void 0) {
36778
+ throw new ProtocolError3(`${fieldLabel}.name: missing`);
36779
+ }
36780
+ if (typeof field.name !== "string" || !SCOPE_KEY_RE.test(field.name)) {
36781
+ throw new ProtocolError3(`${fieldLabel}.name: invalid ${JSON.stringify(field.name)}`);
36782
+ }
36783
+ if (field.selector !== void 0) {
36784
+ if (typeof field.selector !== "string" || !DOM_SELECTOR_RE.test(field.selector)) {
36785
+ throw new ProtocolError3(`${fieldLabel}.selector: invalid ${JSON.stringify(field.selector)}`);
36786
+ }
36787
+ }
36788
+ if (field.attribute !== void 0) {
36789
+ if (typeof field.attribute !== "string" || !DOM_ATTRIBUTE_RE.test(field.attribute)) {
36790
+ throw new ProtocolError3(`${fieldLabel}.attribute: invalid ${JSON.stringify(field.attribute)}`);
36791
+ }
36792
+ }
36793
+ if (fieldNames.has(field.name)) {
36794
+ throw new ProtocolError3(`${label}[${i}].fields: duplicate name ${JSON.stringify(field.name)}`);
36795
+ }
36796
+ fieldNames.add(field.name);
36797
+ for (const k of Object.keys(field)) {
36798
+ if (k !== "name" && k !== "selector" && k !== "attribute") {
36799
+ throw new ProtocolError3(`${fieldLabel}: unexpected field ${JSON.stringify(k)}`);
36800
+ }
36801
+ }
36802
+ }
36803
+ if (entry.maxItems !== void 0) {
36804
+ if (typeof entry.maxItems !== "number" || !Number.isInteger(entry.maxItems) || entry.maxItems < 1 || entry.maxItems > 1e3) {
36805
+ throw new ProtocolError3(`${label}[${i}].maxItems: invalid ${JSON.stringify(entry.maxItems)}`);
36806
+ }
36807
+ }
36808
+ if (seen.has(entry.name)) {
36809
+ throw new ProtocolError3(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
36810
+ }
36811
+ seen.add(entry.name);
36812
+ for (const k of Object.keys(entry)) {
36813
+ if (k !== "name" && k !== "itemSelector" && k !== "fields" && k !== "maxItems") {
36814
+ throw new ProtocolError3(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
36815
+ }
36816
+ }
36817
+ }
36818
+ }
36561
36819
  function assertGraphqlOpsArray(value, label) {
36562
36820
  if (!Array.isArray(value)) {
36563
36821
  throw new ProtocolError3(`${label}: expected array, got ${typeof value}`);
@@ -36604,6 +36862,8 @@ function validateFrame(raw) {
36604
36862
  return validateHelloRejected(raw);
36605
36863
  if (t === "extension-disconnected")
36606
36864
  return { type: "extension-disconnected" };
36865
+ if (t === "peer-gone")
36866
+ return validatePeerGone(raw);
36607
36867
  throw new ProtocolError3(`unknown frame type: ${String(t)}`);
36608
36868
  }
36609
36869
  function validateHello(raw) {
@@ -36674,16 +36934,11 @@ function validateHello(raw) {
36674
36934
  if (raw.domSelectors !== void 0) {
36675
36935
  assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
36676
36936
  }
36677
- if (raw.accepts !== void 0) {
36678
- if (!Array.isArray(raw.accepts)) {
36679
- throw new ProtocolError3("hello.accepts: expected array");
36680
- }
36681
- for (const a of raw.accepts) {
36682
- if (typeof a !== "string") {
36683
- throw new ProtocolError3(`hello.accepts: entry must be string, got ${typeof a}`);
36684
- }
36685
- }
36937
+ if (raw.domListSelectors !== void 0) {
36938
+ assertDomListSelectorsArray(raw.domListSelectors, "hello.domListSelectors");
36686
36939
  }
36940
+ if (raw.accepts !== void 0)
36941
+ assertAcceptsList(raw.accepts);
36687
36942
  if (raw.graphqlOps !== void 0) {
36688
36943
  assertGraphqlOpsArray(raw.graphqlOps, "hello.graphqlOps");
36689
36944
  }
@@ -36705,6 +36960,8 @@ function validateHello(raw) {
36705
36960
  assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
36706
36961
  assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
36707
36962
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
36963
+ if (raw.accepts !== void 0)
36964
+ assertAcceptsList(raw.accepts);
36708
36965
  return raw;
36709
36966
  }
36710
36967
  throw new ProtocolError3(`hello.role: must be 'server' or 'extension', got ${String(role)}`);
@@ -36759,6 +37016,27 @@ function validatePairPending(raw) {
36759
37016
  }
36760
37017
  return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
36761
37018
  }
37019
+ function assertAcceptsList(accepts) {
37020
+ if (!Array.isArray(accepts)) {
37021
+ throw new ProtocolError3("hello.accepts: expected array");
37022
+ }
37023
+ for (const a of accepts) {
37024
+ if (typeof a !== "string") {
37025
+ throw new ProtocolError3(`hello.accepts: entry must be string, got ${typeof a}`);
37026
+ }
37027
+ }
37028
+ }
37029
+ function validatePeerGone(raw) {
37030
+ assertString(raw.mcpId, "peer-gone.mcpId");
37031
+ if (!isValidMcpId(raw.mcpId))
37032
+ throw new ProtocolError3("peer-gone.mcpId: invalid format");
37033
+ for (const k of Object.keys(raw)) {
37034
+ if (k !== "type" && k !== "mcpId") {
37035
+ throw new ProtocolError3(`peer-gone: unexpected field ${JSON.stringify(k)}`);
37036
+ }
37037
+ }
37038
+ return { type: "peer-gone", mcpId: raw.mcpId };
37039
+ }
36762
37040
  function validateHelloRejected(raw) {
36763
37041
  assertString(raw.mcpId, "hello-rejected.mcpId");
36764
37042
  if (!isValidMcpId(raw.mcpId))
@@ -37008,6 +37286,24 @@ function validateInnerRequest(raw) {
37008
37286
  }
37009
37287
  return raw;
37010
37288
  }
37289
+ if (raw.op === "read_dom_list") {
37290
+ assertObject(raw.init, "inner.init");
37291
+ if (raw.init.origin === void 0)
37292
+ throw new ProtocolError3("inner.init.origin: missing");
37293
+ if (raw.init.name === void 0)
37294
+ throw new ProtocolError3("inner.init.name: missing");
37295
+ assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
37296
+ assertString(raw.init.name, "inner.init.name");
37297
+ if (raw.init.name.length === 0) {
37298
+ throw new ProtocolError3("inner.init.name: must be non-empty");
37299
+ }
37300
+ for (const k of Object.keys(raw.init)) {
37301
+ if (k !== "origin" && k !== "name") {
37302
+ throw new ProtocolError3(`inner.init: unexpected field ${JSON.stringify(k)} on read_dom_list`);
37303
+ }
37304
+ }
37305
+ return raw;
37306
+ }
37011
37307
  if (raw.op === "graphql_query") {
37012
37308
  assertObject(raw.init, "inner.init");
37013
37309
  if (raw.init.name === void 0)
@@ -37055,7 +37351,7 @@ function validateInnerRequest(raw) {
37055
37351
  }
37056
37352
  return raw;
37057
37353
  }
37058
- throw new ProtocolError3(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download", "graphql_query", "write_cookies"; got ${JSON.stringify(raw.op)}`);
37354
+ throw new ProtocolError3(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "read_dom_list", "download", "graphql_query", "write_cookies"; got ${JSON.stringify(raw.op)}`);
37059
37355
  }
37060
37356
  function assertNonEmptyKeyArray(value, label) {
37061
37357
  if (!Array.isArray(value)) {
@@ -37159,6 +37455,18 @@ function validateInnerResponse(raw) {
37159
37455
  assertStringMap(raw.values, "inner.values");
37160
37456
  return raw;
37161
37457
  }
37458
+ if (op === "read_dom_list") {
37459
+ if (raw.rows === void 0) {
37460
+ throw new ProtocolError3("inner.rows: missing on read_dom_list response");
37461
+ }
37462
+ if (!Array.isArray(raw.rows)) {
37463
+ throw new ProtocolError3("inner.rows: expected array, got " + typeof raw.rows);
37464
+ }
37465
+ for (const [i, row] of raw.rows.entries()) {
37466
+ assertStringMap(row, `inner.rows[${i}]`);
37467
+ }
37468
+ return raw;
37469
+ }
37162
37470
  if (op === "graphql_query") {
37163
37471
  if (raw.data === void 0) {
37164
37472
  throw new ProtocolError3("inner.data: missing on graphql_query response");
@@ -37291,12 +37599,6 @@ async function sealInnerFrame(sessionKey, mcpId, seq, inner, direction) {
37291
37599
  ciphertext: toB64(ct)
37292
37600
  };
37293
37601
  }
37294
- async function openEncryptedFrame(sessionKey, frame, direction) {
37295
- const result = await openEncryptedFrameDetailed(sessionKey, frame, direction);
37296
- if (result.stage === "ok")
37297
- return result.inner;
37298
- throw result.error instanceof Error ? result.error : new Error(String(result.error));
37299
- }
37300
37602
  async function openEncryptedFrameDetailed(sessionKey, frame, direction) {
37301
37603
  let pt;
37302
37604
  try {
@@ -39645,7 +39947,7 @@ var require_websocket = __commonJS({
39645
39947
  var http = __require("http");
39646
39948
  var net = __require("net");
39647
39949
  var tls = __require("tls");
39648
- var { randomBytes: randomBytes3, createHash } = __require("crypto");
39950
+ var { randomBytes: randomBytes4, createHash } = __require("crypto");
39649
39951
  var { Duplex, Readable } = __require("stream");
39650
39952
  var { URL: URL2 } = __require("url");
39651
39953
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -40183,7 +40485,7 @@ var require_websocket = __commonJS({
40183
40485
  }
40184
40486
  }
40185
40487
  const defaultPort = isSecure ? 443 : 80;
40186
- const key = randomBytes3(16).toString("base64");
40488
+ const key = randomBytes4(16).toString("base64");
40187
40489
  const request = isSecure ? https.request : http.request;
40188
40490
  const protocolSet = /* @__PURE__ */ new Set();
40189
40491
  let perMessageDeflate;
@@ -41157,6 +41459,18 @@ async function buildServerHello(opts) {
41157
41459
  ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
41158
41460
  }));
41159
41461
  }
41462
+ if (opts.domListSelectors && opts.domListSelectors.length > 0) {
41463
+ hello.domListSelectors = opts.domListSelectors.map((d) => ({
41464
+ name: d.name,
41465
+ itemSelector: d.itemSelector,
41466
+ fields: d.fields.map((f) => ({
41467
+ name: f.name,
41468
+ ...f.selector !== void 0 ? { selector: f.selector } : {},
41469
+ ...f.attribute !== void 0 ? { attribute: f.attribute } : {}
41470
+ })),
41471
+ ...d.maxItems !== void 0 ? { maxItems: d.maxItems } : {}
41472
+ }));
41473
+ }
41160
41474
  if (opts.graphqlOps && opts.graphqlOps.length > 0) {
41161
41475
  hello.graphqlOps = opts.graphqlOps.map((d) => ({
41162
41476
  name: d.name,
@@ -41345,7 +41659,7 @@ var init_session_ready = __esm({
41345
41659
  });
41346
41660
 
41347
41661
  // node_modules/@fetchproxy/server/dist/identity.js
41348
- import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink } from "node:fs/promises";
41662
+ import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink, link, lstat } from "node:fs/promises";
41349
41663
  import { randomBytes as randomBytes2 } from "node:crypto";
41350
41664
  import { isAbsolute, join as join2 } from "node:path";
41351
41665
  import { homedir as homedir2 } from "node:os";
@@ -41410,7 +41724,7 @@ async function openIdentityDir(dir) {
41410
41724
  throw e;
41411
41725
  }
41412
41726
  }
41413
- async function sweepStagingFiles(dir, serverName) {
41727
+ async function sweepStagingFiles(dir, serverName, opts = {}) {
41414
41728
  const base = safeIdentityFileBase(serverName).replace(/[.]/g, "\\.");
41415
41729
  const staging = new RegExp(`^${base}\\.json\\.[0-9a-f]{${STAGING_BYTES * 2}}\\.tmp$`);
41416
41730
  let names;
@@ -41419,48 +41733,94 @@ async function sweepStagingFiles(dir, serverName) {
41419
41733
  } catch {
41420
41734
  return;
41421
41735
  }
41422
- await Promise.all(names.filter((name) => staging.test(name)).map((name) => unlink(join2(dir, name)).catch(() => void 0)));
41736
+ const cutoff = opts.olderThanMs === void 0 ? null : Date.now() - opts.olderThanMs;
41737
+ await Promise.all(names.filter((name) => staging.test(name)).map(async (name) => {
41738
+ const path = join2(dir, name);
41739
+ if (cutoff !== null) {
41740
+ const st = await lstat(path).catch(() => null);
41741
+ if (st === null || st.mtimeMs > cutoff)
41742
+ return;
41743
+ }
41744
+ await unlink(path).catch(() => void 0);
41745
+ }));
41423
41746
  }
41424
- async function writeIdentityFile(dir, serverName, id) {
41747
+ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
41425
41748
  const path = identityFilePath(dir, serverName);
41426
41749
  await openIdentityDir(dir);
41427
- await sweepStagingFiles(dir, serverName);
41428
- const tmp = `${path}.${randomBytes2(STAGING_BYTES).toString("hex")}.tmp`;
41429
41750
  try {
41430
- await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
41431
- await chmod(tmp, 384);
41432
- await rename(tmp, path);
41751
+ return parseIdentity(await readFile(path, "utf8"));
41433
41752
  } catch (e) {
41434
- await rm(tmp, { force: true });
41435
- throw e;
41753
+ if (e.code !== "ENOENT" && !(e instanceof SyntaxError))
41754
+ throw e;
41755
+ }
41756
+ let unreadable = null;
41757
+ for (let attempt = 0; attempt < 5; attempt++) {
41758
+ const id = await generateIdentity();
41759
+ if (await publishIdentityIfAbsent(dir, serverName, id))
41760
+ return id;
41761
+ try {
41762
+ return parseIdentity(await readFile(path, "utf8"));
41763
+ } catch (e) {
41764
+ if (e.code === "ENOENT") {
41765
+ continue;
41766
+ }
41767
+ if (!(e instanceof SyntaxError))
41768
+ throw e;
41769
+ unreadable = e;
41770
+ await new Promise((r) => setTimeout(r, 20 * (attempt + 1)));
41771
+ }
41436
41772
  }
41437
- return path;
41773
+ if (unreadable !== null)
41774
+ throw unreadable;
41775
+ throw new Error(`fetchproxy: could not create an identity for ${JSON.stringify(serverName)} in ${dir}`);
41438
41776
  }
41439
- async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
41777
+ async function publishIdentityIfAbsent(dir, serverName, id) {
41440
41778
  const path = identityFilePath(dir, serverName);
41441
- await openIdentityDir(dir);
41779
+ await sweepStagingFiles(dir, serverName, { olderThanMs: 6e4 });
41780
+ const tmp = `${path}.${randomBytes2(STAGING_BYTES).toString("hex")}.tmp`;
41442
41781
  try {
41443
- return parseIdentity(await readFile(path, "utf8"));
41782
+ await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
41783
+ await chmod(tmp, 384);
41784
+ try {
41785
+ await link(tmp, path);
41786
+ } catch (e) {
41787
+ const code = e.code;
41788
+ if (code === void 0 || !NO_HARD_LINKS.has(code))
41789
+ throw e;
41790
+ try {
41791
+ await writeFile(path, serializeIdentity(id), { mode: 384, flag: "wx" });
41792
+ } catch (writeErr) {
41793
+ if (writeErr.code !== "EEXIST") {
41794
+ await rm(path, { force: true }).catch(() => void 0);
41795
+ }
41796
+ throw writeErr;
41797
+ }
41798
+ await chmod(path, 384);
41799
+ }
41800
+ return true;
41444
41801
  } catch (e) {
41445
- if (e.code !== "ENOENT")
41446
- throw e;
41802
+ const code = e.code;
41803
+ if (code === "EEXIST" || code === "ENOENT")
41804
+ return false;
41805
+ throw e;
41806
+ } finally {
41807
+ await rm(tmp, { force: true });
41447
41808
  }
41448
- const id = await generateIdentity();
41449
- await writeIdentityFile(dir, serverName, id);
41450
- return id;
41451
41809
  }
41452
- var SAFE_PLAIN, SAFE_SCOPED, STAGING_BYTES;
41810
+ var SAFE_PLAIN, SAFE_SCOPED, STAGING_BYTES, NO_HARD_LINKS;
41453
41811
  var init_identity = __esm({
41454
41812
  "node_modules/@fetchproxy/server/dist/identity.js"() {
41455
41813
  init_dist3();
41456
41814
  SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
41457
41815
  SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
41458
41816
  STAGING_BYTES = 6;
41817
+ NO_HARD_LINKS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS"]);
41459
41818
  }
41460
41819
  });
41461
41820
 
41462
41821
  // node_modules/@fetchproxy/server/dist/extension-trust.js
41463
- import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
41822
+ import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2, rm as rm2 } from "node:fs/promises";
41823
+ import { randomBytes as randomBytes3 } from "node:crypto";
41464
41824
  import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
41465
41825
  function envTrustDir(env = process.env) {
41466
41826
  const raw = env[TRUST_DIR_ENV];
@@ -41557,10 +41917,15 @@ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
41557
41917
  async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
41558
41918
  const path = extensionTrustPath(serverName, dir);
41559
41919
  await mkdir2(dir, { recursive: true, mode: 448 });
41560
- const tmp = `${path}.tmp`;
41561
- await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
41562
- await chmod2(tmp, 384);
41563
- await rename2(tmp, path);
41920
+ const tmp = `${path}.${randomBytes3(6).toString("hex")}.tmp`;
41921
+ try {
41922
+ await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384, flag: "wx" });
41923
+ await chmod2(tmp, 384);
41924
+ await rename2(tmp, path);
41925
+ } catch (e) {
41926
+ await rm2(tmp, { force: true });
41927
+ throw e;
41928
+ }
41564
41929
  }
41565
41930
  var TRUST_DIR_ENV, TRUST_NEW_EXTENSION_ENV;
41566
41931
  var init_extension_trust = __esm({
@@ -41640,6 +42005,7 @@ async function startHost(opts) {
41640
42005
  localStoragePointers: opts.ownLocalStoragePointers,
41641
42006
  sessionStoragePointers: opts.ownSessionStoragePointers,
41642
42007
  domSelectors: opts.ownDomSelectors,
42008
+ domListSelectors: opts.ownDomListSelectors,
41643
42009
  graphqlOps: opts.ownGraphqlOps
41644
42010
  };
41645
42011
  const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
@@ -41710,6 +42076,31 @@ async function startHost(opts) {
41710
42076
  return null;
41711
42077
  }
41712
42078
  };
42079
+ let ownDecryptFailures = 0;
42080
+ const rehandshakeOwnSession = async (ws) => {
42081
+ const hello = extensionHello;
42082
+ if (extensionWs !== ws || !hello)
42083
+ return;
42084
+ console.warn(`[fetchproxy] ${opts.ownServerName}: ${OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE} frames in a row from the extension failed authentication under this MCP's session key \u2014 re-handshaking this MCP's session (the extension stays connected).`);
42085
+ ownSession = dropOwnSessionAndEphemeral();
42086
+ resetSessionPromise();
42087
+ const mintedKeypair = await generateSessionKeypair();
42088
+ const mintedHello = await buildServerHello({
42089
+ ...ownHelloBase,
42090
+ sessionPub: mintedKeypair.publicKey,
42091
+ answersExtNonce: fromB64(hello.sessionNonce)
42092
+ });
42093
+ if (extensionWs !== ws || extensionHello !== hello || ownSession !== null) {
42094
+ mintedKeypair.privateKey.fill(0);
42095
+ return;
42096
+ }
42097
+ installOwnEphemeral({
42098
+ nonce: fromB64(mintedHello.sessionNonce),
42099
+ pub: mintedKeypair.publicKey,
42100
+ priv: mintedKeypair.privateKey
42101
+ });
42102
+ ws.send(JSON.stringify(mintedHello));
42103
+ };
41713
42104
  wss.on("connection", (ws) => {
41714
42105
  let identified = null;
41715
42106
  let peerMcpId = null;
@@ -41900,6 +42291,7 @@ async function startHost(opts) {
41900
42291
  if (extensionWs !== ws)
41901
42292
  return;
41902
42293
  ownSession = new SessionState(key);
42294
+ ownDecryptFailures = 0;
41903
42295
  ownPendingPairCode = null;
41904
42296
  resolveOwnSession(ownSession);
41905
42297
  } else {
@@ -41921,15 +42313,42 @@ async function startHost(opts) {
41921
42313
  }
41922
42314
  if (claim2 !== "ok")
41923
42315
  return;
41924
- let inner;
42316
+ let result;
41925
42317
  try {
41926
- inner = await openEncryptedFrame(session.sessionKey, frame, "e2s");
42318
+ result = await openEncryptedFrameDetailed(session.sessionKey, frame, "e2s");
41927
42319
  } catch (e) {
41928
42320
  session.releaseInboundSeq(frame.seq);
41929
42321
  throw e;
41930
42322
  }
42323
+ if (result.stage === "decrypt-failed") {
42324
+ session.releaseInboundSeq(frame.seq);
42325
+ console.warn(`[fetchproxy] ${opts.ownServerName}: dropped an inbound frame (seq ${frame.seq}) that failed authentication \u2014 most likely a straggler from a previous session.`);
42326
+ if (session === ownSession) {
42327
+ ownDecryptFailures += 1;
42328
+ if (ownDecryptFailures >= OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE) {
42329
+ ownDecryptFailures = 0;
42330
+ void rehandshakeOwnSession(ws).catch((e) => console.error(`[fetchproxy] ${opts.ownServerName}: re-handshake failed:`, e));
42331
+ }
42332
+ }
42333
+ return;
42334
+ }
42335
+ if (session === ownSession)
42336
+ ownDecryptFailures = 0;
41931
42337
  session.commitInboundSeq(frame.seq);
41932
- ownInnerListeners.forEach((cb) => cb(inner));
42338
+ if (result.stage === "ok") {
42339
+ ownInnerListeners.forEach((cb) => cb(result.inner));
42340
+ } else {
42341
+ console.error("[fetchproxy] host: received a frame that decrypted OK but failed validation:", result.error);
42342
+ const recoveredId = result.recoveredId;
42343
+ if (recoveredId !== void 0) {
42344
+ ownInnerListeners.forEach((cb) => cb({
42345
+ type: "response",
42346
+ id: recoveredId,
42347
+ ok: false,
42348
+ error: `malformed response failed protocol validation: ${String(result.error)}`
42349
+ }));
42350
+ }
42351
+ }
41933
42352
  } else {
41934
42353
  const slot = peers.get(frame.mcpId);
41935
42354
  if (slot)
@@ -41988,6 +42407,7 @@ async function startHost(opts) {
41988
42407
  rejectOwnSession(new Error("extension disconnected before ready"));
41989
42408
  }
41990
42409
  ownSession = dropOwnSessionAndEphemeral();
42410
+ ownDecryptFailures = 0;
41991
42411
  ownPendingPairCode = null;
41992
42412
  resetSessionPromise();
41993
42413
  disconnectListeners.forEach((cb) => cb());
@@ -42002,8 +42422,15 @@ async function startHost(opts) {
42002
42422
  }
42003
42423
  }
42004
42424
  if (identified === "peer" && peerMcpId) {
42005
- if (peers.get(peerMcpId)?.ws === ws)
42425
+ if (peers.get(peerMcpId)?.ws === ws) {
42006
42426
  peers.delete(peerMcpId);
42427
+ if (extensionWs && extensionHello?.accepts?.includes("peer-gone")) {
42428
+ try {
42429
+ extensionWs.send(JSON.stringify({ type: "peer-gone", mcpId: peerMcpId }));
42430
+ } catch {
42431
+ }
42432
+ }
42433
+ }
42007
42434
  }
42008
42435
  });
42009
42436
  });
@@ -42044,7 +42471,7 @@ async function startHost(opts) {
42044
42471
  sessionLinked: () => ownSession !== null
42045
42472
  };
42046
42473
  }
42047
- var HTTP_ORIGIN_RE, PUBLIC_ORIGIN_RE, EXTENSION_ORIGIN_RE, OPAQUE_ORIGIN, ALLOW_LOCAL_ORIGINS_ENV, HANDSHAKE_TIMEOUT_MS, MAX_PAYLOAD_BYTES, enc4;
42474
+ var HTTP_ORIGIN_RE, PUBLIC_ORIGIN_RE, EXTENSION_ORIGIN_RE, OPAQUE_ORIGIN, ALLOW_LOCAL_ORIGINS_ENV, HANDSHAKE_TIMEOUT_MS, MAX_PAYLOAD_BYTES, enc4, OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE;
42048
42475
  var init_host = __esm({
42049
42476
  "node_modules/@fetchproxy/server/dist/host.js"() {
42050
42477
  init_wrapper();
@@ -42062,15 +42489,21 @@ var init_host = __esm({
42062
42489
  HANDSHAKE_TIMEOUT_MS = 15e3;
42063
42490
  MAX_PAYLOAD_BYTES = MAX_FRAME_BYTES;
42064
42491
  enc4 = new TextEncoder();
42492
+ OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE = 3;
42065
42493
  }
42066
42494
  });
42067
42495
 
42068
42496
  // node_modules/@fetchproxy/server/dist/peer.js
42497
+ function notSentError() {
42498
+ return new Error("fetchproxy: request not sent \u2014 the connection to the bridge host closed before it went out, so nothing reached the browser. It is safe to retry.");
42499
+ }
42069
42500
  async function startPeer(opts) {
42070
42501
  const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`, {
42071
42502
  maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES
42072
42503
  });
42504
+ const dialTimeoutMs = opts.dialTimeoutMs ?? PEER_DIAL_TIMEOUT_MS;
42073
42505
  await new Promise((resolve2, reject) => {
42506
+ let timer;
42074
42507
  const onOpen = () => {
42075
42508
  cleanup();
42076
42509
  resolve2();
@@ -42080,11 +42513,21 @@ async function startPeer(opts) {
42080
42513
  reject(e);
42081
42514
  };
42082
42515
  const cleanup = () => {
42516
+ if (timer !== void 0)
42517
+ clearTimeout(timer);
42083
42518
  ws.off("open", onOpen);
42084
42519
  ws.off("error", onError);
42085
42520
  };
42086
42521
  ws.once("open", onOpen);
42087
42522
  ws.once("error", onError);
42523
+ if (dialTimeoutMs > 0) {
42524
+ timer = setTimeout(() => {
42525
+ cleanup();
42526
+ ws.on("error", () => void 0);
42527
+ ws.terminate();
42528
+ reject(new Error(`fetchproxy: the process holding ${opts.host}:${opts.port} did not answer the WebSocket upgrade within ${dialTimeoutMs}ms \u2014 it may be wedged or not a fetchproxy host. Restart the MCP that owns the port (or free it) and retry.`));
42529
+ }, dialTimeoutMs);
42530
+ }
42088
42531
  });
42089
42532
  ws.on("error", (e) => {
42090
42533
  console.warn(`[fetchproxy] peer: socket error: ${String(e)}`);
@@ -42103,6 +42546,7 @@ async function startPeer(opts) {
42103
42546
  captureHeaders: opts.captureHeaders,
42104
42547
  indexedDbScopes: opts.indexedDbScopes,
42105
42548
  domSelectors: opts.domSelectors,
42549
+ domListSelectors: opts.domListSelectors,
42106
42550
  localStoragePointers: opts.localStoragePointers,
42107
42551
  sessionStoragePointers: opts.sessionStoragePointers,
42108
42552
  graphqlOps: opts.graphqlOps,
@@ -42126,6 +42570,7 @@ async function startPeer(opts) {
42126
42570
  const renegotiateListeners = [];
42127
42571
  const pendingPairListeners = [];
42128
42572
  const closeListeners = [];
42573
+ const extensionDisconnectListeners = [];
42129
42574
  let session = null;
42130
42575
  let sessionEphemeral = null;
42131
42576
  function dropSessionEphemeral() {
@@ -42145,10 +42590,24 @@ async function startPeer(opts) {
42145
42590
  let warnedUnverifiablePairCode = false;
42146
42591
  let resolveFirstReady;
42147
42592
  let rejectFirstReady;
42148
- const sessionPromise = new Promise((resolve2, reject) => {
42149
- resolveFirstReady = resolve2;
42150
- rejectFirstReady = reject;
42151
- });
42593
+ let sessionPromise;
42594
+ let sessionPromiseSettled = false;
42595
+ function resetSessionPromise() {
42596
+ sessionPromiseSettled = false;
42597
+ sessionPromise = new Promise((resolve2, reject) => {
42598
+ resolveFirstReady = (st) => {
42599
+ sessionPromiseSettled = true;
42600
+ resolve2(st);
42601
+ };
42602
+ rejectFirstReady = (e) => {
42603
+ sessionPromiseSettled = true;
42604
+ reject(e);
42605
+ };
42606
+ });
42607
+ sessionPromise.catch(() => {
42608
+ });
42609
+ }
42610
+ resetSessionPromise();
42152
42611
  let extensionHello = null;
42153
42612
  let extensionGone = false;
42154
42613
  let cachedPin = void 0;
@@ -42245,6 +42704,12 @@ async function startPeer(opts) {
42245
42704
  extensionGone = true;
42246
42705
  dropSessionEphemeral();
42247
42706
  pendingPairCode = null;
42707
+ if (session !== null) {
42708
+ session = null;
42709
+ if (sessionPromiseSettled)
42710
+ resetSessionPromise();
42711
+ }
42712
+ extensionDisconnectListeners.forEach((cb) => cb());
42248
42713
  return;
42249
42714
  }
42250
42715
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
@@ -42361,19 +42826,30 @@ async function startPeer(opts) {
42361
42826
  }
42362
42827
  closeListeners.forEach((cb) => cb());
42363
42828
  });
42364
- sessionPromise.catch(() => {
42365
- });
42366
42829
  const handle = {
42367
42830
  ws,
42368
- session: sessionPromise,
42831
+ // A getter: `extension-disconnected` replaces the promise (B-BUG-5).
42832
+ get session() {
42833
+ return sessionPromise;
42834
+ },
42369
42835
  sendInner: async (inner) => {
42370
- await awaitSessionReady(sessionPromise, {
42371
- mcpId: opts.mcpId,
42372
- pendingPairCode: () => pendingPairCode
42373
- });
42836
+ try {
42837
+ await awaitSessionReady(sessionPromise, {
42838
+ mcpId: opts.mcpId,
42839
+ pendingPairCode: () => pendingPairCode
42840
+ });
42841
+ } catch (e) {
42842
+ if (ws.readyState !== import_websocket.default.OPEN)
42843
+ throw notSentError();
42844
+ throw e;
42845
+ }
42374
42846
  const s = session;
42847
+ if (s === null)
42848
+ throw new Error("peer: extension disconnected");
42375
42849
  const plaintext = encodeOutboundInnerFrame(opts.mcpId, inner);
42376
42850
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), plaintext, "s2e");
42851
+ if (ws.readyState !== import_websocket.default.OPEN)
42852
+ throw notSentError();
42377
42853
  ws.send(JSON.stringify(sealed));
42378
42854
  },
42379
42855
  onInner: (cb) => {
@@ -42391,11 +42867,14 @@ async function startPeer(opts) {
42391
42867
  onClose: (cb) => {
42392
42868
  closeListeners.push(cb);
42393
42869
  },
42870
+ onExtensionDisconnect: (cb) => {
42871
+ extensionDisconnectListeners.push(cb);
42872
+ },
42394
42873
  close: () => ws.close()
42395
42874
  };
42396
42875
  return handle;
42397
42876
  }
42398
- var enc5;
42877
+ var PEER_DIAL_TIMEOUT_MS, enc5;
42399
42878
  var init_peer = __esm({
42400
42879
  "node_modules/@fetchproxy/server/dist/peer.js"() {
42401
42880
  init_wrapper();
@@ -42406,21 +42885,22 @@ var init_peer = __esm({
42406
42885
  init_session();
42407
42886
  init_session_ready();
42408
42887
  init_extension_trust();
42888
+ PEER_DIAL_TIMEOUT_MS = 5e3;
42409
42889
  enc5 = new TextEncoder();
42410
42890
  }
42411
42891
  });
42412
42892
 
42413
42893
  // node_modules/@fetchproxy/server/dist/error-kind.js
42414
42894
  function classifyFetchError(error62) {
42895
+ if (/^(in-page )?fetch threw:/.test(error62)) {
42896
+ return "tab_fetch_failed";
42897
+ }
42415
42898
  if (/Could not establish connection/i.test(error62) || /Receiving end does not exist/i.test(error62)) {
42416
42899
  return "content_script_unreachable";
42417
42900
  }
42418
42901
  if (/^tab fetch failed:/.test(error62)) {
42419
42902
  return "tab_fetch_failed";
42420
42903
  }
42421
- if (/^(in-page )?fetch threw:/.test(error62)) {
42422
- return "tab_fetch_failed";
42423
- }
42424
42904
  if (/^no tab matching /.test(error62)) {
42425
42905
  return "no_tab";
42426
42906
  }
@@ -42548,7 +43028,32 @@ function assertUrlInDomains(field, url2, domains) {
42548
43028
  const declared = domains.map((d) => JSON.stringify(d)).join(", ");
42549
43029
  throw new Error(`FetchproxyServer: ${field} host "${host}" is outside declared domains [${declared}] \u2014 must be one of them or a subdomain`);
42550
43030
  }
42551
- var FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyHintedError, FetchproxyScopeError, FetchproxyNoTabError, FetchproxyTabOpeningError, FetchproxyWaitedError, SCOPE_REJECTION, NO_TAB_REJECTION, TAB_OPENING_REJECTION, VERB_DEADLINE_GRACE_MS, FetchproxyTimeoutError, SUBDOMAIN_LABEL_RE, DEFAULT_JSON_OK_STATUSES, FetchproxyServer;
43031
+ function parseJsonBody(response) {
43032
+ if (response.status === 204 || response.body.trim() === "")
43033
+ return null;
43034
+ return JSON.parse(response.body);
43035
+ }
43036
+ function isRetrySafeOnTimeout(method, retryOnTimeout) {
43037
+ if (retryOnTimeout !== void 0)
43038
+ return retryOnTimeout;
43039
+ return TIMEOUT_RETRY_SAFE_METHODS.has(method.toUpperCase());
43040
+ }
43041
+ function isResendableRequest(inner) {
43042
+ switch (inner.op) {
43043
+ case "fetch":
43044
+ return isRetrySafeOnTimeout(inner.init.method);
43045
+ case "write_cookies":
43046
+ case "download":
43047
+ // A declared GraphQL operation may be a mutation; the server holds only
43048
+ // its name, so it cannot tell. `graphqlQuery({ retryOnTimeout: true })`
43049
+ // passes `resendable` explicitly for a known query.
43050
+ case "graphql_query":
43051
+ return false;
43052
+ default:
43053
+ return true;
43054
+ }
43055
+ }
43056
+ var FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyHintedError, FetchproxyScopeError, FetchproxyNoTabError, FetchproxyTabOpeningError, FetchproxyWaitedError, SCOPE_REJECTION, NO_TAB_REJECTION, TAB_OPENING_REJECTION, VERB_DEADLINE_GRACE_MS, FetchproxyTimeoutError, SUBDOMAIN_LABEL_RE, DEFAULT_JSON_OK_STATUSES, TIMEOUT_RETRY_SAFE_METHODS, FetchproxyServer;
42552
43057
  var init_ws_server = __esm({
42553
43058
  "node_modules/@fetchproxy/server/dist/ws-server.js"() {
42554
43059
  init_dist3();
@@ -42668,6 +43173,17 @@ var init_ws_server = __esm({
42668
43173
  * the message says which number that was and where it came from.
42669
43174
  */
42670
43175
  requestedTimeoutMs;
43176
+ /**
43177
+ * B-BUG-1: whether re-sending this request is safe. A timeout only means
43178
+ * the reply did not arrive in time — the extension may already have run the
43179
+ * request in the tab — so this is `false` for a non-idempotent `fetch`
43180
+ * (anything but GET/HEAD/OPTIONS) the caller did not mark `retryOnTimeout`,
43181
+ * for `download` and `writeCookies`, and for `graphqlQuery` unless the
43182
+ * caller marked it `retryOnTimeout` (a declared operation may be a
43183
+ * mutation). `retryOnceOnTimeout` honours it. Defaults to `true` (the read
43184
+ * verbs).
43185
+ */
43186
+ retrySafe;
42671
43187
  constructor(args) {
42672
43188
  const graceMs = args.graceMs ?? VERB_DEADLINE_GRACE_MS;
42673
43189
  const callerChose = args.requestedTimeoutMs !== void 0 && args.timeoutMs === args.requestedTimeoutMs + graceMs;
@@ -42680,10 +43196,12 @@ var init_ws_server = __esm({
42680
43196
  this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
42681
43197
  this.retryAttempted = args.retryAttempted ?? false;
42682
43198
  this.requestedTimeoutMs = args.requestedTimeoutMs ?? null;
43199
+ this.retrySafe = args.retrySafe ?? true;
42683
43200
  }
42684
43201
  };
42685
43202
  SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
42686
43203
  DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
43204
+ TIMEOUT_RETRY_SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
42687
43205
  FetchproxyServer = class {
42688
43206
  /**
42689
43207
  * Bridge role. `null` until the first verb call (or an explicit
@@ -42732,12 +43250,22 @@ var init_ws_server = __esm({
42732
43250
  pendingRedirect = /* @__PURE__ */ new Map();
42733
43251
  // 0.4.0+: read_indexed_db awaiters resolve a JSON-typed values map.
42734
43252
  pendingIdb = /* @__PURE__ */ new Map();
43253
+ // 3.1.0+: read_dom_list awaiters resolve an array of field-value rows —
43254
+ // a different shape from `pendingStorage`'s single map, so its own map.
43255
+ pendingDomList = /* @__PURE__ */ new Map();
42735
43256
  // download awaiters resolve the saved-file metadata (path + size + mime).
42736
43257
  pendingDownload = /* @__PURE__ */ new Map();
42737
43258
  // 1.x+: graphql_query awaiters resolve the GraphQL `data` object. Its
42738
43259
  // shape is operation-specific, so the awaiter resolves `unknown` and the
42739
43260
  // caller narrows.
42740
43261
  pendingGraphql = /* @__PURE__ */ new Map();
43262
+ /**
43263
+ * B-BUG-6: the frames of in-flight requests that are safe to send again
43264
+ * if the host that relayed them exits before replying (see
43265
+ * `recoverFromHostLoss`). Keyed by request id; an entry outlives its
43266
+ * request only until the next prune or response.
43267
+ */
43268
+ resendable = /* @__PURE__ */ new Map();
42741
43269
  mcpId = null;
42742
43270
  identity = null;
42743
43271
  // 0.5.3+: in-flight role-election / handle-start promise. Set the
@@ -42783,7 +43311,7 @@ var init_ws_server = __esm({
42783
43311
  }
42784
43312
  for (const c of opts.capabilities) {
42785
43313
  if (!KNOWN_CAPABILITIES.has(c)) {
42786
- throw new Error(`FetchproxyServer: unknown capability ${JSON.stringify(c)} \u2014 known values: ["fetch", "read_cookies"]`);
43314
+ throw new Error(`FetchproxyServer: unknown capability ${JSON.stringify(c)} \u2014 known values: ${JSON.stringify([...KNOWN_CAPABILITIES])}`);
42787
43315
  }
42788
43316
  }
42789
43317
  capabilities = [...opts.capabilities];
@@ -42830,6 +43358,16 @@ var init_ws_server = __esm({
42830
43358
  selector: d.selector,
42831
43359
  ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
42832
43360
  })),
43361
+ domListSelectors: (opts.domListSelectors ?? []).map((d) => ({
43362
+ name: d.name,
43363
+ itemSelector: d.itemSelector,
43364
+ fields: d.fields.map((f) => ({
43365
+ name: f.name,
43366
+ ...f.selector !== void 0 ? { selector: f.selector } : {},
43367
+ ...f.attribute !== void 0 ? { attribute: f.attribute } : {}
43368
+ })),
43369
+ ...d.maxItems !== void 0 ? { maxItems: d.maxItems } : {}
43370
+ })),
42833
43371
  graphqlOps: (opts.graphqlOps ?? []).map((d) => ({
42834
43372
  name: d.name,
42835
43373
  operationName: d.operationName
@@ -42959,6 +43497,7 @@ var init_ws_server = __esm({
42959
43497
  ownLocalStoragePointers: this.opts.localStoragePointers,
42960
43498
  ownSessionStoragePointers: this.opts.sessionStoragePointers,
42961
43499
  ownDomSelectors: this.opts.domSelectors,
43500
+ ownDomListSelectors: this.opts.domListSelectors,
42962
43501
  ownGraphqlOps: this.opts.graphqlOps,
42963
43502
  onPairCode: this.opts.onPairCode,
42964
43503
  extensionTrust: this.extensionTrust()
@@ -42990,6 +43529,7 @@ var init_ws_server = __esm({
42990
43529
  localStoragePointers: this.opts.localStoragePointers,
42991
43530
  sessionStoragePointers: this.opts.sessionStoragePointers,
42992
43531
  domSelectors: this.opts.domSelectors,
43532
+ domListSelectors: this.opts.domListSelectors,
42993
43533
  graphqlOps: this.opts.graphqlOps,
42994
43534
  extensionTrust: this.extensionTrust(),
42995
43535
  requireExtensionIdentity: this.opts.requireExtensionIdentity
@@ -42999,6 +43539,10 @@ var init_ws_server = __esm({
42999
43539
  this.stopKeepalive();
43000
43540
  this.rejectAllPending();
43001
43541
  });
43542
+ this.peerHandle.onExtensionDisconnect(() => {
43543
+ this.stopKeepalive();
43544
+ this.rejectAllPending();
43545
+ });
43002
43546
  this.peerHandle.onPendingPair((code) => {
43003
43547
  this.rejectAllPending(this.pairingErrorMessage(code));
43004
43548
  });
@@ -43010,9 +43554,9 @@ var init_ws_server = __esm({
43010
43554
  if (this.closing || this.peerHandle === null)
43011
43555
  return;
43012
43556
  this.stopKeepalive();
43013
- this.rejectAllPending();
43014
43557
  this.peerHandle = null;
43015
43558
  this.role = null;
43559
+ void this.recoverFromHostLoss();
43016
43560
  });
43017
43561
  }
43018
43562
  }
@@ -43033,7 +43577,7 @@ var init_ws_server = __esm({
43033
43577
  * only when the bridge itself failed (no signed-in tab, extension
43034
43578
  * offline, etc.).
43035
43579
  */
43036
- async fetch(init) {
43580
+ async fetch(init, fetchOpts = {}) {
43037
43581
  await this.ensureConnected();
43038
43582
  const pendingCode = this.currentPendingPairCode();
43039
43583
  if (pendingCode !== null) {
@@ -43045,16 +43589,17 @@ var init_ws_server = __esm({
43045
43589
  retryAttempted: false
43046
43590
  };
43047
43591
  }
43048
- const first = await this._fetchOnceWithTimeout(init);
43592
+ const first = await this._fetchOnceWithTimeout(init, fetchOpts);
43049
43593
  const reviveMs = this.opts.bridgeReviveDelayMs;
43050
43594
  const isColdStartSymptom = !first.ok && (first.kind === "content_script_unreachable" || first.kind === "timeout");
43051
43595
  if (isColdStartSymptom) {
43052
43596
  this.lastEvictionDetectedAt = Date.now();
43053
43597
  }
43054
- if (isColdStartSymptom && reviveMs !== void 0 && reviveMs > 0) {
43598
+ const retryable = isColdStartSymptom && (first.kind === "content_script_unreachable" || isRetrySafeOnTimeout(init.method, fetchOpts.retryOnTimeout));
43599
+ if (retryable && reviveMs !== void 0 && reviveMs > 0) {
43055
43600
  this.lazyReviveAttempts += 1;
43056
43601
  await new Promise((r) => setTimeout(r, reviveMs));
43057
- const second = await this._fetchOnceWithTimeout(init);
43602
+ const second = await this._fetchOnceWithTimeout(init, fetchOpts);
43058
43603
  if (second.ok) {
43059
43604
  this.lazyReviveSuccesses += 1;
43060
43605
  this.recordSuccess();
@@ -43203,13 +43748,16 @@ var init_ws_server = __esm({
43203
43748
  * one of the op maps — request ids are unique) so it doesn't leak until the
43204
43749
  * server closes, then rethrow.
43205
43750
  */
43206
- async sendInnerFrame(inner) {
43751
+ async sendInnerFrame(inner, opts = {}) {
43207
43752
  try {
43208
43753
  if (this.hostHandle) {
43209
43754
  await this.hostHandle.sendOwnInner(inner);
43210
43755
  } else if (this.peerHandle) {
43211
43756
  await this.peerHandle.sendInner(inner);
43212
43757
  }
43758
+ if (inner.type === "request" && (opts.resendable ?? isResendableRequest(inner))) {
43759
+ this.noteResendable(inner);
43760
+ }
43213
43761
  } catch (err) {
43214
43762
  if ("id" in inner && typeof inner.id === "number") {
43215
43763
  const { id } = inner;
@@ -43221,6 +43769,7 @@ var init_ws_server = __esm({
43221
43769
  this.pendingRedirect.delete(id);
43222
43770
  this.pendingDownload.delete(id);
43223
43771
  this.pendingIdb.delete(id);
43772
+ this.pendingDomList.delete(id);
43224
43773
  this.pendingGraphql.delete(id);
43225
43774
  }
43226
43775
  throw err;
@@ -43231,13 +43780,25 @@ var init_ws_server = __esm({
43231
43780
  * On timeout returns the `{ok:false, kind:'timeout'}` envelope —
43232
43781
  * the throwing surface is the convenience methods.
43233
43782
  */
43234
- async _fetchOnceWithTimeout(init) {
43783
+ async _fetchOnceWithTimeout(init, fetchOpts = {}) {
43235
43784
  const id = this.nextRequestId++;
43236
43785
  const inner = { type: "request", id, op: "fetch", init };
43237
43786
  const pending = this.guardPending(new Promise((resolve2) => {
43238
43787
  this.pending.set(id, resolve2);
43239
43788
  }));
43240
- await this.sendInnerFrame(inner);
43789
+ try {
43790
+ await this.sendInnerFrame(inner, {
43791
+ resendable: isRetrySafeOnTimeout(init.method, fetchOpts.retryOnTimeout)
43792
+ });
43793
+ } catch (err) {
43794
+ return {
43795
+ ok: false,
43796
+ error: err instanceof Error ? err.message : String(err),
43797
+ kind: "session_not_ready",
43798
+ retryAttempted: false,
43799
+ cause: err
43800
+ };
43801
+ }
43241
43802
  const timeoutMs = this.opts.fetchTimeoutMs;
43242
43803
  if (timeoutMs === void 0 || timeoutMs <= 0)
43243
43804
  return pending;
@@ -43311,7 +43872,7 @@ var init_ws_server = __esm({
43311
43872
  });
43312
43873
  return pending;
43313
43874
  }
43314
- async _withVerbTimeout(pending, pendingMap, id, url2, requestedTimeoutMs) {
43875
+ async _withVerbTimeout(pending, pendingMap, id, url2, requestedTimeoutMs, retrySafe = true) {
43315
43876
  const transportMs = this.opts.fetchTimeoutMs;
43316
43877
  if (transportMs === void 0 || transportMs <= 0)
43317
43878
  return pending;
@@ -43332,6 +43893,7 @@ var init_ws_server = __esm({
43332
43893
  port: this.opts.port,
43333
43894
  elapsedMs: Date.now() - start,
43334
43895
  retryAttempted: false,
43896
+ retrySafe,
43335
43897
  graceMs,
43336
43898
  ...requestedTimeoutMs !== void 0 ? { requestedTimeoutMs } : {}
43337
43899
  }));
@@ -43349,7 +43911,7 @@ var init_ws_server = __esm({
43349
43911
  * logic re-implemented inline) `captureRequestHeader()` agree on what
43350
43912
  * to throw.
43351
43913
  */
43352
- _typedErrorFor(result, url2, op, retryAttempted) {
43914
+ _typedErrorFor(result, url2, op, retryAttempted, retrySafe = true) {
43353
43915
  if (result.kind === "timeout") {
43354
43916
  return new FetchproxyTimeoutError({
43355
43917
  url: url2,
@@ -43357,7 +43919,8 @@ var init_ws_server = __esm({
43357
43919
  role: this.role,
43358
43920
  port: this.opts.port,
43359
43921
  elapsedMs: result.elapsedMs,
43360
- retryAttempted
43922
+ retryAttempted,
43923
+ retrySafe
43361
43924
  });
43362
43925
  }
43363
43926
  if (result.kind === "content_script_unreachable") {
@@ -43431,9 +43994,12 @@ var init_ws_server = __esm({
43431
43994
  // every version before this option existed.
43432
43995
  ...opts.credentials === "omit" ? { credentials: "omit" } : {}
43433
43996
  };
43434
- const result = await this.fetch(init);
43997
+ const result = await this.fetch(init, opts.retryOnTimeout !== void 0 ? { retryOnTimeout: opts.retryOnTimeout } : {});
43435
43998
  if (!result.ok) {
43436
- throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false);
43999
+ if (result.kind === "session_not_ready" && result.cause instanceof Error) {
44000
+ throw result.cause;
44001
+ }
44002
+ throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false, isRetrySafeOnTimeout(method, opts.retryOnTimeout));
43437
44003
  }
43438
44004
  const response = {
43439
44005
  status: result.status,
@@ -43473,17 +44039,21 @@ var init_ws_server = __esm({
43473
44039
  * GET a path and parse the response body as JSON. Throws
43474
44040
  * `FetchproxyHttpError` if the status is outside the default 2xx
43475
44041
  * happy-path set (`[200, 201, 202, 204]`); pass a custom
43476
- * `expectStatus` to override.
44042
+ * `expectStatus` to override. A 204 or an empty/whitespace body
44043
+ * resolves to `null` (the declared `T` does not say so, to keep
44044
+ * existing call sites compiling — include `| null` in `T` if the
44045
+ * endpoint can answer empty).
43477
44046
  */
43478
44047
  async getJson(path, opts = {}) {
43479
44048
  const response = await this.get(path, this.applyJsonDefaults(opts));
43480
- return JSON.parse(response.body);
44049
+ return parseJsonBody(response);
43481
44050
  }
43482
44051
  /**
43483
44052
  * POST a JSON body and parse the response body as JSON. The body is
43484
44053
  * `JSON.stringify`'d; `Content-Type: application/json` is set unless
43485
44054
  * the caller already provided one. Defaults `expectStatus` to the 2xx
43486
- * happy-path set.
44055
+ * happy-path set. A 204 or an empty/whitespace body resolves to `null`
44056
+ * rather than throwing after the write already happened (see `getJson`).
43487
44057
  */
43488
44058
  async postJson(path, body, opts = {}) {
43489
44059
  const headers = { ...opts.headers ?? {} };
@@ -43495,7 +44065,7 @@ var init_ws_server = __esm({
43495
44065
  headers,
43496
44066
  body: body === void 0 ? void 0 : JSON.stringify(body)
43497
44067
  });
43498
- return JSON.parse(response.body);
44068
+ return parseJsonBody(response);
43499
44069
  }
43500
44070
  /**
43501
44071
  * GET a path and return the response body as a string. Throws
@@ -43516,7 +44086,8 @@ var init_ws_server = __esm({
43516
44086
  * - adds `Content-Type: application/json` only for a non-GET request
43517
44087
  * that carries a `body` (and only if the caller didn't set one);
43518
44088
  * - `JSON.stringify`s the body (GET / no-body sends nothing);
43519
- * - treats a `204` or an empty body as `data: null` (no parse);
44089
+ * - treats a `204`, an empty body, or a whitespace-only body as
44090
+ * `data: null` (no parse), the same rule as getJson/postJson;
43520
44091
  * - otherwise `JSON.parse`s the body.
43521
44092
  *
43522
44093
  * Scope is serialization + header defaults + 204-handling +
@@ -43530,6 +44101,11 @@ var init_ws_server = __esm({
43530
44101
  * Bridge-level failures (no signed-in tab, SW down, timeout) still
43531
44102
  * throw the typed errors via `request()`, exactly like the verb
43532
44103
  * helpers — only successful round-trips (any HTTP status) return.
44104
+ *
44105
+ * `retryOnTimeout` is `RequestOpts.retryOnTimeout`: a timed-out POST is
44106
+ * NOT re-sent unless the caller says it is safe to repeat. A read-only POST
44107
+ * (a search, a GraphQL query) should pass `true` to keep the cold-start
44108
+ * timeout retry (#90).
43533
44109
  */
43534
44110
  async requestJson(method, path, opts = {}) {
43535
44111
  const isGet = method.toUpperCase() === "GET";
@@ -43543,7 +44119,8 @@ var init_ws_server = __esm({
43543
44119
  headers,
43544
44120
  body: sendBody ? JSON.stringify(opts.body) : void 0,
43545
44121
  ...opts.subdomain !== void 0 ? { subdomain: opts.subdomain } : {},
43546
- ...opts.domain !== void 0 ? { domain: opts.domain } : {}
44122
+ ...opts.domain !== void 0 ? { domain: opts.domain } : {},
44123
+ ...opts.retryOnTimeout !== void 0 ? { retryOnTimeout: opts.retryOnTimeout } : {}
43547
44124
  });
43548
44125
  const result = {
43549
44126
  ok: true,
@@ -43551,12 +44128,9 @@ var init_ws_server = __esm({
43551
44128
  url: response.url,
43552
44129
  body: response.body
43553
44130
  };
43554
- if (response.status === 204 || response.body === "") {
43555
- return { data: null, result };
43556
- }
43557
44131
  let data;
43558
44132
  try {
43559
- data = JSON.parse(response.body);
44133
+ data = parseJsonBody(response);
43560
44134
  } catch (e) {
43561
44135
  throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
43562
44136
  }
@@ -43613,18 +44187,19 @@ var init_ws_server = __esm({
43613
44187
  };
43614
44188
  }
43615
44189
  /**
43616
- * Snapshot the user's non-HttpOnly cookies for the chosen domain.
44190
+ * Read the user's cookies for the chosen domain.
43617
44191
  *
43618
44192
  * Requires `'read_cookies'` in `FetchproxyServerOpts.capabilities`.
43619
44193
  * Throws a developer-facing `Error` at the call site if the MCP did
43620
44194
  * not declare the capability — this is a programming mistake, not a
43621
44195
  * runtime condition.
43622
44196
  *
43623
- * The returned string is the raw `document.cookie` value (semicolon-
43624
- * separated `k=v` pairs). HttpOnly cookies are NOT visible to page JS
43625
- * and are therefore not included; the underlying threat model assumes
43626
- * the cookies that matter for the auth bootstrap (session tokens, csrf
43627
- * cookies that the page itself reads) are non-HttpOnly.
44197
+ * The returned string is semicolon-separated `k=v` pairs. With `keys`
44198
+ * (each in the declared `cookieKeys`), the extension reads each name via
44199
+ * `chrome.cookies.get`, which INCLUDES HttpOnly cookies — the login
44200
+ * session cookie among them; that is what cookie-session MCPs use it
44201
+ * for. Without `keys` (legacy), it is the tab's `document.cookie`, which
44202
+ * omits HttpOnly cookies. See docs/SECURITY.md §T-cookie-exfil.
43628
44203
  *
43629
44204
  * Throws `FetchproxyProtocolError` if the bridge could not deliver
43630
44205
  * the request (no signed-in tab, extension offline, etc.).
@@ -43720,7 +44295,15 @@ var init_ws_server = __esm({
43720
44295
  this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
43721
44296
  }));
43722
44297
  await this.sendInnerFrame(inner);
43723
- return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
44298
+ return this._withVerbTimeout(
44299
+ pending,
44300
+ this.pendingWriteCookies,
44301
+ id,
44302
+ `https://${host}`,
44303
+ void 0,
44304
+ // A write: never re-sent after an ambiguous timeout.
44305
+ false
44306
+ );
43724
44307
  }
43725
44308
  /**
43726
44309
  * 0.3.0+: read declared localStorage keys from the user's signed-in
@@ -43823,53 +44406,7 @@ var init_ws_server = __esm({
43823
44406
  throw new Error("FetchproxyServer.captureRequestHeader: pass both host AND headerName, or neither (which defaults to the single declared entry)");
43824
44407
  }
43825
44408
  const callOpts = { ...resolved, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
43826
- try {
43827
- const result = await this._captureRequestHeaderOnce(callOpts);
43828
- this.recordSuccess();
43829
- return result;
43830
- } catch (err) {
43831
- const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
43832
- if (!swDown) {
43833
- this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
43834
- throw err;
43835
- }
43836
- this.lastEvictionDetectedAt = Date.now();
43837
- const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
43838
- if (reviveMs > 0) {
43839
- this.lazyReviveAttempts += 1;
43840
- await new Promise((r) => setTimeout(r, reviveMs));
43841
- try {
43842
- const result = await this._captureRequestHeaderOnce(callOpts);
43843
- this.lazyReviveSuccesses += 1;
43844
- this.recordSuccess();
43845
- return result;
43846
- } catch (retryErr) {
43847
- const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
43848
- if (!stillDown) {
43849
- this.recordFailure(`capture_request_header: ${retryErr.message ?? String(retryErr)}`);
43850
- throw retryErr;
43851
- }
43852
- this.recordFailure(`capture_request_header bridge-down: ${retryErr.message}`);
43853
- throw new FetchproxyBridgeDownError({
43854
- originalError: retryErr.message,
43855
- retryAttempted: true,
43856
- op: "capture_request_header",
43857
- url: `https://${resolved.host}${resolved.path ?? "/*"}`,
43858
- role: this.role,
43859
- port: this.opts.port
43860
- });
43861
- }
43862
- }
43863
- this.recordFailure(`capture_request_header bridge-down: ${err.message}`);
43864
- throw new FetchproxyBridgeDownError({
43865
- originalError: err.message,
43866
- retryAttempted: false,
43867
- op: "capture_request_header",
43868
- url: `https://${resolved.host}${resolved.path ?? "/*"}`,
43869
- role: this.role,
43870
- port: this.opts.port
43871
- });
43872
- }
44409
+ return this.withLazyRevive("capture_request_header", `https://${resolved.host}${resolved.path ?? "/*"}`, () => this._captureRequestHeaderOnce(callOpts));
43873
44410
  }
43874
44411
  async _captureRequestHeaderOnce(opts) {
43875
44412
  const id = this.nextRequestId++;
@@ -43918,53 +44455,7 @@ var init_ws_server = __esm({
43918
44455
  }
43919
44456
  await this.ensureConnected();
43920
44457
  this.throwIfPendingPair();
43921
- try {
43922
- const result = await this._captureRedirectOnce(opts);
43923
- this.recordSuccess();
43924
- return result;
43925
- } catch (err) {
43926
- const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
43927
- if (!swDown) {
43928
- this.recordFailure(`capture_redirect: ${err.message ?? String(err)}`);
43929
- throw err;
43930
- }
43931
- this.lastEvictionDetectedAt = Date.now();
43932
- const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
43933
- if (reviveMs > 0) {
43934
- this.lazyReviveAttempts += 1;
43935
- await new Promise((r) => setTimeout(r, reviveMs));
43936
- try {
43937
- const result = await this._captureRedirectOnce(opts);
43938
- this.lazyReviveSuccesses += 1;
43939
- this.recordSuccess();
43940
- return result;
43941
- } catch (retryErr) {
43942
- const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
43943
- if (!stillDown) {
43944
- this.recordFailure(`capture_redirect: ${retryErr.message ?? String(retryErr)}`);
43945
- throw retryErr;
43946
- }
43947
- this.recordFailure(`capture_redirect bridge-down: ${retryErr.message}`);
43948
- throw new FetchproxyBridgeDownError({
43949
- originalError: retryErr.message,
43950
- retryAttempted: true,
43951
- op: "capture_redirect",
43952
- url: `https://${opts.host}${opts.path ?? "/*"}`,
43953
- role: this.role,
43954
- port: this.opts.port
43955
- });
43956
- }
43957
- }
43958
- this.recordFailure(`capture_redirect bridge-down: ${err.message}`);
43959
- throw new FetchproxyBridgeDownError({
43960
- originalError: err.message,
43961
- retryAttempted: false,
43962
- op: "capture_redirect",
43963
- url: `https://${opts.host}${opts.path ?? "/*"}`,
43964
- role: this.role,
43965
- port: this.opts.port
43966
- });
43967
- }
44458
+ return this.withLazyRevive("capture_redirect", `https://${opts.host}${opts.path ?? "/*"}`, () => this._captureRedirectOnce(opts));
43968
44459
  }
43969
44460
  async _captureRedirectOnce(opts) {
43970
44461
  const id = this.nextRequestId++;
@@ -44009,52 +44500,57 @@ var init_ws_server = __esm({
44009
44500
  assertUrlInDomains("download url", opts.url, this.opts.domains);
44010
44501
  await this.ensureConnected();
44011
44502
  this.throwIfPendingPair();
44503
+ return this.withLazyRevive("download", opts.url, () => this._downloadOnce(opts));
44504
+ }
44505
+ /**
44506
+ * B-QUAL-1: the lazy-revive policy for the throwing verbs
44507
+ * (`capture_request_header`, `capture_redirect`, `download`), in one place.
44508
+ * Runs `once`; on the SW-eviction symptom (`content_script_unreachable` —
44509
+ * the request provably never reached a tab, so re-sending is safe) stamps
44510
+ * the eviction counter, waits `bridgeReviveDelayMs` and retries once, then
44511
+ * surfaces a `FetchproxyBridgeDownError`. Every other error is recorded and
44512
+ * rethrown untouched. `fetch()` keeps its own envelope-shaped variant.
44513
+ */
44514
+ async withLazyRevive(op, url2, once) {
44515
+ const isSwDown = (e) => e instanceof FetchproxyProtocolError && classifyFetchError(e.message) === "content_script_unreachable";
44516
+ const bridgeDown = (e, retryAttempted) => {
44517
+ this.recordFailure(`${op} bridge-down: ${e.message}`);
44518
+ return new FetchproxyBridgeDownError({
44519
+ originalError: e.message,
44520
+ retryAttempted,
44521
+ op,
44522
+ url: url2,
44523
+ role: this.role,
44524
+ port: this.opts.port
44525
+ });
44526
+ };
44012
44527
  try {
44013
- const result = await this._downloadOnce(opts);
44528
+ const result = await once();
44014
44529
  this.recordSuccess();
44015
44530
  return result;
44016
44531
  } catch (err) {
44017
- const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
44018
- if (!swDown) {
44019
- this.recordFailure(`download: ${err.message ?? String(err)}`);
44532
+ if (!isSwDown(err)) {
44533
+ this.recordFailure(`${op}: ${err.message ?? String(err)}`);
44020
44534
  throw err;
44021
44535
  }
44022
44536
  this.lastEvictionDetectedAt = Date.now();
44023
44537
  const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
44024
- if (reviveMs > 0) {
44025
- this.lazyReviveAttempts += 1;
44026
- await new Promise((r) => setTimeout(r, reviveMs));
44027
- try {
44028
- const result = await this._downloadOnce(opts);
44029
- this.lazyReviveSuccesses += 1;
44030
- this.recordSuccess();
44031
- return result;
44032
- } catch (retryErr) {
44033
- const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
44034
- if (!stillDown) {
44035
- this.recordFailure(`download: ${retryErr.message ?? String(retryErr)}`);
44036
- throw retryErr;
44037
- }
44038
- this.recordFailure(`download bridge-down: ${retryErr.message}`);
44039
- throw new FetchproxyBridgeDownError({
44040
- originalError: retryErr.message,
44041
- retryAttempted: true,
44042
- op: "download",
44043
- url: opts.url,
44044
- role: this.role,
44045
- port: this.opts.port
44046
- });
44538
+ if (reviveMs <= 0)
44539
+ throw bridgeDown(err, false);
44540
+ this.lazyReviveAttempts += 1;
44541
+ await new Promise((r) => setTimeout(r, reviveMs));
44542
+ try {
44543
+ const result = await once();
44544
+ this.lazyReviveSuccesses += 1;
44545
+ this.recordSuccess();
44546
+ return result;
44547
+ } catch (retryErr) {
44548
+ if (!isSwDown(retryErr)) {
44549
+ this.recordFailure(`${op}: ${retryErr.message ?? String(retryErr)}`);
44550
+ throw retryErr;
44047
44551
  }
44552
+ throw bridgeDown(retryErr, true);
44048
44553
  }
44049
- this.recordFailure(`download bridge-down: ${err.message}`);
44050
- throw new FetchproxyBridgeDownError({
44051
- originalError: err.message,
44052
- retryAttempted: false,
44053
- op: "download",
44054
- url: opts.url,
44055
- role: this.role,
44056
- port: this.opts.port
44057
- });
44058
44554
  }
44059
44555
  }
44060
44556
  async _downloadOnce(opts) {
@@ -44083,7 +44579,9 @@ var init_ws_server = __esm({
44083
44579
  // CAP and need the identical explanation; missing it then was the point
44084
44580
  // of #279 — "both verbs" was counted off the two call sites in view
44085
44581
  // rather than off the type. Three is still the number.
44086
- opts.timeoutMs
44582
+ opts.timeoutMs,
44583
+ // A second download saves a duplicate "file (1)".
44584
+ false
44087
44585
  );
44088
44586
  }
44089
44587
  /**
@@ -44174,6 +44672,52 @@ var init_ws_server = __esm({
44174
44672
  await this.sendInnerFrame(inner);
44175
44673
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
44176
44674
  }
44675
+ /**
44676
+ * 3.1.0+: read a declared REPEATED DOM structure from the user's
44677
+ * signed-in tab. Requires `'read_dom_list'` in capabilities AND `name` to
44678
+ * match a declared `domListSelectors` entry. The extension resolves the
44679
+ * entry's `itemSelector` via `document.querySelectorAll` in the matched
44680
+ * tab's DOM (isolated world), then resolves each declared field against
44681
+ * every matched item in turn — no page-JS execution, same as `readDom`.
44682
+ *
44683
+ * Returns one row per matched item, in document order, truncated to the
44684
+ * entry's declared `maxItems` when set. A field absent on a given item is
44685
+ * omitted from that item's row (the row itself is still returned). Throws
44686
+ * `FetchproxyProtocolError` on bridge failures and a plain `Error` on
44687
+ * developer mistakes (undeclared capability, undeclared name).
44688
+ *
44689
+ * Reads only what is CURRENTLY rendered — a virtualized or lazily-loaded
44690
+ * list returns only its visible rows. A caller that needs more scrolls
44691
+ * the list (out of band) and calls again.
44692
+ */
44693
+ async readDomList(opts) {
44694
+ if (!this.opts.capabilities.includes("read_dom_list")) {
44695
+ throw new Error('FetchproxyServer.readDomList(): MCP did not declare "read_dom_list" in capabilities');
44696
+ }
44697
+ await this.ensureConnected();
44698
+ this.throwIfPendingPair();
44699
+ if (typeof opts.name !== "string" || opts.name.length === 0) {
44700
+ throw new Error("FetchproxyServer.readDomList: opts.name must be a non-empty string");
44701
+ }
44702
+ this.assertScopeSubset([opts.name], this.opts.domListSelectors.map((d) => d.name), "domListSelectors");
44703
+ if (opts.subdomain !== void 0)
44704
+ assertSubdomainLabel(opts.subdomain);
44705
+ const baseDomain = this.resolveBaseDomain(opts.domain);
44706
+ const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
44707
+ const origin = `https://${host}`;
44708
+ const id = this.nextRequestId++;
44709
+ const inner = {
44710
+ type: "request",
44711
+ id,
44712
+ op: "read_dom_list",
44713
+ init: { origin, name: opts.name }
44714
+ };
44715
+ const pending = this.guardPending(new Promise((resolve2, reject) => {
44716
+ this.pendingDomList.set(id, { resolve: resolve2, reject });
44717
+ }));
44718
+ await this.sendInnerFrame(inner);
44719
+ return this._withVerbTimeout(pending, this.pendingDomList, id, origin);
44720
+ }
44177
44721
  /**
44178
44722
  * 1.x+: run a declared GraphQL operation through the page's own Apollo
44179
44723
  * client (`window.__APOLLO_CLIENT__`) in the signed-in tab's MAIN world.
@@ -44217,8 +44761,9 @@ var init_ws_server = __esm({
44217
44761
  const pending = this.guardPending(new Promise((resolve2, reject) => {
44218
44762
  this.pendingGraphql.set(id, { resolve: resolve2, reject });
44219
44763
  }));
44220
- await this.sendInnerFrame(inner);
44221
- return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
44764
+ const retrySafe = opts.retryOnTimeout === true;
44765
+ await this.sendInnerFrame(inner, { resendable: retrySafe });
44766
+ return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name, void 0, retrySafe);
44222
44767
  }
44223
44768
  assertScopeSubset(requested, declared, label) {
44224
44769
  const undeclared = undeclaredKeys(requested, declared);
@@ -44255,6 +44800,7 @@ var init_ws_server = __esm({
44255
44800
  onInner(inner) {
44256
44801
  if (inner.type !== "response")
44257
44802
  return;
44803
+ this.resendable.delete(inner.id);
44258
44804
  this.lastExtensionMessageAt = Date.now();
44259
44805
  const fetchCb = this.pending.get(inner.id);
44260
44806
  if (fetchCb) {
@@ -44343,6 +44889,20 @@ var init_ws_server = __esm({
44343
44889
  }
44344
44890
  return;
44345
44891
  }
44892
+ const domListCb = this.pendingDomList.get(inner.id);
44893
+ if (domListCb) {
44894
+ this.pendingDomList.delete(inner.id);
44895
+ if (inner.ok) {
44896
+ if (inner.op === "read_dom_list" && inner.rows) {
44897
+ domListCb.resolve(inner.rows.map((row) => ({ ...row })));
44898
+ } else {
44899
+ domListCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_dom_list awaiter`));
44900
+ }
44901
+ } else {
44902
+ domListCb.reject(protocolErrorFrom(inner.error));
44903
+ }
44904
+ return;
44905
+ }
44346
44906
  const downloadCb = this.pendingDownload.get(inner.id);
44347
44907
  if (downloadCb) {
44348
44908
  this.pendingDownload.delete(inner.id);
@@ -44402,42 +44962,133 @@ var init_ws_server = __esm({
44402
44962
  }
44403
44963
  }
44404
44964
  }
44405
- rejectAllPending(reason = "extension disconnected") {
44965
+ rejectAllPending(reason = "extension disconnected", keep = /* @__PURE__ */ new Set()) {
44966
+ for (const id of this.allPendingIds()) {
44967
+ if (!keep.has(id))
44968
+ this.rejectPendingId(id, reason);
44969
+ }
44970
+ if (keep.size === 0)
44971
+ this.resendable.clear();
44972
+ }
44973
+ /** Every request id with a live awaiter, across all op maps. */
44974
+ allPendingIds() {
44975
+ const ids = [];
44976
+ for (const map2 of this.pendingMaps())
44977
+ for (const id of map2.keys())
44978
+ ids.push(id);
44979
+ return ids;
44980
+ }
44981
+ pendingMaps() {
44982
+ return [
44983
+ this.pending,
44984
+ this.pendingReadCookies,
44985
+ this.pendingStorage,
44986
+ this.pendingWriteCookies,
44987
+ this.pendingCapture,
44988
+ this.pendingRedirect,
44989
+ this.pendingIdb,
44990
+ this.pendingDomList,
44991
+ this.pendingDownload,
44992
+ this.pendingGraphql
44993
+ ];
44994
+ }
44995
+ isPendingId(id) {
44996
+ return this.pendingMaps().some((m) => m.has(id));
44997
+ }
44998
+ /** Fail the one awaiter registered under `id`, in its own map's shape. */
44999
+ rejectPendingId(id, reason) {
44406
45000
  const err = new FetchproxyProtocolError(reason);
44407
- for (const cb of this.pending.values()) {
44408
- cb({
45001
+ this.resendable.delete(id);
45002
+ const fetchCb = this.pending.get(id);
45003
+ if (fetchCb) {
45004
+ this.pending.delete(id);
45005
+ fetchCb({
44409
45006
  ok: false,
44410
45007
  error: err.message,
44411
45008
  kind: classifyFetchError(err.message),
44412
45009
  retryAttempted: false
44413
45010
  });
45011
+ return;
45012
+ }
45013
+ const cookieCb = this.pendingReadCookies.get(id);
45014
+ if (cookieCb) {
45015
+ this.pendingReadCookies.delete(id);
45016
+ cookieCb({ ok: false, error: err.message });
45017
+ return;
45018
+ }
45019
+ for (const map2 of [
45020
+ this.pendingStorage,
45021
+ this.pendingWriteCookies,
45022
+ this.pendingCapture,
45023
+ this.pendingRedirect,
45024
+ this.pendingIdb,
45025
+ this.pendingDomList,
45026
+ this.pendingDownload,
45027
+ this.pendingGraphql
45028
+ ]) {
45029
+ const entry = map2.get(id);
45030
+ if (entry) {
45031
+ map2.delete(id);
45032
+ entry.reject(err);
45033
+ return;
45034
+ }
44414
45035
  }
44415
- this.pending.clear();
44416
- for (const cb of this.pendingReadCookies.values()) {
44417
- cb({ ok: false, error: err.message });
44418
- }
44419
- this.pendingReadCookies.clear();
44420
- for (const { reject } of this.pendingStorage.values())
44421
- reject(err);
44422
- this.pendingStorage.clear();
44423
- for (const { reject } of this.pendingWriteCookies.values())
44424
- reject(err);
44425
- this.pendingWriteCookies.clear();
44426
- for (const { reject } of this.pendingCapture.values())
44427
- reject(err);
44428
- this.pendingCapture.clear();
44429
- for (const { reject } of this.pendingRedirect.values())
44430
- reject(err);
44431
- this.pendingRedirect.clear();
44432
- for (const { reject } of this.pendingIdb.values())
44433
- reject(err);
44434
- this.pendingIdb.clear();
44435
- for (const { reject } of this.pendingDownload.values())
44436
- reject(err);
44437
- this.pendingDownload.clear();
44438
- for (const { reject } of this.pendingGraphql.values())
44439
- reject(err);
44440
- this.pendingGraphql.clear();
45036
+ }
45037
+ noteResendable(inner) {
45038
+ this.resendable.set(inner.id, inner);
45039
+ if (this.resendable.size > 256) {
45040
+ for (const id of [...this.resendable.keys()]) {
45041
+ if (!this.isPendingId(id))
45042
+ this.resendable.delete(id);
45043
+ }
45044
+ }
45045
+ }
45046
+ /**
45047
+ * B-BUG-6: the host this peer relayed through has gone — most often a
45048
+ * short-lived process (a bootstrap lift, an `fpx` call) that won the
45049
+ * election and then closed. Fail only the in-flight requests that may
45050
+ * already have run (a non-idempotent fetch, a cookie write, a download);
45051
+ * re-elect and send the rest again. The replies to the originals died with
45052
+ * the old host, and each re-sent request keeps its awaiter and its
45053
+ * original deadline.
45054
+ */
45055
+ async recoverFromHostLoss() {
45056
+ const toResend = /* @__PURE__ */ new Map();
45057
+ for (const [id, inner] of this.resendable) {
45058
+ if (this.isPendingId(id))
45059
+ toResend.set(id, inner);
45060
+ }
45061
+ this.resendable.clear();
45062
+ this.rejectAllPending("fetchproxy: the MCP process holding the bridge port exited while this request was in flight. It was not re-sent because it may already have run in the browser \u2014 check before retrying.", new Set(toResend.keys()));
45063
+ if (toResend.size === 0)
45064
+ return;
45065
+ const lost = (id, e) => this.rejectPendingId(id, `fetchproxy: the bridge host exited and reconnecting failed: ${e instanceof Error ? e.message : String(e)}`);
45066
+ try {
45067
+ await this.ensureConnected();
45068
+ } catch (e) {
45069
+ for (const id of toResend.keys())
45070
+ lost(id, e);
45071
+ return;
45072
+ }
45073
+ for (const [id, inner] of toResend) {
45074
+ if (!this.isPendingId(id))
45075
+ continue;
45076
+ try {
45077
+ await this.resendInnerFrame(inner);
45078
+ } catch (e) {
45079
+ lost(id, e);
45080
+ }
45081
+ }
45082
+ }
45083
+ /** Send an already-registered request again, keeping its awaiter on failure. */
45084
+ async resendInnerFrame(inner) {
45085
+ if (this.hostHandle)
45086
+ await this.hostHandle.sendOwnInner(inner);
45087
+ else if (this.peerHandle)
45088
+ await this.peerHandle.sendInner(inner);
45089
+ else
45090
+ throw new Error("no bridge handle after re-election");
45091
+ this.noteResendable(inner);
44441
45092
  }
44442
45093
  /**
44443
45094
  * 0.5.2+: read the current pair-pending pair code from whichever handle
@@ -44802,7 +45453,8 @@ function createFetchproxyTransport(opts) {
44802
45453
  ...init.headers !== void 0 ? { headers: init.headers } : {},
44803
45454
  ...init.body !== void 0 ? { body: init.body } : {},
44804
45455
  ...init.subdomain !== void 0 ? { subdomain: init.subdomain } : {},
44805
- ...init.domain !== void 0 ? { domain: init.domain } : {}
45456
+ ...init.domain !== void 0 ? { domain: init.domain } : {},
45457
+ ...init.retryOnTimeout !== void 0 ? { retryOnTimeout: init.retryOnTimeout } : {}
44806
45458
  }));
44807
45459
  return { status: response.status, body: response.body, url: response.url };
44808
45460
  },
@@ -44811,7 +45463,8 @@ function createFetchproxyTransport(opts) {
44811
45463
  ...init.headers !== void 0 ? { headers: init.headers } : {},
44812
45464
  ...init.body !== void 0 ? { body: init.body } : {},
44813
45465
  ...init.subdomain !== void 0 ? { subdomain: init.subdomain } : {},
44814
- ...init.domain !== void 0 ? { domain: init.domain } : {}
45466
+ ...init.domain !== void 0 ? { domain: init.domain } : {},
45467
+ ...init.retryOnTimeout !== void 0 ? { retryOnTimeout: init.retryOnTimeout } : {}
44815
45468
  }));
44816
45469
  return {
44817
45470
  data,
@@ -45136,7 +45789,39 @@ function defaultIsRefreshRevoked(err) {
45136
45789
  return false;
45137
45790
  if (err instanceof ApiError && err.status >= 500)
45138
45791
  return false;
45139
- return true;
45792
+ return !isTransientFailure(err);
45793
+ }
45794
+ function isPersistenceFailure(err) {
45795
+ return err !== null && typeof err === "object" && err.persistenceFailure === true;
45796
+ }
45797
+ var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
45798
+ "ECONNRESET",
45799
+ "ECONNREFUSED",
45800
+ "ECONNABORTED",
45801
+ "ETIMEDOUT",
45802
+ "ENOTFOUND",
45803
+ "EAI_AGAIN",
45804
+ "ENETUNREACH",
45805
+ "ENETDOWN",
45806
+ "EHOSTUNREACH",
45807
+ "EPIPE"
45808
+ ]);
45809
+ function isTransientFailure(err) {
45810
+ let cur = err;
45811
+ for (let depth = 0; depth < 4 && cur !== null && typeof cur === "object"; depth++) {
45812
+ const e = cur;
45813
+ if (typeof e.status === "number" && (e.status >= 500 || e.status === 429 || e.status === 408))
45814
+ return true;
45815
+ if (e.name === "AbortError" || e.name === "TimeoutError")
45816
+ return true;
45817
+ if (typeof e.code === "string" && (TRANSIENT_NETWORK_CODES.has(e.code) || e.code.startsWith("UND_ERR_"))) {
45818
+ return true;
45819
+ }
45820
+ if (e.name === "TypeError" && e.message === "fetch failed")
45821
+ return true;
45822
+ cur = e.cause;
45823
+ }
45824
+ return false;
45140
45825
  }
45141
45826
  function isBearerTokens(raw) {
45142
45827
  if (raw === null || typeof raw !== "object")
@@ -45303,6 +45988,8 @@ var TokenManager = class {
45303
45988
  async reBootstrap(err) {
45304
45989
  if (err instanceof StatePersistenceError)
45305
45990
  throw err.cause;
45991
+ if (isPersistenceFailure(err))
45992
+ throw err;
45306
45993
  if (this.bootstrapFn === void 0)
45307
45994
  throw err;
45308
45995
  if (!this.isRefreshRevokedFn(err))
@@ -45318,6 +46005,10 @@ var TokenManager = class {
45318
46005
  try {
45319
46006
  await this.refreshNow();
45320
46007
  } catch (err) {
46008
+ const held = this.tokens;
46009
+ if (!(err instanceof StatePersistenceError) && held !== void 0 && this.now() < held.expiresAt && !this.isRefreshRevokedFn(err)) {
46010
+ return held.accessToken;
46011
+ }
45321
46012
  return (await this.reBootstrap(err)).accessToken;
45322
46013
  }
45323
46014
  tokens = this.tokens ?? tokens;
@@ -45532,17 +46223,33 @@ var OutlookClient = class {
45532
46223
  headers: prefer.length ? { Prefer: prefer.join(", ") } : void 0
45533
46224
  });
45534
46225
  }
45535
- /** GET an absolute URL — used only to follow `@odata.nextLink`. */
45536
- async getAbsolute(url2) {
46226
+ /**
46227
+ * GET an absolute URL — used only to follow `@odata.nextLink`.
46228
+ *
46229
+ * The link is absolute and already carries the base path
46230
+ * (`https://outlook.office.com/api/v2.0/me/messages?$skip=25`), while the
46231
+ * underlying client appends every path to that same base. So the base path is
46232
+ * stripped here, and a link that is not under it — another origin, or another
46233
+ * path on the same host — is refused rather than sent the bearer token.
46234
+ */
46235
+ async getAbsolute(url2, opts = {}) {
45537
46236
  this.#requireConfigured();
45538
46237
  const base = new URL(this.#baseUrl);
45539
- const target = new URL(url2);
45540
- if (target.origin !== base.origin) {
45541
- throw new McpToolError(`Refusing to follow a link to ${target.origin}.`, {
45542
- hint: "Pagination links must stay on the Outlook API host."
46238
+ let target;
46239
+ try {
46240
+ target = new URL(url2);
46241
+ } catch {
46242
+ throw new McpToolError(`Refusing to follow a link that is not a URL: ${url2}`, {
46243
+ hint: "Pass the nextLink value exactly as a previous listing returned it."
46244
+ });
46245
+ }
46246
+ const prefix = base.pathname.replace(/\/+$/, "");
46247
+ if (target.origin !== base.origin || !(target.pathname === prefix || target.pathname.startsWith(`${prefix}/`))) {
46248
+ throw new McpToolError(`Refusing to follow a link to ${target.origin}${target.pathname}.`, {
46249
+ hint: `Pagination links must stay under the Outlook API base (${this.#baseUrl}).`
45543
46250
  });
45544
46251
  }
45545
- return this.#client().fetchJson("GET", target.pathname + target.search);
46252
+ return this.get(target.pathname.slice(prefix.length) + target.search, opts);
45546
46253
  }
45547
46254
  /**
45548
46255
  * The single write path. Everything mutating goes through here so auth and
@@ -45682,6 +46389,22 @@ function projectCollection(payload, project, label) {
45682
46389
  });
45683
46390
  }
45684
46391
 
46392
+ // src/tools/_paging.ts
46393
+ init_zod();
46394
+ var nextLinkParam = external_exports.string().min(1).optional().describe(
46395
+ "Continue a previous listing: pass its `nextLink` value unchanged to fetch the next page. When set, the paging/filter arguments are taken from the link and ignored here."
46396
+ );
46397
+ function fetchPage(client2, nextLink, path, opts = {}) {
46398
+ return nextLink !== void 0 ? client2.getAbsolute(nextLink, opts) : client2.get(path, opts);
46399
+ }
46400
+ function plainCollection(data) {
46401
+ const items = Array.isArray(data?.value) ? data.value : [];
46402
+ const out = { count: items.length, items: items.map(stripOData) };
46403
+ const next = data?.["@odata.nextLink"];
46404
+ if (next) out.nextLink = next;
46405
+ return out;
46406
+ }
46407
+
45685
46408
  // src/tools/mail.ts
45686
46409
  var VIEWS2 = ["compact", "full", "raw"];
45687
46410
  var WELL_KNOWN = [
@@ -45714,11 +46437,14 @@ function registerMailTools(server, client2) {
45714
46437
  annotations: { readOnlyHint: true },
45715
46438
  inputSchema: external_exports.object({
45716
46439
  view: viewParam(VIEWS2),
45717
- limit: external_exports.number().int().min(1).max(200).optional().describe("Max folders (default 50)")
46440
+ limit: external_exports.number().int().min(1).max(200).optional().describe("Max folders (default 50)"),
46441
+ nextLink: nextLinkParam
45718
46442
  })
45719
46443
  },
45720
- async ({ view, limit }) => {
45721
- const data = await client2.get(
46444
+ async ({ view, limit, nextLink }) => {
46445
+ const data = await fetchPage(
46446
+ client2,
46447
+ nextLink,
45722
46448
  `/me/mailfolders${qs({ $top: limit ?? 50 })}`
45723
46449
  );
45724
46450
  if (resolveView(view, VIEWS2) === "raw") return minifiedResult(data);
@@ -45728,7 +46454,7 @@ function registerMailTools(server, client2) {
45728
46454
  server.registerTool(
45729
46455
  "outlook_list_messages",
45730
46456
  {
45731
- description: "List messages in a folder, newest first. Defaults to the inbox. Use `unreadOnly` for a triage view. Bodies are NOT included \u2014 call outlook_get_message for one. Note `search` and `unreadOnly` cannot be combined (the API rejects $search with $filter); pass `search` alone to search the whole mailbox.",
46457
+ description: "List messages in a folder, newest first. Defaults to the inbox. Use `unreadOnly` for a triage view. Bodies are NOT included \u2014 call outlook_get_message for one. Note `search` and `unreadOnly` cannot be combined on a first-page request (the API rejects $search with $filter); pass `search` alone to search the whole mailbox. When following a `nextLink`, both are ignored, so echoing them back is harmless.",
45732
46458
  annotations: { readOnlyHint: true },
45733
46459
  inputSchema: external_exports.object({
45734
46460
  view: viewParam(VIEWS2),
@@ -45736,11 +46462,12 @@ function registerMailTools(server, client2) {
45736
46462
  limit: external_exports.number().int().min(1).max(100).optional().describe("Max messages (default 25)"),
45737
46463
  skip: external_exports.number().int().min(0).optional().describe("Offset for paging"),
45738
46464
  unreadOnly: external_exports.boolean().optional().describe("Only unread messages"),
45739
- search: external_exports.string().optional().describe("Full-text search across the whole mailbox; cannot combine with unreadOnly")
46465
+ search: external_exports.string().optional().describe("Full-text search across the whole mailbox; cannot combine with unreadOnly on a first page (ignored with nextLink)"),
46466
+ nextLink: nextLinkParam
45740
46467
  })
45741
46468
  },
45742
- async ({ view, folder, limit, skip, unreadOnly, search }) => {
45743
- if (search !== void 0 && unreadOnly === true) {
46469
+ async ({ view, folder, limit, skip, unreadOnly, search, nextLink }) => {
46470
+ if (nextLink === void 0 && search !== void 0 && unreadOnly === true) {
45744
46471
  throw new McpToolError("`search` and `unreadOnly` cannot be combined.", {
45745
46472
  hint: "Outlook rejects $search together with $filter. Search first, then filter the results."
45746
46473
  });
@@ -45757,7 +46484,11 @@ function registerMailTools(server, client2) {
45757
46484
  params.$orderby = "ReceivedDateTime desc";
45758
46485
  if (unreadOnly === true) params.$filter = "IsRead eq false";
45759
46486
  }
45760
- const data = await client2.get(`${base}${qs(params)}`);
46487
+ const data = await fetchPage(
46488
+ client2,
46489
+ nextLink,
46490
+ `${base}${qs(params)}`
46491
+ );
45761
46492
  const v = resolveView(view, VIEWS2);
45762
46493
  if (v === "raw") return minifiedResult(data);
45763
46494
  return minifiedResult(
@@ -45791,19 +46522,19 @@ function registerMailTools(server, client2) {
45791
46522
  description: "List a message's attachments (name, size, content type). Metadata only \u2014 this deliberately does not download bytes.",
45792
46523
  annotations: { readOnlyHint: true },
45793
46524
  inputSchema: external_exports.object({
45794
- id: external_exports.string().min(1).describe("Message Id")
46525
+ id: external_exports.string().min(1).describe("Message Id"),
46526
+ nextLink: nextLinkParam
45795
46527
  })
45796
46528
  },
45797
- async ({ id }) => {
45798
- const data = await client2.get(
46529
+ async ({ id, nextLink }) => {
46530
+ const data = await fetchPage(
46531
+ client2,
46532
+ nextLink,
45799
46533
  `/me/messages/${encodeURIComponent(id)}/attachments${qs({
45800
46534
  $select: "Id,Name,Size,ContentType"
45801
46535
  })}`
45802
46536
  );
45803
- return minifiedResult({
45804
- count: data.value?.length ?? 0,
45805
- items: (data.value ?? []).map(stripOData)
45806
- });
46537
+ return minifiedResult(plainCollection(data));
45807
46538
  }
45808
46539
  );
45809
46540
  }
@@ -45850,12 +46581,15 @@ function registerCalendarTools(server, client2) {
45850
46581
  timeZone: external_exports.string().optional().describe(
45851
46582
  'Windows time-zone name for the returned times, e.g. "Eastern Standard Time". NOT an IANA name like America/New_York.'
45852
46583
  ),
45853
- limit: external_exports.number().int().min(1).max(200).optional().describe("Max events (default 50)")
46584
+ limit: external_exports.number().int().min(1).max(200).optional().describe("Max events (default 50)"),
46585
+ nextLink: nextLinkParam
45854
46586
  })
45855
46587
  },
45856
- async ({ view, start, end, timeZone, limit }) => {
46588
+ async ({ view, start, end, timeZone, limit, nextLink }) => {
45857
46589
  const zone = timeZone ?? await mailboxTimeZone(client2);
45858
- const data = await client2.get(
46590
+ const data = await fetchPage(
46591
+ client2,
46592
+ nextLink,
45859
46593
  `/me/calendarview${qs2({
45860
46594
  startDateTime: start,
45861
46595
  endDateTime: end,
@@ -45900,18 +46634,14 @@ function registerCalendarTools(server, client2) {
45900
46634
  description: "List the calendars in the mailbox, for picking one to read or write against.",
45901
46635
  annotations: { readOnlyHint: true },
45902
46636
  inputSchema: external_exports.object({
45903
- view: viewParam(VIEWS2)
46637
+ view: viewParam(VIEWS2),
46638
+ nextLink: nextLinkParam
45904
46639
  })
45905
46640
  },
45906
- async ({ view }) => {
45907
- const data = await client2.get(
45908
- `/me/calendars${qs2({ $select: "Id,Name,Color,CanEdit,Owner" })}`
45909
- );
46641
+ async ({ view, nextLink }) => {
46642
+ const data = await fetchPage(client2, nextLink, `/me/calendars${qs2({ $select: "Id,Name,Color,CanEdit,Owner" })}`);
45910
46643
  if (resolveView(view, VIEWS2) === "raw") return minifiedResult(data);
45911
- return minifiedResult({
45912
- count: data.value?.length ?? 0,
45913
- items: (data.value ?? []).map(stripOData)
45914
- });
46644
+ return minifiedResult(plainCollection(data));
45915
46645
  }
45916
46646
  );
45917
46647
  }
@@ -45929,7 +46659,7 @@ function qs3(params) {
45929
46659
  }
45930
46660
  function collection(data, raw) {
45931
46661
  if (raw) return data;
45932
- return { count: data.value?.length ?? 0, items: (data.value ?? []).map(stripOData) };
46662
+ return plainCollection(data);
45933
46663
  }
45934
46664
  function registerDirectoryTools(server, client2) {
45935
46665
  server.registerTool(
@@ -45958,11 +46688,14 @@ function registerDirectoryTools(server, client2) {
45958
46688
  inputSchema: external_exports.object({
45959
46689
  view: viewParam(VIEWS2),
45960
46690
  limit: external_exports.number().int().min(1).max(200).optional().describe("Max contacts (default 50)"),
45961
- skip: external_exports.number().int().min(0).optional().describe("Offset for paging")
46691
+ skip: external_exports.number().int().min(0).optional().describe("Offset for paging"),
46692
+ nextLink: nextLinkParam
45962
46693
  })
45963
46694
  },
45964
- async ({ view, limit, skip }) => {
45965
- const data = await client2.get(
46695
+ async ({ view, limit, skip, nextLink }) => {
46696
+ const data = await fetchPage(
46697
+ client2,
46698
+ nextLink,
45966
46699
  `/me/contacts${qs3({
45967
46700
  $top: limit ?? 50,
45968
46701
  $skip: skip,
@@ -45981,11 +46714,14 @@ function registerDirectoryTools(server, client2) {
45981
46714
  annotations: { readOnlyHint: true },
45982
46715
  inputSchema: external_exports.object({
45983
46716
  view: viewParam(VIEWS2),
45984
- limit: external_exports.number().int().min(1).max(100).optional().describe("Max people (default 25)")
46717
+ limit: external_exports.number().int().min(1).max(100).optional().describe("Max people (default 25)"),
46718
+ nextLink: nextLinkParam
45985
46719
  })
45986
46720
  },
45987
- async ({ view, limit }) => {
45988
- const data = await client2.get(
46721
+ async ({ view, limit, nextLink }) => {
46722
+ const data = await fetchPage(
46723
+ client2,
46724
+ nextLink,
45989
46725
  `/me/people${qs3({
45990
46726
  $top: limit ?? 25,
45991
46727
  $select: "Id,DisplayName,ScoredEmailAddresses,JobTitle,CompanyName"
@@ -46001,11 +46737,14 @@ function registerDirectoryTools(server, client2) {
46001
46737
  annotations: { readOnlyHint: true },
46002
46738
  inputSchema: external_exports.object({
46003
46739
  view: viewParam(VIEWS2),
46004
- limit: external_exports.number().int().min(1).max(200).optional().describe("Max tasks (default 50)")
46740
+ limit: external_exports.number().int().min(1).max(200).optional().describe("Max tasks (default 50)"),
46741
+ nextLink: nextLinkParam
46005
46742
  })
46006
46743
  },
46007
- async ({ view, limit }) => {
46008
- const data = await client2.get(
46744
+ async ({ view, limit, nextLink }) => {
46745
+ const data = await fetchPage(
46746
+ client2,
46747
+ nextLink,
46009
46748
  `/me/tasks${qs3({
46010
46749
  $top: limit ?? 50,
46011
46750
  $select: "Id,Subject,Status,Importance,DueDateTime,CompletedDateTime"