brightspace-mcp-server 3.9.14 → 3.9.15

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.
@@ -129,8 +129,14 @@ export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
129
129
  // Skip hidden folders
130
130
  if (folder.IsHidden)
131
131
  continue;
132
- // Fetch submissions for this folder
132
+ // Fetch submissions for this folder. Some tenants deny students access
133
+ // to mysubmissions outright (403), in which case an empty list would be
134
+ // indistinguishable from genuinely unsubmitted work — track whether the
135
+ // answer is actually known so the caller never reports denied-but-real
136
+ // work as missing.
137
+ // Adapted from JoshuaMontclair/brightspace-mcp-server (MIT).
133
138
  let submissions = [];
139
+ let submissionsKnown = true;
134
140
  try {
135
141
  const submissionsRaw = await apiClient.get(apiClient.le(courseId, `/dropbox/folders/${folder.Id}/submissions/mysubmissions/`), { ttl: DEFAULT_CACHE_TTLS.assignments });
136
142
  submissions = Array.isArray(submissionsRaw) ? submissionsRaw : submissionsRaw.Objects ?? [];
@@ -138,9 +144,10 @@ export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
138
144
  catch (error) {
139
145
  if (isAuthUnavailable(error))
140
146
  throw error;
141
- // 404 means no submissions yet - that's fine
147
+ // 404 is how D2L reports "nothing submitted yet" - that one is known.
142
148
  if (error?.status !== 404) {
143
- log("DEBUG", `Failed to fetch submissions for folder ${folder.Id}`, error);
149
+ submissionsKnown = false;
150
+ log("DEBUG", `Could not determine submission state for folder ${folder.Id}`, error);
144
151
  }
145
152
  }
146
153
  // Fetch feedback independently of submissions
@@ -191,6 +198,19 @@ export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
191
198
  })) ?? [],
192
199
  })) ?? [],
193
200
  })) ?? null,
201
+ // New, additive fields: existing "submission" stays null either way
202
+ // when nothing is known, but submissionStatus tells apart "nothing
203
+ // was submitted" (404, a known answer) from "the answer is denied"
204
+ // (any other error), so a caller never states real work as missing.
205
+ submissionStatus: !submissionsKnown
206
+ ? "unknown"
207
+ : submissions.length > 0
208
+ ? "submitted"
209
+ : "not_submitted",
210
+ ...(submissionsKnown ? {} : {
211
+ submissionStatusNote: "Brightspace did not return this assignment's submission list, so whether it was submitted " +
212
+ "is unknown. Do not report it as missing or unsubmitted - tell the user to check Brightspace directly.",
213
+ }),
194
214
  submission: submissions.length > 0
195
215
  ? {
196
216
  submittedDate: submissions[0].SubmissionDate,
@@ -311,6 +331,16 @@ export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
311
331
  // False when the tenant refused the attempts endpoint, in which case
312
332
  // every count below is null rather than a guess of zero.
313
333
  attemptsAvailable: completedAttempts !== null,
334
+ // New, additive field mirroring submissionStatus above: "known" is
335
+ // completedAttempts !== null (the same condition attemptsAvailable
336
+ // already tracks), named explicitly so a caller does not have to
337
+ // infer "unknown" from a boolean meant for something else.
338
+ // Adapted from JoshuaMontclair/brightspace-mcp-server (MIT).
339
+ attemptStatus: completedAttempts !== null ? "known" : "unknown",
340
+ ...(completedAttempts !== null ? {} : {
341
+ attemptStatusNote: "Brightspace did not provide this quiz's attempt data, so it is unknown whether it was taken. " +
342
+ "Do not report it as not attempted.",
343
+ }),
314
344
  attemptsUsed: completedAttempts?.length ?? null,
315
345
  attemptsRemaining,
316
346
  attemptWarning,
@@ -375,7 +405,8 @@ function gradebookHeadsUp(raw, fetched, courseId, baseUrl) {
375
405
  export function registerGetAssignments(server, apiClient, config) {
376
406
  server.registerTool("get_assignments", {
377
407
  title: "Get Assignments",
378
- description: "Fetch assignments and quizzes for a specific course or all enrolled courses. Shows dropbox submissions and quizzes with due dates, status, and rubric info. Use this when the user asks about assignments, homework, what to submit, quizzes, or assignment details and rubrics.",
408
+ description: "Fetch assignments and quizzes for a specific course or all enrolled courses. Shows dropbox submissions and quizzes with due dates, status, and rubric info. Use this when the user asks about assignments, homework, what to submit, quizzes, or assignment details and rubrics. " +
409
+ "Read submissionStatus/attemptStatus, not just submission/attemptsUsed, to decide whether something was turned in: some tenants deny students access to submission or attempt data, in which case the status is \"unknown\" even though submission is null or attemptsUsed is 0. Never report an \"unknown\" item as missing, unsubmitted, or not attempted - say it could not be verified and point the user to Brightspace.",
379
410
  inputSchema: GetAssignmentsSchema,
380
411
  }, async (args) => {
381
412
  try {
@@ -7,15 +7,33 @@ import * as path from "node:path";
7
7
  import * as os from "node:os";
8
8
  import { createHash } from "node:crypto";
9
9
  import dotenv from "dotenv";
10
- import { configStoreExists, loadConfigStore } from "./config-store.js";
10
+ import { configStoreExists, getConfigStorePath, loadConfigStore } from "./config-store.js";
11
11
  import { resolveStoredPassword } from "./secure-config.js";
12
12
  import { migrateLegacyState } from "../auth/legacy-state.js";
13
13
  export async function loadConfig() {
14
14
  dotenv.config({ quiet: true });
15
- const store = configStoreExists() ? loadConfigStore() : null;
15
+ // A corrupt or permission-denied config.json must not take the whole server
16
+ // down at startup: env vars alone are a complete, if less convenient,
17
+ // configuration. The failure is still surfaced loudly (never silent) so a
18
+ // broken file doesn't masquerade as "no config.json was ever created".
19
+ // Idea from lmgveerhoek's fork (MIT).
20
+ let store = null;
21
+ let storeLoadError;
22
+ if (configStoreExists()) {
23
+ try {
24
+ store = loadConfigStore();
25
+ }
26
+ catch (error) {
27
+ storeLoadError = error;
28
+ }
29
+ }
16
30
  if (store) {
17
31
  console.error("[config] Loaded base config from ~/.brightspace-mcp/config.json");
18
32
  }
33
+ else if (storeLoadError) {
34
+ console.error(`[config] WARN: Failed to read ${getConfigStorePath()} (${storeLoadError instanceof Error ? storeLoadError.message : String(storeLoadError)}); ` +
35
+ "continuing with environment variables only.");
36
+ }
19
37
  else {
20
38
  console.error("[config] No config.json found, using environment variables");
21
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "3.9.14",
3
+ "version": "3.9.15",
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",