canvas-parent-mcp 1.2.4 → 1.2.5

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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Canvas LMS (Instructure) — courses, grades, assignments, announcements, planner, conversations, files",
10
- "version": "1.2.4"
10
+ "version": "1.2.5"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Canvas LMS",
16
16
  "source": "./",
17
17
  "description": "Canvas LMS (Instructure) MCP server for Claude — student/observer access via natural language",
18
- "version": "1.2.4",
18
+ "version": "1.2.5",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canvas-parent-mcp",
3
3
  "displayName": "Canvas LMS",
4
- "version": "1.2.4",
4
+ "version": "1.2.5",
5
5
  "description": "Canvas LMS (Instructure) MCP server for Claude — courses, grades, assignments, announcements, planner, conversations, files",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/bundle.js CHANGED
@@ -3651,7 +3651,12 @@ var require_fast_uri = __commonJS({
3651
3651
  }
3652
3652
  function resolve(baseURI, relativeURI, options) {
3653
3653
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3654
- const resolved2 = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
3654
+ const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3655
+ const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
3656
+ if (baseMalformed || relativeMalformed) {
3657
+ throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
3658
+ }
3659
+ const resolved2 = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
3655
3660
  schemelessOptions.skipEscape = true;
3656
3661
  return serialize(resolved2, schemelessOptions);
3657
3662
  }
@@ -3777,6 +3782,7 @@ var require_fast_uri = __commonJS({
3777
3782
  }
3778
3783
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3779
3784
  var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
3785
+ var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
3780
3786
  function getParseError(parsed, matches) {
3781
3787
  if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
3782
3788
  return 'URI path must start with "/" when authority is present.';
@@ -3811,6 +3817,20 @@ var require_fast_uri = __commonJS({
3811
3817
  parsed.error = "URI authority must not contain a literal backslash.";
3812
3818
  malformedAuthorityOrPort = true;
3813
3819
  }
3820
+ const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION);
3821
+ if (introducerMatch !== null) {
3822
+ const region = introducerMatch[1];
3823
+ const normalizedRegion = region.replace(/[\t\n\r]/g, "");
3824
+ if (normalizedRegion.length >= 2) {
3825
+ if (normalizedRegion.slice(0, 2) !== "//") {
3826
+ parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
3827
+ malformedAuthorityOrPort = true;
3828
+ } else if (region.length !== normalizedRegion.length) {
3829
+ parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
3830
+ malformedAuthorityOrPort = true;
3831
+ }
3832
+ }
3833
+ }
3814
3834
  const matches = uri.match(URI_PARSE);
3815
3835
  if (matches) {
3816
3836
  parsed.scheme = matches[1];
@@ -10573,7 +10593,7 @@ var require_websocket_server = __commonJS({
10573
10593
  });
10574
10594
 
10575
10595
  // src/index.ts
10576
- import { dirname as dirname2, join as join2 } from "path";
10596
+ import { dirname as dirname2, join as join3 } from "path";
10577
10597
  import { fileURLToPath } from "url";
10578
10598
 
10579
10599
  // node_modules/zod/v3/helpers/util.js
@@ -34998,8 +35018,15 @@ function createOAuth2Refresher(opts) {
34998
35018
  }
34999
35019
 
35000
35020
  // node_modules/@fetchproxy/protocol/dist/frames.js
35001
- var PROTOCOL_VERSION = 2;
35021
+ var PROTOCOL_VERSION = 3;
35002
35022
  var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
35023
+ function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) {
35024
+ const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
35025
+ out.set(mcpHelloNonce, 0);
35026
+ out.set(extHelloNonce, mcpHelloNonce.length);
35027
+ out.set(extensionSessionPub, mcpHelloNonce.length + extHelloNonce.length);
35028
+ return out;
35029
+ }
35003
35030
  var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
35004
35031
  "fetch",
35005
35032
  "read_cookies",
@@ -35010,7 +35037,8 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
35010
35037
  "read_indexed_db",
35011
35038
  "read_dom",
35012
35039
  "download",
35013
- "graphql"
35040
+ "graphql",
35041
+ "write_cookies"
35014
35042
  ]);
35015
35043
 
35016
35044
  // node_modules/@fetchproxy/protocol/dist/mcp-id.js
@@ -35121,6 +35149,15 @@ function assertHttpUrl(x, label) {
35121
35149
  throw new ProtocolError(`${label}: must be http(s), got ${u.protocol}`);
35122
35150
  }
35123
35151
  }
