canvas-parent-mcp 2.1.7 → 2.1.9

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
@@ -27002,6 +27002,17 @@ var storage = new AsyncLocalStorage();
27002
27002
  function withCallSignal(signal, fn, request) {
27003
27003
  return signal ? storage.run({ signal, ...request ? { request } : {} }, fn) : fn();
27004
27004
  }
27005
+ function currentCallSignal() {
27006
+ return storage.getStore()?.signal;
27007
+ }
27008
+ function withAmbientCancellation(own2) {
27009
+ const ambient = currentCallSignal();
27010
+ if (!ambient)
27011
+ return own2;
27012
+ if (!own2)
27013
+ return ambient;
27014
+ return AbortSignal.any([own2, ambient]);
27015
+ }
27005
27016
 
27006
27017
  // node_modules/@chrischall/mcp-utils/dist/caller/index.js
27007
27018
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
@@ -41783,17 +41794,62 @@ var DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024 * 1024;
41783
41794
 
41784
41795
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
41785
41796
  var DEFAULT_ERROR_MESSAGE_MAX = 500;
41797
+ var MCP_TOOL_ERROR_KINDS = /* @__PURE__ */ new Set([
41798
+ "no_credential",
41799
+ "credential_rejected",
41800
+ "edge_blocked",
41801
+ "session_expired",
41802
+ "verification_pending",
41803
+ "timeout",
41804
+ "http",
41805
+ "transport",
41806
+ "too_large",
41807
+ "unknown"
41808
+ ]);
41786
41809
  var McpToolError = class extends Error {
41787
41810
  /** Actionable remediation text, when one applies. */
41788
41811
  hint;
41812
+ /** The upstream HTTP status this failure came from, when known. */
41813
+ status;
41814
+ /** What kind of failure this is, when the thrower knows. */
41815
+ kind;
41789
41816
  constructor(message, opts) {
41790
41817
  super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
41791
41818
  this.name = "McpToolError";
41792
41819
  if (opts?.hint !== void 0)
41793
41820
  this.hint = opts.hint;
41821
+ if (opts?.status !== void 0)
41822
+ this.status = opts.status;
41823
+ if (opts?.kind !== void 0)
41824
+ this.kind = opts.kind;
41794
41825
  Object.setPrototypeOf(this, new.target.prototype);
41795
41826
  }
41796
41827
  };
41828
+ var DECLARATION_CAUSE_DEPTH = 4;
41829
+ function firstDeclared(err, read) {
41830
+ let cur = err;
41831
+ for (let depth = 0; depth < DECLARATION_CAUSE_DEPTH && cur !== null && typeof cur === "object"; depth++) {
41832
+ const found = read(cur);
41833
+ if (found !== void 0)
41834
+ return found;
41835
+ cur = cur.cause;
41836
+ }
41837
+ return void 0;
41838
+ }
41839
+ function errorStatusOf(err) {
41840
+ return firstDeclared(err, (link2) => {
41841
+ const e = link2;
41842
+ if (typeof e.status === "number")
41843
+ return e.status;
41844
+ return typeof e.statusCode === "number" ? e.statusCode : void 0;
41845
+ });
41846
+ }
41847
+ function errorKindOf(err) {
41848
+ return firstDeclared(err, (link2) => {
41849
+ const k = link2.kind;
41850
+ return typeof k === "string" && MCP_TOOL_ERROR_KINDS.has(k) ? k : void 0;
41851
+ });
41852
+ }
41797
41853
  function createHelpfulError(message, opts) {
41798
41854
  return new McpToolError(message, opts);
41799
41855
  }
@@ -41818,7 +41874,7 @@ var API_KEY_RE = new RegExp([
41818
41874
  // webhook signing secret (Stripe-style)
41819
41875
  ].map((p) => `\\b${p}`).join("|"), "g");
41820
41876
  var GOOGLE_OAUTH_TOKEN_RE = /(?<![A-Za-z0-9+/])(?:ya29\.|1\/\/)[A-Za-z0-9._-]{8,}/g;
