canvas-parent-mcp 2.1.2 → 2.1.4

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
  }
@@ -5007,7 +5007,7 @@ var require_memoizer = __commonJS({
5007
5007
  return bucket;
5008
5008
  }
5009
5009
  var handoff2;
5010
- var open2 = [];
5010
+ var open3 = [];
5011
5011
  var memo3 = {
5012
5012
  alloc(_inst, payload, empty) {
5013
5013
  const bucket = handoff2;
@@ -5016,7 +5016,7 @@ var require_memoizer = __commonJS({
5016
5016
  handoff2 = void 0;
5017
5017
  const entry = { value: empty, issues: null };
5018
5018
  bucket.set(payload.value, entry);
5019
- open2.push(entry);
5019
+ open3.push(entry);
5020
5020
  return empty;
5021
5021
  },
5022
5022
  guard(inst) {
@@ -5087,10 +5087,10 @@ var require_memoizer = __commonJS({
5087
5087
  return payload;
5088
5088
  }
5089
5089
  handoff2 = bucket;
5090
- const depth = open2.length;
5090
+ const depth = open3.length;
5091
5091
  const result = base(payload, ctx);
5092
5092
  handoff2 = void 0;
5093
- const entry = open2.length > depth ? open2.pop() : void 0;
5093
+ const entry = open3.length > depth ? open3.pop() : void 0;
5094
5094
  if (result instanceof Promise) {
5095
5095
  return result.then((r) => {
5096
5096
  if (entry)
@@ -26497,7 +26497,7 @@ var require_stream = __commonJS({
26497
26497
  };
26498
26498
  duplex._final = function(callback) {
26499
26499
  if (ws.readyState === ws.CONNECTING) {
26500
- ws.once("open", function open2() {
26500
+ ws.once("open", function open3() {
26501
26501
  duplex._final(callback);
26502
26502
  });
26503
26503
  return;
@@ -26518,7 +26518,7 @@ var require_stream = __commonJS({
26518
26518
  };
26519
26519
  duplex._write = function(chunk2, encoding, callback) {
26520
26520
  if (ws.readyState === ws.CONNECTING) {
26521
- ws.once("open", function open2() {
26521
+ ws.once("open", function open3() {
26522
26522
  duplex._write(chunk2, encoding, callback);
26523
26523
  });
26524
26524
  return;
@@ -26980,7 +26980,7 @@ 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
26986
  // node_modules/@chrischall/mcp-utils/dist/cancel/index.js
@@ -31399,14 +31399,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
31399
31399
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
31400
31400
  }
31401
31401
  function sleep(ms, signal) {
31402
- return new Promise((resolve2, reject) => {
31402
+ return new Promise((resolve5, reject) => {
31403
31403
  if (signal?.aborted) {
31404
31404
  reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
31405
31405
  return;
31406
31406
  }
31407
31407
  const timer = setTimeout(() => {
31408
31408
  signal?.removeEventListener("abort", onAbort);
31409
- resolve2();
31409
+ resolve5();
31410
31410
  }, ms);
31411
31411
  const onAbort = () => {
31412
31412
  clearTimeout(timer);
@@ -32200,7 +32200,7 @@ var Protocol = class {
32200
32200
  const flowStartedAt = Date.now();
32201
32201
  let onAbort;
32202
32202
  let cleanupMessageId;
32203
- return new Promise((resolve2, reject) => {
32203
+ return new Promise((resolve5, reject) => {
32204
32204
  const earlyReject = (error62) => {
32205
32205
  reject(error62);
32206
32206
  };
@@ -32268,7 +32268,7 @@ var Protocol = class {
32268
32268
  }
32269
32269
  if (decoded.kind === "invalid") return reject(decoded.error);
32270
32270
  if (decoded.kind === "input_required") {
32271
- if (options?.allowInputRequired === true) return resolve2(manualInputRequiredValue(decoded));
32271
+ if (options?.allowInputRequired === true) return resolve5(manualInputRequiredValue(decoded));
32272
32272
  const flow = {
32273
32273
  codec: codec2,
32274
32274
  request,
@@ -32280,11 +32280,11 @@ var Protocol = class {
32280
32280
  params
32281
32281
  }, resultSchema, legOptions)
32282
32282
  };
32283
- return resolve2(this._resolveNonCompleteResult(decoded, flow));
32283
+ return resolve5(this._resolveNonCompleteResult(decoded, flow));
32284
32284
  }
32285
32285
  const result = decoded.result;
32286
32286
  validateStandardSchema(resultSchema, result).then((parseResult) => {
32287
- if (parseResult.success) resolve2(parseResult.data);
32287
+ if (parseResult.success) resolve5(parseResult.data);
32288
32288
  else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
32289
32289
  }, reject);
32290
32290
  });
@@ -35196,7 +35196,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin(((exports) => {
35196
35196
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
35197
35197
  const schOrFunc = root.refs[ref];
35198
35198
  if (schOrFunc) return schOrFunc;
35199
- let _sch = resolve2.call(this, root, ref);
35199
+ let _sch = resolve5.call(this, root, ref);
35200
35200
  if (_sch === void 0) {
35201
35201
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
35202
35202
  const { schemaId } = this.opts;
@@ -35222,7 +35222,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin(((exports) => {
35222
35222
  function sameSchemaEnv(s1, s2) {
35223
35223
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
35224
35224
  }
35225
- function resolve2(root, ref) {
35225
+ function resolve5(root, ref) {
35226
35226
  let sch;
35227
35227
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
35228
35228
  return sch || this.schemas[ref] || resolveSchema.call(this, root, ref);
@@ -35672,47 +35672,47 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
35672
35672
  else if (typeof uri === "object") uri = parse3(serialize(uri, options), options);
35673
35673
  return uri;
35674
35674
  }
35675
- function resolve2(baseURI, relativeURI, options) {
35675
+ function resolve5(baseURI, relativeURI, options) {
35676
35676
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
35677
35677
  const resolved2 = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
35678
35678
  schemelessOptions.skipEscape = true;
35679
35679
  return serialize(resolved2, schemelessOptions);
35680
35680
  }
35681
- function resolveComponent(base, relative, options, skipNormalization) {
35681
+ function resolveComponent(base, relative2, options, skipNormalization) {
35682
35682
  const target = {};
35683
35683
  if (!skipNormalization) {
35684
35684
  base = parse3(serialize(base, options), options);
35685
- relative = parse3(serialize(relative, options), options);
35685
+ relative2 = parse3(serialize(relative2, options), options);
35686
35686
  }
35687
35687
  options = options || {};
35688
- if (!options.tolerant && relative.scheme) {
35689
- target.scheme = relative.scheme;
35690
- target.userinfo = relative.userinfo;
35691
- target.host = relative.host;
35692
- target.port = relative.port;
35693
- target.path = removeDotSegments(relative.path || "");
35694
- 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;
35695
35695
  } else {
35696
- if (relative.userinfo !== void 0 || relative.host !== void 0 || relative.port !== void 0) {
35697
- target.userinfo = relative.userinfo;
35698
- target.host = relative.host;
35699
- target.port = relative.port;
35700
- target.path = removeDotSegments(relative.path || "");
35701
- 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;
35702
35702
  } else {
35703
- if (!relative.path) {
35703
+ if (!relative2.path) {
35704
35704
  target.path = base.path;
35705
- if (relative.query !== void 0) target.query = relative.query;
35705
+ if (relative2.query !== void 0) target.query = relative2.query;
35706
35706
  else target.query = base.query;
35707
35707
  } else {
35708
- if (relative.path[0] === "/") target.path = removeDotSegments(relative.path);
35708
+ if (relative2.path[0] === "/") target.path = removeDotSegments(relative2.path);
35709
35709
  else {
35710
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative.path;
35711
- else if (!base.path) target.path = relative.path;
35712
- 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;
35713
35713
  target.path = removeDotSegments(target.path);
35714
35714
  }
35715
- target.query = relative.query;
35715
+ target.query = relative2.query;
35716
35716
  }
35717
35717
  target.userinfo = base.userinfo;
35718
35718
  target.host = base.host;
@@ -35720,7 +35720,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
35720
35720
  }
35721
35721
  target.scheme = base.scheme;
35722
35722
  }
35723
- target.fragment = relative.fragment;
35723
+ target.fragment = relative2.fragment;
35724
35724
  return target;
35725
35725
  }
35726
35726
  function equal(uriA, uriB, options) {
@@ -35846,7 +35846,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
35846
35846
  const fastUri = {
35847
35847
  SCHEMES,
35848
35848
  normalize,
35849
- resolve: resolve2,
35849
+ resolve: resolve5,
35850
35850
  resolveComponent,
35851
35851
  equal,
35852
35852
  serialize,
@@ -41219,7 +41219,7 @@ var StdioServerTransport = class {
41219
41219
  }
41220
41220
  send(message) {
41221
41221
  if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
41222
- return new Promise((resolve2, reject) => {
41222
+ return new Promise((resolve5, reject) => {
41223
41223
  const json2 = serializeMessage(message);
41224
41224
  let settled = false;
41225
41225
  const onError = (error62) => {
@@ -41234,14 +41234,14 @@ var StdioServerTransport = class {
41234
41234
  settled = true;
41235
41235
  this._stdout.off("error", onError);
41236
41236
  this._stdout.off("drain", onDrain);
41237
- resolve2();
41237
+ resolve5();
41238
41238
  };
41239
41239
  this._stdout.once("error", onError);
41240
41240
  if (this._stdout.write(json2)) {
41241
41241
  if (settled) return;
41242
41242
  settled = true;
41243
41243
  this._stdout.off("error", onError);
41244
- resolve2();
41244
+ resolve5();
41245
41245
  } else if (!settled) this._stdout.once("drain", onDrain);
41246
41246
  });
41247
41247
  }
@@ -41295,14 +41295,14 @@ var StdioConnectionChannel = class {
41295
41295
  */
41296
41296
  async whenRequestsAnswered(timeoutMs) {
41297
41297
  if (this._closed || this._pendingRequests.size === 0) return true;
41298
- return await new Promise((resolve2) => {
41298
+ return await new Promise((resolve5) => {
41299
41299
  const waiter = () => {
41300
41300
  clearTimeout(timer);
41301
- resolve2(true);
41301
+ resolve5(true);
41302
41302
  };
41303
41303
  const timer = setTimeout(() => {
41304
41304
  this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
41305
- resolve2(false);
41305
+ resolve5(false);
41306
41306
  }, timeoutMs);
41307
41307
  this._drainWaiters.push(waiter);
41308
41308
  });
@@ -46738,7 +46738,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
46738
46738
  var NONE = 0;
46739
46739
  var ASSUMED = 1;
46740
46740
  var PROVEN = 2;
46741
- function isRecursive(inst, stack, resolve2) {
46741
+ function isRecursive(inst, stack, resolve5) {
46742
46742
  const cached2 = recursive.get(inst);
46743
46743
  if (cached2 !== void 0)
46744
46744
  return cached2 ? PROVEN : NONE;
@@ -46748,7 +46748,7 @@ function isRecursive(inst, stack, resolve2) {
46748
46748
  let result = NONE;
46749
46749
  const check2 = (child) => {
46750
46750
  if (result !== PROVEN && child?._zod) {
46751
- const answer = isRecursive(child, stack, resolve2);
46751
+ const answer = isRecursive(child, stack, resolve5);
46752
46752
  if (answer > result)
46753
46753
  result = answer;
46754
46754
  }
@@ -46759,7 +46759,7 @@ function isRecursive(inst, stack, resolve2) {
46759
46759
  const desc = Object.getOwnPropertyDescriptor(sh, key);
46760
46760
  if (spread && !desc.enumerable)
46761
46761
  continue;
46762
- 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;
46763
46763
  if (child > answer)
46764
46764
  answer = child;
46765
46765
  }
@@ -46823,7 +46823,7 @@ function isRecursive(inst, stack, resolve2) {
46823
46823
  break;
46824
46824
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
46825
46825
  case "lazy": {
46826
- const inner = def._cachedInner ?? (resolve2 ? inst._zod.innerType : void 0);
46826
+ const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
46827
46827
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
46828
46828
  break;
46829
46829
  }
@@ -61639,6 +61639,8 @@ function runMcp(opts) {
61639
61639
  }
61640
61640
 
61641
61641
  // node_modules/@chrischall/mcp-utils/dist/config/index.js
61642
+ import { homedir } from "node:os";
61643
+ import { isAbsolute, join, resolve } from "node:path";
61642
61644
  var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
61643
61645
  function readEnvVar(key, opts = {}) {
61644
61646
  const env = opts.env ?? process.env;
@@ -61665,6 +61667,15 @@ function parseBoolEnv(key, opts = {}) {
61665
61667
  return false;
61666
61668
  return fallback;
61667
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
+ }
61668
61679
  async function loadDotenvSafely(opts = {}) {
61669
61680
  try {
61670
61681
  const mod = await import(
@@ -61682,6 +61693,41 @@ async function loadDotenvSafely(opts = {}) {
61682
61693
  }
61683
61694
  }
61684
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
+
61685
61731
  // node_modules/@chrischall/mcp-utils/dist/http/index.js
61686
61732
  function buildQueryString(params) {
61687
61733
  const parts = [];
@@ -63495,7 +63541,7 @@ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
63495
63541
  async function electRole(opts) {
63496
63542
  const server = createServer();
63497
63543
  const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
63498
- return new Promise((resolve2, reject) => {
63544
+ return new Promise((resolve5, reject) => {
63499
63545
  let timer;
63500
63546
  const clearBindTimer = () => {
63501
63547
  if (timer) {
@@ -63511,7 +63557,7 @@ async function electRole(opts) {
63511
63557
  server.close();
63512
63558
  } catch {
63513
63559
  }
63514
- resolve2({ role: "peer" });
63560
+ resolve5({ role: "peer" });
63515
63561
  } else {
63516
63562
  reject(e);
63517
63563
  }
@@ -63519,7 +63565,7 @@ async function electRole(opts) {
63519
63565
  const onListening = () => {
63520
63566
  clearBindTimer();
63521
63567
  server.removeListener("error", onError);
63522
- resolve2({ role: "host", server });
63568
+ resolve5({ role: "host", server });
63523
63569
  };
63524
63570
  server.once("error", onError);
63525
63571
  server.once("listening", onListening);
@@ -63798,13 +63844,13 @@ async function awaitSessionReady(ready, opts) {
63798
63844
  // node_modules/@fetchproxy/server/dist/extension-trust.js
63799
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";
63800
63846
  import { randomBytes as randomBytes2 } from "node:crypto";
63801
- import { isAbsolute as isAbsolute2, join as join2 } from "node:path";
63847
+ import { isAbsolute as isAbsolute4, join as join3 } from "node:path";
63802
63848
 
63803
63849
  // node_modules/@fetchproxy/server/dist/identity.js
63804
63850
  import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink, link, lstat } from "node:fs/promises";
63805
63851
  import { randomBytes } from "node:crypto";
63806
- import { isAbsolute, join } from "node:path";
63807
- import { homedir } from "node:os";
63852
+ import { isAbsolute as isAbsolute3, join as join2 } from "node:path";
63853
+ import { homedir as homedir2 } from "node:os";
63808
63854
  var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
63809
63855
  var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
63810
63856
  function envIdentityDir() {
@@ -63812,12 +63858,12 @@ function envIdentityDir() {
63812
63858
  if (raw === void 0)
63813
63859
  return void 0;
63814
63860
  const dir = raw.trim();
63815
- if (dir === "" || !isAbsolute(dir))
63861
+ if (dir === "" || !isAbsolute3(dir))
63816
63862
  return void 0;
63817
63863
  return dir;
63818
63864
  }
63819
63865
  function defaultIdentityDir() {
63820
- return envIdentityDir() ?? join(homedir(), ".fetchproxy", "identity");
63866
+ return envIdentityDir() ?? join2(homedir2(), ".fetchproxy", "identity");
63821
63867
  }
63822
63868
  function safeIdentityFileBase(serverName) {
63823
63869
  if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
@@ -63826,7 +63872,7 @@ function safeIdentityFileBase(serverName) {
63826
63872
  return serverName.replace(/\//g, "_");
63827
63873
  }
63828
63874
  function identityFilePath(dir, serverName) {
63829
- return join(dir, `${safeIdentityFileBase(serverName)}.json`);
63875
+ return join2(dir, `${safeIdentityFileBase(serverName)}.json`);
63830
63876
  }
63831
63877
  async function generateIdentity() {
63832
63878
  const x = await generateX25519();
@@ -63880,7 +63926,7 @@ async function sweepStagingFiles(dir, serverName, opts = {}) {
63880
63926
  }
63881
63927
  const cutoff = opts.olderThanMs === void 0 ? null : Date.now() - opts.olderThanMs;
63882
63928
  await Promise.all(names.filter((name) => staging.test(name)).map(async (name) => {
63883
- const path = join(dir, name);
63929
+ const path = join2(dir, name);
63884
63930
  if (cutoff !== null) {
63885
63931
  const st = await lstat(path).catch(() => null);
63886
63932
  if (st === null || st.mtimeMs > cutoff)
@@ -63961,7 +64007,7 @@ function envTrustDir(env = process.env) {
63961
64007
  if (raw === void 0)
63962
64008
  return void 0;
63963
64009
  const dir = raw.trim();
63964
- if (dir !== "" && isAbsolute2(dir))
64010
+ if (dir !== "" && isAbsolute4(dir))
63965
64011
  return dir;
63966
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.`);
63967
64013
  return void 0;
@@ -64009,13 +64055,13 @@ function decideExtensionTrust(args) {
64009
64055
  };
64010
64056
  }
64011
64057
  function extensionTrustPath(serverName, dir = defaultTrustDir()) {
64012
- return join2(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
64058
+ return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
64013
64059
  }
64014
64060
  function extensionTrustPathHint(serverName) {
64015
64061
  try {
64016
64062
  return extensionTrustPath(serverName);
64017
64063
  } catch {
64018
- return join2(defaultTrustDir(), "<server-name>.extension-trust.json");
64064
+ return join3(defaultTrustDir(), "<server-name>.extension-trust.json");
64019
64065
  }
64020
64066
  }
64021
64067
  function isPin(x) {
@@ -64168,8 +64214,8 @@ async function startHost(opts) {
64168
64214
  let rejectOwnSession;
64169
64215
  let ownSessionReady;
64170
64216
  function resetSessionPromise() {
64171
- ownSessionReady = new Promise((resolve2, reject) => {
64172
- resolveOwnSession = resolve2;
64217
+ ownSessionReady = new Promise((resolve5, reject) => {
64218
+ resolveOwnSession = resolve5;
64173
64219
  rejectOwnSession = reject;
64174
64220
  });
64175
64221
  ownSessionReady.catch(() => {
@@ -64571,7 +64617,7 @@ async function startHost(opts) {
64571
64617
  });
64572
64618
  });
64573
64619
  return {
64574
- close: () => new Promise((resolve2) => {
64620
+ close: () => new Promise((resolve5) => {
64575
64621
  for (const client of wss.clients) {
64576
64622
  try {
64577
64623
  client.terminate();
@@ -64579,7 +64625,7 @@ async function startHost(opts) {
64579
64625
  }
64580
64626
  }
64581
64627
  wss.close(() => {
64582
- opts.httpServer.close(() => resolve2());
64628
+ opts.httpServer.close(() => resolve5());
64583
64629
  });
64584
64630
  }),
64585
64631
  sendOwnInner: async (inner) => {
@@ -64619,11 +64665,11 @@ async function startPeer(opts) {
64619
64665
  maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES
64620
64666
  });
64621
64667
  const dialTimeoutMs = opts.dialTimeoutMs ?? PEER_DIAL_TIMEOUT_MS;
64622
- await new Promise((resolve2, reject) => {
64668
+ await new Promise((resolve5, reject) => {
64623
64669
  let timer;
64624
64670
  const onOpen = () => {
64625
64671
  cleanup();
64626
- resolve2();
64672
+ resolve5();
64627
64673
  };
64628
64674
  const onError = (e) => {
64629
64675
  cleanup();
@@ -64711,10 +64757,10 @@ async function startPeer(opts) {
64711
64757
  let sessionPromiseSettled = false;
64712
64758
  function resetSessionPromise() {
64713
64759
  sessionPromiseSettled = false;
64714
- sessionPromise = new Promise((resolve2, reject) => {
64760
+ sessionPromise = new Promise((resolve5, reject) => {
64715
64761
  resolveFirstReady = (st) => {
64716
64762
  sessionPromiseSettled = true;
64717
- resolve2(st);
64763
+ resolve5(st);
64718
64764
  };
64719
64765
  rejectFirstReady = (e) => {
64720
64766
  sessionPromiseSettled = true;
@@ -65864,8 +65910,8 @@ var FetchproxyServer = class {
65864
65910
  async _fetchOnceWithTimeout(init, fetchOpts = {}) {
65865
65911
  const id = this.nextRequestId++;
65866
65912
  const inner = { type: "request", id, op: "fetch", init };
65867
- const pending = this.guardPending(new Promise((resolve2) => {
65868
- this.pending.set(id, resolve2);
65913
+ const pending = this.guardPending(new Promise((resolve5) => {
65914
+ this.pending.set(id, resolve5);
65869
65915
  }));
65870
65916
  try {
65871
65917
  await this.sendInnerFrame(inner, {
@@ -65888,12 +65934,12 @@ var FetchproxyServer = class {
65888
65934
  try {
65889
65935
  return await Promise.race([
65890
65936
  pending,
65891
- new Promise((resolve2) => {
65937
+ new Promise((resolve5) => {
65892
65938
  timer = setTimeout(() => {
65893
65939
  this.pending.delete(id);
65894
65940
  const elapsedMs = Date.now() - start;
65895
65941
  const error62 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
65896
- resolve2({
65942
+ resolve5({
65897
65943
  ok: false,
65898
65944
  error: error62,
65899
65945
  kind: "timeout",
@@ -66037,9 +66083,9 @@ var FetchproxyServer = class {
66037
66083
  if (opts.subdomain !== void 0)
66038
66084
  assertSubdomainLabel(opts.subdomain);
66039
66085
  const baseDomain = this.resolveBaseDomain(opts.domain);
66040
- const isAbsolute3 = path.startsWith("http://") || path.startsWith("https://");
66086
+ const isAbsolute6 = path.startsWith("http://") || path.startsWith("https://");
66041
66087
  let host;
66042
- if (isAbsolute3) {
66088
+ if (isAbsolute6) {
66043
66089
  try {
66044
66090
  host = new URL(path).host;
66045
66091
  } catch {
@@ -66048,7 +66094,7 @@ var FetchproxyServer = class {
66048
66094
  } else {
66049
66095
  host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
66050
66096
  }
66051
- const url4 = isAbsolute3 ? path : `https://${host}${path}`;
66097
+ const url4 = isAbsolute6 ? path : `https://${host}${path}`;
66052
66098
  assertUrlInDomains("request url", url4, this.opts.domains);
66053
66099
  let tabUrl = `https://${host}/`;
66054
66100
  if (opts.viaTab !== void 0) {
@@ -66313,8 +66359,8 @@ var FetchproxyServer = class {
66313
66359
  const tabUrl = `https://${host}/`;
66314
66360
  inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
66315
66361
  }
66316
- const pending = this.guardPending(new Promise((resolve2) => {
66317
- this.pendingReadCookies.set(id, resolve2);
66362
+ const pending = this.guardPending(new Promise((resolve5) => {
66363
+ this.pendingReadCookies.set(id, resolve5);
66318
66364
  }));
66319
66365
  await this.sendInnerFrame(inner);
66320
66366
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
@@ -66372,8 +66418,8 @@ var FetchproxyServer = class {
66372
66418
  ...cookiePath !== void 0 ? { path: cookiePath } : {}
66373
66419
  }
66374
66420
  };
66375
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66376
- this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
66421
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66422
+ this.pendingWriteCookies.set(id, { resolve: resolve5, reject });
66377
66423
  }));
66378
66424
  await this.sendInnerFrame(inner);
66379
66425
  return this._withVerbTimeout(
@@ -66443,8 +66489,8 @@ var FetchproxyServer = class {
66443
66489
  ...opts.pointers ? { pointers: { ...opts.pointers } } : {}
66444
66490
  }
66445
66491
  };
66446
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66447
- this.pendingStorage.set(id, { resolve: resolve2, reject });
66492
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66493
+ this.pendingStorage.set(id, { resolve: resolve5, reject });
66448
66494
  }));
66449
66495
  await this.sendInnerFrame(inner);
66450
66496
  return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
@@ -66502,8 +66548,8 @@ var FetchproxyServer = class {
66502
66548
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
66503
66549
  }
66504
66550
  };
66505
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66506
- this.pendingCapture.set(id, { resolve: resolve2, reject });
66551
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66552
+ this.pendingCapture.set(id, { resolve: resolve5, reject });
66507
66553
  }));
66508
66554
  await this.sendInnerFrame(inner);
66509
66555
  return this._withVerbTimeout(
@@ -66550,8 +66596,8 @@ var FetchproxyServer = class {
66550
66596
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
66551
66597
  }
66552
66598
  };
66553
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66554
- this.pendingRedirect.set(id, { resolve: resolve2, reject });
66599
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66600
+ this.pendingRedirect.set(id, { resolve: resolve5, reject });
66555
66601
  }));
66556
66602
  await this.sendInnerFrame(inner);
66557
66603
  return this._withVerbTimeout(
@@ -66646,8 +66692,8 @@ var FetchproxyServer = class {
66646
66692
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
66647
66693
  }
66648
66694
  };
66649
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66650
- this.pendingDownload.set(id, { resolve: resolve2, reject });
66695
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66696
+ this.pendingDownload.set(id, { resolve: resolve5, reject });
66651
66697
  }));
66652
66698
  await this.sendInnerFrame(inner);
66653
66699
  return this._withVerbTimeout(
@@ -66707,8 +66753,8 @@ var FetchproxyServer = class {
66707
66753
  keys: [...opts.keys]
66708
66754
  }
66709
66755
  };
66710
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66711
- this.pendingIdb.set(id, { resolve: resolve2, reject });
66756
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66757
+ this.pendingIdb.set(id, { resolve: resolve5, reject });
66712
66758
  }));
66713
66759
  await this.sendInnerFrame(inner);
66714
66760
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
@@ -66747,8 +66793,8 @@ var FetchproxyServer = class {
66747
66793
  op: "read_dom",
66748
66794
  init: { origin, names: [...opts.names] }
66749
66795
  };
66750
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66751
- this.pendingStorage.set(id, { resolve: resolve2, reject });
66796
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66797
+ this.pendingStorage.set(id, { resolve: resolve5, reject });
66752
66798
  }));
66753
66799
  await this.sendInnerFrame(inner);
66754
66800
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
@@ -66793,8 +66839,8 @@ var FetchproxyServer = class {
66793
66839
  op: "read_dom_list",
66794
66840
  init: { origin, name: opts.name }
66795
66841
  };
66796
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66797
- this.pendingDomList.set(id, { resolve: resolve2, reject });
66842
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66843
+ this.pendingDomList.set(id, { resolve: resolve5, reject });
66798
66844
  }));
66799
66845
  await this.sendInnerFrame(inner);
66800
66846
  return this._withVerbTimeout(pending, this.pendingDomList, id, origin);
@@ -66839,8 +66885,8 @@ var FetchproxyServer = class {
66839
66885
  ...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
66840
66886
  }
66841
66887
  };
66842
- const pending = this.guardPending(new Promise((resolve2, reject) => {
66843
- this.pendingGraphql.set(id, { resolve: resolve2, reject });
66888
+ const pending = this.guardPending(new Promise((resolve5, reject) => {
66889
+ this.pendingGraphql.set(id, { resolve: resolve5, reject });
66844
66890
  }));
66845
66891
  const retrySafe = opts.retryOnTimeout === true;
66846
66892
  await this.sendInnerFrame(inner, { resendable: retrySafe });
@@ -67535,7 +67581,7 @@ function loadAccount(env = process.env) {
67535
67581
  // package.json
67536
67582
  var package_default = {
67537
67583
  name: "canvas-parent-mcp",
67538
- version: "2.1.2",
67584
+ version: "2.1.4",
67539
67585
  mcpName: "io.github.chrischall/canvas-parent-mcp",
67540
67586
  description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
67541
67587
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -67841,12 +67887,13 @@ function registerCredentialHealthcheckTool(args) {
67841
67887
  }
67842
67888
 
67843
67889
  // src/client.ts
67844
- import { writeFile as writeFile3, stat } from "fs/promises";
67890
+ import { constants as fsConstants } from "fs";
67891
+ import { lstat as lstat2, open as open2, stat } from "fs/promises";
67845
67892
 
67846
67893
  // node_modules/@chrischall/mcp-utils/dist/session/index.js
67847
- import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
67848
- import { dirname, join as join3, resolve } from "node:path";
67849
- import { homedir as homedir2 } from "node:os";
67894
+ import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
67895
+ import { dirname as dirname2, join as join4, resolve as resolve3 } from "node:path";
67896
+ import { homedir as homedir3 } from "node:os";
67850
67897
  import { randomBytes as randomBytes3, createHmac } from "node:crypto";
67851
67898
  function bindingDigest(secret, salt) {
67852
67899
  return createHmac("sha256", salt).update(secret).digest("hex").slice(0, 32);
@@ -67868,7 +67915,7 @@ function createFileStatePersistence(opts) {
67868
67915
  const secret = opts.boundTo;
67869
67916
  return {
67870
67917
  load() {
67871
- if (!existsSync(filePath))
67918
+ if (!existsSync2(filePath))
67872
67919
  return null;
67873
67920
  try {
67874
67921
  const raw = JSON.parse(readFileSync(filePath, "utf8"));
@@ -67898,9 +67945,9 @@ function createFileStatePersistence(opts) {
67898
67945
  }
67899
67946
  },
67900
67947
  save(state) {
67901
- const dir = dirname(filePath);
67948
+ const dir = dirname2(filePath);
67902
67949
  const tmp = `${filePath}.tmp-${randomBytes3(6).toString("hex")}`;
67903
- const dirExisted = existsSync(dir);
67950
+ const dirExisted = existsSync2(dir);
67904
67951
  try {
67905
67952
  mkdirSync(dir, { recursive: true, mode: 448 });
67906
67953
  if (!dirExisted)
@@ -67917,7 +67964,7 @@ function createFileStatePersistence(opts) {
67917
67964
  chmodSync(filePath, 384);
67918
67965
  } catch (err) {
67919
67966
  try {
67920
- if (existsSync(tmp))
67967
+ if (existsSync2(tmp))
67921
67968
  unlinkSync(tmp);
67922
67969
  } catch {
67923
67970
  }
@@ -67926,7 +67973,7 @@ function createFileStatePersistence(opts) {
67926
67973
  },
67927
67974
  clear() {
67928
67975
  try {
67929
- if (existsSync(filePath))
67976
+ if (existsSync2(filePath))
67930
67977
  unlinkSync(filePath);
67931
67978
  } catch {
67932
67979
  }
@@ -67934,11 +67981,11 @@ function createFileStatePersistence(opts) {
67934
67981
  };
67935
67982
  }
67936
67983
  function homeOf(env) {
67937
- return readEnvVar("HOME", { env }) ?? homedir2();
67984
+ return readEnvVar("HOME", { env }) ?? homedir3();
67938
67985
  }
67939
67986
  function resolveStateDir(opts = {}) {
67940
67987
  const env = opts.env;
67941
- return join3(readEnvVar("MCP_DATA_DIR", { env }) ?? homeOf(env), opts.subdir ?? "");
67988
+ return join4(readEnvVar("MCP_DATA_DIR", { env }) ?? homeOf(env), opts.subdir ?? "");
67942
67989
  }
67943
67990
  function resolveStateFile(opts) {
67944
67991
  const override = opts.envVar !== void 0 ? readEnvVar(opts.envVar, { env: opts.env }) : void 0;
@@ -67946,13 +67993,13 @@ function resolveStateFile(opts) {
67946
67993
  if (override === "~")
67947
67994
  return homeOf(opts.env);
67948
67995
  if (override.startsWith("~/"))
67949
- return join3(homeOf(opts.env), override.slice(2));
67950
- return resolve(override);
67996
+ return join4(homeOf(opts.env), override.slice(2));
67997
+ return resolve3(override);
67951
67998
  }
67952
67999
  const dirOpts = { env: opts.env };
67953
68000
  if (opts.subdir !== void 0)
67954
68001
  dirOpts.subdir = opts.subdir;
67955
- return join3(resolveStateDir(dirOpts), opts.fileName);
68002
+ return join4(resolveStateDir(dirOpts), opts.fileName);
67956
68003
  }
67957
68004
  var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
67958
68005
  function isPersistedCookieSession(raw) {
@@ -68242,7 +68289,8 @@ function reportCacheWriteFailure(err) {
68242
68289
  }
68243
68290
 
68244
68291
  // src/client.ts
68245
- import { dirname as dirname2 } from "path";
68292
+ import { dirname as dirname3, isAbsolute as isAbsolute5, join as join5, resolve as resolve4 } from "path";
68293
+ import { homedir as homedir4 } from "os";
68246
68294
 
68247
68295
  // src/session-login.ts
68248
68296
  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";
@@ -68340,6 +68388,8 @@ var CanvasClient = class {
68340
68388
  * what keeps the path alive once the browser cookie lapses.
68341
68389
  */
68342
68390
  refreshSession;
68391
+ /** Explicit download root; when unset, resolved per call (see {@link downloadRoot}). */
68392
+ outputDir;
68343
68393
  auth;
68344
68394
  /** Lazily-built shared refresh_token exchanger (oauth mode only). */
68345
68395
  oauthRefresh = null;
@@ -68355,6 +68405,7 @@ var CanvasClient = class {
68355
68405
  */
68356
68406
  constructor(account, opts = {}) {
68357
68407
  this.account = account;
68408
+ this.outputDir = opts.outputDir;
68358
68409
  this.sessionLoginFn = opts.sessionLogin ?? sessionLogin;
68359
68410
  this.refreshSession = opts.refreshSession ?? null;
68360
68411
  this.auth = new CookieSessionManager({
@@ -68396,33 +68447,85 @@ var CanvasClient = class {
68396
68447
  }
68397
68448
  return out;
68398
68449
  }
68450
+ /**
68451
+ * Download a Canvas file to disk.
68452
+ *
68453
+ * Both inputs arrive from tool arguments, which a prompt injection in
68454
+ * classmate/teacher-authored Canvas content can steer, so both are pinned
68455
+ * BEFORE any credential is attached:
68456
+ * - `path` must resolve to the configured Canvas origin (https, same host —
68457
+ * the `https://canvas@evil/` userinfo trick parses to a foreign host and is
68458
+ * refused) and name a `/files/<id>` resource. Only the initial request is
68459
+ * pinned: Canvas legitimately redirects to its file store, and fetch
68460
+ * strips Authorization/Cookie on a cross-origin redirect.
68461
+ * - `destinationPath` must lie inside the download root (see
68462
+ * {@link downloadRoot}), checked through symlinks; a relative path is
68463
+ * resolved against that root.
68464
+ */
68399
68465
  async download(path, destinationPath, opts = {}) {
68466
+ const url4 = this.pinDownloadUrl(path);
68467
+ const dest = this.confineDestination(destinationPath);
68400
68468
  let destStat = null;
68401
68469
  try {
68402
- destStat = await stat(destinationPath);
68470
+ destStat = await lstat2(dest);
68403
68471
  } catch {
68404
68472
  }
68473
+ if (destStat?.isSymbolicLink()) throw new InvalidPathError(destinationPath, "is a symlink");
68405
68474
  if (destStat?.isDirectory()) throw new InvalidPathError(destinationPath);
68406
68475
  if (destStat && !opts.overwrite) throw new FileExistsError(destinationPath);
68407
- const parent = dirname2(destinationPath);
68476
+ const parent = dirname3(dest);
68408
68477
  try {
68409
68478
  await stat(parent);
68410
68479
  } catch {
68411
68480
  throw new ParentDirectoryMissingError(parent);
68412
68481
  }
68413
- const url4 = /^https?:\/\//i.test(path) ? path : `${this.account.baseUrl}${path}`;
68414
68482
  const res = await this.authedFetch(url4, {});
68415
68483
  if (res.status === 401) throw new TokenExpiredError(this.account.mode);
68416
68484
  if (res.status === 404) throw new Error(`Canvas download 404 for ${path}`);
68417
68485
  if (!res.ok) throw new Error(`Canvas download ${res.status} for ${path}`);
68418
68486
  const buf = new Uint8Array(await res.arrayBuffer());
68419
- await writeFile3(destinationPath, buf);
68487
+ await writeConfined(dest, destinationPath, buf, opts.overwrite === true);
68420
68488
  return {
68421
- path: destinationPath,
68489
+ path: dest,
68422
68490
  bytes: buf.byteLength,
68423
68491
  contentType: res.headers.get("content-type") ?? "application/octet-stream"
68424
68492
  };
68425
68493
  }
68494
+ /**
68495
+ * The directory downloads are confined to: the constructor's `outputDir`,
68496
+ * else `CANVAS_OUTPUT_DIR`, else `~/Downloads`. Read per call so an env
68497
+ * change needs no client rebuild.
68498
+ */
68499
+ downloadRoot() {
68500
+ return expandPath(this.outputDir ?? readEnvVar("CANVAS_OUTPUT_DIR") ?? join5(homedir4(), "Downloads"));
68501
+ }
68502
+ confineDestination(destinationPath) {
68503
+ const root = this.downloadRoot();
68504
+ const candidate = destinationPath.startsWith("~") || isAbsolute5(destinationPath) ? expandPath(destinationPath) : resolve4(root, destinationPath);
68505
+ try {
68506
+ assertPathWithinRoots(candidate, [root]);
68507
+ } catch {
68508
+ throw new DownloadDestinationRejectedError(destinationPath, root);
68509
+ }
68510
+ return candidate;
68511
+ }
68512
+ pinDownloadUrl(path) {
68513
+ const raw = /^[a-z][a-z0-9+.-]*:/i.test(path) ? path : `${this.account.baseUrl}${path}`;
68514
+ let parsed;
68515
+ try {
68516
+ parsed = new URL(raw);
68517
+ } catch {
68518
+ throw new DownloadUrlRejectedError(path, "not a valid URL");
68519
+ }
68520
+ const origin = new URL(this.account.baseUrl).origin;
68521
+ if (parsed.protocol !== "https:" || parsed.origin !== origin || parsed.username || parsed.password) {
68522
+ throw new DownloadUrlRejectedError(path, `must be an https URL on ${origin}`);
68523
+ }
68524
+ if (!/\/files\/[^/]+/.test(parsed.pathname)) {
68525
+ throw new DownloadUrlRejectedError(path, "must name a Canvas file (a /files/<id> path)");
68526
+ }
68527
+ return parsed.href;
68528
+ }
68426
68529
  async doRawRequest(path, opts) {
68427
68530
  const url4 = /^https?:\/\//i.test(path) ? path : `${this.account.baseUrl}${path}`;
68428
68531
  const accept = opts.responseType === "text" ? "text/html, text/plain, */*" : "application/json+canvas-string-ids, application/json";
@@ -68535,8 +68638,8 @@ function parseJsonBody2(text) {
68535
68638
  }
68536
68639
  function injectPerPage(pathOrUrl, perPage) {
68537
68640
  if (/[?&]per_page=/.test(pathOrUrl)) return pathOrUrl;
68538
- const sep = pathOrUrl.includes("?") ? "&" : "?";
68539
- return `${pathOrUrl}${sep}per_page=${perPage}`;
68641
+ const sep2 = pathOrUrl.includes("?") ? "&" : "?";
68642
+ return `${pathOrUrl}${sep2}per_page=${perPage}`;
68540
68643
  }
68541
68644
  var TokenExpiredError = class extends Error {
68542
68645
  constructor(mode, detail) {
@@ -68557,9 +68660,48 @@ var CanvasUnreachableError = class extends Error {
68557
68660
  }
68558
68661
  status;
68559
68662
  };
68663
+ var DownloadUrlRejectedError = class extends Error {
68664
+ constructor(url4, reason) {
68665
+ super(`DownloadUrlRejected: ${reason}; use the \`url\` from canvas_list_course_files. Got: ${url4}`);
68666
+ this.url = url4;
68667
+ this.name = "DownloadUrlRejectedError";
68668
+ }
68669
+ url;
68670
+ };
68671
+ var DownloadDestinationRejectedError = class extends Error {
68672
+ constructor(path, root) {
68673
+ super(
68674
+ `DownloadDestinationRejected: destinationPath must be inside ${root} (set CANVAS_OUTPUT_DIR to change it). Got: ${path}`
68675
+ );
68676
+ this.path = path;
68677
+ this.root = root;
68678
+ this.name = "DownloadDestinationRejectedError";
68679
+ }
68680
+ path;
68681
+ root;
68682
+ };
68683
+ async function writeConfined(dest, requested, buf, overwrite) {
68684
+ const { O_WRONLY, O_CREAT, O_TRUNC, O_EXCL, O_NOFOLLOW } = fsConstants;
68685
+ const flags = O_WRONLY | O_CREAT | O_NOFOLLOW | (overwrite ? O_TRUNC : O_EXCL);
68686
+ let handle;
68687
+ try {
68688
+ handle = await open2(dest, flags, 384);
68689
+ } catch (e) {
68690
+ const code = e.code;
68691
+ if (code === "ELOOP") throw new InvalidPathError(requested, "is a symlink");
68692
+ if (code === "EEXIST") throw new FileExistsError(requested);
68693
+ if (code === "EISDIR") throw new InvalidPathError(requested);
68694
+ throw e;
68695
+ }
68696
+ try {
68697
+ await handle.writeFile(buf);
68698
+ } finally {
68699
+ await handle.close();
68700
+ }
68701
+ }
68560
68702
  var InvalidPathError = class extends Error {
68561
- constructor(path) {
68562
- super(`InvalidPath: destinationPath must be a filename, not a directory: ${path}`);
68703
+ constructor(path, reason = "must be a filename, not a directory") {
68704
+ super(`InvalidPath: destinationPath ${reason}: ${path}`);
68563
68705
  this.path = path;
68564
68706
  this.name = "InvalidPathError";
68565
68707
  }
@@ -68920,12 +69062,13 @@ function registerConversationTools(server, client) {
68920
69062
  return viewResponse(args.view, data);
68921
69063
  });
68922
69064
  server.registerTool("canvas_get_conversation", {
68923
- description: "Get a full Canvas conversation thread with all messages.",
69065
+ description: "Get a full Canvas conversation thread with all messages. Read-only: does not mark the conversation as read.",
68924
69066
  annotations: { readOnlyHint: true },
68925
69067
  inputSchema: getArgs3
68926
69068
  }, async (rawArgs) => {
68927
69069
  const args = getArgs3.parse(rawArgs);
68928
69070
  const path = buildPath(`/api/v1/conversations/${encodeURIComponent(args.id)}`, {
69071
+ auto_mark_as_read: false,
68929
69072
  "include[]": ["participant_avatars"]
68930
69073
  });
68931
69074
  const data = await client.request(path);
@@ -68963,8 +69106,8 @@ var listArgs4 = external_exports.object({
68963
69106
  contentTypes: external_exports.array(external_exports.string()).optional()
68964
69107
  });
68965
69108
  var downloadArgs = external_exports.object({
68966
- url: external_exports.string().describe("The url field returned by canvas_list_course_files (absolute https URL)."),
68967
- destinationPath: external_exports.string().describe("Absolute path where the file should be written."),
69109
+ 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)."),
69110
+ 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."),
68968
69111
  overwrite: external_exports.boolean().optional()
68969
69112
  });
68970
69113
  function registerFileTools(server, client) {
@@ -68982,8 +69125,8 @@ function registerFileTools(server, client) {
68982
69125
  return textContent(data);
68983
69126
  });
68984
69127
  server.registerTool("canvas_download_file", {
68985
- description: "Download a Canvas file to disk. `url` is the absolute URL from canvas_list_course_files; `destinationPath` is required.",
68986
- annotations: { destructiveHint: true },
69128
+ 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).",
69129
+ annotations: { destructiveHint: true, openWorldHint: true },
68987
69130
  inputSchema: downloadArgs
68988
69131
  }, async (rawArgs) => {
68989
69132
  const args = downloadArgs.parse(rawArgs);
@@ -68995,8 +69138,8 @@ function registerFileTools(server, client) {
68995
69138
  }
68996
69139
 
68997
69140
  // src/index.ts
68998
- var __dirname = dirname3(fileURLToPath(import.meta.url));
68999
- await loadDotenvSafely({ path: join4(__dirname, "..", ".env"), override: false });
69141
+ var __dirname = dirname4(fileURLToPath(import.meta.url));
69142
+ await loadDotenvSafely({ path: join6(__dirname, "..", ".env"), override: false });
69000
69143
  var resolved = null;
69001
69144
  var configError = null;
69002
69145
  try {
@@ -69006,7 +69149,7 @@ try {
69006
69149
  }
69007
69150
  await runMcp({
69008
69151
  name: "canvas",
69009
- version: "2.1.2",
69152
+ version: "2.1.4",
69010
69153
  // x-release-please-version
69011
69154
  tools: [
69012
69155
  // Outside the `if (resolved)` on purpose: an unconfigured server must