35152
+ function assertCookiePath(x, label) {
35153
+ assertString(x, label);
35154
+ if (!x.startsWith("/") || x.startsWith("//")) {
35155
+ throw new ProtocolError(`${label}: must be an absolute path like "/campus"`);
35156
+ }
35157
+ if (x.includes("?") || x.includes("#") || x.includes("\\")) {
35158
+ throw new ProtocolError(`${label}: must not contain a query, fragment, or backslash`);
35159
+ }
35160
+ }
35124
35161
  function assertHttpsOriginOnly(x, label) {
35125
35162
  assertString(x, label);
35126
35163
  let u;
@@ -35565,13 +35602,43 @@ function validateInnerRequest(raw) {
35565
35602
  }
35566
35603
  assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
35567
35604
  assertNonEmptyKeyArray(raw.init.keys, "inner.init.keys");
35605
+ if (raw.init.path !== void 0)
35606
+ assertCookiePath(raw.init.path, "inner.init.path");
35568
35607
  for (const k of Object.keys(raw.init)) {
35569
- if (k !== "origin" && k !== "keys") {
35608
+ if (k !== "origin" && k !== "keys" && k !== "path") {
35570
35609
  throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_cookies`);
35571
35610
  }
35572
35611
  }
35573
35612
  return raw;
35574
35613
  }
35614
+ if (raw.op === "write_cookies") {
35615
+ assertObject(raw.init, "inner.init");
35616
+ assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
35617
+ if (!Array.isArray(raw.init.cookies) || raw.init.cookies.length === 0) {
35618
+ throw new ProtocolError("inner.init.cookies: must be a non-empty array");
35619
+ }
35620
+ for (const [i, entry] of raw.init.cookies.entries()) {
35621
+ assertObject(entry, `inner.init.cookies[${i}]`);
35622
+ assertString(entry.name, `inner.init.cookies[${i}].name`);
35623
+ if (!SCOPE_KEY_RE.test(entry.name)) {
35624
+ throw new ProtocolError(`inner.init.cookies[${i}].name: invalid key ${JSON.stringify(entry.name)}`);
35625
+ }
35626
+ assertString(entry.value, `inner.init.cookies[${i}].value`);
35627
+ for (const k of Object.keys(entry)) {
35628
+ if (k !== "name" && k !== "value") {
35629
+ throw new ProtocolError(`inner.init.cookies[${i}]: unexpected field ${JSON.stringify(k)}`);
35630
+ }
35631
+ }
35632
+ }
35633
+ if (raw.init.path !== void 0)
35634
+ assertCookiePath(raw.init.path, "inner.init.path");
35635
+ for (const k of Object.keys(raw.init)) {
35636
+ if (k !== "origin" && k !== "cookies" && k !== "path") {
35637
+ throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on write_cookies`);
35638
+ }
35639
+ }
35640
+ return raw;
35641
+ }
35575
35642
  if (raw.op === "read_local_storage" || raw.op === "read_session_storage") {
35576
35643
  assertObject(raw.init, "inner.init");
35577
35644
  if (raw.init.origin === void 0) {
@@ -35758,7 +35825,7 @@ function validateInnerRequest(raw) {
35758
35825
  }
35759
35826
  return raw;
35760
35827
  }
35761
- throw new ProtocolError(`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"; got ${JSON.stringify(raw.op)}`);
35828
+ throw new ProtocolError(`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)}`);
35762
35829
  }
35763
35830
  function assertNonEmptyKeyArray(value, label) {
35764
35831
  if (!Array.isArray(value)) {
@@ -35819,6 +35886,18 @@ function validateInnerResponse(raw) {
35819
35886
  }
35820
35887
  return raw;
35821
35888
  }
35889
+ if (op === "write_cookies") {
35890
+ if (raw.written === void 0) {
35891
+ throw new ProtocolError("inner.written: missing on write_cookies response");
35892
+ }
35893
+ if (!Array.isArray(raw.written)) {
35894
+ throw new ProtocolError("inner.written: must be an array");
35895
+ }
35896
+ for (const [i, name] of raw.written.entries()) {
35897
+ assertString(name, `inner.written[${i}]`);
35898
+ }
35899
+ return raw;
35900
+ }
35822
35901
  if (op === "read_local_storage" || op === "read_session_storage") {
35823
35902
  if (raw.values === void 0) {
35824
35903
  throw new ProtocolError(`inner.values: missing on ${String(op)} response`);
@@ -36296,6 +36375,148 @@ async function awaitSessionReady(ready, opts) {
36296
36375
  }
36297
36376
  }
36298
36377
 
36378
+ // node_modules/@fetchproxy/server/dist/extension-trust.js
36379
+ import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
36380
+ import { join as join2 } from "node:path";
36381
+
36382
+ // node_modules/@fetchproxy/server/dist/identity.js
36383
+ import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
36384
+ import { join } from "node:path";
36385
+ import { homedir } from "node:os";
36386
+ var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
36387
+ var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
36388
+ function defaultIdentityDir() {
36389
+ return join(homedir(), ".fetchproxy", "identity");
36390
+ }
36391
+ function safeIdentityFileBase(serverName) {
36392
+ if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
36393
+ throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
36394
+ }
36395
+ return serverName.replace(/\//g, "_");
36396
+ }
36397
+ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
36398
+ const safeFile = safeIdentityFileBase(serverName);
36399
+ const path = join(dir, `${safeFile}.json`);
36400
+ await mkdir(dir, { recursive: true, mode: 448 });
36401
+ try {
36402
+ const raw = await readFile(path, "utf8");
36403
+ const j2 = JSON.parse(raw);
36404
+ return {
36405
+ x25519Priv: fromB64(j2.x25519Priv),
36406
+ x25519Pub: fromB64(j2.x25519Pub),
36407
+ ed25519Priv: fromB64(j2.ed25519Priv),
36408
+ ed25519Pub: fromB64(j2.ed25519Pub),
36409
+ createdAt: j2.createdAt
36410
+ };
36411
+ } catch (e) {
36412
+ if (e.code !== "ENOENT")
36413
+ throw e;
36414
+ }
36415
+ const x = await generateX25519();
36416
+ const ed = await generateEd25519();
36417
+ const id = {
36418
+ x25519Priv: x.privateKey,
36419
+ x25519Pub: x.publicKey,
36420
+ ed25519Priv: ed.privateKey,
36421
+ ed25519Pub: ed.publicKey,
36422
+ createdAt: Date.now()
36423
+ };
36424
+ const j = {
36425
+ x25519Priv: toB64(id.x25519Priv),
36426
+ x25519Pub: toB64(id.x25519Pub),
36427
+ ed25519Priv: toB64(id.ed25519Priv),
36428
+ ed25519Pub: toB64(id.ed25519Pub),
36429
+ createdAt: id.createdAt
36430
+ };
36431
+ await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
36432
+ await chmod(path, 384);
36433
+ return id;
36434
+ }
36435
+
36436
+ // node_modules/@fetchproxy/server/dist/extension-trust.js
36437
+ function fileExtensionTrust(args) {
36438
+ return {
36439
+ allowNew: args.allowNew,
36440
+ location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
36441
+ read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
36442
+ write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
36443
+ };
36444
+ }
36445
+ var TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
36446
+ function allowNewExtensionIdentity(explicit, env = process.env) {
36447
+ if (explicit !== void 0)
36448
+ return explicit;
36449
+ return env[TRUST_NEW_EXTENSION_ENV] === "1";
36450
+ }
36451
+ function decideExtensionTrust(args) {
36452
+ const { pin, hello, allowNew, serverName } = args;
36453
+ if (!pin)
36454
+ return { decision: "first-use" };
36455
+ if (pin.identityX25519Pub === hello.identityX25519Pub && pin.identityEd25519Pub === hello.identityEd25519Pub) {
36456
+ return { decision: "pinned" };
36457
+ }
36458
+ const trustPath = args.location ?? extensionTrustPathHint(serverName);
36459
+ if (allowNew) {
36460
+ return {
36461
+ decision: "replace",
36462
+ message: `[fetchproxy] ${serverName}: accepting a NEW extension identity because ${TRUST_NEW_EXTENSION_ENV}=1 \u2014 re-pinning. Unset it once the browser you expect is connected.`
36463
+ };
36464
+ }
36465
+ return {
36466
+ decision: "refused",
36467
+ message: `[fetchproxy] ${serverName}: refusing an extension whose identity is not the one this MCP paired with. If you re-installed the extension or moved to another browser, re-pair deliberately: run this MCP once with ${TRUST_NEW_EXTENSION_ENV}=1, or delete ${trustPath}. If you did neither, something else is answering as your browser.`
36468
+ };
36469
+ }
36470
+ function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
36471
+ return join2(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
36472
+ }
36473
+ function extensionTrustPathHint(serverName) {
36474
+ try {
36475
+ return extensionTrustPath(serverName);
36476
+ } catch {
36477
+ return join2(defaultIdentityDir(), "<server-name>.extension-trust.json");
36478
+ }
36479
+ }
36480
+ function isPin(x) {
36481
+ if (!x || typeof x !== "object")
36482
+ return false;
36483
+ const r = x;
36484
+ return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
36485
+ }
36486
+ async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
36487
+ const path = extensionTrustPath(serverName, dir);
36488
+ let raw;
36489
+ try {
36490
+ raw = await readFile2(path, "utf8");
36491
+ } catch (e) {
36492
+ if (e.code === "ENOENT")
36493
+ return null;
36494
+ throw e;
36495
+ }
36496
+ let parsed;
36497
+ try {
36498
+ parsed = JSON.parse(raw);
36499
+ } catch {
36500
+ throw new Error(`unreadable extension pin at ${path} (not JSON) \u2014 delete it to re-pair`);
36501
+ }
36502
+ if (!isPin(parsed)) {
36503
+ throw new Error(`unreadable extension pin at ${path} (wrong shape) \u2014 delete it to re-pair`);
36504
+ }
36505
+ return {
36506
+ identityX25519Pub: parsed.identityX25519Pub,
36507
+ identityEd25519Pub: parsed.identityEd25519Pub,
36508
+ pinnedAt: parsed.pinnedAt
36509
+ };
36510
+ }
36511
+ async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
36512
+ const path = extensionTrustPath(serverName, dir);
36513
+ await mkdir2(dir, { recursive: true, mode: 448 });
36514
+ const tmp = `${path}.tmp`;
36515
+ await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
36516
+ await chmod2(tmp, 384);
36517
+ await rename(tmp, path);
36518
+ }
36519
+
36299
36520
  // node_modules/@fetchproxy/server/dist/host.js
