canvas-parent-mcp 2.1.1 → 2.1.3

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
@@ -4858,7 +4858,7 @@ var require_memoizer = __commonJS({
4858
4858
  var NONE2 = 0;
4859
4859
  var ASSUMED2 = 1;
4860
4860
  var PROVEN2 = 2;
4861
- function isRecursive2(inst, stack, resolve2) {
4861
+ function isRecursive2(inst, stack, resolve5) {
4862
4862
  const cached2 = recursive2.get(inst);
4863
4863
  if (cached2 !== void 0)
4864
4864
  return cached2 ? PROVEN2 : NONE2;
@@ -4868,7 +4868,7 @@ var require_memoizer = __commonJS({
4868
4868
  let result = NONE2;
4869
4869
  const check2 = (child) => {
4870
4870
  if (result !== PROVEN2 && child?._zod) {
4871
- const answer = isRecursive2(child, stack, resolve2);
4871
+ const answer = isRecursive2(child, stack, resolve5);
4872
4872
  if (answer > result)
4873
4873
  result = answer;
4874
4874
  }
@@ -4879,7 +4879,7 @@ var require_memoizer = __commonJS({
4879
4879
  const desc = Object.getOwnPropertyDescriptor(sh, key);
4880
4880
  if (spread && !desc.enumerable)
4881
4881
  continue;
4882
- const child = desc.get ? ASSUMED2 : desc.value?._zod ? isRecursive2(desc.value, stack, resolve2) : NONE2;
4882
+ const child = desc.get ? ASSUMED2 : desc.value?._zod ? isRecursive2(desc.value, stack, resolve5) : NONE2;
4883
4883
  if (child > answer)
4884
4884
  answer = child;
4885
4885
  }
@@ -4943,7 +4943,7 @@ var require_memoizer = __commonJS({
4943
4943
  break;
4944
4944
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
4945
4945
  case "lazy": {
4946
- const inner = def._cachedInner ?? (resolve2 ? inst._zod.innerType : void 0);
4946
+ const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
4947
4947
  merge2(inner ? isRecursive2(inner, stack, false) : ASSUMED2);
4948
4948
  break;
4949
4949
  }
@@ -25548,7 +25548,7 @@ var require_websocket = __commonJS({
25548
25548
  var http = __require("http");
25549
25549
  var net = __require("net");
25550
25550
  var tls = __require("tls");
25551
- var { randomBytes: randomBytes3, createHash } = __require("crypto");
25551
+ var { randomBytes: randomBytes4, createHash } = __require("crypto");
25552
25552
  var { Duplex, Readable } = __require("stream");
25553
25553
  var { URL: URL2 } = __require("url");
25554
25554
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -26086,7 +26086,7 @@ var require_websocket = __commonJS({
26086
26086
  }
26087
26087
  }
26088
26088
  const defaultPort = isSecure ? 443 : 80;
26089
- const key = randomBytes3(16).toString("base64");
26089
+ const key = randomBytes4(16).toString("base64");
26090
26090
  const request = isSecure ? https.request : http.request;
26091
26091
  const protocolSet = /* @__PURE__ */ new Set();
26092
26092
  let perMessageDeflate;
@@ -26980,9 +26980,23 @@ var require_websocket_server = __commonJS({
26980
26980
  });
26981
26981
 
26982
26982
  // src/index.ts
26983
- import { dirname as dirname3, join as join4 } from "path";
26983
+ import { dirname as dirname4, join as join6 } from "path";
26984
26984
  import { fileURLToPath } from "url";
26985
26985
 
26986
+ // node_modules/@chrischall/mcp-utils/dist/cancel/index.js
26987
+ import { AsyncLocalStorage } from "node:async_hooks";
26988
+ var storage = new AsyncLocalStorage();
26989
+ function withCallSignal(signal, fn, request) {
26990
+ return signal ? storage.run({ signal, ...request ? { request } : {} }, fn) : fn();
26991
+ }
26992
+
26993
+ // node_modules/@chrischall/mcp-utils/dist/caller/index.js
26994
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
26995
+ var storage2 = new AsyncLocalStorage2();
26996
+ function withCallerCapabilities(capabilities, fn) {
26997
+ return capabilities ? storage2.run(capabilities, fn) : fn();
26998
+ }
26999
+
26986
27000
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
26987
27001
  var __create2 = Object.create;
26988
27002
  var __defProp2 = Object.defineProperty;
@@ -31385,14 +31399,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
31385
31399
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
31386
31400
  }
31387
31401
  function sleep(ms, signal) {
31388
- return new Promise((resolve2, reject) => {
31402
+ return new Promise((resolve5, reject) => {
31389
31403
  if (signal?.aborted) {
31390
31404
  reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
31391
31405
  return;
31392
31406
  }
31393
31407
  const timer = setTimeout(() => {
31394
31408
  signal?.removeEventListener("abort", onAbort);
31395
- resolve2();
31409
+ resolve5();
31396
31410
  }, ms);
31397
31411
  const onAbort = () => {
31398
31412
  clearTimeout(timer);
@@ -32186,7 +32200,7 @@ var Protocol = class {
32186
32200
  const flowStartedAt = Date.now();
32187
32201
  let onAbort;
32188
32202
  let cleanupMessageId;
32189
- return new Promise((resolve2, reject) => {
32203
+ return new Promise((resolve5, reject) => {
32190
32204
  const earlyReject = (error62) => {
32191
32205
  reject(error62);
32192
32206
  };
@@ -32254,7 +32268,7 @@ var Protocol = class {
32254
32268
  }
32255
32269
  if (decoded.kind === "invalid") return reject(decoded.error);
32256
32270
  if (decoded.kind === "input_required") {
32257
- if (options?.allowInputRequired === true) return resolve2(manualInputRequiredValue(decoded));
32271
+ if (options?.allowInputRequired === true) return resolve5(manualInputRequiredValue(decoded));
32258
32272
  const flow = {
32259
32273
  codec: codec2,
32260
32274
  request,
@@ -32266,11 +32280,11 @@ var Protocol = class {
32266
32280
  params
32267
32281
  }, resultSchema, legOptions)
32268
32282
  };
32269
- return resolve2(this._resolveNonCompleteResult(decoded, flow));
32283
+ return resolve5(this._resolveNonCompleteResult(decoded, flow));
32270
32284
  }
32271
32285
  const result = decoded.result;
32272
32286
  validateStandardSchema(resultSchema, result).then((parseResult) => {
32273
- if (parseResult.success) resolve2(parseResult.data);
32287
+ if (parseResult.success) resolve5(parseResult.data);
32274
32288
  else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
32275
32289
  }, reject);
32276
32290
  });
@@ -35182,7 +35196,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin(((exports) => {
35182
35196
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
35183
35197
  const schOrFunc = root.refs[ref];
35184
35198
  if (schOrFunc) return schOrFunc;
35185
- let _sch = resolve2.call(this, root, ref);
35199
+ let _sch = resolve5.call(this, root, ref);
35186
35200
  if (_sch === void 0) {
35187
35201
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
35188
35202
  const { schemaId } = this.opts;
@@ -35208,7 +35222,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin(((exports) => {
35208
35222
  function sameSchemaEnv(s1, s2) {
35209
35223
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
35210
35224
  }
35211
- function resolve2(root, ref) {
35225
+ function resolve5(root, ref) {
35212
35226
  let sch;
35213
35227
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
35214
35228
  return sch || this.schemas[ref] || resolveSchema.call(this, root, ref);
@@ -35658,47 +35672,47 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
35658
35672
  else if (typeof uri === "object") uri = parse3(serialize(uri, options), options);
35659
35673
  return uri;
35660
35674
  }
35661
- function resolve2(baseURI, relativeURI, options) {
35675
+ function resolve5(baseURI, relativeURI, options) {
35662
35676
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
35663
35677
  const resolved2 = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
35664
35678
  schemelessOptions.skipEscape = true;
35665
35679
  return serialize(resolved2, schemelessOptions);
35666
35680
  }
35667
- function resolveComponent(base, relative, options, skipNormalization) {
35681
+ function resolveComponent(base, relative2, options, skipNormalization) {
35668
35682
  const target = {};
35669
35683
  if (!skipNormalization) {
35670
35684
  base = parse3(serialize(base, options), options);
35671
- relative = parse3(serialize(relative, options), options);
35685
+ relative2 = parse3(serialize(relative2, options), options);
35672
35686
  }
35673
35687
  options = options || {};
35674
- if (!options.tolerant && relative.scheme) {
35675
- target.scheme = relative.scheme;
35676
- target.userinfo = relative.userinfo;
35677
- target.host = relative.host;
35678
- target.port = relative.port;
35679
- target.path = removeDotSegments(relative.path || "");
35680
- target.query = relative.query;
35688
+ if (!options.tolerant && relative2.scheme) {
35689
+ target.scheme = relative2.scheme;
35690
+ target.userinfo = relative2.userinfo;
35691
+ target.host = relative2.host;
35692
+ target.port = relative2.port;
35693
+ target.path = removeDotSegments(relative2.path || "");
35694
+ target.query = relative2.query;
35681
35695
  } else {
35682
- if (relative.userinfo !== void 0 || relative.host !== void 0 || relative.port !== void 0) {
35683
- target.userinfo = relative.userinfo;
35684
- target.host = relative.host;
35685
- target.port = relative.port;
35686
- target.path = removeDotSegments(relative.path || "");
35687
- target.query = relative.query;
35696
+ if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
35697
+ target.userinfo = relative2.userinfo;
35698
+ target.host = relative2.host;
35699
+ target.port = relative2.port;
35700
+ target.path = removeDotSegments(relative2.path || "");
35701
+ target.query = relative2.query;
35688
35702
  } else {
35689
- if (!relative.path) {
35703
+ if (!relative2.path) {
35690
35704
  target.path = base.path;
35691
- if (relative.query !== void 0) target.query = relative.query;
35705
+ if (relative2.query !== void 0) target.query = relative2.query;
35692
35706
  else target.query = base.query;
35693
35707
  } else {
35694
- if (relative.path[0] === "/") target.path = removeDotSegments(relative.path);
35708
+ if (relative2.path[0] === "/") target.path = removeDotSegments(relative2.path);
35695
35709
  else {
35696
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative.path;
35697
- else if (!base.path) target.path = relative.path;
35698
- else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
35710
+ if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative2.path;
35711
+ else if (!base.path) target.path = relative2.path;
35712
+ else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
35699
35713
  target.path = removeDotSegments(target.path);
35700
35714
  }
35701
- target.query = relative.query;
35715
+ target.query = relative2.query;
35702
35716
  }
35703
35717
  target.userinfo = base.userinfo;
35704
35718
  target.host = base.host;
@@ -35706,7 +35720,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
35706
35720
  }
35707
35721
  target.scheme = base.scheme;
35708
35722
  }
35709
- target.fragment = relative.fragment;
35723
+ target.fragment = relative2.fragment;
35710
35724
  return target;
35711
35725
  }
35712
35726
  function equal(uriA, uriB, options) {
@@ -35832,7 +35846,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
35832
35846
  const fastUri = {
35833
35847
  SCHEMES,
35834
35848
  normalize,
35835
- resolve: resolve2,
35849
+ resolve: resolve5,
35836
35850
  resolveComponent,
35837
35851
  equal,
35838
35852
  serialize,
@@ -41205,7 +41219,7 @@ var StdioServerTransport = class {
41205
41219
  }
41206
41220
  send(message) {
41207
41221
  if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
41208
- return new Promise((resolve2, reject) => {
41222
+ return new Promise((resolve5, reject) => {
41209
41223
  const json2 = serializeMessage(message);
41210
41224
  let settled = false;
41211
41225
  const onError = (error62) => {
@@ -41220,14 +41234,14 @@ var StdioServerTransport = class {
41220
41234
  settled = true;
41221
41235
  this._stdout.off("error", onError);
41222
41236
  this._stdout.off("drain", onDrain);
41223
- resolve2();
41237
+ resolve5();
41224
41238
  };
41225
41239
  this._stdout.once("error", onError);
41226
41240
  if (this._stdout.write(json2)) {
41227
41241
  if (settled) return;
41228
41242
  settled = true;
41229
41243
  this._stdout.off("error", onError);
41230
- resolve2();
41244
+ resolve5();
41231
41245
  } else if (!settled) this._stdout.once("drain", onDrain);
41232
41246
  });
41233
41247
  }
@@ -41281,14 +41295,14 @@ var StdioConnectionChannel = class {
41281
41295
  */
41282
41296
  async whenRequestsAnswered(timeoutMs) {
41283
41297
  if (this._closed || this._pendingRequests.size === 0) return true;
41284
- return await new Promise((resolve2) => {
41298
+ return await new Promise((resolve5) => {
41285
41299
  const waiter = () => {
41286
41300
  clearTimeout(timer);
41287
- resolve2(true);
41301
+ resolve5(true);
41288
41302
  };
41289
41303
  const timer = setTimeout(() => {
41290
41304
  this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
41291
- resolve2(false);
41305
+ resolve5(false);
41292
41306
  }, timeoutMs);
41293
41307
  this._drainWaiters.push(waiter);
41294
41308
  });
@@ -41662,13 +41676,28 @@ var API_KEY_RE = new RegExp([
41662
41676
  "whsec_[A-Za-z0-9]{16,}\\b"
41663
41677
  // webhook signing secret (Stripe-style)
41664
41678
  ].map((p) => `\\b${p}`).join("|"), "g");
41665
- var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
41679
+ var 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;
41680
+ var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
41681
+ var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
41682
+ var HEADER_SECRET_RE = /(\b(?:x[-_][a-z0-9_-]*?(?:api[-_]?key|token|secret|auth)[a-z0-9_-]*|api[-_]?key)\s*:\s*)[^\s,;"'<>`]+/gi;
41666
41683
  var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
41667
- var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
41668
- var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
41669
- var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
41684
+ var JSON_SECRET_KEYS = [
41685
+ "(?:access|refresh|id|auth|session|bearer|csrf|xsrf)[_-]?token",
41686
+ "client[_-]?secret",
41687
+ "api[_-]?secret",
41688
+ "(?:x[_-])?api[_-]?key",
41689
+ "private[_-]?key",
41690
+ "x[_-][a-z0-9_-]*?(?:api[_-]?key|token|secret|auth)[a-z0-9_-]*",
41691
+ "(?:proxy-)?authorization",
41692
+ "password",
41693
+ "passwd",
41694
+ "secret",
41695
+ "token"
41696
+ ].join("|");
41697
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")(?:[^"\\\\]|\\\\.)*(")`, "gi");
41698
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')(?:[^'\\\\]|\\\\.)*(')`, "gi");
41670
41699
  function redactSecrets(text) {
41671
- 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]");
41700
+ 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]");
41672
41701
  }
41673
41702
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
41674
41703
  const str = text === null || text === void 0 ? "" : String(text);
@@ -46709,7 +46738,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
46709
46738
  var NONE = 0;
46710
46739
  var ASSUMED = 1;
46711
46740
  var PROVEN = 2;
46712
- function isRecursive(inst, stack, resolve2) {
46741
+ function isRecursive(inst, stack, resolve5) {
46713
46742
  const cached2 = recursive.get(inst);
46714
46743
  if (cached2 !== void 0)
46715
46744
  return cached2 ? PROVEN : NONE;
@@ -46719,7 +46748,7 @@ function isRecursive(inst, stack, resolve2) {
46719
46748
  let result = NONE;
46720
46749
  const check2 = (child) => {
46721
46750
  if (result !== PROVEN && child?._zod) {
46722
- const answer = isRecursive(child, stack, resolve2);
46751
+ const answer = isRecursive(child, stack, resolve5);
46723
46752
  if (answer > result)
46724
46753
  result = answer;
46725
46754
  }
@@ -46730,7 +46759,7 @@ function isRecursive(inst, stack, resolve2) {
46730
46759
  const desc = Object.getOwnPropertyDescriptor(sh, key);
46731
46760
  if (spread && !desc.enumerable)
46732
46761
  continue;
46733
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve2) : NONE;
46762
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
46734
46763
  if (child > answer)
46735
46764
  answer = child;
46736
46765
  }
@@ -46794,7 +46823,7 @@ function isRecursive(inst, stack, resolve2) {
46794
46823
  break;
46795
46824
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
46796
46825
  case "lazy": {
46797
- const inner = def._cachedInner ?? (resolve2 ? inst._zod.innerType : void 0);
46826
+ const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
46798
46827
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
46799
46828
  break;
46800
46829
  }
@@ -61447,17 +61476,73 @@ Hint: ${err.hint}`);
61447
61476
  }
61448
61477
  throw err;
61449
61478
  }
61479
+ function mcpRequestFrom(args) {
61480
+ const ctx = args.at(-1);
61481
+ if (typeof ctx !== "object" || ctx === null)
61482
+ return void 0;
61483
+ const req = ctx.mcpReq;
61484
+ if (typeof req !== "object" || req === null)
61485
+ return void 0;
61486
+ return typeof req.notify === "function" ? req : void 0;
61487
+ }
61488
+ function callSignalFrom(args) {
61489
+ const ctx = args.at(-1);
61490
+ if (typeof ctx !== "object" || ctx === null)
61491
+ return void 0;
61492
+ const req = ctx.mcpReq;
61493
+ if (typeof req !== "object" || req === null)
61494
+ return void 0;
61495
+ const signal = req.signal;
61496
+ return signal instanceof AbortSignal ? signal : void 0;
61497
+ }
61498
+ function declaredCapabilitiesFrom(server) {
61499
+ const inner = server.server;
61500
+ if (typeof inner !== "object" || inner === null)
61501
+ return void 0;
61502
+ const read = inner.getClientCapabilities;
61503
+ if (typeof read !== "function")
61504
+ return void 0;
61505
+ try {
61506
+ const declared = read.call(inner);
61507
+ return typeof declared === "object" && declared !== null ? declared : void 0;
61508
+ } catch {
61509
+ return void 0;
61510
+ }
61511
+ }
61450
61512
  function surfaceToolHints(server) {
61451
61513
  const register2 = server.registerTool.bind(server);
61452
- server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => {
61453
- let result;
61454
- try {
61455
- result = cb(...args);
61456
- } catch (err) {
61457
- return hintResultOrRethrow(err);
61458
- }
61459
- return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
61460
- });
61514
+ server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => (
61515
+ // THE CALLER'S CANCELLATION, made ambient for the whole handler
61516
+ // (`cancel/index.ts`). The SDK delivers it and the fleet ignored it:
61517
+ // measured against @modelcontextprotocol/server 2.0.0, a cancelled
61518
+ // call aborts `ctx.mcpReq.signal` with the caller's reason and the
61519
+ // handler runs to completion regardless — so the HTTP request stays
61520
+ // in flight, the child keeps burning metered CPU, and the upstream
61521
+ // keeps being hit for somebody who has gone. claude.ai sent 101
61522
+ // cancellations in the week to 2026-09-20.
61523
+ //
61524
+ // Here because this is the one wrapper every tool already passes
61525
+ // through: threading the signal by hand would mean editing several
61526
+ // hundred handlers and missing exactly the ones nobody edits.
61527
+ withCallSignal(callSignalFrom(args), () => (
61528
+ // WHAT THE CALLER CAN DO, made ambient for the same reason and in
61529
+ // the same place (`caller/index.ts`). Only the 2025-era half needs
61530
+ // the wrapper — a 2026-07-28 request answers for itself off its own
61531
+ // envelope — but a guarded tool that cannot tell whether the caller
61532
+ // can be shown a prompt returns one the SDK then refuses to deliver,
61533
+ // and that refusal reaches the caller as an unexplained protocol
61534
+ // error it had no chance to catch.
61535
+ withCallerCapabilities(declaredCapabilitiesFrom(server), () => {
61536
+ let result;
61537
+ try {
61538
+ result = cb(...args);
61539
+ } catch (err) {
61540
+ return hintResultOrRethrow(err);
61541
+ }
61542
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
61543
+ })
61544
+ ), mcpRequestFrom(args))
61545
+ ));
61461
61546
  }
61462
61547
  async function createMcpServer(opts) {
61463
61548
  const server = new McpServer({ name: opts.name, version: opts.version }, { supportedProtocolVersions: [...SERVER_PROTOCOL_VERSIONS] });
@@ -61472,13 +61557,47 @@ async function createMcpServer(opts) {
61472
61557
  }
61473
61558
  return server;
61474
61559
  }
61560
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
61561
+ var DEFAULT_REPEAT_SIGNAL_GRACE_MS = 500;
61475
61562
  function withGracefulShutdown(target, opts = {}) {
61476
61563
  const shouldExit = opts.exit ?? true;
61564
+ if (opts.timeoutMs !== void 0 && !(Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0)) {
61565
+ throw new RangeError("withGracefulShutdown: timeoutMs must be a finite number greater than 0.");
61566
+ }
61567
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
61568
+ if (opts.repeatSignalGraceMs !== void 0 && !(Number.isFinite(opts.repeatSignalGraceMs) && opts.repeatSignalGraceMs >= 0)) {
61569
+ throw new RangeError("withGracefulShutdown: repeatSignalGraceMs must be a finite number >= 0.");
61570
+ }
61571
+ const repeatGraceMs = opts.repeatSignalGraceMs ?? DEFAULT_REPEAT_SIGNAL_GRACE_MS;
61477
61572
  let shuttingDown = false;
61573
+ let firstSignalAt = 0;
61574
+ let exited = false;
61575
+ const exitOnce = () => {
61576
+ if (exited)
61577
+ return;
61578
+ exited = true;
61579
+ process.exit(0);
61580
+ };
61478
61581
  const handler = (signal) => {
61479
- if (shuttingDown)
61582
+ if (shuttingDown) {
61583
+ if (Date.now() - firstSignalAt < repeatGraceMs)
61584
+ return;
61585
+ if (shouldExit) {
61586
+ console.error(`[mcp-utils] second ${signal} during shutdown \u2014 exiting now`);
61587
+ exitOnce();
61588
+ }
61480
61589
  return;
61590
+ }
61481
61591
  shuttingDown = true;
61592
+ firstSignalAt = Date.now();
61593
+ let timer;
61594
+ if (shouldExit) {
61595
+ timer = setTimeout(() => {
61596
+ console.error(`[mcp-utils] graceful shutdown on ${signal} did not finish in ${timeoutMs}ms \u2014 exiting`);
61597
+ exitOnce();
61598
+ }, timeoutMs);
61599
+ timer.unref?.();
61600
+ }
61482
61601
  void (async () => {
61483
61602
  try {
61484
61603
  if (opts.onSignal)
@@ -61487,8 +61606,10 @@ function withGracefulShutdown(target, opts = {}) {
61487
61606
  } catch (err) {
61488
61607
  console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
61489
61608
  } finally {
61609
+ if (timer !== void 0)
61610
+ clearTimeout(timer);
61490
61611
  if (shouldExit)
61491
- process.exit(0);
61612
+ exitOnce();
61492
61613
  }
61493
61614
  })();
61494
61615
  };
@@ -61518,6 +61639,8 @@ function runMcp(opts) {
61518
61639
  }
61519
61640
 
61520
61641
  // node_modules/@chrischall/mcp-utils/dist/config/index.js
61642
+ import { homedir } from "node:os";
61643
+ import { isAbsolute, join, resolve } from "node:path";
61521
61644
  var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
61522
61645
  function readEnvVar(key, opts = {}) {
61523
61646
  const env = opts.env ?? process.env;
@@ -61544,6 +61667,15 @@ function parseBoolEnv(key, opts = {}) {
61544
61667
  return false;
61545
61668
  return fallback;
61546
61669
  }
61670
+ function expandPath(p) {
61671
+ let expanded = p;
61672
+ if (p === "~") {
61673
+ expanded = homedir();
61674
+ } else if (p.startsWith("~/")) {
61675
+ expanded = join(homedir(), p.slice(2));
61676
+ }
61677
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
61678
+ }
61547
61679
  async function loadDotenvSafely(opts = {}) {
61548
61680
  try {
61549
61681
  const mod = await import(
@@ -61561,6 +61693,41 @@ async function loadDotenvSafely(opts = {}) {
61561
61693
  }
61562
61694
  }
61563
61695
 
61696
+ // node_modules/@chrischall/mcp-utils/dist/fs/confine.js
61697
+ import { existsSync, realpathSync } from "node:fs";
61698
+ import { dirname, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
61699
+ function isInside(root, target) {
61700
+ const rel = relative(root, target);
61701
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute2(rel);
61702
+ }
61703
+ function realPathAllowingMissing(p) {
61704
+ const abs = resolve2(p);
61705
+ let existing = abs;
61706
+ const tail = [];
61707
+ while (!existsSync(existing)) {
61708
+ const parent = dirname(existing);
61709
+ if (parent === existing)
61710
+ break;
61711
+ tail.unshift(existing.slice(parent.length).replace(/^[/\\]+/, ""));
61712
+ existing = parent;
61713
+ }
61714
+ let real;
61715
+ try {
61716
+ real = realpathSync(existing);
61717
+ } catch {
61718
+ real = existing;
61719
+ }
61720
+ return tail.length > 0 ? resolve2(real, ...tail) : real;
61721
+ }
61722
+ function assertPathWithinRoots(path, roots) {
61723
+ const target = realPathAllowingMissing(expandPath(path));
61724
+ for (const root of roots) {
61725
+ if (isInside(realPathAllowingMissing(expandPath(root)), target))
61726
+ return target;
61727
+ }
61728
+ throw new Error(`Path is outside the allowed directories: ${path}`);
61729
+ }
61730
+
61564
61731
  // node_modules/@chrischall/mcp-utils/dist/http/index.js
61565
61732
  function buildQueryString(params) {
61566
61733
  const parts = [];
@@ -61695,6 +61862,33 @@ var pageSchema = {
61695
61862
  };
61696
61863
 
61697
61864
  // node_modules/@chrischall/mcp-utils/dist/auth/index.js
61865
+ var OAuth2RefreshError = class extends McpToolError {
61866
+ status;
61867
+ constructor(status, message, opts) {
61868
+ super(message, opts);
61869
+ this.name = "OAuth2RefreshError";
61870
+ this.status = status;
61871
+ Object.setPrototypeOf(this, new.target.prototype);
61872
+ }
61873
+ };
61874
+ var OAuth2RotationPersistError = class extends McpToolError {
61875
+ persistenceFailure = true;
61876
+ result;
61877
+ constructor(result, cause) {
61878
+ super(`OAuth2 refresh token rotated, but saving it failed: ${truncateErrorMessage(messageOf2(cause), 200)}`, {
61879
+ hint: "The new refresh token is still held in memory; fix the storage problem before the process restarts.",
61880
+ cause
61881
+ });
61882
+ this.name = "OAuth2RotationPersistError";
61883
+ this.result = result;
61884
+ Object.setPrototypeOf(this, new.target.prototype);
61885
+ }
61886
+ };
61887
+ function isRetryable(err) {
61888
+ if (!(err instanceof OAuth2RefreshError))
61889
+ return true;
61890
+ return err.status >= 500 || err.status === 429 || err.status === 408;
61891
+ }
61698
61892
  var sleep2 = (ms) => ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
61699
61893
  function createOAuth2Refresher(opts) {
61700
61894
  const doFetch = opts.fetchImpl ?? fetch;
@@ -61702,10 +61896,13 @@ function createOAuth2Refresher(opts) {
61702
61896
  const maxRetries = opts.retry?.count ?? 0;
61703
61897
  const retryDelayMs = opts.retry?.delayMs ?? 0;
61704
61898
  let inFlight = null;
61899
+ let currentRefreshToken = opts.refreshToken;
61900
+ const spent = /* @__PURE__ */ new Set();
61901
+ let rotatedTo;
61705
61902
  async function exchangeOnce() {
61706
61903
  const body = new URLSearchParams({
61707
61904
  grant_type: grantType,
61708
- refresh_token: opts.refreshToken,
61905
+ refresh_token: currentRefreshToken,
61709
61906
  ...opts.params
61710
61907
  }).toString();
61711
61908
  const res = await doFetch(opts.endpoint, {
@@ -61718,7 +61915,7 @@ function createOAuth2Refresher(opts) {
61718
61915
  });
61719
61916
  if (!res.ok) {
61720
61917
  const errText = await res.text().catch(() => "");
61721
- throw createHelpfulError(`OAuth2 token refresh failed: ${res.status} ${res.statusText}: ${truncateErrorMessage(errText, 200)}`, { hint: "The refresh token may be expired or revoked \u2014 re-authenticate." });
61918
+ throw new OAuth2RefreshError(res.status, `OAuth2 token refresh failed: ${res.status} ${res.statusText}: ${truncateErrorMessage(errText, 200)}`, { hint: "The refresh token may be expired or revoked \u2014 re-authenticate." });
61722
61919
  }
61723
61920
  const data = await res.json().catch(() => null);
61724
61921
  const accessToken = data?.access_token;
@@ -61730,6 +61927,11 @@ function createOAuth2Refresher(opts) {
61730
61927
  const result = { accessToken };
61731
61928
  if (typeof data?.refresh_token === "string" && data.refresh_token.length > 0) {
61732
61929
  result.refreshToken = data.refresh_token;
61930
+ if (data.refresh_token !== currentRefreshToken) {
61931
+ spent.add(currentRefreshToken);
61932
+ currentRefreshToken = data.refresh_token;
61933
+ rotatedTo = data.refresh_token;
61934
+ }
61733
61935
  }
61734
61936
  if (typeof data?.expires_in === "number") {
61735
61937
  result.expiresIn = data.expires_in;
@@ -61744,16 +61946,32 @@ function createOAuth2Refresher(opts) {
61744
61946
  return await exchangeOnce();
61745
61947
  } catch (e) {
61746
61948
  lastErr = e;
61949
+ if (!isRetryable(e))
61950
+ break;
61747
61951
  if (attempt < maxRetries)
61748
61952
  await sleep2(retryDelayMs);
61749
61953
  }
61750
61954
  }
61751
61955
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
61752
61956
  }
61753
- return function refresh() {
61957
+ return function refresh(refreshToken) {
61754
61958
  if (inFlight)
61755
61959
  return inFlight;
61756
- const p = exchangeWithRetry().finally(() => {
61960
+ if (typeof refreshToken === "string" && refreshToken.length > 0 && !spent.has(refreshToken)) {
61961
+ currentRefreshToken = refreshToken;
61962
+ }
61963
+ const p = (async () => {
61964
+ rotatedTo = void 0;
61965
+ const result = await exchangeWithRetry();
61966
+ if (rotatedTo !== void 0 && opts.onRotate) {
61967
+ try {
61968
+ await opts.onRotate(rotatedTo);
61969
+ } catch (err) {
61970
+ throw new OAuth2RotationPersistError(result, err);
61971
+ }
61972
+ }
61973
+ return result;
61974
+ })().finally(() => {
61757
61975
  if (inFlight === p)
61758
61976
  inFlight = null;
61759
61977
  });
@@ -61761,6 +61979,9 @@ function createOAuth2Refresher(opts) {
61761
61979
  return p;
61762
61980
  };
61763
61981
  }
61982
+ function messageOf2(err) {
61983
+ return err instanceof Error ? err.message : String(err);
61984
+ }
61764
61985
 
61765
61986
  // node_modules/@fetchproxy/protocol/dist/crypto.js
61766
61987
  var subtle = globalThis.crypto.subtle;
@@ -61959,6 +62180,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
61959
62180
  "capture_redirect",
61960
62181
  "read_indexed_db",
61961
62182
  "read_dom",
62183
+ "read_dom_list",
61962
62184
  "download",
61963
62185
  "graphql",
61964
62186
  "write_cookies"
@@ -62459,6 +62681,79 @@ function assertDomSelectorsArray(value, label) {
62459
62681
  }
62460
62682
  }
62461
62683
  }
62684
+ function assertDomListSelectorsArray(value, label) {
62685
+ if (!Array.isArray(value)) {
62686
+ throw new ProtocolError3(`${label}: expected array, got ${typeof value}`);
62687
+ }
62688
+ const seen = /* @__PURE__ */ new Set();
62689
+ for (let i = 0; i < value.length; i++) {
62690
+ const entry = value[i];
62691
+ assertObject(entry, `${label}[${i}]`);
62692
+ if (entry.name === void 0) {
62693
+ throw new ProtocolError3(`${label}[${i}].name: missing`);
62694
+ }
62695
+ if (entry.itemSelector === void 0) {
62696
+ throw new ProtocolError3(`${label}[${i}].itemSelector: missing`);
62697
+ }
62698
+ if (entry.fields === void 0) {
62699
+ throw new ProtocolError3(`${label}[${i}].fields: missing`);
62700
+ }
62701
+ if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
62702
+ throw new ProtocolError3(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
62703
+ }
62704
+ if (typeof entry.itemSelector !== "string" || !DOM_SELECTOR_RE.test(entry.itemSelector)) {
62705
+ throw new ProtocolError3(`${label}[${i}].itemSelector: invalid ${JSON.stringify(entry.itemSelector)}`);
62706
+ }
62707
+ if (!Array.isArray(entry.fields) || entry.fields.length === 0) {
62708
+ throw new ProtocolError3(`${label}[${i}].fields: expected non-empty array`);
62709
+ }
62710
+ const fieldNames = /* @__PURE__ */ new Set();
62711
+ for (let j = 0; j < entry.fields.length; j++) {
62712
+ const field = entry.fields[j];
62713
+ const fieldLabel = `${label}[${i}].fields[${j}]`;
62714
+ assertObject(field, fieldLabel);
62715
+ if (field.name === void 0) {
62716
+ throw new ProtocolError3(`${fieldLabel}.name: missing`);
62717
+ }
62718
+ if (typeof field.name !== "string" || !SCOPE_KEY_RE.test(field.name)) {
62719
+ throw new ProtocolError3(`${fieldLabel}.name: invalid ${JSON.stringify(field.name)}`);
62720
+ }
62721
+ if (field.selector !== void 0) {
62722
+ if (typeof field.selector !== "string" || !DOM_SELECTOR_RE.test(field.selector)) {
62723
+ throw new ProtocolError3(`${fieldLabel}.selector: invalid ${JSON.stringify(field.selector)}`);
62724
+ }
62725
+ }
62726
+ if (field.attribute !== void 0) {
62727
+ if (typeof field.attribute !== "string" || !DOM_ATTRIBUTE_RE.test(field.attribute)) {
62728
+ throw new ProtocolError3(`${fieldLabel}.attribute: invalid ${JSON.stringify(field.attribute)}`);
62729
+ }
62730
+ }
62731
+ if (fieldNames.has(field.name)) {
62732
+ throw new ProtocolError3(`${label}[${i}].fields: duplicate name ${JSON.stringify(field.name)}`);
62733
+ }
62734
+ fieldNames.add(field.name);
62735
+ for (const k of Object.keys(field)) {
62736
+ if (k !== "name" && k !== "selector" && k !== "attribute") {
62737
+ throw new ProtocolError3(`${fieldLabel}: unexpected field ${JSON.stringify(k)}`);
62738
+ }
62739
+ }
62740
+ }
62741
+ if (entry.maxItems !== void 0) {
62742
+ if (typeof entry.maxItems !== "number" || !Number.isInteger(entry.maxItems) || entry.maxItems < 1 || entry.maxItems > 1e3) {
62743
+ throw new ProtocolError3(`${label}[${i}].maxItems: invalid ${JSON.stringify(entry.maxItems)}`);
62744
+ }
62745
+ }
62746
+ if (seen.has(entry.name)) {
62747
+ throw new ProtocolError3(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
62748
+ }
62749
+ seen.add(entry.name);
62750
+ for (const k of Object.keys(entry)) {
62751
+ if (k !== "name" && k !== "itemSelector" && k !== "fields" && k !== "maxItems") {
62752
+ throw new ProtocolError3(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
62753
+ }
62754
+ }
62755
+ }
62756
+ }
62462
62757
  function assertGraphqlOpsArray(value, label) {
62463
62758
  if (!Array.isArray(value)) {
62464
62759
  throw new ProtocolError3(`${label}: expected array, got ${typeof value}`);
@@ -62505,6 +62800,8 @@ function validateFrame(raw) {
62505
62800
  return validateHelloRejected(raw);
62506
62801
  if (t === "extension-disconnected")
62507
62802
  return { type: "extension-disconnected" };
62803
+ if (t === "peer-gone")
62804
+ return validatePeerGone(raw);
62508
62805
  throw new ProtocolError3(`unknown frame type: ${String(t)}`);
62509
62806
  }
62510
62807
  function validateHello(raw) {
@@ -62575,16 +62872,11 @@ function validateHello(raw) {
62575
62872
  if (raw.domSelectors !== void 0) {
62576
62873
  assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
62577
62874
  }
62578
- if (raw.accepts !== void 0) {
62579
- if (!Array.isArray(raw.accepts)) {
62580
- throw new ProtocolError3("hello.accepts: expected array");
62581
- }
62582
- for (const a of raw.accepts) {
62583
- if (typeof a !== "string") {
62584
- throw new ProtocolError3(`hello.accepts: entry must be string, got ${typeof a}`);
62585
- }
62586
- }
62875
+ if (raw.domListSelectors !== void 0) {
62876
+ assertDomListSelectorsArray(raw.domListSelectors, "hello.domListSelectors");
62587
62877
  }
62878
+ if (raw.accepts !== void 0)
62879
+ assertAcceptsList(raw.accepts);
62588
62880
  if (raw.graphqlOps !== void 0) {
62589
62881
  assertGraphqlOpsArray(raw.graphqlOps, "hello.graphqlOps");
62590
62882
  }
@@ -62606,6 +62898,8 @@ function validateHello(raw) {
62606
62898
  assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
62607
62899
  assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
62608
62900
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
62901
+ if (raw.accepts !== void 0)
62902
+ assertAcceptsList(raw.accepts);
62609
62903
  return raw;
62610
62904
  }
62611
62905
  throw new ProtocolError3(`hello.role: must be 'server' or 'extension', got ${String(role)}`);
@@ -62661,6 +62955,27 @@ function validatePairPending(raw) {
62661
62955
  }
62662
62956
  return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
62663
62957
  }
62958
+ function assertAcceptsList(accepts) {
62959
+ if (!Array.isArray(accepts)) {
62960
+ throw new ProtocolError3("hello.accepts: expected array");
62961
+ }
62962
+ for (const a of accepts) {
62963
+ if (typeof a !== "string") {
62964
+ throw new ProtocolError3(`hello.accepts: entry must be string, got ${typeof a}`);
62965
+ }
62966
+ }
62967
+ }
62968
+ function validatePeerGone(raw) {
62969
+ assertString(raw.mcpId, "peer-gone.mcpId");
62970
+ if (!isValidMcpId(raw.mcpId))
62971
+ throw new ProtocolError3("peer-gone.mcpId: invalid format");
62972
+ for (const k of Object.keys(raw)) {
62973
+ if (k !== "type" && k !== "mcpId") {
62974
+ throw new ProtocolError3(`peer-gone: unexpected field ${JSON.stringify(k)}`);
62975
+ }
62976
+ }
62977
+ return { type: "peer-gone", mcpId: raw.mcpId };
62978
+ }
62664
62979
  function validateHelloRejected(raw) {
62665
62980
  assertString(raw.mcpId, "hello-rejected.mcpId");
62666
62981
  if (!isValidMcpId(raw.mcpId))
@@ -62910,6 +63225,24 @@ function validateInnerRequest(raw) {
62910
63225
  }
62911
63226
  return raw;
62912
63227
  }
63228
+ if (raw.op === "read_dom_list") {
63229
+ assertObject(raw.init, "inner.init");
63230
+ if (raw.init.origin === void 0)
63231
+ throw new ProtocolError3("inner.init.origin: missing");
63232
+ if (raw.init.name === void 0)
63233
+ throw new ProtocolError3("inner.init.name: missing");
63234
+ assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
63235
+ assertString(raw.init.name, "inner.init.name");
63236
+ if (raw.init.name.length === 0) {
63237
+ throw new ProtocolError3("inner.init.name: must be non-empty");
63238
+ }
63239
+ for (const k of Object.keys(raw.init)) {
63240
+ if (k !== "origin" && k !== "name") {
63241
+ throw new ProtocolError3(`inner.init: unexpected field ${JSON.stringify(k)} on read_dom_list`);
63242
+ }
63243
+ }
63244
+ return raw;
63245
+ }
62913
63246
  if (raw.op === "graphql_query") {
62914
63247
  assertObject(raw.init, "inner.init");
62915
63248
  if (raw.init.name === void 0)
@@ -62957,7 +63290,7 @@ function validateInnerRequest(raw) {
62957
63290
  }
62958
63291
  return raw;
62959
63292
  }
62960
- 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)}`);
63293
+ 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)}`);
62961
63294
  }
62962
63295
  function assertNonEmptyKeyArray(value, label) {
62963
63296
  if (!Array.isArray(value)) {
@@ -63065,6 +63398,18 @@ function validateInnerResponse(raw) {
63065
63398
  assertStringMap(raw.values, "inner.values");
63066
63399
  return raw;
63067
63400
  }
63401
+ if (op === "read_dom_list") {
63402
+ if (raw.rows === void 0) {
63403
+ throw new ProtocolError3("inner.rows: missing on read_dom_list response");
63404
+ }
63405
+ if (!Array.isArray(raw.rows)) {
63406
+ throw new ProtocolError3("inner.rows: expected array, got " + typeof raw.rows);
63407
+ }
63408
+ for (const [i, row] of raw.rows.entries()) {
63409
+ assertStringMap(row, `inner.rows[${i}]`);
63410
+ }
63411
+ return raw;
63412
+ }
63068
63413
  if (op === "graphql_query") {
63069
63414
  if (raw.data === void 0) {
63070
63415
  throw new ProtocolError3("inner.data: missing on graphql_query response");
@@ -63166,12 +63511,6 @@ async function sealInnerFrame(sessionKey, mcpId, seq, inner, direction) {
63166
63511
  ciphertext: toB64(ct)
63167
63512
  };
63168
63513
  }
63169
- async function openEncryptedFrame(sessionKey, frame, direction) {
63170
- const result = await openEncryptedFrameDetailed(sessionKey, frame, direction);
63171
- if (result.stage === "ok")
63172
- return result.inner;
63173
- throw result.error instanceof Error ? result.error : new Error(String(result.error));
63174
- }
63175
63514
  async function openEncryptedFrameDetailed(sessionKey, frame, direction) {
63176
63515
  let pt;
63177
63516
  try {
@@ -63202,7 +63541,7 @@ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
63202
63541
  async function electRole(opts) {
63203
63542
  const server = createServer();
63204
63543
  const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
63205
- return new Promise((resolve2, reject) => {
63544
+ return new Promise((resolve5, reject) => {
63206
63545
  let timer;
63207
63546
  const clearBindTimer = () => {
63208
63547
  if (timer) {
@@ -63218,7 +63557,7 @@ async function electRole(opts) {
63218
63557
  server.close();
63219
63558
  } catch {
63220
63559
  }
63221
- resolve2({ role: "peer" });
63560
+ resolve5({ role: "peer" });
63222
63561
  } else {
63223
63562
  reject(e);
63224
63563
  }
@@ -63226,7 +63565,7 @@ async function electRole(opts) {
63226
63565
  const onListening = () => {
63227
63566
  clearBindTimer();
63228
63567
  server.removeListener("error", onError);
63229
- resolve2({ role: "host", server });
63568
+ resolve5({ role: "host", server });
63230
63569
  };
63231
63570
  server.once("error", onError);
63232
63571
  server.once("listening", onListening);
@@ -63323,6 +63662,18 @@ async function buildServerHello(opts) {
63323
63662
  ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
63324
63663
  }));
63325
63664
  }
63665
+ if (opts.domListSelectors && opts.domListSelectors.length > 0) {
63666
+ hello.domListSelectors = opts.domListSelectors.map((d) => ({
63667
+ name: d.name,
63668
+ itemSelector: d.itemSelector,
63669
+ fields: d.fields.map((f) => ({
63670
+ name: f.name,
63671
+ ...f.selector !== void 0 ? { selector: f.selector } : {},
63672
+ ...f.attribute !== void 0 ? { attribute: f.attribute } : {}
63673
+ })),
63674
+ ...d.maxItems !== void 0 ? { maxItems: d.maxItems } : {}
63675
+ }));
63676
+ }
63326
63677
  if (opts.graphqlOps && opts.graphqlOps.length > 0) {
63327
63678
  hello.graphqlOps = opts.graphqlOps.map((d) => ({
63328
63679
  name: d.name,
@@ -63491,14 +63842,15 @@ async function awaitSessionReady(ready, opts) {
63491
63842
  }
63492
63843
 
63493
63844
  // node_modules/@fetchproxy/server/dist/extension-trust.js
63494
- import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
63495
- import { isAbsolute as isAbsolute2, join as join2 } from "node:path";
63845
+ 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";
63846
+ import { randomBytes as randomBytes2 } from "node:crypto";
63847
+ import { isAbsolute as isAbsolute4, join as join3 } from "node:path";
63496
63848
 
63497
63849
  // node_modules/@fetchproxy/server/dist/identity.js
63498
- import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink } from "node:fs/promises";
63850
+ import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink, link, lstat } from "node:fs/promises";
63499
63851
  import { randomBytes } from "node:crypto";
63500
- import { isAbsolute, join } from "node:path";
63501
- import { homedir } from "node:os";
63852
+ import { isAbsolute as isAbsolute3, join as join2 } from "node:path";
63853
+ import { homedir as homedir2 } from "node:os";
63502
63854
  var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
63503
63855
  var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
63504
63856
  function envIdentityDir() {
@@ -63506,12 +63858,12 @@ function envIdentityDir() {
63506
63858
  if (raw === void 0)
63507
63859
  return void 0;
63508
63860
  const dir = raw.trim();
63509
- if (dir === "" || !isAbsolute(dir))
63861
+ if (dir === "" || !isAbsolute3(dir))
63510
63862
  return void 0;
63511
63863
  return dir;
63512
63864
  }
63513
63865
  function defaultIdentityDir() {
63514
- return envIdentityDir() ?? join(homedir(), ".fetchproxy", "identity");
63866
+ return envIdentityDir() ?? join2(homedir2(), ".fetchproxy", "identity");
63515
63867
  }
63516
63868
  function safeIdentityFileBase(serverName) {
63517
63869
  if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
@@ -63520,7 +63872,7 @@ function safeIdentityFileBase(serverName) {
63520
63872
  return serverName.replace(/\//g, "_");
63521
63873
  }
63522
63874
  function identityFilePath(dir, serverName) {
63523
- return join(dir, `${safeIdentityFileBase(serverName)}.json`);
63875
+ return join2(dir, `${safeIdentityFileBase(serverName)}.json`);
63524
63876
  }
63525
63877
  async function generateIdentity() {
63526
63878
  const x = await generateX25519();
@@ -63563,7 +63915,7 @@ async function openIdentityDir(dir) {
63563
63915
  }
63564
63916
  }
63565
63917
  var STAGING_BYTES = 6;
63566
- async function sweepStagingFiles(dir, serverName) {
63918
+ async function sweepStagingFiles(dir, serverName, opts = {}) {
63567
63919
  const base = safeIdentityFileBase(serverName).replace(/[.]/g, "\\.");
63568
63920
  const staging = new RegExp(`^${base}\\.json\\.[0-9a-f]{${STAGING_BYTES * 2}}\\.tmp$`);
63569
63921
  let names;
@@ -63572,35 +63924,80 @@ async function sweepStagingFiles(dir, serverName) {
63572
63924
  } catch {
63573
63925
  return;
63574
63926
  }
63575
- await Promise.all(names.filter((name) => staging.test(name)).map((name) => unlink(join(dir, name)).catch(() => void 0)));
63927
+ const cutoff = opts.olderThanMs === void 0 ? null : Date.now() - opts.olderThanMs;
63928
+ await Promise.all(names.filter((name) => staging.test(name)).map(async (name) => {
63929
+ const path = join2(dir, name);
63930
+ if (cutoff !== null) {
63931
+ const st = await lstat(path).catch(() => null);
63932
+ if (st === null || st.mtimeMs > cutoff)
63933
+ return;
63934
+ }
63935
+ await unlink(path).catch(() => void 0);
63936
+ }));
63576
63937
  }
63577
- async function writeIdentityFile(dir, serverName, id) {
63938
+ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
63578
63939
  const path = identityFilePath(dir, serverName);
63579
63940
  await openIdentityDir(dir);
63580
- await sweepStagingFiles(dir, serverName);
63581
- const tmp = `${path}.${randomBytes(STAGING_BYTES).toString("hex")}.tmp`;
63582
63941
  try {
63583
- await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
63584
- await chmod(tmp, 384);
63585
- await rename(tmp, path);
63942
+ return parseIdentity(await readFile(path, "utf8"));
63586
63943
  } catch (e) {
63587
- await rm(tmp, { force: true });
63588
- throw e;
63944
+ if (e.code !== "ENOENT" && !(e instanceof SyntaxError))
63945
+ throw e;
63946
+ }
63947
+ let unreadable = null;
63948
+ for (let attempt = 0; attempt < 5; attempt++) {
63949
+ const id = await generateIdentity();
63950
+ if (await publishIdentityIfAbsent(dir, serverName, id))
63951
+ return id;
63952
+ try {
63953
+ return parseIdentity(await readFile(path, "utf8"));
63954
+ } catch (e) {
63955
+ if (e.code === "ENOENT") {
63956
+ continue;
63957
+ }
63958
+ if (!(e instanceof SyntaxError))
63959
+ throw e;
63960
+ unreadable = e;
63961
+ await new Promise((r) => setTimeout(r, 20 * (attempt + 1)));
63962
+ }
63589
63963
  }
63590
- return path;
63964
+ if (unreadable !== null)
63965
+ throw unreadable;
63966
+ throw new Error(`fetchproxy: could not create an identity for ${JSON.stringify(serverName)} in ${dir}`);
63591
63967
  }
63592
- async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
63968
+ var NO_HARD_LINKS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS"]);
63969
+ async function publishIdentityIfAbsent(dir, serverName, id) {
63593
63970
  const path = identityFilePath(dir, serverName);
63594
- await openIdentityDir(dir);
63971
+ await sweepStagingFiles(dir, serverName, { olderThanMs: 6e4 });
63972
+ const tmp = `${path}.${randomBytes(STAGING_BYTES).toString("hex")}.tmp`;
63595
63973
  try {
63596
- return parseIdentity(await readFile(path, "utf8"));
63974
+ await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
63975
+ await chmod(tmp, 384);
63976
+ try {
63977
+ await link(tmp, path);
63978
+ } catch (e) {
63979
+ const code = e.code;
63980
+ if (code === void 0 || !NO_HARD_LINKS.has(code))
63981
+ throw e;
63982
+ try {
63983
+ await writeFile(path, serializeIdentity(id), { mode: 384, flag: "wx" });
63984
+ } catch (writeErr) {
63985
+ if (writeErr.code !== "EEXIST") {
63986
+ await rm(path, { force: true }).catch(() => void 0);
63987
+ }
63988
+ throw writeErr;
63989
+ }
63990
+ await chmod(path, 384);
63991
+ }
63992
+ return true;
63597
63993
  } catch (e) {
63598
- if (e.code !== "ENOENT")
63599
- throw e;
63994
+ const code = e.code;
63995
+ if (code === "EEXIST" || code === "ENOENT")
63996
+ return false;
63997
+ throw e;
63998
+ } finally {
63999
+ await rm(tmp, { force: true });
63600
64000
  }
63601
- const id = await generateIdentity();
63602
- await writeIdentityFile(dir, serverName, id);
63603
- return id;
63604
64001
  }
63605
64002
 
63606
64003
  // node_modules/@fetchproxy/server/dist/extension-trust.js
@@ -63610,7 +64007,7 @@ function envTrustDir(env = process.env) {
63610
64007
  if (raw === void 0)
63611
64008
  return void 0;
63612
64009
  const dir = raw.trim();
63613
- if (dir !== "" && isAbsolute2(dir))
64010
+ if (dir !== "" && isAbsolute4(dir))
63614
64011
  return dir;
63615
64012
  console.warn(`[fetchproxy] ignoring ${TRUST_DIR_ENV}=${JSON.stringify(raw)}: it is not an absolute path. The extension pin will be written beside the identity instead, which is not where you pointed it \u2014 if that directory is read-only, no pin is ever kept.`);
63616
64013
  return void 0;
@@ -63658,13 +64055,13 @@ function decideExtensionTrust(args) {
63658
64055
  };
63659
64056
  }
63660
64057
  function extensionTrustPath(serverName, dir = defaultTrustDir()) {
63661
- return join2(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
64058
+ return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
63662
64059
  }
63663
64060
  function extensionTrustPathHint(serverName) {
63664
64061
  try {
63665
64062
  return extensionTrustPath(serverName);
63666
64063
  } catch {
63667
- return join2(defaultTrustDir(), "<server-name>.extension-trust.json");
64064
+ return join3(defaultTrustDir(), "<server-name>.extension-trust.json");
63668
64065
  }
63669
64066
  }
63670
64067
  function isPin(x) {
@@ -63701,10 +64098,15 @@ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
63701
64098
  async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
63702
64099
  const path = extensionTrustPath(serverName, dir);
63703
64100
  await mkdir2(dir, { recursive: true, mode: 448 });
63704
- const tmp = `${path}.tmp`;
63705
- await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
63706
- await chmod2(tmp, 384);
63707
- await rename2(tmp, path);
64101
+ const tmp = `${path}.${randomBytes2(6).toString("hex")}.tmp`;
64102
+ try {
64103
+ await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384, flag: "wx" });
64104
+ await chmod2(tmp, 384);
64105
+ await rename2(tmp, path);
64106
+ } catch (e) {
64107
+ await rm2(tmp, { force: true });
64108
+ throw e;
64109
+ }
63708
64110
  }
63709
64111
 
63710
64112
  // node_modules/@fetchproxy/server/dist/host.js
@@ -63741,6 +64143,7 @@ function originVerdict(origin, allowLocal) {
63741
64143
  var HANDSHAKE_TIMEOUT_MS = 15e3;
63742
64144
  var MAX_PAYLOAD_BYTES = MAX_FRAME_BYTES;
63743
64145
  var enc4 = new TextEncoder();
64146
+ var OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE = 3;
63744
64147
  async function startHost(opts) {
63745
64148
  const allowLocalOrigins = envAllowsLocalOrigins();
63746
64149
  if (allowLocalOrigins) {
@@ -63784,6 +64187,7 @@ async function startHost(opts) {
63784
64187
  localStoragePointers: opts.ownLocalStoragePointers,
63785
64188
  sessionStoragePointers: opts.ownSessionStoragePointers,
63786
64189
  domSelectors: opts.ownDomSelectors,
64190
+ domListSelectors: opts.ownDomListSelectors,
63787
64191
  graphqlOps: opts.ownGraphqlOps
63788
64192
  };
63789
64193
  const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
@@ -63810,8 +64214,8 @@ async function startHost(opts) {
63810
64214
  let rejectOwnSession;
63811
64215
  let ownSessionReady;
63812
64216
  function resetSessionPromise() {
63813
- ownSessionReady = new Promise((resolve2, reject) => {
63814
- resolveOwnSession = resolve2;
64217
+ ownSessionReady = new Promise((resolve5, reject) => {
64218
+ resolveOwnSession = resolve5;
63815
64219
  rejectOwnSession = reject;
63816
64220
  });
63817
64221
  ownSessionReady.catch(() => {
@@ -63854,6 +64258,31 @@ async function startHost(opts) {
63854
64258
  return null;
63855
64259
  }
63856
64260
  };
64261
+ let ownDecryptFailures = 0;
64262
+ const rehandshakeOwnSession = async (ws) => {
64263
+ const hello = extensionHello;
64264
+ if (extensionWs !== ws || !hello)
64265
+ return;
64266
+ 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).`);
64267
+ ownSession = dropOwnSessionAndEphemeral();
64268
+ resetSessionPromise();
64269
+ const mintedKeypair = await generateSessionKeypair();
64270
+ const mintedHello = await buildServerHello({
64271
+ ...ownHelloBase,
64272
+ sessionPub: mintedKeypair.publicKey,
64273
+ answersExtNonce: fromB64(hello.sessionNonce)
64274
+ });
64275
+ if (extensionWs !== ws || extensionHello !== hello || ownSession !== null) {
64276
+ mintedKeypair.privateKey.fill(0);
64277
+ return;
64278
+ }
64279
+ installOwnEphemeral({
64280
+ nonce: fromB64(mintedHello.sessionNonce),
64281
+ pub: mintedKeypair.publicKey,
64282
+ priv: mintedKeypair.privateKey
64283
+ });
64284
+ ws.send(JSON.stringify(mintedHello));
64285
+ };
63857
64286
  wss.on("connection", (ws) => {
63858
64287
  let identified = null;
63859
64288
  let peerMcpId = null;
@@ -64044,6 +64473,7 @@ async function startHost(opts) {
64044
64473
  if (extensionWs !== ws)
64045
64474
  return;
64046
64475
  ownSession = new SessionState(key);
64476
+ ownDecryptFailures = 0;
64047
64477
  ownPendingPairCode = null;
64048
64478
  resolveOwnSession(ownSession);
64049
64479
  } else {
@@ -64065,15 +64495,42 @@ async function startHost(opts) {
64065
64495
  }
64066
64496
  if (claim2 !== "ok")
64067
64497
  return;
64068
- let inner;
64498
+ let result;
64069
64499
  try {
64070
- inner = await openEncryptedFrame(session.sessionKey, frame, "e2s");
64500
+ result = await openEncryptedFrameDetailed(session.sessionKey, frame, "e2s");
64071
64501
  } catch (e) {
64072
64502
  session.releaseInboundSeq(frame.seq);
64073
64503
  throw e;
64074
64504
  }
64505
+ if (result.stage === "decrypt-failed") {
64506
+ session.releaseInboundSeq(frame.seq);
64507
+ console.warn(`[fetchproxy] ${opts.ownServerName}: dropped an inbound frame (seq ${frame.seq}) that failed authentication \u2014 most likely a straggler from a previous session.`);
64508
+ if (session === ownSession) {
64509
+ ownDecryptFailures += 1;
64510
+ if (ownDecryptFailures >= OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE) {
64511
+ ownDecryptFailures = 0;
64512
+ void rehandshakeOwnSession(ws).catch((e) => console.error(`[fetchproxy] ${opts.ownServerName}: re-handshake failed:`, e));
64513
+ }
64514
+ }
64515
+ return;
64516
+ }
64517
+ if (session === ownSession)
64518
+ ownDecryptFailures = 0;
64075
64519
  session.commitInboundSeq(frame.seq);
64076
- ownInnerListeners.forEach((cb) => cb(inner));
64520
+ if (result.stage === "ok") {
64521
+ ownInnerListeners.forEach((cb) => cb(result.inner));
64522
+ } else {
64523
+ console.error("[fetchproxy] host: received a frame that decrypted OK but failed validation:", result.error);
64524
+ const recoveredId = result.recoveredId;
64525
+ if (recoveredId !== void 0) {
64526
+ ownInnerListeners.forEach((cb) => cb({
64527
+ type: "response",
64528
+ id: recoveredId,
64529
+ ok: false,
64530
+ error: `malformed response failed protocol validation: ${String(result.error)}`
64531
+ }));
64532
+ }
64533
+ }
64077
64534
  } else {
64078
64535
  const slot = peers.get(frame.mcpId);
64079
64536
  if (slot)
@@ -64132,6 +64589,7 @@ async function startHost(opts) {
64132
64589
  rejectOwnSession(new Error("extension disconnected before ready"));
64133
64590
  }
64134
64591
  ownSession = dropOwnSessionAndEphemeral();
64592
+ ownDecryptFailures = 0;
64135
64593
  ownPendingPairCode = null;
64136
64594
  resetSessionPromise();
64137
64595
  disconnectListeners.forEach((cb) => cb());
@@ -64146,13 +64604,20 @@ async function startHost(opts) {
64146
64604
  }
64147
64605
  }
64148
64606
  if (identified === "peer" && peerMcpId) {
64149
- if (peers.get(peerMcpId)?.ws === ws)
64607
+ if (peers.get(peerMcpId)?.ws === ws) {
64150
64608
  peers.delete(peerMcpId);
64609
+ if (extensionWs && extensionHello?.accepts?.includes("peer-gone")) {
64610
+ try {
64611
+ extensionWs.send(JSON.stringify({ type: "peer-gone", mcpId: peerMcpId }));
64612
+ } catch {
64613
+ }
64614
+ }
64615
+ }
64151
64616
  }
64152
64617
  });
64153
64618
  });
64154
64619
  return {
64155
- close: () => new Promise((resolve2) => {
64620
+ close: () => new Promise((resolve5) => {
64156
64621
  for (const client of wss.clients) {
64157
64622
  try {
64158
64623
  client.terminate();
@@ -64160,7 +64625,7 @@ async function startHost(opts) {
64160
64625
  }
64161
64626
  }
64162
64627
  wss.close(() => {
64163
- opts.httpServer.close(() => resolve2());
64628
+ opts.httpServer.close(() => resolve5());
64164
64629
  });
64165
64630
  }),
64166
64631
  sendOwnInner: async (inner) => {
@@ -64190,26 +64655,42 @@ async function startHost(opts) {
64190
64655
  }
64191
64656
 
64192
64657
  // node_modules/@fetchproxy/server/dist/peer.js
64658
+ var PEER_DIAL_TIMEOUT_MS = 5e3;
64193
64659
  var enc5 = new TextEncoder();
64660
+ function notSentError() {
64661
+ 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.");
64662
+ }
64194
64663
  async function startPeer(opts) {
64195
64664
  const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`, {
64196
64665
  maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES
64197
64666
  });
64198
- await new Promise((resolve2, reject) => {
64667
+ const dialTimeoutMs = opts.dialTimeoutMs ?? PEER_DIAL_TIMEOUT_MS;
64668
+ await new Promise((resolve5, reject) => {
64669
+ let timer;
64199
64670
  const onOpen = () => {
64200
64671
  cleanup();
64201
- resolve2();
64672
+ resolve5();
64202
64673
  };
64203
64674
  const onError = (e) => {
64204
64675
  cleanup();
64205
64676
  reject(e);
64206
64677
  };
64207
64678
  const cleanup = () => {
64679
+ if (timer !== void 0)
64680
+ clearTimeout(timer);
64208
64681
  ws.off("open", onOpen);
64209
64682
  ws.off("error", onError);
64210
64683
  };
64211
64684
  ws.once("open", onOpen);
64212
64685
  ws.once("error", onError);
64686
+ if (dialTimeoutMs > 0) {
64687
+ timer = setTimeout(() => {
64688
+ cleanup();
64689
+ ws.on("error", () => void 0);
64690
+ ws.terminate();
64691
+ 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.`));
64692
+ }, dialTimeoutMs);
64693
+ }
64213
64694
  });
64214
64695
  ws.on("error", (e) => {
64215
64696
  console.warn(`[fetchproxy] peer: socket error: ${String(e)}`);
@@ -64228,6 +64709,7 @@ async function startPeer(opts) {
64228
64709
  captureHeaders: opts.captureHeaders,
64229
64710
  indexedDbScopes: opts.indexedDbScopes,
64230
64711
  domSelectors: opts.domSelectors,
64712
+ domListSelectors: opts.domListSelectors,
64231
64713
  localStoragePointers: opts.localStoragePointers,
64232
64714
  sessionStoragePointers: opts.sessionStoragePointers,
64233
64715
  graphqlOps: opts.graphqlOps,
@@ -64251,6 +64733,7 @@ async function startPeer(opts) {
64251
64733
  const renegotiateListeners = [];
64252
64734
  const pendingPairListeners = [];
64253
64735
  const closeListeners = [];
64736
+ const extensionDisconnectListeners = [];
64254
64737
  let session = null;
64255
64738
  let sessionEphemeral = null;
64256
64739
  function dropSessionEphemeral() {
@@ -64270,10 +64753,24 @@ async function startPeer(opts) {
64270
64753
  let warnedUnverifiablePairCode = false;
64271
64754
  let resolveFirstReady;
64272
64755
  let rejectFirstReady;
64273
- const sessionPromise = new Promise((resolve2, reject) => {
64274
- resolveFirstReady = resolve2;
64275
- rejectFirstReady = reject;
64276
- });
64756
+ let sessionPromise;
64757
+ let sessionPromiseSettled = false;
64758
+ function resetSessionPromise() {
64759
+ sessionPromiseSettled = false;
64760
+ sessionPromise = new Promise((resolve5, reject) => {
64761
+ resolveFirstReady = (st) => {
64762
+ sessionPromiseSettled = true;
64763
+ resolve5(st);
64764
+ };
64765
+ rejectFirstReady = (e) => {
64766
+ sessionPromiseSettled = true;
64767
+ reject(e);
64768
+ };
64769
+ });
64770
+ sessionPromise.catch(() => {
64771
+ });
64772
+ }
64773
+ resetSessionPromise();
64277
64774
  let extensionHello = null;
64278
64775
  let extensionGone = false;
64279
64776
  let cachedPin = void 0;
@@ -64370,6 +64867,12 @@ async function startPeer(opts) {
64370
64867
  extensionGone = true;
64371
64868
  dropSessionEphemeral();
64372
64869
  pendingPairCode = null;
64870
+ if (session !== null) {
64871
+ session = null;
64872
+ if (sessionPromiseSettled)
64873
+ resetSessionPromise();
64874
+ }
64875
+ extensionDisconnectListeners.forEach((cb) => cb());
64373
64876
  return;
64374
64877
  }
64375
64878
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
@@ -64486,19 +64989,30 @@ async function startPeer(opts) {
64486
64989
  }
64487
64990
  closeListeners.forEach((cb) => cb());
64488
64991
  });
64489
- sessionPromise.catch(() => {
64490
- });
64491
64992
  const handle = {
64492
64993
  ws,
64493
- session: sessionPromise,
64994
+ // A getter: `extension-disconnected` replaces the promise (B-BUG-5).
64995
+ get session() {
64996
+ return sessionPromise;
64997
+ },
64494
64998
  sendInner: async (inner) => {
64495
- await awaitSessionReady(sessionPromise, {
64496
- mcpId: opts.mcpId,
64497
- pendingPairCode: () => pendingPairCode
64498
- });
64999
+ try {
65000
+ await awaitSessionReady(sessionPromise, {
65001
+ mcpId: opts.mcpId,
65002
+ pendingPairCode: () => pendingPairCode
65003
+ });
65004
+ } catch (e) {
65005
+ if (ws.readyState !== import_websocket.default.OPEN)
65006
+ throw notSentError();
65007
+ throw e;
65008
+ }
64499
65009
  const s = session;
65010
+ if (s === null)
65011
+ throw new Error("peer: extension disconnected");
64500
65012
  const plaintext = encodeOutboundInnerFrame(opts.mcpId, inner);
64501
65013
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), plaintext, "s2e");
65014
+ if (ws.readyState !== import_websocket.default.OPEN)
65015
+ throw notSentError();
64502
65016
  ws.send(JSON.stringify(sealed));
64503
65017
  },
64504
65018
  onInner: (cb) => {
@@ -64516,6 +65030,9 @@ async function startPeer(opts) {
64516
65030
  onClose: (cb) => {
64517
65031
  closeListeners.push(cb);
64518
65032
  },
65033
+ onExtensionDisconnect: (cb) => {
65034
+ extensionDisconnectListeners.push(cb);
65035
+ },
64519
65036
  close: () => ws.close()
64520
65037
  };
64521
65038
  return handle;
@@ -64523,15 +65040,15 @@ async function startPeer(opts) {
64523
65040
 
64524
65041
  // node_modules/@fetchproxy/server/dist/error-kind.js
64525
65042
  function classifyFetchError(error62) {
65043
+ if (/^(in-page )?fetch threw:/.test(error62)) {
65044
+ return "tab_fetch_failed";
65045
+ }
64526
65046
  if (/Could not establish connection/i.test(error62) || /Receiving end does not exist/i.test(error62)) {
64527
65047
  return "content_script_unreachable";
64528
65048
  }
64529
65049
  if (/^tab fetch failed:/.test(error62)) {
64530
65050
  return "tab_fetch_failed";
64531
65051
  }
64532
- if (/^(in-page )?fetch threw:/.test(error62)) {
64533
- return "tab_fetch_failed";
64534
- }
64535
65052
  if (/^no tab matching /.test(error62)) {
64536
65053
  return "no_tab";
64537
65054
  }
@@ -64723,6 +65240,17 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
64723
65240
  * the message says which number that was and where it came from.
64724
65241
  */
64725
65242
  requestedTimeoutMs;
65243
+ /**
65244
+ * B-BUG-1: whether re-sending this request is safe. A timeout only means
65245
+ * the reply did not arrive in time — the extension may already have run the
65246
+ * request in the tab — so this is `false` for a non-idempotent `fetch`
65247
+ * (anything but GET/HEAD/OPTIONS) the caller did not mark `retryOnTimeout`,
65248
+ * for `download` and `writeCookies`, and for `graphqlQuery` unless the
65249
+ * caller marked it `retryOnTimeout` (a declared operation may be a
65250
+ * mutation). `retryOnceOnTimeout` honours it. Defaults to `true` (the read
65251
+ * verbs).
65252
+ */
65253
+ retrySafe;
64726
65254
  constructor(args) {
64727
65255
  const graceMs = args.graceMs ?? VERB_DEADLINE_GRACE_MS;
64728
65256
  const callerChose = args.requestedTimeoutMs !== void 0 && args.timeoutMs === args.requestedTimeoutMs + graceMs;
@@ -64735,6 +65263,7 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
64735
65263
  this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
64736
65264
  this.retryAttempted = args.retryAttempted ?? false;
64737
65265
  this.requestedTimeoutMs = args.requestedTimeoutMs ?? null;
65266
+ this.retrySafe = args.retrySafe ?? true;
64738
65267
  }
64739
65268
  };
64740
65269
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
@@ -64774,6 +65303,32 @@ function assertUrlInDomains(field, url4, domains) {
64774
65303
  throw new Error(`FetchproxyServer: ${field} host "${host}" is outside declared domains [${declared}] \u2014 must be one of them or a subdomain`);
64775
65304
  }
64776
65305
  var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
65306
+ function parseJsonBody(response) {
65307
+ if (response.status === 204 || response.body.trim() === "")
65308
+ return null;
65309
+ return JSON.parse(response.body);
65310
+ }
65311
+ var TIMEOUT_RETRY_SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
65312
+ function isRetrySafeOnTimeout(method, retryOnTimeout) {
65313
+ if (retryOnTimeout !== void 0)
65314
+ return retryOnTimeout;
65315
+ return TIMEOUT_RETRY_SAFE_METHODS.has(method.toUpperCase());
65316
+ }
65317
+ function isResendableRequest(inner) {
65318
+ switch (inner.op) {
65319
+ case "fetch":
65320
+ return isRetrySafeOnTimeout(inner.init.method);
65321
+ case "write_cookies":
65322
+ case "download":
65323
+ // A declared GraphQL operation may be a mutation; the server holds only
65324
+ // its name, so it cannot tell. `graphqlQuery({ retryOnTimeout: true })`
65325
+ // passes `resendable` explicitly for a known query.
65326
+ case "graphql_query":
65327
+ return false;
65328
+ default:
65329
+ return true;
65330
+ }
65331
+ }
64777
65332
  var FetchproxyServer = class {
64778
65333
  /**
64779
65334
  * Bridge role. `null` until the first verb call (or an explicit
@@ -64822,12 +65377,22 @@ var FetchproxyServer = class {
64822
65377
  pendingRedirect = /* @__PURE__ */ new Map();
64823
65378
  // 0.4.0+: read_indexed_db awaiters resolve a JSON-typed values map.
64824
65379
  pendingIdb = /* @__PURE__ */ new Map();
65380
+ // 3.1.0+: read_dom_list awaiters resolve an array of field-value rows —
65381
+ // a different shape from `pendingStorage`'s single map, so its own map.
65382
+ pendingDomList = /* @__PURE__ */ new Map();
64825
65383
  // download awaiters resolve the saved-file metadata (path + size + mime).
64826
65384
  pendingDownload = /* @__PURE__ */ new Map();
64827
65385
  // 1.x+: graphql_query awaiters resolve the GraphQL `data` object. Its
64828
65386
  // shape is operation-specific, so the awaiter resolves `unknown` and the
64829
65387
  // caller narrows.
64830
65388
  pendingGraphql = /* @__PURE__ */ new Map();
65389
+ /**
65390
+ * B-BUG-6: the frames of in-flight requests that are safe to send again
65391
+ * if the host that relayed them exits before replying (see
65392
+ * `recoverFromHostLoss`). Keyed by request id; an entry outlives its
65393
+ * request only until the next prune or response.
65394
+ */
65395
+ resendable = /* @__PURE__ */ new Map();
64831
65396
  mcpId = null;
64832
65397
  identity = null;
64833
65398
  // 0.5.3+: in-flight role-election / handle-start promise. Set the
@@ -64873,7 +65438,7 @@ var FetchproxyServer = class {
64873
65438
  }
64874
65439
  for (const c of opts.capabilities) {
64875
65440
  if (!KNOWN_CAPABILITIES.has(c)) {
64876
- throw new Error(`FetchproxyServer: unknown capability ${JSON.stringify(c)} \u2014 known values: ["fetch", "read_cookies"]`);
65441
+ throw new Error(`FetchproxyServer: unknown capability ${JSON.stringify(c)} \u2014 known values: ${JSON.stringify([...KNOWN_CAPABILITIES])}`);
64877
65442
  }
64878
65443
  }
64879
65444
  capabilities = [...opts.capabilities];
@@ -64920,6 +65485,16 @@ var FetchproxyServer = class {
64920
65485
  selector: d.selector,
64921
65486
  ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
64922
65487
  })),
65488
+ domListSelectors: (opts.domListSelectors ?? []).map((d) => ({
65489
+ name: d.name,
65490
+ itemSelector: d.itemSelector,
65491
+ fields: d.fields.map((f) => ({
65492
+ name: f.name,
65493
+ ...f.selector !== void 0 ? { selector: f.selector } : {},
65494
+ ...f.attribute !== void 0 ? { attribute: f.attribute } : {}
65495
+ })),
65496
+ ...d.maxItems !== void 0 ? { maxItems: d.maxItems } : {}
65497
+ })),
64923
65498
  graphqlOps: (opts.graphqlOps ?? []).map((d) => ({
64924
65499
  name: d.name,
64925
65500
  operationName: d.operationName
@@ -65049,6 +65624,7 @@ var FetchproxyServer = class {
65049
65624
  ownLocalStoragePointers: this.opts.localStoragePointers,
65050
65625
  ownSessionStoragePointers: this.opts.sessionStoragePointers,
65051
65626
  ownDomSelectors: this.opts.domSelectors,
65627
+ ownDomListSelectors: this.opts.domListSelectors,
65052
65628
  ownGraphqlOps: this.opts.graphqlOps,
65053
65629
  onPairCode: this.opts.onPairCode,
65054
65630
  extensionTrust: this.extensionTrust()
@@ -65080,6 +65656,7 @@ var FetchproxyServer = class {
65080
65656
  localStoragePointers: this.opts.localStoragePointers,
65081
65657
  sessionStoragePointers: this.opts.sessionStoragePointers,
65082
65658
  domSelectors: this.opts.domSelectors,
65659
+ domListSelectors: this.opts.domListSelectors,
65083
65660
  graphqlOps: this.opts.graphqlOps,
65084
65661
  extensionTrust: this.extensionTrust(),
65085
65662
  requireExtensionIdentity: this.opts.requireExtensionIdentity
@@ -65089,6 +65666,10 @@ var FetchproxyServer = class {
65089
65666
  this.stopKeepalive();
65090
65667
  this.rejectAllPending();
65091
65668
  });
65669
+ this.peerHandle.onExtensionDisconnect(() => {
65670
+ this.stopKeepalive();
65671
+ this.rejectAllPending();
65672
+ });
65092
65673
  this.peerHandle.onPendingPair((code) => {
65093
65674
  this.rejectAllPending(this.pairingErrorMessage(code));
65094
65675
  });
@@ -65100,9 +65681,9 @@ var FetchproxyServer = class {
65100
65681
  if (this.closing || this.peerHandle === null)
65101
65682
  return;
65102
65683
  this.stopKeepalive();
65103
- this.rejectAllPending();
65104
65684
  this.peerHandle = null;
65105
65685
  this.role = null;
65686
+ void this.recoverFromHostLoss();
65106
65687
  });
65107
65688
  }
65108
65689
  }
@@ -65123,7 +65704,7 @@ var FetchproxyServer = class {
65123
65704
  * only when the bridge itself failed (no signed-in tab, extension
65124
65705
  * offline, etc.).
65125
65706
  */
65126
- async fetch(init) {
65707
+ async fetch(init, fetchOpts = {}) {
65127
65708
  await this.ensureConnected();
65128
65709
  const pendingCode = this.currentPendingPairCode();
65129
65710
  if (pendingCode !== null) {
@@ -65135,16 +65716,17 @@ var FetchproxyServer = class {
65135
65716
  retryAttempted: false
65136
65717
  };
65137
65718
  }
65138
- const first = await this._fetchOnceWithTimeout(init);
65719
+ const first = await this._fetchOnceWithTimeout(init, fetchOpts);
65139
65720
  const reviveMs = this.opts.bridgeReviveDelayMs;
65140
65721
  const isColdStartSymptom = !first.ok && (first.kind === "content_script_unreachable" || first.kind === "timeout");
65141
65722
  if (isColdStartSymptom) {
65142
65723
  this.lastEvictionDetectedAt = Date.now();
65143
65724
  }
65144
- if (isColdStartSymptom && reviveMs !== void 0 && reviveMs > 0) {
65725
+ const retryable = isColdStartSymptom && (first.kind === "content_script_unreachable" || isRetrySafeOnTimeout(init.method, fetchOpts.retryOnTimeout));
65726
+ if (retryable && reviveMs !== void 0 && reviveMs > 0) {
65145
65727
  this.lazyReviveAttempts += 1;
65146
65728
  await new Promise((r) => setTimeout(r, reviveMs));
65147
- const second = await this._fetchOnceWithTimeout(init);
65729
+ const second = await this._fetchOnceWithTimeout(init, fetchOpts);
65148
65730
  if (second.ok) {
65149
65731
  this.lazyReviveSuccesses += 1;
65150
65732
  this.recordSuccess();
@@ -65293,13 +65875,16 @@ var FetchproxyServer = class {
65293
65875
  * one of the op maps — request ids are unique) so it doesn't leak until the
65294
65876
  * server closes, then rethrow.
65295
65877
  */
65296
- async sendInnerFrame(inner) {
65878
+ async sendInnerFrame(inner, opts = {}) {
65297
65879
  try {
65298
65880
  if (this.hostHandle) {
65299
65881
  await this.hostHandle.sendOwnInner(inner);
65300
65882
  } else if (this.peerHandle) {
65301
65883
  await this.peerHandle.sendInner(inner);
65302
65884
  }
65885
+ if (inner.type === "request" && (opts.resendable ?? isResendableRequest(inner))) {
65886
+ this.noteResendable(inner);
65887
+ }
65303
65888
  } catch (err) {
65304
65889
  if ("id" in inner && typeof inner.id === "number") {
65305
65890
  const { id } = inner;
@@ -65311,6 +65896,7 @@ var FetchproxyServer = class {
65311
65896
  this.pendingRedirect.delete(id);
65312
65897
  this.pendingDownload.delete(id);
65313
65898
  this.pendingIdb.delete(id);
65899
+ this.pendingDomList.delete(id);
65314
65900
  this.pendingGraphql.delete(id);
65315
65901
  }
65316
65902
  throw err;
@@ -65321,13 +65907,25 @@ var FetchproxyServer = class {
65321
65907
  * On timeout returns the `{ok:false, kind:'timeout'}` envelope —
65322
65908
  * the throwing surface is the convenience methods.
65323
65909
  */
65324
- async _fetchOnceWithTimeout(init) {
65910
+ async _fetchOnceWithTimeout(init, fetchOpts = {}) {
65325
65911
  const id = this.nextRequestId++;
65326
65912
  const inner = { type: "request", id, op: "fetch", init };
65327
- const pending = this.guardPending(new Promise((resolve2) => {
65328
- this.pending.set(id, resolve2);
65913
+ const pending = this.guardPending(new Promise((resolve5) => {
65914
+ this.pending.set(id, resolve5);
65329
65915
  }));
65330
- await this.sendInnerFrame(inner);
65916
+ try {
65917
+ await this.sendInnerFrame(inner, {
65918
+ resendable: isRetrySafeOnTimeout(init.method, fetchOpts.retryOnTimeout)
65919
+ });
65920
+ } catch (err) {
65921
+ return {
65922
+ ok: false,
65923
+ error: err instanceof Error ? err.message : String(err),
65924
+ kind: "session_not_ready",
65925
+ retryAttempted: false,
65926
+ cause: err
65927
+ };
65928
+ }
65331
65929
  const timeoutMs = this.opts.fetchTimeoutMs;
65332
65930
  if (timeoutMs === void 0 || timeoutMs <= 0)
65333
65931
  return pending;
@@ -65336,12 +65934,12 @@ var FetchproxyServer = class {
65336
65934
  try {
65337
65935
  return await Promise.race([
65338
65936
  pending,
65339
- new Promise((resolve2) => {
65937
+ new Promise((resolve5) => {
65340
65938
  timer = setTimeout(() => {
65341
65939
  this.pending.delete(id);
65342
65940
  const elapsedMs = Date.now() - start;
65343
65941
  const error62 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
65344
- resolve2({
65942
+ resolve5({
65345
65943
  ok: false,
65346
65944
  error: error62,
65347
65945
  kind: "timeout",
@@ -65401,7 +65999,7 @@ var FetchproxyServer = class {
65401
65999
  });
65402
66000
  return pending;
65403
66001
  }
65404
- async _withVerbTimeout(pending, pendingMap, id, url4, requestedTimeoutMs) {
66002
+ async _withVerbTimeout(pending, pendingMap, id, url4, requestedTimeoutMs, retrySafe = true) {
65405
66003
  const transportMs = this.opts.fetchTimeoutMs;
65406
66004
  if (transportMs === void 0 || transportMs <= 0)
65407
66005
  return pending;
@@ -65422,6 +66020,7 @@ var FetchproxyServer = class {
65422
66020
  port: this.opts.port,
65423
66021
  elapsedMs: Date.now() - start,
65424
66022
  retryAttempted: false,
66023
+ retrySafe,
65425
66024
  graceMs,
65426
66025
  ...requestedTimeoutMs !== void 0 ? { requestedTimeoutMs } : {}
65427
66026
  }));
@@ -65439,7 +66038,7 @@ var FetchproxyServer = class {
65439
66038
  * logic re-implemented inline) `captureRequestHeader()` agree on what
65440
66039
  * to throw.
65441
66040
  */
65442
- _typedErrorFor(result, url4, op, retryAttempted) {
66041
+ _typedErrorFor(result, url4, op, retryAttempted, retrySafe = true) {
65443
66042
  if (result.kind === "timeout") {
65444
66043
  return new FetchproxyTimeoutError({
65445
66044
  url: url4,
@@ -65447,7 +66046,8 @@ var FetchproxyServer = class {
65447
66046
  role: this.role,
65448
66047
  port: this.opts.port,
65449
66048
  elapsedMs: result.elapsedMs,
65450
- retryAttempted
66049
+ retryAttempted,
66050
+ retrySafe
65451
66051
  });
65452
66052
  }
65453
66053
  if (result.kind === "content_script_unreachable") {
@@ -65483,9 +66083,9 @@ var FetchproxyServer = class {
65483
66083
  if (opts.subdomain !== void 0)
65484
66084
  assertSubdomainLabel(opts.subdomain);
65485
66085
  const baseDomain = this.resolveBaseDomain(opts.domain);
65486
- const isAbsolute3 = path.startsWith("http://") || path.startsWith("https://");
66086
+ const isAbsolute6 = path.startsWith("http://") || path.startsWith("https://");
65487
66087
  let host;
65488
- if (isAbsolute3) {
66088
+ if (isAbsolute6) {
65489
66089
  try {
65490
66090
  host = new URL(path).host;
65491
66091
  } catch {
@@ -65494,7 +66094,7 @@ var FetchproxyServer = class {
65494
66094
  } else {
65495
66095
  host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
65496
66096
  }
65497
- const url4 = isAbsolute3 ? path : `https://${host}${path}`;
66097
+ const url4 = isAbsolute6 ? path : `https://${host}${path}`;
65498
66098
  assertUrlInDomains("request url", url4, this.opts.domains);
65499
66099
  let tabUrl = `https://${host}/`;
65500
66100
  if (opts.viaTab !== void 0) {
@@ -65521,9 +66121,12 @@ var FetchproxyServer = class {
65521
66121
  // every version before this option existed.
65522
66122
  ...opts.credentials === "omit" ? { credentials: "omit" } : {}
65523
66123
  };
65524
- const result = await this.fetch(init);
66124
+ const result = await this.fetch(init, opts.retryOnTimeout !== void 0 ? { retryOnTimeout: opts.retryOnTimeout } : {});
65525
66125
  if (!result.ok) {
65526
- throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false);
66126
+ if (result.kind === "session_not_ready" && result.cause instanceof Error) {
66127
+ throw result.cause;
66128
+ }
66129
+ throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false, isRetrySafeOnTimeout(method, opts.retryOnTimeout));
65527
66130
  }
65528
66131
  const response = {
65529
66132
  status: result.status,
@@ -65563,17 +66166,21 @@ var FetchproxyServer = class {
65563
66166
  * GET a path and parse the response body as JSON. Throws
65564
66167
  * `FetchproxyHttpError` if the status is outside the default 2xx
65565
66168
  * happy-path set (`[200, 201, 202, 204]`); pass a custom
65566
- * `expectStatus` to override.
66169
+ * `expectStatus` to override. A 204 or an empty/whitespace body
66170
+ * resolves to `null` (the declared `T` does not say so, to keep
66171
+ * existing call sites compiling — include `| null` in `T` if the
66172
+ * endpoint can answer empty).
65567
66173
  */
65568
66174
  async getJson(path, opts = {}) {
65569
66175
  const response = await this.get(path, this.applyJsonDefaults(opts));
65570
- return JSON.parse(response.body);
66176
+ return parseJsonBody(response);
65571
66177
  }
65572
66178
  /**
65573
66179
  * POST a JSON body and parse the response body as JSON. The body is
65574
66180
  * `JSON.stringify`'d; `Content-Type: application/json` is set unless
65575
66181
  * the caller already provided one. Defaults `expectStatus` to the 2xx
65576
- * happy-path set.
66182
+ * happy-path set. A 204 or an empty/whitespace body resolves to `null`
66183
+ * rather than throwing after the write already happened (see `getJson`).
65577
66184
  */
65578
66185
  async postJson(path, body, opts = {}) {
65579
66186
  const headers = { ...opts.headers ?? {} };
@@ -65585,7 +66192,7 @@ var FetchproxyServer = class {
65585
66192
  headers,
65586
66193
  body: body === void 0 ? void 0 : JSON.stringify(body)
65587
66194
  });
65588
- return JSON.parse(response.body);
66195
+ return parseJsonBody(response);
65589
66196
  }
65590
66197
  /**
65591
66198
  * GET a path and return the response body as a string. Throws
@@ -65606,7 +66213,8 @@ var FetchproxyServer = class {
65606
66213
  * - adds `Content-Type: application/json` only for a non-GET request
65607
66214
  * that carries a `body` (and only if the caller didn't set one);
65608
66215
  * - `JSON.stringify`s the body (GET / no-body sends nothing);
65609
- * - treats a `204` or an empty body as `data: null` (no parse);
66216
+ * - treats a `204`, an empty body, or a whitespace-only body as
66217
+ * `data: null` (no parse), the same rule as getJson/postJson;
65610
66218
  * - otherwise `JSON.parse`s the body.
65611
66219
  *
65612
66220
  * Scope is serialization + header defaults + 204-handling +
@@ -65620,6 +66228,11 @@ var FetchproxyServer = class {
65620
66228
  * Bridge-level failures (no signed-in tab, SW down, timeout) still
65621
66229
  * throw the typed errors via `request()`, exactly like the verb
65622
66230
  * helpers — only successful round-trips (any HTTP status) return.
66231
+ *
66232
+ * `retryOnTimeout` is `RequestOpts.retryOnTimeout`: a timed-out POST is
66233
+ * NOT re-sent unless the caller says it is safe to repeat. A read-only POST
66234
+ * (a search, a GraphQL query) should pass `true` to keep the cold-start
66235
+ * timeout retry (#90).
65623
66236
  */
65624
66237
  async requestJson(method, path, opts = {}) {
65625
66238
  const isGet = method.toUpperCase() === "GET";
@@ -65633,7 +66246,8 @@ var FetchproxyServer = class {
65633
66246
  headers,
65634
66247
  body: sendBody ? JSON.stringify(opts.body) : void 0,
65635
66248
  ...opts.subdomain !== void 0 ? { subdomain: opts.subdomain } : {},
65636
- ...opts.domain !== void 0 ? { domain: opts.domain } : {}
66249
+ ...opts.domain !== void 0 ? { domain: opts.domain } : {},
66250
+ ...opts.retryOnTimeout !== void 0 ? { retryOnTimeout: opts.retryOnTimeout } : {}
65637
66251
  });
65638
66252
  const result = {
65639
66253
  ok: true,
@@ -65641,12 +66255,9 @@ var FetchproxyServer = class {
65641
66255
  url: response.url,
65642
66256
  body: response.body
65643
66257
  };
65644
- if (response.status === 204 || response.body === "") {
65645
- return { data: null, result };
65646
- }
65647
66258
  let data;
65648
66259
  try {
65649
- data = JSON.parse(response.body);
66260
+ data = parseJsonBody(response);
65650
66261
  } catch (e) {
65651
66262
  throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
65652
66263
  }
@@ -65703,18 +66314,19 @@ var FetchproxyServer = class {
65703
66314
  };
65704
66315
  }
65705
66316
  /**
65706
- * Snapshot the user's non-HttpOnly cookies for the chosen domain.
66317
+ * Read the user's cookies for the chosen domain.
65707
66318
  *
65708
66319
  * Requires `'read_cookies'` in `FetchproxyServerOpts.capabilities`.
65709
66320
  * Throws a developer-facing `Error` at the call site if the MCP did
65710
66321
  * not declare the capability — this is a programming mistake, not a
65711
66322
  * runtime condition.
65712
66323
  *
65713
- * The returned string is the raw `document.cookie` value (semicolon-
65714
- * separated `k=v` pairs). HttpOnly cookies are NOT visible to page JS
65715
- * and are therefore not included; the underlying threat model assumes
65716
- * the cookies that matter for the auth bootstrap (session tokens, csrf
65717
- * cookies that the page itself reads) are non-HttpOnly.
66324
+ * The returned string is semicolon-separated `k=v` pairs. With `keys`
66325
+ * (each in the declared `cookieKeys`), the extension reads each name via
66326
+ * `chrome.cookies.get`, which INCLUDES HttpOnly cookies — the login
66327
+ * session cookie among them; that is what cookie-session MCPs use it
66328
+ * for. Without `keys` (legacy), it is the tab's `document.cookie`, which
66329
+ * omits HttpOnly cookies. See docs/SECURITY.md §T-cookie-exfil.
65718
66330
  *
65719
66331
  * Throws `FetchproxyProtocolError` if the bridge could not deliver
65720
66332
  * the request (no signed-in tab, extension offline, etc.).
@@ -65747,8 +66359,8 @@ var FetchproxyServer = class {
65747
66359
  const tabUrl = `https://${host}/`;
65748
66360
  inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
65749
66361
  }
65750
- const pending = this.guardPending(new Promise((resolve2) => {
65751
- this.pendingReadCookies.set(id, resolve2);
66362
+ const pending = this.guardPending(new Promise((resolve5) => {
66363
+ this.pendingReadCookies.set(id, resolve5);
65752
66364
  }));
65753
66365
  await this.sendInnerFrame(inner);
65754
66366
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
@@ -65806,11 +66418,19 @@ var FetchproxyServer = class {
65806
66418
  ...cookiePath !== void 0 ? { path: cookiePath } : {}
65807
66419
  }
65808
66420
  };
65809
- const pending = this.guardPending(new Promise((resolve2, reject) => {
65810
- this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
66421
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66422
+ this.pendingWriteCookies.set(id, { resolve: resolve5, reject });
65811
66423
  }));
65812
66424
  await this.sendInnerFrame(inner);
65813
- return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
66425
+ return this._withVerbTimeout(
66426
+ pending,
66427
+ this.pendingWriteCookies,
66428
+ id,
66429
+ `https://${host}`,
66430
+ void 0,
66431
+ // A write: never re-sent after an ambiguous timeout.
66432
+ false
66433
+ );
65814
66434
  }
65815
66435
  /**
65816
66436
  * 0.3.0+: read declared localStorage keys from the user's signed-in
@@ -65869,8 +66489,8 @@ var FetchproxyServer = class {
65869
66489
  ...opts.pointers ? { pointers: { ...opts.pointers } } : {}
65870
66490
  }
65871
66491
  };
65872
- const pending = this.guardPending(new Promise((resolve2, reject) => {
65873
- this.pendingStorage.set(id, { resolve: resolve2, reject });
66492
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66493
+ this.pendingStorage.set(id, { resolve: resolve5, reject });
65874
66494
  }));
65875
66495
  await this.sendInnerFrame(inner);
65876
66496
  return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
@@ -65913,53 +66533,7 @@ var FetchproxyServer = class {
65913
66533
  throw new Error("FetchproxyServer.captureRequestHeader: pass both host AND headerName, or neither (which defaults to the single declared entry)");
65914
66534
  }
65915
66535
  const callOpts = { ...resolved2, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
65916
- try {
65917
- const result = await this._captureRequestHeaderOnce(callOpts);
65918
- this.recordSuccess();
65919
- return result;
65920
- } catch (err) {
65921
- const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
65922
- if (!swDown) {
65923
- this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
65924
- throw err;
65925
- }
65926
- this.lastEvictionDetectedAt = Date.now();
65927
- const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
65928
- if (reviveMs > 0) {
65929
- this.lazyReviveAttempts += 1;
65930
- await new Promise((r) => setTimeout(r, reviveMs));
65931
- try {
65932
- const result = await this._captureRequestHeaderOnce(callOpts);
65933
- this.lazyReviveSuccesses += 1;
65934
- this.recordSuccess();
65935
- return result;
65936
- } catch (retryErr) {
65937
- const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
65938
- if (!stillDown) {
65939
- this.recordFailure(`capture_request_header: ${retryErr.message ?? String(retryErr)}`);
65940
- throw retryErr;
65941
- }
65942
- this.recordFailure(`capture_request_header bridge-down: ${retryErr.message}`);
65943
- throw new FetchproxyBridgeDownError({
65944
- originalError: retryErr.message,
65945
- retryAttempted: true,
65946
- op: "capture_request_header",
65947
- url: `https://${resolved2.host}${resolved2.path ?? "/*"}`,
65948
- role: this.role,
65949
- port: this.opts.port
65950
- });
65951
- }
65952
- }
65953
- this.recordFailure(`capture_request_header bridge-down: ${err.message}`);
65954
- throw new FetchproxyBridgeDownError({
65955
- originalError: err.message,
65956
- retryAttempted: false,
65957
- op: "capture_request_header",
65958
- url: `https://${resolved2.host}${resolved2.path ?? "/*"}`,
65959
- role: this.role,
65960
- port: this.opts.port
65961
- });
65962
- }
66536
+ return this.withLazyRevive("capture_request_header", `https://${resolved2.host}${resolved2.path ?? "/*"}`, () => this._captureRequestHeaderOnce(callOpts));
65963
66537
  }
65964
66538
  async _captureRequestHeaderOnce(opts) {
65965
66539
  const id = this.nextRequestId++;
@@ -65974,8 +66548,8 @@ var FetchproxyServer = class {
65974
66548
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
65975
66549
  }
65976
66550
  };
65977
- const pending = this.guardPending(new Promise((resolve2, reject) => {
65978
- this.pendingCapture.set(id, { resolve: resolve2, reject });
66551
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66552
+ this.pendingCapture.set(id, { resolve: resolve5, reject });
65979
66553
  }));
65980
66554
  await this.sendInnerFrame(inner);
65981
66555
  return this._withVerbTimeout(
@@ -66008,53 +66582,7 @@ var FetchproxyServer = class {
66008
66582
  }
66009
66583
  await this.ensureConnected();
66010
66584
  this.throwIfPendingPair();
66011
- try {
66012
- const result = await this._captureRedirectOnce(opts);
66013
- this.recordSuccess();
66014
- return result;
66015
- } catch (err) {
66016
- const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
66017
- if (!swDown) {
66018
- this.recordFailure(`capture_redirect: ${err.message ?? String(err)}`);
66019
- throw err;
66020
- }
66021
- this.lastEvictionDetectedAt = Date.now();
66022
- const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
66023
- if (reviveMs > 0) {
66024
- this.lazyReviveAttempts += 1;
66025
- await new Promise((r) => setTimeout(r, reviveMs));
66026
- try {
66027
- const result = await this._captureRedirectOnce(opts);
66028
- this.lazyReviveSuccesses += 1;
66029
- this.recordSuccess();
66030
- return result;
66031
- } catch (retryErr) {
66032
- const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
66033
- if (!stillDown) {
66034
- this.recordFailure(`capture_redirect: ${retryErr.message ?? String(retryErr)}`);
66035
- throw retryErr;
66036
- }
66037
- this.recordFailure(`capture_redirect bridge-down: ${retryErr.message}`);
66038
- throw new FetchproxyBridgeDownError({
66039
- originalError: retryErr.message,
66040
- retryAttempted: true,
66041
- op: "capture_redirect",
66042
- url: `https://${opts.host}${opts.path ?? "/*"}`,
66043
- role: this.role,
66044
- port: this.opts.port
66045
- });
66046
- }
66047
- }
66048
- this.recordFailure(`capture_redirect bridge-down: ${err.message}`);
66049
- throw new FetchproxyBridgeDownError({
66050
- originalError: err.message,
66051
- retryAttempted: false,
66052
- op: "capture_redirect",
66053
- url: `https://${opts.host}${opts.path ?? "/*"}`,
66054
- role: this.role,
66055
- port: this.opts.port
66056
- });
66057
- }
66585
+ return this.withLazyRevive("capture_redirect", `https://${opts.host}${opts.path ?? "/*"}`, () => this._captureRedirectOnce(opts));
66058
66586
  }
66059
66587
  async _captureRedirectOnce(opts) {
66060
66588
  const id = this.nextRequestId++;
@@ -66068,8 +66596,8 @@ var FetchproxyServer = class {
66068
66596
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
66069
66597
  }
66070
66598
  };
66071
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66072
- this.pendingRedirect.set(id, { resolve: resolve2, reject });
66599
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66600
+ this.pendingRedirect.set(id, { resolve: resolve5, reject });
66073
66601
  }));
66074
66602
  await this.sendInnerFrame(inner);
66075
66603
  return this._withVerbTimeout(
@@ -66099,52 +66627,57 @@ var FetchproxyServer = class {
66099
66627
  assertUrlInDomains("download url", opts.url, this.opts.domains);
66100
66628
  await this.ensureConnected();
66101
66629
  this.throwIfPendingPair();
66630
+ return this.withLazyRevive("download", opts.url, () => this._downloadOnce(opts));
66631
+ }
66632
+ /**
66633
+ * B-QUAL-1: the lazy-revive policy for the throwing verbs
66634
+ * (`capture_request_header`, `capture_redirect`, `download`), in one place.
66635
+ * Runs `once`; on the SW-eviction symptom (`content_script_unreachable` —
66636
+ * the request provably never reached a tab, so re-sending is safe) stamps
66637
+ * the eviction counter, waits `bridgeReviveDelayMs` and retries once, then
66638
+ * surfaces a `FetchproxyBridgeDownError`. Every other error is recorded and
66639
+ * rethrown untouched. `fetch()` keeps its own envelope-shaped variant.
66640
+ */
66641
+ async withLazyRevive(op, url4, once) {
66642
+ const isSwDown = (e) => e instanceof FetchproxyProtocolError && classifyFetchError(e.message) === "content_script_unreachable";
66643
+ const bridgeDown = (e, retryAttempted) => {
66644
+ this.recordFailure(`${op} bridge-down: ${e.message}`);
66645
+ return new FetchproxyBridgeDownError({
66646
+ originalError: e.message,
66647
+ retryAttempted,
66648
+ op,
66649
+ url: url4,
66650
+ role: this.role,
66651
+ port: this.opts.port
66652
+ });
66653
+ };
66102
66654
  try {
66103
- const result = await this._downloadOnce(opts);
66655
+ const result = await once();
66104
66656
  this.recordSuccess();
66105
66657
  return result;
66106
66658
  } catch (err) {
66107
- const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
66108
- if (!swDown) {
66109
- this.recordFailure(`download: ${err.message ?? String(err)}`);
66659
+ if (!isSwDown(err)) {
66660
+ this.recordFailure(`${op}: ${err.message ?? String(err)}`);
66110
66661
  throw err;
66111
66662
  }
66112
66663
  this.lastEvictionDetectedAt = Date.now();
66113
66664
  const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
66114
- if (reviveMs > 0) {
66115
- this.lazyReviveAttempts += 1;
66116
- await new Promise((r) => setTimeout(r, reviveMs));
66117
- try {
66118
- const result = await this._downloadOnce(opts);
66119
- this.lazyReviveSuccesses += 1;
66120
- this.recordSuccess();
66121
- return result;
66122
- } catch (retryErr) {
66123
- const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
66124
- if (!stillDown) {
66125
- this.recordFailure(`download: ${retryErr.message ?? String(retryErr)}`);
66126
- throw retryErr;
66127
- }
66128
- this.recordFailure(`download bridge-down: ${retryErr.message}`);
66129
- throw new FetchproxyBridgeDownError({
66130
- originalError: retryErr.message,
66131
- retryAttempted: true,
66132
- op: "download",
66133
- url: opts.url,
66134
- role: this.role,
66135
- port: this.opts.port
66136
- });
66665
+ if (reviveMs <= 0)
66666
+ throw bridgeDown(err, false);
66667
+ this.lazyReviveAttempts += 1;
66668
+ await new Promise((r) => setTimeout(r, reviveMs));
66669
+ try {
66670
+ const result = await once();
66671
+ this.lazyReviveSuccesses += 1;
66672
+ this.recordSuccess();
66673
+ return result;
66674
+ } catch (retryErr) {
66675
+ if (!isSwDown(retryErr)) {
66676
+ this.recordFailure(`${op}: ${retryErr.message ?? String(retryErr)}`);
66677
+ throw retryErr;
66137
66678
  }
66679
+ throw bridgeDown(retryErr, true);
66138
66680
  }
66139
- this.recordFailure(`download bridge-down: ${err.message}`);
66140
- throw new FetchproxyBridgeDownError({
66141
- originalError: err.message,
66142
- retryAttempted: false,
66143
- op: "download",
66144
- url: opts.url,
66145
- role: this.role,
66146
- port: this.opts.port
66147
- });
66148
66681
  }
66149
66682
  }
66150
66683
  async _downloadOnce(opts) {
@@ -66159,8 +66692,8 @@ var FetchproxyServer = class {
66159
66692
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
66160
66693
  }
66161
66694
  };
66162
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66163
- this.pendingDownload.set(id, { resolve: resolve2, reject });
66695
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66696
+ this.pendingDownload.set(id, { resolve: resolve5, reject });
66164
66697
  }));
66165
66698
  await this.sendInnerFrame(inner);
66166
66699
  return this._withVerbTimeout(
@@ -66173,7 +66706,9 @@ var FetchproxyServer = class {
66173
66706
  // CAP and need the identical explanation; missing it then was the point
66174
66707
  // of #279 — "both verbs" was counted off the two call sites in view
66175
66708
  // rather than off the type. Three is still the number.
66176
- opts.timeoutMs
66709
+ opts.timeoutMs,
66710
+ // A second download saves a duplicate "file (1)".
66711
+ false
66177
66712
  );
66178
66713
  }
66179
66714
  /**
@@ -66218,8 +66753,8 @@ var FetchproxyServer = class {
66218
66753
  keys: [...opts.keys]
66219
66754
  }
66220
66755
  };
66221
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66222
- this.pendingIdb.set(id, { resolve: resolve2, reject });
66756
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66757
+ this.pendingIdb.set(id, { resolve: resolve5, reject });
66223
66758
  }));
66224
66759
  await this.sendInnerFrame(inner);
66225
66760
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
@@ -66258,12 +66793,58 @@ var FetchproxyServer = class {
66258
66793
  op: "read_dom",
66259
66794
  init: { origin, names: [...opts.names] }
66260
66795
  };
66261
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66262
- this.pendingStorage.set(id, { resolve: resolve2, reject });
66796
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66797
+ this.pendingStorage.set(id, { resolve: resolve5, reject });
66263
66798
  }));
66264
66799
  await this.sendInnerFrame(inner);
66265
66800
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
66266
66801
  }
66802
+ /**
66803
+ * 3.1.0+: read a declared REPEATED DOM structure from the user's
66804
+ * signed-in tab. Requires `'read_dom_list'` in capabilities AND `name` to
66805
+ * match a declared `domListSelectors` entry. The extension resolves the
66806
+ * entry's `itemSelector` via `document.querySelectorAll` in the matched
66807
+ * tab's DOM (isolated world), then resolves each declared field against
66808
+ * every matched item in turn — no page-JS execution, same as `readDom`.
66809
+ *
66810
+ * Returns one row per matched item, in document order, truncated to the
66811
+ * entry's declared `maxItems` when set. A field absent on a given item is
66812
+ * omitted from that item's row (the row itself is still returned). Throws
66813
+ * `FetchproxyProtocolError` on bridge failures and a plain `Error` on
66814
+ * developer mistakes (undeclared capability, undeclared name).
66815
+ *
66816
+ * Reads only what is CURRENTLY rendered — a virtualized or lazily-loaded
66817
+ * list returns only its visible rows. A caller that needs more scrolls
66818
+ * the list (out of band) and calls again.
66819
+ */
66820
+ async readDomList(opts) {
66821
+ if (!this.opts.capabilities.includes("read_dom_list")) {
66822
+ throw new Error('FetchproxyServer.readDomList(): MCP did not declare "read_dom_list" in capabilities');
66823
+ }
66824
+ await this.ensureConnected();
66825
+ this.throwIfPendingPair();
66826
+ if (typeof opts.name !== "string" || opts.name.length === 0) {
66827
+ throw new Error("FetchproxyServer.readDomList: opts.name must be a non-empty string");
66828
+ }
66829
+ this.assertScopeSubset([opts.name], this.opts.domListSelectors.map((d) => d.name), "domListSelectors");
66830
+ if (opts.subdomain !== void 0)
66831
+ assertSubdomainLabel(opts.subdomain);
66832
+ const baseDomain = this.resolveBaseDomain(opts.domain);
66833
+ const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
66834
+ const origin = `https://${host}`;
66835
+ const id = this.nextRequestId++;
66836
+ const inner = {
66837
+ type: "request",
66838
+ id,
66839
+ op: "read_dom_list",
66840
+ init: { origin, name: opts.name }
66841
+ };
66842
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66843
+ this.pendingDomList.set(id, { resolve: resolve5, reject });
66844
+ }));
66845
+ await this.sendInnerFrame(inner);
66846
+ return this._withVerbTimeout(pending, this.pendingDomList, id, origin);
66847
+ }
66267
66848
  /**
66268
66849
  * 1.x+: run a declared GraphQL operation through the page's own Apollo
66269
66850
  * client (`window.__APOLLO_CLIENT__`) in the signed-in tab's MAIN world.
@@ -66304,11 +66885,12 @@ var FetchproxyServer = class {
66304
66885
  ...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
66305
66886
  }
66306
66887
  };
66307
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66308
- this.pendingGraphql.set(id, { resolve: resolve2, reject });
66888
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66889
+ this.pendingGraphql.set(id, { resolve: resolve5, reject });
66309
66890
  }));
66310
- await this.sendInnerFrame(inner);
66311
- return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
66891
+ const retrySafe = opts.retryOnTimeout === true;
66892
+ await this.sendInnerFrame(inner, { resendable: retrySafe });
66893
+ return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name, void 0, retrySafe);
66312
66894
  }
66313
66895
  assertScopeSubset(requested, declared, label) {
66314
66896
  const undeclared = undeclaredKeys(requested, declared);
@@ -66345,6 +66927,7 @@ var FetchproxyServer = class {
66345
66927
  onInner(inner) {
66346
66928
  if (inner.type !== "response")
66347
66929
  return;
66930
+ this.resendable.delete(inner.id);
66348
66931
  this.lastExtensionMessageAt = Date.now();
66349
66932
  const fetchCb = this.pending.get(inner.id);
66350
66933
  if (fetchCb) {
@@ -66433,6 +67016,20 @@ var FetchproxyServer = class {
66433
67016
  }
66434
67017
  return;
66435
67018
  }
67019
+ const domListCb = this.pendingDomList.get(inner.id);
67020
+ if (domListCb) {
67021
+ this.pendingDomList.delete(inner.id);
67022
+ if (inner.ok) {
67023
+ if (inner.op === "read_dom_list" && inner.rows) {
67024
+ domListCb.resolve(inner.rows.map((row) => ({ ...row })));
67025
+ } else {
67026
+ domListCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_dom_list awaiter`));
67027
+ }
67028
+ } else {
67029
+ domListCb.reject(protocolErrorFrom(inner.error));
67030
+ }
67031
+ return;
67032
+ }
66436
67033
  const downloadCb = this.pendingDownload.get(inner.id);
66437
67034
  if (downloadCb) {
66438
67035
  this.pendingDownload.delete(inner.id);
@@ -66492,42 +67089,133 @@ var FetchproxyServer = class {
66492
67089
  }
66493
67090
  }
66494
67091
  }
66495
- rejectAllPending(reason = "extension disconnected") {
67092
+ rejectAllPending(reason = "extension disconnected", keep = /* @__PURE__ */ new Set()) {
67093
+ for (const id of this.allPendingIds()) {
67094
+ if (!keep.has(id))
67095
+ this.rejectPendingId(id, reason);
67096
+ }
67097
+ if (keep.size === 0)
67098
+ this.resendable.clear();
67099
+ }
67100
+ /** Every request id with a live awaiter, across all op maps. */
67101
+ allPendingIds() {
67102
+ const ids = [];
67103
+ for (const map2 of this.pendingMaps())
67104
+ for (const id of map2.keys())
67105
+ ids.push(id);
67106
+ return ids;
67107
+ }
67108
+ pendingMaps() {
67109
+ return [
67110
+ this.pending,
67111
+ this.pendingReadCookies,
67112
+ this.pendingStorage,
67113
+ this.pendingWriteCookies,
67114
+ this.pendingCapture,
67115
+ this.pendingRedirect,
67116
+ this.pendingIdb,
67117
+ this.pendingDomList,
67118
+ this.pendingDownload,
67119
+ this.pendingGraphql
67120
+ ];
67121
+ }
67122
+ isPendingId(id) {
67123
+ return this.pendingMaps().some((m) => m.has(id));
67124
+ }
67125
+ /** Fail the one awaiter registered under `id`, in its own map's shape. */
67126
+ rejectPendingId(id, reason) {
66496
67127
  const err = new FetchproxyProtocolError(reason);
66497
- for (const cb of this.pending.values()) {
66498
- cb({
67128
+ this.resendable.delete(id);
67129
+ const fetchCb = this.pending.get(id);
67130
+ if (fetchCb) {
67131
+ this.pending.delete(id);
67132
+ fetchCb({
66499
67133
  ok: false,
66500
67134
  error: err.message,
66501
67135
  kind: classifyFetchError(err.message),
66502
67136
  retryAttempted: false
66503
67137
  });
67138
+ return;
67139
+ }
67140
+ const cookieCb = this.pendingReadCookies.get(id);
67141
+ if (cookieCb) {
67142
+ this.pendingReadCookies.delete(id);
67143
+ cookieCb({ ok: false, error: err.message });
67144
+ return;
67145
+ }
67146
+ for (const map2 of [
67147
+ this.pendingStorage,
67148
+ this.pendingWriteCookies,
67149
+ this.pendingCapture,
67150
+ this.pendingRedirect,
67151
+ this.pendingIdb,
67152
+ this.pendingDomList,
67153
+ this.pendingDownload,
67154
+ this.pendingGraphql
67155
+ ]) {
67156
+ const entry = map2.get(id);
67157
+ if (entry) {
67158
+ map2.delete(id);
67159
+ entry.reject(err);
67160
+ return;
67161
+ }
67162
+ }
67163
+ }
67164
+ noteResendable(inner) {
67165
+ this.resendable.set(inner.id, inner);
67166
+ if (this.resendable.size > 256) {
67167
+ for (const id of [...this.resendable.keys()]) {
67168
+ if (!this.isPendingId(id))
67169
+ this.resendable.delete(id);
67170
+ }
66504
67171
  }
66505
- this.pending.clear();
66506
- for (const cb of this.pendingReadCookies.values()) {
66507
- cb({ ok: false, error: err.message });
66508
- }
66509
- this.pendingReadCookies.clear();
66510
- for (const { reject } of this.pendingStorage.values())
66511
- reject(err);
66512
- this.pendingStorage.clear();
66513
- for (const { reject } of this.pendingWriteCookies.values())
66514
- reject(err);
66515
- this.pendingWriteCookies.clear();
66516
- for (const { reject } of this.pendingCapture.values())
66517
- reject(err);
66518
- this.pendingCapture.clear();
66519
- for (const { reject } of this.pendingRedirect.values())
66520
- reject(err);
66521
- this.pendingRedirect.clear();
66522
- for (const { reject } of this.pendingIdb.values())
66523
- reject(err);
66524
- this.pendingIdb.clear();
66525
- for (const { reject } of this.pendingDownload.values())
66526
- reject(err);
66527
- this.pendingDownload.clear();
66528
- for (const { reject } of this.pendingGraphql.values())
66529
- reject(err);
66530
- this.pendingGraphql.clear();
67172
+ }
67173
+ /**
67174
+ * B-BUG-6: the host this peer relayed through has gone — most often a
67175
+ * short-lived process (a bootstrap lift, an `fpx` call) that won the
67176
+ * election and then closed. Fail only the in-flight requests that may
67177
+ * already have run (a non-idempotent fetch, a cookie write, a download);
67178
+ * re-elect and send the rest again. The replies to the originals died with
67179
+ * the old host, and each re-sent request keeps its awaiter and its
67180
+ * original deadline.
67181
+ */
67182
+ async recoverFromHostLoss() {
67183
+ const toResend = /* @__PURE__ */ new Map();
67184
+ for (const [id, inner] of this.resendable) {
67185
+ if (this.isPendingId(id))
67186
+ toResend.set(id, inner);
67187
+ }
67188
+ this.resendable.clear();
67189
+ 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()));
67190
+ if (toResend.size === 0)
67191
+ return;
67192
+ const lost = (id, e) => this.rejectPendingId(id, `fetchproxy: the bridge host exited and reconnecting failed: ${e instanceof Error ? e.message : String(e)}`);
67193
+ try {
67194
+ await this.ensureConnected();
67195
+ } catch (e) {
67196
+ for (const id of toResend.keys())
67197
+ lost(id, e);
67198
+ return;
67199
+ }
67200
+ for (const [id, inner] of toResend) {
67201
+ if (!this.isPendingId(id))
67202
+ continue;
67203
+ try {
67204
+ await this.resendInnerFrame(inner);
67205
+ } catch (e) {
67206
+ lost(id, e);
67207
+ }
67208
+ }
67209
+ }
67210
+ /** Send an already-registered request again, keeping its awaiter on failure. */
67211
+ async resendInnerFrame(inner) {
67212
+ if (this.hostHandle)
67213
+ await this.hostHandle.sendOwnInner(inner);
67214
+ else if (this.peerHandle)
67215
+ await this.peerHandle.sendInner(inner);
67216
+ else
67217
+ throw new Error("no bridge handle after re-election");
67218
+ this.noteResendable(inner);
66531
67219
  }
66532
67220
  /**
66533
67221
  * 0.5.2+: read the current pair-pending pair code from whichever handle
@@ -66630,6 +67318,12 @@ async function runOneLift(opts) {
66630
67318
  if (envVal !== void 0 && envVal !== "" && envVal !== "0" && envVal !== "false") {
66631
67319
  throw new BootstrapDisabledError(opts.serverName, envVar);
66632
67320
  }
67321
+ for (const bucket of ["cookies", "localStorage", "sessionStorage"]) {
67322
+ const glob = opts.declare[bucket].find((k) => k.endsWith("*"));
67323
+ if (glob !== void 0) {
67324
+ throw new Error(`fetchproxy bootstrap: declare.${bucket} contains ${JSON.stringify(glob)}, a glob. A lift reads each key by its exact name, so a glob can never match \u2014 list the concrete key names instead.`);
67325
+ }
67326
+ }
66633
67327
  const factory = opts._serverFactory ?? defaultFactory;
66634
67328
  const indexedDb = opts.declare.indexedDb ?? [];
66635
67329
  const localStoragePointers = opts.declare.localStoragePointers ?? [];
@@ -66887,7 +67581,7 @@ function loadAccount(env = process.env) {
66887
67581
  // package.json
66888
67582
  var package_default = {
66889
67583
  name: "canvas-parent-mcp",
66890
- version: "2.1.1",
67584
+ version: "2.1.3",
66891
67585
  mcpName: "io.github.chrischall/canvas-parent-mcp",
66892
67586
  description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
66893
67587
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -66934,12 +67628,12 @@ var package_default = {
66934
67628
  typecheck: "tsc -p tsconfig.json --noEmit"
66935
67629
  },
66936
67630
  dependencies: {
66937
- "@chrischall/mcp-utils": "^1.0.0",
66938
- "@fetchproxy/bootstrap": "^3.0.1",
66939
- "@fetchproxy/server": "^3.0.1",
67631
+ "@chrischall/mcp-utils": "^2.4.0",
67632
+ "@fetchproxy/bootstrap": "^3.2.0",
67633
+ "@fetchproxy/server": "^3.2.0",
66940
67634
  "@modelcontextprotocol/server": "^2.0.0",
66941
67635
  dotenv: "^18.0.0",
66942
- zod: "^4.6.2"
67636
+ zod: "^4.6.5"
66943
67637
  },
66944
67638
  devDependencies: {
66945
67639
  "@modelcontextprotocol/client": "^2.0.0",
@@ -67196,10 +67890,10 @@ function registerCredentialHealthcheckTool(args) {
67196
67890
  import { writeFile as writeFile3, stat } from "fs/promises";
67197
67891
 
67198
67892
  // node_modules/@chrischall/mcp-utils/dist/session/index.js
67199
- import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
67200
- import { dirname, join as join3, resolve } from "node:path";
67201
- import { homedir as homedir2 } from "node:os";
67202
- import { randomBytes as randomBytes2, createHmac } from "node:crypto";
67893
+ import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
67894
+ import { dirname as dirname2, join as join4, resolve as resolve3 } from "node:path";
67895
+ import { homedir as homedir3 } from "node:os";
67896
+ import { randomBytes as randomBytes3, createHmac } from "node:crypto";
67203
67897
  function bindingDigest(secret, salt) {
67204
67898
  return createHmac("sha256", salt).update(secret).digest("hex").slice(0, 32);
67205
67899
  }
@@ -67220,7 +67914,7 @@ function createFileStatePersistence(opts) {
67220
67914
  const secret = opts.boundTo;
67221
67915
  return {
67222
67916
  load() {
67223
- if (!existsSync(filePath))
67917
+ if (!existsSync2(filePath))
67224
67918
  return null;
67225
67919
  try {
67226
67920
  const raw = JSON.parse(readFileSync(filePath, "utf8"));
@@ -67250,16 +67944,16 @@ function createFileStatePersistence(opts) {
67250
67944
  }
67251
67945
  },
67252
67946
  save(state) {
67253
- const dir = dirname(filePath);
67254
- const tmp = `${filePath}.tmp-${randomBytes2(6).toString("hex")}`;
67255
- const dirExisted = existsSync(dir);
67947
+ const dir = dirname2(filePath);
67948
+ const tmp = `${filePath}.tmp-${randomBytes3(6).toString("hex")}`;
67949
+ const dirExisted = existsSync2(dir);
67256
67950
  try {
67257
67951
  mkdirSync(dir, { recursive: true, mode: 448 });
67258
67952
  if (!dirExisted)
67259
67953
  chmodSync(dir, 448);
67260
67954
  let boundTo;
67261
67955
  if (secret !== void 0) {
67262
- const salt = randomBytes2(16).toString("hex");
67956
+ const salt = randomBytes3(16).toString("hex");
67263
67957
  boundTo = { salt, digest: bindingDigest(secret, salt) };
67264
67958
  }
67265
67959
  const envelope = { v: 1, ...boundTo !== void 0 ? { boundTo } : {}, state };
@@ -67269,7 +67963,7 @@ function createFileStatePersistence(opts) {
67269
67963
  chmodSync(filePath, 384);
67270
67964
  } catch (err) {
67271
67965
  try {
67272
- if (existsSync(tmp))
67966
+ if (existsSync2(tmp))
67273
67967
  unlinkSync(tmp);
67274
67968
  } catch {
67275
67969
  }
@@ -67278,7 +67972,7 @@ function createFileStatePersistence(opts) {
67278
67972
  },
67279
67973
  clear() {
67280
67974
  try {
67281
- if (existsSync(filePath))
67975
+ if (existsSync2(filePath))
67282
67976
  unlinkSync(filePath);
67283
67977
  } catch {
67284
67978
  }
@@ -67286,11 +67980,11 @@ function createFileStatePersistence(opts) {
67286
67980
  };
67287
67981
  }
67288
67982
  function homeOf(env) {
67289
- return readEnvVar("HOME", { env }) ?? homedir2();
67983
+ return readEnvVar("HOME", { env }) ?? homedir3();
67290
67984
  }
67291
67985
  function resolveStateDir(opts = {}) {
67292
67986
  const env = opts.env;
67293
- return join3(readEnvVar("MCP_DATA_DIR", { env }) ?? homeOf(env), opts.subdir ?? "");
67987
+ return join4(readEnvVar("MCP_DATA_DIR", { env }) ?? homeOf(env), opts.subdir ?? "");
67294
67988
  }
67295
67989
  function resolveStateFile(opts) {
67296
67990
  const override = opts.envVar !== void 0 ? readEnvVar(opts.envVar, { env: opts.env }) : void 0;
@@ -67298,13 +67992,13 @@ function resolveStateFile(opts) {
67298
67992
  if (override === "~")
67299
67993
  return homeOf(opts.env);
67300
67994
  if (override.startsWith("~/"))
67301
- return join3(homeOf(opts.env), override.slice(2));
67302
- return resolve(override);
67995
+ return join4(homeOf(opts.env), override.slice(2));
67996
+ return resolve3(override);
67303
67997
  }
67304
67998
  const dirOpts = { env: opts.env };
67305
67999
  if (opts.subdir !== void 0)
67306
68000
  dirOpts.subdir = opts.subdir;
67307
- return join3(resolveStateDir(dirOpts), opts.fileName);
68001
+ return join4(resolveStateDir(dirOpts), opts.fileName);
67308
68002
  }
67309
68003
  var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
67310
68004
  function isPersistedCookieSession(raw) {
@@ -67594,7 +68288,8 @@ function reportCacheWriteFailure(err) {
67594
68288
  }
67595
68289
 
67596
68290
  // src/client.ts
67597
- import { dirname as dirname2 } from "path";
68291
+ import { dirname as dirname3, isAbsolute as isAbsolute5, join as join5, resolve as resolve4 } from "path";
68292
+ import { homedir as homedir4 } from "os";
67598
68293
 
67599
68294
  // src/session-login.ts
67600
68295
  var USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36";
@@ -67692,6 +68387,8 @@ var CanvasClient = class {
67692
68387
  * what keeps the path alive once the browser cookie lapses.
67693
68388
  */
67694
68389
  refreshSession;
68390
+ /** Explicit download root; when unset, resolved per call (see {@link downloadRoot}). */
68391
+ outputDir;
67695
68392
  auth;
67696
68393
  /** Lazily-built shared refresh_token exchanger (oauth mode only). */
67697
68394
  oauthRefresh = null;
@@ -67707,6 +68404,7 @@ var CanvasClient = class {
67707
68404
  */
67708
68405
  constructor(account, opts = {}) {
67709
68406
  this.account = account;
68407
+ this.outputDir = opts.outputDir;
67710
68408
  this.sessionLoginFn = opts.sessionLogin ?? sessionLogin;
67711
68409
  this.refreshSession = opts.refreshSession ?? null;
67712
68410
  this.auth = new CookieSessionManager({
@@ -67729,7 +68427,7 @@ var CanvasClient = class {
67729
68427
  const res = await this.doRawRequest(path, opts);
67730
68428
  const text = await res.text();
67731
68429
  if (opts.responseType === "text") return text;
67732
- return parseJsonBody(text) ?? null;
68430
+ return parseJsonBody2(text) ?? null;
67733
68431
  }
67734
68432
  async requestPaginated(path, opts = {}) {
67735
68433
  const perPage = opts.perPage ?? DEFAULT_PER_PAGE;
@@ -67739,7 +68437,7 @@ var CanvasClient = class {
67739
68437
  for (let page = 0; page < maxPages; page++) {
67740
68438
  const res = await this.doRawRequest(url4, opts);
67741
68439
  const text = await res.text();
67742
- const parsed = parseJsonBody(text) ?? [];
68440
+ const parsed = parseJsonBody2(text) ?? [];
67743
68441
  for (const item of parsed) out.push(item);
67744
68442
  const linkHeader = res.headers.get("link");
67745
68443
  const next = linkHeader ? parseLinkHeader(linkHeader).next : void 0;
@@ -67748,33 +68446,84 @@ var CanvasClient = class {
67748
68446
  }
67749
68447
  return out;
67750
68448
  }
68449
+ /**
68450
+ * Download a Canvas file to disk.
68451
+ *
68452
+ * Both inputs arrive from tool arguments, which a prompt injection in
68453
+ * classmate/teacher-authored Canvas content can steer, so both are pinned
68454
+ * BEFORE any credential is attached:
68455
+ * - `path` must resolve to the configured Canvas origin (https, same host —
68456
+ * the `https://canvas@evil/` userinfo trick parses to a foreign host and is
68457
+ * refused) and name a `/files/<id>` resource. Only the initial request is
68458
+ * pinned: Canvas legitimately redirects to its file store, and fetch
68459
+ * strips Authorization/Cookie on a cross-origin redirect.
68460
+ * - `destinationPath` must lie inside the download root (see
68461
+ * {@link downloadRoot}), checked through symlinks; a relative path is
68462
+ * resolved against that root.
68463
+ */
67751
68464
  async download(path, destinationPath, opts = {}) {
68465
+ const url4 = this.pinDownloadUrl(path);
68466
+ const dest = this.confineDestination(destinationPath);
67752
68467
  let destStat = null;
67753
68468
  try {
67754
- destStat = await stat(destinationPath);
68469
+ destStat = await stat(dest);
67755
68470
  } catch {
67756
68471
  }
67757
68472
  if (destStat?.isDirectory()) throw new InvalidPathError(destinationPath);
67758
68473
  if (destStat && !opts.overwrite) throw new FileExistsError(destinationPath);
67759
- const parent = dirname2(destinationPath);
68474
+ const parent = dirname3(dest);
67760
68475
  try {
67761
68476
  await stat(parent);
67762
68477
  } catch {
67763
68478
  throw new ParentDirectoryMissingError(parent);
67764
68479
  }
67765
- const url4 = /^https?:\/\//i.test(path) ? path : `${this.account.baseUrl}${path}`;
67766
68480
  const res = await this.authedFetch(url4, {});
67767
68481
  if (res.status === 401) throw new TokenExpiredError(this.account.mode);
67768
68482
  if (res.status === 404) throw new Error(`Canvas download 404 for ${path}`);
67769
68483
  if (!res.ok) throw new Error(`Canvas download ${res.status} for ${path}`);
67770
68484
  const buf = new Uint8Array(await res.arrayBuffer());
67771
- await writeFile3(destinationPath, buf);
68485
+ await writeFile3(dest, buf);
67772
68486
  return {
67773
- path: destinationPath,
68487
+ path: dest,
67774
68488
  bytes: buf.byteLength,
67775
68489
  contentType: res.headers.get("content-type") ?? "application/octet-stream"
67776
68490
  };
67777
68491
  }
68492
+ /**
68493
+ * The directory downloads are confined to: the constructor's `outputDir`,
68494
+ * else `CANVAS_OUTPUT_DIR`, else `~/Downloads`. Read per call so an env
68495
+ * change needs no client rebuild.
68496
+ */
68497
+ downloadRoot() {
68498
+ return expandPath(this.outputDir ?? readEnvVar("CANVAS_OUTPUT_DIR") ?? join5(homedir4(), "Downloads"));
68499
+ }
68500
+ confineDestination(destinationPath) {
68501
+ const root = this.downloadRoot();
68502
+ const candidate = destinationPath.startsWith("~") || isAbsolute5(destinationPath) ? expandPath(destinationPath) : resolve4(root, destinationPath);
68503
+ try {
68504
+ assertPathWithinRoots(candidate, [root]);
68505
+ } catch {
68506
+ throw new DownloadDestinationRejectedError(destinationPath, root);
68507
+ }
68508
+ return candidate;
68509
+ }
68510
+ pinDownloadUrl(path) {
68511
+ const raw = /^[a-z][a-z0-9+.-]*:/i.test(path) ? path : `${this.account.baseUrl}${path}`;
68512
+ let parsed;
68513
+ try {
68514
+ parsed = new URL(raw);
68515
+ } catch {
68516
+ throw new DownloadUrlRejectedError(path, "not a valid URL");
68517
+ }
68518
+ const origin = new URL(this.account.baseUrl).origin;
68519
+ if (parsed.protocol !== "https:" || parsed.origin !== origin || parsed.username || parsed.password) {
68520
+ throw new DownloadUrlRejectedError(path, `must be an https URL on ${origin}`);
68521
+ }
68522
+ if (!/\/files\/[^/]+/.test(parsed.pathname)) {
68523
+ throw new DownloadUrlRejectedError(path, "must name a Canvas file (a /files/<id> path)");
68524
+ }
68525
+ return parsed.href;
68526
+ }
67778
68527
  async doRawRequest(path, opts) {
67779
68528
  const url4 = /^https?:\/\//i.test(path) ? path : `${this.account.baseUrl}${path}`;
67780
68529
  const accept = opts.responseType === "text" ? "text/html, text/plain, */*" : "application/json+canvas-string-ids, application/json";
@@ -67880,15 +68629,15 @@ var CanvasClient = class {
67880
68629
  };
67881
68630
  }
67882
68631
  };
67883
- function parseJsonBody(text) {
68632
+ function parseJsonBody2(text) {
67884
68633
  if (!text) return null;
67885
68634
  const stripped = text.replace(/^while\(1\);/, "");
67886
68635
  return JSON.parse(stripped);
67887
68636
  }
67888
68637
  function injectPerPage(pathOrUrl, perPage) {
67889
68638
  if (/[?&]per_page=/.test(pathOrUrl)) return pathOrUrl;
67890
- const sep = pathOrUrl.includes("?") ? "&" : "?";
67891
- return `${pathOrUrl}${sep}per_page=${perPage}`;
68639
+ const sep2 = pathOrUrl.includes("?") ? "&" : "?";
68640
+ return `${pathOrUrl}${sep2}per_page=${perPage}`;
67892
68641
  }
67893
68642
  var TokenExpiredError = class extends Error {
67894
68643
  constructor(mode, detail) {
@@ -67909,6 +68658,26 @@ var CanvasUnreachableError = class extends Error {
67909
68658
  }
67910
68659
  status;
67911
68660
  };
68661
+ var DownloadUrlRejectedError = class extends Error {
68662
+ constructor(url4, reason) {
68663
+ super(`DownloadUrlRejected: ${reason}; use the \`url\` from canvas_list_course_files. Got: ${url4}`);
68664
+ this.url = url4;
68665
+ this.name = "DownloadUrlRejectedError";
68666
+ }
68667
+ url;
68668
+ };
68669
+ var DownloadDestinationRejectedError = class extends Error {
68670
+ constructor(path, root) {
68671
+ super(
68672
+ `DownloadDestinationRejected: destinationPath must be inside ${root} (set CANVAS_OUTPUT_DIR to change it). Got: ${path}`
68673
+ );
68674
+ this.path = path;
68675
+ this.root = root;
68676
+ this.name = "DownloadDestinationRejectedError";
68677
+ }
68678
+ path;
68679
+ root;
68680
+ };
67912
68681
  var InvalidPathError = class extends Error {
67913
68682
  constructor(path) {
67914
68683
  super(`InvalidPath: destinationPath must be a filename, not a directory: ${path}`);
@@ -68315,8 +69084,8 @@ var listArgs4 = external_exports.object({
68315
69084
  contentTypes: external_exports.array(external_exports.string()).optional()
68316
69085
  });
68317
69086
  var downloadArgs = external_exports.object({
68318
- url: external_exports.string().describe("The url field returned by canvas_list_course_files (absolute https URL)."),
68319
- destinationPath: external_exports.string().describe("Absolute path where the file should be written."),
69087
+ url: external_exports.string().describe("The url field returned by canvas_list_course_files (an https /files/ URL on the configured Canvas host; anything else is refused)."),
69088
+ destinationPath: external_exports.string().describe("Where to write the file. Must be inside the download directory (CANVAS_OUTPUT_DIR, default ~/Downloads); a relative path is resolved against it."),
68320
69089
  overwrite: external_exports.boolean().optional()
68321
69090
  });
68322
69091
  function registerFileTools(server, client) {
@@ -68334,8 +69103,8 @@ function registerFileTools(server, client) {
68334
69103
  return textContent(data);
68335
69104
  });
68336
69105
  server.registerTool("canvas_download_file", {
68337
- description: "Download a Canvas file to disk. `url` is the absolute URL from canvas_list_course_files; `destinationPath` is required.",
68338
- annotations: { destructiveHint: true },
69106
+ description: "Download a Canvas file to disk. `url` is the absolute URL from canvas_list_course_files (only file URLs on the configured Canvas host are accepted); `destinationPath` is required and must be inside the download directory (CANVAS_OUTPUT_DIR, default ~/Downloads).",
69107
+ annotations: { destructiveHint: true, openWorldHint: true },
68339
69108
  inputSchema: downloadArgs
68340
69109
  }, async (rawArgs) => {
68341
69110
  const args = downloadArgs.parse(rawArgs);
@@ -68347,8 +69116,8 @@ function registerFileTools(server, client) {
68347
69116
  }
68348
69117
 
68349
69118
  // src/index.ts
68350
- var __dirname = dirname3(fileURLToPath(import.meta.url));
68351
- await loadDotenvSafely({ path: join4(__dirname, "..", ".env"), override: false });
69119
+ var __dirname = dirname4(fileURLToPath(import.meta.url));
69120
+ await loadDotenvSafely({ path: join6(__dirname, "..", ".env"), override: false });
68352
69121
  var resolved = null;
68353
69122
  var configError = null;
68354
69123
  try {
@@ -68358,7 +69127,7 @@ try {
68358
69127
  }
68359
69128
  await runMcp({
68360
69129
  name: "canvas",
68361
- version: "2.1.1",
69130
+ version: "2.1.3",
68362
69131
  // x-release-please-version
68363
69132
  tools: [
68364
69133
  // Outside the `if (resolved)` on purpose: an unconfigured server must