brightspace-mcp-server 3.7.4 → 3.7.6
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/build/api/client.js +3 -7
- package/build/api/retry.js +22 -0
- package/build/utils/config.js +24 -7
- package/package.json +1 -1
package/build/api/client.js
CHANGED
|
@@ -8,7 +8,7 @@ import { TTLCache } from "./cache.js";
|
|
|
8
8
|
import { TokenBucket } from "./rate-limiter.js";
|
|
9
9
|
import { discoverVersions } from "./version-discovery.js";
|
|
10
10
|
import { ApiError, RateLimitError, NetworkError } from "./errors.js";
|
|
11
|
-
import { withRetry, isRetryableFailure, retryAfterMsFrom } from "./retry.js";
|
|
11
|
+
import { withRetry, isRetryableFailure, retryAfterMsFrom, parseRetryAfter, } from "./retry.js";
|
|
12
12
|
import { log } from "../utils/logger.js";
|
|
13
13
|
import { AUTH_COMMAND } from "../utils/commands.js";
|
|
14
14
|
/** An ordinary course HTML link to the login page is not an expired session. */
|
|
@@ -306,9 +306,7 @@ export class D2LApiClient {
|
|
|
306
306
|
}
|
|
307
307
|
// Handle 429 rate limiting
|
|
308
308
|
if (response.status === 429) {
|
|
309
|
-
|
|
310
|
-
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : undefined;
|
|
311
|
-
throw new RateLimitError(path, retryAfterSeconds);
|
|
309
|
+
throw new RateLimitError(path, parseRetryAfter(response.headers.get("Retry-After")));
|
|
312
310
|
}
|
|
313
311
|
// Handle 403 (common for past-semester courses)
|
|
314
312
|
if (response.status === 403) {
|
|
@@ -371,9 +369,7 @@ export class D2LApiClient {
|
|
|
371
369
|
}
|
|
372
370
|
// Handle 429 rate limiting
|
|
373
371
|
if (response.status === 429) {
|
|
374
|
-
|
|
375
|
-
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : undefined;
|
|
376
|
-
throw new RateLimitError(path, retryAfterSeconds);
|
|
372
|
+
throw new RateLimitError(path, parseRetryAfter(response.headers.get("Retry-After")));
|
|
377
373
|
}
|
|
378
374
|
// Handle 403 (common for past-semester courses or no access)
|
|
379
375
|
if (response.status === 403) {
|
package/build/api/retry.js
CHANGED
|
@@ -40,6 +40,28 @@ export function isRetryableFailure(error) {
|
|
|
40
40
|
return error.status >= 500 && error.status <= 599;
|
|
41
41
|
return error instanceof NetworkError;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Read a Retry-After header as whole seconds, or undefined when it asks for
|
|
45
|
+
* no wait. RFC 9110 allows delta-seconds ("120") or an HTTP-date; a date is
|
|
46
|
+
* measured from `now` and rounded up. Anything else is ignored so the normal
|
|
47
|
+
* backoff applies.
|
|
48
|
+
*/
|
|
49
|
+
export function parseRetryAfter(value, now = Date.now()) {
|
|
50
|
+
const trimmed = value?.trim();
|
|
51
|
+
if (!trimmed)
|
|
52
|
+
return undefined;
|
|
53
|
+
let seconds;
|
|
54
|
+
if (/^\d+$/.test(trimmed)) {
|
|
55
|
+
seconds = Number(trimmed);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const at = Date.parse(trimmed);
|
|
59
|
+
if (Number.isNaN(at))
|
|
60
|
+
return undefined;
|
|
61
|
+
seconds = Math.ceil((at - now) / 1000);
|
|
62
|
+
}
|
|
63
|
+
return seconds > 0 ? seconds : undefined;
|
|
64
|
+
}
|
|
43
65
|
/** The Retry-After a 429 carried, in milliseconds, or undefined. */
|
|
44
66
|
export function retryAfterMsFrom(error) {
|
|
45
67
|
if (error instanceof RateLimitError && typeof error.retryAfter === "number" && error.retryAfter > 0) {
|
package/build/utils/config.js
CHANGED
|
@@ -26,9 +26,9 @@ export async function loadConfig() {
|
|
|
26
26
|
? expandTilde(store.sessionDir)
|
|
27
27
|
: path.join(os.homedir(), ".d2l-session");
|
|
28
28
|
// Code-entry and other interactive MFA methods need a visible browser.
|
|
29
|
-
const headless = process.env.D2L_HEADLESS
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const headless = envBoolean(process.env.D2L_HEADLESS, "D2L_HEADLESS")
|
|
30
|
+
?? store?.headless
|
|
31
|
+
?? true;
|
|
32
32
|
// Resolve tokenTtl: env > store > default (3600)
|
|
33
33
|
const tokenTtl = positiveSeconds(process.env.D2L_TOKEN_TTL, "D2L_TOKEN_TTL")
|
|
34
34
|
?? positiveSeconds(store?.tokenTtl, "tokenTtl in config.json")
|
|
@@ -42,10 +42,9 @@ export async function loadConfig() {
|
|
|
42
42
|
? process.env.D2L_EXCLUDE_COURSES.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n))
|
|
43
43
|
: store?.excludeCourses;
|
|
44
44
|
// Resolve activeOnly: env > store > default (true)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
45
|
+
const activeOnly = envBoolean(process.env.D2L_ACTIVE_ONLY, "D2L_ACTIVE_ONLY")
|
|
46
|
+
?? store?.activeOnly
|
|
47
|
+
?? true;
|
|
49
48
|
const configuredUrl = new URL(process.env.D2L_BASE_URL || store?.baseUrl || "https://purdue.brightspace.com");
|
|
50
49
|
if (configuredUrl.protocol !== "https:" || configuredUrl.username || configuredUrl.password) {
|
|
51
50
|
throw new Error("The Brightspace URL must be an HTTPS school URL without embedded credentials.");
|
|
@@ -93,6 +92,24 @@ function positiveSeconds(value, source) {
|
|
|
93
92
|
console.error(`[config] Ignoring ${source}=${JSON.stringify(value)}: expected a positive whole number of seconds`);
|
|
94
93
|
return undefined;
|
|
95
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* An on/off environment variable. Comparing against the exact string "false"
|
|
97
|
+
* read "0", "no", "False" and a typo as true, so D2L_HEADLESS=0 kept the
|
|
98
|
+
* browser hidden from a user who needed it to enter an MFA code. An empty
|
|
99
|
+
* value counts as unset, and anything unrecognized is ignored with a warning
|
|
100
|
+
* so config.json or the default applies, as positiveSeconds does.
|
|
101
|
+
*/
|
|
102
|
+
function envBoolean(value, source) {
|
|
103
|
+
const text = value?.trim().toLowerCase();
|
|
104
|
+
if (!text)
|
|
105
|
+
return undefined;
|
|
106
|
+
if (["true", "1", "yes", "on"].includes(text))
|
|
107
|
+
return true;
|
|
108
|
+
if (["false", "0", "no", "off"].includes(text))
|
|
109
|
+
return false;
|
|
110
|
+
console.error(`[config] Ignoring ${source}=${JSON.stringify(value)}: expected true or false`);
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
96
113
|
function expandTilde(filePath) {
|
|
97
114
|
if (filePath.startsWith("~")) {
|
|
98
115
|
return path.join(os.homedir(), filePath.slice(1));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.6",
|
|
4
4
|
"mcpName": "io.github.rohanmuppa/brightspace",
|
|
5
5
|
"description": "MCP server for Brightspace (D2L). Check grades, due dates, assignments, announcements, syllabus, rosters and more via Claude, ChatGPT, Cursor, Windsurf, or any MCP client.",
|
|
6
6
|
"type": "module",
|