canvas-parent-mcp 1.1.6 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.1.6"
10
+ "version": "1.1.7"
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.1.6",
18
+ "version": "1.1.7",
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.1.6",
4
+ "version": "1.1.7",
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/auth.js CHANGED
@@ -14,17 +14,20 @@
14
14
  // most reliable mode but most schools have disabled token creation
15
15
  // for non-admins. Unchanged from pre-fetchproxy behavior.
16
16
  //
17
- // 2. OAuth refresh token (existing)
18
- // CANVAS_CLIENT_ID + CANVAS_CLIENT_SECRET + CANVAS_REFRESH_TOKEN →
19
- // `grant_type=refresh_token` against `/login/oauth2/token`, bootstrap
20
- // via the bundled QR-login helper. Unchanged.
21
- //
22
- // 3. Username/password session-scrape (existing)
17
+ // 2. Username/password session-scrape (existing)
23
18
  // CANVAS_USERNAME + CANVAS_PASSWORD → scrape `authenticity_token`
24
19
  // from `/login/canvas`, POST creds, capture `canvas_session` +
25
20
  // `pseudonym_credentials` cookies. Brittle (breaks on SSO/2FA and
26
21
  // every Canvas login-page restyling) but works for direct Canvas
27
- // accounts. Unchanged from pre-fetchproxy behavior.
22
+ // accounts. Takes precedence over OAuth (path 3) — see the
23
+ // precedence order in `loadAccount()` (config.ts), which returns the
24
+ // username/password account before checking the OAuth triple.
25
+ // Unchanged from pre-fetchproxy behavior.
26
+ //
27
+ // 3. OAuth refresh token (existing)
28
+ // CANVAS_CLIENT_ID + CANVAS_CLIENT_SECRET + CANVAS_REFRESH_TOKEN →
29
+ // `grant_type=refresh_token` against `/login/oauth2/token`, bootstrap
30
+ // via the bundled QR-login helper. Unchanged.
28
31
  //
29
32
  // 4. fetchproxy fallback (new)
30
33
  // When no env vars are set, lift the user's session out of their
package/dist/bundle.js CHANGED
@@ -13,7 +13,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
13
13
  throw Error('Dynamic require of "' + x + '" is not supported');
14
14
  });
15
15
  var __commonJS = (cb, mod) => function __require2() {
16
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
16
+ try {
17
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
18
+ } catch (e) {
19
+ throw mod = 0, e;
20
+ }
17
21
  };
