brightspace-mcp-server 5.1.25 → 5.1.26

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
@@ -159,6 +159,8 @@ npx -y brightspace-mcp-server@latest auth
159
159
 
160
160
  **Want fewer 2FA prompts?** `setup` asks one question, "How often do you want to approve sign-ins on your phone?": **1** every time (default), **2** remember this computer, or **3** let this computer answer Microsoft's code requests itself (needs an authenticator setup key). Options 2 and 3 are opt-in and not for shared computers; to turn them off, rerun `setup` and pick 1. The tradeoffs and the steps for getting the key are in [docs/sign-in.md](docs/sign-in.md#fewer-2fa-prompts).
161
161
 
162
+ **To sign in,** just ask your AI app to sign you in to Brightspace — it can start the sign-in itself now, and it opens a window for you when no password is saved. You can also run `npx -y brightspace-mcp-server@latest auth` in a terminal.
163
+
162
164
  **To sign out early** (on a shared computer, say), just ask your AI app to sign you out of Brightspace, or run `npx -y brightspace-mcp-server@latest auth --logout` in a terminal. It deletes this computer's saved Brightspace session, so the next question does a full sign-in. Your saved password, authenticator key, and settings are kept, so with a saved authenticator key that sign-in can finish without you; on a shared computer, also delete the saved `brightspace-mcp-server` entries from your computer's password store (Keychain Access on a Mac, Credential Manager on Windows). Other AI apps open on this computer are signed out too and will ask you to sign in on their next question. (An app you opened before updating may keep answering for up to an hour; quit and reopen it to sign it out right away.) It only clears this computer; it does not sign you out of Microsoft or Brightspace anywhere else. Both also cancel a sign-in an AI app started that is still waiting for you to approve it (`auth --logout` can't on Windows, and leaves an `auth` run in another terminal alone, deleting nothing). Cancelling doesn't get you a new prompt any sooner: Microsoft won't send another until the earlier one expires, up to 5 minutes after it was sent.
163
165
 
164
166
  How often you're asked is up to your school, not this tool. Everything else about sign-in — Duo, authenticator codes, visible-browser mode, and running without a browser at all (Docker, WSL, hardware keys) — is in [docs/sign-in.md](docs/sign-in.md).
@@ -422,6 +422,15 @@ export class AuthRunner {
422
422
  timeoutMs;
423
423
  onProgress;
424
424
  sessionDir;
425
+ /**
426
+ * Whether the next child this runner spawns should open a window, set by
427
+ * sign_in and cleared as that child starts. A first sign-in with no saved
428
+ * password cannot finish headlessly: there is nothing to type into the
429
+ * school's form, so it waits and times out. Opt-in and one-shot, because
430
+ * background recovery must never put a browser over the user's work on its
431
+ * own; that is the standing rule the D2L_HEADLESS default encodes.
432
+ */
433
+ visibleNextChild = false;
425
434
  constructor(options = {}) {
426
435
  // Resolve paths relative to this file's compiled location (build/auth/auth-runner.js)
427
436
  const thisDir = path.dirname(fileURLToPath(import.meta.url));
@@ -440,6 +449,15 @@ export class AuthRunner {
440
449
  busy() {
441
450
  return this.inFlight !== null || this.childDone !== null;
442
451
  }
452
+ /**
453
+ * Ask that the next child this runner spawns sign in with a visible window
454
+ * (see visibleNextChild). Takes effect only for a child this runner starts:
455
+ * a call that joins a sign-in already running inherits that sign-in's mode,
456
+ * so sign_in checks busy() before asking.
457
+ */
458
+ requestVisibleBrowser() {
459
+ this.visibleNextChild = true;
460
+ }
443
461
  /**
444
462
  * Cancel this process's sign-in, if one is running: stop the child at once
445
463
  * (no MFA cooldown is recorded, see forceStop) and answer every call waiting
@@ -811,6 +829,10 @@ export class AuthRunner {
811
829
  */
812
830
  async spawnAuth() {
813
831
  const started = Date.now();
832
+ // Read and cleared here, so a visible request never outlives the one
833
+ // child it was made for.
834
+ const visible = this.visibleNextChild;
835
+ this.visibleNextChild = false;
814
836
  const recoveryId = newRecoveryId();
815
837
  devActivity("recovery_started", { recoveryId });
816
838
  log("INFO", "Auto-launching brightspace-auth...");
@@ -822,7 +844,15 @@ export class AuthRunner {
822
844
  // unless D2L_HEADLESS explicitly asks for one; the saved
823
845
  // visible-browser preference applies to manual sign-in only.
824
846
  // The recovery id lets the child's dev activity join this recovery.
825
- env: { D2L_HEADLESS: "true", ...process.env, [RECOVERY_ID_ENV]: recoveryId },
847
+ // An explicit sign_in request for a window comes last, so it wins
848
+ // over an inherited D2L_HEADLESS that would hide the one page the
849
+ // user has to finish by hand.
850
+ env: {
851
+ D2L_HEADLESS: "true",
852
+ ...process.env,
853
+ [RECOVERY_ID_ENV]: recoveryId,
854
+ ...(visible ? { D2L_HEADLESS: "false" } : {}),
855
+ },
826
856
  stdio: ["ignore", "pipe", "pipe"],
827
857
  detached: process.platform !== "win32",
828
858
  windowsHide: true,
package/build/server.js CHANGED
@@ -9,7 +9,7 @@ import { D2LApiClient } from "./api/index.js";
9
9
  import { trackToolActivity } from "./utils/dev-activity.js";
10
10
  import { log } from "./utils/logger.js";
11
11
  import { envCredentialRejectedMessage, ENV_COOKIE_REJECTED } from "./utils/commands.js";
12
- import { registerGetMyCourses, registerGetUpcomingDueDates, registerGetCalendarEvents, registerGetMyGrades, registerGetAnnouncements, registerGetAssignments, registerGetAssignmentRubric, registerGetAssignmentFiles, registerGetAnnouncementFiles, registerGetCourseContent, registerDownloadFile, COURSE_FILE_NEEDS_COOKIE, registerGetClasslistEmails, registerGetRoster, registerGetMyGroups, registerGetSyllabus, registerGetDiscussions, registerGetVideoTranscript, registerGetServerInfo, registerSearchCourse, registerSignOut, registerGetDropboxFolders, registerGetDropboxSubmissions, registerGetDropboxUserSubmissions, registerGetDropboxFeedback, registerGetRubricsForObject, registerDownloadDropboxSubmissionFile, acceptOmittedArguments, relayMfaChallenges, currentMfaAnnouncer, } from "./tools/index.js";
12
+ import { registerGetMyCourses, registerGetUpcomingDueDates, registerGetCalendarEvents, registerGetMyGrades, registerGetAnnouncements, registerGetAssignments, registerGetAssignmentRubric, registerGetAssignmentFiles, registerGetAnnouncementFiles, registerGetCourseContent, registerDownloadFile, COURSE_FILE_NEEDS_COOKIE, registerGetClasslistEmails, registerGetRoster, registerGetMyGroups, registerGetSyllabus, registerGetDiscussions, registerGetVideoTranscript, registerGetServerInfo, registerSearchCourse, registerSignOut, registerSignIn, registerGetDropboxFolders, registerGetDropboxSubmissions, registerGetDropboxUserSubmissions, registerGetDropboxFeedback, registerGetRubricsForObject, registerDownloadDropboxSubmissionFile, acceptOmittedArguments, relayMfaChallenges, currentMfaAnnouncer, } from "./tools/index.js";
13
13
  import { registerWeeklyBriefingPrompt, registerGradeAuditPrompt, registerStudyPlannerPrompt, registerCourseSummaryPrompt, } from "./prompts/index.js";
14
14
  /**
15
15
  * Build the shared runtime. Nothing here reaches Brightspace: API versions
@@ -94,6 +94,7 @@ export function createMcpServer(runtime) {
94
94
  registerGetServerInfo(server, config, version, apiClient);
95
95
  registerSearchCourse(server, apiClient);
96
96
  registerSignOut(server, config, apiClient, tokenManager, authRunner);
97
+ registerSignIn(server, config, tokenManager, authRunner);
97
98
  // Instructor / TA dropbox tools (read-only) — 403 for a student account.
98
99
  registerGetDropboxFolders(server, apiClient);
99
100
  registerGetDropboxSubmissions(server, apiClient);
@@ -24,6 +24,7 @@ export { registerGetVideoTranscript } from "./get-video-transcript.js";
24
24
  export { registerGetServerInfo } from "./get-server-info.js";
25
25
  export { registerSearchCourse } from "./search-course.js";
26
26
  export { registerSignOut } from "./sign-out.js";
27
+ export { registerSignIn } from "./sign-in.js";
27
28
  // Instructor / TA dropbox tools (read-only)
28
29
  export { registerGetDropboxFolders } from "./get-dropbox-folders.js";
29
30
  export { registerGetDropboxSubmissions } from "./get-dropbox-submissions.js";
@@ -162,6 +162,10 @@ export const GetMyGroupsSchema = z.object({
162
162
  });
163
163
  export const GetServerInfoSchema = z.object({});
164
164
  export const SignOutSchema = z.object({});
165
+ export const SignInSchema = z.object({
166
+ visible: z.boolean().optional()
167
+ .describe("Open a browser window for the user to sign in by hand. Omit to decide automatically: a window opens only when no password is saved on this computer, because a sign-in with no window has nothing to type into the school's form and would only time out. Pass true when a background sign-in has already failed or the school's page needs a step it cannot do, false to insist on a background sign-in."),
168
+ });
165
169
  // ── Instructor / TA dropbox tools ──────────────────────────────────────────
166
170
  // Read-only, instructor/TA-side views of a course's dropbox (assignment)
167
171
  // folders and the submissions inside them. These answer "who has/hasn't
@@ -0,0 +1,108 @@
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 { AuthProcessError } from "../auth/auth-runner.js";
7
+ import { log } from "../utils/logger.js";
8
+ import { createSSOFlow } from "../auth/sso-flow.js";
9
+ import { SignInSchema } from "./schemas.js";
10
+ import { authPendingNotice, sanitizeError, textResponse as text } from "./tool-helpers.js";
11
+ import { AUTH_COMMAND, SETUP_COMMAND } from "../utils/commands.js";
12
+ const ALREADY = "Already signed in to Brightspace on this computer; the saved session is still valid. Nothing was changed. Go ahead and call the tool you actually need.";
13
+ const ENV_CREDENTIAL = "Nothing to do. This server authenticates with a credential from its configuration (D2L_ACCESS_TOKEN or D2L_SESSION_COOKIE), not with a sign-in it can start. If Brightspace is rejecting it, the user has to paste a fresh one into their AI app's configuration or .env file and restart the app.";
14
+ const NO_RUNNER = `This server cannot start a sign-in itself. Tell the user to run \`${AUTH_COMMAND}\` in a terminal, prefixed with D2L_HEADLESS=false if they need to see the sign-in page and finish it by hand.`;
15
+ const SUCCESS = "Signed in to Brightspace. The session is saved on this computer, so other Brightspace apps here can use it too.";
16
+ /** What an open window means for the caller: this one waits on a person, not on a retry. */
17
+ const WINDOW_OPEN = "A browser window is opening on the user's screen for them to sign in by hand. Tell the user to complete it, including any 2FA, then ask them to say when they are done.";
18
+ const NEEDS_PASSWORD_FOR_HEADLESS = `No password is saved on this computer, so a sign-in with no window has nothing to type into the school's form and would only wait and time out. Either call sign_in again with visible true, or tell the user to run \`${SETUP_COMMAND}\` to save one.`;
19
+ /**
20
+ * A sign-in still working is progress, not a failure, so these are not marked
21
+ * isError: the shared guidance for them already tells the caller to come back
22
+ * (each call polls the running sign-in), and isError invites a model to give
23
+ * up or apologize instead.
24
+ */
25
+ const STILL_WORKING = new Set([
26
+ "mfaPending",
27
+ "automaticPending",
28
+ "inProgress",
29
+ ]);
30
+ /**
31
+ * Register sign_in tool.
32
+ *
33
+ * The counterpart to sign_out, and the reason it exists: an AI app had no way
34
+ * to *start* a sign-in. Recovery happened only as a side effect of some other
35
+ * tool hitting a 401, and the one documented fix for a first sign-in — open a
36
+ * window — lived in an environment variable that nothing reachable from a
37
+ * chat could set. So an assistant whose session had lapsed could only tell
38
+ * the user to go and run a terminal command.
39
+ *
40
+ * What is specific to this tool is the decision to sign in at all, and the
41
+ * window: the saved session is read first (TokenManager), so calling this
42
+ * speculatively costs the user no MFA approval, and `visible`, left out,
43
+ * opens a window exactly when the school's sign-in flow has nothing to sign
44
+ * in with (no username, or no password and not passwordless), which is when a
45
+ * headless sign-in can only time out.
46
+ * A window cannot be asked of a sign-in already running, which keeps its own
47
+ * mode, so busy() is checked before one is promised.
48
+ *
49
+ * Everything a *failure* should say is authPendingNotice's, not this file's:
50
+ * the per-kind guidance, the MFA number, the dedup when a parallel call
51
+ * already carried it, and the "ask for 2FA less often" tip. Writing that
52
+ * wording again here would be a second policy to keep in step with
53
+ * AuthRunner — and a wrong one, since the house guidance tells a caller to
54
+ * keep calling while a sign-in polls, which is the opposite of what a
55
+ * hand-written "do not retry" would say.
56
+ */
57
+ export function registerSignIn(server, config, tokenManager, authRunner) {
58
+ server.registerTool("sign_in", {
59
+ title: "Sign In to Brightspace",
60
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
61
+ description: "Sign in to Brightspace on this computer so the other tools work. Does nothing and says so if the saved session is already valid, so it is safe to call first when you are unsure. Opens a browser window for the user to sign in by hand when no password is saved (nothing to type means a windowless sign-in can only time out); otherwise it signs in in the background and reports the 2FA number for the user to approve on their phone. Call this when a Brightspace tool failed because of authentication, or when the user asks to sign in or log in to Brightspace. Every answer says what to do next, including whether to call again.",
62
+ inputSchema: SignInSchema,
63
+ }, async ({ visible }) => {
64
+ if (config.envAccessToken || config.envSessionCookie)
65
+ return text(ENV_CREDENTIAL);
66
+ if (!authRunner)
67
+ return text(NO_RUNNER);
68
+ // First, so signing in again when the saved session is fine never asks
69
+ // the user for an approval they did not need to give.
70
+ try {
71
+ const token = await tokenManager.getToken();
72
+ if (token && tokenManager.isValid(token))
73
+ return text(ALREADY);
74
+ }
75
+ catch (error) {
76
+ // A session that cannot be read is exactly what signing in fixes.
77
+ log("DEBUG", "sign_in could not read the saved session; signing in", error);
78
+ }
79
+ // The same test the sign-in itself makes before typing anything: each
80
+ // school's flow decides. A passwordless account keeps no password yet
81
+ // signs in without a window (the phone approves it).
82
+ const canSignInHeadless = createSSOFlow(config).hasCredentials();
83
+ // A window only when a headless sign-in could not work anyway.
84
+ const wantWindow = visible ?? !canSignInHeadless;
85
+ // A joined sign-in keeps the mode it started with, so asking would only
86
+ // promise the user a window that never appears.
87
+ const windowOpened = wantWindow && !authRunner.busy();
88
+ if (windowOpened)
89
+ authRunner.requestVisibleBrowser();
90
+ else if (wantWindow)
91
+ log("INFO", "sign_in was asked for a window but a sign-in is already running");
92
+ if (!wantWindow && !canSignInHeadless)
93
+ return text(NEEDS_PASSWORD_FOR_HEADLESS, true);
94
+ try {
95
+ await authRunner.run();
96
+ return text(SUCCESS);
97
+ }
98
+ catch (error) {
99
+ if (error instanceof AuthProcessError) {
100
+ const guidance = authPendingNotice(error);
101
+ return text(windowOpened ? `${WINDOW_OPEN} ${guidance}` : guidance, !STILL_WORKING.has(error.kind));
102
+ }
103
+ // Everything else goes through the house path, which logs it and
104
+ // keeps stack traces and raw responses out of the answer.
105
+ return sanitizeError(error);
106
+ }
107
+ });
108
+ }
@@ -9,7 +9,7 @@ import { clearSavedSession, RemoveError } from "../auth/logout.js";
9
9
  import { authLockPath } from "../auth/mfa-challenge.js";
10
10
  import { log } from "../utils/logger.js";
11
11
  import { SignOutSchema } from "./schemas.js";
12
- import { withUpdateNotice } from "./tool-helpers.js";
12
+ import { textResponse as text } from "./tool-helpers.js";
13
13
  const KEPT = "Kept (if you set them up): your saved password, your settings, and your authenticator key.";
14
14
  const OTHER_APPS = "Other Brightspace apps open on this computer are signed out too and will sign in again from scratch on their next question. An app you opened before updating to this version may keep answering for up to an hour; quit and reopen it to sign it out right away.";
15
15
  const NEXT_SIGN_IN = "Because your saved password (and authenticator key, if you saved one) stay on this computer, that next sign-in may just ask you to approve it, or finish on its own if you saved an authenticator key. On a shared computer, also delete the saved \"brightspace-mcp-server\" entries from your computer's password store (Keychain Access on a Mac, Credential Manager on Windows).";
@@ -20,10 +20,6 @@ const NEXT_QUESTION = "Microsoft keeps that prompt open for up to 5 minutes afte
20
20
  const ALREADY_SIGNED_OUT = "You're already signed out of Brightspace on this computer; there was no saved session to remove.";
21
21
  const NOTHING_SAVED = "There was no saved session to remove.";
22
22
  const UNEXPECTED_FAILURE = "Could not clear the saved session because of an unexpected error. Nothing more was changed; try again, or run `npx -y brightspace-mcp-server@latest auth --logout` in a terminal to see the details.";
23
- function text(message, isError = false) {
24
- const result = withUpdateNotice([{ type: "text", text: message }]);
25
- return isError ? { ...result, isError: true } : result;
26
- }
27
23
  /**
28
24
  * Register sign_out tool.
29
25
  *
@@ -29,6 +29,15 @@ export function withUpdateNotice(content) {
29
29
  content.push({ type: "text", text: notice });
30
30
  return { content };
31
31
  }
32
+ /**
33
+ * A plain-text answer, as the sign-in tools give: prose a model relays to the
34
+ * user, not a JSON envelope to read fields out of. Carries the update notice
35
+ * like every other response.
36
+ */
37
+ export function textResponse(message, isError = false) {
38
+ const result = withUpdateNotice([{ type: "text", text: message }]);
39
+ return isError ? { ...result, isError: true } : result;
40
+ }
32
41
  /**
33
42
  * Wrap data as MCP-compatible tool result.
34
43
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "5.1.25",
3
+ "version": "5.1.26",
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",