brightspace-mcp-server 3.13.0 → 3.13.1

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.
@@ -24,6 +24,20 @@ const ENTITY_TYPES = {
24
24
  "D2L.LE.Survey.Survey": "survey",
25
25
  };
26
26
  const HOUR_MS = 60 * 60 * 1000;
27
+ const DAY_MS = 24 * HOUR_MS;
28
+ /**
29
+ * When an event stops counting as upcoming (epoch ms). A timed event counts
30
+ * only until it starts. An all-day event lasts through the end of its last
31
+ * day — EndDay names that day, not the midnight after it — so today's exam
32
+ * day or a week-long break still underway is not dropped as already past.
33
+ */
34
+ export function eventEndsAt(event) {
35
+ const start = new Date(event.start).getTime();
36
+ if (!event.allDay)
37
+ return start;
38
+ const lastDay = event.end ? new Date(event.end).getTime() : start;
39
+ return Math.max(start, Number.isFinite(lastDay) ? lastDay : start) + DAY_MS;
40
+ }
27
41
  function descriptionMarkdown(description) {
28
42
  const html = typeof description === "string" ? description : description?.Html || description?.Text || "";
29
43
  return convertHtmlToMarkdown(html).markdown.trim();
@@ -34,6 +48,7 @@ function toCalendarEvent(raw, baseUrl, course) {
34
48
  const start = raw.StartDateTime ?? raw.StartDay ?? null;
35
49
  if (!start)
36
50
  return null;
51
+ const allDay = !raw.StartDateTime;
37
52
  const end = raw.EndDateTime ?? raw.EndDay ?? null;
38
53
  const location = raw.LocationName?.trim();
39
54
  const description = descriptionMarkdown(raw.Description);
@@ -45,6 +60,7 @@ function toCalendarEvent(raw, baseUrl, course) {
45
60
  courseName: course.name,
46
61
  start,
47
62
  ...(end ? { end } : {}),
63
+ ...(allDay ? { allDay: true } : {}),
48
64
  ...(location ? { location } : {}),
49
65
  ...(description ? { description } : {}),
50
66
  url: calendarUrl(baseUrl, course.id),
@@ -59,13 +75,16 @@ function toCalendarEvent(raw, baseUrl, course) {
59
75
  };
60
76
  }
61
77
  /**
62
- * Every calendar event in one course that starts inside [from, to] (epoch ms).
78
+ * Every calendar event in one course that starts inside [from, to] (epoch ms),
79
+ * plus any all-day event still underway at `from`.
63
80
  *
64
81
  * 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.
82
+ * cache TTL share one cached response, and its start reaches back a day so an
83
+ * all-day event that began at today's midnight is in the response at all; the
84
+ * exact window is applied here.
66
85
  */
67
86
  export async function fetchCourseCalendarEvents(apiClient, baseUrl, course, from, to) {
68
- const requestFrom = new Date(Math.floor(from / HOUR_MS) * HOUR_MS).toISOString();
87
+ const requestFrom = new Date(Math.floor((from - DAY_MS) / HOUR_MS) * HOUR_MS).toISOString();
69
88
  const requestTo = new Date(Math.ceil(to / HOUR_MS) * HOUR_MS).toISOString();
70
89
  const raw = await fetchAllObjects(apiClient, apiClient.le(course.id, `/calendar/events/myEvents/?startDateTime=${encodeURIComponent(requestFrom)}&endDateTime=${encodeURIComponent(requestTo)}`), { ttl: DEFAULT_CACHE_TTLS.calendar });
71
90
  return raw
@@ -74,6 +93,6 @@ export async function fetchCourseCalendarEvents(apiClient, baseUrl, course, from
74
93
  if (!event)
75
94
  return false;
76
95
  const start = new Date(event.start).getTime();
77
- return Number.isFinite(start) && start >= from && start <= to;
96
+ return Number.isFinite(start) && eventEndsAt(event) >= from && start <= to;
78
97
  });
79
98
  }
@@ -12,7 +12,7 @@ import { quizUrl, discussionUrl } from "../utils/deep-links.js";
12
12
  import { assignmentLinkResolver } from "./assignment-links.js";
13
13
  import { dueIn } from "../utils/due-in.js";
14
14
  import { resolveCourses } from "./resolve-courses.js";
15
- import { fetchCourseCalendarEvents } from "./calendar-events.js";
15
+ import { fetchCourseCalendarEvents, eventEndsAt } from "./calendar-events.js";
16
16
  /** D2L list endpoints return either a paged { Objects: [...] } or a flat array. */
17
17
  function unwrapList(raw) {
18
18
  return Array.isArray(raw) ? raw : (raw?.Objects ?? []);
@@ -63,6 +63,7 @@ function calendarItems(events, items) {
63
63
  dueIn: dueIn(event.start),
64
64
  startDate: null,
65
65
  endDate: event.end ?? null,
66
+ ...(event.allDay ? { allDay: true } : {}),
66
67
  ...(event.location ? { location: event.location } : {}),
67
68
  url: event.url,
68
69
  }));
@@ -214,11 +215,15 @@ export function registerGetUpcomingDueDates(server, apiClient, config) {
214
215
  log("DEBUG", "get_upcoming_due_dates: skipping course after fetch failure", result.reason);
215
216
  return [];
216
217
  });
217
- // Keep what falls inside the window, soonest first
218
+ // Keep what falls inside the window, soonest first. An all-day
219
+ // event counts until its last day ends, not just until its midnight start.
218
220
  const upcoming = items
219
221
  .filter((item) => {
220
222
  const due = new Date(item.dueDate).getTime();
221
- return Number.isFinite(due) && due >= now && due <= windowEnd;
223
+ const until = item.allDay
224
+ ? eventEndsAt({ start: item.dueDate, end: item.endDate ?? undefined, allDay: true })
225
+ : due;
226
+ return Number.isFinite(due) && until >= now && due <= windowEnd;
222
227
  })
223
228
  .sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime());
224
229
  log("INFO", `get_upcoming_due_dates: Retrieved ${upcoming.length} items across ${courses.length} courses` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "3.13.0",
3
+ "version": "3.13.1",
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",