brightspace-mcp-server 5.1.23 → 5.1.24

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.
@@ -23,6 +23,13 @@ function richTextHtml(field) {
23
23
  const nested = typeof field.Text === "object" && field.Text !== null ? field.Text.Html : null;
24
24
  return nested ?? field.Html ?? null;
25
25
  }
26
+ /** The highest Score among attempts D2L has scored, or null when none has one. */
27
+ function bestAttemptScore(attempts) {
28
+ const scores = (attempts ?? [])
29
+ .map((a) => a.Score)
30
+ .filter((score) => typeof score === "number");
31
+ return scores.length > 0 ? Math.max(...scores) : null;
32
+ }
26
33
  /**
27
34
  * The grade types a student is actually scored on. D2L documents exactly four
28
35
  * GradeObject shapes, told apart by a string GradeType; anything else a
@@ -764,9 +771,9 @@ async function fetchCourseInventory(apiClient, courseId, baseUrl) {
764
771
  attemptsUsed: completedAttempts?.length ?? null,
765
772
  attemptsRemaining,
766
773
  attemptWarning,
767
- bestScore: completedAttempts && completedAttempts.length > 0
768
- ? Math.max(...completedAttempts.map((a) => a.Score ?? 0))
769
- : null,
774
+ // An attempt awaiting manual grading carries Score null: it is not a
775
+ // zero, so only scored attempts count toward the best (issue #370).
776
+ bestScore: bestAttemptScore(completedAttempts),
770
777
  gracePeriodMinutes: quiz.SubmissionGracePeriod ?? null,
771
778
  hasPassword: Boolean(quiz.Password),
772
779
  // Additive fields, each emitted only when it has something to say.
@@ -59,6 +59,13 @@ function quickLinkFilePath(input, courseId, origin) {
59
59
  function enforcedFilePath(input, courseId, origin) {
60
60
  let path = input;
61
61
  if (!path.startsWith("/")) {
62
+ // Check the raw authority before URL parsing can normalize it. The first
63
+ // literal slash must start the path, not occur inside a query or fragment.
64
+ const authorityStart = path.indexOf("//") + 2;
65
+ const pathStart = path.indexOf("/", authorityStart);
66
+ if (pathStart <= authorityStart ||
67
+ /[?#\\\x00-\x20\x7f]/.test(path.slice(authorityStart, pathStart)))
68
+ return null;
62
69
  let link;
63
70
  try {
64
71
  link = new URL(path);
@@ -68,7 +75,7 @@ function enforcedFilePath(input, courseId, origin) {
68
75
  }
69
76
  if (!origin || link.origin !== new URL(origin).origin || link.username || link.password)
70
77
  return null;
71
- path = path.slice(path.indexOf("/", path.indexOf("//") + 2));
78
+ path = path.slice(pathStart);
72
79
  }
73
80
  if (!path.startsWith(ENFORCED_PREFIX))
74
81
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "5.1.23",
3
+ "version": "5.1.24",
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",