canvas-parent-mcp 1.0.2 → 1.0.3
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 +1 -1
- package/README.md +24 -7
- package/dist/auth.js +167 -0
- package/dist/bundle.js +6898 -1027
- package/dist/client.js +16 -0
- package/dist/index.js +41 -17
- package/package.json +4 -3
- package/server.json +2 -2
|
@@ -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.0.
|
|
10
|
+
"version": "1.0.3"
|
|
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.0.
|
|
18
|
+
"version": "1.0.3",
|
|
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.0.
|
|
4
|
+
"version": "1.0.3",
|
|
5
5
|
"description": "Canvas LMS (Instructure) MCP server for Claude — courses, grades, assignments, announcements, planner, conversations, files",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/README.md
CHANGED
|
@@ -27,9 +27,28 @@ Tools that the harness will gate as write/IO operations: `canvas_download_file`.
|
|
|
27
27
|
|
|
28
28
|
## Configuration
|
|
29
29
|
|
|
30
|
-
Set
|
|
30
|
+
Set `CANVAS_BASE_URL` plus one of four auth modes. `canvas-parent-mcp` tries them in priority order:
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
1. **`CANVAS_TOKEN`** → personal access token
|
|
33
|
+
2. **`CANVAS_CLIENT_ID` + `CANVAS_CLIENT_SECRET` + `CANVAS_REFRESH_TOKEN`** → OAuth
|
|
34
|
+
3. **`CANVAS_USERNAME` + `CANVAS_PASSWORD`** → session-scrape (direct Canvas accounts only)
|
|
35
|
+
4. **fetchproxy fallback** → no env vars needed; reads `canvas_session` + `pseudonym_credentials` cookies from your signed-in Canvas tab via the [fetchproxy](https://github.com/chrischall/fetchproxy) browser extension
|
|
36
|
+
|
|
37
|
+
If none succeed, you get an error that names every escape hatch.
|
|
38
|
+
|
|
39
|
+
### Mode A — fetchproxy fallback (recommended, zero config)
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
CANVAS_BASE_URL=https://cms.instructure.com
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Install the fetchproxy 0.3.0 Chrome / Safari extension (Chrome Web Store / Safari `.dmg`), sign into your Canvas instance once, and the MCP reads your session cookies at startup. After that, all Canvas API calls go directly from Node — the extension is **not** in the request hot path. Works with any auth flow (SSO/SAML/2FA included) because Canvas itself handled the sign-in.
|
|
46
|
+
|
|
47
|
+
Multiple districts? Declared domain `instructure.com` matches every `*.instructure.com` host, so you only pair the extension once. The MCP uses whichever district you set in `CANVAS_BASE_URL`.
|
|
48
|
+
|
|
49
|
+
Set `CANVAS_DISABLE_FETCHPROXY=1` to opt out (missing creds become a hard error — useful in headless CI).
|
|
50
|
+
|
|
51
|
+
### Mode B — username/password (legacy session-scrape)
|
|
33
52
|
|
|
34
53
|
```
|
|
35
54
|
CANVAS_BASE_URL=https://cms.instructure.com
|
|
@@ -38,12 +57,10 @@ CANVAS_PASSWORD=your-canvas-password
|
|
|
38
57
|
CANVAS_NAME=cms # optional, defaults to host portion of base URL
|
|
39
58
|
```
|
|
40
59
|
|
|
41
|
-
**Direct Canvas accounts only** — won't work with SAML/Google/Microsoft SSO or 2FA. Treat `.env` like a password file
|
|
60
|
+
**Direct Canvas accounts only** — won't work with SAML/Google/Microsoft SSO or 2FA. Brittle (breaks on every Canvas login-page restyling). Prefer fetchproxy if your tab is already signed in. Treat `.env` like a password file.
|
|
42
61
|
|
|
43
62
|
### Advanced alternatives
|
|
44
63
|
|
|
45
|
-
If your admin still allows tokens, or your account uses SSO (so username/password won't work), pick one of these instead:
|
|
46
|
-
|
|
47
64
|
<details>
|
|
48
65
|
<summary><b>Personal access token</b> — simplest if your admin allows it</summary>
|
|
49
66
|
|
|
@@ -65,10 +82,10 @@ CANVAS_CLIENT_SECRET=...
|
|
|
65
82
|
CANVAS_REFRESH_TOKEN=...
|
|
66
83
|
```
|
|
67
84
|
|
|
68
|
-
If your account uses SSO
|
|
85
|
+
If your account uses SSO and you can't use fetchproxy (e.g. headless server), mint OAuth credentials by reusing the Canvas mobile-app QR-login flow — see [Bootstrapping OAuth via the mobile QR code](#bootstrapping-oauth-via-the-mobile-qr-code) below.
|
|
69
86
|
</details>
|
|
70
87
|
|
|
71
|
-
**Precedence when multiple are set:** `CANVAS_TOKEN` >
|
|
88
|
+
**Precedence when multiple are set:** `CANVAS_TOKEN` > username/password > OAuth > fetchproxy.
|
|
72
89
|
|
|
73
90
|
See `.env.example`.
|
|
74
91
|
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Auth resolution — Pattern A template
|
|
3
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Canvas supports four auth paths. This file picks one, in priority order,
|
|
6
|
+
// and hands the chosen path to `CanvasClient`. It mirrors the Pattern A
|
|
7
|
+
// shape used by sibling MCPs (ofw-mcp/src/auth.ts, signupgenius-mcp/src/auth.ts)
|
|
8
|
+
// so all the MCPs in this family stay structurally aligned.
|
|
9
|
+
//
|
|
10
|
+
// THE FOUR PATHS, in priority order:
|
|
11
|
+
//
|
|
12
|
+
// 1. Personal access token (existing)
|
|
13
|
+
// CANVAS_TOKEN set → stateless `Authorization: Bearer <token>`. The
|
|
14
|
+
// most reliable mode but most schools have disabled token creation
|
|
15
|
+
// for non-admins. Unchanged from pre-fetchproxy behavior.
|
|
16
|
+
//
|
|
17
|
+
// 2. OAuth refresh token (existing)
|
|
18
|
+
// CANVAS_CLIENT_ID + CANVAS_CLIENT_SECRET + CANVAS_REFRESH_TOKEN →
|
|
19
|
+
// `grant_type=refresh_token` against `/login/oauth2/token`, bootstrap
|
|
20
|
+
// via the bundled QR-login helper. Unchanged.
|
|
21
|
+
//
|
|
22
|
+
// 3. Username/password session-scrape (existing)
|
|
23
|
+
// CANVAS_USERNAME + CANVAS_PASSWORD → scrape `authenticity_token`
|
|
24
|
+
// from `/login/canvas`, POST creds, capture `canvas_session` +
|
|
25
|
+
// `pseudonym_credentials` cookies. Brittle (breaks on SSO/2FA and
|
|
26
|
+
// every Canvas login-page restyling) but works for direct Canvas
|
|
27
|
+
// accounts. Unchanged from pre-fetchproxy behavior.
|
|
28
|
+
//
|
|
29
|
+
// 4. fetchproxy fallback (new)
|
|
30
|
+
// When no env vars are set, lift the user's session out of their
|
|
31
|
+
// already-signed-in canvas tab. `@fetchproxy/bootstrap` opens a
|
|
32
|
+
// one-shot WebSocket bridge, asks the extension for the
|
|
33
|
+
// `canvas_session` + `pseudonym_credentials` cookies (declared
|
|
34
|
+
// upfront — that's the security boundary), and closes the bridge.
|
|
35
|
+
// Subsequent Canvas API calls go out via plain Node `fetch()` with
|
|
36
|
+
// those cookies attached — fetchproxy is NOT in the request hot path.
|
|
37
|
+
//
|
|
38
|
+
// Note: `pseudonym_credentials` is HttpOnly, which is fine —
|
|
39
|
+
// @fetchproxy/bootstrap@^0.3.0 uses `chrome.cookies.get` to read
|
|
40
|
+
// it, and the security gate is the declared cookie key list, not
|
|
41
|
+
// HttpOnly status.
|
|
42
|
+
//
|
|
43
|
+
// Users opt out with CANVAS_DISABLE_FETCHPROXY=1 (anyone who wants
|
|
44
|
+
// the old behavior of "fail loudly when creds are missing").
|
|
45
|
+
//
|
|
46
|
+
// 5. Error
|
|
47
|
+
// Nothing to authenticate with. We throw a message that names every
|
|
48
|
+
// escape hatch the user can try.
|
|
49
|
+
//
|
|
50
|
+
// Testability:
|
|
51
|
+
// - `@fetchproxy/bootstrap` is mocked at the module boundary in tests.
|
|
52
|
+
// - `loadAccount()` (the existing env-var resolver) is reused as-is so
|
|
53
|
+
// the legacy paths keep working unchanged.
|
|
54
|
+
import { bootstrap } from '@fetchproxy/bootstrap';
|
|
55
|
+
import { loadAccount } from './config.js';
|
|
56
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
57
|
+
function readEnv(key) {
|
|
58
|
+
const raw = process.env[key];
|
|
59
|
+
if (typeof raw !== 'string')
|
|
60
|
+
return undefined;
|
|
61
|
+
const trimmed = raw.trim();
|
|
62
|
+
if (trimmed.length === 0)
|
|
63
|
+
return undefined;
|
|
64
|
+
if (trimmed === 'undefined' || trimmed === 'null')
|
|
65
|
+
return undefined;
|
|
66
|
+
if (/^\$\{[^}]*\}$/.test(trimmed))
|
|
67
|
+
return undefined;
|
|
68
|
+
return trimmed;
|
|
69
|
+
}
|
|
70
|
+
function fetchproxyDisabled() {
|
|
71
|
+
const raw = readEnv('CANVAS_DISABLE_FETCHPROXY');
|
|
72
|
+
if (raw === undefined)
|
|
73
|
+
return false;
|
|
74
|
+
return ['1', 'true', 'yes', 'on'].includes(raw.toLowerCase());
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The exact error message `loadAccount()` throws when NO auth env vars are
|
|
78
|
+
* set. We catch this specific string so partial-config errors (which the
|
|
79
|
+
* user MUST fix) still propagate, but the "you didn't set anything at all"
|
|
80
|
+
* case falls through to fetchproxy.
|
|
81
|
+
*/
|
|
82
|
+
const NO_ENV_CONFIG_MARKER = 'Missing Canvas auth config';
|
|
83
|
+
/**
|
|
84
|
+
* Resolve Canvas auth using the four-path priority described at the top of
|
|
85
|
+
* this file. Throws with an actionable message when no path succeeds.
|
|
86
|
+
*/
|
|
87
|
+
export async function resolveAuth() {
|
|
88
|
+
// ── Paths 1, 2, 3: env-var credentials. loadAccount() handles precedence,
|
|
89
|
+
// partial-config errors, and env-var sanitization for us.
|
|
90
|
+
try {
|
|
91
|
+
const account = loadAccount();
|
|
92
|
+
return { account, source: 'env' };
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
// Partial-config errors (missing one of USERNAME/PASSWORD, incomplete
|
|
96
|
+
// OAuth triple, non-https BASE_URL, etc.) are USER MISTAKES — they
|
|
97
|
+
// propagate. Only the "no auth config set at all" case falls through.
|
|
98
|
+
if (!e.message.startsWith(NO_ENV_CONFIG_MARKER)) {
|
|
99
|
+
throw e;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ── Path 4: fetchproxy fallback.
|
|
103
|
+
if (!fetchproxyDisabled()) {
|
|
104
|
+
// CANVAS_BASE_URL is guaranteed valid here — loadAccount() validates it
|
|
105
|
+
// before throwing the NO_ENV_CONFIG_MARKER error we caught above.
|
|
106
|
+
const baseUrl = readEnv('CANVAS_BASE_URL');
|
|
107
|
+
const baseHost = new URL(baseUrl).hostname;
|
|
108
|
+
// Wildcard match: Canvas tenants live on per-district subdomains of
|
|
109
|
+
// *.instructure.com. The 0.2.0+ matcher does `*.${domain}` matching, so
|
|
110
|
+
// declaring `instructure.com` covers every district the user might
|
|
111
|
+
// switch between. Self-hosted Canvas installations (rare) declare the
|
|
112
|
+
// literal hostname instead.
|
|
113
|
+
const declaredDomain = baseHost.endsWith('.instructure.com')
|
|
114
|
+
? 'instructure.com'
|
|
115
|
+
: baseHost;
|
|
116
|
+
const name = readEnv('CANVAS_NAME') ?? baseHost;
|
|
117
|
+
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
|
|
118
|
+
try {
|
|
119
|
+
const session = await bootstrap({
|
|
120
|
+
serverName: pkg.name,
|
|
121
|
+
version: pkg.version,
|
|
122
|
+
domains: [declaredDomain],
|
|
123
|
+
declare: {
|
|
124
|
+
cookies: ['canvas_session', 'pseudonym_credentials'],
|
|
125
|
+
localStorage: [],
|
|
126
|
+
sessionStorage: [],
|
|
127
|
+
captureHeaders: [],
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
const canvasSession = session.cookies['canvas_session'];
|
|
131
|
+
const pseudoCreds = session.cookies['pseudonym_credentials'];
|
|
132
|
+
if (!canvasSession || !pseudoCreds) {
|
|
133
|
+
throw new Error(`required cookies not found on ${baseHost}. ` +
|
|
134
|
+
'Sign into your Canvas instance in the browser ' +
|
|
135
|
+
'(with the fetchproxy extension installed) and retry.');
|
|
136
|
+
}
|
|
137
|
+
const cookie = `canvas_session=${canvasSession}; pseudonym_credentials=${pseudoCreds}`;
|
|
138
|
+
// Synthesize a session account with empty creds — the client will see
|
|
139
|
+
// `preloaded` and skip the form login. This mirrors the cookie shape
|
|
140
|
+
// the legacy session-scrape path produces, so everything downstream
|
|
141
|
+
// (Cookie header, 401-retry, file downloads) keeps working.
|
|
142
|
+
const account = {
|
|
143
|
+
mode: 'session',
|
|
144
|
+
name,
|
|
145
|
+
baseUrl: cleanBaseUrl,
|
|
146
|
+
username: '',
|
|
147
|
+
password: '',
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
account,
|
|
151
|
+
preloaded: { cookie },
|
|
152
|
+
source: 'fetchproxy',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
157
|
+
throw new Error(`Canvas auth: no CANVAS_TOKEN, CANVAS_CLIENT_*/CANVAS_REFRESH_TOKEN, or CANVAS_USERNAME/CANVAS_PASSWORD set, ` +
|
|
158
|
+
`and fetchproxy fallback failed: ${msg}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// ── Path 5: nothing configured and fetchproxy explicitly disabled.
|
|
162
|
+
throw new Error('Missing Canvas auth config. Set one of: CANVAS_TOKEN (personal access token), ' +
|
|
163
|
+
'CANVAS_USERNAME+CANVAS_PASSWORD (auto-login), ' +
|
|
164
|
+
'all three of CANVAS_CLIENT_ID, CANVAS_CLIENT_SECRET, CANVAS_REFRESH_TOKEN (OAuth), ' +
|
|
165
|
+
'or install the fetchproxy extension and sign into your Canvas instance ' +
|
|
166
|
+
'(unset CANVAS_DISABLE_FETCHPROXY if it is set).');
|
|
167
|
+
}
|