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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/dist/bundle.js +128 -8
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/skills/canvas-parent/SKILL.md +92 -0
- package/skills/canvas-parent-api/SKILL.md +138 -0
- package/skills/canvas-parent-api/references/canvas-endpoints.md +223 -0
- package/skills/canvas/SKILL.md +0 -202
|
@@ -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.
|
|
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.
|
|
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.
|
|
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": "./
|
|
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.
|
|
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.
|
|
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: "^
|
|
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.
|
|
39478
|
+
version: "1.2.0",
|
|
39359
39479
|
// x-release-please-version
|
|
39360
39480
|
tools: [
|
|
39361
39481
|
(server) => {
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvas-parent-mcp",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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": "^
|
|
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.
|
|
9
|
+
"version": "1.2.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "canvas-parent-mcp",
|
|
14
|
-
"version": "1.
|
|
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`).
|
package/skills/canvas/SKILL.md
DELETED
|
@@ -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`).
|