canvas-parent-mcp 1.1.8 → 1.2.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Canvas LMS (Instructure) — courses, grades, assignments, announcements, planner, conversations, files",
10
- "version": "1.1.8"
10
+ "version": "1.2.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Canvas LMS",
16
16
  "source": "./",
17
17
  "description": "Canvas LMS (Instructure) MCP server for Claude — student/observer access via natural language",
18
- "version": "1.1.8",
18
+ "version": "1.2.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canvas-parent-mcp",
3
3
  "displayName": "Canvas LMS",
4
- "version": "1.1.8",
4
+ "version": "1.2.0",
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",
@@ -20,6 +20,6 @@
20
20
  "education",
21
21
  "mcp"
22
22
  ],
23
- "skills": "./SKILL.md",
23
+ "skills": "./skills/",
24
24
  "mcp": "./.mcp.json"
25
25
  }
package/dist/bundle.js CHANGED
@@ -34989,6 +34989,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
34989
34989
  "capture_request_header",
34990
34990
  "capture_redirect",
34991
34991
  "read_indexed_db",
34992
+ "read_dom",
34992
34993
  "download"
34993
34994
  ]);
34994
34995
 
@@ -35290,6 +35291,44 @@ function assertIndexedDbScopesArray(value, label) {
35290
35291
  }
35291
35292
  }
35292
35293
  }
35294
+ var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
35295
+ var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
35296
+ function assertDomSelectorsArray(value, label) {
35297
+ if (!Array.isArray(value)) {
35298
+ throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
35299
+ }
35300
+ const seen = /* @__PURE__ */ new Set();
35301
+ for (let i = 0; i < value.length; i++) {
35302
+ const entry = value[i];
35303
+ assertObject(entry, `${label}[${i}]`);
35304
+ if (entry.name === void 0) {
35305
+ throw new ProtocolError(`${label}[${i}].name: missing`);
35306
+ }
35307
+ if (entry.selector === void 0) {
35308
+ throw new ProtocolError(`${label}[${i}].selector: missing`);
35309
+ }
35310
+ if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
35311
+ throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
35312
+ }
35313
+ if (typeof entry.selector !== "string" || !DOM_SELECTOR_RE.test(entry.selector)) {
35314
+ throw new ProtocolError(`${label}[${i}].selector: invalid ${JSON.stringify(entry.selector)}`);
35315
+ }
35316
+ if (entry.attribute !== void 0) {
35317
+ if (typeof entry.attribute !== "string" || !DOM_ATTRIBUTE_RE.test(entry.attribute)) {
35318
+ throw new ProtocolError(`${label}[${i}].attribute: invalid ${JSON.stringify(entry.attribute)}`);
35319
+ }
35320
+ }
35321
+ if (seen.has(entry.name)) {
35322
+ throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
35323
+ }
35324
+ seen.add(entry.name);
35325
+ for (const k of Object.keys(entry)) {
35326
+ if (k !== "name" && k !== "selector" && k !== "attribute") {
35327
+ throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
35328
+ }
35329
+ }
35330
+ }
35331
+ }
35293
35332
  function validateFrame(raw) {
35294
35333
  assertObject(raw, "frame");
35295
35334
  const t = raw.type;
@@ -35365,6 +35404,9 @@ function validateHello(raw) {
35365
35404
  if (raw.sessionStoragePointers !== void 0) {
35366
35405
  assertStoragePointersArray(raw.sessionStoragePointers, "hello.sessionStoragePointers", raw.sessionStorageKeys);
35367
35406
  }
35407
+ if (raw.domSelectors !== void 0) {
35408
+ assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
35409
+ }
35368
35410
  assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
35369
35411
  assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
35370
35412
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
@@ -35599,6 +35641,21 @@ function validateInnerRequest(raw) {
35599
35641
  }
35600
35642
  return raw;
35601
35643
  }
35644
+ if (raw.op === "read_dom") {
35645
+ assertObject(raw.init, "inner.init");
35646
+ if (raw.init.origin === void 0)
35647
+ throw new ProtocolError("inner.init.origin: missing");
35648
+ if (raw.init.names === void 0)
35649
+ throw new ProtocolError("inner.init.names: missing");
35650
+ assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
35651
+ assertNonEmptyKeyArray(raw.init.names, "inner.init.names");
35652
+ for (const k of Object.keys(raw.init)) {
35653
+ if (k !== "origin" && k !== "names") {
35654
+ throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_dom`);
35655
+ }
35656
+ }
35657
+ return raw;
35658
+ }
35602
35659
  if (raw.op === "download") {
35603
35660
  assertObject(raw.init, "inner.init");
35604
35661
  if (raw.init.url === void 0) {
@@ -35624,7 +35681,7 @@ function validateInnerRequest(raw) {
35624
35681
  }
35625
35682
  return raw;
35626
35683
  }
35627
- throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "download"; got ${JSON.stringify(raw.op)}`);
35684
+ throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download"; got ${JSON.stringify(raw.op)}`);
35628
35685
  }
35629
35686
  function assertNonEmptyKeyArray(value, label) {
35630
35687
  if (!Array.isArray(value)) {
@@ -35709,6 +35766,13 @@ function validateInnerResponse(raw) {
35709
35766
  assertObject(raw.values, "inner.values");
35710
35767
  return raw;
35711
35768
  }
35769
+ if (op === "read_dom") {
35770
+ if (raw.values === void 0) {
35771
+ throw new ProtocolError("inner.values: missing on read_dom response");
35772
+ }
35773
+ assertStringMap(raw.values, "inner.values");
35774
+ return raw;
35775
+ }
35712
35776
  if (op === "download") {
35713
35777
  assertObject(raw.value, "inner.value");
35714
35778
  assertString(raw.value.path, "inner.value.path");
@@ -36048,6 +36112,13 @@ async function buildServerHello(opts) {
36048
36112
  jsonPointer: d.jsonPointer
36049
36113
  }));
36050
36114
  }
36115
+ if (opts.domSelectors && opts.domSelectors.length > 0) {
36116
+ hello.domSelectors = opts.domSelectors.map((d) => ({
36117
+ name: d.name,
36118
+ selector: d.selector,
36119
+ ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
36120
+ }));
36121
+ }
36051
36122
  return hello;
36052
36123
  }
36053
36124
 
@@ -36137,7 +36208,8 @@ async function startHost(opts) {
36137
36208
  captureHeaders: opts.ownCaptureHeaders,
36138
36209
  indexedDbScopes: opts.ownIndexedDbScopes,
36139
36210
  localStoragePointers: opts.ownLocalStoragePointers,
36140
- sessionStoragePointers: opts.ownSessionStoragePointers
36211
+ sessionStoragePointers: opts.ownSessionStoragePointers,
36212
+ domSelectors: opts.ownDomSelectors
36141
36213
  });
