canvas-lms-mcp 1.24.0 → 1.26.0

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.
Files changed (83) hide show
  1. package/README.md +31 -2
  2. package/dist/canvas/concurrency.d.ts +19 -0
  3. package/dist/canvas/concurrency.d.ts.map +1 -0
  4. package/dist/canvas/index.cjs +3 -3
  5. package/dist/canvas/index.js +2 -2
  6. package/dist/canvas/pages.d.ts +11 -3
  7. package/dist/canvas/pages.d.ts.map +1 -1
  8. package/dist/{chunk-7VJLBXZD.cjs → chunk-DWITOJXW.cjs} +2 -2
  9. package/dist/chunk-DWITOJXW.cjs.map +1 -0
  10. package/dist/{chunk-LKR72KFD.js → chunk-P3XSHNRJ.js} +2 -2
  11. package/dist/chunk-P3XSHNRJ.js.map +1 -0
  12. package/dist/{chunk-4GMSSKK6.cjs → chunk-VPWK2XWW.cjs} +33 -9
  13. package/dist/chunk-VPWK2XWW.cjs.map +1 -0
  14. package/dist/{chunk-6II3HREL.js → chunk-VXP7HVLD.js} +30 -6
  15. package/dist/chunk-VXP7HVLD.js.map +1 -0
  16. package/dist/{chunk-5ZWXPFMW.js → chunk-YM7NTCXP.js} +226 -31
  17. package/dist/chunk-YM7NTCXP.js.map +1 -0
  18. package/dist/{chunk-AI4XQHKN.cjs → chunk-ZN6ASOZQ.cjs} +256 -61
  19. package/dist/chunk-ZN6ASOZQ.cjs.map +1 -0
  20. package/dist/discovery/manifests.d.ts +1 -0
  21. package/dist/discovery/manifests.d.ts.map +1 -1
  22. package/dist/http.cjs +5 -5
  23. package/dist/http.js +3 -3
  24. package/dist/init.cjs +3 -3
  25. package/dist/init.js +1 -1
  26. package/dist/server.cjs +4 -4
  27. package/dist/server.js +3 -3
  28. package/dist/stdio.cjs +4 -4
  29. package/dist/stdio.js +3 -3
  30. package/dist/tools/accessibility-audit.d.ts.map +1 -1
  31. package/dist/tools/accounts.d.ts.map +1 -1
  32. package/dist/tools/analytics.d.ts.map +1 -1
  33. package/dist/tools/appointment-groups.d.ts.map +1 -1
  34. package/dist/tools/assignment-overrides.d.ts.map +1 -1
  35. package/dist/tools/assignments.d.ts.map +1 -1
  36. package/dist/tools/attention.d.ts.map +1 -1
  37. package/dist/tools/calendar.d.ts.map +1 -1
  38. package/dist/tools/content-exports.d.ts.map +1 -1
  39. package/dist/tools/content-migrations.d.ts.map +1 -1
  40. package/dist/tools/conversations.d.ts.map +1 -1
  41. package/dist/tools/course-setup.d.ts.map +1 -1
  42. package/dist/tools/courses.d.ts.map +1 -1
  43. package/dist/tools/dashboard.d.ts.map +1 -1
  44. package/dist/tools/discussions.d.ts.map +1 -1
  45. package/dist/tools/enrollments.d.ts.map +1 -1
  46. package/dist/tools/files.d.ts.map +1 -1
  47. package/dist/tools/grade-explanation.d.ts.map +1 -1
  48. package/dist/tools/grade-projection.d.ts.map +1 -1
  49. package/dist/tools/gradebook-history.d.ts.map +1 -1
  50. package/dist/tools/grading-policy.d.ts.map +1 -1
  51. package/dist/tools/grading-standards.d.ts.map +1 -1
  52. package/dist/tools/groups.d.ts.map +1 -1
  53. package/dist/tools/health.d.ts.map +1 -1
  54. package/dist/tools/index.d.ts.map +1 -1
  55. package/dist/tools/link-audit.d.ts.map +1 -1
  56. package/dist/tools/modules.d.ts.map +1 -1
  57. package/dist/tools/new-quiz-accommodations.d.ts.map +1 -1
  58. package/dist/tools/new-quizzes.d.ts.map +1 -1
  59. package/dist/tools/outcomes.d.ts.map +1 -1
  60. package/dist/tools/pages.d.ts.map +1 -1
  61. package/dist/tools/peer-reviews.d.ts.map +1 -1
  62. package/dist/tools/pseudonym.d.ts.map +1 -1
  63. package/dist/tools/quiz-accommodations.d.ts.map +1 -1
  64. package/dist/tools/quiz-question-responses.d.ts.map +1 -1
  65. package/dist/tools/quizzes.d.ts.map +1 -1
  66. package/dist/tools/rubrics.d.ts.map +1 -1
  67. package/dist/tools/student-search.d.ts.map +1 -1
  68. package/dist/tools/student.d.ts.map +1 -1
  69. package/dist/tools/submission-files.d.ts.map +1 -1
  70. package/dist/tools/submissions-awaiting-grading.d.ts.map +1 -1
  71. package/dist/tools/submissions.d.ts.map +1 -1
  72. package/dist/tools/titles.d.ts +26 -0
  73. package/dist/tools/titles.d.ts.map +1 -0
  74. package/dist/tools/types.d.ts +9 -0
  75. package/dist/tools/types.d.ts.map +1 -1
  76. package/dist/tools/users.d.ts.map +1 -1
  77. package/package.json +1 -13
  78. package/dist/chunk-4GMSSKK6.cjs.map +0 -1
  79. package/dist/chunk-5ZWXPFMW.js.map +0 -1
  80. package/dist/chunk-6II3HREL.js.map +0 -1
  81. package/dist/chunk-7VJLBXZD.cjs.map +0 -1
  82. package/dist/chunk-AI4XQHKN.cjs.map +0 -1
  83. package/dist/chunk-LKR72KFD.js.map +0 -1
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
  [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
9
9
  [![npm downloads](https://img.shields.io/npm/dw/canvas-lms-mcp)](https://www.npmjs.com/package/canvas-lms-mcp)
10
- [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.bruchris%2Fcanvas--lms--mcp-blue)](https://registry.modelcontextprotocol.io/v0/servers/io.github.bruchris%2Fcanvas-lms-mcp)
10
+ [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.bruchris%2Fcanvas--lms--mcp-blue)](https://registry.modelcontextprotocol.io/v0/servers/io.github.bruchris%2Fcanvas-lms-mcp/versions/latest)
11
11
 
12
12
  MCP server for [Canvas LMS](https://www.instructure.com/canvas). Read courses, assignments, submissions, rubrics, quizzes; grade, comment, manage course content, and handle Canvas admin workflows from any AI agent.
13
13
 
@@ -17,12 +17,29 @@ MCP server for [Canvas LMS](https://www.instructure.com/canvas). Read courses, a
17
17
 
18
18
  1. **[Download `canvas-lms-mcp.mcpb`](https://github.com/bruchris/canvas-lms-mcp/releases/latest/download/canvas-lms-mcp.mcpb)** from the latest release.
19
19
  2. Double-click the file (or drag it into Claude Desktop's Extensions settings).
20
- 3. When prompted, paste your Canvas API token and Canvas API base URL (e.g. `https://school.instructure.com/api/v1`). Teachers and staff handling student data can also flip **FERPA mode — pseudonymize students** on in the same dialog ([what it does](#ferpa-mode-student-pseudonymization)).
20
+ 3. When prompted, paste your Canvas API token and Canvas base URL — your institution's origin only, e.g. `https://school.instructure.com` (do not append `/api/v1`). Teachers and staff handling student data can also flip **FERPA mode — pseudonymize students** on in the same dialog ([what it does](#ferpa-mode-student-pseudonymization)).
21
21
 
22
22
  No terminal, no Node.js install, no config-file editing — Claude Desktop bundles the runtime and handles config for you. The same `.mcpb` works in Claude Code and MCP for Windows.
23
23
 
24
24
  Prefer the terminal? Use the [Quick Start](#quick-start) below.
25
25
 
26
+ ## One-click install (Cursor / VS Code)
27
+
28
+ [![Add to Cursor](https://img.shields.io/badge/Add%20to-Cursor-black?logo=cursor&logoColor=white)](cursor://anysphere.cursor-deeplink/mcp/install?name=canvas-lms-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNhbnZhcy1sbXMtbWNwIl0sImVudiI6eyJDQU5WQVNfQVBJX1RPS0VOIjoieW91ci10b2tlbi1oZXJlIiwiQ0FOVkFTX0JBU0VfVVJMIjoiaHR0cHM6Ly95b3VyLWluc3RpdHV0aW9uLmluc3RydWN0dXJlLmNvbSJ9fQ==)
29
+ [![Install in VS Code](https://img.shields.io/badge/Install%20in-VS%20Code-007ACC?logo=visual-studio-code&logoColor=white)](vscode:mcp/install?%7B%22name%22%3A%22canvas-lms%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22canvas-lms-mcp%22%5D%2C%22env%22%3A%7B%22CANVAS_API_TOKEN%22%3A%22your-token-here%22%2C%22CANVAS_BASE_URL%22%3A%22https%3A%2F%2Fyour-institution.instructure.com%22%7D%7D)
30
+ [![Install in VS Code Insiders](https://img.shields.io/badge/Install%20in-VS%20Code%20Insiders-24bfa5?logo=visual-studio-code&logoColor=white)](vscode-insiders:mcp/install?%7B%22name%22%3A%22canvas-lms%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22canvas-lms-mcp%22%5D%2C%22env%22%3A%7B%22CANVAS_API_TOKEN%22%3A%22your-token-here%22%2C%22CANVAS_BASE_URL%22%3A%22https%3A%2F%2Fyour-institution.instructure.com%22%7D%7D)
31
+
32
+ Click a badge to open Cursor or VS Code with canvas-lms-mcp pre-configured (placeholder credentials filled in — replace with your actual Canvas API token and base URL after install). For manual config-file setup, see [docs/manual-setup.md](./docs/manual-setup.md).
33
+
34
+ ## One-click install (Claude Code plugin)
35
+
36
+ ```
37
+ /plugin marketplace add bruchris/canvas-lms-mcp
38
+ /plugin install canvas-lms-mcp
39
+ ```
40
+
41
+ Installs the MCP server (via `npx canvas-lms-mcp`) and all 16 [Agent Skills](#agent-skills) in a single step, versioned and updatable through Claude Code's plugin manager. On enable, Claude Code prompts for your Canvas API token and base URL (and the optional FERPA pseudonymization settings). See the [Claude Code plugins reference](https://code.claude.com/docs/en/plugins-reference) for how marketplaces and plugin manifests work.
42
+
26
43
  ## Comparison
27
44
 
28
45
  | | canvas-lms-mcp | [vishalsachdev/canvas-mcp](https://github.com/vishalsachdev/canvas-mcp) | [DMontgomery40/mcp-canvas-lms](https://github.com/DMontgomery40/mcp-canvas-lms) |
@@ -67,6 +84,8 @@ npx skills add bruchris/canvas-lms-mcp
67
84
  | `canvas-at-risk-students` | Surface students with missing assignments or declining grades and send targeted outreach |
68
85
  | `canvas-gradebook-audit` | Inspect the full grade-change audit trail — who changed what grade, when, and by how much |
69
86
  | `canvas-outcome-tracker` | Track learning outcome mastery and class-wide proficiency for accreditation and program review |
87
+ | `canvas-accessibility-sweep` | Pre-launch WCAG accessibility and broken-link sweep of a course, with a prioritised remediation list |
88
+ | `canvas-office-hours` | Create, publish, and manage Canvas Scheduler office-hour sign-up slots and see who reserved |
70
89
 
71
90
  Skills are markdown workflow files (no extra dependencies). They work with the MCP server you already have installed. See the [`skills/` directory](./skills/) for the full list.
72
91
 
@@ -365,6 +384,16 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full contribution and validatio
365
384
  - [Educator Assignment Review Workflow](docs/workflows/educator-assignment-review.md) -- Read-first grading flow with write-safety guidance
366
385
  - [Student Weekly Planning Workflow](docs/workflows/student-weekly-planning.md) -- Read-only weekly planning sequence for students
367
386
 
387
+ ## Privacy Policy
388
+
389
+ `canvas-lms-mcp` runs entirely on your own machine. The maintainers operate no
390
+ servers and collect no telemetry or analytics — your Canvas API token and all
391
+ Canvas data stay local and travel only between your machine and your own Canvas
392
+ instance. Optional FERPA pseudonymization runs locally; the only data written to
393
+ disk is the optional pseudonym map and audit log. Full details — data
394
+ collection, usage, storage, third-party sharing, retention, and contact — are in
395
+ [PRIVACY.md](PRIVACY.md).
396
+
368
397
  ## License
369
398
 
370
399
  MIT
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Run `fn` across `items` with at most `limit` invocations in flight at once,
3
+ * returning the results in the SAME order as `items` regardless of the order in
4
+ * which the individual calls settle (`results[i]` always corresponds to
5
+ * `items[i]`).
6
+ *
7
+ * This is the single shared implementation of the batched fan-out first written
8
+ * inline for `find_student_across_courses` (PR #252). Whole-course scans such as
9
+ * {@link PagesModule.listWithBodies} would otherwise issue one Canvas request per
10
+ * item all at once — a 300-page course means 300 simultaneous requests with no
11
+ * backstop. Bounding the fan-out keeps a large course from hammering Canvas.
12
+ *
13
+ * Error semantics match a plain `Promise.all`: the first rejection propagates
14
+ * unchanged and no further batches are started. Call sites that want to tolerate
15
+ * per-item failures (like `find_student_across_courses`) do so by catching inside
16
+ * `fn` and resolving with a discriminated result, never by letting `fn` throw.
17
+ */
18
+ export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
19
+ //# sourceMappingURL=concurrency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.d.ts","sourceRoot":"","sources":["../../src/canvas/concurrency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAC3C,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GACzC,OAAO,CAAC,CAAC,EAAE,CAAC,CAcd"}
@@ -1,17 +1,17 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunk4GMSSKK6cjs = require('../chunk-4GMSSKK6.cjs');
3
+ var _chunkVPWK2XWWcjs = require('../chunk-VPWK2XWW.cjs');
4
4
 
5
5
 
6
6
 
7
7
 
8
8
 
9
- var _chunk7VJLBXZDcjs = require('../chunk-7VJLBXZD.cjs');
9
+ var _chunkDWITOJXWcjs = require('../chunk-DWITOJXW.cjs');
10
10
 
11
11
 
12
12
 
13
13
 
14
14
 
15
15
 
16
- exports.CanvasApiError = _chunk7VJLBXZDcjs.CanvasApiError; exports.CanvasClient = _chunk4GMSSKK6cjs.CanvasClient; exports.CanvasHttpClient = _chunk7VJLBXZDcjs.CanvasHttpClient; exports.appendCanvasQuery = _chunk7VJLBXZDcjs.appendCanvasQuery; exports.toCanvasQuery = _chunk7VJLBXZDcjs.toCanvasQuery;
16
+ exports.CanvasApiError = _chunkDWITOJXWcjs.CanvasApiError; exports.CanvasClient = _chunkVPWK2XWWcjs.CanvasClient; exports.CanvasHttpClient = _chunkDWITOJXWcjs.CanvasHttpClient; exports.appendCanvasQuery = _chunkDWITOJXWcjs.appendCanvasQuery; exports.toCanvasQuery = _chunkDWITOJXWcjs.toCanvasQuery;
17
17
  //# sourceMappingURL=index.cjs.map
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  CanvasClient
3
- } from "../chunk-6II3HREL.js";
3
+ } from "../chunk-VXP7HVLD.js";
4
4
  import {
5
5
  CanvasApiError,
6
6
  CanvasHttpClient,
7
7
  appendCanvasQuery,
8
8
  toCanvasQuery
9
- } from "../chunk-LKR72KFD.js";
9
+ } from "../chunk-P3XSHNRJ.js";
10
10
  export {
11
11
  CanvasApiError,
12
12
  CanvasClient,
@@ -1,5 +1,11 @@
1
1
  import type { CanvasHttpClient } from './client';
2
2
  import type { CanvasPage } from './types';
3
+ /**
4
+ * Max concurrent page-body fetches issued by {@link PagesModule.listWithBodies}.
5
+ * Matches the `CONCURRENT_COURSE_LIMIT` precedent in `student-search.ts`; kept
6
+ * as a named export so the cap is greppable and adjustable in one place.
7
+ */
8
+ export declare const PAGE_BODY_CONCURRENCY_LIMIT = 10;
3
9
  export declare class PagesModule {
4
10
  private client;
5
11
  constructor(client: CanvasHttpClient);
@@ -8,9 +14,11 @@ export declare class PagesModule {
8
14
  /**
9
15
  * List every page in a course with its full HTML `body`. The paginated list
10
16
  * endpoint returns page stubs without `body`, so this fans out a `get()` per
11
- * page (mirroring the `listWithItems` pattern in `modules.ts`). Used by the
12
- * link-audit tool to scan page content. `CanvasApiError` from either the list
13
- * or any individual fetch propagates unchanged.
17
+ * page, bounded to at most {@link PAGE_BODY_CONCURRENCY_LIMIT} concurrent
18
+ * fetches so a large course does not open one Canvas request per page all at
19
+ * once. Result order matches the listed page order. Used by the link-audit and
20
+ * accessibility-audit tools to scan page content. `CanvasApiError` from either
21
+ * the list or any individual fetch propagates unchanged.
14
22
  */
15
23
  listWithBodies(courseId: number): Promise<CanvasPage[]>;
16
24
  create(courseId: number, params: {
@@ -1 +1 @@
1
- {"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/canvas/pages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEzC,qBAAa,WAAW;IACV,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,gBAAgB;IAEtC,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAI7C,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAMjE;;;;;;OAMG;IACG,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAKvD,MAAM,CACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GACpF,OAAO,CAAC,UAAU,CAAC;IAOhB,MAAM,CACV,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GACrF,OAAO,CAAC,UAAU,CAAC;IAUhB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAM/D"}
1
+ {"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/canvas/pages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAA;AAE7C,qBAAa,WAAW;IACV,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,gBAAgB;IAEtC,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAI7C,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAMjE;;;;;;;;OAQG;IACG,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAOvD,MAAM,CACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GACpF,OAAO,CAAC,UAAU,CAAC;IAOhB,MAAM,CACV,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GACrF,OAAO,CAAC,UAAU,CAAC;IAUhB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAM/D"}
@@ -1,5 +1,5 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// package.json
2
- var version = "1.24.0";
2
+ var version = "1.26.0";
3
3
 
4
4
  // src/canvas/query.ts
5
5
  function appendCanvasQuery(target, params) {
@@ -163,4 +163,4 @@ var CanvasHttpClient = class {
163
163
 
164
164
 
165
165
  exports.version = version; exports.appendCanvasQuery = appendCanvasQuery; exports.toCanvasQuery = toCanvasQuery; exports.CanvasApiError = CanvasApiError; exports.CanvasHttpClient = CanvasHttpClient;
166
- //# sourceMappingURL=chunk-7VJLBXZD.cjs.map
166
+ //# sourceMappingURL=chunk-DWITOJXW.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-DWITOJXW.cjs","../package.json","../src/canvas/query.ts","../src/canvas/client.ts"],"names":[],"mappings":"AAAA;ACEE,IAAA,QAAA,EAAW,QAAA;ADAb;AACA;AEGO,SAAS,iBAAA,CAAkB,MAAA,EAAyB,MAAA,EAAkC;AAC3F,EAAA,GAAA,CAAI,CAAC,MAAA,EAAQ,MAAA;AACb,EAAA,IAAA,CAAA,MAAW,CAAC,GAAA,EAAK,KAAK,EAAA,GAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,EAAG;AACjD,IAAA,GAAA,CAAI,MAAA,IAAU,KAAA,EAAA,GAAa,MAAA,IAAU,IAAA,EAAM,QAAA;AAC3C,IAAA,GAAA,CAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,MAAA,GAAA,CAAI,KAAA,CAAM,OAAA,IAAW,CAAA,EAAG,QAAA;AACxB,MAAA,MAAM,SAAA,EAAW,GAAA,CAAI,QAAA,CAAS,IAAI,EAAA,EAAI,IAAA,EAAM,CAAA,EAAA;AAClB,MAAA;AACiB,QAAA;AACL,QAAA;AACtC,MAAA;AACK,IAAA;AACwB,MAAA;AAC/B,IAAA;AACF,EAAA;AACF;AAE2E;AACtC,EAAA;AACH,EAAA;AACzB,EAAA;AACT;AFFmD;AACA;AGtBT;AACxC,EAAA;AACA,EAAA;AAE+D,EAAA;AAChD,IAAA;AACD,IAAA;AACE,IAAA;AACE,IAAA;AAClB,EAAA;AACF;AAEqC;AACO;AAWd;AACpB,EAAA;AACA,EAAA;AACA,EAAA;AAEgC,EAAA;AAClB,IAAA;AAC2B,IAAA;AACd,IAAA;AACnC,EAAA;AAEsB,EAAA;AACR,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOmF,EAAA;AACtD,IAAA;AACa,IAAA;AAC7B,IAAA;AACiB,MAAA;AACkB,MAAA;AACtB,MAAA;AACxB,IAAA;AAEsC,IAAA;AACQ,IAAA;AAClC,MAAA;AACR,QAAA;AACF,MAAA;AACF,IAAA;AAEwC,IAAA;AACH,MAAA;AACrB,MAAA;AAChB,IAAA;AACe,IAAA;AACa,MAAA;AAC5B,IAAA;AAEkC,IAAA;AAC7B,MAAA;AACM,MAAA;AACJ,QAAA;AACK,QAAA;AACV,MAAA;AACD,IAAA;AAEiB,IAAA;AAC0B,MAAA;AAEX,MAAA;AACY,MAAA;AAC7C,IAAA;AAE6B,IAAA;AACpB,MAAA;AACT,IAAA;AAEqB,IAAA;AACvB,EAAA;AAE8E,EAAA;AAC7B,IAAA;AACR,IAAA;AACC,MAAA;AACxC,IAAA;AAC0C,IAAA;AAEpB,IAAA;AACoB,IAAA;AAC9B,IAAA;AAEmB,IAAA;AACS,MAAA;AAC3B,QAAA;AAC4B,UAAA;AACrB,UAAA;AAChB,QAAA;AACD,MAAA;AAEiB,MAAA;AAC0B,QAAA;AAEtB,QAAA;AACuB,QAAA;AAC7C,MAAA;AAEkC,MAAA;AACd,MAAA;AACpB,MAAA;AAEsC,MAAA;AACxC,IAAA;AAEO,IAAA;AACT,EAAA;AAKE,EAAA;AAE+C,IAAA;AACR,IAAA;AACC,MAAA;AACxC,IAAA;AAC0C,IAAA;AAEpB,IAAA;AACoB,IAAA;AAC9B,IAAA;AAEmB,IAAA;AACS,MAAA;AAC3B,QAAA;AAC4B,UAAA;AACrB,UAAA;AAChB,QAAA;AACD,MAAA;AAEiB,MAAA;AAC0B,QAAA;AAEtB,QAAA;AACuB,QAAA;AAC7C,MAAA;AAEkC,MAAA;AACC,MAAA;AACf,MAAA;AACpB,MAAA;AAEsC,MAAA;AACxC,IAAA;AAEO,IAAA;AACT,EAAA;AAEgE,EAAA;AACtC,IAAA;AACO,IAAA;AACV,IAAA;AACvB,EAAA;AACF;AHnBmD;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-DWITOJXW.cjs","sourcesContent":[null,"{\n \"name\": \"canvas-lms-mcp\",\n \"version\": \"1.26.0\",\n \"mcpName\": \"io.github.bruchris/canvas-lms-mcp\",\n \"description\": \"TypeScript MCP 1.x server for Canvas LMS — 163 tools across Canvas courses, assignments, grades, gradebook history, quizzes, New Quizzes (LTI), outcomes, discussions, admin workflows, and more.\",\n \"type\": \"module\",\n \"types\": \"./dist/server.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/server.d.ts\",\n \"import\": \"./dist/server.js\",\n \"require\": \"./dist/server.cjs\"\n },\n \"./stdio\": {\n \"types\": \"./dist/stdio.d.ts\",\n \"import\": \"./dist/stdio.js\",\n \"require\": \"./dist/stdio.cjs\"\n },\n \"./http\": {\n \"types\": \"./dist/http.d.ts\",\n \"import\": \"./dist/http.js\",\n \"require\": \"./dist/http.cjs\"\n },\n \"./canvas\": {\n \"types\": \"./dist/canvas/index.d.ts\",\n \"import\": \"./dist/canvas/index.js\",\n \"require\": \"./dist/canvas/index.cjs\"\n }\n },\n \"bin\": {\n \"canvas-lms-mcp\": \"bin/canvas-lms-mcp.js\"\n },\n \"files\": [\n \"bin/\",\n \"dist/\"\n ],\n \"scripts\": {\n \"dev\": \"tsup --watch\",\n \"build\": \"tsup && tsc -p tsconfig.build.json\",\n \"generate:manifests\": \"tsx scripts/generate-manifests.ts\",\n \"mcpb:pack\": \"node scripts/pack-mcpb.mjs\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"lint\": \"eslint src/ tests/ && prettier --check src/ tests/\",\n \"lint:fix\": \"eslint src/ tests/ --fix && prettier --write src/ tests/\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"keywords\": [\n \"canvas\",\n \"canvas-lms\",\n \"lms\",\n \"mcp\",\n \"model-context-protocol\",\n \"education\",\n \"grading\",\n \"ai-agent\",\n \"ai-tools\",\n \"claude-desktop\",\n \"cursor\",\n \"vscode\",\n \"instructure\",\n \"education-technology\",\n \"edtech\",\n \"teacher\",\n \"student\",\n \"grading-automation\",\n \"typescript\"\n ],\n \"author\": \"Christian Bru\",\n \"license\": \"MIT\",\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/bruchris/canvas-lms-mcp.git\"\n },\n \"engines\": {\n \"node\": \">=22\"\n },\n \"packageManager\": \"pnpm@10.11.0\",\n \"dependencies\": {\n \"@iarna/toml\": \"^2.2.5\",\n \"@modelcontextprotocol/sdk\": \"^1.30.0\",\n \"prompts\": \"^2.4.2\",\n \"zod\": \"^4.4.3\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^10.0.1\",\n \"@types/node\": \"^25.9.3\",\n \"@types/prompts\": \"^2.4.9\",\n \"@typescript-eslint/eslint-plugin\": \"^8.65.0\",\n \"@typescript-eslint/parser\": \"^8.65.0\",\n \"@vitest/coverage-v8\": \"^4.1.10\",\n \"eslint\": \"^10.7.0\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"eslint-plugin-prettier\": \"^5.5.6\",\n \"prettier\": \"^3.9.6\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.23.1\",\n \"typescript\": \"^6.0.3\",\n \"typescript-eslint\": \"^8.65.0\",\n \"vite\": \"^8.1.5\",\n \"vitest\": \"^4.1.10\"\n }\n}\n","export type CanvasQueryPrimitive = string | number | boolean\n\nexport type CanvasQueryValue = CanvasQueryPrimitive | ReadonlyArray<CanvasQueryPrimitive>\n\nexport type CanvasQueryParams = Record<string, CanvasQueryValue | undefined | null>\n\nexport function appendCanvasQuery(target: URLSearchParams, params?: CanvasQueryParams): void {\n if (!params) return\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) continue\n if (Array.isArray(value)) {\n if (value.length === 0) continue\n const arrayKey = key.endsWith('[]') ? key : `${key}[]`\n for (const item of value) {\n if (item === undefined || item === null) continue\n target.append(arrayKey, String(item))\n }\n } else {\n target.set(key, String(value))\n }\n }\n}\n\nexport function toCanvasQuery(params?: CanvasQueryParams): URLSearchParams {\n const search = new URLSearchParams()\n appendCanvasQuery(search, params)\n return search\n}\n","import { version } from '../../package.json'\nimport { appendCanvasQuery, type CanvasQueryParams } from './query'\nimport type { CanvasClientConfig, CanvasErrorResponse } from './types'\n\nexport class CanvasApiError extends Error {\n status: number\n endpoint: string\n\n constructor(message: string, status: number, endpoint: string) {\n super(message)\n this.name = 'CanvasApiError'\n this.status = status\n this.endpoint = endpoint\n }\n}\n\nconst DEFAULT_MAX_PAGINATION_PAGES = 1000\nconst USER_AGENT = `canvas-lms-mcp/${version}`\n\nexport interface CanvasRequestOptions extends RequestInit {\n /**\n * Query params to append to the endpoint URL. Arrays produce repeated\n * `key[]=v1&key[]=v2` entries (the convention Canvas uses for `include[]`).\n * Preserves any query string already present on the endpoint.\n */\n query?: CanvasQueryParams\n}\n\nexport class CanvasHttpClient {\n private token: string\n private _baseUrl: string\n private maxPaginationPages: number\n\n constructor(config: CanvasClientConfig) {\n this.token = config.token\n this._baseUrl = config.baseUrl.replace(/\\/+$/, '')\n this.maxPaginationPages = config.maxPaginationPages ?? DEFAULT_MAX_PAGINATION_PAGES\n }\n\n get baseUrl(): string {\n return this._baseUrl\n }\n\n /**\n * Makes a single authenticated request to the Canvas API.\n * Returns `undefined` (typed as `T`) when Canvas responds with 204 No Content,\n * which is the expected response for DELETE operations.\n */\n async request<T>(endpoint: string, options: CanvasRequestOptions = {}): Promise<T> {\n const { query, ...init } = options\n let url = endpoint.startsWith('http') ? endpoint : `${this._baseUrl}${endpoint}`\n if (query) {\n const parsed = new URL(url)\n appendCanvasQuery(parsed.searchParams, query)\n url = parsed.toString()\n }\n\n const method = (init.method ?? 'GET').toUpperCase()\n if (init.body != null && (method === 'GET' || method === 'HEAD')) {\n throw new Error(\n `GET requests must not include a body (Canvas CloudFront CDN rejects them with 403): ${endpoint}`,\n )\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\n }\n if (init.body) {\n headers['Content-Type'] = 'application/json'\n }\n\n const response = await fetch(url, {\n ...init,\n headers: {\n ...headers,\n ...init.headers,\n },\n })\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as CanvasErrorResponse\n const message =\n body.errors?.[0]?.message ?? body.message ?? `Canvas API error: ${response.status}`\n throw new CanvasApiError(message, response.status, endpoint)\n }\n\n if (response.status === 204) {\n return undefined as T\n }\n\n return response.json() as Promise<T>\n }\n\n async paginate<T>(endpoint: string, params?: CanvasQueryParams): Promise<T[]> {\n const url = new URL(`${this._baseUrl}${endpoint}`)\n if (!url.searchParams.has('per_page')) {\n url.searchParams.set('per_page', '100')\n }\n appendCanvasQuery(url.searchParams, params)\n\n const results: T[] = []\n let nextUrl: string | null = url.toString()\n let pages = 0\n\n while (nextUrl && pages < this.maxPaginationPages) {\n const response = await fetch(nextUrl, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\n },\n })\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as CanvasErrorResponse\n const message =\n body.errors?.[0]?.message ?? body.message ?? `Canvas API error: ${response.status}`\n throw new CanvasApiError(message, response.status, endpoint)\n }\n\n const data = (await response.json()) as T[]\n results.push(...data)\n pages++\n\n nextUrl = this.parseNextLink(response.headers.get('Link'))\n }\n\n return results\n }\n\n async paginateEnvelope<T>(\n endpoint: string,\n envelopeKey: string,\n params?: CanvasQueryParams,\n ): Promise<T[]> {\n const url = new URL(`${this._baseUrl}${endpoint}`)\n if (!url.searchParams.has('per_page')) {\n url.searchParams.set('per_page', '100')\n }\n appendCanvasQuery(url.searchParams, params)\n\n const results: T[] = []\n let nextUrl: string | null = url.toString()\n let pages = 0\n\n while (nextUrl && pages < this.maxPaginationPages) {\n const response = await fetch(nextUrl, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\n },\n })\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as CanvasErrorResponse\n const message =\n body.errors?.[0]?.message ?? body.message ?? `Canvas API error: ${response.status}`\n throw new CanvasApiError(message, response.status, endpoint)\n }\n\n const body = (await response.json()) as Record<string, T[]>\n const data = body[envelopeKey] ?? []\n results.push(...data)\n pages++\n\n nextUrl = this.parseNextLink(response.headers.get('Link'))\n }\n\n return results\n }\n\n private parseNextLink(linkHeader: string | null): string | null {\n if (!linkHeader) return null\n const match = linkHeader.match(/<([^>]+)>;\\s*rel=\"next\"/)\n return match?.[1] ?? null\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "1.24.0";
2
+ var version = "1.26.0";
3
3
 
4
4
  // src/canvas/query.ts
5
5
  function appendCanvasQuery(target, params) {
@@ -163,4 +163,4 @@ export {
163
163
  CanvasApiError,
164
164
  CanvasHttpClient
165
165
  };
166
- //# sourceMappingURL=chunk-LKR72KFD.js.map
166
+ //# sourceMappingURL=chunk-P3XSHNRJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../package.json","../src/canvas/query.ts","../src/canvas/client.ts"],"sourcesContent":["{\n \"name\": \"canvas-lms-mcp\",\n \"version\": \"1.26.0\",\n \"mcpName\": \"io.github.bruchris/canvas-lms-mcp\",\n \"description\": \"TypeScript MCP 1.x server for Canvas LMS — 163 tools across Canvas courses, assignments, grades, gradebook history, quizzes, New Quizzes (LTI), outcomes, discussions, admin workflows, and more.\",\n \"type\": \"module\",\n \"types\": \"./dist/server.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/server.d.ts\",\n \"import\": \"./dist/server.js\",\n \"require\": \"./dist/server.cjs\"\n },\n \"./stdio\": {\n \"types\": \"./dist/stdio.d.ts\",\n \"import\": \"./dist/stdio.js\",\n \"require\": \"./dist/stdio.cjs\"\n },\n \"./http\": {\n \"types\": \"./dist/http.d.ts\",\n \"import\": \"./dist/http.js\",\n \"require\": \"./dist/http.cjs\"\n },\n \"./canvas\": {\n \"types\": \"./dist/canvas/index.d.ts\",\n \"import\": \"./dist/canvas/index.js\",\n \"require\": \"./dist/canvas/index.cjs\"\n }\n },\n \"bin\": {\n \"canvas-lms-mcp\": \"bin/canvas-lms-mcp.js\"\n },\n \"files\": [\n \"bin/\",\n \"dist/\"\n ],\n \"scripts\": {\n \"dev\": \"tsup --watch\",\n \"build\": \"tsup && tsc -p tsconfig.build.json\",\n \"generate:manifests\": \"tsx scripts/generate-manifests.ts\",\n \"mcpb:pack\": \"node scripts/pack-mcpb.mjs\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"lint\": \"eslint src/ tests/ && prettier --check src/ tests/\",\n \"lint:fix\": \"eslint src/ tests/ --fix && prettier --write src/ tests/\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"keywords\": [\n \"canvas\",\n \"canvas-lms\",\n \"lms\",\n \"mcp\",\n \"model-context-protocol\",\n \"education\",\n \"grading\",\n \"ai-agent\",\n \"ai-tools\",\n \"claude-desktop\",\n \"cursor\",\n \"vscode\",\n \"instructure\",\n \"education-technology\",\n \"edtech\",\n \"teacher\",\n \"student\",\n \"grading-automation\",\n \"typescript\"\n ],\n \"author\": \"Christian Bru\",\n \"license\": \"MIT\",\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/bruchris/canvas-lms-mcp.git\"\n },\n \"engines\": {\n \"node\": \">=22\"\n },\n \"packageManager\": \"pnpm@10.11.0\",\n \"dependencies\": {\n \"@iarna/toml\": \"^2.2.5\",\n \"@modelcontextprotocol/sdk\": \"^1.30.0\",\n \"prompts\": \"^2.4.2\",\n \"zod\": \"^4.4.3\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^10.0.1\",\n \"@types/node\": \"^25.9.3\",\n \"@types/prompts\": \"^2.4.9\",\n \"@typescript-eslint/eslint-plugin\": \"^8.65.0\",\n \"@typescript-eslint/parser\": \"^8.65.0\",\n \"@vitest/coverage-v8\": \"^4.1.10\",\n \"eslint\": \"^10.7.0\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"eslint-plugin-prettier\": \"^5.5.6\",\n \"prettier\": \"^3.9.6\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.23.1\",\n \"typescript\": \"^6.0.3\",\n \"typescript-eslint\": \"^8.65.0\",\n \"vite\": \"^8.1.5\",\n \"vitest\": \"^4.1.10\"\n }\n}\n","export type CanvasQueryPrimitive = string | number | boolean\n\nexport type CanvasQueryValue = CanvasQueryPrimitive | ReadonlyArray<CanvasQueryPrimitive>\n\nexport type CanvasQueryParams = Record<string, CanvasQueryValue | undefined | null>\n\nexport function appendCanvasQuery(target: URLSearchParams, params?: CanvasQueryParams): void {\n if (!params) return\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) continue\n if (Array.isArray(value)) {\n if (value.length === 0) continue\n const arrayKey = key.endsWith('[]') ? key : `${key}[]`\n for (const item of value) {\n if (item === undefined || item === null) continue\n target.append(arrayKey, String(item))\n }\n } else {\n target.set(key, String(value))\n }\n }\n}\n\nexport function toCanvasQuery(params?: CanvasQueryParams): URLSearchParams {\n const search = new URLSearchParams()\n appendCanvasQuery(search, params)\n return search\n}\n","import { version } from '../../package.json'\nimport { appendCanvasQuery, type CanvasQueryParams } from './query'\nimport type { CanvasClientConfig, CanvasErrorResponse } from './types'\n\nexport class CanvasApiError extends Error {\n status: number\n endpoint: string\n\n constructor(message: string, status: number, endpoint: string) {\n super(message)\n this.name = 'CanvasApiError'\n this.status = status\n this.endpoint = endpoint\n }\n}\n\nconst DEFAULT_MAX_PAGINATION_PAGES = 1000\nconst USER_AGENT = `canvas-lms-mcp/${version}`\n\nexport interface CanvasRequestOptions extends RequestInit {\n /**\n * Query params to append to the endpoint URL. Arrays produce repeated\n * `key[]=v1&key[]=v2` entries (the convention Canvas uses for `include[]`).\n * Preserves any query string already present on the endpoint.\n */\n query?: CanvasQueryParams\n}\n\nexport class CanvasHttpClient {\n private token: string\n private _baseUrl: string\n private maxPaginationPages: number\n\n constructor(config: CanvasClientConfig) {\n this.token = config.token\n this._baseUrl = config.baseUrl.replace(/\\/+$/, '')\n this.maxPaginationPages = config.maxPaginationPages ?? DEFAULT_MAX_PAGINATION_PAGES\n }\n\n get baseUrl(): string {\n return this._baseUrl\n }\n\n /**\n * Makes a single authenticated request to the Canvas API.\n * Returns `undefined` (typed as `T`) when Canvas responds with 204 No Content,\n * which is the expected response for DELETE operations.\n */\n async request<T>(endpoint: string, options: CanvasRequestOptions = {}): Promise<T> {\n const { query, ...init } = options\n let url = endpoint.startsWith('http') ? endpoint : `${this._baseUrl}${endpoint}`\n if (query) {\n const parsed = new URL(url)\n appendCanvasQuery(parsed.searchParams, query)\n url = parsed.toString()\n }\n\n const method = (init.method ?? 'GET').toUpperCase()\n if (init.body != null && (method === 'GET' || method === 'HEAD')) {\n throw new Error(\n `GET requests must not include a body (Canvas CloudFront CDN rejects them with 403): ${endpoint}`,\n )\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\n }\n if (init.body) {\n headers['Content-Type'] = 'application/json'\n }\n\n const response = await fetch(url, {\n ...init,\n headers: {\n ...headers,\n ...init.headers,\n },\n })\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as CanvasErrorResponse\n const message =\n body.errors?.[0]?.message ?? body.message ?? `Canvas API error: ${response.status}`\n throw new CanvasApiError(message, response.status, endpoint)\n }\n\n if (response.status === 204) {\n return undefined as T\n }\n\n return response.json() as Promise<T>\n }\n\n async paginate<T>(endpoint: string, params?: CanvasQueryParams): Promise<T[]> {\n const url = new URL(`${this._baseUrl}${endpoint}`)\n if (!url.searchParams.has('per_page')) {\n url.searchParams.set('per_page', '100')\n }\n appendCanvasQuery(url.searchParams, params)\n\n const results: T[] = []\n let nextUrl: string | null = url.toString()\n let pages = 0\n\n while (nextUrl && pages < this.maxPaginationPages) {\n const response = await fetch(nextUrl, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\n },\n })\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as CanvasErrorResponse\n const message =\n body.errors?.[0]?.message ?? body.message ?? `Canvas API error: ${response.status}`\n throw new CanvasApiError(message, response.status, endpoint)\n }\n\n const data = (await response.json()) as T[]\n results.push(...data)\n pages++\n\n nextUrl = this.parseNextLink(response.headers.get('Link'))\n }\n\n return results\n }\n\n async paginateEnvelope<T>(\n endpoint: string,\n envelopeKey: string,\n params?: CanvasQueryParams,\n ): Promise<T[]> {\n const url = new URL(`${this._baseUrl}${endpoint}`)\n if (!url.searchParams.has('per_page')) {\n url.searchParams.set('per_page', '100')\n }\n appendCanvasQuery(url.searchParams, params)\n\n const results: T[] = []\n let nextUrl: string | null = url.toString()\n let pages = 0\n\n while (nextUrl && pages < this.maxPaginationPages) {\n const response = await fetch(nextUrl, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\n },\n })\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as CanvasErrorResponse\n const message =\n body.errors?.[0]?.message ?? body.message ?? `Canvas API error: ${response.status}`\n throw new CanvasApiError(message, response.status, endpoint)\n }\n\n const body = (await response.json()) as Record<string, T[]>\n const data = body[envelopeKey] ?? []\n results.push(...data)\n pages++\n\n nextUrl = this.parseNextLink(response.headers.get('Link'))\n }\n\n return results\n }\n\n private parseNextLink(linkHeader: string | null): string | null {\n if (!linkHeader) return null\n const match = linkHeader.match(/<([^>]+)>;\\s*rel=\"next\"/)\n return match?.[1] ?? null\n }\n}\n"],"mappings":"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","names":["body"]}
@@ -1,7 +1,7 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
3
 
4
- var _chunk7VJLBXZDcjs = require('./chunk-7VJLBXZD.cjs');
4
+ var _chunkDWITOJXWcjs = require('./chunk-DWITOJXW.cjs');
5
5
 
6
6
  // src/canvas/courses.ts
7
7
  var DEFAULT_LIST_INCLUDE = ["term"];
@@ -779,7 +779,24 @@ var ModulesModule = class {
779
779
  }
780
780
  };
781
781
 
782
+ // src/canvas/concurrency.ts
783
+ async function mapWithConcurrency(items, limit, fn) {
784
+ if (limit < 1) {
785
+ throw new RangeError(`mapWithConcurrency: limit must be >= 1, got ${limit}`);
786
+ }
787
+ const results = new Array(items.length);
788
+ for (let start = 0; start < items.length; start += limit) {
789
+ const batch = items.slice(start, start + limit);
790
+ const settled = await Promise.all(batch.map((item, offset) => fn(item, start + offset)));
791
+ settled.forEach((result, offset) => {
792
+ results[start + offset] = result;
793
+ });
794
+ }
795
+ return results;
796
+ }
797
+
782
798
  // src/canvas/pages.ts
799
+ var PAGE_BODY_CONCURRENCY_LIMIT = 10;
783
800
  var PagesModule = class {
784
801
  constructor(client) {
785
802
  this.client = client;
@@ -796,13 +813,19 @@ var PagesModule = class {
796
813
  /**
797
814
  * List every page in a course with its full HTML `body`. The paginated list
798
815
  * endpoint returns page stubs without `body`, so this fans out a `get()` per
799
- * page (mirroring the `listWithItems` pattern in `modules.ts`). Used by the
800
- * link-audit tool to scan page content. `CanvasApiError` from either the list
801
- * or any individual fetch propagates unchanged.
816
+ * page, bounded to at most {@link PAGE_BODY_CONCURRENCY_LIMIT} concurrent
817
+ * fetches so a large course does not open one Canvas request per page all at
818
+ * once. Result order matches the listed page order. Used by the link-audit and
819
+ * accessibility-audit tools to scan page content. `CanvasApiError` from either
820
+ * the list or any individual fetch propagates unchanged.
802
821
  */
803
822
  async listWithBodies(courseId) {
804
823
  const stubs = await this.client.paginate(`/api/v1/courses/${courseId}/pages`);
805
- return Promise.all(stubs.map((stub) => this.get(courseId, stub.url)));
824
+ return mapWithConcurrency(
825
+ stubs,
826
+ PAGE_BODY_CONCURRENCY_LIMIT,
827
+ (stub) => this.get(courseId, stub.url)
828
+ );
806
829
  }
807
830
  async create(courseId, params) {
808
831
  return this.client.request(`/api/v1/courses/${courseId}/pages`, {
@@ -1330,7 +1353,7 @@ var NewQuizzesModule = class {
1330
1353
  `/api/quiz/v1/courses/${courseId}/accommodations/${userId}`
1331
1354
  );
1332
1355
  } catch (err) {
1333
- if (err instanceof _chunk7VJLBXZDcjs.CanvasApiError && err.status === 404) return null;
1356
+ if (err instanceof _chunkDWITOJXWcjs.CanvasApiError && err.status === 404) return null;
1334
1357
  throw err;
1335
1358
  }
1336
1359
  }
@@ -1673,7 +1696,7 @@ var CanvasClient = class {
1673
1696
 
1674
1697
 
1675
1698
  constructor(config) {
1676
- this.client = new (0, _chunk7VJLBXZDcjs.CanvasHttpClient)(config);
1699
+ this.client = new (0, _chunkDWITOJXWcjs.CanvasHttpClient)(config);
1677
1700
  this.courses = new CoursesModule(this.client);
1678
1701
  this.assignments = new AssignmentsModule(this.client);
1679
1702
  this.submissions = new SubmissionsModule(this.client);
@@ -1712,5 +1735,6 @@ var CanvasClient = class {
1712
1735
 
1713
1736
 
1714
1737
 
1715
- exports.SEARCH_CONTENT_TYPES = SEARCH_CONTENT_TYPES; exports.OUTCOME_CONTEXT_TYPES = OUTCOME_CONTEXT_TYPES; exports.OUTCOME_DETAIL_LEVELS = OUTCOME_DETAIL_LEVELS; exports.OUTCOME_ROLLUP_AGGREGATE_STATS = OUTCOME_ROLLUP_AGGREGATE_STATS; exports.OUTCOME_ROLLUP_SORT_BY = OUTCOME_ROLLUP_SORT_BY; exports.OUTCOME_SORT_ORDER = OUTCOME_SORT_ORDER; exports.OUTCOME_EXCLUDE_OPTIONS = OUTCOME_EXCLUDE_OPTIONS; exports.CanvasClient = CanvasClient;
1716
- //# sourceMappingURL=chunk-4GMSSKK6.cjs.map
1738
+
1739
+ exports.mapWithConcurrency = mapWithConcurrency; exports.SEARCH_CONTENT_TYPES = SEARCH_CONTENT_TYPES; exports.OUTCOME_CONTEXT_TYPES = OUTCOME_CONTEXT_TYPES; exports.OUTCOME_DETAIL_LEVELS = OUTCOME_DETAIL_LEVELS; exports.OUTCOME_ROLLUP_AGGREGATE_STATS = OUTCOME_ROLLUP_AGGREGATE_STATS; exports.OUTCOME_ROLLUP_SORT_BY = OUTCOME_ROLLUP_SORT_BY; exports.OUTCOME_SORT_ORDER = OUTCOME_SORT_ORDER; exports.OUTCOME_EXCLUDE_OPTIONS = OUTCOME_EXCLUDE_OPTIONS; exports.CanvasClient = CanvasClient;
1740
+ //# sourceMappingURL=chunk-VPWK2XWW.cjs.map