brightspace-mcp-server 3.6.1 → 3.7.0
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 +2 -1
- package/build/api/types.js +1 -0
- package/build/index.js +4 -3
- package/build/tools/calendar-events.js +79 -0
- package/build/tools/get-calendar-events.js +44 -0
- package/build/tools/get-upcoming-due-dates.js +37 -45
- package/build/tools/index.js +1 -0
- package/build/tools/resolve-courses.js +46 -0
- package/build/tools/schemas.js +10 -0
- package/build/utils/deep-links.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -120,7 +120,8 @@ Run it from your home folder. On macOS, a terminal that lacks Files and Folders
|
|
|
120
120
|
| Discussions | "What are people saying in the final project thread?" · "Summarize the latest discussion posts" |
|
|
121
121
|
| Video transcripts | "What did the professor say about pinch-off in Tuesday's lecture recording?" · "Summarize last week's BoilerCast video" — works for Kaltura and YouTube embeds; other platforms report that they aren't supported yet |
|
|
122
122
|
| Troubleshooting | "Which version of the Brightspace server am I running?" · "Where is my Brightspace config file?" — `get_server_info` reports the version, Node runtime, platform, config and session paths, school URL, and whether a credential is stored, without contacting Brightspace or revealing secrets |
|
|
123
|
-
|
|
|
123
|
+
| Calendar | "When is my midterm?" · "What's on my calendar this week?" · "Is lab cancelled on Thursday?" — reads exams, labs, review sessions, and deadlines instructors put only on the course calendar |
|
|
124
|
+
| Planning | "Build me a study schedule based on my upcoming due dates" · "Which class needs the most attention right now?" — pulls from assignments, quizzes, graded discussion topics (any topic with a due date), and course calendar events such as exams and labs |
|
|
124
125
|
|
|
125
126
|
|
|
126
127
|
Licensed under the MIT License.
|
package/build/api/types.js
CHANGED
package/build/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { startUpdateChecks } from "./utils/update-checker.js";
|
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { dirname, resolve } from "node:path";
|
|
19
|
-
import { registerGetMyCourses, registerGetUpcomingDueDates, registerGetMyGrades, registerGetAnnouncements, registerGetAssignments, registerGetAssignmentFiles, registerGetAnnouncementFiles, registerGetCourseContent, registerDownloadFile, registerGetClasslistEmails, registerGetRoster, registerGetSyllabus, registerGetDiscussions, registerGetVideoTranscript, registerGetServerInfo, } from "./tools/index.js";
|
|
19
|
+
import { registerGetMyCourses, registerGetUpcomingDueDates, registerGetCalendarEvents, registerGetMyGrades, registerGetAnnouncements, registerGetAssignments, registerGetAssignmentFiles, registerGetAnnouncementFiles, registerGetCourseContent, registerDownloadFile, registerGetClasslistEmails, registerGetRoster, registerGetSyllabus, registerGetDiscussions, registerGetVideoTranscript, registerGetServerInfo, } from "./tools/index.js";
|
|
20
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
21
|
const __dirname = dirname(__filename);
|
|
22
22
|
const PKG_VERSION = (() => {
|
|
@@ -102,6 +102,7 @@ else {
|
|
|
102
102
|
// Register MCP tools
|
|
103
103
|
registerGetMyCourses(server, apiClient, config);
|
|
104
104
|
registerGetUpcomingDueDates(server, apiClient, config);
|
|
105
|
+
registerGetCalendarEvents(server, apiClient, config);
|
|
105
106
|
registerGetMyGrades(server, apiClient, config);
|
|
106
107
|
registerGetAnnouncements(server, apiClient, config);
|
|
107
108
|
registerGetAssignments(server, apiClient, config);
|
|
@@ -115,11 +116,11 @@ else {
|
|
|
115
116
|
registerGetDiscussions(server, apiClient);
|
|
116
117
|
registerGetVideoTranscript(server, apiClient);
|
|
117
118
|
registerGetServerInfo(server, config, PKG_VERSION);
|
|
118
|
-
log("DEBUG", "MCP tools registered (
|
|
119
|
+
log("DEBUG", "MCP tools registered (16 tools)");
|
|
119
120
|
// Connect stdio transport
|
|
120
121
|
const transport = new StdioServerTransport();
|
|
121
122
|
await server.connect(transport);
|
|
122
|
-
log("INFO", "Brightspace MCP Server by Rohan Muppa — running on stdio (
|
|
123
|
+
log("INFO", "Brightspace MCP Server by Rohan Muppa — running on stdio (16 tools registered)");
|
|
123
124
|
log("INFO", "Setup: see README.md for MCP client configuration (Claude Desktop, ChatGPT Desktop, Cursor, etc.)");
|
|
124
125
|
}
|
|
125
126
|
catch (error) {
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { DEFAULT_CACHE_TTLS } from "../api/index.js";
|
|
7
|
+
import { fetchAllObjects } from "../api/paginate.js";
|
|
8
|
+
import { convertHtmlToMarkdown } from "../utils/html-converter.js";
|
|
9
|
+
import { calendarUrl } from "../utils/deep-links.js";
|
|
10
|
+
/**
|
|
11
|
+
* The item a calendar event was generated from. The types an existing tool
|
|
12
|
+
* already reports use that tool's own `type` names, so a caller can match an
|
|
13
|
+
* event to the item it came from.
|
|
14
|
+
*/
|
|
15
|
+
const ENTITY_TYPES = {
|
|
16
|
+
"D2L.LE.Dropbox.Dropbox": "assignment",
|
|
17
|
+
"D2L.LE.Quizzing.Quiz": "quiz",
|
|
18
|
+
"D2L.LE.Discussions.DiscussionTopic": "discussion",
|
|
19
|
+
"D2L.LE.Discussions.DiscussionForum": "forum",
|
|
20
|
+
"D2L.LE.Content.ContentObject.ModuleCO": "module",
|
|
21
|
+
"D2L.LE.Content.ContentObject.TopicCO": "topic",
|
|
22
|
+
"D2L.LE.Grades.GradeObject": "grade",
|
|
23
|
+
"D2L.LE.Checklist.ChecklistItem": "checklist",
|
|
24
|
+
"D2L.LE.Survey.Survey": "survey",
|
|
25
|
+
};
|
|
26
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
27
|
+
function descriptionMarkdown(description) {
|
|
28
|
+
const html = typeof description === "string" ? description : description?.Html || description?.Text || "";
|
|
29
|
+
return convertHtmlToMarkdown(html).markdown.trim();
|
|
30
|
+
}
|
|
31
|
+
/** One raw event as a CalendarEvent, or null when it has no start at all. */
|
|
32
|
+
function toCalendarEvent(raw, baseUrl, course) {
|
|
33
|
+
// All-day events carry their dates in StartDay/EndDay instead.
|
|
34
|
+
const start = raw.StartDateTime ?? raw.StartDay ?? null;
|
|
35
|
+
if (!start)
|
|
36
|
+
return null;
|
|
37
|
+
const end = raw.EndDateTime ?? raw.EndDay ?? null;
|
|
38
|
+
const location = raw.LocationName?.trim();
|
|
39
|
+
const description = descriptionMarkdown(raw.Description);
|
|
40
|
+
const entity = raw.AssociatedEntity;
|
|
41
|
+
return {
|
|
42
|
+
id: raw.CalendarEventId,
|
|
43
|
+
title: raw.Title,
|
|
44
|
+
courseId: course.id,
|
|
45
|
+
courseName: course.name,
|
|
46
|
+
start,
|
|
47
|
+
...(end ? { end } : {}),
|
|
48
|
+
...(location ? { location } : {}),
|
|
49
|
+
...(description ? { description } : {}),
|
|
50
|
+
url: calendarUrl(baseUrl, course.id),
|
|
51
|
+
...(entity
|
|
52
|
+
? {
|
|
53
|
+
generatedFrom: {
|
|
54
|
+
type: ENTITY_TYPES[entity.AssociatedEntityType] ?? entity.AssociatedEntityType,
|
|
55
|
+
id: entity.AssociatedEntityId,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
: {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Every calendar event in one course that starts inside [from, to] (epoch ms).
|
|
63
|
+
*
|
|
64
|
+
* The request window is widened to whole hours so repeated calls within the
|
|
65
|
+
* cache TTL share one cached response; the exact window is applied here.
|
|
66
|
+
*/
|
|
67
|
+
export async function fetchCourseCalendarEvents(apiClient, baseUrl, course, from, to) {
|
|
68
|
+
const requestFrom = new Date(Math.floor(from / HOUR_MS) * HOUR_MS).toISOString();
|
|
69
|
+
const requestTo = new Date(Math.ceil(to / HOUR_MS) * HOUR_MS).toISOString();
|
|
70
|
+
const raw = await fetchAllObjects(apiClient, apiClient.le(course.id, `/calendar/events/myEvents/?startDateTime=${encodeURIComponent(requestFrom)}&endDateTime=${encodeURIComponent(requestTo)}`), { ttl: DEFAULT_CACHE_TTLS.calendar });
|
|
71
|
+
return raw
|
|
72
|
+
.map((event) => toCalendarEvent(event, baseUrl, course))
|
|
73
|
+
.filter((event) => {
|
|
74
|
+
if (!event)
|
|
75
|
+
return false;
|
|
76
|
+
const start = new Date(event.start).getTime();
|
|
77
|
+
return Number.isFinite(start) && start >= from && start <= to;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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 { GetCalendarEventsSchema } from "./schemas.js";
|
|
7
|
+
import { toolResponse, sanitizeError } from "./tool-helpers.js";
|
|
8
|
+
import { log } from "../utils/logger.js";
|
|
9
|
+
import { resolveCourses } from "./resolve-courses.js";
|
|
10
|
+
import { fetchCourseCalendarEvents } from "./calendar-events.js";
|
|
11
|
+
const DEFAULT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
12
|
+
/**
|
|
13
|
+
* Register get_calendar_events tool
|
|
14
|
+
*/
|
|
15
|
+
export function registerGetCalendarEvents(server, apiClient, config) {
|
|
16
|
+
server.registerTool("get_calendar_events", {
|
|
17
|
+
title: "Get Calendar Events",
|
|
18
|
+
description: "Fetch course calendar events — exams, midterms, labs, recitations, review sessions, schedule changes, and deadlines instructors typed straight onto the calendar — across all your courses or one course, in a time window (default: the next 7 days). Use this when the user asks when an exam is, what's on their calendar, or what's happening this week.",
|
|
19
|
+
inputSchema: GetCalendarEventsSchema,
|
|
20
|
+
}, async (args) => {
|
|
21
|
+
try {
|
|
22
|
+
log("DEBUG", "get_calendar_events tool called", { args });
|
|
23
|
+
const { courseId, from, to, includeGenerated } = GetCalendarEventsSchema.parse(args);
|
|
24
|
+
const windowStart = from ? new Date(from).getTime() : Date.now();
|
|
25
|
+
const windowEnd = to ? new Date(to).getTime() : windowStart + DEFAULT_WINDOW_MS;
|
|
26
|
+
const courses = await resolveCourses(apiClient, config, courseId);
|
|
27
|
+
const results = await Promise.allSettled(courses.map((course) => fetchCourseCalendarEvents(apiClient, config.baseUrl, course, windowStart, windowEnd)));
|
|
28
|
+
const events = results
|
|
29
|
+
.flatMap((result, i) => {
|
|
30
|
+
if (result.status === "fulfilled")
|
|
31
|
+
return result.value;
|
|
32
|
+
log("DEBUG", `get_calendar_events: skipping course ${courses[i].id} after fetch failure`, result.reason);
|
|
33
|
+
return [];
|
|
34
|
+
})
|
|
35
|
+
.filter((event) => includeGenerated || !event.generatedFrom)
|
|
36
|
+
.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
|
37
|
+
log("INFO", `get_calendar_events: Retrieved ${events.length} events across ${courses.length} courses`);
|
|
38
|
+
return toolResponse(events);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return sanitizeError(error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -4,53 +4,16 @@
|
|
|
4
4
|
* Licensed under MIT — see LICENSE file for details.
|
|
5
5
|
*/
|
|
6
6
|
import { DEFAULT_CACHE_TTLS } from "../api/index.js";
|
|
7
|
-
import { fetchAllItems } from "../api/paginate.js";
|
|
8
7
|
import { GetUpcomingDueDatesSchema, } from "./schemas.js";
|
|
9
8
|
import { toolResponse, sanitizeError } from "./tool-helpers.js";
|
|
10
9
|
import { log } from "../utils/logger.js";
|
|
11
|
-
import { applyCourseFilter } from "../utils/course-filter.js";
|
|
12
10
|
import { assignmentUrl, quizUrl, discussionUrl } from "../utils/deep-links.js";
|
|
11
|
+
import { resolveCourses } from "./resolve-courses.js";
|
|
12
|
+
import { fetchCourseCalendarEvents } from "./calendar-events.js";
|
|
13
13
|
/** D2L list endpoints return either a paged { Objects: [...] } or a flat array. */
|
|
14
14
|
function unwrapList(raw) {
|
|
15
15
|
return Array.isArray(raw) ? raw : (raw?.Objects ?? []);
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Resolve which courses to query, and their names.
|
|
19
|
-
*
|
|
20
|
-
* A tool-level courseId bypasses the configured course filter, but enrollments
|
|
21
|
-
* are still fetched so the course can be named.
|
|
22
|
-
*/
|
|
23
|
-
async function resolveCourses(apiClient, config, courseId) {
|
|
24
|
-
let items = [];
|
|
25
|
-
try {
|
|
26
|
-
// isActive=true tracks the configured policy rather than being pinned on:
|
|
27
|
-
// a user who set activeOnly:false is asking to see archived courses, and a
|
|
28
|
-
// query that withholds them leaves applyCourseFilter nothing to let
|
|
29
|
-
// through. Enrollments are paged, so follow the bookmark chain — a long
|
|
30
|
-
// enrollment history would otherwise lose every course past the first page,
|
|
31
|
-
// and every deadline in those courses with it.
|
|
32
|
-
items = await fetchAllItems(apiClient, apiClient.lp(`/enrollments/myenrollments/?orgUnitTypeId=3${config.courseFilter.activeOnly ? "&isActive=true" : ""}`), { ttl: DEFAULT_CACHE_TTLS.enrollments });
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
// Without enrollments there is no course list to walk, so only the explicit
|
|
36
|
-
// single-course case can continue (with an unnamed course).
|
|
37
|
-
if (!courseId)
|
|
38
|
-
throw error;
|
|
39
|
-
log("DEBUG", "get_upcoming_due_dates: could not fetch enrollments for course name", error);
|
|
40
|
-
}
|
|
41
|
-
if (courseId) {
|
|
42
|
-
const match = items.find((item) => item.OrgUnit.Id === courseId);
|
|
43
|
-
return [{ id: courseId, name: match?.OrgUnit.Name ?? null }];
|
|
44
|
-
}
|
|
45
|
-
const filtered = applyCourseFilter(items.map((item) => ({
|
|
46
|
-
id: item.OrgUnit.Id,
|
|
47
|
-
name: item.OrgUnit.Name,
|
|
48
|
-
code: item.OrgUnit.Code,
|
|
49
|
-
isActive: item.Access.IsActive,
|
|
50
|
-
canAccess: item.Access.CanAccess,
|
|
51
|
-
})), config.courseFilter);
|
|
52
|
-
return filtered.map((course) => ({ id: course.id, name: course.name }));
|
|
53
|
-
}
|
|
54
17
|
/**
|
|
55
18
|
* Collect every graded, dated discussion topic for one course.
|
|
56
19
|
*
|
|
@@ -77,19 +40,42 @@ async function fetchDiscussionDueTopics(apiClient, courseId) {
|
|
|
77
40
|
return topics;
|
|
78
41
|
}
|
|
79
42
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
43
|
+
* Calendar events as upcoming items, minus the ones Brightspace generated from
|
|
44
|
+
* an item already in `items` — that item is the better record of the same
|
|
45
|
+
* deadline. Hand-made events (exams, labs) have no source item and always stay.
|
|
46
|
+
*/
|
|
47
|
+
function calendarItems(events, items) {
|
|
48
|
+
const listed = new Set(items.map((item) => `${item.type}:${item.id}`));
|
|
49
|
+
return events
|
|
50
|
+
.filter((event) => !event.generatedFrom || !listed.has(`${event.generatedFrom.type}:${event.generatedFrom.id}`))
|
|
51
|
+
.map((event) => ({
|
|
52
|
+
type: "event",
|
|
53
|
+
id: event.id,
|
|
54
|
+
title: event.title,
|
|
55
|
+
courseId: event.courseId,
|
|
56
|
+
courseName: event.courseName,
|
|
57
|
+
dueDate: event.start,
|
|
58
|
+
startDate: null,
|
|
59
|
+
endDate: event.end ?? null,
|
|
60
|
+
...(event.location ? { location: event.location } : {}),
|
|
61
|
+
url: event.url,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Collect every dated assignment, quiz, graded discussion topic, and calendar
|
|
66
|
+
* event starting in [from, to] for one course.
|
|
82
67
|
*
|
|
83
68
|
* No submissions or attempts: the due date lives on the item itself, which is
|
|
84
69
|
* what makes this cheap enough to run across all enrolled courses. A
|
|
85
70
|
* discussion topic counts only when it has a DueDate — an ungraded chat forum
|
|
86
71
|
* has none and should not clutter the list.
|
|
87
72
|
*/
|
|
88
|
-
async function fetchCourseDueItems(apiClient, baseUrl, course) {
|
|
89
|
-
const [dropboxResult, quizResult, discussionResult] = await Promise.allSettled([
|
|
73
|
+
async function fetchCourseDueItems(apiClient, baseUrl, course, from, to) {
|
|
74
|
+
const [dropboxResult, quizResult, discussionResult, calendarResult] = await Promise.allSettled([
|
|
90
75
|
apiClient.get(apiClient.le(course.id, "/dropbox/folders/"), { ttl: DEFAULT_CACHE_TTLS.assignments }),
|
|
91
76
|
apiClient.get(apiClient.le(course.id, "/quizzes/"), { ttl: DEFAULT_CACHE_TTLS.assignments }),
|
|
92
77
|
fetchDiscussionDueTopics(apiClient, course.id),
|
|
78
|
+
fetchCourseCalendarEvents(apiClient, baseUrl, course, from, to),
|
|
93
79
|
]);
|
|
94
80
|
const items = [];
|
|
95
81
|
if (dropboxResult.status === "fulfilled") {
|
|
@@ -161,6 +147,12 @@ async function fetchCourseDueItems(apiClient, baseUrl, course) {
|
|
|
161
147
|
else {
|
|
162
148
|
log("DEBUG", `get_upcoming_due_dates: failed to fetch discussions for course ${course.id}`, discussionResult.reason);
|
|
163
149
|
}
|
|
150
|
+
if (calendarResult.status === "fulfilled") {
|
|
151
|
+
items.push(...calendarItems(calendarResult.value, items));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
log("DEBUG", `get_upcoming_due_dates: failed to fetch calendar events for course ${course.id}`, calendarResult.reason);
|
|
155
|
+
}
|
|
164
156
|
return items;
|
|
165
157
|
}
|
|
166
158
|
/**
|
|
@@ -169,7 +161,7 @@ async function fetchCourseDueItems(apiClient, baseUrl, course) {
|
|
|
169
161
|
export function registerGetUpcomingDueDates(server, apiClient, config) {
|
|
170
162
|
server.registerTool("get_upcoming_due_dates", {
|
|
171
163
|
title: "Get Upcoming Due Dates",
|
|
172
|
-
description: "Fetch upcoming due dates across all your courses, derived from the due dates on assignments (dropbox folders), quizzes, and graded discussion topics themselves. Use this when the user asks about deadlines, what's due, upcoming work, or what they need to do this week.",
|
|
164
|
+
description: "Fetch upcoming due dates across all your courses, derived from the due dates on assignments (dropbox folders), quizzes, and graded discussion topics themselves, plus course calendar events such as exams and labs (type: event). Use this when the user asks about deadlines, what's due, upcoming work, or what they need to do this week.",
|
|
173
165
|
inputSchema: GetUpcomingDueDatesSchema,
|
|
174
166
|
}, async (args) => {
|
|
175
167
|
try {
|
|
@@ -182,7 +174,7 @@ export function registerGetUpcomingDueDates(server, apiClient, config) {
|
|
|
182
174
|
const courses = await resolveCourses(apiClient, config, courseId);
|
|
183
175
|
log("DEBUG", `get_upcoming_due_dates: querying ${courses.length} course(s), window=${daysAhead} days`);
|
|
184
176
|
// Fetch every course in parallel; the API client rate limits itself
|
|
185
|
-
const results = await Promise.allSettled(courses.map((course) => fetchCourseDueItems(apiClient, config.baseUrl, course)));
|
|
177
|
+
const results = await Promise.allSettled(courses.map((course) => fetchCourseDueItems(apiClient, config.baseUrl, course, now, windowEnd)));
|
|
186
178
|
const items = results.flatMap((result) => {
|
|
187
179
|
if (result.status === "fulfilled")
|
|
188
180
|
return result.value;
|
package/build/tools/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Tool registration functions - barrel export
|
|
7
7
|
export { registerGetMyCourses } from "./get-my-courses.js";
|
|
8
8
|
export { registerGetUpcomingDueDates } from "./get-upcoming-due-dates.js";
|
|
9
|
+
export { registerGetCalendarEvents } from "./get-calendar-events.js";
|
|
9
10
|
export { registerGetMyGrades } from "./get-my-grades.js";
|
|
10
11
|
export { registerGetAnnouncements } from "./get-announcements.js";
|
|
11
12
|
export { registerGetAssignments } from "./get-assignments.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
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 { DEFAULT_CACHE_TTLS } from "../api/index.js";
|
|
7
|
+
import { fetchAllItems } from "../api/paginate.js";
|
|
8
|
+
import { log } from "../utils/logger.js";
|
|
9
|
+
import { applyCourseFilter } from "../utils/course-filter.js";
|
|
10
|
+
/**
|
|
11
|
+
* Resolve which courses to query, and their names.
|
|
12
|
+
*
|
|
13
|
+
* A tool-level courseId bypasses the configured course filter, but enrollments
|
|
14
|
+
* are still fetched so the course can be named.
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveCourses(apiClient, config, courseId) {
|
|
17
|
+
let items = [];
|
|
18
|
+
try {
|
|
19
|
+
// isActive=true tracks the configured policy rather than being pinned on:
|
|
20
|
+
// a user who set activeOnly:false is asking to see archived courses, and a
|
|
21
|
+
// query that withholds them leaves applyCourseFilter nothing to let
|
|
22
|
+
// through. Enrollments are paged, so follow the bookmark chain — a long
|
|
23
|
+
// enrollment history would otherwise lose every course past the first page,
|
|
24
|
+
// and every deadline in those courses with it.
|
|
25
|
+
items = await fetchAllItems(apiClient, apiClient.lp(`/enrollments/myenrollments/?orgUnitTypeId=3${config.courseFilter.activeOnly ? "&isActive=true" : ""}`), { ttl: DEFAULT_CACHE_TTLS.enrollments });
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// Without enrollments there is no course list to walk, so only the explicit
|
|
29
|
+
// single-course case can continue (with an unnamed course).
|
|
30
|
+
if (!courseId)
|
|
31
|
+
throw error;
|
|
32
|
+
log("DEBUG", "resolveCourses: could not fetch enrollments for course name", error);
|
|
33
|
+
}
|
|
34
|
+
if (courseId) {
|
|
35
|
+
const match = items.find((item) => item.OrgUnit.Id === courseId);
|
|
36
|
+
return [{ id: courseId, name: match?.OrgUnit.Name ?? null }];
|
|
37
|
+
}
|
|
38
|
+
const filtered = applyCourseFilter(items.map((item) => ({
|
|
39
|
+
id: item.OrgUnit.Id,
|
|
40
|
+
name: item.OrgUnit.Name,
|
|
41
|
+
code: item.OrgUnit.Code,
|
|
42
|
+
isActive: item.Access.IsActive,
|
|
43
|
+
canAccess: item.Access.CanAccess,
|
|
44
|
+
})), config.courseFilter);
|
|
45
|
+
return filtered.map((course) => ({ id: course.id, name: course.name }));
|
|
46
|
+
}
|
package/build/tools/schemas.js
CHANGED
|
@@ -22,6 +22,16 @@ export const GetUpcomingDueDatesSchema = z.object({
|
|
|
22
22
|
daysAhead: z.coerce.number().int().min(1).max(90).default(7).describe("Number of days ahead to look for due dates"),
|
|
23
23
|
courseId: z.coerce.number().int().positive().optional().describe("Filter to a specific course ID"),
|
|
24
24
|
});
|
|
25
|
+
export const GetCalendarEventsSchema = z.object({
|
|
26
|
+
courseId: z.coerce.number().int().positive().optional()
|
|
27
|
+
.describe("Course ID to get calendar events for. If omitted, returns events across all enrolled courses."),
|
|
28
|
+
from: z.string().datetime({ offset: true, message: "from must be an ISO 8601 datetime, e.g. 2026-01-15T00:00:00Z" }).optional()
|
|
29
|
+
.describe("Start of the window, ISO 8601 (e.g. 2026-01-15T00:00:00Z). Defaults to now."),
|
|
30
|
+
to: z.string().datetime({ offset: true, message: "to must be an ISO 8601 datetime, e.g. 2026-01-22T00:00:00Z" }).optional()
|
|
31
|
+
.describe("End of the window, ISO 8601 (e.g. 2026-01-22T00:00:00Z). Defaults to 7 days after from."),
|
|
32
|
+
includeGenerated: z.boolean().default(false)
|
|
33
|
+
.describe("Include the events Brightspace generates from assignment, quiz, and discussion due dates (marked with generatedFrom). Off by default because get_upcoming_due_dates and get_assignments already report those."),
|
|
34
|
+
});
|
|
25
35
|
export const GetMyGradesSchema = z.object({
|
|
26
36
|
courseId: z.coerce.number().int().positive().optional().describe("Course ID to get grades for. If omitted, returns grades for all enrolled courses."),
|
|
27
37
|
});
|
|
@@ -46,3 +46,10 @@ export function gradebookUrl(baseUrl, courseId) {
|
|
|
46
46
|
export function discussionUrl(baseUrl, courseId, topicId) {
|
|
47
47
|
return `${trimBaseUrl(baseUrl)}/d2l/lms/discussions/threadlist.d2l?ou=${courseId}&tId=${topicId}`;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Link to a course's calendar. Calendar events have no per-event student page
|
|
51
|
+
* the API names, so every event in a course shares this one link.
|
|
52
|
+
*/
|
|
53
|
+
export function calendarUrl(baseUrl, courseId) {
|
|
54
|
+
return `${trimBaseUrl(baseUrl)}/d2l/le/calendar/${courseId}`;
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
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",
|