36142
36214
  const ownSessionNonce = fromB64(ownHello.sessionNonce);
36143
36215
  let extensionWs = null;
@@ -36372,6 +36444,7 @@ async function startPeer(opts) {
36372
36444
  sessionStorageKeys: opts.sessionStorageKeys,
36373
36445
  captureHeaders: opts.captureHeaders,
36374
36446
  indexedDbScopes: opts.indexedDbScopes,
36447
+ domSelectors: opts.domSelectors,
36375
36448
  localStoragePointers: opts.localStoragePointers,
36376
36449
  sessionStoragePointers: opts.sessionStoragePointers
36377
36450
  });
@@ -36779,6 +36852,11 @@ var FetchproxyServer = class {
36779
36852
  key: d.key,
36780
36853
  jsonPointer: d.jsonPointer
36781
36854
  })),
36855
+ domSelectors: (opts.domSelectors ?? []).map((d) => ({
36856
+ name: d.name,
36857
+ selector: d.selector,
36858
+ ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
36859
+ })),
36782
36860
  // 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
36783
36861
  // adapter was about to set these to the same numbers anyway; the
36784
36862
  // back-door is `0` (explicit opt-out) if a caller genuinely wants
@@ -36899,6 +36977,7 @@ var FetchproxyServer = class {
36899
36977
  ownIndexedDbScopes: this.opts.indexedDbScopes,
36900
36978
  ownLocalStoragePointers: this.opts.localStoragePointers,
36901
36979
  ownSessionStoragePointers: this.opts.sessionStoragePointers,
36980
+ ownDomSelectors: this.opts.domSelectors,
36902
36981
  onPairCode: this.opts.onPairCode
36903
36982
  });
36904
36983
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
@@ -36926,7 +37005,8 @@ var FetchproxyServer = class {
36926
37005
  captureHeaders: this.opts.captureHeaders,
36927
37006
  indexedDbScopes: this.opts.indexedDbScopes,
36928
37007
  localStoragePointers: this.opts.localStoragePointers,
36929
- sessionStoragePointers: this.opts.sessionStoragePointers
37008
+ sessionStoragePointers: this.opts.sessionStoragePointers,
37009
+ domSelectors: this.opts.domSelectors
36930
37010
  });
36931
37011
  this.peerHandle.onInner((inner) => this.onInner(inner));
