canvas-parent-mcp 1.0.2 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.2"
10
+ "version": "1.1.2"
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.2",
18
+ "version": "1.1.2",
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.2",
4
+ "version": "1.1.2",
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 the base URL plus *one* auth mode. **Username/password is recommended** — most schools have disabled personal-access-token creation, and this mode auto-logs-in on first request and silently re-mints session cookies on 401, so you never have to re-bootstrap.
30
+ Set `CANVAS_BASE_URL` plus one of four auth modes. `canvas-parent-mcp` tries them in priority order:
31
31
 
32
- **Username/password (recommended):**
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: do not commit it.
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 (so username/password can't auth), 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.
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` > `CANVAS_USERNAME`+`CANVAS_PASSWORD` > OAuth.
88
+ **Precedence when multiple are set:** `CANVAS_TOKEN` > username/password > OAuth > fetchproxy.
72
89
 
73
90
  See `.env.example`.
74
91
 
@@ -91,3 +108,27 @@ This reuses the same SSO + OAuth endpoints the official Canvas Student/Parent ap
91
108
  ## Status
92
109
 
93
110
  Unofficial — not affiliated with Instructure. AI-maintained.
111
+
112
+ ## Acknowledgement of Terms
113
+
114
+ By using this MCP server, you acknowledge and agree to the following:
115
+
116
+ **1. This server accesses your own Canvas account via the official Canvas REST API.** Auth happens via your own personal access token, issued by your institution. It does not — and cannot — access anyone else's enrollments, grades, or messages.
117
+
118
+ **2. [Instructure's Canvas API Policy](https://www.instructure.com/policies/canvas-api-policy) governs your use of this server**, in addition to your institution's own acceptable-use policy. The clauses most relevant here:
119
+
120
+ > You may not use our APIs on behalf of any third-party… You may not use or access our APIs for competitive purposes… You may not interfere with our APIs, our systems, or other users… You may not circumvent any contractual usage limits.
121
+
122
+ On rate limits: *"limits are enforced per user access token… with dynamic throttling."* On data: *"Any user information retrieved through the API—including course enrollments, grades, and profile information—should be considered and treated as private information."*
123
+
124
+ You are agreeing to those terms — read by the maintainer 2026-05-23 — every time you invoke a tool in this server.
125
+
126
+ **3. Personal, observer/student/parent use only.** This project is not affiliated with, endorsed by, sponsored by, or in partnership with Instructure, Inc. or any school district. It is a personal automation tool for an authenticated Canvas user (typically a parent observer) to read their own (or their student's) enrollments, assignments, grades, and announcements. Do not use it to bulk-extract a district's course content, redistribute student data, or train AI models on student records.
127
+
128
+ **4. FERPA + your institution's AUP apply.** Student educational records are protected under the federal Family Educational Rights and Privacy Act (FERPA). Even though your token grants you lawful access, **how you store, redistribute, or feed that data into LLMs is regulated**. Treat any output (grades, assignments, comments, conversations) as confidential student data. Your institution's acceptable-use policy may add further restrictions on automated access — check before automating.
129
+
130
+ **5. Your token is yours alone.** **Do not commit `CANVAS_API_TOKEN` to git**, do not paste it in shared chats, and rotate it if it's ever exposed. A leaked token grants full Canvas access scoped to your user.
131
+
132
+ **6. You accept full responsibility** for any consequences of using this server in connection with your Canvas account — rate limiting (dynamic throttling kicks in well below documented limits when Canvas is under load), token revocation, account warnings, institution-admin investigations, or any enforcement action. If Instructure or your institution objects to your use, stop using this server.
133
+
134
+ This section is the maintainer's good-faith summary of the terms — it is not legal advice and does not modify or supersede Instructure's actual Canvas API Policy or your institution's policies.
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
+ }