brightspace-mcp-server 3.11.7 → 3.11.8
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.
|
@@ -10,6 +10,18 @@ import { toolResponse, toolResponseWithNotice, sanitizeError, isAuthUnavailable,
|
|
|
10
10
|
import { log } from "../utils/logger.js";
|
|
11
11
|
import { applyCourseFilter } from "../utils/course-filter.js";
|
|
12
12
|
import { matchesModifiedSince } from "../utils/modified-since.js";
|
|
13
|
+
function pageInfo(offset, count, returned, total, unavailableCourseIds = []) {
|
|
14
|
+
const nextOffset = total !== null && offset + returned < total ? offset + returned : null;
|
|
15
|
+
return { offset, count, returned, total, nextOffset, truncated: nextOffset !== null,
|
|
16
|
+
coverageComplete: total !== null && unavailableCourseIds.length === 0, unavailableCourseIds };
|
|
17
|
+
}
|
|
18
|
+
// Preserve the original array in content[0] and existing auth notices, while
|
|
19
|
+
// exposing machine-readable continuation metadata in a separate text block.
|
|
20
|
+
function arrayPage(items, pagination, notice) {
|
|
21
|
+
const result = notice ? toolResponseWithNotice(items, notice) : toolResponse(items);
|
|
22
|
+
result.content.push({ type: "text", text: JSON.stringify({ pagination }) });
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
13
25
|
/**
|
|
14
26
|
* A date the runtime can actually order, or null. Unreadable and absent are the
|
|
15
27
|
* same answer, so a caller can fall through to the next best.
|
|
@@ -87,13 +99,13 @@ export function mapNewsItem(item) {
|
|
|
87
99
|
export function registerGetAnnouncements(server, apiClient, config) {
|
|
88
100
|
server.registerTool("get_announcements", {
|
|
89
101
|
title: "Get Announcements",
|
|
90
|
-
description: "Fetch
|
|
102
|
+
description: "Fetch announcements from one course with courseId, or all courses when omitted, newest first. count is 1-50 per page. Continue with offset= pagination.nextOffset until it is null. Pagination metadata is in a separate JSON text block for array responses, or the pagination field with modifiedSince. A false coverageComplete means some courses were not fetched: retry from offset 0 after recovery. Offsets reflect the current filtered list; new posts can shift pages. Attachments are listed per announcement; fetch them with download_file (newsId + fileId) or read them with get_announcement_files.",
|
|
91
103
|
inputSchema: GetAnnouncementsSchema,
|
|
92
104
|
}, async (args) => {
|
|
93
105
|
try {
|
|
94
106
|
log("DEBUG", "get_announcements tool called", { args });
|
|
95
107
|
// Parse and validate input
|
|
96
|
-
const { courseId, count, modifiedSince } = GetAnnouncementsSchema.parse(args);
|
|
108
|
+
const { courseId, count, offset, modifiedSince } = GetAnnouncementsSchema.parse(args);
|
|
97
109
|
const cutoff = modifiedSince ? new Date(modifiedSince) : null;
|
|
98
110
|
// Single course case
|
|
99
111
|
if (courseId) {
|
|
@@ -104,16 +116,18 @@ export function registerGetAnnouncements(server, apiClient, config) {
|
|
|
104
116
|
const matched = cutoff
|
|
105
117
|
? published.filter((a) => matchesModifiedSince(a.lastModified, cutoff))
|
|
106
118
|
: published;
|
|
107
|
-
const announcements = matched.sort(newestFirst).slice(
|
|
119
|
+
const announcements = matched.sort(newestFirst).slice(offset, offset + count);
|
|
120
|
+
const pagination = pageInfo(offset, count, announcements.length, matched.length);
|
|
108
121
|
log("INFO", `get_announcements: Retrieved ${announcements.length} announcements for course ${courseId}`);
|
|
109
|
-
return
|
|
110
|
-
? {
|
|
122
|
+
return modifiedSince
|
|
123
|
+
? toolResponse({
|
|
111
124
|
announcements,
|
|
125
|
+
pagination,
|
|
112
126
|
modifiedSince,
|
|
113
127
|
returned: announcements.length,
|
|
114
128
|
filteredOut: published.length - matched.length,
|
|
115
|
-
}
|
|
116
|
-
: announcements);
|
|
129
|
+
})
|
|
130
|
+
: arrayPage(announcements, pagination);
|
|
117
131
|
}
|
|
118
132
|
catch (error) {
|
|
119
133
|
// A pending sign-in is not an empty course: the route never
|
|
@@ -137,10 +151,11 @@ export function registerGetAnnouncements(server, apiClient, config) {
|
|
|
137
151
|
returned: 0,
|
|
138
152
|
filteredOut: 0,
|
|
139
153
|
authPending: true,
|
|
154
|
+
pagination: pageInfo(offset, count, 0, null, [courseId]),
|
|
140
155
|
notice,
|
|
141
156
|
});
|
|
142
157
|
}
|
|
143
|
-
return
|
|
158
|
+
return arrayPage([], pageInfo(offset, count, 0, null, [courseId]), notice);
|
|
144
159
|
}
|
|
145
160
|
throw error;
|
|
146
161
|
}
|
|
@@ -181,7 +196,7 @@ export function registerGetAnnouncements(server, apiClient, config) {
|
|
|
181
196
|
// 403 means no access (past course, etc) - log and skip
|
|
182
197
|
if (error?.status === 403) {
|
|
183
198
|
log("DEBUG", `get_announcements: 403 Forbidden for course ${item.OrgUnit.Id} (${item.OrgUnit.Name}) - skipping`);
|
|
184
|
-
return
|
|
199
|
+
return { unavailable: true, courseId: item.OrgUnit.Id };
|
|
185
200
|
}
|
|
186
201
|
// A pending sign-in only means this course's route never
|
|
187
202
|
// answered — it says nothing about the other courses, whose
|
|
@@ -201,13 +216,16 @@ export function registerGetAnnouncements(server, apiClient, config) {
|
|
|
201
216
|
.map((r) => r.value);
|
|
202
217
|
const pending = settled.filter((v) => !Array.isArray(v) && v?.authPending === true);
|
|
203
218
|
const allAnnouncements = settled.filter((v) => Array.isArray(v)).flat();
|
|
219
|
+
const unavailableCourseIds = results.flatMap((result, index) => result.status === "rejected" || (!Array.isArray(result.value) && result.value?.courseId)
|
|
220
|
+
? [filteredEnrollments[index].OrgUnit.Id] : []);
|
|
204
221
|
const allMatched = cutoff
|
|
205
222
|
? allAnnouncements.filter((a) => matchesModifiedSince(a.lastModified, cutoff))
|
|
206
223
|
: allAnnouncements;
|
|
207
224
|
// Sort by the scheduled date and slice to count
|
|
208
225
|
const announcements = allMatched
|
|
209
226
|
.sort(newestFirst)
|
|
210
|
-
.slice(
|
|
227
|
+
.slice(offset, offset + count);
|
|
228
|
+
const pagination = pageInfo(offset, count, announcements.length, allMatched.length, unavailableCourseIds);
|
|
211
229
|
log("INFO", `get_announcements: Retrieved ${announcements.length} announcements (out of ${allAnnouncements.length} total ` +
|
|
212
230
|
`across ${enrollmentItems.length} courses${pending.length > 0 ? `, ${pending.length} pending sign-in` : ""})`);
|
|
213
231
|
// With modifiedSince the response is already an object (the shape
|
|
@@ -218,14 +236,15 @@ export function registerGetAnnouncements(server, apiClient, config) {
|
|
|
218
236
|
// the notice instead of changing content[0].
|
|
219
237
|
if (!modifiedSince) {
|
|
220
238
|
if (pending.length === 0)
|
|
221
|
-
return
|
|
222
|
-
return
|
|
239
|
+
return arrayPage(announcements, pagination);
|
|
240
|
+
return arrayPage(announcements, pagination, "Sign-in to Brightspace is still in progress, so announcements for " +
|
|
223
241
|
`${pending.length} course(s) (${pending.map((c) => c.courseId).join(", ")}) could not be ` +
|
|
224
242
|
`fetched yet. ${authPendingNotice(pending[0].authError)} Call get_announcements again once ` +
|
|
225
243
|
"sign-in finishes.");
|
|
226
244
|
}
|
|
227
245
|
const response = {
|
|
228
246
|
announcements,
|
|
247
|
+
pagination,
|
|
229
248
|
modifiedSince,
|
|
230
249
|
returned: announcements.length,
|
|
231
250
|
filteredOut: allAnnouncements.length - allMatched.length,
|
package/build/tools/schemas.js
CHANGED
|
@@ -37,7 +37,8 @@ export const GetMyGradesSchema = z.object({
|
|
|
37
37
|
});
|
|
38
38
|
export const GetAnnouncementsSchema = z.object({
|
|
39
39
|
courseId: z.coerce.number().int().positive().optional().describe("Course ID to get announcements for. If omitted, returns recent announcements across all courses."),
|
|
40
|
-
count: z.coerce.number().int().min(1).max(50).default(10).describe("
|
|
40
|
+
count: z.coerce.number().int().min(1).max(50).default(10).describe("Announcements per page: 1-50 (default 10)."),
|
|
41
|
+
offset: z.coerce.number().int().min(0).default(0).describe("Announcement offset after filtering and sorting. Continue with pagination.nextOffset; restart at 0 after a failed course becomes available."),
|
|
41
42
|
modifiedSince: z.string().datetime({ offset: true, message: "modifiedSince must be an ISO 8601 datetime, e.g. 2026-01-15T00:00:00Z" }).optional()
|
|
42
43
|
.describe("Only return announcements last modified at or after this ISO 8601 datetime (e.g. 2026-01-15T00:00:00Z). Announcements with no modified timestamp are always included. When set, the response reports how many announcements were filtered out."),
|
|
43
44
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.8",
|
|
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",
|