brightspace-mcp-server 3.1.0 → 3.3.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.
Files changed (281) hide show
  1. package/README.md +1 -0
  2. package/build/api/cache.js +0 -1
  3. package/build/api/client.js +0 -1
  4. package/build/api/errors.js +0 -1
  5. package/build/api/index.js +0 -1
  6. package/build/api/paginate.js +0 -1
  7. package/build/api/rate-limiter.js +0 -1
  8. package/build/api/retry.js +0 -1
  9. package/build/api/types.js +0 -1
  10. package/build/api/version-discovery.js +0 -1
  11. package/build/auth/auth-cooldown.js +0 -1
  12. package/build/auth/auth-lock.js +0 -1
  13. package/build/auth/auth-runner.js +0 -1
  14. package/build/auth/browser-auth.js +0 -1
  15. package/build/auth/browser-state-store.js +0 -1
  16. package/build/auth/credential-store.js +0 -1
  17. package/build/auth/duo-mfa.js +0 -1
  18. package/build/auth/encrypted-store.js +0 -1
  19. package/build/auth/index.js +0 -1
  20. package/build/auth/legacy-profile.js +0 -1
  21. package/build/auth/legacy-state.js +0 -1
  22. package/build/auth/purdue-sso.js +0 -1
  23. package/build/auth/session-store.js +0 -1
  24. package/build/auth/sso-flow.js +0 -1
  25. package/build/auth/suny-sso.js +0 -1
  26. package/build/auth/token-manager.js +0 -1
  27. package/build/auth/token-mint.js +0 -1
  28. package/build/auth/western-sso.js +0 -1
  29. package/build/auth-cli.js +0 -1
  30. package/build/index.js +4 -4
  31. package/build/setup.js +0 -1
  32. package/build/tools/download-file.js +0 -1
  33. package/build/tools/get-announcements.js +0 -3
  34. package/build/tools/get-assignment-files.js +0 -1
  35. package/build/tools/get-assignments.js +5 -6
  36. package/build/tools/get-classlist-emails.js +0 -1
  37. package/build/tools/get-course-content.js +8 -9
  38. package/build/tools/get-discussions.js +0 -1
  39. package/build/tools/get-my-courses.js +0 -1
  40. package/build/tools/get-my-grades.js +0 -1
  41. package/build/tools/get-roster.js +0 -1
  42. package/build/tools/get-syllabus.js +1 -2
  43. package/build/tools/get-upcoming-due-dates.js +0 -1
  44. package/build/tools/get-video-transcript.js +167 -0
  45. package/build/tools/index.js +1 -1
  46. package/build/tools/schemas.js +12 -1
  47. package/build/tools/tool-helpers.js +1 -2
  48. package/build/types/index.js +0 -1
  49. package/build/update.js +0 -1
  50. package/build/utils/atomic-write.js +0 -1
  51. package/build/utils/browser-install.js +0 -1
  52. package/build/utils/commands.js +0 -1
  53. package/build/utils/config-store.js +0 -1
  54. package/build/utils/config.js +0 -1
  55. package/build/utils/course-filter.js +0 -1
  56. package/build/utils/deep-links.js +0 -1
  57. package/build/utils/download-errors.js +0 -1
  58. package/build/utils/download-helpers.js +0 -1
  59. package/build/utils/errors.js +0 -1
  60. package/build/utils/file-validator.js +10 -2
  61. package/build/utils/html-converter.js +16 -11
  62. package/build/utils/install-sites.js +0 -1
  63. package/build/utils/logger.js +0 -1
  64. package/build/utils/mcp-client-cli.js +0 -1
  65. package/build/utils/modified-since.js +0 -1
  66. package/build/utils/pdf-extractor.js +0 -1
  67. package/build/utils/secure-config.js +0 -1
  68. package/build/utils/self-update.js +0 -1
  69. package/build/utils/transcript/captions.js +62 -0
  70. package/build/utils/transcript/errors.js +19 -0
  71. package/build/utils/transcript/kaltura.js +93 -0
  72. package/build/utils/transcript/platform.js +97 -0
  73. package/build/utils/transcript/types.js +6 -0
  74. package/build/utils/transcript/youtube.js +56 -0
  75. package/build/utils/update-checker.js +0 -1
  76. package/build/utils/zip-extract.js +0 -1
  77. package/package.json +1 -2
  78. package/build/api/cache.d.ts +0 -15
  79. package/build/api/cache.d.ts.map +0 -1
  80. package/build/api/cache.js.map +0 -1
  81. package/build/api/client.d.ts +0 -141
  82. package/build/api/client.d.ts.map +0 -1
  83. package/build/api/client.js.map +0 -1
  84. package/build/api/errors.d.ts +0 -27
  85. package/build/api/errors.d.ts.map +0 -1
  86. package/build/api/errors.js.map +0 -1
  87. package/build/api/index.d.ts +0 -13
  88. package/build/api/index.d.ts.map +0 -1
  89. package/build/api/index.js.map +0 -1
  90. package/build/api/paginate.d.ts +0 -23
  91. package/build/api/paginate.d.ts.map +0 -1
  92. package/build/api/paginate.js.map +0 -1
  93. package/build/api/rate-limiter.d.ts +0 -17
  94. package/build/api/rate-limiter.d.ts.map +0 -1
  95. package/build/api/rate-limiter.js.map +0 -1
  96. package/build/api/retry.d.ts +0 -42
  97. package/build/api/retry.d.ts.map +0 -1
  98. package/build/api/retry.js.map +0 -1
  99. package/build/api/types.d.ts +0 -39
  100. package/build/api/types.d.ts.map +0 -1
  101. package/build/api/types.js.map +0 -1
  102. package/build/api/version-discovery.d.ts +0 -16
  103. package/build/api/version-discovery.d.ts.map +0 -1
  104. package/build/api/version-discovery.js.map +0 -1
  105. package/build/auth/auth-cooldown.d.ts +0 -24
  106. package/build/auth/auth-cooldown.d.ts.map +0 -1
  107. package/build/auth/auth-cooldown.js.map +0 -1
  108. package/build/auth/auth-lock.d.ts +0 -7
  109. package/build/auth/auth-lock.d.ts.map +0 -1
  110. package/build/auth/auth-lock.js.map +0 -1
  111. package/build/auth/auth-runner.d.ts +0 -61
  112. package/build/auth/auth-runner.d.ts.map +0 -1
  113. package/build/auth/auth-runner.js.map +0 -1
  114. package/build/auth/browser-auth.d.ts +0 -94
  115. package/build/auth/browser-auth.d.ts.map +0 -1
  116. package/build/auth/browser-auth.js.map +0 -1
  117. package/build/auth/browser-state-store.d.ts +0 -19
  118. package/build/auth/browser-state-store.d.ts.map +0 -1
  119. package/build/auth/browser-state-store.js.map +0 -1
  120. package/build/auth/credential-store.d.ts +0 -29
  121. package/build/auth/credential-store.d.ts.map +0 -1
  122. package/build/auth/credential-store.js.map +0 -1
  123. package/build/auth/duo-mfa.d.ts +0 -23
  124. package/build/auth/duo-mfa.d.ts.map +0 -1
  125. package/build/auth/duo-mfa.js.map +0 -1
  126. package/build/auth/encrypted-store.d.ts +0 -25
  127. package/build/auth/encrypted-store.d.ts.map +0 -1
  128. package/build/auth/encrypted-store.js.map +0 -1
  129. package/build/auth/index.d.ts +0 -14
  130. package/build/auth/index.d.ts.map +0 -1
  131. package/build/auth/index.js.map +0 -1
  132. package/build/auth/legacy-profile.d.ts +0 -3
  133. package/build/auth/legacy-profile.d.ts.map +0 -1
  134. package/build/auth/legacy-profile.js.map +0 -1
  135. package/build/auth/legacy-state.d.ts +0 -14
  136. package/build/auth/legacy-state.d.ts.map +0 -1
  137. package/build/auth/legacy-state.js.map +0 -1
  138. package/build/auth/purdue-sso.d.ts +0 -57
  139. package/build/auth/purdue-sso.d.ts.map +0 -1
  140. package/build/auth/purdue-sso.js.map +0 -1
  141. package/build/auth/session-store.d.ts +0 -28
  142. package/build/auth/session-store.d.ts.map +0 -1
  143. package/build/auth/session-store.js.map +0 -1
  144. package/build/auth/sso-flow.d.ts +0 -42
  145. package/build/auth/sso-flow.d.ts.map +0 -1
  146. package/build/auth/sso-flow.js.map +0 -1
  147. package/build/auth/suny-sso.d.ts +0 -53
  148. package/build/auth/suny-sso.d.ts.map +0 -1
  149. package/build/auth/suny-sso.js.map +0 -1
  150. package/build/auth/token-manager.d.ts +0 -74
  151. package/build/auth/token-manager.d.ts.map +0 -1
  152. package/build/auth/token-manager.js.map +0 -1
  153. package/build/auth/token-mint.d.ts +0 -32
  154. package/build/auth/token-mint.d.ts.map +0 -1
  155. package/build/auth/token-mint.js.map +0 -1
  156. package/build/auth/western-sso.d.ts +0 -15
  157. package/build/auth/western-sso.d.ts.map +0 -1
  158. package/build/auth/western-sso.js.map +0 -1
  159. package/build/auth-cli.d.ts +0 -4
  160. package/build/auth-cli.d.ts.map +0 -1
  161. package/build/auth-cli.js.map +0 -1
  162. package/build/index.d.ts +0 -10
  163. package/build/index.d.ts.map +0 -1
  164. package/build/index.js.map +0 -1
  165. package/build/setup.d.ts +0 -10
  166. package/build/setup.d.ts.map +0 -1
  167. package/build/setup.js.map +0 -1
  168. package/build/tools/download-file.d.ts +0 -16
  169. package/build/tools/download-file.d.ts.map +0 -1
  170. package/build/tools/download-file.js.map +0 -1
  171. package/build/tools/get-announcements.d.ts +0 -77
  172. package/build/tools/get-announcements.d.ts.map +0 -1
  173. package/build/tools/get-announcements.js.map +0 -1
  174. package/build/tools/get-assignment-files.d.ts +0 -12
  175. package/build/tools/get-assignment-files.d.ts.map +0 -1
  176. package/build/tools/get-assignment-files.js.map +0 -1
  177. package/build/tools/get-assignments.d.ts +0 -19
  178. package/build/tools/get-assignments.d.ts.map +0 -1
  179. package/build/tools/get-assignments.js.map +0 -1
  180. package/build/tools/get-classlist-emails.d.ts +0 -12
  181. package/build/tools/get-classlist-emails.d.ts.map +0 -1
  182. package/build/tools/get-classlist-emails.js.map +0 -1
  183. package/build/tools/get-course-content.d.ts +0 -12
  184. package/build/tools/get-course-content.d.ts.map +0 -1
  185. package/build/tools/get-course-content.js.map +0 -1
  186. package/build/tools/get-discussions.d.ts +0 -12
  187. package/build/tools/get-discussions.d.ts.map +0 -1
  188. package/build/tools/get-discussions.js.map +0 -1
  189. package/build/tools/get-my-courses.d.ts +0 -13
  190. package/build/tools/get-my-courses.d.ts.map +0 -1
  191. package/build/tools/get-my-courses.js.map +0 -1
  192. package/build/tools/get-my-grades.d.ts +0 -13
  193. package/build/tools/get-my-grades.d.ts.map +0 -1
  194. package/build/tools/get-my-grades.js.map +0 -1
  195. package/build/tools/get-roster.d.ts +0 -12
  196. package/build/tools/get-roster.d.ts.map +0 -1
  197. package/build/tools/get-roster.js.map +0 -1
  198. package/build/tools/get-syllabus.d.ts +0 -12
  199. package/build/tools/get-syllabus.d.ts.map +0 -1
  200. package/build/tools/get-syllabus.js.map +0 -1
  201. package/build/tools/get-upcoming-due-dates.d.ts +0 -13
  202. package/build/tools/get-upcoming-due-dates.d.ts.map +0 -1
  203. package/build/tools/get-upcoming-due-dates.js.map +0 -1
  204. package/build/tools/index.d.ts +0 -20
  205. package/build/tools/index.d.ts.map +0 -1
  206. package/build/tools/index.js.map +0 -1
  207. package/build/tools/schemas.d.ts +0 -76
  208. package/build/tools/schemas.d.ts.map +0 -1
  209. package/build/tools/schemas.js.map +0 -1
  210. package/build/tools/tool-helpers.d.ts +0 -27
  211. package/build/tools/tool-helpers.d.ts.map +0 -1
  212. package/build/tools/tool-helpers.js.map +0 -1
  213. package/build/types/index.d.ts +0 -63
  214. package/build/types/index.d.ts.map +0 -1
  215. package/build/types/index.js.map +0 -1
  216. package/build/update.d.ts +0 -10
  217. package/build/update.d.ts.map +0 -1
  218. package/build/update.js.map +0 -1
  219. package/build/utils/atomic-write.d.ts +0 -18
  220. package/build/utils/atomic-write.d.ts.map +0 -1
  221. package/build/utils/atomic-write.js.map +0 -1
  222. package/build/utils/browser-install.d.ts +0 -23
  223. package/build/utils/browser-install.d.ts.map +0 -1
  224. package/build/utils/browser-install.js.map +0 -1
  225. package/build/utils/commands.d.ts +0 -26
  226. package/build/utils/commands.d.ts.map +0 -1
  227. package/build/utils/commands.js.map +0 -1
  228. package/build/utils/config-store.d.ts +0 -24
  229. package/build/utils/config-store.d.ts.map +0 -1
  230. package/build/utils/config-store.js.map +0 -1
  231. package/build/utils/config.d.ts +0 -10
  232. package/build/utils/config.d.ts.map +0 -1
  233. package/build/utils/config.js.map +0 -1
  234. package/build/utils/course-filter.d.ts +0 -31
  235. package/build/utils/course-filter.d.ts.map +0 -1
  236. package/build/utils/course-filter.js.map +0 -1
  237. package/build/utils/deep-links.d.ts +0 -17
  238. package/build/utils/deep-links.d.ts.map +0 -1
  239. package/build/utils/deep-links.js.map +0 -1
  240. package/build/utils/download-errors.d.ts +0 -45
  241. package/build/utils/download-errors.d.ts.map +0 -1
  242. package/build/utils/download-errors.js.map +0 -1
  243. package/build/utils/download-helpers.d.ts +0 -31
  244. package/build/utils/download-helpers.d.ts.map +0 -1
  245. package/build/utils/download-helpers.js.map +0 -1
  246. package/build/utils/errors.d.ts +0 -21
  247. package/build/utils/errors.d.ts.map +0 -1
  248. package/build/utils/errors.js.map +0 -1
  249. package/build/utils/file-validator.d.ts +0 -56
  250. package/build/utils/file-validator.d.ts.map +0 -1
  251. package/build/utils/file-validator.js.map +0 -1
  252. package/build/utils/html-converter.d.ts +0 -17
  253. package/build/utils/html-converter.d.ts.map +0 -1
  254. package/build/utils/html-converter.js.map +0 -1
  255. package/build/utils/install-sites.d.ts +0 -60
  256. package/build/utils/install-sites.d.ts.map +0 -1
  257. package/build/utils/install-sites.js.map +0 -1
  258. package/build/utils/logger.d.ts +0 -10
  259. package/build/utils/logger.d.ts.map +0 -1
  260. package/build/utils/logger.js.map +0 -1
  261. package/build/utils/mcp-client-cli.d.ts +0 -12
  262. package/build/utils/mcp-client-cli.d.ts.map +0 -1
  263. package/build/utils/mcp-client-cli.js.map +0 -1
  264. package/build/utils/modified-since.d.ts +0 -19
  265. package/build/utils/modified-since.d.ts.map +0 -1
  266. package/build/utils/modified-since.js.map +0 -1
  267. package/build/utils/pdf-extractor.d.ts +0 -14
  268. package/build/utils/pdf-extractor.d.ts.map +0 -1
  269. package/build/utils/pdf-extractor.js.map +0 -1
  270. package/build/utils/secure-config.d.ts +0 -6
  271. package/build/utils/secure-config.d.ts.map +0 -1
  272. package/build/utils/secure-config.js.map +0 -1
  273. package/build/utils/self-update.d.ts +0 -61
  274. package/build/utils/self-update.d.ts.map +0 -1
  275. package/build/utils/self-update.js.map +0 -1
  276. package/build/utils/update-checker.d.ts +0 -112
  277. package/build/utils/update-checker.d.ts.map +0 -1
  278. package/build/utils/update-checker.js.map +0 -1
  279. package/build/utils/zip-extract.d.ts +0 -15
  280. package/build/utils/zip-extract.d.ts.map +0 -1
  281. package/build/utils/zip-extract.js.map +0 -1
@@ -41,4 +41,3 @@ export function applyCourseFilter(courses, config) {
41
41
  }
42
42
  return filtered;
43
43
  }
44
- //# sourceMappingURL=course-filter.js.map
@@ -32,4 +32,3 @@ export function gradebookUrl(baseUrl, courseId) {
32
32
  export function discussionUrl(baseUrl, courseId, topicId) {
33
33
  return `${trimBaseUrl(baseUrl)}/d2l/lms/discussions/threadlist.d2l?ou=${courseId}&tId=${topicId}`;
34
34
  }
35
- //# sourceMappingURL=deep-links.js.map
@@ -42,4 +42,3 @@ export class DownloadError extends Error {
42
42
  export function isSafeDetail(value) {
43
43
  return value.length <= 64 && /^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/.test(value);
44
44
  }
45
- //# sourceMappingURL=download-errors.js.map
@@ -91,4 +91,3 @@ export async function secureDownload(options) {
91
91
  mime,
92
92
  };
93
93
  }
94
- //# sourceMappingURL=download-helpers.js.map
@@ -33,4 +33,3 @@ export class SessionStoreError extends AuthError {
33
33
  this.name = "SessionStoreError";
34
34
  }
35
35
  }
36
- //# sourceMappingURL=errors.js.map
@@ -4,7 +4,6 @@
4
4
  * Licensed under MIT — see LICENSE file for details.
5
5
  */
6
6
  import path from "node:path";
7
- import { fileTypeFromBuffer } from "file-type";
8
7
  import sanitizeFilename from "sanitize-filename";
9
8
  import { DownloadError } from "./download-errors.js";
10
9
  /**
@@ -20,6 +19,15 @@ import { DownloadError } from "./download-errors.js";
20
19
  * extension instead, which admits the formats the allowlist already intended
21
20
  * and still refuses everything else.
22
21
  */
22
+ // file-type v21 is ESM-only and costs ~50ms; defer the import to first use
23
+ // since validateFileType is already async.
24
+ let fileTypeFromBufferPromise;
25
+ function getFileTypeFromBuffer() {
26
+ if (!fileTypeFromBufferPromise) {
27
+ fileTypeFromBufferPromise = import("file-type").then((m) => m.fileTypeFromBuffer);
28
+ }
29
+ return fileTypeFromBufferPromise;
30
+ }
23
31
  const CFB_MIME = "application/x-cfb";
24
32
  const CFB_EXTENSION_MIMES = {
25
33
  ".doc": "application/msword",
@@ -113,6 +121,7 @@ export function validateDownloadPath(baseDir, filename) {
113
121
  */
114
122
  export async function validateFileType(buffer, allowedTypes = ALLOWED_MIME_TYPES, filename) {
115
123
  // Try magic byte detection first
124
+ const fileTypeFromBuffer = await getFileTypeFromBuffer();
116
125
  const detected = await fileTypeFromBuffer(buffer);
117
126
  if (detected) {
118
127
  if (detected.mime === CFB_MIME) {
@@ -181,4 +190,3 @@ export function validateBaseUrl(url, expectedBaseUrl) {
181
190
  throw new Error(`URL must start with ${expectedBaseUrl}, got: ${url.substring(0, 50)}...`);
182
191
  }
183
192
  }
184
- //# sourceMappingURL=file-validator.js.map
@@ -3,15 +3,21 @@
3
3
  * Copyright (c) 2026 Rohan Muppa. All rights reserved.
4
4
  * Licensed under MIT — see LICENSE file for details.
5
5
  */
6
- import TurndownService from "turndown";
7
- /**
8
- * Singleton TurndownService instance configured for converting D2L HTML to markdown.
9
- * Uses ATX-style headings (###) and fenced code blocks (```).
10
- */
11
- const turndownService = new TurndownService({
12
- headingStyle: "atx",
13
- codeBlockStyle: "fenced",
14
- });
6
+ import { createRequire } from "node:module";
7
+ // turndown is CJS and costs ~50ms to import; defer it to first use via
8
+ // createRequire so callers keep a synchronous API instead of turning async.
9
+ const require = createRequire(import.meta.url);
10
+ let turndownService;
11
+ function getTurndownService() {
12
+ if (!turndownService) {
13
+ const TurndownService = require("turndown");
14
+ turndownService = new TurndownService({
15
+ headingStyle: "atx",
16
+ codeBlockStyle: "fenced",
17
+ });
18
+ }
19
+ return turndownService;
20
+ }
15
21
  /**
16
22
  * Convert D2L HTML content to clean markdown.
17
23
  * Returns both markdown (for LLM readability) and raw HTML (for fallback).
@@ -25,7 +31,7 @@ export function convertHtmlToMarkdown(html) {
25
31
  return { markdown: "", html: "" };
26
32
  }
27
33
  try {
28
- const markdown = turndownService.turndown(html);
34
+ const markdown = getTurndownService().turndown(html);
29
35
  return { markdown, html };
30
36
  }
31
37
  catch (error) {
@@ -33,4 +39,3 @@ export function convertHtmlToMarkdown(html) {
33
39
  return { markdown: html, html };
34
40
  }
35
41
  }
36
- //# sourceMappingURL=html-converter.js.map
@@ -194,4 +194,3 @@ export function formatSkewNotice(sites, runningVersion, home = homedir()) {
194
194
  `An out-of-date auth CLI fails during sign-in. Run \`${GLOBAL_INSTALL_COMMAND}\`, or use \`${AUTH_COMMAND}\`.`,
195
195
  ].join("\n");
196
196
  }
197
- //# sourceMappingURL=install-sites.js.map
@@ -83,4 +83,3 @@ export function enableStdoutGuard() {
83
83
  console.error("[WARN] console.log intercepted (would corrupt stdio):", ...args);
84
84
  };
85
85
  }
86
- //# sourceMappingURL=logger.js.map
@@ -39,4 +39,3 @@ export function configureCliMcpClient(client, runCommand = defaultRunCommand) {
39
39
  ? "configured"
40
40
  : "failed";
41
41
  }
42
- //# sourceMappingURL=mcp-client-cli.js.map
@@ -23,4 +23,3 @@ export function matchesModifiedSince(lastModified, cutoff) {
23
23
  return true;
24
24
  return time >= cutoff.getTime();
25
25
  }
26
- //# sourceMappingURL=modified-since.js.map
@@ -24,4 +24,3 @@ export async function extractPdfText(buffer) {
24
24
  return null;
25
25
  }
26
26
  }
27
- //# sourceMappingURL=pdf-extractor.js.map
@@ -54,4 +54,3 @@ export async function resolveStoredPassword(baseUrl, username, store) {
54
54
  }
55
55
  return (await getStoredPassword(baseUrl, username)) ?? undefined;
56
56
  }
57
- //# sourceMappingURL=secure-config.js.map
@@ -89,4 +89,3 @@ export async function reexecLatestIfStale(deps = {}) {
89
89
  child.on("exit", (code) => resolve(code ?? 1));
90
90
  });
91
91
  }
92
- //# sourceMappingURL=self-update.js.map
@@ -0,0 +1,62 @@
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
+ /** "01:02:03.456" or "01:02:03,456" or the hours-omitted "02:03.456". */
7
+ function parseTimestamp(raw) {
8
+ const match = raw.trim().match(/^(?:(\d+):)?(\d{2}):(\d{2})[.,](\d{3})$/);
9
+ if (!match)
10
+ return NaN;
11
+ const [, hours, minutes, seconds, millis] = match;
12
+ return ((Number(hours ?? 0) * 3600 + Number(minutes) * 60 + Number(seconds)) * 1000 +
13
+ Number(millis));
14
+ }
15
+ /**
16
+ * Parse WebVTT or SRT text into cues. Format is detected from the WEBVTT
17
+ * header rather than taken on faith, since a caller only knows what the
18
+ * platform's API claims to have served.
19
+ */
20
+ export function parseCaptions(raw) {
21
+ const normalized = raw.replace(/^/, "").replace(/\r\n?/g, "\n");
22
+ const format = /^WEBVTT/.test(normalized.trimStart()) ? "vtt" : "srt";
23
+ const cues = [];
24
+ for (const block of normalized.split(/\n{2,}/)) {
25
+ const lines = block.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
26
+ const timingIndex = lines.findIndex((l) => l.includes("-->"));
27
+ if (timingIndex === -1)
28
+ continue;
29
+ const [startRaw, endRaw] = lines[timingIndex].split("-->");
30
+ const startMs = parseTimestamp(startRaw.trim().split(/\s+/)[0] ?? "");
31
+ const endMs = parseTimestamp((endRaw ?? "").trim().split(/\s+/)[0] ?? "");
32
+ if (Number.isNaN(startMs) || Number.isNaN(endMs))
33
+ continue;
34
+ const text = lines
35
+ .slice(timingIndex + 1)
36
+ .join(" ")
37
+ .replace(/<[^>]+>/g, "")
38
+ .trim();
39
+ if (text)
40
+ cues.push({ startMs, endMs, text });
41
+ }
42
+ return { format, cues };
43
+ }
44
+ /** "H:MM:SS", matching how course-length recordings are usually captioned. */
45
+ export function formatTimestamp(ms) {
46
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
47
+ const hours = Math.floor(totalSeconds / 3600);
48
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
49
+ const seconds = totalSeconds % 60;
50
+ return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
51
+ }
52
+ /** One line per cue, timestamped, in reading order. */
53
+ export function cuesToText(cues) {
54
+ return cues.map((cue) => `[${formatTimestamp(cue.startMs)}] ${cue.text}`).join("\n");
55
+ }
56
+ /** Character-offset paging, mirroring the maxChars/truncated convention used elsewhere for large text. */
57
+ export function paginateText(text, offset, maxChars) {
58
+ const totalChars = text.length;
59
+ const window = text.slice(offset, offset + maxChars);
60
+ const truncated = offset + maxChars < totalChars;
61
+ return { window, truncated, nextOffset: truncated ? offset + maxChars : null, totalChars };
62
+ }
@@ -0,0 +1,19 @@
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
+ /** The video exists and answered, but has no captions — an expected outcome, not a failure. */
7
+ export class NoTranscriptError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "NoTranscriptError";
11
+ }
12
+ }
13
+ /** The platform's API could not be reached or rejected the request. */
14
+ export class TranscriptFetchError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "TranscriptFetchError";
18
+ }
19
+ }
@@ -0,0 +1,93 @@
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
+ /**
7
+ * Kaltura transcript retrieval — the priority-one platform from issue #33,
8
+ * since Purdue's BoilerCast and many other D2L schools run on it.
9
+ *
10
+ * This does not reuse the Brightspace session: Kaltura is a separate SaaS
11
+ * tenant with its own auth, and an embedded player normally authenticates
12
+ * to it with a "widget session" (a KS token scoped read-only to the entries
13
+ * a partner has made embeddable), not the D2L cookie. That widget-session
14
+ * flow is the same mechanism generic Kaltura embed players use, and it is
15
+ * anonymous by design — it needs no Brightspace credential to obtain. A
16
+ * captions request that fails because the entry actually requires an
17
+ * authenticated (non-widget) session surfaces as TranscriptFetchError, which
18
+ * the tool turns into a "open it in Brightspace directly" message rather
19
+ * than a silent empty result.
20
+ */
21
+ import { parseCaptions } from "./captions.js";
22
+ import { NoTranscriptError, TranscriptFetchError } from "./errors.js";
23
+ const KALTURA_API_BASE = "https://cdnapisec.kaltura.com/api_v3/service";
24
+ async function startWidgetSession(partnerId, fetchImpl) {
25
+ const res = await fetchImpl(`${KALTURA_API_BASE}/session/action/startWidgetSession?widgetId=_${encodeURIComponent(partnerId)}&format=1`);
26
+ if (!res.ok) {
27
+ throw new TranscriptFetchError(`Kaltura session request failed (HTTP ${res.status}).`);
28
+ }
29
+ const data = (await res.json());
30
+ if (data.error) {
31
+ throw new TranscriptFetchError(`Kaltura rejected the session request: ${data.error.message ?? "unknown error"}.`);
32
+ }
33
+ if (!data.ks) {
34
+ throw new TranscriptFetchError("Kaltura did not return a session token.");
35
+ }
36
+ return data.ks;
37
+ }
38
+ async function listCaptionAssets(ks, entryId, fetchImpl) {
39
+ const res = await fetchImpl(`${KALTURA_API_BASE}/caption_captionasset/action/list?format=1` +
40
+ `&ks=${encodeURIComponent(ks)}&filter:entryIdEqual=${encodeURIComponent(entryId)}`);
41
+ if (!res.ok) {
42
+ throw new TranscriptFetchError(`Kaltura caption list request failed (HTTP ${res.status}).`);
43
+ }
44
+ const data = (await res.json());
45
+ if (data.error) {
46
+ throw new TranscriptFetchError(`Kaltura rejected the caption list request: ${data.error.message ?? "unknown error"}.`);
47
+ }
48
+ return data.objects ?? [];
49
+ }
50
+ async function serveCaption(ks, captionAssetId, fetchImpl) {
51
+ const res = await fetchImpl(`${KALTURA_API_BASE}/caption_captionasset/action/serve?ks=${encodeURIComponent(ks)}` +
52
+ `&captionAssetId=${encodeURIComponent(captionAssetId)}`);
53
+ if (!res.ok) {
54
+ throw new TranscriptFetchError(`Kaltura caption download failed (HTTP ${res.status}).`);
55
+ }
56
+ return res.text();
57
+ }
58
+ /** Best-effort title/duration — a caption-only entry still has a usable transcript without this. */
59
+ async function fetchMediaInfo(ks, entryId, fetchImpl) {
60
+ try {
61
+ const res = await fetchImpl(`${KALTURA_API_BASE}/media/action/get?ks=${encodeURIComponent(ks)}&entryId=${encodeURIComponent(entryId)}`);
62
+ if (!res.ok)
63
+ return { title: null, durationSeconds: null };
64
+ const data = (await res.json());
65
+ if (data.error)
66
+ return { title: null, durationSeconds: null };
67
+ return { title: data.name ?? null, durationSeconds: typeof data.duration === "number" ? data.duration : null };
68
+ }
69
+ catch {
70
+ return { title: null, durationSeconds: null };
71
+ }
72
+ }
73
+ export async function getKalturaTranscript(partnerId, entryId, fetchImpl) {
74
+ const ks = await startWidgetSession(partnerId, fetchImpl);
75
+ const assets = await listCaptionAssets(ks, entryId, fetchImpl);
76
+ if (assets.length === 0) {
77
+ throw new NoTranscriptError("This Kaltura video has no captions available.");
78
+ }
79
+ const asset = assets.find((a) => a.isDefault) ?? assets[0];
80
+ const raw = await serveCaption(ks, asset.id, fetchImpl);
81
+ const { format, cues } = parseCaptions(raw);
82
+ if (cues.length === 0) {
83
+ throw new NoTranscriptError("This Kaltura video has a caption track, but it contained no readable cues.");
84
+ }
85
+ const media = await fetchMediaInfo(ks, entryId, fetchImpl);
86
+ return {
87
+ cues,
88
+ format,
89
+ language: asset.languageCode ?? asset.language ?? null,
90
+ title: media.title,
91
+ durationSeconds: media.durationSeconds,
92
+ };
93
+ }
@@ -0,0 +1,97 @@
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
+ function safeHostname(url) {
7
+ try {
8
+ return new URL(url).hostname.toLowerCase();
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ /**
15
+ * Kaltura's KAF "browseAndEmbed" front end (what a school's own branded
16
+ * domain, e.g. BoilerCast, typically uses) puts params as path segments
17
+ * rather than a query string: `/browseandembed/index/media/entry_id/1_abc/wid/_123`.
18
+ */
19
+ function pathSegmentParam(pathname, name) {
20
+ const match = pathname.match(new RegExp(`/${name}/([^/]+)`, "i"));
21
+ return match?.[1] ?? null;
22
+ }
23
+ export function detectVideoPlatform(url) {
24
+ const host = safeHostname(url);
25
+ if (!host)
26
+ return "unknown";
27
+ // Kaltura is embedded either from its own multi-tenant domains (kaltura.com,
28
+ // mediaspace.kaltura.com) or from a school's own KAF front end (e.g.
29
+ // BoilerCast at Purdue), which is why entry_id/wid are also checked as
30
+ // query params or path segments before falling back to "unknown".
31
+ if (/kaltura/.test(host))
32
+ return "kaltura";
33
+ if (/youtube\.com$|youtu\.be$/.test(host))
34
+ return "youtube";
35
+ if (/panopto/.test(host))
36
+ return "panopto";
37
+ if (/yuja/.test(host))
38
+ return "yuja";
39
+ if (/echo360/.test(host))
40
+ return "echo360";
41
+ if (/vimeo/.test(host))
42
+ return "vimeo";
43
+ if (extractKalturaIds(url))
44
+ return "kaltura";
45
+ return "unknown";
46
+ }
47
+ /** Kaltura embeds carry the entry and partner ID as query params or path segments; naming varies by embed generator. */
48
+ export function extractKalturaIds(url) {
49
+ let parsed;
50
+ try {
51
+ parsed = new URL(url);
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ const params = parsed.searchParams;
57
+ const pathname = parsed.pathname;
58
+ const pathEntryMatch = pathname.match(/\/(?:media\/t|id|entryid)\/([\w-]+)/i);
59
+ const entryId = params.get("entry_id") ??
60
+ params.get("entryId") ??
61
+ pathEntryMatch?.[1] ??
62
+ pathSegmentParam(pathname, "entry_id") ??
63
+ null;
64
+ const widParam = params.get("wid") ??
65
+ params.get("partner_id") ??
66
+ params.get("partnerId") ??
67
+ params.get("pid") ??
68
+ pathSegmentParam(pathname, "wid") ??
69
+ pathSegmentParam(pathname, "partner_id");
70
+ const partnerId = widParam ? widParam.replace(/^_/, "") : null;
71
+ if (!entryId || !partnerId)
72
+ return null;
73
+ return { partnerId, entryId };
74
+ }
75
+ /** youtube.com/watch?v=, youtu.be/, and /embed/ links. */
76
+ export function extractYouTubeVideoId(url) {
77
+ let parsed;
78
+ try {
79
+ parsed = new URL(url);
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ const host = parsed.hostname.toLowerCase();
85
+ if (host === "youtu.be" || host.endsWith(".youtu.be")) {
86
+ return parsed.pathname.slice(1).split("/")[0] || null;
87
+ }
88
+ if (host.endsWith("youtube.com") || host.endsWith("youtube-nocookie.com")) {
89
+ const v = parsed.searchParams.get("v");
90
+ if (v)
91
+ return v;
92
+ const embedMatch = parsed.pathname.match(/\/embed\/([\w-]+)/);
93
+ if (embedMatch)
94
+ return embedMatch[1];
95
+ }
96
+ return null;
97
+ }
@@ -0,0 +1,6 @@
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
+ export {};
@@ -0,0 +1,56 @@
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
+ /**
7
+ * YouTube transcript retrieval via its public timedtext endpoint. No
8
+ * authentication is needed — this is the same caption track a signed-out
9
+ * browser can fetch — so unlike Kaltura there is no session to reuse or fail
10
+ * to obtain.
11
+ */
12
+ import { parseCaptions } from "./captions.js";
13
+ import { NoTranscriptError, TranscriptFetchError } from "./errors.js";
14
+ const TIMEDTEXT_BASE = "https://www.youtube.com/api/timedtext";
15
+ async function listCaptionTracks(videoId, fetchImpl) {
16
+ const res = await fetchImpl(`${TIMEDTEXT_BASE}?type=list&v=${encodeURIComponent(videoId)}`);
17
+ if (!res.ok) {
18
+ throw new TranscriptFetchError(`YouTube caption list request failed (HTTP ${res.status}).`);
19
+ }
20
+ const xml = await res.text();
21
+ return [...xml.matchAll(/<track\b[^>]*\blang_code="([^"]+)"/g)].map((m) => m[1]);
22
+ }
23
+ async function fetchTitle(videoId, fetchImpl) {
24
+ try {
25
+ const watchUrl = `https://www.youtube.com/watch?v=${encodeURIComponent(videoId)}`;
26
+ const res = await fetchImpl(`https://www.youtube.com/oembed?url=${encodeURIComponent(watchUrl)}&format=json`);
27
+ if (!res.ok)
28
+ return null;
29
+ const data = (await res.json());
30
+ return data.title ?? null;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ export async function getYouTubeTranscript(videoId, fetchImpl) {
37
+ const tracks = await listCaptionTracks(videoId, fetchImpl);
38
+ if (tracks.length === 0) {
39
+ throw new NoTranscriptError("This YouTube video has no caption tracks available.");
40
+ }
41
+ const lang = tracks.find((t) => t.startsWith("en")) ?? tracks[0];
42
+ const res = await fetchImpl(`${TIMEDTEXT_BASE}?lang=${encodeURIComponent(lang)}&v=${encodeURIComponent(videoId)}&fmt=vtt`);
43
+ if (!res.ok) {
44
+ throw new TranscriptFetchError(`YouTube caption download failed (HTTP ${res.status}).`);
45
+ }
46
+ const vtt = await res.text();
47
+ if (!vtt.trim()) {
48
+ throw new NoTranscriptError("This YouTube video has no caption tracks available.");
49
+ }
50
+ const { format, cues } = parseCaptions(vtt);
51
+ if (cues.length === 0) {
52
+ throw new NoTranscriptError("This YouTube video has a caption track, but it contained no readable cues.");
53
+ }
54
+ const title = await fetchTitle(videoId, fetchImpl);
55
+ return { cues, format, language: lang, title, durationSeconds: null };
56
+ }
@@ -229,4 +229,3 @@ export function getUpdateNotice(now = Date.now(), minIntervalMs = NOTICE_REPEAT_
229
229
  export function clearUpdateNotice() {
230
230
  state = null;
231
231
  }
232
- //# sourceMappingURL=update-checker.js.map
@@ -167,4 +167,3 @@ export function officeDocumentText(buffer) {
167
167
  function slideNumber(name) {
168
168
  return Number(/slide(\d+)\.xml$/.exec(name)?.[1] ?? 0);
169
169
  }
170
- //# sourceMappingURL=zip-extract.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "3.1.0",
3
+ "version": "3.3.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",
@@ -29,7 +29,6 @@
29
29
  "dependencies": {
30
30
  "@modelcontextprotocol/sdk": "^1.26.0",
31
31
  "@napi-rs/keyring": "^2.0.0",
32
- "ajv": "^8.18.0",
33
32
  "dotenv": "^17.2.4",
34
33
  "file-type": "^21.3.0",
35
34
  "playwright": "^1.58.2",
@@ -1,15 +0,0 @@
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
- export declare class TTLCache<T = unknown> {
7
- private cache;
8
- set(key: string, value: T, ttlMs: number): void;
9
- get(key: string): T | undefined;
10
- has(key: string): boolean;
11
- delete(key: string): boolean;
12
- clear(): void;
13
- get size(): number;
14
- }
15
- //# sourceMappingURL=cache.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/api/cache.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,qBAAa,QAAQ,CAAC,CAAC,GAAG,OAAO;IAC/B,OAAO,CAAC,KAAK,CAAoC;IAEjD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAsB/C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAK/B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIzB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAU5B,KAAK,IAAI,IAAI;IASb,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/api/cache.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,MAAM,OAAO,QAAQ;IACX,KAAK,GAAG,IAAI,GAAG,EAAyB,CAAC;IAEjD,GAAG,CAAC,GAAW,EAAE,KAAQ,EAAE,KAAa;QACtC,qCAAqC;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,QAAQ,EAAE,CAAC;YACb,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC;QAED,yCAAyC;QACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAC9B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACzB,CAAC,EAAE,KAAK,CAAC,CAAC;QACV,iEAAiE;QACjE,sEAAsE;QACtE,kCAAkC;QAClC,IAAI,OAAQ,OAAkC,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YACnE,OAAiC,CAAC,KAAK,EAAE,CAAC;QAC7C,CAAC;QAED,cAAc;QACd,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,GAAG,CAAC,GAAW;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,OAAO,KAAK,EAAE,IAAI,CAAC;IACrB,CAAC;IAED,GAAG,CAAC,GAAW;QACb,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,KAAK,EAAE,CAAC;YACV,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK;QACH,mBAAmB;QACnB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YACxC,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;QACD,YAAY;QACZ,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;CACF"}