41821
- var QUERY_SECRET_RE = new RegExp(`([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
41877
+ var QUERY_SECRET_RE = new RegExp(`([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig|(?:code[_-]?)?verifier|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
41822
41878
  var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
41823
41879
  var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
41824
41880
  var HEADER_SECRET_RE = new RegExp(`(\\b(?:x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN}|api[-_]?key)\\s*:\\s*)[^\\s,;"'<>\`]+`, "gi");
@@ -41829,6 +41885,8 @@ var JSON_SECRET_KEYS = [
41829
41885
  "api[_-]?secret",
41830
41886
  "(?:x[_-])?api[_-]?key",
41831
41887
  "private[_-]?key",
41888
+ "code[_-]?verifier",
41889
+ // PKCE; a bare `verifier` key is too generic for JSON
41832
41890
  `x[_-]${HEADER_NAME_RUN}?(?:api[_-]?key|token|secret|auth)${HEADER_NAME_RUN}`,
41833
41891
  // bounded like HEADER_SECRET_RE
41834
41892
  "(?:proxy-)?authorization",
@@ -61660,7 +61718,7 @@ function readEnvVar(key, opts = {}) {
61660
61718
  if (typeof raw === "string") {
61661
61719
  const trimmed = raw.trim();
61662
61720
  if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
61663
- return trimmed;
61721
+ return opts.trim === false ? raw : trimmed;
61664
61722
  }
61665
61723
  }
61666
61724
  return opts.default;
@@ -61714,12 +61772,14 @@ var SERVER_PROTOCOL_VERSIONS = Object.freeze([
61714
61772
  ...SUPPORTED_PROTOCOL_VERSIONS
61715
61773
  ]);
61716
61774
  function hintResultOrRethrow(err) {
61775
+ if (err instanceof ProtocolError && err.code === ProtocolErrorCode.UrlElicitationRequired)
61776
+ throw err;
61717
61777
  if (err instanceof McpToolError && err.hint) {
61718
61778
  return errorResult(`${err.message}
61719
61779
 
61720
61780
  Hint: ${err.hint}`);
61721
61781
  }
61722
- throw err;
61782
+ return errorResult(messageOf(err));
61723
61783
  }
61724
61784
  function mcpRequestFrom(args) {
61725
61785
  const ctx = args.at(-1);
@@ -61949,6 +62009,18 @@ function isCloudflareChallenge(html) {
61949
62009
  }
61950
62010
 
61951
62011
  // node_modules/@chrischall/mcp-utils/dist/http/index.js
62012
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
62013
+ var RequestTimeoutError = class extends Error {
62014
+ timeoutMs;
62015
+ /** Always `'timeout'` unless a subclass says otherwise. */
62016
+ kind = "timeout";
62017
+ constructor(service, timeoutMs) {
62018
+ super(`Request to ${service} timed out after ${timeoutMs}ms.`);
62019
+ this.name = "RequestTimeoutError";
62020
+ this.timeoutMs = timeoutMs;
62021
+ Object.setPrototypeOf(this, new.target.prototype);
62022
+ }
62023
+ };
61952
62024
  var ApiError = class extends Error {
61953
62025
  status;
61954
62026
  constructor(status, message) {
@@ -61961,6 +62033,8 @@ var ApiError = class extends Error {
61961
62033
  var EdgeBlockedError = class extends ApiError {
61962
62034
  /** Which edge refused the request, e.g. `'CloudFront'` or `'Cloudflare'`. */
61963
62035
  vendor;
62036
+ /** Always `'edge_blocked'`: the credential was never judged. */
62037
+ kind = "edge_blocked";
61964
62038
  /**
61965
62039
  * @param where The service the request was for, and — when the thrower knows
61966
62040
  * it — the method and path, which the message names.
@@ -62140,7 +62214,7 @@ var PositiveInt = external_exports.number().int().positive();
62140
62214
  var NonNegInt = external_exports.number().int().nonnegative();
62141
62215
  var NonEmptyString = external_exports.string().min(1);
62142
62216
  var IsoDate = external_exports.iso.date();
62143
- var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
62217
+ var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30").overwrite((v) => v.padStart(5, "0"));
62144
62218
  var NumericIdString = external_exports.string().regex(/^\d+$/, "must be a numeric id (digits only)").describe("A numeric id string (digits only), safe to interpolate into a URL path.");
62145
62219
  var SafePathSegment = external_exports.string().min(1).regex(/^[^/?#\s]+$/, 'must not contain "/", "?", "#", or whitespace').refine((v) => !v.includes(".."), 'must not contain ".."').describe('A single URL path segment, safe to interpolate: no "/", "..", "?", "#", or whitespace.');
62146
62220
  var schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
@@ -62158,7 +62232,11 @@ var pageSchema = {
62158
62232
  var OAuth2RefreshError = class extends McpToolError {
62159
62233
  status;
62160
62234
  constructor(status, message, opts) {
62161
- super(message, opts);
62235
+ super(message, {
62236
+ ...opts,
62237
+ status,
62238
+ kind: status >= 400 && status < 500 && status !== 408 && status !== 429 ? "credential_rejected" : "http"
62239
+ });
62162
62240
  this.name = "OAuth2RefreshError";
62163
62241
  this.status = status;
62164
62242
  Object.setPrototypeOf(this, new.target.prototype);
@@ -62190,6 +62268,7 @@ function createOAuth2Refresher(opts) {
62190
62268
  const grantType = opts.grantType ?? "refresh_token";
62191
62269
  const maxRetries = opts.retry?.count ?? 0;
62192
62270
  const retryDelayMs = opts.retry?.delayMs ?? 0;
62271
+ const timeoutMs = opts.timeout === false ? 0 : opts.timeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
62193
62272
  let inFlight = null;
62194
62273
  let currentRefreshToken = opts.refreshToken;
62195
62274
  const spent = /* @__PURE__ */ new Set();
@@ -62200,22 +62279,57 @@ function createOAuth2Refresher(opts) {
62200
62279
  refresh_token: currentRefreshToken,
62201
62280
  ...opts.params
62202
62281
  }).toString();
62203
- const res = await doFetch(opts.endpoint, {
62204
- method: "POST",
62205
- headers: {
62206
- "Content-Type": "application/x-www-form-urlencoded",
62207
- Accept: "application/json"
62208
- },
62209
- body
62282
+ const controller = timeoutMs > 0 ? new AbortController() : void 0;
62283
+ const timedOut = () => new RequestTimeoutError(endpointWhere(opts.endpoint).service, timeoutMs);
62284
+ let timer;
62285
+ const expired = controller ? new Promise((_resolve, reject) => {
62286
+ timer = setTimeout(() => {
62287
+ controller.abort();
62288
+ reject(timedOut());
62289
+ }, timeoutMs);
62290
+ }) : void 0;
62291
+ expired?.catch(() => {
62210
62292
  });
62293
+ const bounded = (p) => expired ? Promise.race([p, expired]) : p;
62294
+ try {
62295
+ return await exchangeBounded(body, controller?.signal, bounded, timedOut);
62296
+ } finally {
62297
+ clearTimeout(timer);
62298
+ }
62299
+ }
62300
+ async function exchangeBounded(body, signal, bounded, timedOut) {
62301
+ let res;
62302
+ try {
62303
+ res = await bounded(doFetch(opts.endpoint, {
62304
+ method: "POST",
62305
+ headers: {
62306
+ "Content-Type": "application/x-www-form-urlencoded",
62307
+ Accept: "application/json"
62308
+ },
62309
+ body,
62310
+ ...signal ? { signal } : {}
62311
+ }));
62312
+ } catch (err) {
62313
+ if (signal?.aborted && !(err instanceof RequestTimeoutError))
62314
+ throw timedOut();
62315
+ throw err;
62316
+ }
62317
+ const keepTimeout = (fallback) => (err) => {
62318
+ if (err instanceof RequestTimeoutError) {
62319
+ res.body?.cancel().catch(() => {
62320
+ });
62321
+ throw err;
62322
+ }
62323
+ return fallback;
62324
+ };
62211
62325
  if (!res.ok) {
62212
- const errText = await res.text().catch(() => "");
62326
+ const errText = await bounded(res.text()).catch(keepTimeout(""));
62213
62327
  const edge = detectEdgeBlock({ body: errText, headers: res.headers, status: res.status });
62214
62328
  if (edge)
62215
62329
  throw new EdgeBlockedError(res.status, edge.vendor, endpointWhere(opts.endpoint));
62216
62330
  throw new OAuth2RefreshError(res.status, `OAuth2 token refresh failed: ${res.status} ${res.statusText}: ${truncateErrorMessage(errText, 200)}`, { hint: "The refresh token may be expired or revoked \u2014 re-authenticate." });
62217
62331
  }
62218
- const data = await res.json().catch(() => null);
62332
+ const data = await bounded(res.json()).catch(keepTimeout(null));
62219
62333
  const accessToken = data?.access_token;
62220
62334
  if (typeof accessToken !== "string" || accessToken.length === 0) {
62221
62335
  throw createHelpfulError("OAuth2 token refresh returned no access_token.", {
@@ -62253,6 +62367,14 @@ function createOAuth2Refresher(opts) {
62253
62367
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
62254
62368
  }
62255
62369
  return function refresh(refreshToken) {
62370
+ const signal = currentCallSignal();
62371
+ if (!signal)
62372
+ return shared(refreshToken);
62373
+ if (signal.aborted)
62374
+ return Promise.reject(abortReasonOf(signal));
62375
+ return releaseOnAbort(shared(refreshToken), signal);
62376
+ };
62377
+ function shared(refreshToken) {
62256
62378
  if (inFlight)
62257
62379
  return inFlight;
62258
62380
  if (typeof refreshToken === "string" && refreshToken.length > 0 && !spent.has(refreshToken)) {
@@ -62275,7 +62397,18 @@ function createOAuth2Refresher(opts) {
62275
62397
  });
62276
62398
  inFlight = p;
62277
62399
  return p;
62278
- };
62400
+ }
62401
+ }
62402
+ function releaseOnAbort(p, signal) {
62403
+ return new Promise((resolve5, reject) => {
62404
+ const onAbort = () => reject(abortReasonOf(signal));
62405
+ signal.addEventListener("abort", onAbort, { once: true });
62406
+ p.then(resolve5, reject).finally(() => signal.removeEventListener("abort", onAbort));
62407
+ });
62408
+ }
62409
+ function abortReasonOf(signal) {
62410
+ const reason = signal.reason;
62411
+ return reason instanceof Error ? reason : new Error(String(reason));
62279
62412
  }
62280
62413
  function endpointWhere(endpoint) {
62281
62414
  try {
@@ -62493,6 +62626,17 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
62493
62626
  ]);
62494
62627
  var ACCOUNT_KEY_FRAME = "account-key";
62495
62628
  var ACCOUNT_ATTEST_FRAME = "account-attest";
62629
+ var BRIDGE_ROLE_FRAME = "bridge-role";
62630
+ var BRIDGE_SERVE_FRAME = "bridge-serve";
62631
+ var ROOM_PING_FRAME = "room-ping";
62632
+ var ROOM_PONG_FRAME = "room-pong";
62633
+ var BRIDGE_ROLE_LABEL_MAX = 64;
62634
+ var ROOM_FRAME_ACCEPTS = Object.freeze({
62635
+ [BRIDGE_ROLE_FRAME]: BRIDGE_ROLE_FRAME,
62636
+ [BRIDGE_SERVE_FRAME]: BRIDGE_SERVE_FRAME,
62637
+ [ROOM_PING_FRAME]: ROOM_PING_FRAME,
62638
+ [ROOM_PONG_FRAME]: ROOM_PING_FRAME
62639
+ });
62496
62640
 
62497
62641
  // node_modules/@fetchproxy/protocol/dist/mcp-id.js
62498
62642
  var ID_RE = /^([^:]+):([^:]+):([0-9a-f]{16})$/;
@@ -63158,6 +63302,12 @@ function validateFrame(raw) {
63158
63302
  return validateAccountKey(raw);
63159
63303
  if (t === ACCOUNT_ATTEST_FRAME)
63160
63304
  return validateAccountAttest(raw);
63305
+ if (t === BRIDGE_ROLE_FRAME)
63306
+ return validateBridgeRole(raw);
63307
+ if (t === BRIDGE_SERVE_FRAME || t === ROOM_PING_FRAME || t === ROOM_PONG_FRAME) {
63308
+ assertExactFields(raw, TYPE_ONLY_FIELDS, t);
63309
+ return { type: t };
63310
+ }
63161
63311
  throw new ProtocolError3(`unknown frame type: ${String(t)}`);
63162
63312
  }
63163
63313
  function validateHello(raw) {
@@ -63483,6 +63633,43 @@ function validateAccountAttest(raw) {
63483
63633
  sig: raw.sig
63484
63634
  };
63485
63635
  }
63636
+ var TYPE_ONLY_FIELDS = ["type"];
63637
+ var BRIDGE_ROLE_FIELDS = ["type", "role", "canServe", "serving"];
63638
+ var BRIDGE_ROLE_SERVING_FIELDS = ["label", "since"];
63639
+ function validateBridgeRole(raw) {
63640
+ const L = BRIDGE_ROLE_FRAME;
63641
+ const role = raw.role;
63642
+ if (role !== "serving" && role !== "standby") {
63643
+ throw new ProtocolError3(`${L}.role: must be one of serving, standby`);
63644
+ }
63645
+ const canServe = raw.canServe;
63646
+ assertBoolean(canServe, `${L}.canServe`);
63647
+ assertExactFields(raw, BRIDGE_ROLE_FIELDS, L);
63648
+ const serving = raw.serving;
63649
+ if (role === "serving") {
63650
+ if (serving !== void 0) {
63651
+ throw new ProtocolError3(`${L}.serving: must be absent when role is serving`);
63652
+ }
63653
+ return { type: BRIDGE_ROLE_FRAME, role, canServe };
63654
+ }
63655
+ if (serving === void 0) {
63656
+ throw new ProtocolError3(`${L}.serving: required when role is standby`);
63657
+ }
63658
+ assertObject(serving, `${L}.serving`);
63659
+ const label = serving.label;
63660
+ assertDisplayString(label, `${L}.serving.label`, 1, BRIDGE_ROLE_LABEL_MAX);
63661
+ const since = serving.since;
63662
+ if (typeof since !== "number" || !Number.isSafeInteger(since) || since < 0) {
63663
+ throw new ProtocolError3(`${L}.serving.since: expected non-negative safe integer (Unix ms)`);
63664
+ }
63665
+ assertExactFields(serving, BRIDGE_ROLE_SERVING_FIELDS, `${L}.serving`);
63666
+ return {
63667
+ type: BRIDGE_ROLE_FRAME,
63668
+ role,
63669
+ canServe,
63670
+ serving: { label, since }
63671
+ };
63672
+ }
63486
63673
  function validateInnerFrame(raw) {
63487
63674
  assertObject(raw, "inner");
63488
63675
  const t = raw.type;
@@ -65425,9 +65612,9 @@ async function startHost(opts) {
65425
65612
  });
65426
65613
  return {
65427
65614
  close: () => new Promise((resolve5) => {
65428
- for (const client of wss.clients) {
65615
+ for (const client2 of wss.clients) {
65429
65616
  try {
65430
- client.terminate();
65617
+ client2.terminate();
65431
65618
  } catch {
65432
65619
  }
65433
65620
  }
@@ -68517,7 +68704,7 @@ function loadAccount(env = process.env) {
68517
68704
  // package.json
68518
68705
  var package_default = {
68519
68706
  name: "canvas-parent-mcp",
68520
- version: "2.1.7",
68707
+ version: "2.1.9",
68521
68708
  mcpName: "io.github.chrischall/canvas-parent-mcp",
68522
68709
  description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
68523
68710
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -68564,9 +68751,9 @@ var package_default = {
68564
68751
  typecheck: "tsc -p tsconfig.json --noEmit"
68565
68752
  },
68566
68753
  dependencies: {
68567
- "@chrischall/mcp-utils": "^2.14.0",
68568
- "@fetchproxy/bootstrap": "^3.4.1",
68569
- "@fetchproxy/server": "^3.4.1",
68754
+ "@chrischall/mcp-utils": "^3.0.0",
68755
+ "@fetchproxy/bootstrap": "^3.6.0",
68756
+ "@fetchproxy/server": "^3.6.0",
68570
68757
  "@modelcontextprotocol/server": "^2.3.0",
68571
68758
  dotenv: "^18.0.0",
68572
68759
  zod: "^4.6.5"
@@ -68724,12 +68911,6 @@ async function responseEdgeBlock(res) {
68724
68911
  }
68725
68912
 
68726
68913
  // node_modules/@chrischall/mcp-utils/dist/healthcheck/index.js
68727
- function statusOf2(err) {
68728
- if (typeof err !== "object" || err === null)
68729
- return void 0;
68730
- const s = err.status ?? err.statusCode;
68731
- return typeof s === "number" ? s : void 0;
68732
- }
68733
68914
  var CREDENTIAL_ARMS = /* @__PURE__ */ new Set([
68734
68915
  "ok",
68735
68916
  "no_credential",
@@ -68745,6 +68926,9 @@ var CREDENTIAL_ARMS = /* @__PURE__ */ new Set([
68745
68926
  function isArm(kind) {
68746
68927
  return kind !== void 0 && CREDENTIAL_ARMS.has(kind);
68747
68928
  }
68929
+ function fromSource(source) {
68930
+ return source !== null ? ` from '${source}'` : "";
68931
+ }
68748
68932
  function credentialHint(arm, prefix, hostLabel, source) {
68749
68933
  switch (arm) {
68750
68934
  case "ok":
@@ -68752,15 +68936,15 @@ function credentialHint(arm, prefix, hostLabel, source) {
68752
68936
  case "no_credential":
68753
68937
  return `No credential resolved. Nothing was available to authenticate with \u2014 sign in and reconnect the connector so ${prefix} receives a token, or set the documented environment variable.`;
68754
68938
  case "credential_rejected":
68755
- return `${hostLabel} rejected the credential from '${source}'. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
68939
+ return `${hostLabel} rejected the credential${fromSource(source)}. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
68756
68940
  case "edge_blocked":
68757
68941
  return `${hostLabel} refused the request at its CDN/WAF before it reached the API, so the credential${source !== null ? ` from '${source}'` : ""} was never judged \u2014 re-signing in or rotating it will not help. This is usually a block on this host's IP address or request fingerprint: route through the ContextMint Bridge if this connector supports it, or retry later from a different network.`;
68758
68942
  case "session_expired":
68759
- return `The credential from '${source}' is configured, but no session is live \u2014 ${hostLabel} served a sign-in page rather than the data. Sign in again; a cookie-session portal expires these on its own, so this recurs between uses.`;
68943
+ return `The credential${fromSource(source)} is configured, but no session is live \u2014 ${hostLabel} served a sign-in page rather than the data. Sign in again; a cookie-session portal expires these on its own, so this recurs between uses.`;
68760
68944
  case "verification_pending":
68761
68945
  return `${hostLabel} is holding the sign-in on a second factor rather than refusing it. Supply the verification code the ACCOUNT HOLDER received \u2014 the credential itself is not the problem, so changing it will not help.`;
68762
68946
  case "timeout":
68763
- return `The credential from '${source}' resolved, but ${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
68947
+ return `${source !== null ? `The credential from '${source}' resolved, but ` : ""}${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
68764
68948
  case "http":
68765
68949
  return `${hostLabel} answered with an error status that is not an auth rejection. That is USUALLY a ${hostLabel}-side problem rather than an auth one \u2014 but a 404 here more often means the probe path is wrong than that ${hostLabel} is broken, so check error.message and probe.url before concluding anything about the credential.`;
68766
68950
  case "transport":
@@ -68781,7 +68965,8 @@ async function runCredentialHealthcheck(args) {
68781
68965
  state = await resolveCredential();
68782
68966
  } catch (e) {
68783
68967
  const edge = edgeBlockOf(e);
68784
- const classified = classifyThrown?.(e) ?? (edge !== null ? { kind: "edge_blocked", detail: { vendor: edge.vendor } } : void 0);
68968
+ const declared = errorKindOf(e);
68969
+ const classified = classifyThrown?.(e) ?? (edge !== null ? { kind: "edge_blocked", detail: { vendor: edge.vendor } } : declared !== void 0 ? { kind: declared } : void 0);
68785
68970
  const result2 = {
68786
68971
  ok: false,
68787
68972
  // Still false, and still no source: a classification explains WHY
@@ -68828,10 +69013,11 @@ async function runCredentialHealthcheck(args) {
68828
69013
  try {
68829
69014
  await probeFn();
68830
69015
  } catch (e) {
68831
- status = statusOf2(e);
69016
+ status = errorStatusOf(e);
68832
69017
  const aborted2 = e instanceof Error && e.name === "AbortError";
68833
69018
  const edge = edgeBlockOf(e);
68834
- arm = edge !== null ? "edge_blocked" : status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
69019
+ const declared = errorKindOf(e);
69020
+ arm = edge !== null ? "edge_blocked" : declared !== void 0 ? declared : status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
68835
69021
  let kind = arm;
68836
69022
  let detail = edge !== null ? { vendor: edge.vendor } : void 0;
68837
69023
  const custom2 = classifyThrown?.(e);
@@ -68877,9 +69063,35 @@ function registerCredentialHealthcheckTool(args) {
68877
69063
  }, async () => runCredentialHealthcheck(args));
68878
69064
  }
68879
69065
 
69066
+ // src/tools/healthcheck.ts
69067
+ function registerHealthcheckTools(server, state) {
69068
+ registerCredentialHealthcheckTool({
69069
+ server,
69070
+ prefix: "canvas",
69071
+ hostLabel: "canvas",
69072
+ probePath: "/api/v1/users/self/profile",
69073
+ resolveCredential: async () => {
69074
+ if (!state.resolved) {
69075
+ throw state.configError ?? new Error("Canvas auth is not configured.");
69076
+ }
69077
+ return {
69078
+ source: state.resolved.source,
69079
+ detail: {
69080
+ base_url: state.resolved.account.baseUrl,
69081
+ mode: state.resolved.account.mode
69082
+ }
69083
+ };
69084
+ },
69085
+ probeFn: async () => {
69086
+ if (!state.client) throw new Error("Canvas client is not initialised.");
69087
+ return state.client.request("/api/v1/users/self/profile");
69088
+ }
69089
+ });
69090
+ }
69091
+
68880
69092
  // src/client.ts
68881
69093
  import { constants as fsConstants3 } from "fs";
68882
- import { lstat as lstat2, open as open2, stat } from "fs/promises";
69094
+ import { lstat as lstat2, open as open2, rm as rm3, stat } from "fs/promises";
68883
69095
 
68884
69096
  // node_modules/@chrischall/mcp-utils/dist/session/index.js
68885
69097
  import { existsSync as existsSync4, readFileSync as readFileSync2, mkdirSync as mkdirSync3, chmodSync as chmodSync2, renameSync as renameSync2, unlinkSync as unlinkSync3 } from "node:fs";
@@ -69434,6 +69646,12 @@ function reportCacheWriteFailure(err) {
69434
69646
  import { dirname as dirname4, isAbsolute as isAbsolute5, join as join6, resolve as resolve4 } from "path";
69435
69647
  import { homedir as homedir4 } from "os";
69436
69648
 
69649
+ // src/signal.ts
69650
+ var REQUEST_TIMEOUT_MS = 3e4;
69651
+ function requestSignal(timeoutMs = REQUEST_TIMEOUT_MS) {
69652
+ return withAmbientCancellation(AbortSignal.timeout(timeoutMs));
69653
+ }
69654
+
69437
69655
  // src/session-login.ts
69438
69656
  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";
69439
69657
  var SessionLoginError = class extends Error {
@@ -69465,6 +69683,7 @@ async function sessionLogin(opts) {
69465
69683
  const baseUrl = opts.baseUrl.replace(/\/$/, "");
69466
69684
  const loginUrl = `${baseUrl}/login/canvas`;
69467
69685
  const getRes = await fetch(loginUrl, {
69686
+ signal: requestSignal(),
69468
69687
  headers: {
69469
69688
  "User-Agent": USER_AGENT,
69470
69689
  Accept: "text/html,application/xhtml+xml"
@@ -69495,6 +69714,7 @@ async function sessionLogin(opts) {
69495
69714
  body.append("pseudonym_session[remember_me]", "1");
69496
69715
  const postRes = await fetch(loginUrl, {
69497
69716
  method: "POST",
69717
+ signal: requestSignal(),
69498
69718
  redirect: "manual",
69499
69719
  headers: {
69500
69720
  "Content-Type": "application/x-www-form-urlencoded",
@@ -69526,6 +69746,8 @@ async function sessionLogin(opts) {
69526
69746
  // src/client.ts
69527
69747
  var DEFAULT_PER_PAGE = 100;
69528
69748
  var DEFAULT_MAX_PAGES = 50;
69749
+ var DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
69750
+ var MAX_DOWNLOAD_BYTES = 1024 ** 3;
69529
69751
  var CanvasClient = class {
69530
69752
  account;
69531
69753
  sessionLoginFn;
@@ -69536,6 +69758,7 @@ var CanvasClient = class {
69536
69758
  refreshSession;
69537
69759
  /** Explicit download root; when unset, resolved per call (see {@link downloadRoot}). */
69538
69760
  outputDir;
69761
+ maxDownloadBytes;
69539
69762
  auth;
69540
69763
  /** Lazily-built shared refresh_token exchanger (oauth mode only). */
69541
69764
  oauthRefresh = null;
@@ -69552,6 +69775,7 @@ var CanvasClient = class {
69552
69775
  constructor(account, opts = {}) {
69553
69776
  this.account = account;
69554
69777
  this.outputDir = opts.outputDir;
69778
+ this.maxDownloadBytes = opts.maxDownloadBytes ?? MAX_DOWNLOAD_BYTES;
69555
69779
  this.sessionLoginFn = opts.sessionLogin ?? sessionLogin;
69556
69780
  this.refreshSession = opts.refreshSession ?? null;
69557
69781
  this.auth = new CookieSessionManager({
@@ -69607,6 +69831,10 @@ var CanvasClient = class {
69607
69831
  * - `destinationPath` must lie inside the download root (see
69608
69832
  * {@link downloadRoot}), checked through symlinks; a relative path is
69609
69833
  * resolved against that root.
69834
+ *
69835
+ * The body is streamed to disk (never buffered whole) and capped at
69836
+ * `maxDownloadBytes`; a transfer that fails or overruns part-way leaves no
69837
+ * partial file behind.
69610
69838
  */
69611
69839
  async download(path, destinationPath, opts = {}) {
69612
69840
  const url4 = this.pinDownloadUrl(path);
@@ -69625,16 +69853,25 @@ var CanvasClient = class {
69625
69853
  } catch {
69626
69854
  throw new ParentDirectoryMissingError(parent);
69627
69855
  }
69628
- const res = await this.authedFetch(url4, {});
69856
+ const res = await this.authedFetch(url4, {}, DOWNLOAD_TIMEOUT_MS);
69629
69857
  await throwIfEdgeBlocked(res, "GET", path);
69630
- if (res.status === 401) throw new TokenExpiredError(this.account.mode);
69631
- if (res.status === 404) throw new Error(`Canvas download 404 for ${path}`);
69632
- if (!res.ok) throw new Error(`Canvas download ${res.status} for ${path}`);
69633
- const buf = new Uint8Array(await res.arrayBuffer());
69634
- await writeConfined(dest, destinationPath, buf, opts.overwrite === true);
69858
+ if (res.status === 401) throw this.expiredError();
69859
+ if (res.status === 404) throw new Error(`Canvas download 404 for ${redactPath(path)}`);
69860
+ if (!res.ok) throw new Error(`Canvas download ${res.status} for ${redactPath(path)}`);
69861
+ if (Number(res.headers.get("content-length")) > this.maxDownloadBytes) {
69862
+ await res.body?.cancel();
69863
+ throw new DownloadTooLargeError(this.maxDownloadBytes);
69864
+ }
69865
+ const bytes = await writeConfined(
69866
+ dest,
69867
+ destinationPath,
69868
+ res.body,
69869
+ opts.overwrite === true,
69870
+ this.maxDownloadBytes
69871
+ );
69635
69872
  return {
69636
69873
  path: dest,
69637
- bytes: buf.byteLength,
69874
+ bytes,
69638
69875
  contentType: res.headers.get("content-type") ?? "application/octet-stream"
69639
69876
  };
69640
69877
  }
@@ -69682,10 +69919,10 @@ var CanvasClient = class {
69682
69919
  body: opts.body
69683
69920
  });
69684
69921
  await throwIfEdgeBlocked(res, opts.method ?? "GET", path);
69685
- if (res.status === 401) throw new TokenExpiredError(this.account.mode);
69686
- if (res.status === 404) throw new Error(`Canvas 404 ${path}`);
69922
+ if (res.status === 401) throw this.expiredError();
69923
+ if (res.status === 404) throw new Error(`Canvas 404 ${redactPath(path)}`);
69687
69924
  if (res.status >= 500) throw new CanvasUnreachableError(res.status);
69688
- if (!res.ok) throw withBodyPreview(new Error(`Canvas ${res.status} ${res.statusText} for ${path}`), await res.text());
69925
+ if (!res.ok) throw withBodyPreview(new Error(`Canvas ${res.status} ${res.statusText} for ${redactPath(path)}`), await res.text());
69689
69926
  return res;
69690
69927
  }
69691
69928
  /**
@@ -69697,12 +69934,14 @@ var CanvasClient = class {
69697
69934
  * download callers map that to a {@link TokenExpiredError}. Used by both API
69698
69935
  * requests and file downloads.
69699
69936
  */
69700
- async authedFetch(url4, init) {
69937
+ async authedFetch(url4, init, timeoutMs) {
69701
69938
  this.proactivelyExpire();
69702
69939
  return this.auth.withSession(
69703
69940
  async (state) => fetch(url4, {
69704
69941
  ...init,
69705
- headers: { ...state.headers, ...init.headers }
69942
+ headers: { ...state.headers, ...init.headers },
69943
+ // Fresh per attempt, so a 401 replay gets its own full timeout.
69944
+ signal: requestSignal(timeoutMs)
69706
69945
  })
69707
69946
  );
69708
69947
  }
@@ -69720,6 +69959,14 @@ var CanvasClient = class {
69720
69959
  this.auth.invalidate();
69721
69960
  }
69722
69961
  }
69962
+ /**
69963
+ * The error for a 401 that survived any re-auth. On the fetchproxy path the
69964
+ * session came from the browser and the user never set CANVAS_USERNAME /
69965
+ * CANVAS_PASSWORD, so the message must point at the browser instead.
69966
+ */
69967
+ expiredError() {
69968
+ return new TokenExpiredError(this.account.mode, void 0, { browser: this.refreshSession !== null });
69969
+ }
69723
69970
  /** Whether a 401 in the current mode can be recovered by re-running login(). */
69724
69971
  canReauth() {
69725
69972
  const acct = this.account;
@@ -69762,7 +70009,8 @@ var CanvasClient = class {
69762
70009
  this.oauthRefresh ??= createOAuth2Refresher({
69763
70010
  endpoint: `${acct.baseUrl}/login/oauth2/token`,
69764
70011
  refreshToken: acct.refreshToken,
69765
- params: { client_id: acct.clientId, client_secret: acct.clientSecret }
70012
+ params: { client_id: acct.clientId, client_secret: acct.clientSecret },
70013
+ fetchImpl: (input2, init) => fetch(input2, { ...init, signal: requestSignal() })
69766
70014
  });
69767
70015
  let accessToken;
69768
70016
  let expiresIn;
@@ -69806,8 +70054,12 @@ function injectPerPage(pathOrUrl, perPage) {
69806
70054
  return `${pathOrUrl}${sep2}per_page=${perPage}`;
69807
70055
  }
69808
70056
  var TokenExpiredError = class extends Error {
69809
- constructor(mode, detail) {
69810
- const base = mode === "token" ? "Canvas access token rejected (401). Check CANVAS_TOKEN \u2014 it may be expired or revoked." : mode === "session" ? "Canvas session login failed (401). Check CANVAS_USERNAME / CANVAS_PASSWORD \u2014 they may have changed, or the account may be locked or behind SSO." : "Canvas OAuth refresh failed. Check CANVAS_CLIENT_ID, CANVAS_CLIENT_SECRET, CANVAS_REFRESH_TOKEN.";
70057
+ /**
70058
+ * `opts.browser`: the session was lifted from the user's signed-in browser
70059
+ * tab (the fetchproxy path), not minted from CANVAS_USERNAME/PASSWORD.
70060
+ */
70061
+ constructor(mode, detail, opts = {}) {
70062
+ const base = mode === "token" ? "Canvas access token rejected (401). Check CANVAS_TOKEN \u2014 it may be expired or revoked." : mode === "session" ? opts.browser ? "Canvas rejected the session read from your browser (401). Sign back into Canvas in your browser (the tab the ContextMint Bridge extension reads), then retry." : "Canvas session login failed (401). Check CANVAS_USERNAME / CANVAS_PASSWORD \u2014 they may have changed, or the account may be locked or behind SSO." : "Canvas OAuth refresh failed. Check CANVAS_CLIENT_ID, CANVAS_CLIENT_SECRET, CANVAS_REFRESH_TOKEN.";
69811
70063
  super(detail ? `${base} (${detail})` : base);
69812
70064
  this.mode = mode;
69813
70065
  this.detail = detail;
@@ -69832,6 +70084,14 @@ var DownloadUrlRejectedError = class extends Error {
69832
70084
  }
69833
70085
  url;
69834
70086
  };
70087
+ var DownloadTooLargeError = class extends Error {
70088
+ constructor(maxBytes) {
70089
+ super(`DownloadTooLarge: the file is larger than the ${maxBytes} bytes canvas_download_file accepts.`);
70090
+ this.maxBytes = maxBytes;
70091
+ this.name = "DownloadTooLargeError";
70092
+ }
70093
+ maxBytes;
70094
+ };
69835
70095
  var DownloadDestinationRejectedError = class extends Error {
69836
70096
  constructor(path, root) {
69837
70097
  super(
@@ -69844,7 +70104,7 @@ var DownloadDestinationRejectedError = class extends Error {
69844
70104
  path;
69845
70105
  root;
69846
70106
  };
69847
- async function writeConfined(dest, requested, buf, overwrite) {
70107
+ async function writeConfined(dest, requested, body, overwrite, maxBytes) {
69848
70108
  const { O_WRONLY: O_WRONLY2, O_CREAT: O_CREAT2, O_TRUNC: O_TRUNC2, O_EXCL: O_EXCL2, O_NOFOLLOW: O_NOFOLLOW3 } = fsConstants3;
69849
70109
  const flags = O_WRONLY2 | O_CREAT2 | O_NOFOLLOW3 | (overwrite ? O_TRUNC2 : O_EXCL2);
69850
70110
  let handle;
@@ -69857,11 +70117,22 @@ async function writeConfined(dest, requested, buf, overwrite) {
69857
70117
  if (code === "EISDIR") throw new InvalidPathError(requested);
69858
70118
  throw e;
69859
70119
  }
70120
+ let bytes = 0;
69860
70121
  try {
69861
- await handle.writeFile(buf);
69862
- } finally {
70122
+ if (body) {
70123
+ for await (const chunk2 of body) {
70124
+ bytes += chunk2.byteLength;
70125
+ if (bytes > maxBytes) throw new DownloadTooLargeError(maxBytes);
70126
+ await handle.write(chunk2);
70127
+ }
70128
+ }
70129
+ } catch (e) {
69863
70130
  await handle.close();
70131
+ await rm3(dest, { force: true });
70132
+ throw e;
69864
70133
  }
70134
+ await handle.close();
70135
+ return bytes;
69865
70136
  }
69866
70137
  var InvalidPathError = class extends Error {
69867
70138
  constructor(path, reason = "must be a filename, not a directory") {
@@ -69888,34 +70159,6 @@ var FileExistsError = class extends Error {
69888
70159
  path;
69889
70160
  };
69890
70161
 
69891
- // src/tools/healthcheck.ts
69892
- function registerHealthcheckTools(server, state) {
69893
- registerCredentialHealthcheckTool({
69894
- server,
69895
- prefix: "canvas",
69896
- hostLabel: "canvas",
69897
- probePath: "/api/v1/users/self/profile",
69898
- resolveCredential: async () => {
69899
- if (!state.resolved) {
69900
- throw state.configError ?? new Error("Canvas auth is not configured.");
69901
- }
69902
- return {
69903
- source: state.resolved.source,
69904
- detail: {
69905
- base_url: state.resolved.account.baseUrl,
69906
- mode: state.resolved.account.mode
69907
- }
69908
- };
69909
- },
69910
- probeFn: async () => {
69911
- const client = new CanvasClient(state.resolved.account, {
69912
- refreshSession: state.resolved.refresh
69913
- });
69914
- return client.request("/api/v1/users/self/profile");
69915
- }
69916
- });
69917
- }
69918
-
69919
70162
  // src/view.ts
69920
70163
  var CV_VIEWS = ["compact", "full"];
69921
70164
  var NOTE = `compact removes the avatar URLs Canvas attaches to user objects (avatar_url, avatar_image_url); "full" returns Canvas's payload untouched. Link fields (url, html_url, preview_url) survive both rungs. No field projection: this server passes Canvas's payload through verbatim and has no verified record of which of its fields a caller needs.`;
@@ -69931,14 +70174,14 @@ function viewResponse(view, data) {
69931
70174
  var argsSchema = external_exports.object({
69932
70175
  view: viewArg()
69933
70176
  });
69934
- function registerProfileTools(server, client) {
70177
+ function registerProfileTools(server, client2) {
69935
70178
  server.registerTool("canvas_get_profile", {
69936
70179
  description: "Get the logged-in user's Canvas profile (id, name, primary_email, login_id, locale, time_zone). Useful first call to confirm credentials.",
69937
- annotations: { readOnlyHint: true },
70180
+ annotations: { readOnlyHint: true, openWorldHint: true },
69938
70181
  inputSchema: argsSchema
69939
70182
  }, async (rawArgs) => {
69940
70183
  const args = argsSchema.parse(rawArgs);
69941
- const data = await client.request("/api/v1/users/self/profile");
70184
+ const data = await client2.request("/api/v1/users/self/profile");
69942
70185
  return viewResponse(args.view, data);
69943
70186
  });
69944
70187
  }
@@ -69947,14 +70190,14 @@ function registerProfileTools(server, client) {
69947
70190
  var argsSchema2 = external_exports.object({
69948
70191
  view: viewArg()
69949
70192
  });
69950
- function registerObserveeTools(server, client) {
70193
+ function registerObserveeTools(server, client2) {
69951
70194
  server.registerTool("canvas_list_observees", {
69952
70195
  description: "List students linked to your Canvas observer account. Returns an empty array for plain student tokens.",
69953
- annotations: { readOnlyHint: true },
70196
+ annotations: { readOnlyHint: true, openWorldHint: true },
69954
70197
  inputSchema: argsSchema2
69955
70198
  }, async (rawArgs) => {
69956
70199
  const args = argsSchema2.parse(rawArgs);
69957
- const data = await client.requestPaginated("/api/v1/users/self/observees?include[]=avatar_url");
70200
+ const data = await client2.requestPaginated("/api/v1/users/self/observees?include[]=avatar_url");
69958
70201
  return viewResponse(args.view, data);
69959
70202
  });
69960
70203
  }
@@ -69976,10 +70219,10 @@ var getArgs = external_exports.object({
69976
70219
  courseId: external_exports.string(),
69977
70220
  view: viewArg()
69978
70221
  });
69979
- function registerCourseTools(server, client) {
70222
+ function registerCourseTools(server, client2) {
69980
70223
  server.registerTool("canvas_list_courses", {
69981
70224
  description: "List active Canvas courses (with course-level grades, total scores, current grading period scores, and term).",
69982
- annotations: { readOnlyHint: true },
70225
+ annotations: { readOnlyHint: true, openWorldHint: true },
69983
70226
  inputSchema: listArgs
69984
70227
  }, async (rawArgs) => {
69985
70228
  const args = listArgs.parse(rawArgs);
@@ -69988,19 +70231,19 @@ function registerCourseTools(server, client) {
69988
70231
  "state[]": "available",
69989
70232
  "include[]": ["total_scores", "current_grading_period_scores", "term"]
69990
70233
  });
69991
- const data = await client.requestPaginated(path);
70234
+ const data = await client2.requestPaginated(path);
69992
70235
  return textContent(data);
69993
70236
  });
69994
70237
  server.registerTool("canvas_get_course", {
69995
70238
  description: "Get a single Canvas course with its syllabus, teachers, and term.",
69996
- annotations: { readOnlyHint: true },
70239
+ annotations: { readOnlyHint: true, openWorldHint: true },
69997
70240
  inputSchema: getArgs
69998
70241
  }, async (rawArgs) => {
69999
70242
  const args = getArgs.parse(rawArgs);
70000
70243
  const path = buildPath(`/api/v1/courses/${encodeURIComponent(args.courseId)}`, {
70001
70244
  "include[]": ["syllabus_body", "teachers", "term"]
70002
70245
  });
70003
- const data = await client.request(path);
70246
+ const data = await client2.request(path);
70004
70247
  return viewResponse(args.view, data);
70005
70248
  });
70006
70249
  }
@@ -70011,13 +70254,13 @@ var listArgs2 = external_exports.object({
70011
70254
  bucket: external_exports.enum(["past", "overdue", "undated", "ungraded", "unsubmitted", "upcoming", "future"]).optional().describe("Optional Canvas-side filter.")
70012
70255
  });
70013
70256
  var missingArgs = external_exports.object({
70014
- observeeId: external_exports.string().optional(),
70015
- courseIds: external_exports.array(external_exports.string()).optional().describe("Required when observeeId is set.")
70257
+ observeeId: external_exports.string().optional().describe("A linked observee's user id. When set, courseIds is required."),
70258
+ courseIds: external_exports.array(external_exports.string()).optional().describe("Required (non-empty) when observeeId is set; optional otherwise.")
70016
70259
  });
70017
- function registerAssignmentTools(server, client) {
70260
+ function registerAssignmentTools(server, client2) {
70018
70261
  server.registerTool("canvas_list_assignments", {
70019
70262
  description: "List a course's assignments (with the user's submission inline). Supports the standard Canvas `bucket` filter.",
70020
- annotations: { readOnlyHint: true },
70263
+ annotations: { readOnlyHint: true, openWorldHint: true },
70021
70264
  inputSchema: listArgs2
70022
70265
  }, async (rawArgs) => {
70023
70266
  const args = listArgs2.parse(rawArgs);
@@ -70026,21 +70269,24 @@ function registerAssignmentTools(server, client) {
70026
70269
  bucket: args.bucket,
70027
70270
  order_by: "due_at"
70028
70271
  });
70029
- const data = await client.requestPaginated(path);
70272
+ const data = await client2.requestPaginated(path);
70030
70273
  return textContent(data);
70031
70274
  });
70032
70275
  server.registerTool("canvas_list_missing_submissions", {
70033
70276
  description: "List past-due unsubmitted assignments for the user (or a linked observee). For an observee, courseIds is required.",
70034
- annotations: { readOnlyHint: true },
70277
+ annotations: { readOnlyHint: true, openWorldHint: true },
70035
70278
  inputSchema: missingArgs
70036
70279
  }, async (rawArgs) => {
70037
70280
  const args = missingArgs.parse(rawArgs);
70281
+ if (args.observeeId !== void 0 && !args.courseIds?.length) {
70282
+ throw new Error("courseIds is required when observeeId is set (list the observee's courses with canvas_list_courses).");
70283
+ }
70038
70284
  const path = buildPath(`/api/v1/${userSegment(args.observeeId)}/missing_submissions`, {
70039
70285
  "include[]": ["planner_overrides", "course"],
70040
70286
  "filter[]": "submittable",
70041
70287
  "course_ids[]": args.courseIds
70042
70288
  });
70043
- const data = await client.requestPaginated(path);
70289
+ const data = await client2.requestPaginated(path);
70044
70290
  return textContent(data);
70045
70291
  });
70046
70292
  }
@@ -70058,10 +70304,10 @@ var recentArgs = external_exports.object({
70058
70304
  since: external_exports.string().optional().describe("ISO 8601 timestamp; defaults to 14 days ago."),
70059
70305
  view: viewArg()
70060
70306
  });
70061
- function registerSubmissionTools(server, client) {
70307
+ function registerSubmissionTools(server, client2) {
70062
70308
  server.registerTool("canvas_get_submission", {
70063
70309
  description: "Get a single submission with rubric assessment and grader comments. userId defaults to 'self'.",
70064
- annotations: { readOnlyHint: true },
70310
+ annotations: { readOnlyHint: true, openWorldHint: true },
70065
70311
  inputSchema: getArgs2
70066
70312
  }, async (rawArgs) => {
70067
70313
  const args = getArgs2.parse(rawArgs);
@@ -70070,12 +70316,12 @@ function registerSubmissionTools(server, client) {
70070
70316
  `/api/v1/courses/${encodeURIComponent(args.courseId)}/assignments/${encodeURIComponent(args.assignmentId)}/submissions/${encodeURIComponent(userId)}`,
70071
70317
  { "include[]": ["submission_comments", "rubric_assessment", "assignment"] }
70072
70318
  );
70073
- const data = await client.request(path);
70319
+ const data = await client2.request(path);
70074
70320
  return viewResponse(args.view, data);
70075
70321
  });
70076
70322
  server.registerTool("canvas_list_recent_submissions", {
70077
70323
  description: "List recently graded submissions in a course. Defaults to a 14-day window for the calling user.",
70078
- annotations: { readOnlyHint: true },
70324
+ annotations: { readOnlyHint: true, openWorldHint: true },
70079
70325
  inputSchema: recentArgs
70080
70326
  }, async (rawArgs) => {
70081
70327
  const args = recentArgs.parse(rawArgs);
@@ -70087,7 +70333,7 @@ function registerSubmissionTools(server, client) {
70087
70333
  graded_since: since,
70088
70334
  "include[]": ["assignment", "submission_comments"]
70089
70335
  });
70090
- const data = await client.requestPaginated(path);
70336
+ const data = await client2.requestPaginated(path);
70091
70337
  return viewResponse(args.view, data);
70092
70338
  });
70093
70339
  }
@@ -70096,10 +70342,10 @@ function registerSubmissionTools(server, client) {
70096
70342
  var argsSchema3 = external_exports.object({
70097
70343
  observeeId: external_exports.string().optional()
70098
70344
  });
70099
- function registerGradeTools(server, client) {
70345
+ function registerGradeTools(server, client2) {
70100
70346
  server.registerTool("canvas_list_enrollments", {
70101
70347
  description: "List active student enrollments with per-course grades (current_score, final_score, current_grade, final_grade, current grading period info).",
70102
- annotations: { readOnlyHint: true },
70348
+ annotations: { readOnlyHint: true, openWorldHint: true },
70103
70349
  inputSchema: argsSchema3
70104
70350
  }, async (rawArgs) => {
70105
70351
  const args = argsSchema3.parse(rawArgs);
@@ -70108,7 +70354,7 @@ function registerGradeTools(server, client) {
70108
70354
  "type[]": "StudentEnrollment",
70109
70355
  "include[]": ["current_points", "grades"]
70110
70356
  });
70111
- const data = await client.requestPaginated(path);
70357
+ const data = await client2.requestPaginated(path);
70112
70358
  return textContent(data);
70113
70359
  });
70114
70360
  }
@@ -70121,10 +70367,10 @@ var eventsArgs = external_exports.object({
70121
70367
  endDate: external_exports.string().optional(),
70122
70368
  allEvents: external_exports.boolean().optional()
70123
70369
  });
70124
- function registerCalendarTools(server, client) {
70370
+ function registerCalendarTools(server, client2) {
70125
70371
  server.registerTool("canvas_list_calendar_events", {
70126
70372
  description: "List Canvas calendar events or assignments across selected contexts (courses/users).",
70127
- annotations: { readOnlyHint: true },
70373
+ annotations: { readOnlyHint: true, openWorldHint: true },
70128
70374
  inputSchema: eventsArgs
70129
70375
  }, async (rawArgs) => {
70130
70376
  const args = eventsArgs.parse(rawArgs);
@@ -70135,14 +70381,14 @@ function registerCalendarTools(server, client) {
70135
70381
  "context_codes[]": args.contextCodes,
70136
70382
  all_events: args.allEvents
70137
70383
  });
70138
- const data = await client.requestPaginated(path);
70384
+ const data = await client2.requestPaginated(path);
70139
70385
  return textContent(data);
70140
70386
  });
70141
70387
  server.registerTool("canvas_list_upcoming_events", {
70142
70388
  description: "List the calling user's upcoming events (Canvas's curated next-7-days view).",
70143
- annotations: { readOnlyHint: true }
70389
+ annotations: { readOnlyHint: true, openWorldHint: true }
70144
70390
  }, async () => {
70145
- const data = await client.request("/api/v1/users/self/upcoming_events");
70391
+ const data = await client2.request("/api/v1/users/self/upcoming_events");
70146
70392
  return textContent(data);
70147
70393
  });
70148
70394
  }
@@ -70155,10 +70401,10 @@ var argsSchema4 = external_exports.object({
70155
70401
  contextCodes: external_exports.array(external_exports.string()).optional(),
70156
70402
  filter: external_exports.enum(["new_activity"]).optional()
70157
70403
  });
70158
- function registerPlannerTools(server, client) {
70404
+ function registerPlannerTools(server, client2) {
70159
70405
  server.registerTool("canvas_list_planner_items", {
70160
70406
  description: "List planner items (assignments + announcements + planner notes + calendar events) for the user or a linked observee.",
70161
- annotations: { readOnlyHint: true },
70407
+ annotations: { readOnlyHint: true, openWorldHint: true },
70162
70408
  inputSchema: argsSchema4
70163
70409
  }, async (rawArgs) => {
70164
70410
  const args = argsSchema4.parse(rawArgs);
@@ -70169,7 +70415,7 @@ function registerPlannerTools(server, client) {
70169
70415
  "context_codes[]": args.contextCodes,
70170
70416
  filter: args.filter
70171
70417
  });
70172
- const data = await client.requestPaginated(path);
70418
+ const data = await client2.requestPaginated(path);
70173
70419
  return textContent(data);
70174
70420
  });
70175
70421
  }
@@ -70182,10 +70428,10 @@ var argsSchema5 = external_exports.object({
70182
70428
  activeOnly: external_exports.boolean().optional(),
70183
70429
  view: viewArg()
70184
70430
  });
70185
- function registerAnnouncementTools(server, client) {
70431
+ function registerAnnouncementTools(server, client2) {
70186
70432
  server.registerTool("canvas_list_announcements", {
70187
70433
  description: 'List announcements across one or more courses. `contextCodes` is required (e.g. ["course_123"]). Defaults to active-only.',
70188
- annotations: { readOnlyHint: true },
70434
+ annotations: { readOnlyHint: true, openWorldHint: true },
70189
70435
  inputSchema: argsSchema5
70190
70436
  }, async (rawArgs) => {
70191
70437
  const args = argsSchema5.parse(rawArgs);
@@ -70195,7 +70441,7 @@ function registerAnnouncementTools(server, client) {
70195
70441
  end_date: args.endDate,
70196
70442
  active_only: args.activeOnly ?? true
70197
70443
  });
70198
- const data = await client.requestPaginated(path);
70444
+ const data = await client2.requestPaginated(path);
70199
70445
  return viewResponse(args.view, data);
70200
70446
  });
70201
70447
  }
@@ -70210,10 +70456,10 @@ var getArgs3 = external_exports.object({
70210
70456
  id: external_exports.string(),
70211
70457
  view: viewArg()
70212
70458
  });
70213
- function registerConversationTools(server, client) {
70459
+ function registerConversationTools(server, client2) {
70214
70460
  server.registerTool("canvas_list_conversations", {
70215
70461
  description: "List Canvas inbox conversations. Optional `scope` (unread/starred/archived/sent) and `filter` (array of context codes).",
70216
- annotations: { readOnlyHint: true },
70462
+ annotations: { readOnlyHint: true, openWorldHint: true },
70217
70463
  inputSchema: listArgs3
70218
70464
  }, async (rawArgs) => {
70219
70465
  const args = listArgs3.parse(rawArgs);
@@ -70222,12 +70468,12 @@ function registerConversationTools(server, client) {
70222
70468
  "filter[]": args.filter,
70223
70469
  "include[]": ["participant_avatars"]
70224
70470
  });
70225
- const data = await client.requestPaginated(path);
70471
+ const data = await client2.requestPaginated(path);
70226
70472
  return viewResponse(args.view, data);
70227
70473
  });
70228
70474
  server.registerTool("canvas_get_conversation", {
70229
70475
  description: "Get a full Canvas conversation thread with all messages. Read-only: does not mark the conversation as read.",
70230
- annotations: { readOnlyHint: true },
70476
+ annotations: { readOnlyHint: true, openWorldHint: true },
70231
70477
  inputSchema: getArgs3
70232
70478
  }, async (rawArgs) => {
70233
70479
  const args = getArgs3.parse(rawArgs);
@@ -70235,7 +70481,7 @@ function registerConversationTools(server, client) {
70235
70481
  auto_mark_as_read: false,
70236
70482
  "include[]": ["participant_avatars"]
70237
70483
  });
70238
- const data = await client.request(path);
70484
+ const data = await client2.request(path);
70239
70485
  return viewResponse(args.view, data);
70240
70486
  });
70241
70487
  }
@@ -70247,10 +70493,10 @@ var argsSchema6 = external_exports.object({
70247
70493
  orderBy: external_exports.enum(["position", "recent_activity", "title"]).optional(),
70248
70494
  view: viewArg()
70249
70495
  });
70250
- function registerDiscussionTools(server, client) {
70496
+ function registerDiscussionTools(server, client2) {
70251
70497
  server.registerTool("canvas_list_discussion_topics", {
70252
70498
  description: "List discussion topics for a course (read-only).",
70253
- annotations: { readOnlyHint: true },
70499
+ annotations: { readOnlyHint: true, openWorldHint: true },
70254
70500
  inputSchema: argsSchema6
70255
70501
  }, async (rawArgs) => {
70256
70502
  const args = argsSchema6.parse(rawArgs);
@@ -70258,7 +70504,7 @@ function registerDiscussionTools(server, client) {
70258
70504
  only_announcements: args.onlyAnnouncements ?? false,
70259
70505
  order_by: args.orderBy
70260
70506
  });
70261
- const data = await client.requestPaginated(path);
70507
+ const data = await client2.requestPaginated(path);
70262
70508
  return viewResponse(args.view, data);
70263
70509
  });
70264
70510
  }
@@ -70274,10 +70520,10 @@ var downloadArgs = external_exports.object({
70274
70520
  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."),
70275
70521
  overwrite: external_exports.boolean().optional()
70276
70522
  });
70277
- function registerFileTools(server, client) {
70523
+ function registerFileTools(server, client2) {
70278
70524
  server.registerTool("canvas_list_course_files", {
70279
70525
  description: "List a course's files (metadata only \u2014 use canvas_download_file with the `url` field).",
70280
- annotations: { readOnlyHint: true },
70526
+ annotations: { readOnlyHint: true, openWorldHint: true },
70281
70527
  inputSchema: listArgs4
70282
70528
  }, async (rawArgs) => {
70283
70529
  const args = listArgs4.parse(rawArgs);
@@ -70285,16 +70531,16 @@ function registerFileTools(server, client) {
70285
70531
  search_term: args.searchTerm,
70286
70532
  "content_types[]": args.contentTypes
70287
70533
  });
70288
- const data = await client.requestPaginated(path);
70534
+ const data = await client2.requestPaginated(path);
70289
70535
  return textContent(data);
70290
70536
  });
70291
70537
  server.registerTool("canvas_download_file", {
70292
70538
  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).",
70293
- annotations: { destructiveHint: true, openWorldHint: true },
70539
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
70294
70540
  inputSchema: downloadArgs
70295
70541
  }, async (rawArgs) => {
70296
70542
  const args = downloadArgs.parse(rawArgs);
70297
- const meta3 = await client.download(args.url, args.destinationPath, {
70543
+ const meta3 = await client2.download(args.url, args.destinationPath, {
70298
70544
  overwrite: args.overwrite ?? false
70299
70545
  });
70300
70546
  return textContent(meta3);
@@ -70311,19 +70557,17 @@ try {
70311
70557
  } catch (e) {
70312
70558
  configError = e;
70313
70559
  }
70560
+ var client = resolved ? new CanvasClient(resolved.account, { refreshSession: resolved.refresh }) : null;
70314
70561
  await runMcp({
70315
70562
  name: "canvas",
70316
- version: "2.1.7",
70563
+ version: "2.1.9",
70317
70564
  // x-release-please-version
70318
70565
  tools: [
70319
70566
  // Outside the `if (resolved)` on purpose: an unconfigured server must
70320
70567
  // still expose something that can say WHY (see the tool's docblock).
70321
- (server) => registerHealthcheckTools(server, { resolved, configError }),
70568
+ (server) => registerHealthcheckTools(server, { resolved, configError, client }),
70322
70569
  (server) => {
70323
- if (resolved) {
70324
- const client = new CanvasClient(resolved.account, {
70325
- refreshSession: resolved.refresh
70326
- });
70570
+ if (resolved && client) {
70327
70571
  registerProfileTools(server, client);
70328
70572
  registerObserveeTools(server, client);
70329
70573
  registerCourseTools(server, client);