canvas-parent-mcp 1.2.2 → 1.2.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/dist/auth.js +84 -55
- package/dist/bundle.js +143 -84
- package/dist/client.js +28 -21
- package/dist/index.js +2 -2
- package/package.json +2 -2
- 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.2.
|
|
10
|
+
"version": "1.2.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.2.
|
|
18
|
+
"version": "1.2.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.2.
|
|
4
|
+
"version": "1.2.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/dist/auth.js
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
// - `@fetchproxy/bootstrap` is mocked at the module boundary in tests.
|
|
55
55
|
// - `loadAccount()` (the existing env-var resolver) is reused as-is so
|
|
56
56
|
// the legacy paths keep working unchanged.
|
|
57
|
-
import {
|
|
57
|
+
import { createSessionLifter } from '@fetchproxy/bootstrap';
|
|
58
58
|
import { classifyBridgeError } from '@fetchproxy/server';
|
|
59
59
|
import { readEnvVar } from '@chrischall/mcp-utils';
|
|
60
60
|
import { loadAccount } from './config.js';
|
|
@@ -98,6 +98,12 @@ export async function resolveAuth() {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
// ── Path 4: fetchproxy fallback.
|
|
101
|
+
//
|
|
102
|
+
// NOTE: the browser is NOT read here. resolveAuth() runs once at process
|
|
103
|
+
// start; the lift is handed to the client as `refresh` and runs lazily on
|
|
104
|
+
// the first request and again on every 401. A cookie captured at boot made
|
|
105
|
+
// a 401 terminal — the account has empty creds, so there was nothing to
|
|
106
|
+
// re-mint with and only a restart recovered.
|
|
101
107
|
if (!fetchproxyDisabled()) {
|
|
102
108
|
// CANVAS_BASE_URL is guaranteed valid here — loadAccount() validates it
|
|
103
109
|
// before throwing the NO_ENV_CONFIG_MARKER error we caught above.
|
|
@@ -113,60 +119,22 @@ export async function resolveAuth() {
|
|
|
113
119
|
: baseHost;
|
|
114
120
|
const name = readEnv('CANVAS_NAME') ?? baseHost;
|
|
115
121
|
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
'Sign into your Canvas instance in the browser ' +
|
|
133
|
-
'(with the fetchproxy extension installed) and retry.');
|
|
134
|
-
}
|
|
135
|
-
const cookie = `canvas_session=${canvasSession}; pseudonym_credentials=${pseudoCreds}`;
|
|
136
|
-
// Synthesize a session account with empty creds — the client will see
|
|
137
|
-
// `preloaded` and skip the form login. This mirrors the cookie shape
|
|
138
|
-
// the legacy session-scrape path produces, so everything downstream
|
|
139
|
-
// (Cookie header, 401-retry, file downloads) keeps working.
|
|
140
|
-
const account = {
|
|
141
|
-
mode: 'session',
|
|
142
|
-
name,
|
|
143
|
-
baseUrl: cleanBaseUrl,
|
|
144
|
-
username: '',
|
|
145
|
-
password: '',
|
|
146
|
-
};
|
|
147
|
-
return {
|
|
148
|
-
account,
|
|
149
|
-
preloaded: { cookie },
|
|
150
|
-
source: 'fetchproxy',
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
catch (e) {
|
|
154
|
-
// 0.8.0+ typed-error discrimination. The fetchproxy server already
|
|
155
|
-
// retries once on SW eviction (bridgeReviveDelayMs=2000 default), so
|
|
156
|
-
// a thrown FetchproxyBridgeDownError means the retry also failed —
|
|
157
|
-
// the extension's service worker is genuinely down and the user
|
|
158
|
-
// needs to wake it. The `.hint` is the actionable copy
|
|
159
|
-
// ("click the extension toolbar icon...") that we'd otherwise have
|
|
160
|
-
// to hand-write here. Surface it verbatim so users in path 4 get
|
|
161
|
-
// the same self-service guidance as path 5.
|
|
162
|
-
if (classifyBridgeError(e) === 'bridge_down') {
|
|
163
|
-
const downErr = e;
|
|
164
|
-
throw new Error(`Canvas auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`);
|
|
165
|
-
}
|
|
166
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
167
|
-
throw new Error(`Canvas auth: no CANVAS_TOKEN, CANVAS_CLIENT_*/CANVAS_REFRESH_TOKEN, or CANVAS_USERNAME/CANVAS_PASSWORD set, ` +
|
|
168
|
-
`and fetchproxy fallback failed: ${msg}`);
|
|
169
|
-
}
|
|
122
|
+
// Synthesized session account with empty creds — the client sees `refresh`
|
|
123
|
+
// and skips the form login. Cookie shape matches the legacy session-scrape
|
|
124
|
+
// path so everything downstream (Cookie header, 401 replay, downloads)
|
|
125
|
+
// keeps working.
|
|
126
|
+
const account = {
|
|
127
|
+
mode: 'session',
|
|
128
|
+
name,
|
|
129
|
+
baseUrl: cleanBaseUrl,
|
|
130
|
+
username: '',
|
|
131
|
+
password: '',
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
account,
|
|
135
|
+
refresh: () => liftBrowserCookie(declaredDomain, baseHost),
|
|
136
|
+
source: 'fetchproxy',
|
|
137
|
+
};
|
|
170
138
|
}
|
|
171
139
|
// ── Path 5: nothing configured and fetchproxy explicitly disabled.
|
|
172
140
|
throw new Error('Missing Canvas auth config. Set one of: CANVAS_TOKEN (personal access token), ' +
|
|
@@ -175,3 +143,64 @@ export async function resolveAuth() {
|
|
|
175
143
|
'or install the fetchproxy extension and sign into your Canvas instance ' +
|
|
176
144
|
'(unset CANVAS_DISABLE_FETCHPROXY if it is set).');
|
|
177
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* One lifter per declared domain, built lazily and reused.
|
|
148
|
+
*
|
|
149
|
+
* The scope is not static — self-hosted Canvas declares its literal hostname
|
|
150
|
+
* while *.instructure.com tenants share one wildcard — so the lifter cannot be
|
|
151
|
+
* a module-level constant. Caching is load-bearing rather than an
|
|
152
|
+
* optimization: `createSessionLifter` single-flights concurrent calls, and
|
|
153
|
+
* that only helps if renewals share ONE lifter instead of constructing a fresh
|
|
154
|
+
* one per call.
|
|
155
|
+
*/
|
|
156
|
+
const liftersByDomain = new Map();
|
|
157
|
+
function lifterFor(declaredDomain) {
|
|
158
|
+
let lift = liftersByDomain.get(declaredDomain);
|
|
159
|
+
if (!lift) {
|
|
160
|
+
lift = createSessionLifter({
|
|
161
|
+
serverName: pkg.name,
|
|
162
|
+
version: pkg.version,
|
|
163
|
+
domains: [declaredDomain],
|
|
164
|
+
declare: {
|
|
165
|
+
cookies: ['canvas_session', 'pseudonym_credentials'],
|
|
166
|
+
localStorage: [],
|
|
167
|
+
sessionStorage: [],
|
|
168
|
+
captureHeaders: [],
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
liftersByDomain.set(declaredDomain, lift);
|
|
172
|
+
}
|
|
173
|
+
return lift;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Lift a fresh Canvas session cookie out of the user's signed-in tab.
|
|
177
|
+
*
|
|
178
|
+
* Runs on every mint, not once at startup — fetchproxy is not in the request
|
|
179
|
+
* hot path, only the renewal path.
|
|
180
|
+
*/
|
|
181
|
+
async function liftBrowserCookie(declaredDomain, baseHost) {
|
|
182
|
+
try {
|
|
183
|
+
const session = await lifterFor(declaredDomain)();
|
|
184
|
+
const canvasSession = session.cookies['canvas_session'];
|
|
185
|
+
const pseudoCreds = session.cookies['pseudonym_credentials'];
|
|
186
|
+
if (!canvasSession || !pseudoCreds) {
|
|
187
|
+
throw new Error(`required cookies not found on ${baseHost}. ` +
|
|
188
|
+
'Sign into your Canvas instance in the browser ' +
|
|
189
|
+
'(with the fetchproxy extension installed) and retry.');
|
|
190
|
+
}
|
|
191
|
+
return `canvas_session=${canvasSession}; pseudonym_credentials=${pseudoCreds}`;
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
// 0.8.0+ typed-error discrimination. The fetchproxy server already retries
|
|
195
|
+
// once on SW eviction, so a thrown FetchproxyBridgeDownError means the
|
|
196
|
+
// retry also failed — the extension's service worker is genuinely down and
|
|
197
|
+
// the user needs to wake it. `.hint` is the actionable copy.
|
|
198
|
+
if (classifyBridgeError(e) === 'bridge_down') {
|
|
199
|
+
const downErr = e;
|
|
200
|
+
throw new Error(`Canvas auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`);
|
|
201
|
+
}
|
|
202
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
203
|
+
throw new Error(`Canvas auth: no CANVAS_TOKEN, CANVAS_CLIENT_*/CANVAS_REFRESH_TOKEN, or CANVAS_USERNAME/CANVAS_PASSWORD set, ` +
|
|
204
|
+
`and fetchproxy lift failed: ${msg}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
package/dist/bundle.js
CHANGED
|
@@ -26729,17 +26729,33 @@ function normalizeObjectSchema(schema) {
|
|
|
26729
26729
|
}
|
|
26730
26730
|
return void 0;
|
|
26731
26731
|
}
|
|
26732
|
+
function getDotPath(path) {
|
|
26733
|
+
if (path.length === 0) {
|
|
26734
|
+
return "object root";
|
|
26735
|
+
}
|
|
26736
|
+
return path.reduce((acc, seg, index) => {
|
|
26737
|
+
if (index === 0) {
|
|
26738
|
+
return String(seg);
|
|
26739
|
+
}
|
|
26740
|
+
if (typeof seg === "number") {
|
|
26741
|
+
return `${acc}[${seg}]`;
|
|
26742
|
+
}
|
|
26743
|
+
return `${acc}.${seg}`;
|
|
26744
|
+
}, "");
|
|
26745
|
+
}
|
|
26732
26746
|
function getParseErrorMessage(error51) {
|
|
26733
26747
|
if (error51 && typeof error51 === "object") {
|
|
26748
|
+
if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
|
|
26749
|
+
return error51.issues.map((i) => {
|
|
26750
|
+
if (!i.path?.length) {
|
|
26751
|
+
return i.message;
|
|
26752
|
+
}
|
|
26753
|
+
return `${i.message} at ${getDotPath(i.path)}`;
|
|
26754
|
+
}).join("\n");
|
|
26755
|
+
}
|
|
26734
26756
|
if ("message" in error51 && typeof error51.message === "string") {
|
|
26735
26757
|
return error51.message;
|
|
26736
26758
|
}
|
|
26737
|
-
if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
|
|
26738
|
-
const firstIssue = error51.issues[0];
|
|
26739
|
-
if (firstIssue && typeof firstIssue === "object" && "message" in firstIssue) {
|
|
26740
|
-
return String(firstIssue.message);
|
|
26741
|
-
}
|
|
26742
|
-
}
|
|
26743
26759
|
try {
|
|
26744
26760
|
return JSON.stringify(error51);
|
|
26745
26761
|
} catch {
|
|
@@ -33354,16 +33370,7 @@ var Server = class extends Protocol {
|
|
|
33354
33370
|
if (!methodSchema) {
|
|
33355
33371
|
throw new Error("Schema is missing a method literal");
|
|
33356
33372
|
}
|
|
33357
|
-
|
|
33358
|
-
if (isZ4Schema(methodSchema)) {
|
|
33359
|
-
const v4Schema = methodSchema;
|
|
33360
|
-
const v4Def = v4Schema._zod?.def;
|
|
33361
|
-
methodValue = v4Def?.value ?? v4Schema.value;
|
|
33362
|
-
} else {
|
|
33363
|
-
const v3Schema = methodSchema;
|
|
33364
|
-
const legacyDef = v3Schema._def;
|
|
33365
|
-
methodValue = legacyDef?.value ?? v3Schema.value;
|
|
33366
|
-
}
|
|
33373
|
+
const methodValue = getLiteralValue(methodSchema);
|
|
33367
33374
|
if (typeof methodValue !== "string") {
|
|
33368
33375
|
throw new Error("Schema method literal must be a string");
|
|
33369
33376
|
}
|
|
@@ -34551,8 +34558,17 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
34551
34558
|
import process3 from "node:process";
|
|
34552
34559
|
|
|
34553
34560
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
34561
|
+
var STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
34554
34562
|
var ReadBuffer = class {
|
|
34563
|
+
constructor(options) {
|
|
34564
|
+
this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE;
|
|
34565
|
+
}
|
|
34555
34566
|
append(chunk2) {
|
|
34567
|
+
const newSize = (this._buffer?.length ?? 0) + chunk2.length;
|
|
34568
|
+
if (newSize > this._maxBufferSize) {
|
|
34569
|
+
this.clear();
|
|
34570
|
+
throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`);
|
|
34571
|
+
}
|
|
34556
34572
|
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk2]) : chunk2;
|
|
34557
34573
|
}
|
|
34558
34574
|
readMessage() {
|
|
@@ -34580,18 +34596,24 @@ function serializeMessage(message) {
|
|
|
34580
34596
|
|
|
34581
34597
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
34582
34598
|
var StdioServerTransport = class {
|
|
34583
|
-
constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
|
|
34599
|
+
constructor(_stdin = process3.stdin, _stdout = process3.stdout, options) {
|
|
34584
34600
|
this._stdin = _stdin;
|
|
34585
34601
|
this._stdout = _stdout;
|
|
34586
|
-
this._readBuffer = new ReadBuffer();
|
|
34587
34602
|
this._started = false;
|
|
34588
34603
|
this._ondata = (chunk2) => {
|
|
34589
|
-
|
|
34590
|
-
|
|
34604
|
+
try {
|
|
34605
|
+
this._readBuffer.append(chunk2);
|
|
34606
|
+
this.processReadBuffer();
|
|
34607
|
+
} catch (error51) {
|
|
34608
|
+
this.onerror?.(error51);
|
|
34609
|
+
this.close().catch(() => {
|
|
34610
|
+
});
|
|
34611
|
+
}
|
|
34591
34612
|
};
|
|
34592
34613
|
this._onerror = (error51) => {
|
|
34593
34614
|
this.onerror?.(error51);
|
|
34594
34615
|
};
|
|
34616
|
+
this._readBuffer = new ReadBuffer({ maxBufferSize: options?.maxBufferSize });
|
|
34595
34617
|
}
|
|
34596
34618
|
/**
|
|
34597
34619
|
* Starts listening for messages on stdin.
|
|
@@ -38442,7 +38464,20 @@ var FetchproxyServer = class {
|
|
|
38442
38464
|
|
|
38443
38465
|
// node_modules/@fetchproxy/bootstrap/dist/index.js
|
|
38444
38466
|
var defaultFactory = (opts) => new FetchproxyServer(opts);
|
|
38445
|
-
|
|
38467
|
+
function createSessionLifter(opts) {
|
|
38468
|
+
let inFlight = null;
|
|
38469
|
+
return () => {
|
|
38470
|
+
if (inFlight)
|
|
38471
|
+
return inFlight;
|
|
38472
|
+
const p = runOneLift(opts).finally(() => {
|
|
38473
|
+
if (inFlight === p)
|
|
38474
|
+
inFlight = null;
|
|
38475
|
+
});
|
|
38476
|
+
inFlight = p;
|
|
38477
|
+
return p;
|
|
38478
|
+
};
|
|
38479
|
+
}
|
|
38480
|
+
async function runOneLift(opts) {
|
|
38446
38481
|
const envVar = opts.serverName.toUpperCase().replace(/[^A-Z0-9]/g, "_").replace(/^_+/, "") + "_DISABLE_FETCHPROXY";
|
|
38447
38482
|
const envVal = process.env[envVar];
|
|
38448
38483
|
if (envVal !== void 0 && envVal !== "" && envVal !== "0" && envVal !== "false") {
|
|
@@ -38569,12 +38604,18 @@ async function bootstrap(opts) {
|
|
|
38569
38604
|
});
|
|
38570
38605
|
indexedDbBucket[`${d.database}/${d.store}`] = values;
|
|
38571
38606
|
}
|
|
38607
|
+
const absent = (declared, got) => declared.filter((k) => !(k in got));
|
|
38572
38608
|
return {
|
|
38573
38609
|
cookies,
|
|
38574
38610
|
localStorage,
|
|
38575
38611
|
sessionStorage,
|
|
38576
38612
|
capturedHeaders,
|
|
38577
|
-
indexedDb: indexedDbBucket
|
|
38613
|
+
indexedDb: indexedDbBucket,
|
|
38614
|
+
missing: {
|
|
38615
|
+
cookies: absent(opts.declare.cookies, cookies),
|
|
38616
|
+
localStorage: absent(opts.declare.localStorage, localStorage),
|
|
38617
|
+
sessionStorage: absent(opts.declare.sessionStorage, sessionStorage)
|
|
38618
|
+
}
|
|
38578
38619
|
};
|
|
38579
38620
|
} finally {
|
|
38580
38621
|
try {
|
|
@@ -38679,7 +38720,7 @@ function loadAccount(env = process.env) {
|
|
|
38679
38720
|
// package.json
|
|
38680
38721
|
var package_default = {
|
|
38681
38722
|
name: "canvas-parent-mcp",
|
|
38682
|
-
version: "1.2.
|
|
38723
|
+
version: "1.2.3",
|
|
38683
38724
|
mcpName: "io.github.chrischall/canvas-parent-mcp",
|
|
38684
38725
|
description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
|
|
38685
38726
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38724,7 +38765,7 @@ var package_default = {
|
|
|
38724
38765
|
},
|
|
38725
38766
|
dependencies: {
|
|
38726
38767
|
"@chrischall/mcp-utils": "^0.14.0",
|
|
38727
|
-
"@fetchproxy/bootstrap": "^1.
|
|
38768
|
+
"@fetchproxy/bootstrap": "^1.9.1",
|
|
38728
38769
|
"@fetchproxy/server": "^1.7.0",
|
|
38729
38770
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38730
38771
|
dotenv: "^17.4.2",
|
|
@@ -38764,54 +38805,65 @@ async function resolveAuth() {
|
|
|
38764
38805
|
const declaredDomain = baseHost.endsWith(".instructure.com") ? "instructure.com" : baseHost;
|
|
38765
38806
|
const name = readEnv("CANVAS_NAME") ?? baseHost;
|
|
38766
38807
|
const cleanBaseUrl = baseUrl.replace(/\/$/, "");
|
|
38767
|
-
|
|
38768
|
-
|
|
38769
|
-
|
|
38770
|
-
|
|
38771
|
-
|
|
38772
|
-
|
|
38773
|
-
|
|
38774
|
-
|
|
38775
|
-
|
|
38776
|
-
|
|
38777
|
-
|
|
38778
|
-
|
|
38779
|
-
|
|
38780
|
-
|
|
38781
|
-
|
|
38782
|
-
|
|
38783
|
-
|
|
38784
|
-
|
|
38785
|
-
|
|
38786
|
-
|
|
38787
|
-
|
|
38788
|
-
|
|
38789
|
-
|
|
38790
|
-
|
|
38791
|
-
|
|
38792
|
-
|
|
38793
|
-
|
|
38794
|
-
|
|
38795
|
-
|
|
38796
|
-
|
|
38797
|
-
source: "fetchproxy"
|
|
38798
|
-
};
|
|
38799
|
-
} catch (e) {
|
|
38800
|
-
if (classifyBridgeError(e) === "bridge_down") {
|
|
38801
|
-
const downErr = e;
|
|
38802
|
-
throw new Error(
|
|
38803
|
-
`Canvas auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
|
|
38804
|
-
);
|
|
38808
|
+
const account = {
|
|
38809
|
+
mode: "session",
|
|
38810
|
+
name,
|
|
38811
|
+
baseUrl: cleanBaseUrl,
|
|
38812
|
+
username: "",
|
|
38813
|
+
password: ""
|
|
38814
|
+
};
|
|
38815
|
+
return {
|
|
38816
|
+
account,
|
|
38817
|
+
refresh: () => liftBrowserCookie(declaredDomain, baseHost),
|
|
38818
|
+
source: "fetchproxy"
|
|
38819
|
+
};
|
|
38820
|
+
}
|
|
38821
|
+
throw new Error(
|
|
38822
|
+
"Missing Canvas auth config. Set one of: CANVAS_TOKEN (personal access token), CANVAS_USERNAME+CANVAS_PASSWORD (auto-login), all three of CANVAS_CLIENT_ID, CANVAS_CLIENT_SECRET, CANVAS_REFRESH_TOKEN (OAuth), or install the fetchproxy extension and sign into your Canvas instance (unset CANVAS_DISABLE_FETCHPROXY if it is set)."
|
|
38823
|
+
);
|
|
38824
|
+
}
|
|
38825
|
+
var liftersByDomain = /* @__PURE__ */ new Map();
|
|
38826
|
+
function lifterFor(declaredDomain) {
|
|
38827
|
+
let lift = liftersByDomain.get(declaredDomain);
|
|
38828
|
+
if (!lift) {
|
|
38829
|
+
lift = createSessionLifter({
|
|
38830
|
+
serverName: package_default.name,
|
|
38831
|
+
version: package_default.version,
|
|
38832
|
+
domains: [declaredDomain],
|
|
38833
|
+
declare: {
|
|
38834
|
+
cookies: ["canvas_session", "pseudonym_credentials"],
|
|
38835
|
+
localStorage: [],
|
|
38836
|
+
sessionStorage: [],
|
|
38837
|
+
captureHeaders: []
|
|
38805
38838
|
}
|
|
38806
|
-
|
|
38839
|
+
});
|
|
38840
|
+
liftersByDomain.set(declaredDomain, lift);
|
|
38841
|
+
}
|
|
38842
|
+
return lift;
|
|
38843
|
+
}
|
|
38844
|
+
async function liftBrowserCookie(declaredDomain, baseHost) {
|
|
38845
|
+
try {
|
|
38846
|
+
const session = await lifterFor(declaredDomain)();
|
|
38847
|
+
const canvasSession = session.cookies["canvas_session"];
|
|
38848
|
+
const pseudoCreds = session.cookies["pseudonym_credentials"];
|
|
38849
|
+
if (!canvasSession || !pseudoCreds) {
|
|
38850
|
+
throw new Error(
|
|
38851
|
+
`required cookies not found on ${baseHost}. Sign into your Canvas instance in the browser (with the fetchproxy extension installed) and retry.`
|
|
38852
|
+
);
|
|
38853
|
+
}
|
|
38854
|
+
return `canvas_session=${canvasSession}; pseudonym_credentials=${pseudoCreds}`;
|
|
38855
|
+
} catch (e) {
|
|
38856
|
+
if (classifyBridgeError(e) === "bridge_down") {
|
|
38857
|
+
const downErr = e;
|
|
38807
38858
|
throw new Error(
|
|
38808
|
-
`Canvas auth:
|
|
38859
|
+
`Canvas auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
|
|
38809
38860
|
);
|
|
38810
38861
|
}
|
|
38862
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
38863
|
+
throw new Error(
|
|
38864
|
+
`Canvas auth: no CANVAS_TOKEN, CANVAS_CLIENT_*/CANVAS_REFRESH_TOKEN, or CANVAS_USERNAME/CANVAS_PASSWORD set, and fetchproxy lift failed: ${msg}`
|
|
38865
|
+
);
|
|
38811
38866
|
}
|
|
38812
|
-
throw new Error(
|
|
38813
|
-
"Missing Canvas auth config. Set one of: CANVAS_TOKEN (personal access token), CANVAS_USERNAME+CANVAS_PASSWORD (auto-login), all three of CANVAS_CLIENT_ID, CANVAS_CLIENT_SECRET, CANVAS_REFRESH_TOKEN (OAuth), or install the fetchproxy extension and sign into your Canvas instance (unset CANVAS_DISABLE_FETCHPROXY if it is set)."
|
|
38814
|
-
);
|
|
38815
38867
|
}
|
|
38816
38868
|
|
|
38817
38869
|
// src/client.ts
|
|
@@ -39049,28 +39101,35 @@ var DEFAULT_MAX_PAGES = 50;
|
|
|
39049
39101
|
var CanvasClient = class {
|
|
39050
39102
|
account;
|
|
39051
39103
|
sessionLoginFn;
|
|
39052
|
-
|
|
39104
|
+
/**
|
|
39105
|
+
* fetchproxy lift. Called on the first login AND on every renewal, which is
|
|
39106
|
+
* what keeps the path alive once the browser cookie lapses.
|
|
39107
|
+
*/
|
|
39108
|
+
refreshSession;
|
|
39053
39109
|
auth;
|
|
39054
39110
|
/** Lazily-built shared refresh_token exchanger (oauth mode only). */
|
|
39055
39111
|
oauthRefresh = null;
|
|
39056
39112
|
/**
|
|
39057
|
-
* `
|
|
39058
|
-
* the
|
|
39059
|
-
*
|
|
39060
|
-
*
|
|
39061
|
-
*
|
|
39062
|
-
*
|
|
39113
|
+
* `refreshSession` is the fetchproxy escape hatch: when set, it REPLACES
|
|
39114
|
+
* `sessionLogin()` as the way this client mints a session cookie. It is
|
|
39115
|
+
* called lazily on the first request and again on every 401, so a lapsed
|
|
39116
|
+
* browser cookie recovers by re-reading the tab instead of dead-ending.
|
|
39117
|
+
*
|
|
39118
|
+
* It must re-read the browser each time rather than return a captured
|
|
39119
|
+
* value — that capture-once shape is precisely what made a 401 terminal
|
|
39120
|
+
* here, with a restart the only cure.
|
|
39063
39121
|
*/
|
|
39064
39122
|
constructor(account, opts = {}) {
|
|
39065
39123
|
this.account = account;
|
|
39066
39124
|
this.sessionLoginFn = opts.sessionLogin ?? sessionLogin;
|
|
39067
|
-
this.
|
|
39125
|
+
this.refreshSession = opts.refreshSession ?? null;
|
|
39068
39126
|
this.auth = new CookieSessionManager({
|
|
39069
39127
|
login: () => this.login(),
|
|
39070
|
-
// Reactive expiry: only a 401 in a mode that can re-mint warrants a
|
|
39071
|
-
// token mode
|
|
39072
|
-
//
|
|
39073
|
-
//
|
|
39128
|
+
// Reactive expiry: only a 401 in a mode that can re-mint warrants a
|
|
39129
|
+
// replay. token mode still can't (no refresh path), and a session
|
|
39130
|
+
// account with neither a lift nor credentials still can't — those 401s
|
|
39131
|
+
// fall through as a Response and `mapStatus` turns them into a
|
|
39132
|
+
// TokenExpiredError.
|
|
39074
39133
|
isExpired: (res) => res.status === 401 && this.canReauth()
|
|
39075
39134
|
});
|
|
39076
39135
|
}
|
|
@@ -39178,7 +39237,7 @@ var CanvasClient = class {
|
|
|
39178
39237
|
canReauth() {
|
|
39179
39238
|
const acct = this.account;
|
|
39180
39239
|
if (acct.mode === "oauth") return true;
|
|
39181
|
-
if (acct.mode === "session") return !!acct.username && !!acct.password;
|
|
39240
|
+
if (acct.mode === "session") return !!this.refreshSession || !!acct.username && !!acct.password;
|
|
39182
39241
|
return false;
|
|
39183
39242
|
}
|
|
39184
39243
|
/** Mint the auth credential for the current mode. Invoked by the manager. */
|
|
@@ -39188,8 +39247,8 @@ var CanvasClient = class {
|
|
|
39188
39247
|
return { headers: { Authorization: `Bearer ${acct.token}` } };
|
|
39189
39248
|
}
|
|
39190
39249
|
if (acct.mode === "session") {
|
|
39191
|
-
if (this.
|
|
39192
|
-
return { headers: { Cookie: this.
|
|
39250
|
+
if (this.refreshSession !== null) {
|
|
39251
|
+
return { headers: { Cookie: await this.refreshSession() } };
|
|
39193
39252
|
}
|
|
39194
39253
|
return { headers: { Cookie: await this.mintSessionCookie(acct) } };
|
|
39195
39254
|
}
|
|
@@ -39655,13 +39714,13 @@ try {
|
|
|
39655
39714
|
}
|
|
39656
39715
|
await runMcp({
|
|
39657
39716
|
name: "canvas",
|
|
39658
|
-
version: "1.2.
|
|
39717
|
+
version: "1.2.3",
|
|
39659
39718
|
// x-release-please-version
|
|
39660
39719
|
tools: [
|
|
39661
39720
|
(server) => {
|
|
39662
39721
|
if (resolved) {
|
|
39663
39722
|
const client = new CanvasClient(resolved.account, {
|
|
39664
|
-
|
|
39723
|
+
refreshSession: resolved.refresh
|
|
39665
39724
|
});
|
|
39666
39725
|
registerProfileTools(server, client);
|
|
39667
39726
|
registerObserveeTools(server, client);
|
package/dist/client.js
CHANGED
|
@@ -13,28 +13,35 @@ const DEFAULT_MAX_PAGES = 50;
|
|
|
13
13
|
export class CanvasClient {
|
|
14
14
|
account;
|
|
15
15
|
sessionLoginFn;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* fetchproxy lift. Called on the first login AND on every renewal, which is
|
|
18
|
+
* what keeps the path alive once the browser cookie lapses.
|
|
19
|
+
*/
|
|
20
|
+
refreshSession;
|
|
17
21
|
auth;
|
|
18
22
|
/** Lazily-built shared refresh_token exchanger (oauth mode only). */
|
|
19
23
|
oauthRefresh = null;
|
|
20
24
|
/**
|
|
21
|
-
* `
|
|
22
|
-
* the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* `refreshSession` is the fetchproxy escape hatch: when set, it REPLACES
|
|
26
|
+
* `sessionLogin()` as the way this client mints a session cookie. It is
|
|
27
|
+
* called lazily on the first request and again on every 401, so a lapsed
|
|
28
|
+
* browser cookie recovers by re-reading the tab instead of dead-ending.
|
|
29
|
+
*
|
|
30
|
+
* It must re-read the browser each time rather than return a captured
|
|
31
|
+
* value — that capture-once shape is precisely what made a 401 terminal
|
|
32
|
+
* here, with a restart the only cure.
|
|
27
33
|
*/
|
|
28
34
|
constructor(account, opts = {}) {
|
|
29
35
|
this.account = account;
|
|
30
36
|
this.sessionLoginFn = opts.sessionLogin ?? defaultSessionLogin;
|
|
31
|
-
this.
|
|
37
|
+
this.refreshSession = opts.refreshSession ?? null;
|
|
32
38
|
this.auth = new CookieSessionManager({
|
|
33
39
|
login: () => this.login(),
|
|
34
|
-
// Reactive expiry: only a 401 in a mode that can re-mint warrants a
|
|
35
|
-
// token mode
|
|
36
|
-
//
|
|
37
|
-
//
|
|
40
|
+
// Reactive expiry: only a 401 in a mode that can re-mint warrants a
|
|
41
|
+
// replay. token mode still can't (no refresh path), and a session
|
|
42
|
+
// account with neither a lift nor credentials still can't — those 401s
|
|
43
|
+
// fall through as a Response and `mapStatus` turns them into a
|
|
44
|
+
// TokenExpiredError.
|
|
38
45
|
isExpired: (res) => res.status === 401 && this.canReauth(),
|
|
39
46
|
});
|
|
40
47
|
}
|
|
@@ -157,11 +164,12 @@ export class CanvasClient {
|
|
|
157
164
|
const acct = this.account;
|
|
158
165
|
if (acct.mode === 'oauth')
|
|
159
166
|
return true;
|
|
160
|
-
// session:
|
|
161
|
-
//
|
|
162
|
-
//
|
|
167
|
+
// session: either a browser lift (fetchproxy) or real form credentials
|
|
168
|
+
// can re-mint. The lift is the fetchproxy path's re-auth — before it
|
|
169
|
+
// existed, that path synthesized empty username/password and a captured
|
|
170
|
+
// cookie, so its 401 was terminal and only a restart recovered.
|
|
163
171
|
if (acct.mode === 'session')
|
|
164
|
-
return !!acct.username && !!acct.password;
|
|
172
|
+
return !!this.refreshSession || (!!acct.username && !!acct.password);
|
|
165
173
|
return false; // token mode: no refresh path.
|
|
166
174
|
}
|
|
167
175
|
/** Mint the auth credential for the current mode. Invoked by the manager. */
|
|
@@ -171,11 +179,10 @@ export class CanvasClient {
|
|
|
171
179
|
return { headers: { Authorization: `Bearer ${acct.token}` } };
|
|
172
180
|
}
|
|
173
181
|
if (acct.mode === 'session') {
|
|
174
|
-
// fetchproxy path:
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return { headers: { Cookie: this.preloadedCookie } };
|
|
182
|
+
// fetchproxy path: re-read the browser instead of posting a form login.
|
|
183
|
+
// Runs on every mint, not just the first, so an expiry renews.
|
|
184
|
+
if (this.refreshSession !== null) {
|
|
185
|
+
return { headers: { Cookie: await this.refreshSession() } };
|
|
179
186
|
}
|
|
180
187
|
return { headers: { Cookie: await this.mintSessionCookie(acct) } };
|
|
181
188
|
}
|
package/dist/index.js
CHANGED
|
@@ -46,12 +46,12 @@ catch (e) {
|
|
|
46
46
|
// host's install-time `tools/list` succeeds.
|
|
47
47
|
await runMcp({
|
|
48
48
|
name: 'canvas',
|
|
49
|
-
version: '1.2.
|
|
49
|
+
version: '1.2.3', // x-release-please-version
|
|
50
50
|
tools: [
|
|
51
51
|
(server) => {
|
|
52
52
|
if (resolved) {
|
|
53
53
|
const client = new CanvasClient(resolved.account, {
|
|
54
|
-
|
|
54
|
+
refreshSession: resolved.refresh,
|
|
55
55
|
});
|
|
56
56
|
registerProfileTools(server, client);
|
|
57
57
|
registerObserveeTools(server, client);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvas-parent-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
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>",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@chrischall/mcp-utils": "^0.14.0",
|
|
48
|
-
"@fetchproxy/bootstrap": "^1.
|
|
48
|
+
"@fetchproxy/bootstrap": "^1.9.1",
|
|
49
49
|
"@fetchproxy/server": "^1.7.0",
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
51
51
|
"dotenv": "^17.4.2",
|
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.2.
|
|
9
|
+
"version": "1.2.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "canvas-parent-mcp",
|
|
14
|
-
"version": "1.2.
|
|
14
|
+
"version": "1.2.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|