brightspace-mcp-server 3.8.1 → 3.8.3

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
@@ -6,7 +6,7 @@ Talk to your Brightspace courses with AI. Ask about grades, due dates, quizzes,
6
6
 
7
7
  This is an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that connects your AI to D2L Brightspace so it can pull your grades, assignments, syllabus, and course content on demand.
8
8
 
9
- Connects to D2L Brightspace. Automatic login supports Purdue's Microsoft Entra flow, SUNY campus selection, Western University, and TU Delft NetID via SURFconext. Other schools need a compatible automated sign-in flow; unsupported login pages return an actionable error.
9
+ Connects to D2L Brightspace. Automatic login supports Purdue's Microsoft Entra flow, SUNY campus selection, Western University, TU Delft NetID via SURFconext, and CUNY Login. Other schools need a compatible automated sign-in flow; unsupported login pages return an actionable error.
10
10
 
11
11
  <p align="center">
12
12
  <img src="https://raw.githubusercontent.com/RohanMuppa/brightspace-mcp-server/main/docs/how-it-works.svg" alt="Architecture diagram" width="100%">
@@ -28,7 +28,7 @@ Paste this into Claude Code, Cursor, Windsurf, Copilot, Codex, or any AI coding
28
28
  ```
29
29
  Install brightspace-mcp-server for me by following
30
30
  https://github.com/RohanMuppa/brightspace-mcp-server/blob/main/LLMs.md
31
- (use --purdue at Purdue, --suny at SUNY, or --tudelft at TU Delft).
31
+ (use --purdue at Purdue, --suny at SUNY, --tudelft at TU Delft, or --cuny at CUNY).
32
32
  ```
33
33
 
34
34
  **Option 2: Run it yourself**
@@ -58,6 +58,16 @@ npx -y brightspace-mcp-server@latest setup --tudelft
58
58
 
59
59
  Use your NetID rather than your student email address. The TU Delft flow is headless NetID username and password sign-in only, including automatic re-authentication when the saved session expires; it does not support MFA or any other interactive step. If your account requires one, [open an issue](https://github.com/RohanMuppa/brightspace-mcp-server/issues) — that tenant isn't supported yet.
60
60
 
61
+ CUNY students can add `--cuny`. Sign in with your full CUNY Login address
62
+ (`firstname.lastname01@login.cuny.edu`); authentication asks in the terminal
63
+ for the 6-digit code from your authenticator app. CUNY asks for that code on
64
+ every full sign-in, so when your Brightspace session ends, run
65
+ `npx -y brightspace-mcp-server@latest auth` in a terminal again:
66
+
67
+ ```bash
68
+ npx -y brightspace-mcp-server@latest setup --cuny
69
+ ```
70
+
61
71
  The wizard saves your password in the native credential store and asks how you'll complete MFA: wait for approval or number matching, enter a terminal code from Google Authenticator or another app, or use a visible browser for other interactive methods. It can also configure Claude Desktop, Cursor, Codex Desktop and CLI, and Claude Code when installed — restart your AI client when it finishes.
62
72
 
63
73
  Any other D2L school: run `setup` without a flag and paste your Brightspace URL (for example `https://yourschool.brightspace.com`).
@@ -99,7 +109,7 @@ There's no login step — asking a question signs you in.
99
109
 
100
110
  **Normal days:** tokens renew over HTTPS, and a background browser silently replays your saved Microsoft session (silent SSO) if it lapses.
101
111
 
102
- **When MFA is asked:** the number to approve shows up right in the tool's response, and sign-in finishes in the background — approve it, call the tool again, and use the newest number if one goes stale. TOTP apps (Google Authenticator, etc.) get prompted via the terminal; visible-browser mode opens a window instead, though automatic recovery during a tool call still runs headless unless you set `D2L_HEADLESS=false`. On Duo, sign-in auto-answers "Is this your device?" with **yes**, since a headless run has nobody to click it — this also makes Duo remember the device, so skip it on shared machines. Set `D2L_DUO_PASSCODE` to swap the push for a typed passcode. On Microsoft's MFA page the server ticks "Don't ask again" when your school offers it, so later sign-ins can skip the second factor; how long that lasts is the school's setting, not the server's (`D2L_REMEMBER_MFA=false` turns this off, and `get_server_info` shows whether it was offered).
112
+ **When MFA is asked:** the number to approve shows up right in the tool's response, and sign-in finishes in the background — approve it, call the tool again, and use the newest number if one goes stale. TOTP apps (Google Authenticator, etc.) get prompted via the terminal; visible-browser mode opens a window instead, though automatic recovery during a tool call still runs headless unless you set `D2L_HEADLESS=false`. On Duo, sign-in auto-answers "Is this your device?" with **yes**, since a headless run has nobody to click it — this also makes Duo remember the device, so skip it on shared machines. Set `D2L_DUO_PASSCODE` to swap the push for a typed passcode. On Microsoft's MFA page, setting `D2L_REMEMBER_MFA=true` makes the server tick "Don't ask again" when your school offers it, so later sign-ins can skip the second factor; how long that lasts is the school's setting, not the server's. It is off by default — leave it off on a shared machine — and `get_server_info` shows whether the box was ticked, offered, or left alone.
103
113
 
104
114
  **If it gets stuck:** a missed MFA approval pauses automatic sign-in for 5 minutes. This retries immediately, takes over a stuck sign-in, or asks for a code:
105
115
 
@@ -123,7 +133,7 @@ Run it from your home folder — macOS blocks `npx` from Documents, Desktop, or
123
133
  | Roster | "Who are the TAs for ECE 264?" · "Get me my instructor's email" |
124
134
  | Discussions | "What are people saying in the final project thread?" · "Summarize the latest discussion posts" |
125
135
  | Video transcripts | "What did the professor say about pinch-off in Tuesday's lecture recording?" · "Summarize last week's BoilerCast video" — works for Kaltura and YouTube embeds; other platforms report that they aren't supported yet |
126
- | Troubleshooting | "Which version of the Brightspace server am I running?" · "Where is my Brightspace config file?" — `get_server_info` reports the version, Node runtime, platform, config and session paths, school URL, whether a credential is stored, and what Microsoft remembered (`microsoftSession`: stay-signed-in and its expiry, plus whether "Don't ask again" was ticked, already on, or not offered), without contacting Brightspace or revealing secrets |
136
+ | Troubleshooting | "Which version of the Brightspace server am I running?" · "Where is my Brightspace config file?" — `get_server_info` reports the version, Node runtime, platform, config and session paths, school URL, whether a credential is stored, and what Microsoft remembered (`microsoftSession`: stay-signed-in and its expiry, plus whether "Don't ask again" was ticked, already on, not offered, or left off because `D2L_REMEMBER_MFA` isn't set), without contacting Brightspace or revealing secrets |
127
137
  | Calendar | "When is my midterm?" · "What's on my calendar this week?" · "Is lab cancelled on Thursday?" — reads exams, labs, review sessions, and deadlines instructors put only on the course calendar |
128
138
  | Planning | "Build me a study schedule based on my upcoming due dates" · "Which class needs the most attention right now?" — pulls from assignments, quizzes, graded discussion topics (any topic with a due date), and course calendar events such as exams and labs |
129
139
 
@@ -26,7 +26,9 @@ const SILENT_SSO = {
26
26
  // get past this gate otherwise.
27
27
  emailFields: ["input[type=email]", "input[name=loginfmt]", "input[name=j_username]", "input#signinid"],
28
28
  credentialFields: ['input#username', 'input#userName', 'input[type="password"]'],
29
- mfaChallenges: ["#idRichContext_DisplaySign", "#idDiv_SAOTCAS_Title", "#idDiv_SAOTCC_Title"],
29
+ // Entra's number match and code prompts, then CUNY Login's authenticator-code
30
+ // field (cuny-sso.ts CUNY_TOTP_SELECTOR).
31
+ mfaChallenges: ["#idRichContext_DisplaySign", "#idDiv_SAOTCAS_Title", "#idDiv_SAOTCC_Title", '[id="otpValue|input"]'],
30
32
  campusSaml: 'a[href*="/d2l/lp/auth/saml/initiate-login"]',
31
33
  kmsiCheckbox: "#KmsiCheckboxField",
32
34
  kmsiTitle: "Stay signed in?",
@@ -0,0 +1,216 @@
1
+ /** CUNY login entry point. Licensed under MIT; see LICENSE. */
2
+ import { BrowserAuthError } from "../utils/errors.js";
3
+ import { log } from "../utils/logger.js";
4
+ import { AUTH_COMMAND, SETUP_COMMAND } from "../utils/commands.js";
5
+ import { MfaApprovalError, UnsupportedAuthenticationError } from "./sso-flow.js";
6
+ /** All CUNY campuses share one Brightspace tenant behind CUNY Login (Oracle OAM). */
7
+ const CUNY_BRIGHTSPACE_HOST = "brightspace.cuny.edu";
8
+ const CUNY_LOGIN_HOST = "ssologin.cuny.edu";
9
+ /**
10
+ * Oracle JET generates the authenticator-code field's id with a pipe in it,
11
+ * which is not valid in an `#id` selector, so match the attribute instead.
12
+ * browser-auth.ts lists the same selector so a run that resumes on this page
13
+ * is recognized as an MFA challenge.
14
+ */
15
+ export const CUNY_TOTP_SELECTOR = '[id="otpValue|input"]';
16
+ const SELECTORS = {
17
+ // The visible field takes the full @login.cuny.edu address. CUNY's own
18
+ // submit handler copies the short name into a hidden #CUNYLoginUsername,
19
+ // so that hidden field is left for the page to fill.
20
+ username: "#CUNYLoginUsernameDisplay",
21
+ password: "#CUNYLoginPassword",
22
+ submit: "#submit",
23
+ // Only rendered on the reload OAM serves after rejecting the password.
24
+ serverError: "#serverError",
25
+ totp: CUNY_TOTP_SELECTOR,
26
+ };
27
+ const POLL_MS = 500;
28
+ /** CUNY parks on a processing page for about ten seconds before the code challenge. */
29
+ const CHALLENGE_TIMEOUT_MS = 60_000;
30
+ /** A person has to find their authenticator and read a code. */
31
+ const MFA_TIMEOUT_MS = 5 * 60 * 1000;
32
+ /** True for the shared CUNY Brightspace instance. */
33
+ export function isCunyBrightspace(baseUrl) {
34
+ try {
35
+ return new URL(baseUrl).hostname.toLowerCase() === CUNY_BRIGHTSPACE_HOST;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /** Compare hosts, not substrings: the SAML round trip carries other hosts in its query. */
42
+ function isCunyLogin(url) {
43
+ try {
44
+ const parsed = new URL(url);
45
+ return parsed.protocol === "https:" && parsed.hostname.toLowerCase() === CUNY_LOGIN_HOST;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ /** A rejected code reloads the challenge with `?emsg=Entered+TOTP+is+incorrect.` */
52
+ function hasErrorMessage(url) {
53
+ try {
54
+ return new URL(url).searchParams.has("emsg");
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ /**
61
+ * Login flow for CUNY Brightspace.
62
+ *
63
+ * Brightspace federates by SAML to CUNY Login at ssologin.cuny.edu, an Oracle
64
+ * Access Manager page rather than Entra, Shibboleth, or Duo:
65
+ *
66
+ * /d2l/login → /oamfed/idp/samlv20 (username + password)
67
+ * → /oaa/authnui (a "Processing Request" page, roughly ten seconds)
68
+ * → /oaa-totp-factor/rui/index.html (authenticator code) → /d2l/home
69
+ *
70
+ * Its username field is a plain text input, which none of the default flow's
71
+ * username selectors match, and its code challenge is an Oracle JET page the
72
+ * default MFA loop does not know. The code itself still comes from a person,
73
+ * through the same terminal prompt or visible browser every other code-based
74
+ * MFA uses.
75
+ */
76
+ export class CunySSOFlow {
77
+ config;
78
+ constructor(config) {
79
+ this.config = config;
80
+ }
81
+ hasCredentials() {
82
+ return Boolean(this.config.username && this.config.password);
83
+ }
84
+ async login(page) {
85
+ try {
86
+ log("INFO", "Starting CUNY Login sign-in flow");
87
+ await this.completeLogin(page);
88
+ return true;
89
+ }
90
+ catch (error) {
91
+ if (error instanceof BrowserAuthError)
92
+ throw error;
93
+ throw new UnsupportedAuthenticationError("CUNY Login could not complete automatic sign-in. Check saved credentials and your authenticator settings.", error);
94
+ }
95
+ }
96
+ /**
97
+ * Poll the page and answer whichever CUNY Login step is on screen. A run
98
+ * can hand over mid-flow (awaitSilentSSO also stops on the code page), so
99
+ * no step is assumed to come first.
100
+ */
101
+ async completeLogin(page) {
102
+ // A visible browser may be answering a different CUNY MFA method by hand.
103
+ let deadline = Date.now() + (this.config.headless === false ? MFA_TIMEOUT_MS : CHALLENGE_TIMEOUT_MS);
104
+ let credentialsSubmitted = false;
105
+ let challenged = false;
106
+ /** The challenge URL a code was submitted from; a reload away from it with ?emsg= is a rejection. */
107
+ let codeSubmittedFrom = null;
108
+ let announced = false;
109
+ while (Date.now() < deadline) {
110
+ if (await this.isAuthenticated(page)) {
111
+ log("INFO", "Login successful - verified Brightspace home");
112
+ return;
113
+ }
114
+ // Never type the CUNY password into anything but CUNY Login itself.
115
+ if (isCunyLogin(page.url())) {
116
+ if (await this.isVisible(page, SELECTORS.serverError)) {
117
+ throw new BrowserAuthError(`CUNY Login rejected the saved username or password. Run \`${SETUP_COMMAND} --cuny\` to update them.`, "credentials");
118
+ }
119
+ if (await this.isVisible(page, SELECTORS.totp)) {
120
+ if (!challenged) {
121
+ challenged = true;
122
+ deadline = Date.now() + MFA_TIMEOUT_MS;
123
+ }
124
+ if (this.config.headless === false) {
125
+ // The person at the window types the code and can retry a typo
126
+ // there, so a rejection is theirs to answer, not a failure here.
127
+ if (!announced) {
128
+ announced = true;
129
+ log("WARN", "Enter the code from your authenticator app in the CUNY Login window. Waiting up to 5 minutes.");
130
+ this.config.onMfaChallenge?.(null);
131
+ }
132
+ }
133
+ else {
134
+ const url = page.url();
135
+ if (codeSubmittedFrom !== null && url !== codeSubmittedFrom && hasErrorMessage(url)) {
136
+ throw new MfaApprovalError(new Error("CUNY Login rejected the authenticator code."));
137
+ }
138
+ if (codeSubmittedFrom === null) {
139
+ codeSubmittedFrom = url;
140
+ await this.submitCode(page);
141
+ }
142
+ }
143
+ }
144
+ else if (!credentialsSubmitted && await this.isVisible(page, SELECTORS.username)) {
145
+ // CUNY Login asks for a code on every full sign-in and does not
146
+ // re-federate Brightspace from its own cookies, so a headless run with
147
+ // no terminal (the MCP server's background re-authentication) can
148
+ // never finish. Stop before the password is sent: otherwise every tool
149
+ // call after the session ends posts it again, and a stale saved
150
+ // password would keep counting toward a CUNY lockout.
151
+ if (this.config.headless !== false && !this.config.requestMfaCode)
152
+ throw this.codePromptUnavailable();
153
+ credentialsSubmitted = true;
154
+ await this.enterCredentials(page);
155
+ }
156
+ }
157
+ await page.waitForTimeout(POLL_MS);
158
+ }
159
+ if (challenged)
160
+ throw new MfaApprovalError();
161
+ throw new UnsupportedAuthenticationError("CUNY Login did not reach the authenticator-code challenge or Brightspace. If your default CUNY MFA method is not an authenticator app, set D2L_HEADLESS=false and finish sign-in in the browser.");
162
+ }
163
+ async enterCredentials(page) {
164
+ if (!this.config.username)
165
+ throw new BrowserAuthError("Username is required for SSO login", "credentials");
166
+ if (!this.config.password)
167
+ throw new BrowserAuthError("Password is required for SSO login", "credentials");
168
+ log("INFO", "Entering CUNY Login credentials");
169
+ await page.locator(SELECTORS.username).first().fill(this.config.username);
170
+ await page.locator(SELECTORS.password).first().fill(this.config.password);
171
+ // OAM answers the POST with a full navigation, which can detach the button.
172
+ await page.locator(SELECTORS.submit).first().click().catch(() => { });
173
+ }
174
+ codePromptUnavailable() {
175
+ return new UnsupportedAuthenticationError(`CUNY Login requires an authenticator code. Run \`${AUTH_COMMAND}\` in a terminal to enter it.`);
176
+ }
177
+ /** Feed CUNY's code challenge from the terminal prompt. */
178
+ async submitCode(page) {
179
+ if (!this.config.requestMfaCode)
180
+ throw this.codePromptUnavailable();
181
+ const code = (await this.config.requestMfaCode()).replace(/\s/g, "");
182
+ if (!/^\d{6,8}$/.test(code))
183
+ throw new UnsupportedAuthenticationError("The MFA code must contain 6-8 digits.");
184
+ await page.locator(SELECTORS.totp).first().fill(code);
185
+ // Oracle JET gives the Verify button no id; its label is the stable handle.
186
+ const verify = page.getByRole("button", { name: /^\s*verify\s*$/i }).first();
187
+ if (await verify.isVisible().catch(() => false))
188
+ await verify.click().catch(() => { });
189
+ else
190
+ await page.locator(SELECTORS.totp).first().press("Enter");
191
+ log("INFO", "Authenticator code submitted");
192
+ }
193
+ async isVisible(page, selector) {
194
+ return page.locator(selector).first().isVisible().catch(() => false);
195
+ }
196
+ /** The login shell also exposes D2L.LP, so verify origin and home as well. */
197
+ async isAuthenticated(page) {
198
+ try {
199
+ const expected = new URL(this.config.baseUrl ?? `https://${CUNY_BRIGHTSPACE_HOST}`);
200
+ const current = new URL(page.url());
201
+ if (current.origin !== expected.origin || !/^\/d2l\/home(?:\/|$)/.test(current.pathname))
202
+ return false;
203
+ const cookies = await page.context().cookies(expected.origin);
204
+ if (!cookies.some(cookie => cookie.name === "d2lSessionVal" && Boolean(cookie.value)))
205
+ return false;
206
+ return await page.evaluate(() => {
207
+ const d2l = window.D2L;
208
+ return Boolean(d2l?.LP);
209
+ });
210
+ }
211
+ catch {
212
+ // Redirects can replace the execution context mid-check. Keep polling.
213
+ return false;
214
+ }
215
+ }
216
+ }
@@ -7,6 +7,7 @@ export { BrowserAuth } from "./browser-auth.js";
7
7
  export { PurdueSSOFlow } from "./purdue-sso.js";
8
8
  export { SunySSOFlow, isSunyBrightspace } from "./suny-sso.js";
9
9
  export { TUDelftSSOFlow, isTUDelftBrightspace } from "./tudelft-sso.js";
10
+ export { CunySSOFlow, isCunyBrightspace } from "./cuny-sso.js";
10
11
  export { createSSOFlow } from "./sso-flow.js";
11
12
  export { TokenManager } from "./token-manager.js";
12
13
  export { SessionStore } from "./session-store.js";
@@ -10,7 +10,7 @@ import { BROWSER_STATE_FILE } from "./browser-state-store.js";
10
10
  const SUMMARY_FILE = "microsoft-session.json";
11
11
  const ENTRA_HOST = "login.microsoftonline.com";
12
12
  const ENTRA_COOKIES = new Set(["ESTSAUTH", "ESTSAUTHPERSISTENT", "ESTSAUTHLIGHT"]);
13
- const OUTCOMES = ["ticked", "already", "absent", "unknown"];
13
+ const OUTCOMES = ["ticked", "already", "absent", "unknown", "off"];
14
14
  /** Latest expiry of each Entra sign-in cookie in a jar. */
15
15
  function entraExpiries(state) {
16
16
  const expiries = new Map();
@@ -39,6 +39,7 @@ const REMEMBER_MFA_LOG = {
39
39
  already: "already checked",
40
40
  absent: "not offered by tenant",
41
41
  unknown: "could not be ticked",
42
+ off: "off (set D2L_REMEMBER_MFA=true to tick it)",
42
43
  };
43
44
  /** How often to look for the number while waiting on MFA. */
44
45
  const NUMBER_MATCH_POLL_MS = 2000;
@@ -469,11 +470,18 @@ export class PurdueSSOFlow {
469
470
  }
470
471
  /**
471
472
  * Tick Entra's "Don't ask again" box, once per login and never in a loop.
473
+ * Opt-in: without an explicit true the box is left alone and the outcome
474
+ * is recorded as "off", so get_server_info can say why nothing was ticked.
472
475
  * An already-checked box is left alone so this can never untick it.
473
476
  */
474
477
  async rememberMfaDevice(page) {
475
- if (this.rememberMfa || this.config.rememberMfa === false)
478
+ if (this.rememberMfa)
476
479
  return;
480
+ if (this.config.rememberMfa !== true) {
481
+ this.rememberMfa = { outcome: "off", at: new Date().toISOString() };
482
+ log("INFO", `Entra remember-MFA checkbox: ${REMEMBER_MFA_LOG.off}`);
483
+ return;
484
+ }
477
485
  if (new URL(page.url()).hostname !== "login.microsoftonline.com")
478
486
  return;
479
487
  // A nicety, never a reason to fail sign-in: any surprise is "unknown".
@@ -7,6 +7,7 @@ import { PurdueSSOFlow } from "./purdue-sso.js";
7
7
  import { SunySSOFlow, isSunyBrightspace } from "./suny-sso.js";
8
8
  import { WesternSSOFlow, isWesternBrightspace } from "./western-sso.js";
9
9
  import { TUDelftSSOFlow, isTUDelftBrightspace } from "./tudelft-sso.js";
10
+ import { CunySSOFlow, isCunyBrightspace } from "./cuny-sso.js";
10
11
  import { BrowserAuthError } from "../utils/errors.js";
11
12
  import { AUTH_COMMAND } from "../utils/commands.js";
12
13
  export class UnsupportedAuthenticationError extends BrowserAuthError {
@@ -55,5 +56,8 @@ export function createSSOFlow(config, requestMfaCode, onMfaChallenge) {
55
56
  if (isWesternBrightspace(config.baseUrl)) {
56
57
  return new WesternSSOFlow(credentials);
57
58
  }
59
+ if (isCunyBrightspace(config.baseUrl)) {
60
+ return new CunySSOFlow(credentials);
61
+ }
58
62
  return new PurdueSSOFlow(credentials);
59
63
  }
package/build/setup.js CHANGED
@@ -52,9 +52,16 @@ export const SCHOOL_PRESETS = {
52
52
  mfaNote: "Approve the sign-in request from your MFA app.",
53
53
  usernameHint: "Use your full sign-in address if your Western account requires it.",
54
54
  },
55
+ cuny: {
56
+ name: "CUNY",
57
+ baseUrl: "https://brightspace.cuny.edu",
58
+ usernameLabel: "CUNY Login username",
59
+ mfaNote: "Type the 6-digit code from your authenticator app when prompted.",
60
+ usernameHint: "Use your full CUNY Login address, e.g. firstname.lastname01@login.cuny.edu",
61
+ },
55
62
  };
56
63
  /**
57
- * Pick the school preset named by `--purdue`, `--suny`, `--western`, etc.
64
+ * Pick the school preset named by `--purdue`, `--suny`, `--western`, `--cuny`, etc.
58
65
  *
59
66
  * Own properties only: a bare index would make `--constructor` or
60
67
  * `--__proto__` resolve to something off `Object.prototype` and hand the
@@ -29,8 +29,10 @@ export async function loadConfig() {
29
29
  const headless = envBoolean(process.env.D2L_HEADLESS, "D2L_HEADLESS")
30
30
  ?? store?.headless
31
31
  ?? true;
32
- // Ask Microsoft to skip the second factor for its "Don't ask again" window.
33
- const rememberMfa = envBoolean(process.env.D2L_REMEMBER_MFA, "D2L_REMEMBER_MFA") ?? true;
32
+ // Opt-in: ask Microsoft to skip the second factor for its "Don't ask again"
33
+ // window. Off unless D2L_REMEMBER_MFA=true, so a shared machine never
34
+ // remembers MFA without the user choosing it.
35
+ const rememberMfa = envBoolean(process.env.D2L_REMEMBER_MFA, "D2L_REMEMBER_MFA") ?? false;
34
36
  // Resolve tokenTtl: env > store > default (3600)
35
37
  const tokenTtl = positiveSeconds(process.env.D2L_TOKEN_TTL, "D2L_TOKEN_TTL")
36
38
  ?? positiveSeconds(store?.tokenTtl, "tokenTtl in config.json")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "3.8.1",
3
+ "version": "3.8.3",
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",