18
22
  var __export = (target, all) => {
19
23
  for (var name in all)
@@ -35743,10 +35747,20 @@ async function openEncryptedFrame(sessionKey, frame) {
35743
35747
 
35744
35748
  // node_modules/@fetchproxy/server/dist/election.js
35745
35749
  import { createServer, Server as HttpServer } from "node:http";
35750
+ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
35746
35751
  async function electRole(opts) {
35747
35752
  const server = createServer();
35753
+ const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
35748
35754
  return new Promise((resolve, reject) => {
35755
+ let timer;
35756
+ const clearBindTimer = () => {
35757
+ if (timer) {
35758
+ clearTimeout(timer);
35759
+ timer = void 0;
35760
+ }
35761
+ };
35749
35762
  const onError = (e) => {
35763
+ clearBindTimer();
35750
35764
  server.removeListener("listening", onListening);
35751
35765
  if (e.code === "EADDRINUSE") {
35752
35766
  try {
@@ -35759,11 +35773,25 @@ async function electRole(opts) {
35759
35773
  }
35760
35774
  };
35761
35775
  const onListening = () => {
35776
+ clearBindTimer();
35762
35777
  server.removeListener("error", onError);
35763
35778
  resolve({ role: "host", server });
35764
35779
  };
35765
35780
  server.once("error", onError);
35766
35781
  server.once("listening", onListening);
35782
+ if (bindTimeoutMs > 0) {
35783
+ timer = setTimeout(() => {
35784
+ server.removeListener("error", onError);
35785
+ server.removeListener("listening", onListening);
35786
+ try {
35787
+ server.close();
35788
+ } catch {
35789
+ }
35790
+ reject(new Error(`fetchproxy: bind to ${opts.host}:${opts.port} timed out after ${bindTimeoutMs}ms (port may be in a bad state)`));
35791
+ }, bindTimeoutMs);
35792
+ if (typeof timer.unref === "function")
35793
+ timer.unref();
35794
+ }
35767
35795
  server.listen(opts.port, opts.host);
35768
35796
  });
35769
35797
  }
@@ -35856,6 +35884,44 @@ var SessionState = class {
35856
35884
  }
35857
35885
  };
35858
35886
 
35887
+ // node_modules/@fetchproxy/server/dist/session-ready.js
35888
+ var SESSION_READY_TIMEOUT_MS = 3e4;
35889
+ var FetchproxySessionNotReadyError = class extends Error {
35890
+ reason;
35891
+ pairCode;
35892
+ mcpId;
35893
+ hint;
35894
+ constructor(info) {
35895
+ const pairing = info.pairCode !== null && info.pairCode !== "";
35896
+ const hint = pairing ? `Open the Transporter extension popup and approve pair code ${info.pairCode} for "${info.mcpId}", then retry.` : `The extension is connected but hasn't confirmed a session for "${info.mcpId}" \u2014 sign in to the target site in that browser (and approve the requested scope if it changed), then retry.`;
35897
+ super(`fetchproxy: ${pairing ? "pairing not yet approved" : "no confirmed browser session"} for "${info.mcpId}". ${hint}`);
35898
+ this.name = "FetchproxySessionNotReadyError";
35899
+ this.reason = pairing ? "pair-required" : "not-ready";
35900
+ this.pairCode = pairing ? info.pairCode : null;
35901
+ this.mcpId = info.mcpId;
35902
+ this.hint = hint;
35903
+ Object.setPrototypeOf(this, new.target.prototype);
35904
+ }
35905
+ };
35906
+ async function awaitSessionReady(ready, opts) {
35907
+ const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
35908
+ if (ms <= 0)
35909
+ return ready;
35910
+ let timer;
35911
+ const timeout = new Promise((_, reject) => {
35912
+ timer = setTimeout(() => {
35913
+ reject(new FetchproxySessionNotReadyError({ mcpId: opts.mcpId, pairCode: opts.pendingPairCode() }));
35914
+ }, ms);
35915
+ timer.unref?.();
35916
+ });
35917
+ try {
35918
+ return await Promise.race([ready, timeout]);
35919
+ } finally {
35920
+ if (timer)
35921
+ clearTimeout(timer);
35922
+ }
35923
+ }
35924
+
35859
35925
  // node_modules/@fetchproxy/server/dist/host.js
35860
35926
  var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
35861
35927
  var enc2 = new TextEncoder();
@@ -35943,6 +36009,29 @@ async function startHost(opts) {
35943
36009
  return;
35944
36010
  }
35945
36011
  if (frame.type === "hello" && frame.role === "server") {
36012
+ const peerEdPub = fromB64(frame.identityEd25519Pub);
36013
+ const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
36014
+ const peerSig = fromB64(frame.sessionSig);
36015
+ let peerSigOk = false;
36016
+ try {
36017
+ peerSigOk = await ed25519Verify(peerEdPub, peerSigMsg, peerSig);
36018
+ } catch {
36019
+ peerSigOk = false;
36020
+ }
36021
+ if (!peerSigOk) {
36022
+ console.warn("[fetchproxy] peer hello signature invalid \u2014 refusing registration (possible squatter)");
36023
+ ws.close(1008, "peer hello signature invalid");
36024
+ return;
36025
+ }
36026
+ const existing = peers.get(frame.mcpId);
36027
+ if (existing && existing.ws !== ws) {
36028
+ const existingEdPub = existing.helloFrame.identityEd25519Pub;
36029
+ if (existingEdPub !== frame.identityEd25519Pub) {
36030
+ console.warn("[fetchproxy] peer mcpId already mapped to a different identity \u2014 refusing (mcpId squatting)");
36031
+ ws.close(1008, "mcpId already registered to another identity");
36032
+ return;
36033
+ }
36034
+ }
35946
36035
  identified = "peer";
35947
36036
  peerMcpId = frame.mcpId;
35948
36037
  peers.set(frame.mcpId, { ws, helloFrame: frame });
@@ -36035,8 +36124,10 @@ async function startHost(opts) {
36035
36124
  resetSessionPromise();
36036
36125
  disconnectListeners.forEach((cb) => cb());
36037
36126
  }
36038
- if (identified === "peer" && peerMcpId)
36039
- peers.delete(peerMcpId);
36127
+ if (identified === "peer" && peerMcpId) {
36128
+ if (peers.get(peerMcpId)?.ws === ws)
36129
+ peers.delete(peerMcpId);
36130
+ }
36040
36131
  });
36041
36132
  });
36042
36133
  return {
@@ -36052,7 +36143,10 @@ async function startHost(opts) {
36052
36143
  });
36053
36144
  }),
