brightspace-mcp-server 3.13.14 → 3.13.16

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/README.md CHANGED
@@ -65,7 +65,7 @@ School-specific quirks: [docs/sign-in.md](docs/sign-in.md#per-school-notes).
65
65
 
66
66
  | About | Try |
67
67
  |-------|-----|
68
- | Grades | "Am I passing all my classes?" · "Compare my grades across courses" · "Is there feedback on my midterm?" (quiz-scored grades link to the quiz, since quiz feedback, including feedback only viewable in LockDown Browser, isn't readable through the API) |
68
+ | Grades | "Am I passing all my classes?" · "Compare my grades across courses" · "What did my professor say about Mini-Assignment 1?" (own assignment feedback is read from every route Brightspace documents for students) · "Is there feedback on my midterm?" (quiz-scored grades link to the quiz, since quiz feedback, including feedback only viewable in LockDown Browser, isn't readable through the API) |
69
69
  | Due dates | "What's due in the next 48 hours?" · "Build me a study schedule for the week" |
70
70
  | Assignments and rubrics | "What does the lab 4 spec actually ask for?" · "Why did I lose points on the analysis criterion?" |
71
71
  | Quizzes and exams | "Which quizzes close this week?" · "Is there a midterm in the gradebook that isn't on my assignments list?" |
@@ -76,7 +76,7 @@ School-specific quirks: [docs/sign-in.md](docs/sign-in.md#per-school-notes).
76
76
  | Discussions | "Summarize the latest posts in the final project thread" |
77
77
  | Lecture videos | "What did the professor say about pinch-off in Tuesday's recording?" (Kaltura, including BoilerCast LTI links, and YouTube) |
78
78
  | Calendar | "When is my midterm?" · "Is lab cancelled on Thursday?" |
79
- | For instructors and TAs | "Which students haven't submitted Lab 4 yet?" · "What feedback did I leave on this student's homework?" · "Download that student's submitted PDF" (students see a clear "instructor access required" note) |
79
+ | For instructors and TAs | "Which students haven't submitted Lab 4 yet?" · "What feedback did I leave on this student's homework?" · "Download that student's submitted PDF" (students get a clear "instructor access required" note for these, but can read their own released feedback, below) |
80
80
  | Signing out | "Sign me out of Brightspace" (your AI app may ask you to confirm first; details under [When it asks you to sign in](#when-it-asks-you-to-sign-in)) |
81
81
 
82
82
  ## Prompts
@@ -14,6 +14,7 @@ import { gradebookUrl, quizUrl } from "../utils/deep-links.js";
14
14
  import { assignmentLinkResolver } from "./assignment-links.js";
15
15
  import { dueIn } from "../utils/due-in.js";
16
16
  import { feedbackText, FEEDBACK_UNAVAILABLE_NOTE } from "../utils/feedback.js";
17
+ import { fetchCurrentUserId } from "./group-membership.js";
17
18
  import { toOutputAssessment } from "./get-assignment-rubric.js";
18
19
  /** The display HTML of a quiz rich-text field, nested shape or flat. */
19
20
  function richTextHtml(field) {
@@ -144,9 +145,14 @@ export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
144
145
  // Adapted from JoshuaMontclair/brightspace-mcp-server (MIT).
145
146
  let submissions = [];
146
147
  let submissionsKnown = true;
148
+ let embeddedFeedback = null;
147
149
  try {
148
150
  const submissionsRaw = await apiClient.get(apiClient.le(courseId, `/dropbox/folders/${folder.Id}/submissions/mysubmissions/`), { ttl: DEFAULT_CACHE_TTLS.assignments });
149
- submissions = Array.isArray(submissionsRaw) ? submissionsRaw : submissionsRaw.Objects ?? [];
151
+ const entries = Array.isArray(submissionsRaw) ? submissionsRaw : submissionsRaw.Objects ?? [];
152
+ // D2L documents EntityDropbox ({ Feedback, Submissions: [...] }) for this
153
+ // route; older tenants answer with the flat submissions. Accept both.
154
+ embeddedFeedback = entries.find((e) => e.Feedback)?.Feedback ?? null;
155
+ submissions = entries.flatMap((e) => (Array.isArray(e.Submissions) ? e.Submissions : [e]));
150
156
  }
151
157
  catch (error) {
152
158
  if (isAuthUnavailable(error))
@@ -175,6 +181,34 @@ export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
175
181
  log("DEBUG", `Failed to fetch feedback for folder ${folder.Id}`, error);
176
182
  }
177
183
  }
184
+ // myFeedback is not one of D2L's documented entity types, so when it has
185
+ // nothing, read the student's own released feedback from the documented
186
+ // routes: the Feedback embedded in mysubmissions (already fetched), then
187
+ // feedback/user/{me}. Draft (IsGraded: false) feedback is not released.
188
+ let feedbackSource = feedback ? "myFeedback" : null;
189
+ if (!feedback && submissions.length > 0) {
190
+ if (embeddedFeedback && embeddedFeedback.IsGraded !== false) {
191
+ feedback = embeddedFeedback;
192
+ feedbackSource = "submission";
193
+ }
194
+ else if (folder.GroupTypeId === null || folder.GroupTypeId === undefined) {
195
+ try {
196
+ const myId = await fetchCurrentUserId(apiClient);
197
+ const own = await apiClient.get(apiClient.le(courseId, `/dropbox/folders/${folder.Id}/feedback/user/${myId}`), { ttl: DEFAULT_CACHE_TTLS.assignments });
198
+ if (own && own.IsGraded !== false) {
199
+ feedback = own;
200
+ feedbackSource = "user";
201
+ }
202
+ }
203
+ catch (error) {
204
+ if (isAuthUnavailable(error))
205
+ throw error;
206
+ log("DEBUG", `Own-feedback fallback failed for folder ${folder.Id}`, error);
207
+ }
208
+ }
209
+ if (feedback)
210
+ feedbackStatus = "retrieved";
211
+ }
178
212
  // Instructor-provided URL links attached to the folder. Omitted
179
213
  // entirely (rather than an empty array) when the folder has none, to
180
214
  // keep the common case's shape exactly as it was.
@@ -251,6 +285,7 @@ export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
251
285
  }
252
286
  : null,
253
287
  feedbackStatus,
288
+ ...(feedbackSource ? { feedbackSource } : {}),
254
289
  feedbackCommentsStatus: commentsRetrieved ? "retrieved" : "unavailable",
255
290
  feedbackUrl: baseUrl ? gradebookUrl(baseUrl, courseId) : null,
256
291
  ...(commentsRetrieved ? {} : { feedbackStatusNote: FEEDBACK_UNAVAILABLE_NOTE }),
@@ -8,6 +8,7 @@ import { GetDropboxFeedbackSchema } from "./schemas.js";
8
8
  import { toolResponse, sanitizeError, errorResponse } from "./tool-helpers.js";
9
9
  import { convertHtmlToMarkdown } from "../utils/html-converter.js";
10
10
  import { log } from "../utils/logger.js";
11
+ import { fetchCurrentUserId } from "./group-membership.js";
11
12
  /**
12
13
  * Register get_dropbox_feedback tool.
13
14
  */
@@ -15,15 +16,26 @@ export function registerGetDropboxFeedback(server, apiClient) {
15
16
  server.registerTool("get_dropbox_feedback", {
16
17
  title: "Get Dropbox Feedback",
17
18
  annotations: { readOnlyHint: true },
18
- description: "Retrieve the existing feedback already saved for a specific user or group in a dropbox folder, " +
19
- "for an instructor or TA — score, graded state, feedback text, and rubric assessment detail. " +
20
- "Use this before drafting new feedback to see what has already been recorded. Requires " +
21
- "instructor or TA access to the course.",
19
+ description: "Retrieve the feedback saved for a dropbox folder — score, graded state, feedback text, and rubric " +
20
+ "assessment detail. Students: omit entityType and entityId to read your OWN released feedback on an " +
21
+ "assignment (it is not hidden; use this when get_assignments shows feedback unavailable). " +
22
+ "Instructors and TAs: pass entityType and entityId to read what is already saved for any user or " +
23
+ "group, e.g. before drafting new feedback; reading another user's feedback requires instructor or TA " +
24
+ "access to the course.",
22
25
  inputSchema: GetDropboxFeedbackSchema,
23
26
  }, async (args) => {
24
27
  try {
25
28
  log("DEBUG", "get_dropbox_feedback tool called", { args });
26
- const { courseId, folderId, entityType, entityId } = GetDropboxFeedbackSchema.parse(args);
29
+ const parsed = GetDropboxFeedbackSchema.parse(args);
30
+ const { courseId, folderId } = parsed;
31
+ // Omitted entity = the signed-in user's own feedback, which Brightspace
32
+ // lets a student read once it is released.
33
+ const own = parsed.entityId === undefined;
34
+ if (own && parsed.entityType === "group") {
35
+ return errorResponse("entityId is required when entityType is group.");
36
+ }
37
+ const entityType = parsed.entityType ?? "user";
38
+ const entityId = parsed.entityId ?? await fetchCurrentUserId(apiClient);
27
39
  const feedbackPath = apiClient.le(courseId, `/dropbox/folders/${folderId}/feedback/${entityType}/${entityId}`);
28
40
  let feedback;
29
41
  try {
@@ -31,8 +43,11 @@ export function registerGetDropboxFeedback(server, apiClient) {
31
43
  }
32
44
  catch (error) {
33
45
  if (error instanceof ApiError && error.status === 403) {
34
- return errorResponse("Instructor or TA access required for this course. This tool reads another user's " +
35
- "grading feedback, which Brightspace only grants to course staff.");
46
+ return errorResponse(own
47
+ ? "Brightspace refused this read of your own feedback; it has not been released yet, or " +
48
+ "this course does not expose it through the API. Check the assignment in Brightspace."
49
+ : "Instructor or TA access required for this course. Reading another user's grading " +
50
+ "feedback is only granted to course staff; to read your own, omit entityType and entityId.");
36
51
  }
37
52
  if (error instanceof ApiError && error.status === 404) {
38
53
  return toolResponse({
@@ -41,7 +56,9 @@ export function registerGetDropboxFeedback(server, apiClient) {
41
56
  entityType,
42
57
  entityId,
43
58
  feedback: null,
44
- note: "No feedback found. This submission has not been graded yet.",
59
+ note: own
60
+ ? "No feedback found. This submission has not been graded yet, or its feedback has not been released."
61
+ : "No feedback found. This submission has not been graded yet.",
45
62
  });
46
63
  }
47
64
  throw error;
@@ -61,7 +61,7 @@ export function registerGetDropboxFolders(server, apiClient) {
61
61
  name: f.Name,
62
62
  categoryId: f.CategoryId,
63
63
  isHidden: f.IsHidden,
64
- isGroup: f.GroupTypeId !== null,
64
+ isGroup: f.GroupTypeId !== null && f.GroupTypeId !== undefined,
65
65
  submissionType: submissionTypeLabel(f.SubmissionType),
66
66
  dueDate: f.DueDate ?? null,
67
67
  startDate: f.StartDate ?? null,
@@ -2,12 +2,17 @@ import { DEFAULT_CACHE_TTLS } from "../api/index.js";
2
2
  import { ApiError } from "../api/errors.js";
3
3
  const inaccessible = (error) => error instanceof ApiError
4
4
  && (error.status === 403 || error.status === 404);
5
- /** Shared membership lookup. API cache/coalescing bounds repeat reads; no classlist is needed. */
6
- export async function fetchMyGroupMemberships(apiClient, courseId, categoryId) {
5
+ /** The signed-in user's id, from the cached whoami read. */
6
+ export async function fetchCurrentUserId(apiClient) {
7
7
  const me = await apiClient.get(apiClient.lp("/users/whoami"), { ttl: DEFAULT_CACHE_TTLS.roster });
8
8
  const myId = Number(me.Identifier);
9
9
  if (!Number.isSafeInteger(myId) || myId <= 0)
10
10
  throw new Error("Brightspace returned an invalid current user identifier.");
11
+ return myId;
12
+ }
13
+ /** Shared membership lookup. API cache/coalescing bounds repeat reads; no classlist is needed. */
14
+ export async function fetchMyGroupMemberships(apiClient, courseId, categoryId) {
15
+ const myId = await fetchCurrentUserId(apiClient);
11
16
  let categories;
12
17
  try {
13
18
  categories = categoryId === undefined
@@ -196,10 +196,10 @@ export const GetDropboxFeedbackSchema = z.object({
196
196
  .describe("Course ID the dropbox folder belongs to."),
197
197
  folderId: z.coerce.number().int().positive()
198
198
  .describe("Dropbox folder ID."),
199
- entityType: z.enum(["user", "group"])
200
- .describe("Whether entityId identifies an individual user or a group."),
201
- entityId: z.coerce.number().int().positive()
202
- .describe("User ID or group ID whose existing feedback to retrieve."),
199
+ entityType: z.enum(["user", "group"]).optional()
200
+ .describe("Whether entityId identifies an individual user or a group. Omit both entityType and entityId to read your own feedback."),
201
+ entityId: z.coerce.number().int().positive().optional()
202
+ .describe("User ID or group ID whose existing feedback to retrieve. Omit to read the signed-in user's own feedback."),
203
203
  });
204
204
  export const GetRubricsForObjectSchema = z.object({
205
205
  courseId: z.coerce.number().int().positive()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "3.13.14",
3
+ "version": "3.13.16",
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",