brightspace-mcp-server 5.0.13 → 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.
@@ -3,6 +3,47 @@ import { randomUUID } from "node:crypto";
3
3
  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
+ 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
+ }
6
47
  const calls = new AsyncLocalStorage();
7
48
  const runId = randomUUID();
8
49
  const MAX_BYTES = 5 * 1024 * 1024;
@@ -42,6 +83,10 @@ function write(event, fields, context = calls.getStore(), idleMs, sinceSuccessMs
42
83
  record.reason = fields.reason;
43
84
  if (AUTH_PHASES.includes(fields.phase ?? ""))
44
85
  record.phase = fields.phase;
86
+ if (ERROR_KINDS.includes(fields.errorKind ?? ""))
87
+ record.errorKind = fields.errorKind;
88
+ if (OPERATION_NAMES.includes(fields.operation ?? ""))
89
+ record.operation = fields.operation;
45
90
  try {
46
91
  mkdirSync(directory, { recursive: true, mode: 0o700 });
47
92
  const file = join(directory, `activity-${at.slice(0, 10)}-${process.pid}-${runId}.jsonl`);
@@ -77,16 +122,24 @@ export function trackToolActivity(server) {
77
122
  lastStart = started;
78
123
  return calls.run(context, async () => {
79
124
  let outcome = "error";
125
+ let kind;
80
126
  try {
81
127
  const result = await callback(...args);
82
- if (!result?.isError) {
128
+ if (result?.isError) {
129
+ kind = resultErrorKind(result);
130
+ }
131
+ else {
83
132
  outcome = "success";
84
133
  lastSuccess = Date.now();
85
134
  }
86
135
  return result;
87
136
  }
137
+ catch (error) {
138
+ kind = errorKind(error);
139
+ throw error;
140
+ }
88
141
  finally {
89
- write("tool_finished", { outcome, elapsedMs: Date.now() - started });
142
+ write("tool_finished", { outcome, errorKind: kind, elapsedMs: Date.now() - started });
90
143
  }
91
144
  });
92
145
  })));
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Purdue Brightspace MCP Server
3
+ * Copyright (c) 2026 Rohan Muppa. All rights reserved.
4
+ * Licensed under MIT — see LICENSE file for details.
5
+ *
6
+ * A diagnostic category for a failed tool call, chosen from a closed set this
7
+ * package controls. Derived from the error's type and status only, never from
8
+ * its message, so the dev activity log can say why a call failed without
9
+ * recording paths, URLs, arguments, or content.
10
+ */
11
+ import { ZodError } from "zod";
12
+ import { ApiError, NetworkError } from "../api/errors.js";
13
+ import { AuthProcessError } from "../auth/auth-runner.js";
14
+ import { DownloadError } from "./download-errors.js";
15
+ export const ERROR_KINDS = ["invalidInput", "destination", "notFound", "permission", "auth", "rateLimited", "http",
16
+ "transport", "providerBlocked", "unsupportedFormat", "rejectedFile", "unknown"];
17
+ const API_STATUS_KINDS = {
18
+ 401: "auth", 403: "permission", 404: "notFound", 429: "rateLimited",
19
+ };
20
+ export function errorKind(error) {
21
+ if (error instanceof ZodError)
22
+ return "invalidInput";
23
+ if (error instanceof AuthProcessError)
24
+ return "auth";
25
+ if (error instanceof ApiError)
26
+ return API_STATUS_KINDS[error.status] ?? "http";
27
+ if (error instanceof NetworkError)
28
+ return "transport";
29
+ if (error instanceof DownloadError) {
30
+ return error.kind === "unsupportedType" || error.kind === "undetectableType" ? "unsupportedFormat" : "rejectedFile";
31
+ }
32
+ return "unknown";
33
+ }
34
+ const resultKinds = new WeakMap();
35
+ /** Remembers which error a returned tool result describes. */
36
+ export function withErrorKind(result, error) {
37
+ return tagErrorKind(result, errorKind(error));
38
+ }
39
+ /** Records the category of an error result a tool built itself. */
40
+ export function tagErrorKind(result, kind) {
41
+ resultKinds.set(result, kind);
42
+ return result;
43
+ }
44
+ /** The category of a returned error result; "unknown" when no error was attached. */
45
+ export function resultErrorKind(result) {
46
+ return (typeof result === "object" && result !== null && resultKinds.get(result)) || "unknown";
47
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Purdue Brightspace MCP Server
3
+ * Copyright (c) 2026 Rohan Muppa. All rights reserved.
4
+ * Licensed under MIT — see LICENSE file for details.
5
+ */
6
+ import { log } from "./logger.js";
7
+ /** How long a phase may run before one "still waiting" warning is logged. */
8
+ const SLOW_PHASE_MS = 10_000;
9
+ /**
10
+ * Run one startup step with timestamped stderr lines around it: one before
11
+ * the work starts, one with its duration when it ends, and a single warning
12
+ * if it is still running after SLOW_PHASE_MS. A client whose initialize
13
+ * request times out can then see which step the server was stuck in.
14
+ *
15
+ * The phase name is printed verbatim, so it must never carry a username,
16
+ * URL, or any other account detail.
17
+ */
18
+ export async function startupPhase(name, work) {
19
+ log("INFO", `Startup: ${name}...`);
20
+ const started = Date.now();
21
+ const slow = setTimeout(() => {
22
+ log("WARN", `Startup: still ${name} after ${SLOW_PHASE_MS / 1000} s`);
23
+ }, SLOW_PHASE_MS);
24
+ slow.unref?.();
25
+ try {
26
+ const result = await work();
27
+ log("INFO", `Startup: ${name} done in ${Date.now() - started} ms`);
28
+ return result;
29
+ }
30
+ catch (error) {
31
+ log("WARN", `Startup: ${name} failed after ${Date.now() - started} ms`);
32
+ throw error;
33
+ }
34
+ finally {
35
+ clearTimeout(slow);
36
+ }
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "5.0.13",
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",