brightspace-mcp-server 5.1.4 → 5.1.5

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.
@@ -11,7 +11,7 @@ import { ApiError, RateLimitError, NetworkError } from "./errors.js";
11
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
- import { devActivity } from "../utils/dev-activity.js";
14
+ import { devActivity, httpOperation } from "../utils/dev-activity.js";
15
15
  /**
16
16
  * True when a followed redirect ended on this Brightspace's sign-in page. A
17
17
  * web route asked with a dead cookie 302s there rather than serving the
@@ -426,8 +426,8 @@ export class D2LApiClient {
426
426
  return send(loggedIn);
427
427
  }
428
428
  /** Buckets one observed HTTP status into statsData.statusClasses. */
429
- recordStatus(status) {
430
- devActivity("http_response", { status });
429
+ recordStatus(status, path) {
430
+ devActivity("http_response", { status, operation: httpOperation(path) });
431
431
  const classes = this.statsData.statusClasses;
432
432
  if (status >= 200 && status < 300)
433
433
  classes["2xx"]++;
@@ -490,7 +490,7 @@ export class D2LApiClient {
490
490
  headers,
491
491
  signal: AbortSignal.timeout(this.timeoutMs),
492
492
  });
493
- this.recordStatus(response.status);
493
+ this.recordStatus(response.status, path);
494
494
  // Preserve cookie material: a 401 only rejects this access token.
495
495
  if (response.status === 401) {
496
496
  throw new ApiError(401, path, "Brightspace rejected the access token.");
@@ -558,7 +558,7 @@ export class D2LApiClient {
558
558
  headers,
559
559
  signal: idle.signal,
560
560
  });
561
- this.recordStatus(response.status);
561
+ this.recordStatus(response.status, path);
562
562
  // Preserve cookie material for the shared HTTP refresh path.
563
563
  if (response.status === 401) {
564
564
  throw new ApiError(401, path, "Brightspace rejected the access token.");
@@ -4,6 +4,46 @@ import { appendFileSync, mkdirSync, statSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { AUTH_PHASES } from "../auth/auth-phases.js";
6
6
  import { ERROR_KINDS, errorKind, resultErrorKind } from "./error-kind.js";
7
+ const API = String.raw `^/d2l/api/l[pe]/[^/]+/`;
8
+ const COURSE = API + String.raw `\d+/`;
9
+ /** First match wins; a path that matches none is "other". Matched on the path alone, query dropped. */
10
+ const HTTP_OPERATIONS = [
11
+ ["versions", "^/d2l/api/versions/"],
12
+ ["whoami", API + "users/whoami"],
13
+ ["enrollments", API + "enrollments/"],
14
+ ["groups", COURSE + String.raw `groupcategories/\d+/groups/`],
15
+ ["groupCategories", COURSE + "groupcategories/"],
16
+ ["ltiLinks", API + "lti/link/"],
17
+ ["quizList", COURSE + "quizzes/?$"],
18
+ ["quizAttempts", COURSE + String.raw `quizzes/\d+/attempts/`],
19
+ ["quiz", COURSE + "quizzes/"],
20
+ ["dropboxFolders", COURSE + "dropbox/folders/?$"],
21
+ ["dropboxSubmissions", COURSE + String.raw `dropbox/folders/\d+/submissions/`],
22
+ ["dropboxFeedback", COURSE + String.raw `dropbox/folders/\d+/feedback/`],
23
+ ["dropboxAttachment", COURSE + String.raw `dropbox/folders/\d+/attachments/`],
24
+ ["dropboxFolder", COURSE + "dropbox/folders/"],
25
+ ["dropboxFeedbackPage", "^/d2l/lms/dropbox/user/folder_user_view_feedback"],
26
+ ["rubrics", COURSE + "rubrics"],
27
+ ["grades", COURSE + "grades/"],
28
+ ["contentFile", COURSE + String.raw `content/topics/\d+/file`],
29
+ ["content", COURSE + "content/"],
30
+ ["newsAttachment", COURSE + String.raw `news/\d+/attachments/`],
31
+ ["news", COURSE + "news/"],
32
+ ["discussions", COURSE + "discussions/"],
33
+ ["overviewAttachment", COURSE + "overview/attachment"],
34
+ ["overview", COURSE + "overview"],
35
+ ["classlist", COURSE + "classlist/"],
36
+ ["quickLink", "^/d2l/common/dialogs/quickLink/"],
37
+ // The LTI launch page a video quickLink leads to (get_video_transcript).
38
+ ["ltiLaunch", String.raw `^/d2l/le/lti/\d+/toolLaunch/`],
39
+ ];
40
+ const OPERATION_PATTERNS = HTTP_OPERATIONS.map(([name, pattern]) => [name, new RegExp(pattern)]);
41
+ const OPERATION_NAMES = [...HTTP_OPERATIONS.map(([name]) => name), "other"];
42
+ /** Names what a request was for without keeping its endpoint, IDs or query. */
43
+ export function httpOperation(path) {
44
+ const bare = path.split(/[?#]/, 1)[0];
45
+ return OPERATION_PATTERNS.find(([, pattern]) => pattern.test(bare))?.[0] ?? "other";
46
+ }
7
47
  const calls = new AsyncLocalStorage();
8
48
  const runId = randomUUID();
9
49
  const MAX_BYTES = 5 * 1024 * 1024;
@@ -45,6 +85,8 @@ function write(event, fields, context = calls.getStore(), idleMs, sinceSuccessMs
45
85
  record.phase = fields.phase;
46
86
  if (ERROR_KINDS.includes(fields.errorKind ?? ""))
47
87
  record.errorKind = fields.errorKind;
88
+ if (OPERATION_NAMES.includes(fields.operation ?? ""))
89
+ record.operation = fields.operation;
48
90
  try {
49
91
  mkdirSync(directory, { recursive: true, mode: 0o700 });
50
92
  const file = join(directory, `activity-${at.slice(0, 10)}-${process.pid}-${runId}.jsonl`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "5.1.4",
3
+ "version": "5.1.5",
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",