brightspace-mcp-server 3.12.1 → 3.12.2

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.
@@ -53,6 +53,14 @@ const CALL_BUDGET_MS = 55000;
53
53
  * from another one) means nobody is approving.
54
54
  */
55
55
  const ABANDON_MS = MFA_POLL_MS;
56
+ /**
57
+ * Calls arriving within this long of a batch's first call count as that batch
58
+ * (issue #212). A client running tool calls in parallel sends them within
59
+ * milliseconds; a model retrying after reading an answer takes a whole round
60
+ * trip, and each call is held up to CALL_BUDGET_MS anyway, so a retry always
61
+ * lands in a fresh batch and gets the number again.
62
+ */
63
+ const WAVE_MS = 5000;
56
64
  /**
57
65
  * Milliseconds until a background sign-in counts as abandoned; zero or less
58
66
  * means it already is. A caller still waiting keeps it alive outright;
@@ -84,6 +92,7 @@ function mfaPendingFailure(numberMatch) {
84
92
  export class AuthProcessError extends AuthError {
85
93
  kind;
86
94
  numberMatch;
95
+ duplicate;
87
96
  constructor(kind, message,
88
97
  /**
89
98
  * Entra number-match digits, when the failure is "mfaPending". Crossed
@@ -91,10 +100,17 @@ export class AuthProcessError extends AuthError {
91
100
  * MFA_NUMBER_MARKER below) — already bounded to 1-3 digits at the point
92
101
  * it was scraped from the page, so it is safe to surface verbatim.
93
102
  */
94
- numberMatch) {
103
+ numberMatch,
104
+ /**
105
+ * Another call in the same parallel batch already carried this exact
106
+ * challenge to the user (issue #212), so this one should answer briefly
107
+ * instead of repeating the number and the instructions.
108
+ */
109
+ duplicate = false) {
95
110
  super(message);
96
111
  this.kind = kind;
97
112
  this.numberMatch = numberMatch;
113
+ this.duplicate = duplicate;
98
114
  this.name = "AuthProcessError";
99
115
  }
100
116
  }
@@ -157,20 +173,21 @@ function forwardLines(stream, emit) {
157
173
  buffered = "";
158
174
  });
159
175
  }
160
- /**
161
- * Launches the brightspace-auth CLI as a child process to
162
- * re-authenticate when the current session has expired.
163
- *
164
- * The child inherits the parent's resolved environment and working directory,
165
- * so both processes read the same account configuration and .env file. It
166
- * runs headless unless D2L_HEADLESS is set explicitly.
167
- *
168
- * run() settles as soon as the child reports an MFA challenge (with or
169
- * without a number to display) rather than waiting for the child to exit —
170
- * a tool call must not block for the whole approval window. The child keeps
171
- * running in the background; a later run() call joins that background child
172
- * instead of spawning a second one.
173
- */
176
+ function dedupeChallenge(error, wave) {
177
+ if (!(error instanceof AuthProcessError) || error.kind !== "mfaPending" || error.duplicate)
178
+ return error;
179
+ const key = error.numberMatch ?? "";
180
+ if (!wave.delivered.has(key)) {
181
+ wave.delivered.add(key);
182
+ // Once a challenge has gone back to the user, the batch is over: every
183
+ // call already in it is a sibling, but a call arriving from now on is a
184
+ // RETRY -- quite possibly because the client hid this very answer -- and
185
+ // must get the digits in full. This is what keeps #201's guarantee.
186
+ wave.closed = true;
187
+ return error;
188
+ }
189
+ return new AuthProcessError(error.kind, error.message, error.numberMatch, true);
190
+ }
174
191
  export class AuthRunner {
175
192
  /**
176
193
  * The login this process already started, if one is still running.
@@ -203,6 +220,12 @@ export class AuthRunner {
203
220
  waiting = 0;
204
221
  /** When the last caller left run(). */
205
222
  lastAttendedAt = 0;
223
+ /**
224
+ * The current parallel batch and the challenges already handed to one of
225
+ * its calls (issue #212). The sign-in itself is shared through inFlight and
226
+ * childDone; this only stops every call in the batch repeating the number.
227
+ */
228
+ wave = null;
206
229
  scriptPath;
207
230
  timeoutMs;
208
231
  onProgress;
@@ -232,15 +255,27 @@ export class AuthRunner {
232
255
  * the next call to join.
233
256
  */
234
257
  async run(onChallenge) {
258
+ // Synchronous, before any await: calls in one tick cannot split a batch.
259
+ const wave = this.joinWave();
235
260
  this.waiting += 1;
236
261
  try {
237
262
  return await this.attend(onChallenge);
238
263
  }
264
+ catch (error) {
265
+ throw dedupeChallenge(error, wave);
266
+ }
239
267
  finally {
240
268
  this.waiting -= 1;
241
269
  this.lastAttendedAt = Date.now();
242
270
  }
243
271
  }
272
+ joinWave() {
273
+ const now = Date.now();
274
+ if (!this.wave || this.wave.closed || now - this.wave.openedAt > WAVE_MS) {
275
+ this.wave = { openedAt: now, delivered: new Set(), closed: false };
276
+ }
277
+ return this.wave;
278
+ }
244
279
  async attend(onChallenge) {
245
280
  const startedAt = Date.now();
246
281
  try {
@@ -427,6 +462,7 @@ export class AuthRunner {
427
462
  this.childDone = null;
428
463
  this.pendingChallenge = null;
429
464
  this.challengeSignal = null;
465
+ this.wave = null;
430
466
  }
431
467
  });
432
468
  trackedCompletion.catch(() => { });
@@ -122,7 +122,14 @@ const AUTH_FAILURE_GUIDANCE = {
122
122
  * auth-runner.ts MFA_NUMBER_MARKER) — a bounded, pre-validated value, not
123
123
  * arbitrary child output, so it is safe to interpolate here.
124
124
  */
125
+ const MFA_ALREADY_REPORTED = "Brightspace sign-in is already in progress; another call in this batch has the number to " +
126
+ "approve. Retry this call once the sign-in completes.";
125
127
  function authFailureMessage(error) {
128
+ // Another call in this parallel batch already carries the number and the
129
+ // instructions (issue #212). Repeating them in every response made a batch
130
+ // unreadable; one short line is enough for this call.
131
+ if (error.kind === "mfaPending" && error.duplicate)
132
+ return MFA_ALREADY_REPORTED;
126
133
  if (error.kind === "mfaPending" && error.numberMatch) {
127
134
  return `Open Microsoft Authenticator and enter ${error.numberMatch} within 5 minutes. ` +
128
135
  "Tell the user that number, then call this tool again right away without waiting for them " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "3.12.1",
3
+ "version": "3.12.2",
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",