36932
37012
  this.peerHandle.onRenegotiate(() => {
@@ -37901,6 +37981,46 @@ var FetchproxyServer = class {
37901
37981
  await this.sendInnerFrame(inner);
37902
37982
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
37903
37983
  }
37984
+ /**
37985
+ * 1.4.0+: read declared DOM values from the user's signed-in tab.
37986
+ * Requires `'read_dom'` in capabilities AND every requested `name` to
37987
+ * match a declared `domSelectors` entry. The extension reads each
37988
+ * declared selector from the matched tab's DOM (isolated-world
37989
+ * `querySelector`, value or attribute) — no page-JS execution.
37990
+ *
37991
+ * Returns a `Record<string, string>` of `name → value`, with names
37992
+ * whose element (or attribute) was absent omitted. Throws
37993
+ * `FetchproxyProtocolError` on bridge failures and a plain `Error` on
37994
+ * developer mistakes (undeclared capability, undeclared name).
37995
+ */
37996
+ async readDom(opts) {
37997
+ if (!this.opts.capabilities.includes("read_dom")) {
37998
+ throw new Error('FetchproxyServer.readDom(): MCP did not declare "read_dom" in capabilities');
37999
+ }
38000
+ await this.ensureConnected();
38001
+ this.throwIfPendingPair();
38002
+ if (!Array.isArray(opts.names) || opts.names.length === 0) {
38003
+ throw new Error("FetchproxyServer.readDom: opts.names must be a non-empty array");
38004
+ }
38005
+ this.assertScopeSubset(opts.names, this.opts.domSelectors.map((d) => d.name), "domSelectors");
38006
+ if (opts.subdomain !== void 0)
38007
+ assertSubdomainLabel(opts.subdomain);
38008
+ const baseDomain = this.resolveBaseDomain(opts.domain);
38009
+ const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
38010
+ const origin = `https://${host}`;
38011
+ const id = this.nextRequestId++;
38012
+ const inner = {
38013
+ type: "request",
38014
+ id,
38015
+ op: "read_dom",
38016
+ init: { origin, names: [...opts.names] }
38017
+ };
38018
+ const pending = new Promise((resolve, reject) => {
38019
+ this.pendingStorage.set(id, { resolve, reject });
38020
+ });
38021
+ await this.sendInnerFrame(inner);
38022
+ return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
38023
+ }
37904
38024
  assertScopeSubset(requested, declared, label) {
37905
38025
  const undeclared = undeclaredKeys(requested, declared);
37906
38026
  if (undeclared.length > 0) {
@@ -37972,7 +38092,7 @@ var FetchproxyServer = class {
37972
38092
  if (storageCb) {
37973
38093
  this.pendingStorage.delete(inner.id);
37974
38094
  if (inner.ok) {
37975
- if ((inner.op === "read_local_storage" || inner.op === "read_session_storage") && inner.values) {
38095
+ if ((inner.op === "read_local_storage" || inner.op === "read_session_storage" || inner.op === "read_dom") && inner.values) {
37976
38096
  storageCb.resolve({ ...inner.values });
37977
38097
  } else {
37978
38098
  storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
@@ -38379,7 +38499,7 @@ function loadAccount(env = process.env) {
38379
38499
  // package.json
38380
38500
  var package_default = {
38381
38501
  name: "canvas-parent-mcp",
38382
- version: "1.1.8",
38502
+ version: "1.2.0",
38383
38503
  mcpName: "io.github.chrischall/canvas-parent-mcp",
38384
38504
  description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
38385
38505
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38423,7 +38543,7 @@ var package_default = {
38423
38543
  "test:watch": "vitest"
38424
38544
  },
38425
38545
  dependencies: {
38426
- "@chrischall/mcp-utils": "^0.12.0",
38546
+ "@chrischall/mcp-utils": "^0.13.0",
38427
38547
  "@fetchproxy/bootstrap": "^1.0.0",
38428
38548
  "@fetchproxy/server": "^1.0.0",
38429
38549
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -38434,7 +38554,7 @@ var package_default = {
38434
38554
  "@types/node": "^26.0.0",
38435
38555
  "@vitest/coverage-v8": "^4.1.7",
38436
38556
  esbuild: "^0.28.0",
38437
- typescript: "^6.0.3",
38557
+ typescript: "^7.0.2",
38438
38558
  vitest: "^4.1.7"
38439
38559
  }
38440
38560
  };
@@ -39355,7 +39475,7 @@ try {
39355
39475
  }
39356
39476
  await runMcp({
39357
39477
  name: "canvas",
39358
- version: "1.1.8",
39478
+ version: "1.2.0",
39359
39479
  // x-release-please-version
39360
39480
  tools: [
39361
39481
  (server) => {
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@ catch (e) {
46
46
  // host's install-time `tools/list` succeeds.
47
47
  await runMcp({
48
48
  name: 'canvas',
49
- version: '1.1.8', // x-release-please-version
49
+ version: '1.2.0', // x-release-please-version
50
50
  tools: [
51
51
  (server) => {
52
52
  if (resolved) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvas-parent-mcp",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "mcpName": "io.github.chrischall/canvas-parent-mcp",
5
5
  "description": "Canvas LMS (Instructure) MCP server — student/observer read access with optional file download",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -44,7 +44,7 @@
44
44
  "test:watch": "vitest"
45
45
  },
46
46
  "dependencies": {
47
- "@chrischall/mcp-utils": "^0.12.0",
47
+ "@chrischall/mcp-utils": "^0.13.0",
48
48
  "@fetchproxy/bootstrap": "^1.0.0",
49
49
  "@fetchproxy/server": "^1.0.0",
50
50
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -55,7 +55,7 @@
55
55
  "@types/node": "^26.0.0",
56
56
  "@vitest/coverage-v8": "^4.1.7",
57
57
  "esbuild": "^0.28.0",
58
- "typescript": "^6.0.3",
58
+ "typescript": "^7.0.2",
59
59
  "vitest": "^4.1.7"
60
60
  }
61
61
  }
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/canvas-parent-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.1.8",
9
+ "version": "1.2.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "canvas-parent-mcp",
14
- "version": "1.1.8",
14
+ "version": "1.2.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: canvas-parent-mcp
3
+ description: This skill should be used when the user asks about Canvas LMS data — their own student account or any observed student. Triggers on phrases like "check Canvas", "what's my grade", "Canvas inbox", "what's due", "missing assignments", "Canvas LMS", "Instructure", "course announcements", "syllabus", or any request about courses, assignments, grades, conversations, announcements, planner items, or files in Canvas.
4
+ ---
5
+
6
+ # canvas-parent-mcp
7
+
8
+ MCP server for Canvas LMS (Instructure) — read courses, grades, assignments, announcements, planner items, and conversations; download course files. Mirrors the parent/observer scope of the sibling `infinitecampus-mcp`.
9
+
10
+ - **npm:** [npmjs.com/package/canvas-parent-mcp](https://www.npmjs.com/package/canvas-parent-mcp)
11
+ - **Source:** [github.com/chrischall/canvas-parent-mcp](https://github.com/chrischall/canvas-parent-mcp)
12
+
13
+ ## Setup
14
+
15
+ ### Option A — npx (recommended)
16
+
17
+ Add to `.mcp.json` in your project or `~/.claude/mcp.json`:
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "canvas": {
23
+ "command": "npx",
24
+ "args": ["-y", "canvas-parent-mcp"],
25
+ "env": {
26
+ "CANVAS_BASE_URL": "https://cms.instructure.com"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ With the [fetchproxy extension](https://github.com/chrischall/fetchproxy) installed and a signed-in Canvas tab, that's enough — the MCP reads your session cookies at startup. Add `CANVAS_TOKEN`, `CANVAS_CLIENT_*`/`CANVAS_REFRESH_TOKEN`, or `CANVAS_USERNAME`/`CANVAS_PASSWORD` to the `env` block if you'd rather use one of those modes.
34
+
35
+ ### Option B — from source
36
+
37
+ ```bash
38
+ git clone https://github.com/chrischall/canvas-parent-mcp
39
+ cd canvas-parent-mcp
40
+ npm install && npm run build
41
+ ```
42
+
43
+ ## Authentication
44
+
45
+ **fetchproxy fallback (recommended, zero-config).** Set only `CANVAS_BASE_URL`. Install the [fetchproxy](https://github.com/chrischall/fetchproxy) browser extension, sign into your Canvas instance once. The MCP reads `canvas_session` + `pseudonym_credentials` cookies from your tab at startup; all API calls go directly from Node after that. Works with any auth flow (SSO/SAML/2FA included).
46
+
47
+ ### Alternatives (env-var)
48
+
49
+ - **Personal access token** — set `CANVAS_TOKEN`. Most institutions have disabled this for non-admins.
50
+ - **OAuth** — set `CANVAS_CLIENT_ID`, `CANVAS_CLIENT_SECRET`, `CANVAS_REFRESH_TOKEN`. Bootstrap via `canvas-parent-mcp-qr-login`.
51
+ - **Username/password (session-scrape)** — set `CANVAS_USERNAME` + `CANVAS_PASSWORD`. Direct Canvas accounts only (no SSO/2FA). Brittle.
52
+
53
+ Precedence when multiple are set: `CANVAS_TOKEN` > username/password > OAuth > fetchproxy. Set `CANVAS_DISABLE_FETCHPROXY=1` to opt out of the fallback.
54
+
55
+ ## Tools (prefix `canvas_`)
56
+
57
+ ### Profile & observees
58
+ - `canvas_get_profile` — your Canvas profile
59
+ - `canvas_list_observees` — students linked to your observer account
60
+
61
+ ### Courses
62
+ - `canvas_list_courses` — your active courses with grades
63
+ - `canvas_get_course(courseId)` — course detail with syllabus + teachers
64
+
65
+ ### Assignments & submissions
66
+ - `canvas_list_assignments(courseId)` — assignments in a course
67
+ - `canvas_list_missing_submissions` — past-due unsubmitted work
68
+ - `canvas_get_submission(courseId, assignmentId)` — your submission with comments + rubric
69
+ - `canvas_list_recent_submissions(courseId)` — recently graded submissions (default 14d)
70
+
71
+ ### Grades
72
+ - `canvas_list_enrollments` — per-course grades
73
+
74
+ ### Calendar & planner
75
+ - `canvas_list_calendar_events` — calendar events / assignments
76
+ - `canvas_list_upcoming_events` — server-curated next 7 days
77
+ - `canvas_list_planner_items` — unified to-do feed
78
+
79
+ ### Communication
80
+ - `canvas_list_announcements(contextCodes)` — course announcements
81
+ - `canvas_list_conversations` — inbox
82
+ - `canvas_get_conversation(id)` — full conversation thread
83
+ - `canvas_list_discussion_topics(courseId)` — course discussion topics
84
+
85
+ ### Files
86
+ - `canvas_list_course_files(courseId)` — file metadata
87
+ - `canvas_download_file(url, destinationPath)` — download a file to disk
88
+
89
+ ## Notes
90
+
91
+ - Set `CANVAS_NAME` if you want a friendly label other than the host portion of the base URL.
92
+ - All read tools that target a user accept an optional `observeeId` parameter (defaults to `self`) — useful when an observer is checking on a linked student.
@@ -0,0 +1,138 @@
1
+ ---
2
+ name: canvas-parent-api
3
+ description: "Query Canvas LMS (Instructure) from a shell with curl and a bearer access token instead of running the canvas-parent-mcp server — courses, grades, assignments, submissions, calendar, planner, announcements, conversations, discussions, and files for yourself or a linked observee. Use when you want Canvas data without the MCP, in a script, or on a machine where the MCP isn't installed."
4
+ ---
5
+
6
+ # Canvas LMS via curl (no MCP)
7
+
8
+ Canvas's REST API (`/api/v1/...`) is a normal per-institution API reachable
9
+ directly with `curl` — no browser bridge, no bot wall. Every request needs an
10
+ `Authorization: Bearer <token>` header; there is no anonymous access. The
11
+ host is **per-institution** (`https://<district>.instructure.com`, or a
12
+ self-hosted domain) — always read it from `$CANVAS_BASE_URL`, never hardcode
13
+ one tenant.
14
+
15
+ This is the same data the `canvas_*` MCP tools return, reached with plain
16
+ `curl` instead of a running server.
17
+
18
+ ## One-time setup — get a bearer token
19
+
20
+ Pick whichever your institution allows (mirrors `canvas-parent-mcp`'s
21
+ `CANVAS_TOKEN` / OAuth modes — the session-cookie and fetchproxy modes need
22
+ the MCP or a browser and are out of scope here):
23
+
24
+ **A. Personal access token (simplest, if allowed)**
25
+
26
+ Canvas web UI → Account → Settings → **+ New Access Token**. Most
27
+ institutions disable this for non-admins, so try B if it's not offered.
28
+
29
+ ```sh
30
+ export CANVAS_BASE_URL=https://<district>.instructure.com # per-institution, no trailing slash
31
+ export CANVAS_TOKEN=<token from the UI>
32
+ ```
33
+
34
+ **B. OAuth via the mobile QR-login flow**
35
+
36
+ In the Canvas mobile app: Account → **QR for Login** (or "Pair with
37
+ Observer/QR Login"), scan it with any camera to get the URL it encodes
38
+ (`https://sso.canvaslms.com/canvas/login?domain=...&code=...`). Exchange it
39
+ once for OAuth credentials with the helper this same repo ships (no MCP
40
+ server needs to be *running* — it's a one-off CLI).
41
+
42
+ It prints four `NAME=value` lines to stdout
43
+ (`CANVAS_BASE_URL`/`CANVAS_CLIENT_ID`/`CANVAS_CLIENT_SECRET`/`CANVAS_REFRESH_TOKEN`)
44
+ — **export them into the shell**, since the next `curl` reads them as env
45
+ vars and this step can't be skipped:
46
+
47
+ ```sh
48
+ eval "$(npx canvas-parent-mcp-qr-login "<qr-url>" | sed 's/^/export /')"
49
+ ```
50
+
51
+ Then mint (and later re-mint) a short-lived access token from the refresh
52
+ token with `curl` directly — no need to re-scan the QR each time:
53
+
54
+ ```sh
55
+ curl -s -X POST "$CANVAS_BASE_URL/login/oauth2/token" \
56
+ -d grant_type=refresh_token \
57
+ -d client_id="$CANVAS_CLIENT_ID" \
58
+ -d client_secret="$CANVAS_CLIENT_SECRET" \
59
+ -d refresh_token="$CANVAS_REFRESH_TOKEN" \
60
+ | jq -r '.access_token'
61
+ ```
62
+
63
+ Export the result as `CANVAS_TOKEN` (access tokens expire in ~1h — re-run
64
+ this when calls start 401ing).
65
+
66
+ ## Core call pattern
67
+
68
+ ```sh
69
+ curl -s \
70
+ -H "Authorization: Bearer $CANVAS_TOKEN" \
71
+ -H "Accept: application/json+canvas-string-ids, application/json" \
72
+ "$CANVAS_BASE_URL/api/v1/users/self/profile" \
73
+ | sed 's/^while(1);//' | jq .
74
+ ```
75
+
76
+ Two quirks every call needs:
77
+
78
+ - **`Accept: application/json+canvas-string-ids, application/json`** — without
79
+ it, large numeric IDs can silently round-trip wrong (JS `2^53` overflow).
80
+ - **XSSI prefix** — some endpoints prepend `while(1);` to the JSON body.
81
+ Strip it before piping to `jq` (`sed 's/^while(1);//'`), or it'll fail to
82
+ parse.
83
+
84
+ ## Resolve-first rule: get IDs before detail
85
+
86
+ Most detail/list-by-course endpoints need a `courseId` and (for parents) an
87
+ observee's user ID — get those first, then substitute them into the paths in
88
+ `references/canvas-endpoints.md`:
89
+
90
+ ```sh
91
+ # Your own profile (sanity-check the token works)
92
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
93
+ "$CANVAS_BASE_URL/api/v1/users/self/profile" | sed 's/^while(1);//' | jq '{id, name, primary_email}'
94
+
95
+ # Students linked to your observer account (empty array for a plain student token)
96
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
97
+ "$CANVAS_BASE_URL/api/v1/users/self/observees?include[]=avatar_url" | sed 's/^while(1);//' | jq '.[] | {id, name}'
98
+
99
+ # Your (or an observee's) active courses, with grades
100
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
101
+ "$CANVAS_BASE_URL/api/v1/users/self/courses?enrollment_state=active&state[]=available&include[]=total_scores&include[]=current_grading_period_scores&include[]=term" \
102
+ | sed 's/^while(1);//' | jq '.[] | {id, name}'
103
+ ```
104
+
105
+ For an observee, swap `users/self` for `users/<observeeId>` in any path that
106
+ starts that way (per-user endpoints); course-scoped endpoints
107
+ (`/api/v1/courses/<courseId>/...`) don't need the observee ID at all — Canvas
108
+ scopes them to whichever user the token belongs to.
109
+
110
+ ## Pagination
111
+
112
+ Canvas paginates with an RFC 5988 `Link` header, not a body field. Fetch
113
+ headers separately to follow `rel="next"`:
114
+
115
+ ```sh
116
+ url="$CANVAS_BASE_URL/api/v1/courses/123/students/submissions?student_ids[]=self&per_page=100"
117
+ while [ -n "$url" ]; do
118
+ headers=$(curl -sD - -o page.json -H "Authorization: Bearer $CANVAS_TOKEN" \
119
+ -H "Accept: application/json+canvas-string-ids, application/json" "$url")
120
+ sed 's/^while(1);//' page.json | jq -c '.[]' >> all.jsonl
121
+ url=$(echo "$headers" | grep -i '^link:' | grep -oE '<[^>]+>; rel="next"' | grep -oE 'https?://[^>]+')
122
+ done
123
+ ```
124
+
125
+ Bumping `per_page` (Canvas allows up to 100) is usually enough for one-off
126
+ queries; only bother with the loop for genuinely large collections.
127
+
128
+ ## Auth expiry
129
+
130
+ - **Personal token**: a 401 means it's expired or revoked — mint a fresh one
131
+ in the Canvas UI (tokens don't self-refresh).
132
+ - **OAuth access token**: expires in ~1h — re-run the `refresh_token`
133
+ exchange above; the refresh token itself is long-lived.
134
+
135
+ All 18 endpoints (courses, assignments, submissions, grades, calendar,
136
+ planner, announcements, conversations, discussions, files) are in
137
+ `references/canvas-endpoints.md` with the real query params and a `jq`
138
+ projection for each.
@@ -0,0 +1,223 @@
1
+ # Canvas API endpoints for curl
2
+
3
+ All 18 `canvas_*` MCP tools, transcribed from `src/tools/*.ts` in
4
+ `canvas-parent-mcp`. Every call needs:
5
+
6
+ ```sh
7
+ -H "Authorization: Bearer $CANVAS_TOKEN"
8
+ -H "Accept: application/json+canvas-string-ids, application/json"
9
+ ```
10
+
11
+ against `$CANVAS_BASE_URL` (per-institution, e.g.
12
+ `https://<district>.instructure.com`). Pipe every response through
13
+ `sed 's/^while(1);//'` before `jq` (Canvas's XSSI guard). List endpoints are
14
+ marked **paginated** — see the `Link`-header loop in `SKILL.md` for anything
15
+ you expect to run long; for a quick look just bump `per_page`.
16
+
17
+ `{userSegment}` below means `users/self`, or `users/<observeeId>` to read a
18
+ linked observee's data instead (get IDs from §2).
19
+
20
+ ---
21
+
22
+ ## 1. Profile
23
+
24
+ ```sh
25
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
26
+ "$CANVAS_BASE_URL/api/v1/users/self/profile" | sed 's/^while(1);//' \
27
+ | jq '{id, name, primary_email, login_id, locale, time_zone}'
28
+ ```
29
+
30
+ ## 2. Observees — students linked to your observer account
31
+
32
+ Empty array for a plain student token.
33
+
34
+ ```sh
35
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
36
+ "$CANVAS_BASE_URL/api/v1/users/self/observees?include[]=avatar_url" | sed 's/^while(1);//' \
37
+ | jq '.[] | {id, name}'
38
+ ```
39
+
40
+ *(paginated)*
41
+
42
+ ## 3. Courses — active, with grades
43
+
44
+ ```sh
45
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
46
+ "$CANVAS_BASE_URL/api/v1/{userSegment}/courses?enrollment_state=active&state[]=available&include[]=total_scores&include[]=current_grading_period_scores&include[]=term" \
47
+ | sed 's/^while(1);//' | jq '.[] | {id, name, total_scores}'
48
+ ```
49
+
50
+ *(paginated)*
51
+
52
+ ## 4. Single course — syllabus, teachers, term
53
+
54
+ ```sh
55
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
56
+ "$CANVAS_BASE_URL/api/v1/courses/{courseId}?include[]=syllabus_body&include[]=teachers&include[]=term" \
57
+ | sed 's/^while(1);//' | jq '{id, name, syllabus_body, teachers, term}'
58
+ ```
59
+
60
+ ## 5. Assignments — a course's assignment list, with your submission inline
61
+
62
+ `bucket` (optional): `past` `overdue` `undated` `ungraded` `unsubmitted`
63
+ `upcoming` `future`.
64
+
65
+ ```sh
66
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
67
+ "$CANVAS_BASE_URL/api/v1/courses/{courseId}/assignments?include[]=submission&order_by=due_at&bucket=upcoming" \
68
+ | sed 's/^while(1);//' | jq '.[] | {id, name, due_at, submission: .submission.workflow_state}'
69
+ ```
70
+
71
+ *(paginated)*
72
+
73
+ ## 6. Missing submissions — past-due, unsubmitted
74
+
75
+ For an observee, `course_ids[]` (repeat the param per ID) is required.
76
+
77
+ ```sh
78
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
79
+ "$CANVAS_BASE_URL/api/v1/{userSegment}/missing_submissions?include[]=planner_overrides&include[]=course&filter[]=submittable&course_ids[]=123&course_ids[]=456" \
80
+ | sed 's/^while(1);//' | jq '.[] | {id, name, due_at, course_id}'
81
+ ```
82
+
83
+ *(paginated)*
84
+
85
+ ## 7. Single submission — rubric + grader comments
86
+
87
+ `userId` defaults to `self`; pass a numeric Canvas user ID for an observee.
88
+
89
+ ```sh
90
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
91
+ "$CANVAS_BASE_URL/api/v1/courses/{courseId}/assignments/{assignmentId}/submissions/self?include[]=submission_comments&include[]=rubric_assessment&include[]=assignment" \
92
+ | sed 's/^while(1);//' | jq '{score, grade, submitted_at, submission_comments}'
93
+ ```
94
+
95
+ ## 8. Recent graded submissions in a course
96
+
97
+ `student_ids[]` defaults to `self`; `graded_since` defaults to 14 days ago
98
+ (ISO 8601, compute it yourself for curl — Canvas doesn't).
99
+
100
+ ```sh
101
+ since=$(date -u -v-14d +%Y-%m-%dT%H:%M:%SZ) # macOS date; use `date -u -d '14 days ago' ...` on GNU
102
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
103
+ "$CANVAS_BASE_URL/api/v1/courses/{courseId}/students/submissions?student_ids[]=self&workflow_state[]=graded&graded_since=$since&include[]=assignment&include[]=submission_comments" \
104
+ | sed 's/^while(1);//' | jq '.[] | {assignment: .assignment.name, score, grade}'
105
+ ```
106
+
107
+ *(paginated)*
108
+
109
+ ## 9. Enrollments — per-course grades
110
+
111
+ ```sh
112
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
113
+ "$CANVAS_BASE_URL/api/v1/{userSegment}/enrollments?state[]=active&type[]=StudentEnrollment&include[]=current_points&include[]=grades" \
114
+ | sed 's/^while(1);//' | jq '.[] | {course_id, grades}'
115
+ ```
116
+
117
+ *(paginated)*
118
+
119
+ ## 10. Calendar events / assignments across contexts
120
+
121
+ `context_codes[]` looks like `course_123`, `user_456` (repeat the param per
122
+ code). `type` is `event` or `assignment`.
123
+
124
+ ```sh
125
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
126
+ "$CANVAS_BASE_URL/api/v1/calendar_events?type=assignment&start_date=2026-07-01&end_date=2026-07-31&context_codes[]=course_123" \
127
+ | sed 's/^while(1);//' | jq '.[] | {title, start_at}'
128
+ ```
129
+
130
+ *(paginated)*
131
+
132
+ ## 11. Upcoming events — Canvas's curated next-7-days view
133
+
134
+ ```sh
135
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
136
+ "$CANVAS_BASE_URL/api/v1/users/self/upcoming_events" | sed 's/^while(1);//' | jq .
137
+ ```
138
+
139
+ ## 12. Planner items — assignments + announcements + notes + events
140
+
141
+ Omit `{observeeId}` (use `/api/v1/planner/items`) for yourself.
142
+
143
+ ```sh
144
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
145
+ "$CANVAS_BASE_URL/api/v1/users/{observeeId}/planner/items?start_date=2026-07-01&end_date=2026-07-31" \
146
+ | sed 's/^while(1);//' | jq '.[] | {plannable_type, plannable_date: .plannable_date}'
147
+ ```
148
+
149
+ *(paginated; self variant: `$CANVAS_BASE_URL/api/v1/planner/items?...`)*
150
+
151
+ ## 13. Announcements — across one or more courses
152
+
153
+ `context_codes[]` is **required** (e.g. `course_123`); defaults to
154
+ `active_only=true`.
155
+
156
+ ```sh
157
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
158
+ "$CANVAS_BASE_URL/api/v1/announcements?context_codes[]=course_123&context_codes[]=course_456&active_only=true" \
159
+ | sed 's/^while(1);//' | jq '.[] | {title, posted_at}'
160
+ ```
161
+
162
+ *(paginated)*
163
+
164
+ ## 14. Conversations (inbox) — list
165
+
166
+ `scope` (optional): `unread` `starred` `archived` `sent`. `filter[]` is an
167
+ array of context codes.
168
+
169
+ ```sh
170
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
171
+ "$CANVAS_BASE_URL/api/v1/conversations?scope=unread&include[]=participant_avatars" \
172
+ | sed 's/^while(1);//' | jq '.[] | {id, subject, last_message}'
173
+ ```
174
+
175
+ *(paginated)*
176
+
177
+ ## 15. Conversation detail — full thread
178
+
179
+ ```sh
180
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
181
+ "$CANVAS_BASE_URL/api/v1/conversations/{id}?include[]=participant_avatars" \
182
+ | sed 's/^while(1);//' | jq '.messages[] | {author_id, body, created_at}'
183
+ ```
184
+
185
+ ## 16. Discussion topics — a course's list
186
+
187
+ `only_announcements` defaults `false`; `order_by` (optional): `position`
188
+ `recent_activity` `title`.
189
+
190
+ ```sh
191
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
192
+ "$CANVAS_BASE_URL/api/v1/courses/{courseId}/discussion_topics?only_announcements=false&order_by=recent_activity" \
193
+ | sed 's/^while(1);//' | jq '.[] | {id, title, posted_at}'
194
+ ```
195
+
196
+ *(paginated)*
197
+
198
+ ## 17. Course files — metadata list
199
+
200
+ `search_term` and `content_types[]` are optional filters.
201
+
202
+ ```sh
203
+ curl -s -H "Authorization: Bearer $CANVAS_TOKEN" -H "Accept: application/json+canvas-string-ids, application/json" \
204
+ "$CANVAS_BASE_URL/api/v1/courses/{courseId}/files?search_term=syllabus" \
205
+ | sed 's/^while(1);//' | jq '.[] | {id, display_name, url, "content-type"}'
206
+ ```
207
+
208
+ *(paginated)*
209
+
210
+ ## 18. Download a file
211
+
212
+ Use the absolute `url` field from §17 directly — it's already a
213
+ fully-qualified, Bearer-authable Canvas URL (no `/api/v1` prefix needed):
214
+
215
+ ```sh
216
+ curl -sL -H "Authorization: Bearer $CANVAS_TOKEN" "$FILE_URL" -o /path/to/destination.pdf
217
+ ```
218
+
219
+ `-L` follows Canvas's redirect to the actual file storage backend. The MCP's
220
+ `canvas_download_file` tool additionally refuses to overwrite an existing
221
+ destination unless told to and validates the parent directory exists —
222
+ replicate that yourself in a script if it matters (`[ -f dest ] && exit 1`,
223
+ `[ -d "$(dirname dest)" ] || exit 1`).
@@ -1,202 +0,0 @@
1
- ---
2
- name: canvas
3
- description: This skill should be used when the user asks about Canvas LMS (Instructure) data for themselves or any student they observe. Triggers on phrases like "check Canvas", "what's my grade", "Canvas inbox", "what's due", "missing assignments", "Canvas LMS", "Instructure", "course announcements", "syllabus", or any request about courses, assignments, grades, conversations, announcements, planner items, or files.
4
- ---
5
-
6
- # canvas-parent-mcp
7
-
8
- MCP server for Canvas LMS (Instructure) — 18 tools covering profile, observees, courses, assignments, submissions, grades, calendar, planner, announcements, conversations, discussions, and files. Read-only except for one file-download tool.
9
-
10
- - **Source:** [github.com/chrischall/canvas-parent-mcp](https://github.com/chrischall/canvas-parent-mcp)
11
- - **npm:** [npmjs.com/package/canvas-parent-mcp](https://www.npmjs.com/package/canvas-parent-mcp)
12
-
13
- ## Setup
14
-
15
- Pick one auth mode. **Username/password is recommended** — most schools have disabled personal-access-token creation.
16
-
17
- ### Option A — Claude Code (direct MCP, no mcporter)
18
-
19
- Add to `.mcp.json` in your project or `~/.claude/mcp.json`:
20
-
21
- ```json
22
- {
23
- "mcpServers": {
24
- "canvas": {
25
- "command": "npx",
26
- "args": ["-y", "canvas-parent-mcp"],
27
- "env": {
28
- "CANVAS_BASE_URL": "https://cms.instructure.com",
29
- "CANVAS_USERNAME": "me@example.com",
30
- "CANVAS_PASSWORD": "your-canvas-password",
31
- "CANVAS_NAME": "CMS"
32
- }
33
- }
34
- }
35
- }
36
- ```
37
-
38
- The server logs in lazily on the first request and silently re-mints cookies on 401, so you never have to re-bootstrap. Direct Canvas accounts only — won't work with SAML/Google/Microsoft SSO or 2FA.
39
-
40
- #### Advanced alternatives
41
-
42
- If your admin still allows tokens, swap `CANVAS_USERNAME`/`CANVAS_PASSWORD` for `CANVAS_TOKEN`:
43
-
44
- ```json
45
- "env": {
46
- "CANVAS_BASE_URL": "https://cms.instructure.com",
47
- "CANVAS_TOKEN": "your-personal-access-token"
48
- }
49
- ```
50
-
51
- If your account uses SSO (so username/password won't work), mint OAuth credentials by reusing the Canvas mobile-app QR-login flow (`canvas-parent-mcp-qr-login`), then:
52
-
53
- ```json
54
- "env": {
55
- "CANVAS_BASE_URL": "https://cms.instructure.com",
56
- "CANVAS_CLIENT_ID": "...",
57
- "CANVAS_CLIENT_SECRET": "...",
58
- "CANVAS_REFRESH_TOKEN": "..."
59
- }
60
- ```
61
-
62
- ### Option B — mcporter
63
-
64
- #### 1. Install
65
-
66
- ```bash
67
- npm install -g canvas-parent-mcp
68
- ```
69
-
70
- Or from source:
71
- ```bash
72
- git clone https://github.com/chrischall/canvas-parent-mcp
73
- cd canvas-parent-mcp
74
- npm install && npm run build
75
- ```
76
-
77
- #### 2. Configure credentials
78
-
79
- ```bash
80
- cp .env.example .env
81
- # Edit .env: set CANVAS_BASE_URL plus one of CANVAS_TOKEN, CANVAS_USERNAME+CANVAS_PASSWORD, or the OAuth triple.
82
- ```
83
-
84
- #### 3. Register with mcporter
85
-
86
- ```bash
87
- mcporter config add canvas \
88
- --command "canvas-parent-mcp" \
89
- --env "CANVAS_BASE_URL=https://cms.instructure.com" \
90
- --env "CANVAS_USERNAME=me@example.com" \
91
- --env "CANVAS_PASSWORD=your-canvas-password" \
92
- --env "CANVAS_NAME=CMS" \
93
- --config ~/.mcporter/mcporter.json
94
- ```
95
-
96
- (Or substitute `CANVAS_TOKEN` / OAuth env vars per the alternatives above.)
97
-
98
- #### 4. Verify
99
-
100
- ```bash
101
- mcporter list --config ~/.mcporter/mcporter.json
102
- mcporter call canvas.canvas_get_profile --config ~/.mcporter/mcporter.json
103
- ```
104
-
105
- ## Calling tools (mcporter)
106
-
107
- ```bash
108
- mcporter call canvas.<tool_name> [key=value ...] --config ~/.mcporter/mcporter.json
109
- ```
110
-
111
- Always pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter.json` exists.
112
-
113
- Most tools accept an optional `observeeId` (defaults to `self`) — set it to a student's user ID when an observer is checking on a linked student. List the candidates with `canvas_list_observees`.
114
-
115
- ## Tools
116
-
117
- ### Profile & observees
118
- | Tool | Notes |
119
- |------|-------|
120
- | `canvas_get_profile` | Your Canvas profile (id, name, email, login, locale, timezone). Call this first to confirm credentials work. |
121
- | `canvas_list_observees` | Students linked to your observer account. Returns `[]` for plain student tokens. |
122
-
123
- ### Courses
124
- | Tool | Notes |
125
- |------|-------|
126
- | `canvas_list_courses(observeeId?)` | Active courses with course-level grades. Includes total scores + current grading period scores. |
127
- | `canvas_get_course(courseId)` | Course detail: syllabus, teachers, term. |
128
-
129
- ### Assignments & submissions
130
- | Tool | Notes |
131
- |------|-------|
132
- | `canvas_list_assignments(courseId, bucket?)` | Assignments for a course. `bucket` can be `upcoming`, `overdue`, `past`, or `undated`. Includes the user's submission inline. |
133
- | `canvas_list_missing_submissions(observeeId?, courseIds?)` | Past-due unsubmitted assignments. For an observee, `courseIds` is required. |
134
- | `canvas_get_submission(courseId, assignmentId, userId?)` | A single submission with comments + rubric assessment. `userId` defaults to `self`. |
135
- | `canvas_list_recent_submissions(courseId, since?)` | Recently graded submissions in a course. Defaults to a 14-day window. |
136
-
137
- ### Grades
138
- | Tool | Notes |
139
- |------|-------|
140
- | `canvas_list_enrollments(observeeId?)` | Active student enrollments with `grades` (current_score, final_score, current_grade, final_grade, current grading period). |
141
-
142
- ### Calendar & planner
143
- | Tool | Notes |
144
- |------|-------|
145
- | `canvas_list_calendar_events(contextCodes?, type?, startDate?, endDate?)` | Calendar events / assignments. `contextCodes` is an array like `["course_123", "user_456"]`. |
146
- | `canvas_list_upcoming_events` | Canvas's curated next-7-days view. |
147
- | `canvas_list_planner_items(observeeId?, startDate?, endDate?, contextCodes?)` | Unified planner: assignments + announcements + planner notes + calendar events. |
148
-
149
- ### Communication
150
- | Tool | Notes |
151
- |------|-------|
152
- | `canvas_list_announcements(contextCodes, startDate?, endDate?)` | Announcements across one or more courses. `contextCodes` is required (e.g. `["course_123"]`). Defaults to the last 14 days. |
153
- | `canvas_list_conversations(scope?)` | Inbox conversation list. `scope` ∈ `unread` / `starred` / `archived`. |
154
- | `canvas_get_conversation(id)` | Full conversation thread with messages. |
155
- | `canvas_list_discussion_topics(courseId)` | Discussion topic list for a course (read-only). |
156
-
157
- ### Files
158
- | Tool | Notes |
159
- |------|-------|
160
- | `canvas_list_course_files(courseId, searchTerm?)` | File metadata: id, display_name, size, content-type, **`url`**. |
161
- | `canvas_download_file(url, destinationPath, overwrite?)` | Writes the file to `destinationPath` on disk. **`destinationPath` is required** — confirm the path with the user before calling. |
162
-
163
- ## Workflows
164
-
165
- **Discovery (first time):**
166
- 1. `canvas_get_profile` → confirm credentials
167
- 2. `canvas_list_observees` → if empty, you're a student; otherwise capture observee IDs
168
- 3. `canvas_list_courses` → capture course IDs
169
-
170
- **Is everything OK at school?**
171
- 1. `canvas_list_courses` (capture IDs)
172
- 2. `canvas_list_recent_submissions(courseId)` for each — last 14 days of grading
173
- 3. `canvas_list_missing_submissions` — past-due work
174
- 4. `canvas_list_announcements(contextCodes=["course_<id>"])` — recent announcements
175
-
176
- **What got graded this week?**
177
- - `canvas_list_recent_submissions(courseId, since="YYYY-MM-DDTHH:MM:SSZ")`
178
-
179
- **What's due soon?**
180
- - `canvas_list_planner_items(startDate=..., endDate=...)` — unified planner
181
- - Or `canvas_list_assignments(courseId, bucket="upcoming")` per course
182
-
183
- **Today's calendar:**
184
- - `canvas_list_upcoming_events`
185
-
186
- **Read a syllabus:**
187
- - `canvas_get_course(courseId)` — `syllabus_body` is included
188
-
189
- **Inbox:**
190
- 1. `canvas_list_conversations` — find a thread
191
- 2. `canvas_get_conversation(id)` — read it
192
-
193
- **Download a file:**
194
- 1. `canvas_list_course_files(courseId)` → find the file's `url`
195
- 2. Confirm destination path with the user
196
- 3. `canvas_download_file(url, destinationPath="/Users/.../file.pdf")`
197
-
198
- ## Caution
199
-
200
- - `canvas_download_file` writes to disk at `destinationPath` — confirm the path with the user; pass `overwrite:true` to replace.
201
- - All auth credentials (password, token, refresh token) grant the same access as the user account. Treat them as secrets — never commit `.env`.
202
- - Endpoints that return paginated results follow Canvas's RFC 5988 `Link` headers automatically up to 50 pages (5,000 items at the default `per_page=100`).