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.
@@ -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.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.2",
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.2",
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 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
 
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
+ }