canvas-lms-mcp 1.29.0 → 1.29.2
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/README.md +63 -4
- package/dist/canvas/index.cjs +3 -3
- package/dist/canvas/index.js +2 -2
- package/dist/{chunk-Y7342FIO.cjs → chunk-2ENRMOJW.cjs} +2 -2
- package/dist/chunk-2ENRMOJW.cjs.map +1 -0
- package/dist/{chunk-36BW4FIO.js → chunk-3GRH22UA.js} +2 -2
- package/dist/{chunk-ZTZ5OH2B.cjs → chunk-DWCGX2LJ.cjs} +4 -4
- package/dist/{chunk-ZTZ5OH2B.cjs.map → chunk-DWCGX2LJ.cjs.map} +1 -1
- package/dist/{chunk-55WIXZWE.js → chunk-FHB5523D.js} +2 -2
- package/dist/chunk-FHB5523D.js.map +1 -0
- package/dist/{chunk-EYELCMRH.js → chunk-TNJAWWDM.js} +94 -4
- package/dist/{chunk-6VAQBQ2V.cjs.map → chunk-TNJAWWDM.js.map} +1 -1
- package/dist/{chunk-6VAQBQ2V.cjs → chunk-YI3DY6GQ.cjs} +133 -43
- package/dist/chunk-YI3DY6GQ.cjs.map +1 -0
- package/dist/http.cjs +15 -8
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +13 -6
- package/dist/http.js.map +1 -1
- package/dist/init.cjs +3 -3
- package/dist/init.js +1 -1
- package/dist/pseudonym/pseudonymizer.d.ts +75 -1
- package/dist/pseudonym/pseudonymizer.d.ts.map +1 -1
- package/dist/schema-dialect.d.ts +46 -0
- package/dist/schema-dialect.d.ts.map +1 -0
- package/dist/server.cjs +6 -4
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +26 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +7 -5
- package/dist/stdio.cjs +9 -5
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.js +8 -4
- package/dist/stdio.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/pseudonym.d.ts +13 -4
- package/dist/tools/pseudonym.d.ts.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-55WIXZWE.js.map +0 -1
- package/dist/chunk-EYELCMRH.js.map +0 -1
- package/dist/chunk-Y7342FIO.cjs.map +0 -1
- /package/dist/{chunk-36BW4FIO.js.map → chunk-3GRH22UA.js.map} +0 -0
package/README.md
CHANGED
|
@@ -148,9 +148,9 @@ Once configured, try these prompts with your AI client:
|
|
|
148
148
|
| Student Search | `find_student_across_courses` |
|
|
149
149
|
| Dashboard | `get_dashboard_cards`, `get_todo_items`, `get_upcoming_events`, `get_missing_submissions` |
|
|
150
150
|
| Attention | `list_submission_comments_needing_attention`, `list_students_needing_attention` |
|
|
151
|
-
| FERPA (conditional) | `resolve_pseudonym` —
|
|
151
|
+
| FERPA (conditional) | `resolve_pseudonym` — stdio only, registered when `CANVAS_PSEUDONYMIZE_STUDENTS=true` and `CANVAS_PSEUDONYMIZE_REVERSE_LOOKUP=true` |
|
|
152
152
|
|
|
153
|
-
117 tools are read-only and 48 tools perform Canvas write operations. When FERPA mode is enabled
|
|
153
|
+
117 tools are read-only and 48 tools perform Canvas write operations. When FERPA mode is enabled **on the stdio transport**, `resolve_pseudonym` is registered as the 166th tool overall (118th read tool). The HTTP transport never registers it — see [FERPA mode](#ferpa-mode-student-pseudonymization).
|
|
154
154
|
|
|
155
155
|
All write tools require appropriate Canvas permissions. Canvas enforces its own permission model -- the MCP server does not bypass it.
|
|
156
156
|
|
|
@@ -192,6 +192,14 @@ A tool returning a list wraps it under a single plural key, since MCP requires a
|
|
|
192
192
|
|
|
193
193
|
Which tools are migrated is recorded per tool as `structuredOutput` in [`docs/generated/tool-manifest.json`](docs/generated/tool-manifest.json) (manifest schema 1.1). Currently: the five `pages` tools.
|
|
194
194
|
|
|
195
|
+
#### JSON Schema dialect
|
|
196
|
+
|
|
197
|
+
Every advertised `inputSchema` and `outputSchema` declares **JSON Schema 2020-12** (`"$schema": "https://json-schema.org/draft/2020-12/schema"`).
|
|
198
|
+
|
|
199
|
+
`@modelcontextprotocol/sdk` v1 converts Zod with a fixed draft-07 target and `registerTool` accepts no override, so the server re-declares the dialect on the `tools/list` response. That is a declaration change only: CI asks the SDK's own converter for both dialects and requires the emitted bodies to be byte-identical for every registered schema, with a tuple schema as the control for a case where the two genuinely differ. A 2020-12-only validator (Ajv's `2020` entry point, the same family Claude Desktop uses) compiles all 168 schemas in the guard suite.
|
|
200
|
+
|
|
201
|
+
Clients that support 2020-12 only rejected the five tools advertising an `outputSchema` before this — see [#341](https://github.com/bruchris/canvas-lms-mcp/issues/341). The rewrite is installed during tool registration rather than in a transport, so stdio, HTTP and the library factory are all covered.
|
|
202
|
+
|
|
195
203
|
### Interactive widgets
|
|
196
204
|
|
|
197
205
|
`view_course_structure` is an [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) tool: hosts that support the spec render an interactive tree explorer (collapsible modules, type-filter chips, title search, published/unpublished badges, links open in a new tab); hosts that don't fall back transparently to the same JSON payload that `get_course_structure` returns. The widget is self-contained — no external scripts, fonts, or network calls — and is shipped inline with the tool definition.
|
|
@@ -295,7 +303,7 @@ const courses = await canvas.courses.list()
|
|
|
295
303
|
| `CANVAS_ROLE` | No | Filter the tool list by role: `student`, `teacher`, or `admin` (see [Role-based tool filtering](#role-based-tool-filtering)) |
|
|
296
304
|
| `CANVAS_ENABLE_ASSIGNMENT_SUBMISSION` | No | Set to `true` to register the opt-in [assignment submission tools](#student-assignment-submission-opt-in) |
|
|
297
305
|
| `CANVAS_PSEUDONYMIZE_STUDENTS` | No | Set to `true` to enable [FERPA mode](#ferpa-mode-student-pseudonymization) |
|
|
298
|
-
| `CANVAS_PSEUDONYMIZE_REVERSE_LOOKUP` | No | Set to `true` (with `CANVAS_PSEUDONYMIZE_STUDENTS=true`) to register the `resolve_pseudonym` audit tool |
|
|
306
|
+
| `CANVAS_PSEUDONYMIZE_REVERSE_LOOKUP` | No | **stdio only.** Set to `true` (with `CANVAS_PSEUDONYMIZE_STUDENTS=true`) to register the `resolve_pseudonym` audit tool. Ignored on the HTTP transport, with a warning |
|
|
299
307
|
| `CANVAS_PSEUDONYM_DIR` | No | Absolute path that overrides the default pseudonym map directory |
|
|
300
308
|
| `CANVAS_PSEUDONYM_AUDIT_LOG` | No | Path to an append-only file that mirrors `resolve_pseudonym` audit lines (stderr is always written) |
|
|
301
309
|
| `CANVAS_PROVENANCE_FENCING` | No | **On by default.** Set to exactly `false` to disable [provenance fencing](#provenance-fencing-untrusted-canvas-content) |
|
|
@@ -429,6 +437,57 @@ Conversation participants are pseudonymized as `Person N` from a cross-course po
|
|
|
429
437
|
|
|
430
438
|
Optional `resolve_pseudonym` reverse-lookup tool: register it only by also setting `CANVAS_PSEUDONYMIZE_REVERSE_LOOKUP=true`. Every call is audit-logged to stderr (and to `CANVAS_PSEUDONYM_AUDIT_LOG` if set). When the flag is off the tool is absent from `tools/list` — a prompt-injection attempt to call it fails at the protocol layer.
|
|
431
439
|
|
|
440
|
+
**Reverse lookup requires a single-caller deployment.** A server process that serves callers with **different** Canvas credentials shares one pseudonym map across all of them, and `resolve_pseudonym` makes no Canvas call — so on such a deployment it would let one caller resolve a student another caller's token had seeded. What that means depends on how the server is run:
|
|
441
|
+
|
|
442
|
+
| Deployment | Reverse lookup | How it is decided |
|
|
443
|
+
| --- | --- | --- |
|
|
444
|
+
| **Built-in HTTP** (`canvas-lms-mcp serve`) | Never registered | Shared by construction. The flag is ignored and setting it prints a startup warning. |
|
|
445
|
+
| **stdio** (`canvas-lms-mcp`) | Available | One process, one user, one token. |
|
|
446
|
+
| **Your own transport** (`createCanvasMCPServer`) | You declare it | See [Embedding a custom transport](#embedding-a-custom-transport). The factory refuses to start rather than guess. |
|
|
447
|
+
|
|
448
|
+
Pseudonymization itself is unaffected in all three.
|
|
449
|
+
|
|
450
|
+
Safe configuration for a shared/hosted deployment:
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
# HTTP: pseudonymize, never reverse-resolve
|
|
454
|
+
CANVAS_PSEUDONYMIZE_STUDENTS=true canvas-lms-mcp serve --base-url https://school.instructure.com
|
|
455
|
+
|
|
456
|
+
# stdio (single user, own token, own machine): reverse lookup is available
|
|
457
|
+
CANVAS_PSEUDONYMIZE_STUDENTS=true CANVAS_PSEUDONYMIZE_REVERSE_LOOKUP=true \
|
|
458
|
+
canvas-lms-mcp --base-url https://school.instructure.com
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
`X-Canvas-Role` does not change this: the role is a client-supplied UX filter, so it can never authorize a reverse lookup.
|
|
462
|
+
|
|
463
|
+
#### Embedding a custom transport
|
|
464
|
+
|
|
465
|
+
The built-in transports declare their own shape. If you connect `createCanvasMCPServer` to a transport of your own, that declaration is yours to make — it is a fact about your deployment, and the server must never infer it from a request header, a role, or any other caller-supplied value.
|
|
466
|
+
|
|
467
|
+
If one process serves callers with different Canvas credentials, build the pseudonymizer with `createSharedPseudonymizer` and give it to every server. It is shared by construction, so `resolve_pseudonym` is not registered and a direct `reverseLookup()` refuses before it reads the map:
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
import { createCanvasMCPServer, createSharedPseudonymizer } from 'canvas-lms-mcp'
|
|
471
|
+
|
|
472
|
+
// Once, at startup: one map, shared, reverse lookup permanently off.
|
|
473
|
+
const pseudonymizer = createSharedPseudonymizer({ baseUrl: process.env.CANVAS_BASE_URL! })
|
|
474
|
+
|
|
475
|
+
// Per request, with that caller's own token.
|
|
476
|
+
const { server } = createCanvasMCPServer({
|
|
477
|
+
token: callerToken,
|
|
478
|
+
baseUrl: process.env.CANVAS_BASE_URL!,
|
|
479
|
+
pseudonymizer,
|
|
480
|
+
})
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
If one process serves exactly one caller identity — a desktop client, a per-user sidecar — say so, and reverse lookup behaves as it does on stdio:
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
const { server } = createCanvasMCPServer({ token, baseUrl, sharedAcrossCallers: false })
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Say nothing and the deployment shape is undeclared. Nothing changes unless `CANVAS_PSEUDONYMIZE_REVERSE_LOOKUP` is on, in which case `createCanvasMCPServer` throws rather than pick an answer for you.
|
|
490
|
+
|
|
432
491
|
Threat model and design rationale in [docs/superpowers/specs/2026-05-25-ferpa-pseudonymization.md](docs/superpowers/specs/2026-05-25-ferpa-pseudonymization.md).
|
|
433
492
|
|
|
434
493
|
## Role-based tool filtering
|
|
@@ -455,7 +514,7 @@ Notes:
|
|
|
455
514
|
- **Equivalent to `CANVAS_ROLE` in [vishalsachdev/canvas-mcp](https://github.com/vishalsachdev/canvas-mcp)** — set the same value to migrate.
|
|
456
515
|
- Role values are **case-insensitive**; `all` is accepted as an explicit "no filter". An unrecognised value logs a warning to stderr and registers all tools (a config typo never stops the server).
|
|
457
516
|
- `teacher` / `admin` do **not** see the student-only `get_my_*` tools in v1 — they should use `list_submissions` / `get_submission` etc. instead.
|
|
458
|
-
- The FERPA `resolve_pseudonym` tool is `teacher`/`admin`-only and is never exposed to `student`, even when reverse lookup is enabled.
|
|
517
|
+
- The FERPA `resolve_pseudonym` tool is `teacher`/`admin`-only and is never exposed to `student`, even when reverse lookup is enabled. It is also never exposed on the built-in HTTP transport — or on any custom transport that declares itself shared across callers — for **any** role, because the role header is client-supplied and cannot be an authorization boundary.
|
|
459
518
|
- **HTTP transport:** the role is read per request from the `X-Canvas-Role` header, falling back to `CANVAS_ROLE` from the server config. A valid header (or `all`) overrides the configured default; an invalid header is ignored with a warning.
|
|
460
519
|
- Tool counts above are a snapshot and grow as tools are added — the authoritative guarantee is that every tool resolves to exactly one audience (enforced by `tests/tools/audience-coverage.test.ts`).
|
|
461
520
|
|
package/dist/canvas/index.cjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunkDWCGX2LJcjs = require('../chunk-DWCGX2LJ.cjs');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
var
|
|
9
|
+
var _chunk2ENRMOJWcjs = require('../chunk-2ENRMOJW.cjs');
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
exports.CanvasApiError =
|
|
16
|
+
exports.CanvasApiError = _chunk2ENRMOJWcjs.CanvasApiError; exports.CanvasClient = _chunkDWCGX2LJcjs.CanvasClient; exports.CanvasHttpClient = _chunk2ENRMOJWcjs.CanvasHttpClient; exports.appendCanvasQuery = _chunk2ENRMOJWcjs.appendCanvasQuery; exports.toCanvasQuery = _chunk2ENRMOJWcjs.toCanvasQuery;
|
|
17
17
|
//# sourceMappingURL=index.cjs.map
|
package/dist/canvas/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CanvasClient
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-3GRH22UA.js";
|
|
4
4
|
import {
|
|
5
5
|
CanvasApiError,
|
|
6
6
|
CanvasHttpClient,
|
|
7
7
|
appendCanvasQuery,
|
|
8
8
|
toCanvasQuery
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-FHB5523D.js";
|
|
10
10
|
export {
|
|
11
11
|
CanvasApiError,
|
|
12
12
|
CanvasClient,
|
|
@@ -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.29.
|
|
2
|
+
var version = "1.29.2";
|
|
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-
|
|
166
|
+
//# sourceMappingURL=chunk-2ENRMOJW.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-2ENRMOJW.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-2ENRMOJW.cjs","sourcesContent":[null,"{\n \"name\": \"canvas-lms-mcp\",\n \"version\": \"1.29.2\",\n \"mcpName\": \"io.github.bruchris/canvas-lms-mcp\",\n \"description\": \"TypeScript MCP 1.x server for Canvas LMS — 165 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/jsdom\": \"^30.0.0\",\n \"@types/node\": \"^26.4.0\",\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 \"ajv\": \"^8.20.0\",\n \"eslint\": \"^10.7.0\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"eslint-plugin-prettier\": \"^5.5.6\",\n \"jsdom\": \"^30.0.1\",\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,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CanvasApiError,
|
|
3
3
|
CanvasHttpClient
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FHB5523D.js";
|
|
5
5
|
|
|
6
6
|
// src/canvas/courses.ts
|
|
7
7
|
var DEFAULT_LIST_INCLUDE = ["term"];
|
|
@@ -1771,4 +1771,4 @@ export {
|
|
|
1771
1771
|
OUTCOME_EXCLUDE_OPTIONS,
|
|
1772
1772
|
CanvasClient
|
|
1773
1773
|
};
|
|
1774
|
-
//# sourceMappingURL=chunk-
|
|
1774
|
+
//# sourceMappingURL=chunk-3GRH22UA.js.map
|
|
@@ -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
|
|
4
|
+
var _chunk2ENRMOJWcjs = require('./chunk-2ENRMOJW.cjs');
|
|
5
5
|
|
|
6
6
|
// src/canvas/courses.ts
|
|
7
7
|
var DEFAULT_LIST_INCLUDE = ["term"];
|
|
@@ -1387,7 +1387,7 @@ var NewQuizzesModule = class {
|
|
|
1387
1387
|
`/api/quiz/v1/courses/${courseId}/accommodations/${userId}`
|
|
1388
1388
|
);
|
|
1389
1389
|
} catch (err) {
|
|
1390
|
-
if (err instanceof
|
|
1390
|
+
if (err instanceof _chunk2ENRMOJWcjs.CanvasApiError && err.status === 404) return null;
|
|
1391
1391
|
throw err;
|
|
1392
1392
|
}
|
|
1393
1393
|
}
|
|
@@ -1730,7 +1730,7 @@ var CanvasClient = class {
|
|
|
1730
1730
|
|
|
1731
1731
|
|
|
1732
1732
|
constructor(config) {
|
|
1733
|
-
this.client = new (0,
|
|
1733
|
+
this.client = new (0, _chunk2ENRMOJWcjs.CanvasHttpClient)(config);
|
|
1734
1734
|
this.courses = new CoursesModule(this.client);
|
|
1735
1735
|
this.assignments = new AssignmentsModule(this.client);
|
|
1736
1736
|
this.submissions = new SubmissionsModule(this.client);
|
|
@@ -1771,4 +1771,4 @@ var CanvasClient = class {
|
|
|
1771
1771
|
|
|
1772
1772
|
|
|
1773
1773
|
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;
|
|
1774
|
-
//# sourceMappingURL=chunk-
|
|
1774
|
+
//# sourceMappingURL=chunk-DWCGX2LJ.cjs.map
|