canvas-parent-mcp 2.1.9 → 2.1.10

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": "2.1.9"
10
+ "version": "2.1.10"
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": "2.1.9",
18
+ "version": "2.1.10",
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": "2.1.9",
4
+ "version": "2.1.10",
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/.mcp.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "mcpServers": {
3
3
  "canvas": {
4
- "command": "node",
5
- "args": ["${CLAUDE_PLUGIN_ROOT}/dist/bundle.js"],
4
+ "command": "npx",
5
+ "args": ["-y", "canvas-parent-mcp"],
6
6
  "env": {
7
7
  "CANVAS_BASE_URL": "${CANVAS_BASE_URL}",
8
8
  "CANVAS_TOKEN": "${CANVAS_TOKEN}",
package/dist/bundle.js CHANGED
@@ -91,6 +91,7 @@ var require_util = __commonJS({
91
91
  exports.explicitlyAborted = explicitlyAborted2;
92
92
  exports.prefixIssues = prefixIssues2;
93
93
  exports.unwrapMessage = unwrapMessage2;
94
+ exports.sizeCaps = sizeCaps2;
94
95
  exports.attachSchema = attachSchema2;
95
96
  exports.finalizeIssue = finalizeIssue2;
96
97
  exports.getSizableOrigin = getSizableOrigin2;
@@ -625,6 +626,18 @@ var require_util = __commonJS({
625
626
  function unwrapMessage2(message) {
626
627
  return typeof message === "string" ? message : message?.message;
627
628
  }
629
+ function sizeCaps2(defChecks) {
630
+ const caps = [];
631
+ for (const ch of defChecks ?? []) {
632
+ const d = ch._zod.def;
633
+ if (!/_(length|size)$|_equals$/.test(d.check))
634
+ break;
635
+ const max = d.maximum ?? d.length ?? d.size;
636
+ if (max >= 0)
637
+ caps.push([max, ch]);
638
+ }
639
+ return caps;
640
+ }
628
641
  function attachSchema2(issues, start, inst) {
629
642
  var _a3;
630
643
  for (let i = start; i < issues.length; i++) {
@@ -2289,7 +2302,7 @@ var require_versions = __commonJS({
2289
2302
  exports.version = {
2290
2303
  major: 4,
2291
2304
  minor: 6,
2292
- patch: 5
2305
+ patch: 7
2293
2306
  };
2294
2307
  (function() {
2295
2308
  var keys = Object.getOwnPropertyNames(exports);
@@ -3123,6 +3136,31 @@ var require_schemas = __commonJS({
3123
3136
  return payload;
3124
3137
  };
3125
3138
  });
3139
+ function capSize2(inst, size) {
3140
+ const caps = util.sizeCaps(inst._zod.def.checks);
3141
+ if (!caps.length)
3142
+ return;
3143
+ const cap = Math.min(...caps.map((c) => c[0]));
3144
+ const parse3 = inst._zod.parse;
3145
+ inst._zod.parse = (payload, ctx) => {
3146
+ const n = size(payload.value);
3147
+ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
3148
+ for (const [max, ch] of util.sizeCaps(inst._zod.def.checks)) {
3149
+ if (n > max && ch._zod.def.when(payload)) {
3150
+ const start = payload.issues.length;
3151
+ ch._zod.check(payload);
3152
+ payload.issues[start].continue = false;
3153
+ util.attachSchema(payload.issues, start, inst);
3154
+ return payload;
3155
+ }
3156
+ }
3157
+ }
3158
+ return parse3(payload, ctx);
3159
+ };
3160
+ }
3161
+ var arraySize2 = (input2) => Array.isArray(input2) ? input2.length : -1;
3162
+ var mapSize2 = (input2) => input2 instanceof Map ? input2.size : -1;
3163
+ var setSize2 = (input2) => input2 instanceof Set ? input2.size : -1;
3126
3164
  function handleArrayResult2(result, final, index) {
3127
3165
  if (result.issues.length) {
3128
3166
  final.issues.push(...util.prefixIssues(index, result.issues));
@@ -3166,6 +3204,7 @@ var require_schemas = __commonJS({
3166
3204
  }
3167
3205
  return payload;
3168
3206
  };
3207
+ capSize2(inst, arraySize2);
3169
3208
  });
3170
3209
  function handlePropertyResult2(result, final, key, input2, optin, optout) {
3171
3210
  const isPresent = key in input2;
@@ -4122,6 +4161,7 @@ var require_schemas = __commonJS({
4122
4161
  return Promise.all(proms).then(() => payload);
4123
4162
  return payload;
4124
4163
  };
4164
+ capSize2(inst, mapSize2);
4125
4165
  });
4126
4166
  function handleMapResult2(keyResult, valueResult, final, key, input2, inst, ctx) {
4127
4167
  if (keyResult.issues.length) {
@@ -4188,6 +4228,7 @@ var require_schemas = __commonJS({
4188
4228
  return Promise.all(proms).then(() => payload);
4189
4229
  return payload;
4190
4230
  };
4231
+ capSize2(inst, setSize2);
4191
4232
  });
4192
4233
  function handleSetResult2(result, final) {
4193
4234
  if (result.issues.length) {
@@ -16292,9 +16333,15 @@ ${code}
16292
16333
  doc.write(`}`);
16293
16334
  return outputVar;
16294
16335
  }
16336
+ function generateSizeGate2(doc, schema, size) {
16337
+ for (const [max, check2] of util.sizeCaps(schema._zod.def.checks)) {
16338
+ doc.write(`if (${size} > ${numericOperand2(max, check2._zod.def.check)}) return INVALID;`);
16339
+ }
16340
+ }
16295
16341
  function generateArrayCheck2(doc, ctx, schema, accessor, buildsValue = true) {
16296
16342
  const def = schema._zod.def;
16297
16343
  doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
16344
+ generateSizeGate2(doc, schema, `${accessor}.length`);
16298
16345
  const outputVar = buildsValue ? newVar2(ctx) : null;
16299
16346
  const iVar = newVar2(ctx);
16300
16347
  const elemVar = newVar2(ctx);
@@ -16694,6 +16741,7 @@ ${code}
16694
16741
  function generateMapCheck2(doc, ctx, schema, accessor) {
16695
16742
  const def = schema._zod.def;
16696
16743
  doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
16744
+ generateSizeGate2(doc, schema, `${accessor}.size`);
16697
16745
  const outputVar = newVar2(ctx);
16698
16746
  const kVar = newVar2(ctx);
16699
16747
  const valVar = newVar2(ctx);
@@ -16710,6 +16758,7 @@ ${code}
16710
16758
  function generateSetCheck2(doc, ctx, schema, accessor) {
16711
16759
  const def = schema._zod.def;
16712
16760
  doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
16761
+ generateSizeGate2(doc, schema, `${accessor}.size`);
16713
16762
  const outputVar = newVar2(ctx);
16714
16763
  const valVar = newVar2(ctx);
16715
16764
  doc.write(`const ${outputVar} = new Set();`);
@@ -27078,7 +27127,7 @@ function declaredDialect(schema, remedy) {
27078
27127
  throw new Error(`JSON Schema declares an unsupported dialect ("$schema": "${schema.$schema.slice(0, 200)}"). The default validator supports JSON Schema 2020-12, 2019-09, draft-07, and draft-06; ${remedy}`);
27079
27128
  }
27080
27129
 
27081
- // node_modules/@modelcontextprotocol/core/dist/auth-BNDyLTqp.mjs
27130
+ // node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/dist/auth-BNDyLTqp.mjs
27082
27131
  var z = __toESM(require_v4(), 1);
27083
27132
  var LATEST_PROTOCOL_VERSION = "2025-11-25";
27084
27133
  var SUPPORTED_PROTOCOL_VERSIONS = [
@@ -41874,7 +41923,7 @@ var API_KEY_RE = new RegExp([
41874
41923
  // webhook signing secret (Stripe-style)
41875
41924
  ].map((p) => `\\b${p}`).join("|"), "g");
41876
41925
  var GOOGLE_OAUTH_TOKEN_RE = /(?<![A-Za-z0-9+/])(?:ya29\.|1\/\/)[A-Za-z0-9._-]{8,}/g;
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");
41926
+ var QUERY_SECRET_RE = new RegExp(`([?&#;](?:(?:access|refresh|id|auth|session|csrf|xsrf|api)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|secret|token|auth|key|sig|(?:code[_-]?)?verifier|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
41878
41927
  var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
41879
41928
  var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
41880
41929
  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");
@@ -41885,6 +41934,8 @@ var JSON_SECRET_KEYS = [
41885
41934
  "api[_-]?secret",
41886
41935
  "(?:x[_-])?api[_-]?key",
41887
41936
  "private[_-]?key",
41937
+ "aws[_-]?secret[_-]?access[_-]?key",
41938
+ // fleet-audit#1297
41888
41939
  "code[_-]?verifier",
41889
41940
  // PKCE; a bare `verifier` key is too generic for JSON
41890
41941
  `x[_-]${HEADER_NAME_RUN}?(?:api[_-]?key|token|secret|auth)${HEADER_NAME_RUN}`,
@@ -42582,6 +42633,7 @@ __export(util_exports, {
42582
42633
  required: () => required,
42583
42634
  safeExtend: () => safeExtend,
42584
42635
  shallowClone: () => shallowClone,
42636
+ sizeCaps: () => sizeCaps,
42585
42637
  slugify: () => slugify,
42586
42638
  stringifyPrimitive: () => stringifyPrimitive,
42587
42639
  toZod: () => toZod,
@@ -43101,6 +43153,18 @@ function prefixIssues(path, issues) {
43101
43153
  function unwrapMessage(message) {
43102
43154
  return typeof message === "string" ? message : message?.message;
43103
43155
  }
43156
+ function sizeCaps(defChecks) {
43157
+ const caps = [];
43158
+ for (const ch of defChecks ?? []) {
43159
+ const d = ch._zod.def;
43160
+ if (!/_(length|size)$|_equals$/.test(d.check))
43161
+ break;
43162
+ const max = d.maximum ?? d.length ?? d.size;
43163
+ if (max >= 0)
43164
+ caps.push([max, ch]);
43165
+ }
43166
+ return caps;
43167
+ }
43104
43168
  function attachSchema(issues, start, inst) {
43105
43169
  var _a3;
43106
43170
  for (let i = start; i < issues.length; i++) {
@@ -44521,7 +44585,7 @@ ${content.join("\n")}
44521
44585
  var version = {
44522
44586
  major: 4,
44523
44587
  minor: 6,
44524
- patch: 5
44588
+ patch: 7
44525
44589
  };
44526
44590
 
44527
44591
  // node_modules/zod/v4/core/schemas.js
@@ -45277,6 +45341,31 @@ var $ZodDate = /* @__PURE__ */ $constructor("$ZodDate", (inst, def) => {
45277
45341
  return payload;
45278
45342
  };
45279
45343
  });
45344
+ function capSize(inst, size) {
45345
+ const caps = sizeCaps(inst._zod.def.checks);
45346
+ if (!caps.length)
45347
+ return;
45348
+ const cap = Math.min(...caps.map((c) => c[0]));
45349
+ const parse3 = inst._zod.parse;
45350
+ inst._zod.parse = (payload, ctx) => {
45351
+ const n = size(payload.value);
45352
+ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
45353
+ for (const [max, ch] of sizeCaps(inst._zod.def.checks)) {
45354
+ if (n > max && ch._zod.def.when(payload)) {
45355
+ const start = payload.issues.length;
45356
+ ch._zod.check(payload);
45357
+ payload.issues[start].continue = false;
45358
+ attachSchema(payload.issues, start, inst);
45359
+ return payload;
45360
+ }
45361
+ }
45362
+ }
45363
+ return parse3(payload, ctx);
45364
+ };
45365
+ }
45366
+ var arraySize = (input2) => Array.isArray(input2) ? input2.length : -1;
45367
+ var mapSize = (input2) => input2 instanceof Map ? input2.size : -1;
45368
+ var setSize = (input2) => input2 instanceof Set ? input2.size : -1;
45280
45369
  function handleArrayResult(result, final, index) {
45281
45370
  if (result.issues.length) {
45282
45371
  final.issues.push(...prefixIssues(index, result.issues));
@@ -45320,6 +45409,7 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
45320
45409
  }
45321
45410
  return payload;
45322
45411
  };
45412
+ capSize(inst, arraySize);
45323
45413
  });
45324
45414
  function handlePropertyResult(result, final, key, input2, optin, optout) {
45325
45415
  const isPresent = key in input2;
@@ -46276,6 +46366,7 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
46276
46366
  return Promise.all(proms).then(() => payload);
46277
46367
  return payload;
46278
46368
  };
46369
+ capSize(inst, mapSize);
46279
46370
  });
46280
46371
  function handleMapResult(keyResult, valueResult, final, key, input2, inst, ctx) {
46281
46372
  if (keyResult.issues.length) {
@@ -46342,6 +46433,7 @@ var $ZodSet = /* @__PURE__ */ $constructor("$ZodSet", (inst, def) => {
46342
46433
  return Promise.all(proms).then(() => payload);
46343
46434
  return payload;
46344
46435
  };
46436
+ capSize(inst, setSize);
46345
46437
  });
46346
46438
  function handleSetResult(result, final) {
46347
46439
  if (result.issues.length) {
@@ -55872,9 +55964,15 @@ function generateNullableCheck(doc, ctx, schema, accessor, buildsValue = true) {
55872
55964
  doc.write(`}`);
55873
55965
  return outputVar;
55874
55966
  }
55967
+ function generateSizeGate(doc, schema, size) {
55968
+ for (const [max, check2] of sizeCaps(schema._zod.def.checks)) {
55969
+ doc.write(`if (${size} > ${numericOperand(max, check2._zod.def.check)}) return INVALID;`);
55970
+ }
55971
+ }
55875
55972
  function generateArrayCheck(doc, ctx, schema, accessor, buildsValue = true) {
55876
55973
  const def = schema._zod.def;
55877
55974
  doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
55975
+ generateSizeGate(doc, schema, `${accessor}.length`);
55878
55976
  const outputVar = buildsValue ? newVar(ctx) : null;
55879
55977
  const iVar = newVar(ctx);
55880
55978
  const elemVar = newVar(ctx);
@@ -56274,6 +56372,7 @@ function literalPropertyKey(ctx, key) {
56274
56372
  function generateMapCheck(doc, ctx, schema, accessor) {
56275
56373
  const def = schema._zod.def;
56276
56374
  doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
56375
+ generateSizeGate(doc, schema, `${accessor}.size`);
56277
56376
  const outputVar = newVar(ctx);
56278
56377
  const kVar = newVar(ctx);
56279
56378
  const valVar = newVar(ctx);
@@ -56290,6 +56389,7 @@ function generateMapCheck(doc, ctx, schema, accessor) {
56290
56389
  function generateSetCheck(doc, ctx, schema, accessor) {
56291
56390
  const def = schema._zod.def;
56292
56391
  doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
56392
+ generateSizeGate(doc, schema, `${accessor}.size`);
56293
56393
  const outputVar = newVar(ctx);
56294
56394
  const valVar = newVar(ctx);
56295
56395
  doc.write(`const ${outputVar} = new Set();`);
@@ -62002,10 +62102,34 @@ import { constants as fsConstants2, realpathSync as realpathSync2 } from "node:f
62002
62102
  var O_NOFOLLOW2 = fsConstants2.O_NOFOLLOW ?? 0;
62003
62103
  var O_NONBLOCK = fsConstants2.O_NONBLOCK ?? 0;
62004
62104
 
62105
+ // node_modules/@chrischall/mcp-utils/dist/internal/ascii.js
62106
+ function asciiLower(s) {
62107
+ return s.replace(/[A-Z]+/g, (m) => m.toLowerCase());
62108
+ }
62109
+
62005
62110
  // node_modules/@chrischall/mcp-utils/dist/scrape/index.js
62006
62111
  var NEXT_DATA_MAX_CHARS = 16 * 1024 * 1024;
62007
62112
  function isCloudflareChallenge(html) {
62008
- return /_cf_chl_opt/.test(html) || /<title[^>]*>\s*Just a moment/i.test(html);
62113
+ return html.includes("_cf_chl_opt") || hasJustAMomentTitle(html);
62114
+ }
62115
+ var JUST_A_MOMENT = "just a moment";
62116
+ function hasJustAMomentTitle(html) {
62117
+ const lower = asciiLower(html);
62118
+ let i = 0;
62119
+ for (; ; ) {
62120
+ const open3 = lower.indexOf("<title", i);
62121
+ if (open3 < 0)
62122
+ return false;
62123
+ const gt = lower.indexOf(">", open3 + 6);
62124
+ if (gt < 0)
62125
+ return false;
62126
+ let j = gt + 1;
62127
+ while (j < lower.length && /\s/.test(lower[j]))
62128
+ j++;
62129
+ if (lower.startsWith(JUST_A_MOMENT, j))
62130
+ return true;
62131
+ i = gt + 1;
62132
+ }
62009
62133
  }
62010
62134
 
62011
62135
  // node_modules/@chrischall/mcp-utils/dist/http/index.js
@@ -62869,6 +62993,17 @@ function isPublicSuffix(host) {
62869
62993
  return false;
62870
62994
  }
62871
62995
 
62996
+ // node_modules/@fetchproxy/protocol/dist/ip-literal.js
62997
+ function isIpLiteralHost(host) {
62998
+ if (host.includes(":") || host.startsWith("["))
62999
+ return true;
63000
+ const labels = host.split(".");
63001
+ if (labels.length > 1 && labels[labels.length - 1] === "")
63002
+ labels.pop();
63003
+ const last = labels[labels.length - 1] ?? "";
63004
+ return /^[0-9]+$/.test(last) || /^0x[0-9a-f]*$/i.test(last);
63005
+ }
63006
+
62872
63007
  // node_modules/@fetchproxy/protocol/dist/validate.js
62873
63008
  var ProtocolError3 = class extends Error {
62874
63009
  constructor(message) {
@@ -63334,6 +63469,9 @@ function validateHello(raw) {
63334
63469
  if (!HOSTNAME_RE.test(d)) {
63335
63470
  throw new ProtocolError3(`hello.domains: invalid hostname ${JSON.stringify(d)}`);
63336
63471
  }
63472
+ if (isIpLiteralHost(d)) {
63473
+ throw new ProtocolError3(`hello.domains: ${JSON.stringify(d)} is an IP address \u2014 declare the site's DNS hostname; IP-literal origins (LAN, loopback, link-local) are not reachable through the bridge`);
63474
+ }
63337
63475
  if (isPublicSuffix(d)) {
63338
63476
  throw new ProtocolError3(`hello.domains: ${JSON.stringify(d)} is a public suffix \u2014 declare a registrable domain (e.g. "example.${d}"), not the suffix every site under it shares`);
63339
63477
  }
@@ -65459,7 +65597,7 @@ async function startHost(opts) {
65459
65597
  if (ownRetryableRefusal)
65460
65598
  resetSessionPromise();
65461
65599
  resolveOwnSession(ownSession);
65462
- } else {
65600
+ } else if (identified === "extension" && extensionWs === ws) {
65463
65601
  const slot = peers.get(frame.mcpId);
65464
65602
  if (slot)
65465
65603
  slot.ws.send(JSON.stringify(frame));
@@ -66485,6 +66623,9 @@ var FetchproxyServer = class {
66485
66623
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
66486
66624
  }
66487
66625
  for (const d of opts.domains) {
66626
+ if (typeof d === "string" && isIpLiteralHost(d)) {
66627
+ throw new Error(`FetchproxyServer: opts.domains entry ${JSON.stringify(d)} is an IP address \u2014 declare the site's DNS hostname; IP-literal origins (LAN, loopback, link-local) are not reachable through the bridge`);
66628
+ }
66488
66629
  if (typeof d === "string" && isPublicSuffix(d)) {
66489
66630
  throw new Error(`FetchproxyServer: opts.domains entry ${JSON.stringify(d)} is a public suffix \u2014 declare a registrable domain (e.g. "example.${d}"), not the suffix every site under it shares`);
66490
66631
  }
@@ -68419,6 +68560,9 @@ var FetchproxyServer = class {
68419
68560
  }
68420
68561
  };
68421
68562
 
68563
+ // node_modules/@fetchproxy/server/dist/bot-wall.js
68564
+ var CLOUDFLARE_TITLE_SCAN_MAX = 64 * 1024;
68565
+
68422
68566
  // node_modules/@fetchproxy/bootstrap/dist/index.js
68423
68567
  var defaultFactory = (opts) => new FetchproxyServer(opts);
68424
68568
  function createSessionLifter(opts) {
@@ -68704,7 +68848,7 @@ function loadAccount(env = process.env) {
68704
68848
  // package.json
68705
68849
  var package_default = {
68706
68850
  name: "canvas-parent-mcp",
68707
- version: "2.1.9",
68851
+ version: "2.1.10",
68708
68852
  mcpName: "io.github.chrischall/canvas-parent-mcp",
68709
68853
  description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
68710
68854
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -68751,9 +68895,9 @@ var package_default = {
68751
68895
  typecheck: "tsc -p tsconfig.json --noEmit"
68752
68896
  },
68753
68897
  dependencies: {
68754
- "@chrischall/mcp-utils": "^3.0.0",
68755
- "@fetchproxy/bootstrap": "^3.6.0",
68756
- "@fetchproxy/server": "^3.6.0",
68898
+ "@chrischall/mcp-utils": "^3.0.3",
68899
+ "@fetchproxy/bootstrap": "^3.6.2",
68900
+ "@fetchproxy/server": "^3.6.2",
68757
68901
  "@modelcontextprotocol/server": "^2.3.0",
68758
68902
  dotenv: "^18.0.0",
68759
68903
  zod: "^4.6.5"
@@ -69813,10 +69957,29 @@ var CanvasClient = class {
69813
69957
  const linkHeader = res.headers.get("link");
69814
69958
  const next = linkHeader ? parseLinkHeader(linkHeader).next : void 0;
69815
69959
  if (!next) break;
69816
- url4 = next;
69960
+ url4 = this.pinPaginationUrl(next);
69817
69961
  }
69818
69962
  return out;
69819
69963
  }
69964
+ /**
69965
+ * The Link header is upstream-controlled, and the next hop carries the
69966
+ * parent's credential (Bearer token or session cookies). Only follow it on
69967
+ * the configured Canvas origin over https; anything else is refused rather
69968
+ * than fetched — a truncated list reported as complete would mislead too.
69969
+ */
69970
+ pinPaginationUrl(next) {
69971
+ const base = new URL(this.account.baseUrl);
69972
+ let parsed;
69973
+ try {
69974
+ parsed = new URL(next, base);
69975
+ } catch {
69976
+ throw new PaginationOriginError(base.origin);
69977
+ }
69978
+ if (parsed.protocol !== "https:" || parsed.origin !== base.origin || parsed.username || parsed.password) {
69979
+ throw new PaginationOriginError(base.origin);
69980
+ }
69981
+ return parsed.href;
69982
+ }
69820
69983
  /**
69821
69984
  * Download a Canvas file to disk.
69822
69985
  *
@@ -70076,6 +70239,14 @@ var CanvasUnreachableError = class extends Error {
70076
70239
  }
70077
70240
  status;
70078
70241
  };
70242
+ var PaginationOriginError = class extends Error {
70243
+ constructor(origin) {
70244
+ super(`PaginationOriginRejected: Canvas sent a next-page link off ${origin}; stopped without following it (the request carries your Canvas credential).`);
70245
+ this.origin = origin;
70246
+ this.name = "PaginationOriginError";
70247
+ }
70248
+ origin;
70249
+ };
70079
70250
  var DownloadUrlRejectedError = class extends Error {
70080
70251
  constructor(url4, reason) {
70081
70252
  super(`DownloadUrlRejected: ${reason}; use the \`url\` from canvas_list_course_files. Got: ${url4}`);
@@ -70560,7 +70731,7 @@ try {
70560
70731
  var client = resolved ? new CanvasClient(resolved.account, { refreshSession: resolved.refresh }) : null;
70561
70732
  await runMcp({
70562
70733
  name: "canvas",
70563
- version: "2.1.9",
70734
+ version: "2.1.10",
70564
70735
  // x-release-please-version
70565
70736
  tools: [
70566
70737
  // Outside the `if (resolved)` on purpose: an unconfigured server must
package/dist/client.js CHANGED
@@ -90,10 +90,30 @@ export class CanvasClient {
90
90
  const next = linkHeader ? parseLinkHeader(linkHeader).next : undefined;
91
91
  if (!next)
92
92
  break;
93
- url = next;
93
+ url = this.pinPaginationUrl(next);
94
94
  }
95
95
  return out;
96
96
  }
97
+ /**
98
+ * The Link header is upstream-controlled, and the next hop carries the
99
+ * parent's credential (Bearer token or session cookies). Only follow it on
100
+ * the configured Canvas origin over https; anything else is refused rather
101
+ * than fetched — a truncated list reported as complete would mislead too.
102
+ */
103
+ pinPaginationUrl(next) {
104
+ const base = new URL(this.account.baseUrl);
105
+ let parsed;
106
+ try {
107
+ parsed = new URL(next, base);
108
+ }
109
+ catch {
110
+ throw new PaginationOriginError(base.origin);
111
+ }
112
+ if (parsed.protocol !== 'https:' || parsed.origin !== base.origin || parsed.username || parsed.password) {
113
+ throw new PaginationOriginError(base.origin);
114
+ }
115
+ return parsed.href;
116
+ }
97
117
  /**
98
118
  * Download a Canvas file to disk.
99
119
  *
@@ -409,6 +429,14 @@ export class CanvasUnreachableError extends Error {
409
429
  this.name = 'CanvasUnreachableError';
410
430
  }
411
431
  }
432
+ export class PaginationOriginError extends Error {
433
+ origin;
434
+ constructor(origin) {
435
+ super(`PaginationOriginRejected: Canvas sent a next-page link off ${origin}; stopped without following it (the request carries your Canvas credential).`);
436
+ this.origin = origin;
437
+ this.name = 'PaginationOriginError';
438
+ }
439
+ }
412
440
  export class DownloadUrlRejectedError extends Error {
413
441
  url;
414
442
  constructor(url, reason) {
package/dist/index.js CHANGED
@@ -54,7 +54,7 @@ const client = resolved
54
54
  // host's install-time `tools/list` succeeds.
55
55
  await runMcp({
56
56
  name: 'canvas',
57
- version: '2.1.9', // x-release-please-version
57
+ version: '2.1.10', // x-release-please-version
58
58
  tools: [
59
59
  // Outside the `if (resolved)` on purpose: an unconfigured server must
60
60
  // still expose something that can say WHY (see the tool's docblock).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvas-parent-mcp",
3
- "version": "2.1.9",
3
+ "version": "2.1.10",
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>",
@@ -47,9 +47,9 @@
47
47
  "typecheck": "tsc -p tsconfig.json --noEmit"
48
48
  },
49
49
  "dependencies": {
50
- "@chrischall/mcp-utils": "^3.0.0",
51
- "@fetchproxy/bootstrap": "^3.6.0",
52
- "@fetchproxy/server": "^3.6.0",
50
+ "@chrischall/mcp-utils": "^3.0.3",
51
+ "@fetchproxy/bootstrap": "^3.6.2",
52
+ "@fetchproxy/server": "^3.6.2",
53
53
  "@modelcontextprotocol/server": "^2.3.0",
54
54
  "dotenv": "^18.0.0",
55
55
  "zod": "^4.6.5"
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": "2.1.9",
9
+ "version": "2.1.10",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "canvas-parent-mcp",
14
- "version": "2.1.9",
14
+ "version": "2.1.10",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },