brightspace-mcp-server 5.0.3 → 5.0.4

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
@@ -76,7 +76,7 @@ School-specific quirks: [docs/sign-in.md](docs/sign-in.md#per-school-notes).
76
76
  | Syllabus | "What's the late policy in ECE 264's syllabus?" (if the course keeps its syllabus in an external tool such as Simple Syllabus, you get the link to open instead) |
77
77
  | People | "Who are the TAs for ECE 264?" · "Who is in my project group?" |
78
78
  | Discussions | "Summarize the latest posts in the final project thread" |
79
- | Lecture videos | "What did the professor say about pinch-off in Tuesday's recording?" (Kaltura, including BoilerCast LTI links, and YouTube) |
79
+ | Lecture videos | "What did the professor say about pinch-off in Tuesday's recording?" (Kaltura, including BoilerCast LTI links, and YouTube. When YouTube asks for a sign-in or a "not a bot" check, the answer says so, with the video link and how to get the transcript yourself: open the link in your browser, choose "Show transcript", and paste it in. The server never gets past that check for you) |
80
80
  | Calendar | "When is my midterm?" · "Is lab cancelled on Thursday?" |
81
81
  | 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) |
82
82
  | 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)) |
@@ -11,7 +11,7 @@ import { detectVideoPlatform, extractKalturaIds, extractYouTubeVideoId, } from "
11
11
  import { readLtiLaunchPage, toBrightspacePath } from "../utils/transcript/lti.js";
12
12
  import { getKalturaTranscript } from "../utils/transcript/kaltura.js";
13
13
  import { getYouTubeTranscript } from "../utils/transcript/youtube.js";
14
- import { NoTranscriptError, TranscriptFetchError } from "../utils/transcript/errors.js";
14
+ import { NoTranscriptError, TranscriptFetchError, } from "../utils/transcript/errors.js";
15
15
  import { log } from "../utils/logger.js";
16
16
  import { stripD2lSessionParams } from "../utils/session-params.js";
17
17
  const UNSUPPORTED_PLATFORM_LABEL = {
@@ -48,6 +48,24 @@ async function followLtiLaunch(apiClient, path) {
48
48
  }
49
49
  return null;
50
50
  }
51
+ /**
52
+ * What a person can do about each failure. A sign-in or bot check is never
53
+ * worked around here: the way past it is the user's own browser, and the
54
+ * result says plainly that nothing was read.
55
+ */
56
+ function transcriptRecovery(reason, videoUrl) {
57
+ switch (reason) {
58
+ case "sign_in_required":
59
+ case "access_blocked":
60
+ return (`Open ${videoUrl} in your own browser and complete any sign-in or "not a bot" check it shows. ` +
61
+ 'Then choose "Show transcript" (on YouTube, below the video description) and paste the text into ' +
62
+ "this conversation. This server does not get past those checks, and no transcript was read from this video.");
63
+ case "unavailable":
64
+ return `Open ${videoUrl} in your browser to see whether the video was removed or made private.`;
65
+ case "fetch_failed":
66
+ return `Try again in a moment. If it keeps failing, open ${videoUrl} in your browser.`;
67
+ }
68
+ }
51
69
  async function fetchTranscript(platform, videoUrl, fetchImpl) {
52
70
  switch (platform) {
53
71
  case "kaltura": {
@@ -55,6 +73,7 @@ async function fetchTranscript(platform, videoUrl, fetchImpl) {
55
73
  if (!ids) {
56
74
  return {
57
75
  failed: `Could not find a Kaltura entry ID and partner ID in this URL: ${stripD2lSessionParams(videoUrl)}`,
76
+ reason: "fetch_failed",
58
77
  };
59
78
  }
60
79
  try {
@@ -67,6 +86,7 @@ async function fetchTranscript(platform, videoUrl, fetchImpl) {
67
86
  return {
68
87
  failed: `${error.message} This can happen when the video needs an authenticated Kaltura ` +
69
88
  "session rather than an anonymous one. Open it in Brightspace directly.",
89
+ reason: error.reason,
70
90
  };
71
91
  }
72
92
  throw error;
@@ -75,7 +95,10 @@ async function fetchTranscript(platform, videoUrl, fetchImpl) {
75
95
  case "youtube": {
76
96
  const videoId = extractYouTubeVideoId(videoUrl);
77
97
  if (!videoId) {
78
- return { failed: `Could not find a YouTube video ID in this URL: ${stripD2lSessionParams(videoUrl)}` };
98
+ return {
99
+ failed: `Could not find a YouTube video ID in this URL: ${stripD2lSessionParams(videoUrl)}`,
100
+ reason: "fetch_failed",
101
+ };
79
102
  }
80
103
  try {
81
104
  return { result: await getYouTubeTranscript(videoId, fetchImpl) };
@@ -84,7 +107,7 @@ async function fetchTranscript(platform, videoUrl, fetchImpl) {
84
107
  if (error instanceof NoTranscriptError)
85
108
  return { noTranscript: error.message };
86
109
  if (error instanceof TranscriptFetchError)
87
- return { failed: error.message };
110
+ return { failed: error.message, reason: error.reason };
88
111
  throw error;
89
112
  }
90
113
  }
@@ -114,7 +137,8 @@ export function registerGetVideoTranscript(server, apiClient, fetchImpl = fetch)
114
137
  "Call it with courseId and topicId from get_course_content (typeFilter: 'video' or 'other'), or with videoUrl " +
115
138
  "directly if you already have the link. Returns transcript text with timestamps, plus title and duration when " +
116
139
  "available. Currently supports Kaltura (e.g. BoilerCast, including its Brightspace LTI links) and YouTube; other platforms return a clear message " +
117
- "naming what isn't supported yet. Each call returns at most maxChars characters (up to 100000); to read a whole " +
140
+ "naming what isn't supported yet. A failure returns its message plus a JSON block with reason (e.g. sign_in_required " +
141
+ "for a YouTube bot check) and recovery steps for the user; never present a failed call as a read transcript. Each call returns at most maxChars characters (up to 100000); to read a whole " +
118
142
  "transcript, call again with offset set to the returned nextOffset until truncated is false. Read only — this " +
119
143
  "never marks the video as watched.",
120
144
  inputSchema: GetVideoTranscriptSchema,
@@ -178,11 +202,27 @@ export function registerGetVideoTranscript(server, apiClient, fetchImpl = fetch)
178
202
  videoUrl: publicUrl,
179
203
  platform,
180
204
  hasTranscript: false,
205
+ reason: "no_captions",
181
206
  message: outcome.noTranscript,
182
207
  });
183
208
  }
184
209
  if ("failed" in outcome) {
185
- return errorResponse(outcome.failed);
210
+ // The message stays the first block, as it always was; the second
211
+ // carries the same failure in a shape a caller can branch on.
212
+ const failure = errorResponse(outcome.failed);
213
+ failure.content.push({
214
+ type: "text",
215
+ text: JSON.stringify({
216
+ courseId,
217
+ topicId,
218
+ videoUrl: publicUrl,
219
+ platform,
220
+ hasTranscript: false,
221
+ reason: outcome.reason,
222
+ recovery: transcriptRecovery(outcome.reason, publicUrl),
223
+ }),
224
+ });
225
+ return failure;
186
226
  }
187
227
  const { result } = outcome;
188
228
  const fullText = cuesToText(result.cues);
@@ -12,8 +12,10 @@ export class NoTranscriptError extends Error {
12
12
  }
13
13
  /** The platform's API could not be reached or rejected the request. */
14
14
  export class TranscriptFetchError extends Error {
15
- constructor(message) {
15
+ reason;
16
+ constructor(message, reason = "fetch_failed") {
16
17
  super(message);
18
+ this.reason = reason;
17
19
  this.name = "TranscriptFetchError";
18
20
  }
19
21
  }
@@ -51,10 +51,10 @@ function trackLabel(track) {
51
51
  function playabilityFailure(status, reason) {
52
52
  const detail = reason ? ` YouTube said: ${reason}` : "";
53
53
  if (status === "LOGIN_REQUIRED") {
54
- return new TranscriptFetchError(`YouTube would not serve this video without a sign-in.${detail}`);
54
+ return new TranscriptFetchError(`YouTube would not serve this video without a sign-in.${detail}`, "sign_in_required");
55
55
  }
56
56
  if (status === "UNPLAYABLE" || status === "ERROR") {
57
- return new TranscriptFetchError(`YouTube reports this video as unavailable.${detail}`);
57
+ return new TranscriptFetchError(`YouTube reports this video as unavailable.${detail}`, "unavailable");
58
58
  }
59
59
  return new TranscriptFetchError(`YouTube refused to describe this video (${status}).${detail}`);
60
60
  }
@@ -140,7 +140,7 @@ export async function getYouTubeTranscript(videoId, fetchImpl, language = "en")
140
140
  }
141
141
  if (needsProofOfOrigin(track.baseUrl)) {
142
142
  throw new TranscriptFetchError("YouTube issued this caption track behind a proof-of-origin check this server cannot answer, " +
143
- "so the captions could not be downloaded. The video may still have captions.");
143
+ "so the captions could not be downloaded. The video may still have captions.", "access_blocked");
144
144
  }
145
145
  const res = await fetchImpl(captionUrlAsVtt(track.baseUrl));
146
146
  if (!res.ok) {
@@ -152,7 +152,7 @@ export async function getYouTubeTranscript(videoId, fetchImpl, language = "en")
152
152
  // users looking for a transcript that was there all along.
153
153
  if (!body.trim()) {
154
154
  throw new TranscriptFetchError("YouTube listed a caption track for this video but served an empty response for it. " +
155
- "This is usually a temporary block rather than a missing transcript; try again.");
155
+ "This is usually a temporary block rather than a missing transcript; try again.", "access_blocked");
156
156
  }
157
157
  const { format, cues } = parseCaptions(body);
158
158
  if (cues.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
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",