36054
36145
  sendOwnInner: async (inner) => {
36055
- const session = await ownSessionReady;
36146
+ const session = await awaitSessionReady(ownSessionReady, {
36147
+ mcpId: opts.ownMcpId,
36148
+ pendingPairCode: () => ownPendingPairCode
36149
+ });
36056
36150
  if (!extensionWs)
36057
36151
  throw new Error("host: no extension connected");
36058
36152
  const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
@@ -36157,7 +36251,10 @@ async function startPeer(opts) {
36157
36251
  ws,
36158
36252
  session: sessionPromise,
36159
36253
  sendInner: async (inner) => {
36160
- await sessionPromise;
36254
+ await awaitSessionReady(sessionPromise, {
36255
+ mcpId: opts.mcpId,
36256
+ pendingPairCode: () => pendingPairCode
36257
+ });
36161
36258
  const s = session;
36162
36259
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
36163
36260
  ws.send(JSON.stringify(sealed));
@@ -36823,6 +36920,35 @@ var FetchproxyServer = class {
36823
36920
  this.keepAliveTimer = null;
36824
36921
  }
36825
36922
  }
36923
+ /**
36924
+ * Send an inner request frame via whichever bridge handle is active. If the
36925
+ * send throws (e.g. `FetchproxySessionNotReadyError` — the session never
36926
+ * confirmed), the frame never reached the bridge, so no reply will arrive:
36927
+ * drop the just-registered pending resolver for this id (it lives in exactly
36928
+ * one of the op maps — request ids are unique) so it doesn't leak until the
36929
+ * server closes, then rethrow.
36930
+ */
36931
+ async sendInnerFrame(inner) {
36932
+ try {
36933
+ if (this.hostHandle) {
36934
+ await this.hostHandle.sendOwnInner(inner);
36935
+ } else if (this.peerHandle) {
36936
+ await this.peerHandle.sendInner(inner);
36937
+ }
36938
+ } catch (err) {
36939
+ if ("id" in inner && typeof inner.id === "number") {
36940
+ const { id } = inner;
36941
+ this.pending.delete(id);
36942
+ this.pendingReadCookies.delete(id);
36943
+ this.pendingStorage.delete(id);
36944
+ this.pendingCapture.delete(id);
36945
+ this.pendingRedirect.delete(id);
36946
+ this.pendingDownload.delete(id);
36947
+ this.pendingIdb.delete(id);
36948
+ }
36949
+ throw err;
36950
+ }
36951
+ }
36826
36952
  /**
36827
36953
  * Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
36828
36954
  * On timeout returns the `{ok:false, kind:'timeout'}` envelope —
@@ -36834,11 +36960,7 @@ var FetchproxyServer = class {
36834
36960
  const pending = new Promise((resolve) => {
36835
36961
  this.pending.set(id, resolve);
36836
36962
  });
36837
- if (this.hostHandle) {
36838
- await this.hostHandle.sendOwnInner(inner);
36839
- } else if (this.peerHandle) {
36840
- await this.peerHandle.sendInner(inner);
36841
- }
36963
+ await this.sendInnerFrame(inner);
36842
36964
  const timeoutMs = this.opts.fetchTimeoutMs;
36843
36965
  if (timeoutMs === void 0 || timeoutMs <= 0)
36844
36966
  return pending;
@@ -36867,6 +36989,51 @@ var FetchproxyServer = class {
36867
36989
  clearTimeout(timer);
36868
36990
  }
36869
36991
  }
36992
+ /**
36993
+ * FP-B2: bound a non-`fetch` verb's reply wait by `fetchTimeoutMs`.
36994
+ *
36995
+ * Before this, only `fetch()` raced its `pending` reply against a timer
36996
+ * (`_fetchOnceWithTimeout`); `readCookies`, the storage reads,
36997
+ * `capture_request_header`, `capture_redirect`, `download`, and
36998
+ * `read_indexed_db` awaited their `pending` promise with no race, so a
36999
+ * wedged extension hung the tool call indefinitely.
37000
+ *
37001
+ * `pending` is the already-registered reply promise. `pendingMap`/`id`
37002
+ * point at the op-specific map entry so we can drop it on expiry exactly
37003
+ * as `_fetchOnceWithTimeout` does — otherwise a late bridge reply would
37004
+ * resolve into a stale resolver / leak. On timeout we reject with a
37005
+ * `FetchproxyTimeoutError` (the same throwable the convenience methods
37006
+ * already surface for fetch timeouts). `0`/unset opts out (unbounded),
37007
+ * matching the fetch path.
37008
+ */
37009
+ async _withVerbTimeout(pending, pendingMap, id, url2) {
37010
+ const timeoutMs = this.opts.fetchTimeoutMs;
37011
+ if (timeoutMs === void 0 || timeoutMs <= 0)
37012
+ return pending;
37013
+ let timer;
37014
+ const start = Date.now();
37015
+ try {
37016
+ return await Promise.race([
37017
+ pending,
37018
+ new Promise((_resolve, reject) => {
37019
+ timer = setTimeout(() => {
37020
+ pendingMap.delete(id);
37021
+ reject(new FetchproxyTimeoutError({
37022
+ url: url2,
37023
+ timeoutMs,
37024
+ role: this.role,
37025
+ port: this.opts.port,
37026
+ elapsedMs: Date.now() - start,
37027
+ retryAttempted: false
37028
+ }));
37029
+ }, timeoutMs);
37030
+ })
37031
+ ]);
37032
+ } finally {
37033
+ if (timer)
37034
+ clearTimeout(timer);
37035
+ }
37036
+ }
36870
37037
  /**
36871
37038
  * Map an `ok:false` fetch result to its typed throwable. Centralizes
36872
37039
  * the kind-to-error-class switch so `request()` and (via the same
@@ -37157,12 +37324,8 @@ var FetchproxyServer = class {
37157
37324
  const pending = new Promise((resolve) => {
37158
37325
  this.pendingReadCookies.set(id, resolve);
37159
37326
  });
37160
- if (this.hostHandle) {
37161
- await this.hostHandle.sendOwnInner(inner);
37162
- } else if (this.peerHandle) {
37163
- await this.peerHandle.sendInner(inner);
37164
- }
37165
- const result = await pending;
37327
+ await this.sendInnerFrame(inner);
37328
+ const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
37166
37329
  if (!result.ok) {
37167
37330
  throw new FetchproxyProtocolError(result.error);
37168
37331
  }
@@ -37228,12 +37391,8 @@ var FetchproxyServer = class {
37228
37391
  const pending = new Promise((resolve, reject) => {
37229
37392
  this.pendingStorage.set(id, { resolve, reject });
37230
37393
  });
37231
- if (this.hostHandle) {
37232
- await this.hostHandle.sendOwnInner(inner);
37233
- } else if (this.peerHandle) {
37234
- await this.peerHandle.sendInner(inner);
37235
- }
37236
- return pending;
37394
+ await this.sendInnerFrame(inner);
37395
+ return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
37237
37396
  }
37238
37397
  /**
37239
37398
  * 0.3.0+: snapshot the next outgoing request's named header. Single-
@@ -37337,12 +37496,8 @@ var FetchproxyServer = class {
37337
37496
  const pending = new Promise((resolve, reject) => {
37338
37497
  this.pendingCapture.set(id, { resolve, reject });
37339
37498
  });
37340
- if (this.hostHandle) {
37341
- await this.hostHandle.sendOwnInner(inner);
37342
- } else if (this.peerHandle) {
37343
- await this.peerHandle.sendInner(inner);
37344
- }
37345
- return pending;
37499
+ await this.sendInnerFrame(inner);
37500
+ return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
37346
37501
  }
37347
37502
  /**
37348
37503
  * Snapshot the redirect target URL of the next request the browser
@@ -37426,12 +37581,8 @@ var FetchproxyServer = class {
37426
37581
  const pending = new Promise((resolve, reject) => {
37427
37582
  this.pendingRedirect.set(id, { resolve, reject });
37428
37583
  });
37429
- if (this.hostHandle) {
37430
- await this.hostHandle.sendOwnInner(inner);
37431
- } else if (this.peerHandle) {
37432
- await this.peerHandle.sendInner(inner);
37433
- }
37434
- return pending;
37584
+ await this.sendInnerFrame(inner);
37585
+ return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
37435
37586
  }
37436
37587
  /**
37437
37588
  * Download `url` through the BROWSER's own network stack via
@@ -37512,12 +37663,8 @@ var FetchproxyServer = class {
37512
37663
  const pending = new Promise((resolve, reject) => {
37513
37664
  this.pendingDownload.set(id, { resolve, reject });
37514
37665
  });
37515
- if (this.hostHandle) {
37516
- await this.hostHandle.sendOwnInner(inner);
37517
- } else if (this.peerHandle) {
37518
- await this.peerHandle.sendInner(inner);
37519
- }
37520
- return pending;
37666
+ await this.sendInnerFrame(inner);
37667
+ return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
37521
37668
  }
37522
37669
  /**
37523
37670
  * 0.4.0+: read declared IndexedDB keys from the user's signed-in
@@ -37564,12 +37711,8 @@ var FetchproxyServer = class {
37564
37711
  const pending = new Promise((resolve, reject) => {
37565
37712
  this.pendingIdb.set(id, { resolve, reject });
37566
37713
  });
37567
- if (this.hostHandle) {
37568
- await this.hostHandle.sendOwnInner(inner);
37569
- } else if (this.peerHandle) {
37570
- await this.peerHandle.sendInner(inner);
37571
- }
37572
- return pending;
37714
+ await this.sendInnerFrame(inner);
37715
+ return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
37573
37716
  }
37574
37717
  assertScopeSubset(requested, declared, label) {
37575
37718
  const undeclared = undeclaredKeys(requested, declared);
@@ -38049,7 +38192,7 @@ function loadAccount(env = process.env) {
38049
38192
  // package.json
38050
38193
  var package_default = {
38051
38194
  name: "canvas-parent-mcp",
38052
- version: "1.1.6",
38195
+ version: "1.1.7",
38053
38196
  mcpName: "io.github.chrischall/canvas-parent-mcp",
38054
38197
  description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
38055
38198
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38093,7 +38236,7 @@ var package_default = {
38093
38236
  "test:watch": "vitest"
38094
38237
  },
38095
38238
  dependencies: {
38096
- "@chrischall/mcp-utils": "^0.8.0",
38239
+ "@chrischall/mcp-utils": "^0.10.3",
38097
38240
  "@fetchproxy/bootstrap": "^1.0.0",
38098
38241
  "@fetchproxy/server": "^1.0.0",
38099
38242
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -38101,7 +38244,7 @@ var package_default = {
38101
38244
  zod: "^4.4.3"
38102
38245
  },
38103
38246
  devDependencies: {
38104
- "@types/node": "^25.9.1",
38247
+ "@types/node": "^26.0.0",
38105
38248
  "@vitest/coverage-v8": "^4.1.7",
38106
38249
  esbuild: "^0.28.0",
38107
38250
  typescript: "^6.0.3",
@@ -38200,7 +38343,7 @@ var CookieSessionManager = class {
38200
38343
  isPermanentErrorFn;
38201
38344
  constructor(opts) {
38202
38345
  this.loginFn = opts.login;
38203
- this.isExpiredFn = opts.isExpired;
38346
+ this.isExpiredFn = opts.isExpired ?? (() => false);
38204
38347
  this.isPermanentErrorFn = opts.isPermanentError ?? (() => false);
38205
38348
  }
38206
38349
  /** The current session, or `undefined` before the first successful login. */
@@ -38270,6 +38413,10 @@ var CookieSessionManager = class {
38270
38413
  * rather than looping. If the re-login itself fails, the original
38271
38414
  * (expired-looking) response is returned so the caller can surface a clean
38272
38415
  * sign-in error rather than the login failure.
38416
+ *
38417
+ * `call` resolves to `R` (the generic response type, default {@link Response});
38418
+ * the manager passes it untouched to {@link CookieSessionManagerOptions.isExpired}
38419
+ * and returns it untouched, so a custom transport type flows through cleanly.
38273
38420
  */
38274
38421
  async withSession(call) {
38275
38422
  const session = await this.ensure();
@@ -39005,7 +39152,7 @@ try {
39005
39152
  }
39006
39153
  await runMcp({
39007
39154
  name: "canvas",
39008
- version: "1.1.6",
39155
+ version: "1.1.7",
39009
39156
  // x-release-please-version
39010
39157
  tools: [
39011
39158
  (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.1.6', // x-release-please-version
49
+ version: '1.1.7', // 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.1.6",
3
+ "version": "1.1.7",
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>",
@@ -44,7 +44,7 @@
44
44
  "test:watch": "vitest"
45
45
  },
46
46
  "dependencies": {
47
- "@chrischall/mcp-utils": "^0.8.0",
47
+ "@chrischall/mcp-utils": "^0.10.3",
48
48
  "@fetchproxy/bootstrap": "^1.0.0",
49
49
  "@fetchproxy/server": "^1.0.0",
50
50
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -52,7 +52,7 @@
52
52
  "zod": "^4.4.3"
53
53
  },
54
54
  "devDependencies": {
55
- "@types/node": "^25.9.1",
55
+ "@types/node": "^26.0.0",
56
56
  "@vitest/coverage-v8": "^4.1.7",
57
57
  "esbuild": "^0.28.0",
58
58
  "typescript": "^6.0.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.1.6",
9
+ "version": "1.1.7",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "canvas-parent-mcp",
14
- "version": "1.1.6",
14
+ "version": "1.1.7",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },