brightspace-mcp-server 3.11.12 → 3.11.13
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/build/auth/auth-lock.js +8 -0
- package/build/auth/auth-runner.js +66 -0
- package/build/auth/browser-auth.js +17 -3
- package/build/auth/mfa-challenge.js +61 -0
- package/build/auth-cli.js +9 -2
- package/build/index.js +1 -0
- package/build/tools/tool-helpers.js +11 -2
- package/package.json +1 -1
package/build/auth/auth-lock.js
CHANGED
|
@@ -38,8 +38,15 @@ async function renameThroughContention(from, to) {
|
|
|
38
38
|
}
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The MFA challenge the lock owner's sign-in is showing, published inside the
|
|
43
|
+
* lock directory so it lives and dies with the lock (see mfa-challenge.ts).
|
|
44
|
+
*/
|
|
45
|
+
export const CHALLENGE_FILE = "challenge.json";
|
|
41
46
|
export class AuthenticationInProgressError extends Error {
|
|
42
47
|
code = "AUTH_IN_PROGRESS";
|
|
48
|
+
/** The owner's MFA challenge, when the caller looked it up (see BrowserAuth.authenticate). */
|
|
49
|
+
challenge;
|
|
43
50
|
constructor() {
|
|
44
51
|
super("Authentication already in progress. Retry after the current authentication finishes.");
|
|
45
52
|
this.name = "AuthenticationInProgressError";
|
|
@@ -89,6 +96,7 @@ export const lockOps = {
|
|
|
89
96
|
/** Remove only our fixed metadata names, after the directory was retired. */
|
|
90
97
|
async function cleanRetired(lockPath) {
|
|
91
98
|
await fs.unlink(path.join(lockPath, "owner.json")).catch(() => { });
|
|
99
|
+
await fs.unlink(path.join(lockPath, CHALLENGE_FILE)).catch(() => { });
|
|
92
100
|
await cleanChild(path.join(lockPath, "reclaim.lock"));
|
|
93
101
|
await fs.rmdir(lockPath).catch(() => { });
|
|
94
102
|
}
|
|
@@ -11,6 +11,7 @@ import { AuthError } from "../utils/errors.js";
|
|
|
11
11
|
import { AUTH_COMMAND } from "../utils/commands.js";
|
|
12
12
|
import { devActivity } from "../utils/dev-activity.js";
|
|
13
13
|
import { parsePhaseMarker } from "./auth-phases.js";
|
|
14
|
+
import { authLockPath, challengeRelayedAt } from "./mfa-challenge.js";
|
|
14
15
|
/**
|
|
15
16
|
* Timeout for the auth process. It has to outlast the child's own MFA wait,
|
|
16
17
|
* which is five minutes: a person has to find their phone, unlock it, and read
|
|
@@ -41,6 +42,25 @@ const MFA_POLL_MS = 45000;
|
|
|
41
42
|
* the number in the answer.
|
|
42
43
|
*/
|
|
43
44
|
const CALL_BUDGET_MS = 55000;
|
|
45
|
+
/**
|
|
46
|
+
* How long a background sign-in past its MFA challenge keeps running once
|
|
47
|
+
* nobody is waiting on it (issue #199). The answer carrying the number can be
|
|
48
|
+
* cancelled, or held in a parallel batch the user never sees; the child then
|
|
49
|
+
* kept its browser and the cross-process lock for the whole 5-minute MFA
|
|
50
|
+
* window, and every server sharing the session was stuck behind it. A user
|
|
51
|
+
* who did see the number calls again, which polls for another MFA_POLL_MS, so
|
|
52
|
+
* the same span without any caller (in this process, or relaying the number
|
|
53
|
+
* from another one) means nobody is approving.
|
|
54
|
+
*/
|
|
55
|
+
const ABANDON_MS = MFA_POLL_MS;
|
|
56
|
+
/**
|
|
57
|
+
* Milliseconds until a background sign-in counts as abandoned; zero or less
|
|
58
|
+
* means it already is. A caller still waiting keeps it alive outright;
|
|
59
|
+
* otherwise the clock runs from the latest moment anyone attended to it.
|
|
60
|
+
*/
|
|
61
|
+
function abandonDelayMs(waiting, attendedAt, now) {
|
|
62
|
+
return waiting > 0 ? ABANDON_MS : attendedAt + ABANDON_MS - now;
|
|
63
|
+
}
|
|
44
64
|
/** How long to poll after a challenge seen elapsedMs into the call. */
|
|
45
65
|
function pollWindowMs(elapsedMs) {
|
|
46
66
|
return Math.min(MFA_POLL_MS, CALL_BUDGET_MS - elapsedMs);
|
|
@@ -179,15 +199,21 @@ export class AuthRunner {
|
|
|
179
199
|
pendingChallenge = null;
|
|
180
200
|
/** Resolves on the next marker from the current child; null between children. */
|
|
181
201
|
challengeSignal = null;
|
|
202
|
+
/** Callers inside run() right now. */
|
|
203
|
+
waiting = 0;
|
|
204
|
+
/** When the last caller left run(). */
|
|
205
|
+
lastAttendedAt = 0;
|
|
182
206
|
scriptPath;
|
|
183
207
|
timeoutMs;
|
|
184
208
|
onProgress;
|
|
209
|
+
sessionDir;
|
|
185
210
|
constructor(options = {}) {
|
|
186
211
|
// Resolve paths relative to this file's compiled location (build/auth/auth-runner.js)
|
|
187
212
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
188
213
|
this.scriptPath = path.resolve(thisDir, "..", "auth-cli.js");
|
|
189
214
|
this.timeoutMs = options.timeoutMs ?? AUTH_TIMEOUT_MS;
|
|
190
215
|
this.onProgress = options.onProgress;
|
|
216
|
+
this.sessionDir = options.sessionDir;
|
|
191
217
|
}
|
|
192
218
|
/**
|
|
193
219
|
* Authenticate, joining the login this process already started if there is
|
|
@@ -206,6 +232,16 @@ export class AuthRunner {
|
|
|
206
232
|
* the next call to join.
|
|
207
233
|
*/
|
|
208
234
|
async run(onChallenge) {
|
|
235
|
+
this.waiting += 1;
|
|
236
|
+
try {
|
|
237
|
+
return await this.attend(onChallenge);
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
this.waiting -= 1;
|
|
241
|
+
this.lastAttendedAt = Date.now();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async attend(onChallenge) {
|
|
209
245
|
const startedAt = Date.now();
|
|
210
246
|
try {
|
|
211
247
|
return await withinCallBudget(this.runOnce(startedAt), startedAt);
|
|
@@ -333,6 +369,8 @@ export class AuthRunner {
|
|
|
333
369
|
// against double cleanup.
|
|
334
370
|
let childFinished = false;
|
|
335
371
|
let numberMatch;
|
|
372
|
+
let challengeSeen = false;
|
|
373
|
+
let abandonTimer;
|
|
336
374
|
let killTimer;
|
|
337
375
|
const kill = (signal) => {
|
|
338
376
|
try {
|
|
@@ -400,8 +438,30 @@ export class AuthRunner {
|
|
|
400
438
|
// joinBackgroundChild. Idempotent on the "already known" question, but
|
|
401
439
|
// a later number still overwrites a numberless pendingChallenge so a
|
|
402
440
|
// fresh joiner sees it — see MFA_PENDING_MARKER's own comment.
|
|
441
|
+
// Once the child is past its challenge, stop it when nobody is waiting
|
|
442
|
+
// on it any more (see ABANDON_MS). SIGKILL rather than SIGTERM: a
|
|
443
|
+
// graceful stop runs the MFA loop's failure path, which records the
|
|
444
|
+
// cooldown, and the user never had a chance to answer. The dead owner
|
|
445
|
+
// leaves a stale lock the next sign-in reclaims.
|
|
446
|
+
const watchAttention = () => {
|
|
447
|
+
if (childFinished)
|
|
448
|
+
return;
|
|
449
|
+
const relayedAt = this.sessionDir ? challengeRelayedAt(authLockPath(this.sessionDir)) ?? 0 : 0;
|
|
450
|
+
const delayMs = abandonDelayMs(this.waiting, Math.max(this.lastAttendedAt, relayedAt), Date.now());
|
|
451
|
+
if (delayMs > 0) {
|
|
452
|
+
abandonTimer = setTimeout(watchAttention, delayMs);
|
|
453
|
+
abandonTimer.unref?.();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
log("WARN", "Stopping a background sign-in nobody is waiting on; its MFA prompt went unanswered");
|
|
457
|
+
kill("SIGKILL");
|
|
458
|
+
finishChild(new AuthProcessError("timeout", `The sign-in was stopped because nobody approved its MFA prompt. Try again, or run ${AUTH_COMMAND}.`));
|
|
459
|
+
};
|
|
403
460
|
const publishChallenge = (matched) => {
|
|
461
|
+
challengeSeen = true;
|
|
404
462
|
const firstChallenge = this.pendingChallenge === null;
|
|
463
|
+
if (firstChallenge)
|
|
464
|
+
watchAttention();
|
|
405
465
|
if (firstChallenge)
|
|
406
466
|
devActivity("mfa_observed", { elapsedMs: Date.now() - started });
|
|
407
467
|
if (firstChallenge || matched) {
|
|
@@ -424,6 +484,8 @@ export class AuthRunner {
|
|
|
424
484
|
clearTimeout(timer);
|
|
425
485
|
if (killTimer)
|
|
426
486
|
clearTimeout(killTimer);
|
|
487
|
+
if (abandonTimer)
|
|
488
|
+
clearTimeout(abandonTimer);
|
|
427
489
|
process.off("exit", onExit);
|
|
428
490
|
const answeredEarly = callerSettled;
|
|
429
491
|
settleCaller(error);
|
|
@@ -500,6 +562,10 @@ export class AuthRunner {
|
|
|
500
562
|
6: ["transport", "Brightspace authentication is temporarily unavailable because of a network or server failure. Your saved session was preserved. Try again later."],
|
|
501
563
|
7: mfaPendingFailure(numberMatch),
|
|
502
564
|
};
|
|
565
|
+
// Busy, but the owning sign-in's challenge came through: the caller
|
|
566
|
+
// was told what to approve, so answer with that, not "busy".
|
|
567
|
+
if (code === 2 && challengeSeen)
|
|
568
|
+
failures[2] = mfaPendingFailure(numberMatch);
|
|
503
569
|
const [kind, message] = failures[code ?? -1] ?? ["failed", `Authentication failed. Run ${AUTH_COMMAND} to try again.`];
|
|
504
570
|
kill("SIGKILL");
|
|
505
571
|
finishChild(new AuthProcessError(kind, message, kind === "mfaPending" ? numberMatch : undefined));
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
4
|
* Licensed under MIT: see LICENSE file for details.
|
|
5
5
|
*/
|
|
6
|
-
import * as path from "node:path";
|
|
7
6
|
import { readFileSync, accessSync } from "node:fs";
|
|
8
7
|
import { BrowserAuthError } from "../utils/errors.js";
|
|
9
8
|
import { log } from "../utils/logger.js";
|
|
@@ -11,7 +10,8 @@ import { createSSOFlow, UnsupportedAuthenticationError, MfaApprovalError } from
|
|
|
11
10
|
import { isDuoPrompt } from "./duo-mfa.js";
|
|
12
11
|
import { BrowserStateStore } from "./browser-state-store.js";
|
|
13
12
|
import { hasNewerEntraState, recordMicrosoftSession } from "./microsoft-session.js";
|
|
14
|
-
import { acquireProcessLock } from "./auth-lock.js";
|
|
13
|
+
import { acquireProcessLock, AuthenticationInProgressError } from "./auth-lock.js";
|
|
14
|
+
import { authLockPath, publishChallenge, relayChallenge } from "./mfa-challenge.js";
|
|
15
15
|
import { AuthCooldown } from "./auth-cooldown.js";
|
|
16
16
|
import { mintAccessToken } from "./token-mint.js";
|
|
17
17
|
import { isMissingBrowserError, PLAYWRIGHT_INSTALL_HINT } from "../utils/browser-install.js";
|
|
@@ -54,12 +54,19 @@ export class BrowserAuth {
|
|
|
54
54
|
onPhase;
|
|
55
55
|
/** When the current login first reported an MFA challenge; splits credentials from approvalWait. */
|
|
56
56
|
challengeSeenAt;
|
|
57
|
+
lockPath;
|
|
58
|
+
/** The latest challenge write, awaited before the lock is released so none lands after it. */
|
|
59
|
+
publishing = Promise.resolve();
|
|
57
60
|
constructor(config, options = {}) {
|
|
58
61
|
this.config = config;
|
|
59
62
|
this.onPhase = options.onPhase;
|
|
60
63
|
const { onMfaChallenge } = options;
|
|
64
|
+
this.lockPath = authLockPath(config.sessionDir);
|
|
61
65
|
this.ssoFlow = createSSOFlow(config, options.requestMfaCode, onMfaChallenge && ((number) => {
|
|
62
66
|
this.challengeSeenAt ??= Date.now();
|
|
67
|
+
// Other server processes sharing this session cannot see our caller's
|
|
68
|
+
// tool response; the lock directory is where they look instead.
|
|
69
|
+
this.publishing = this.publishing.then(() => publishChallenge(this.lockPath, number)).catch(() => { });
|
|
63
70
|
onMfaChallenge(number);
|
|
64
71
|
}));
|
|
65
72
|
this.stateStore = new BrowserStateStore(config.sessionDir);
|
|
@@ -101,7 +108,13 @@ export class BrowserAuth {
|
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
110
|
async authenticate(options = {}) {
|
|
104
|
-
const release = await acquireProcessLock(
|
|
111
|
+
const release = await acquireProcessLock(this.lockPath, { mode: options.automatic ? "automatic" : "explicit" }).catch(async (error) => {
|
|
112
|
+
// Hand back the number the owner is showing, so this caller's user can
|
|
113
|
+
// approve that sign-in instead of being told only to wait for it.
|
|
114
|
+
if (error instanceof AuthenticationInProgressError)
|
|
115
|
+
error.challenge = await relayChallenge(this.lockPath);
|
|
116
|
+
throw error;
|
|
117
|
+
});
|
|
105
118
|
try {
|
|
106
119
|
// Even a cookie-only SAML redirect can issue an MFA push. Suppress all
|
|
107
120
|
// automatic browser attempts during cooldown; HTTP token refresh runs
|
|
@@ -114,6 +127,7 @@ export class BrowserAuth {
|
|
|
114
127
|
return token;
|
|
115
128
|
}
|
|
116
129
|
finally {
|
|
130
|
+
await this.publishing;
|
|
117
131
|
await release();
|
|
118
132
|
}
|
|
119
133
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brightspace MCP Server. Copyright (c) 2026 Rohan Muppa. MIT licensed.
|
|
3
|
+
*
|
|
4
|
+
* The MFA challenge a sign-in is showing, shared with every process that
|
|
5
|
+
* shares its session store (issue #199).
|
|
6
|
+
*
|
|
7
|
+
* Several server processes, one per open MCP client, can share one session
|
|
8
|
+
* directory, and only the one holding the sign-in lock can sign in. The number
|
|
9
|
+
* to enter used to reach the user only through the one tool call that started
|
|
10
|
+
* that sign-in. The owner now writes its challenge into the lock directory, so
|
|
11
|
+
* it disappears with the lock, and a contender reads it back and relays it.
|
|
12
|
+
* Relaying touches the file, which tells the owner's server that someone is
|
|
13
|
+
* still waiting on the sign-in (see AuthRunner's abandonment check).
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from "node:fs/promises";
|
|
16
|
+
import { statSync } from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { CHALLENGE_FILE } from "./auth-lock.js";
|
|
19
|
+
/** Same bound the auth child's stdout marker enforces: 1-3 digits and nothing else. */
|
|
20
|
+
const NUMBER_MATCH = /^\d{1,3}$/;
|
|
21
|
+
export function authLockPath(sessionDir) {
|
|
22
|
+
return path.join(sessionDir, ".auth.lock");
|
|
23
|
+
}
|
|
24
|
+
/** Record the challenge this process's sign-in is showing. Call only while holding the lock. */
|
|
25
|
+
export async function publishChallenge(lockPath, numberMatch) {
|
|
26
|
+
const challenge = numberMatch && NUMBER_MATCH.test(numberMatch) ? { numberMatch } : {};
|
|
27
|
+
await fs.writeFile(path.join(lockPath, CHALLENGE_FILE), JSON.stringify(challenge), { mode: 0o600 });
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The challenge the lock owner is showing, or undefined when it has published
|
|
31
|
+
* none. Marks the challenge as just relayed, so the owner keeps waiting.
|
|
32
|
+
*/
|
|
33
|
+
export async function relayChallenge(lockPath) {
|
|
34
|
+
const file = path.join(lockPath, CHALLENGE_FILE);
|
|
35
|
+
let challenge;
|
|
36
|
+
try {
|
|
37
|
+
challenge = JSON.parse(await fs.readFile(file, "utf8"));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
if (typeof challenge !== "object" || challenge === null)
|
|
43
|
+
return undefined;
|
|
44
|
+
const now = new Date();
|
|
45
|
+
await fs.utimes(file, now, now).catch(() => { });
|
|
46
|
+
const numberMatch = challenge.numberMatch;
|
|
47
|
+
return typeof numberMatch === "string" && NUMBER_MATCH.test(numberMatch) ? { numberMatch } : {};
|
|
48
|
+
}
|
|
49
|
+
/** When the lock's challenge was last published or relayed, or undefined when there is none. */
|
|
50
|
+
export function challengeRelayedAt(lockPath) {
|
|
51
|
+
try {
|
|
52
|
+
return statSync(path.join(lockPath, CHALLENGE_FILE)).mtimeMs;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The stdout marker AuthRunner parses for a challenge. */
|
|
59
|
+
export function challengeMarker(numberMatch) {
|
|
60
|
+
return numberMatch ? `MFA_NUMBER:${numberMatch}` : "MFA_PENDING";
|
|
61
|
+
}
|
package/build/auth-cli.js
CHANGED
|
@@ -11,6 +11,8 @@ import { MfaApprovalError } from "./auth/sso-flow.js";
|
|
|
11
11
|
import { NativeCredentialStoreError } from "./auth/credential-store.js";
|
|
12
12
|
import { retireLegacyProfile } from "./auth/legacy-profile.js";
|
|
13
13
|
import { formatPhaseMarker } from "./auth/auth-phases.js";
|
|
14
|
+
import { AuthenticationInProgressError } from "./auth/auth-lock.js";
|
|
15
|
+
import { challengeMarker } from "./auth/mfa-challenge.js";
|
|
14
16
|
import { AUTH_COMMAND, SETUP_COMMAND } from "./utils/commands.js";
|
|
15
17
|
import { initUpdateChecker, peekUpdateNotice } from "./utils/update-checker.js";
|
|
16
18
|
import { reexecLatestIfStale } from "./utils/self-update.js";
|
|
@@ -62,7 +64,7 @@ async function main() {
|
|
|
62
64
|
// seconds instead of waiting out the whole approval window. Stdout only
|
|
63
65
|
// — the parent parses stdout for structured markers, never stderr.
|
|
64
66
|
const onMfaChallenge = automatic
|
|
65
|
-
? (number) => console.log(number
|
|
67
|
+
? (number) => console.log(challengeMarker(number))
|
|
66
68
|
: undefined;
|
|
67
69
|
// Stage timings for the server's dev activity log, same stdout channel.
|
|
68
70
|
const onPhase = automatic
|
|
@@ -88,7 +90,12 @@ async function main() {
|
|
|
88
90
|
// where they were scraped — can cross the process boundary as data
|
|
89
91
|
// rather than free-form text.
|
|
90
92
|
if (error instanceof MfaApprovalError && error.numberMatch) {
|
|
91
|
-
console.log(
|
|
93
|
+
console.log(challengeMarker(error.numberMatch));
|
|
94
|
+
}
|
|
95
|
+
// Another process's sign-in holds the lock and is showing a challenge:
|
|
96
|
+
// pass it on, so this caller can tell its user what to approve.
|
|
97
|
+
if (automatic && error instanceof AuthenticationInProgressError && error.challenge) {
|
|
98
|
+
console.log(challengeMarker(error.challenge.numberMatch));
|
|
92
99
|
}
|
|
93
100
|
process.exitCode = error instanceof NativeCredentialStoreError ? 5
|
|
94
101
|
: code === "AUTH_IN_PROGRESS" ? 2
|
package/build/index.js
CHANGED
|
@@ -86,6 +86,7 @@ else {
|
|
|
86
86
|
const authRunner = envAuthActive
|
|
87
87
|
? undefined
|
|
88
88
|
: new AuthRunner({
|
|
89
|
+
sessionDir: config.sessionDir,
|
|
89
90
|
onProgress: (message) => {
|
|
90
91
|
void server.sendLoggingMessage({ level: "info", logger: "brightspace-auth", data: message }).catch(() => { });
|
|
91
92
|
},
|
|
@@ -82,6 +82,13 @@ export function errorResponse(message) {
|
|
|
82
82
|
* nothing is derived from the caught message, so a child process cannot put
|
|
83
83
|
* words in the response. The full error still goes to the log.
|
|
84
84
|
*/
|
|
85
|
+
/**
|
|
86
|
+
* Issue #199: a client running calls in a parallel batch can hold the answer
|
|
87
|
+
* carrying the number until the whole batch finishes, and the user never sees
|
|
88
|
+
* it in time. Ask the model to relay it on its own and call one tool at a time.
|
|
89
|
+
*/
|
|
90
|
+
const MFA_SOLO_CALL = "Don't run other Brightspace calls in parallel until the sign-in completes: a batched " +
|
|
91
|
+
"answer can reach the user too late to approve. Make one call at a time.";
|
|
85
92
|
const AUTH_FAILURE_GUIDANCE = {
|
|
86
93
|
busy: "A sign-in is already running in another process. Let it finish, then try again, " +
|
|
87
94
|
`or run \`${AUTH_COMMAND}\` in a terminal to take over that background attempt.`,
|
|
@@ -101,7 +108,8 @@ const AUTH_FAILURE_GUIDANCE = {
|
|
|
101
108
|
mfaPending: "Approve the sign-in request on your phone (Microsoft Authenticator or Duo). " +
|
|
102
109
|
"Tell the user that, then call this tool again right away without waiting for them to " +
|
|
103
110
|
"confirm: each call waits up to 45 seconds for the approval and returns the result as soon " +
|
|
104
|
-
"as the sign-in completes. Keep calling until it succeeds or reports a different error."
|
|
111
|
+
"as the sign-in completes. Keep calling until it succeeds or reports a different error. " +
|
|
112
|
+
MFA_SOLO_CALL,
|
|
105
113
|
inProgress: "Brightspace sign-in is still starting in the background (opening the browser and the " +
|
|
106
114
|
"school's login pages). Call this tool again right away: the next call joins the same " +
|
|
107
115
|
"sign-in, so it sends no second MFA prompt, and returns the number to approve as soon as " +
|
|
@@ -119,7 +127,8 @@ function authFailureMessage(error) {
|
|
|
119
127
|
return `Open Microsoft Authenticator and enter ${error.numberMatch} within 5 minutes. ` +
|
|
120
128
|
"Tell the user that number, then call this tool again right away without waiting for them " +
|
|
121
129
|
"to confirm: each call waits up to 45 seconds for the approval and returns the result as " +
|
|
122
|
-
"soon as the sign-in completes. Keep calling until it succeeds or reports a different error."
|
|
130
|
+
"soon as the sign-in completes. Keep calling until it succeeds or reports a different error. " +
|
|
131
|
+
MFA_SOLO_CALL;
|
|
123
132
|
}
|
|
124
133
|
return AUTH_FAILURE_GUIDANCE[error.kind];
|
|
125
134
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.13",
|
|
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",
|