36300
36521
  var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
36301
36522
  var enc2 = new TextEncoder();
@@ -36349,9 +36570,12 @@ async function startHost(opts) {
36349
36570
  }
36350
36571
  resetSessionPromise();
36351
36572
  let extensionHello = null;
36573
+ let extensionClaim = null;
36352
36574
  wss.on("connection", (ws) => {
36353
36575
  let identified = null;
36354
36576
  let peerMcpId = null;
36577
+ let closed = false;
36578
+ let pinOnReady = false;
36355
36579
  ws.on("message", async (data) => {
36356
36580
  try {
36357
36581
  let frame;
@@ -36363,10 +36587,43 @@ async function startHost(opts) {
36363
36587
  return;
36364
36588
  }
36365
36589
  if (frame.type === "hello" && frame.role === "extension") {
36366
- if (extensionWs) {
36590
+ if (extensionWs || extensionClaim) {
36367
36591
  ws.close(1008, "extension already connected");
36368
36592
  return;
36369
36593
  }
36594
+ extensionClaim = ws;
36595
+ let pin;
36596
+ try {
36597
+ pin = await opts.extensionTrust.read();
36598
+ } catch (e) {
36599
+ console.error(`[fetchproxy] ${String(e)}`);
36600
+ if (extensionClaim === ws)
36601
+ extensionClaim = null;
36602
+ ws.close(1008, "extension pin unreadable");
36603
+ return;
36604
+ }
36605
+ const outcome = decideExtensionTrust({
36606
+ pin,
36607
+ hello: frame,
36608
+ allowNew: opts.extensionTrust.allowNew,
36609
+ serverName: opts.ownServerName,
36610
+ location: opts.extensionTrust.location
36611
+ });
36612
+ if (outcome.decision === "refused") {
36613
+ console.warn(outcome.message);
36614
+ if (extensionClaim === ws)
36615
+ extensionClaim = null;
36616
+ ws.close(1008, "extension identity is not the pinned one");
36617
+ return;
36618
+ }
36619
+ if (outcome.decision === "replace")
36620
+ console.warn(outcome.message);
36621
+ if (closed || ws.readyState !== import_websocket.default.OPEN) {
36622
+ if (extensionClaim === ws)
36623
+ extensionClaim = null;
36624
+ return;
36625
+ }
36626
+ pinOnReady = outcome.decision !== "pinned";
36370
36627
  identified = "extension";
36371
36628
  extensionWs = ws;
36372
36629
  extensionHello = frame;
@@ -36378,6 +36635,8 @@ async function startHost(opts) {
36378
36635
  console.error("[fetchproxy] onPairCode threw:", e);
36379
36636
  }
36380
36637
  }
36638
+ for (const slot of peers.values())
36639
+ slot.ws.send(JSON.stringify(frame));
36381
36640
  ws.send(JSON.stringify(ownHello));
36382
36641
  for (const slot of peers.values()) {
36383
36642
  ws.send(JSON.stringify(slot.helloFrame));
@@ -36413,6 +36672,8 @@ async function startHost(opts) {
36413
36672
  peers.set(frame.mcpId, { ws, helloFrame: frame });
36414
36673
  if (extensionWs)
36415
36674
  extensionWs.send(JSON.stringify(frame));
36675
+ if (extensionHello)
36676
+ ws.send(JSON.stringify(extensionHello));
36416
36677
  return;
36417
36678
  }
36418
36679
  if (frame.type === "ready") {
@@ -36424,7 +36685,7 @@ async function startHost(opts) {
36424
36685
  }
36425
36686
  const extEdPub = fromB64(extensionHello.identityEd25519Pub);
36426
36687
  const extNonce = fromB64(extensionHello.sessionNonce);
36427
- const msg = concatBytes(ownSessionNonce, extNonce);
36688
+ const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
36428
36689
  const sig = fromB64(frame.sessionSig);
36429
36690
  let sigOk = false;
36430
36691
  try {
@@ -36437,6 +36698,18 @@ async function startHost(opts) {
36437
36698
  ws.close(1008, "extension session signature invalid");
36438
36699
  return;
36439
36700
  }
36701
+ if (pinOnReady) {
36702
+ pinOnReady = false;
36703
+ try {
36704
+ await opts.extensionTrust.write({
36705
+ identityX25519Pub: extensionHello.identityX25519Pub,
36706
+ identityEd25519Pub: extensionHello.identityEd25519Pub,
36707
+ pinnedAt: Date.now()
36708
+ });
36709
+ } catch (e) {
36710
+ console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
36711
+ }
36712
+ }
36440
36713
  const extPub = fromB64(frame.extensionSessionPub);
36441
36714
  const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
36442
36715
  const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
@@ -36490,6 +36763,9 @@ async function startHost(opts) {
36490
36763
  }
36491
36764
  });
36492
36765
  ws.on("close", () => {
36766
+ closed = true;
36767
+ if (extensionClaim === ws)
36768
+ extensionClaim = null;
36493
36769
  if (identified === "extension" && extensionWs === ws) {
36494
36770
  extensionWs = null;
36495
36771
  extensionHello = null;
@@ -36580,11 +36856,84 @@ async function startPeer(opts) {
36580
36856
  resolveFirstReady = resolve;
36581
36857
  rejectFirstReady = reject;
36582
36858
  });
36859
+ let extensionHello = null;
36860
+ let warnedUnverifiable = false;
36861
+ let cachedPin = void 0;
36862
+ const authenticateExtension = async (sessionSig, extensionSessionPub) => {
36863
+ if (!extensionHello) {
36864
+ if (opts.requireExtensionIdentity) {
36865
+ console.error(`[fetchproxy] ${opts.serverName}: the concentrator does not forward the extension's identity, so this session cannot be verified \u2014 refusing. Upgrade the MCP holding the bridge port to 1.12.0 or later.`);
36866
+ return false;
36867
+ }
36868
+ if (!warnedUnverifiable) {
36869
+ warnedUnverifiable = true;
36870
+ console.warn(`[fetchproxy] ${opts.serverName}: the concentrator does not forward the extension's identity (pre-1.12.0), so this peer cannot verify which browser it is talking to. Upgrade the MCP holding the bridge port to close this.`);
36871
+ }
36872
+ return true;
36873
+ }
36874
+ const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
36875
+ let sigOk = false;
36876
+ try {
36877
+ sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
36878
+ } catch {
36879
+ sigOk = false;
36880
+ }
36881
+ if (!sigOk) {
36882
+ console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
36883
+ return false;
36884
+ }
36885
+ if (cachedPin === void 0) {
36886
+ try {
36887
+ cachedPin = await opts.extensionTrust.read();
36888
+ } catch (e) {
36889
+ console.error(`[fetchproxy] ${String(e)}`);
36890
+ return false;
36891
+ }
36892
+ }
36893
+ const pin = cachedPin;
36894
+ const outcome = decideExtensionTrust({
36895
+ pin,
36896
+ hello: extensionHello,
36897
+ allowNew: opts.extensionTrust.allowNew,
36898
+ serverName: opts.serverName,
36899
+ location: opts.extensionTrust.location
36900
+ });
36901
+ if (outcome.decision === "refused") {
36902
+ console.warn(outcome.message);
36903
+ return false;
36904
+ }
36905
+ if (outcome.decision === "replace")
36906
+ console.warn(outcome.message);
36907
+ if (outcome.decision !== "pinned") {
36908
+ try {
36909
+ const written = {
36910
+ identityX25519Pub: extensionHello.identityX25519Pub,
36911
+ identityEd25519Pub: extensionHello.identityEd25519Pub,
36912
+ pinnedAt: Date.now()
36913
+ };
36914
+ await opts.extensionTrust.write(written);
36915
+ cachedPin = written;
36916
+ } catch (e) {
36917
+ console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
36918
+ }
36919
+ }
36920
+ return true;
36921
+ };
36583
36922
  const onMessage = async (data) => {
36584
36923
  try {
36585
36924
  const raw = JSON.parse(data.toString());
36586
36925
  const frame = validateFrame(raw);
36926
+ if (frame.type === "hello" && frame.role === "extension") {
36927
+ extensionHello = frame;
36928
+ return;
36929
+ }
36587
36930
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
36931
+ const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
36932
+ if (!authorised) {
36933
+ ws.close(1008, "extension identity refused");
36934
+ rejectFirstReady(new Error("peer: extension identity refused"));
36935
+ return;
36936
+ }
36588
36937
  const extPub = fromB64(frame.extensionSessionPub);
36589
36938
  const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
36590
36939
  const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
@@ -36665,57 +37014,6 @@ async function startPeer(opts) {
36665
37014
  return handle;
36666
37015
  }
36667
37016
 
36668
- // node_modules/@fetchproxy/server/dist/identity.js
36669
- import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
36670
- import { join } from "node:path";
36671
- import { homedir } from "node:os";
36672
- var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
36673
- var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
36674
- function defaultIdentityDir() {
36675
- return join(homedir(), ".fetchproxy", "identity");
36676
- }
36677
- async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
36678
- if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
36679
- throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
36680
- }
36681
- const safeFile = serverName.replace(/\//g, "_");
36682
- const path = join(dir, `${safeFile}.json`);
36683
- await mkdir(dir, { recursive: true, mode: 448 });
36684
- try {
36685
- const raw = await readFile(path, "utf8");
36686
- const j2 = JSON.parse(raw);
36687
- return {
36688
- x25519Priv: fromB64(j2.x25519Priv),
36689
- x25519Pub: fromB64(j2.x25519Pub),
36690
- ed25519Priv: fromB64(j2.ed25519Priv),
36691
- ed25519Pub: fromB64(j2.ed25519Pub),
36692
- createdAt: j2.createdAt
36693
- };
36694
- } catch (e) {
36695
- if (e.code !== "ENOENT")
36696
- throw e;
36697
- }
36698
- const x = await generateX25519();
36699
- const ed = await generateEd25519();
36700
- const id = {
36701
- x25519Priv: x.privateKey,
36702
- x25519Pub: x.publicKey,
36703
- ed25519Priv: ed.privateKey,
36704
- ed25519Pub: ed.publicKey,
36705
- createdAt: Date.now()
36706
- };
36707
- const j = {
36708
- x25519Priv: toB64(id.x25519Priv),
36709
- x25519Pub: toB64(id.x25519Pub),
36710
- ed25519Priv: toB64(id.ed25519Priv),
36711
- ed25519Pub: toB64(id.ed25519Pub),
36712
- createdAt: id.createdAt
36713
- };
36714
- await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
36715
- await chmod(path, 384);
36716
- return id;
36717
- }
36718
-
36719
37017
  // node_modules/@fetchproxy/server/dist/error-kind.js
36720
37018
  function classifyFetchError(error51) {
36721
37019
  if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
@@ -36797,6 +37095,39 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
36797
37095
  this.hint = hint;
36798
37096
  }
36799
37097
  };
37098
+ var FetchproxyHintedError = class extends FetchproxyProtocolError {
37099
+ /** The extension's raw rejection, unmodified. */
37100
+ originalError;
37101
+ /** What the user should actually do, in prose. */
37102
+ hint;
37103
+ constructor(originalError, hint) {
37104
+ super(`${originalError} \u2014 ${hint}`);
37105
+ this.name = "FetchproxyHintedError";
37106
+ this.originalError = originalError;
37107
+ this.hint = hint;
37108
+ }
37109
+ };
37110
+ var FetchproxyScopeError = class extends FetchproxyHintedError {
37111
+ constructor(originalError) {
37112
+ super(originalError, "the declared scope changed since you paired, so the extension is refusing the request. Revoke this MCP in the Transporter extension popup, then re-run \u2014 you will be asked to approve the new scope. This is not a version problem and does not need an update.");
37113
+ this.name = "FetchproxyScopeError";
37114
+ }
37115
+ };
37116
+ var FetchproxyNoTabError = class extends FetchproxyHintedError {
37117
+ constructor(originalError) {
37118
+ super(originalError, "open a tab on that host and sign in, then re-run. This is not a version problem and does not need an update.");
37119
+ this.name = "FetchproxyNoTabError";
37120
+ }
37121
+ };
37122
+ var SCOPE_REJECTION = /not in declared/;
37123
+ var NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
37124
+ function protocolErrorFrom(error51) {
37125
+ if (SCOPE_REJECTION.test(error51))
37126
+ return new FetchproxyScopeError(error51);
37127
+ if (NO_TAB_REJECTION.test(error51))
37128
+ return new FetchproxyNoTabError(error51);
37129
+ return new FetchproxyProtocolError(error51);
37130
+ }
36800
37131
  var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
36801
37132
  url;
36802
37133
  timeoutMs;
@@ -36826,6 +37157,17 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
36826
37157
  }
36827
37158
  };
36828
37159
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
37160
+ function normalizeCookiePath(path) {
37161
+ if (path === void 0 || path === "")
37162
+ return void 0;
37163
+ const trimmed = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
37164
+ try {
37165
+ assertCookiePath(trimmed, "path");
37166
+ } catch (e) {
37167
+ throw new Error(`FetchproxyServer: ${e instanceof Error ? e.message : String(e)} (got ${JSON.stringify(path)})`);
37168
+ }
37169
+ return trimmed;
37170
+ }
36829
37171
  function assertSubdomainLabel(label) {
36830
37172
  if (!SUBDOMAIN_LABEL_RE.test(label)) {
36831
37173
  throw new Error(`FetchproxyServer: subdomain must be a DNS label like "www" or "api" (or dot-separated like "auth.api"), got ${JSON.stringify(label)}`);
@@ -36890,6 +37232,9 @@ var FetchproxyServer = class {
36890
37232
  // them off from `pending` (fetch) and `pendingReadCookies` (legacy
36891
37233
  // string-shape) so the response routing in `onInner` stays linear.
36892
37234
  pendingStorage = /* @__PURE__ */ new Map();
37235
+ // 1.12.0+: write-cookies awaiters resolve the list of names actually
37236
+ // written, so a caller can confirm rather than assume.
37237
+ pendingWriteCookies = /* @__PURE__ */ new Map();
36893
37238
  // 0.3.0+: capture-header awaiters resolve a single string.
36894
37239
  pendingCapture = /* @__PURE__ */ new Map();
36895
37240
  // capture_redirect awaiters resolve the captured redirect URL string.
@@ -37016,6 +37361,8 @@ var FetchproxyServer = class {
37016
37361
  keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
37017
37362
  keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
37018
37363
  identityDir: opts.identityDir,
37364
+ allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
37365
+ requireExtensionIdentity: opts.requireExtensionIdentity,
37019
37366
  onPairCode: opts.onPairCode
37020
37367
  };
37021
37368
  }
@@ -37115,7 +37462,8 @@ var FetchproxyServer = class {
37115
37462
  ownSessionStoragePointers: this.opts.sessionStoragePointers,
37116
37463
  ownDomSelectors: this.opts.domSelectors,
37117
37464
  ownGraphqlOps: this.opts.graphqlOps,
37118
- onPairCode: this.opts.onPairCode
37465
+ onPairCode: this.opts.onPairCode,
37466
+ extensionTrust: this.extensionTrust()
37119
37467
  });
37120
37468
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
37121
37469
  this.hostHandle.onExtensionDisconnect(() => {
@@ -37144,7 +37492,9 @@ var FetchproxyServer = class {
37144
37492
  localStoragePointers: this.opts.localStoragePointers,
37145
37493
  sessionStoragePointers: this.opts.sessionStoragePointers,
37146
37494
  domSelectors: this.opts.domSelectors,
37147
- graphqlOps: this.opts.graphqlOps
37495
+ graphqlOps: this.opts.graphqlOps,
37496
+ extensionTrust: this.extensionTrust(),
37497
+ requireExtensionIdentity: this.opts.requireExtensionIdentity
37148
37498
  });
37149
37499
  this.peerHandle.onInner((inner) => this.onInner(inner));
37150
37500
  this.peerHandle.onRenegotiate(() => {
@@ -37283,6 +37633,23 @@ var FetchproxyServer = class {
37283
37633
  markActive() {
37284
37634
  this.noteActivityForKeepalive();
37285
37635
  }
37636
+ /**
37637
+ * #208: this MCP's pin on the extension's identity, stored beside its own
37638
+ * identity key and so following `identityDir` wherever the caller put it.
37639
+ *
37640
+ * `allowNewExtensionIdentity` falls back to an environment variable when the
37641
+ * caller expressed no opinion, because the thirteen MCPs that construct this
37642
+ * class are separate packages: an operator whose extension re-install has
37643
+ * just locked all of them out needs one lever that does not require patching
37644
+ * every one of them.
37645
+ */
37646
+ extensionTrust() {
37647
+ return fileExtensionTrust({
37648
+ serverName: this.opts.serverName,
37649
+ dir: this.opts.identityDir,
37650
+ allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
37651
+ });
37652
+ }
37286
37653
  noteActivityForKeepalive() {
37287
37654
  const intervalMs = this.opts.keepAliveIntervalMs;
37288
37655
  if (intervalMs <= 0)
@@ -37346,6 +37713,7 @@ var FetchproxyServer = class {
37346
37713
  this.pending.delete(id);
37347
37714
  this.pendingReadCookies.delete(id);
37348
37715
  this.pendingStorage.delete(id);
37716
+ this.pendingWriteCookies.delete(id);
37349
37717
  this.pendingCapture.delete(id);
37350
37718
  this.pendingRedirect.delete(id);
37351
37719
  this.pendingDownload.delete(id);
@@ -37467,7 +37835,7 @@ var FetchproxyServer = class {
37467
37835
  port: this.opts.port
37468
37836
  });
37469
37837
  }
37470
- return new FetchproxyProtocolError(result.error);
37838
+ return protocolErrorFrom(result.error);
37471
37839
  }
37472
37840
  /**
37473
37841
  * Convenience wrapper around `fetch()`. Builds the URL from a path
@@ -37503,10 +37871,20 @@ var FetchproxyServer = class {
37503
37871
  }
37504
37872
  const url2 = isAbsolute ? path : `https://${host}${path}`;
37505
37873
  assertUrlInDomains("request url", url2, this.opts.domains);
37874
+ let tabUrl = `https://${host}/`;
37875
+ if (opts.viaTab !== void 0) {
37876
+ try {
37877
+ new URL(opts.viaTab);
37878
+ } catch {
37879
+ throw new Error(`FetchproxyServer.request: viaTab is not a valid URL: ${JSON.stringify(opts.viaTab)}`);
37880
+ }
37881
+ assertUrlInDomains("viaTab", opts.viaTab, this.opts.domains);
37882
+ tabUrl = opts.viaTab;
37883
+ }
37506
37884
  const init = {
37507
37885
  url: url2,
37508
37886
  method,
37509
- tabUrl: `https://${host}/`,
37887
+ tabUrl,
37510
37888
  headers: opts.headers,
37511
37889
  body: opts.body
37512
37890
  };
@@ -37718,9 +38096,14 @@ var FetchproxyServer = class {
37718
38096
  let inner;
37719
38097
  if (opts.keys !== void 0) {
37720
38098
  this.assertScopeSubset(opts.keys, this.opts.cookieKeys, "cookieKeys");
38099
+ const cookiePath = normalizeCookiePath(opts.path);
37721
38100
  const initV3 = {
38101
+ // Origin stays BARE. The path travels as its own validated field —
38102
+ // `assertHttpsOriginOnly` deliberately refuses a path here so one
38103
+ // cannot be used to re-point the read past the domain gate.
37722
38104
  origin: `https://${host}`,
37723
- keys: [...opts.keys]
38105
+ keys: [...opts.keys],
38106
+ ...cookiePath !== void 0 ? { path: cookiePath } : {}
37724
38107
  };
37725
38108
  inner = { type: "request", id, op: "read_cookies", init: initV3 };
37726
38109
  } else {
@@ -37733,10 +38116,65 @@ var FetchproxyServer = class {
37733
38116
  await this.sendInnerFrame(inner);
37734
38117
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
37735
38118
  if (!result.ok) {
37736
- throw new FetchproxyProtocolError(result.error);
38119
+ throw protocolErrorFrom(result.error);
37737
38120
  }
37738
38121
  return result.cookies;
37739
38122
  }
38123
+ /**
38124
+ * 1.12.0+: overwrite the value of cookies this MCP already declares.
38125
+ *
38126
+ * The bridge's only write verb, and it exists for one failure class. Sites
38127
+ * that ROTATE a credential cookie hand back a new value on every refresh; if
38128
+ * the MCP refreshes and keeps the result to itself, the copy in the browser's
38129
+ * cookie jar is dead, and the user gets signed out of a tab they never
38130
+ * touched — usually reported to them as "inactivity". Writing the rotated
38131
+ * value back is the only thing that repairs it.
38132
+ *
38133
+ * Requires `'write_cookies'` in capabilities, which the user approves at pair
38134
+ * time as its own line. Every name must ALSO be in declared `cookieKeys`: a
38135
+ * write can never reach a cookie the MCP was not already trusted to read, so
38136
+ * granting it cannot widen which cookies are in play — only what may be done
38137
+ * to the ones already listed.
38138
+ *
38139
+ * The extension refuses the whole request unless every named cookie already
38140
+ * exists; this refreshes a value in place and deliberately cannot author new
38141
+ * cookies. Returns the names actually written.
38142
+ */
38143
+ async writeCookies(opts) {
38144
+ if (!this.opts.capabilities.includes("write_cookies")) {
38145
+ throw new Error('FetchproxyServer.writeCookies(): MCP did not declare "write_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
38146
+ }
38147
+ const names = Object.keys(opts.cookies);
38148
+ if (names.length === 0) {
38149
+ throw new Error("FetchproxyServer.writeCookies(): no cookies given");
38150
+ }
38151
+ await this.ensureConnected();
38152
+ this.throwIfPendingPair();
38153
+ if (opts.subdomain !== void 0)
38154
+ assertSubdomainLabel(opts.subdomain);
38155
+ const baseDomain = this.resolveBaseDomain(opts.domain);
38156
+ const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
38157
+ this.assertScopeSubset(names, this.opts.cookieKeys, "cookieKeys");
38158
+ const cookiePath = normalizeCookiePath(opts.path);
38159
+ const id = this.nextRequestId++;
38160
+ const inner = {
38161
+ type: "request",
38162
+ id,
38163
+ op: "write_cookies",
38164
+ init: {
38165
+ // Bare origin, same invariant as the read path: a path must never be
38166
+ // able to move the request past the domain gate.
38167
+ origin: `https://${host}`,
38168
+ cookies: Object.entries(opts.cookies).map(([name, value]) => ({ name, value })),
38169
+ ...cookiePath !== void 0 ? { path: cookiePath } : {}
38170
+ }
38171
+ };
38172
+ const pending = new Promise((resolve, reject) => {
38173
+ this.pendingWriteCookies.set(id, { resolve, reject });
38174
+ });
38175
+ await this.sendInnerFrame(inner);
38176
+ return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
38177
+ }
37740
38178
  /**
37741
38179
  * 0.3.0+: read declared localStorage keys from the user's signed-in
37742
38180
  * tab. Requires `'read_local_storage'` in capabilities AND each key
@@ -38283,7 +38721,7 @@ var FetchproxyServer = class {
38283
38721
  storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
38284
38722
  }
38285
38723
  } else {
38286
- storageCb.reject(new FetchproxyProtocolError(inner.error));
38724
+ storageCb.reject(protocolErrorFrom(inner.error));
38287
38725
  }
38288
38726
  return;
38289
38727
  }
@@ -38297,7 +38735,7 @@ var FetchproxyServer = class {
38297
38735
  captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
38298
38736
  }
38299
38737
  } else {
38300
- captureCb.reject(new FetchproxyProtocolError(inner.error));
38738
+ captureCb.reject(protocolErrorFrom(inner.error));
38301
38739
  }
38302
38740
  return;
38303
38741
  }
@@ -38311,7 +38749,7 @@ var FetchproxyServer = class {
38311
38749
  redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
38312
38750
  }
38313
38751
  } else {
38314
- redirectCb.reject(new FetchproxyProtocolError(inner.error));
38752
+ redirectCb.reject(protocolErrorFrom(inner.error));
38315
38753
  }
38316
38754
  return;
38317
38755
  }
@@ -38325,7 +38763,7 @@ var FetchproxyServer = class {
38325
38763
  idbCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_indexed_db awaiter`));
38326
38764
  }
38327
38765
  } else {
38328
- idbCb.reject(new FetchproxyProtocolError(inner.error));
38766
+ idbCb.reject(protocolErrorFrom(inner.error));
38329
38767
  }
38330
38768
  return;
38331
38769
  }
@@ -38339,7 +38777,7 @@ var FetchproxyServer = class {
38339
38777
  downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
38340
38778
  }
38341
38779
  } else {
38342
- downloadCb.reject(new FetchproxyProtocolError(inner.error));
38780
+ downloadCb.reject(protocolErrorFrom(inner.error));
38343
38781
  }
38344
38782
  return;
38345
38783
  }
@@ -38353,7 +38791,17 @@ var FetchproxyServer = class {
38353
38791
  graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
38354
38792
  }
38355
38793
  } else {
38356
- graphqlCb.reject(new FetchproxyProtocolError(inner.error));
38794
+ graphqlCb.reject(protocolErrorFrom(inner.error));
38795
+ }
38796
+ return;
38797
+ }
38798
+ const writeCookiesCb = this.pendingWriteCookies.get(inner.id);
38799
+ if (writeCookiesCb) {
38800
+ this.pendingWriteCookies.delete(inner.id);
38801
+ if (inner.ok && inner.op === "write_cookies") {
38802
+ writeCookiesCb.resolve([...inner.written]);
38803
+ } else {
38804
+ writeCookiesCb.reject(protocolErrorFrom(inner.ok ? "write_cookies response had the wrong op" : inner.error));
38357
38805
  }
38358
38806
  return;
38359
38807
  }
@@ -38396,6 +38844,9 @@ var FetchproxyServer = class {
38396
38844
  for (const { reject } of this.pendingStorage.values())
38397
38845
  reject(err);
38398
38846
  this.pendingStorage.clear();
38847
+ for (const { reject } of this.pendingWriteCookies.values())
38848
+ reject(err);
38849
+ this.pendingWriteCookies.clear();
38399
38850
  for (const { reject } of this.pendingCapture.values())
38400
38851
  reject(err);
38401
38852
  this.pendingCapture.clear();
@@ -38546,7 +38997,9 @@ async function runOneLift(opts) {
38546
38997
  if (opts.declare.cookies.length > 0) {
38547
38998
  const joined = await server.readCookies({
38548
38999
  keys: opts.declare.cookies,
38549
- ...storageDomainOpts
39000
+ ...storageDomainOpts,
39001
+ // Cookie-only: the other buckets are origin-scoped and ignore path.
39002
+ ...opts.storagePath !== void 0 ? { path: opts.storagePath } : {}
38550
39003
  });
38551
39004
  for (const piece of joined.split("; ")) {
38552
39005
  if (!piece)
@@ -38720,7 +39173,7 @@ function loadAccount(env = process.env) {
38720
39173
  // package.json
38721
39174
  var package_default = {
38722
39175
  name: "canvas-parent-mcp",
38723
- version: "1.2.4",
39176
+ version: "1.2.5",
38724
39177
  mcpName: "io.github.chrischall/canvas-parent-mcp",
38725
39178
  description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
38726
39179
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38765,8 +39218,8 @@ var package_default = {
38765
39218
  },
38766
39219
  dependencies: {
38767
39220
  "@chrischall/mcp-utils": "^0.14.0",
38768
- "@fetchproxy/bootstrap": "^1.9.1",
38769
- "@fetchproxy/server": "^1.7.0",
39221
+ "@fetchproxy/bootstrap": "^2.0.0",
39222
+ "@fetchproxy/server": "^2.0.0",
38770
39223
  "@modelcontextprotocol/sdk": "^1.29.0",
38771
39224
  dotenv: "^17.4.2",
38772
39225
  zod: "^4.4.3"
@@ -38873,7 +39326,7 @@ async function liftBrowserCookie(declaredDomain, baseHost) {
38873
39326
  }
38874
39327
 
38875
39328
  // src/client.ts
38876
- import { writeFile as writeFile2, stat } from "fs/promises";
39329
+ import { writeFile as writeFile3, stat } from "fs/promises";
38877
39330
  import { dirname } from "path";
38878
39331
 
38879
39332
  // node_modules/@chrischall/mcp-utils/dist/session/index.js
@@ -39186,7 +39639,7 @@ var CanvasClient = class {
39186
39639
  if (res.status === 404) throw new Error(`Canvas download 404 for ${path}`);
39187
39640
  if (!res.ok) throw new Error(`Canvas download ${res.status} for ${path}`);
39188
39641
  const buf = new Uint8Array(await res.arrayBuffer());
39189
- await writeFile2(destinationPath, buf);
39642
+ await writeFile3(destinationPath, buf);
39190
39643
  return {
39191
39644
  path: destinationPath,
39192
39645
  bytes: buf.byteLength,
@@ -39710,7 +40163,7 @@ function registerFileTools(server, client) {
39710
40163
 
39711
40164
  // src/index.ts
39712
40165
  var __dirname = dirname2(fileURLToPath(import.meta.url));
39713
- await loadDotenvSafely({ path: join2(__dirname, "..", ".env"), override: false });
40166
+ await loadDotenvSafely({ path: join3(__dirname, "..", ".env"), override: false });
39714
40167
  var resolved = null;
39715
40168
  var configError = null;
39716
40169
  try {
@@ -39720,7 +40173,7 @@ try {
39720
40173
  }
39721
40174
  await runMcp({
39722
40175
  name: "canvas",
39723
- version: "1.2.4",
40176
+ version: "1.2.5",
39724
40177
  // x-release-please-version
39725
40178
  tools: [
39726
40179
  (server) => {
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@ catch (e) {
46
46
  // host's install-time `tools/list` succeeds.
47
47
  await runMcp({
48
48
  name: 'canvas',
49
- version: '1.2.4', // x-release-please-version
49
+ version: '1.2.5', // x-release-please-version
50
50
  tools: [
51
51
  (server) => {
52
52
  if (resolved) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvas-parent-mcp",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "mcpName": "io.github.chrischall/canvas-parent-mcp",
5
5
  "description": "Canvas LMS (Instructure) MCP server — student/observer read access with optional file download",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -45,8 +45,8 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@chrischall/mcp-utils": "^0.14.0",
48
- "@fetchproxy/bootstrap": "^1.9.1",
49
- "@fetchproxy/server": "^1.7.0",
48
+ "@fetchproxy/bootstrap": "^2.0.0",
49
+ "@fetchproxy/server": "^2.0.0",
50
50
  "@modelcontextprotocol/sdk": "^1.29.0",
51
51
  "dotenv": "^17.4.2",
52
52
  "zod": "^4.4.3"
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/canvas-parent-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.2.4",
9
+ "version": "1.2.5",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "canvas-parent-mcp",
14
- "version": "1.2.4",
14
+ "version": "1.2.5",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },