brightspace-mcp-server 5.1.14 → 5.1.16
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
CHANGED
|
@@ -196,12 +196,13 @@ async function waitForTakeover(lockPath, stale) {
|
|
|
196
196
|
export async function acquireProcessLock(lockPath, options = {}) {
|
|
197
197
|
await fs.mkdir(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
198
198
|
const mode = options.mode ?? "explicit";
|
|
199
|
-
const lease = await acquireLease(lockPath, 0, mode);
|
|
199
|
+
const lease = await acquireLease(lockPath, 0, mode, options.abandoned);
|
|
200
200
|
return () => lease.release();
|
|
201
201
|
}
|
|
202
|
-
async function acquireLease(lockPath, depth, mode) {
|
|
202
|
+
async function acquireLease(lockPath, depth, mode, abandoned) {
|
|
203
203
|
if (depth > 16)
|
|
204
204
|
throw new AuthenticationInProgressError();
|
|
205
|
+
const gone = (candidate) => lockOps.isDead(candidate) || abandoned?.(candidate.pid) === true;
|
|
205
206
|
const owner = { pid: process.pid, host: hostname(), nonce: randomUUID(), mode };
|
|
206
207
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
207
208
|
try {
|
|
@@ -224,7 +225,7 @@ async function acquireLease(lockPath, depth, mode) {
|
|
|
224
225
|
const stale = await readOwner(lockPath);
|
|
225
226
|
if (!stale)
|
|
226
227
|
throw new AuthenticationInProgressError();
|
|
227
|
-
if (!
|
|
228
|
+
if (!gone(stale)) {
|
|
228
229
|
// A live owner is not stale — the existing behavior for two contending
|
|
229
230
|
// explicit runs, or an automatic run finding anything alive at all.
|
|
230
231
|
// The one exception: an explicit run may take over a live *automatic*
|
|
@@ -244,7 +245,7 @@ async function acquireLease(lockPath, depth, mode) {
|
|
|
244
245
|
const claim = await acquireLease(path.join(lockPath, "reclaim.lock"), depth + 1, mode);
|
|
245
246
|
try {
|
|
246
247
|
const current = await readOwner(lockPath);
|
|
247
|
-
if (current?.nonce !== stale.nonce || !
|
|
248
|
+
if (current?.nonce !== stale.nonce || !gone(current))
|
|
248
249
|
throw new AuthenticationInProgressError();
|
|
249
250
|
const retired = `${lockPath}.stale.${owner.nonce}`;
|
|
250
251
|
// A rename blocked by contention means another contender is inside this
|
package/build/runtime-relay.js
CHANGED
|
@@ -144,11 +144,18 @@ async function post(address, message, signal, reply) {
|
|
|
144
144
|
await response.body?.cancel();
|
|
145
145
|
throw new Unreachable(`HTTP ${response.status}`);
|
|
146
146
|
}
|
|
147
|
+
// A runtime answers every request with JSON or an event stream, or with a
|
|
148
|
+
// bodiless 202. Anything else is another program that took a dead
|
|
149
|
+
// runtime's port, so this is no runtime at all: start a fresh one.
|
|
150
|
+
const type = response.headers.get("content-type") ?? "";
|
|
151
|
+
if (response.status !== 202 && !/application\/json|text\/event-stream/i.test(type)) {
|
|
152
|
+
await response.body?.cancel();
|
|
153
|
+
throw new Unreachable(`HTTP ${response.status} from something other than a Brightspace runtime`);
|
|
154
|
+
}
|
|
147
155
|
if (!response.ok)
|
|
148
156
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
149
157
|
if (response.status === 202 || !response.body)
|
|
150
158
|
return;
|
|
151
|
-
const type = response.headers.get("content-type") ?? "";
|
|
152
159
|
if (!type.includes("text/event-stream")) {
|
|
153
160
|
const body = JSON.parse(await response.text());
|
|
154
161
|
for (const item of Array.isArray(body) ? body : [body])
|
package/build/shared-runtime.js
CHANGED
|
@@ -13,6 +13,8 @@ import { log } from "./utils/logger.js";
|
|
|
13
13
|
export const DEFAULT_IDLE_MS = 10 * 60_000;
|
|
14
14
|
/** How long a starting runtime waits for a runtime that is starting or stopping to settle. */
|
|
15
15
|
const CLAIM_WAIT_MS = 10_000;
|
|
16
|
+
/** How long a published runtime has to answer before it is taken for dead. */
|
|
17
|
+
const PROBE_TIMEOUT_MS = 2_000;
|
|
16
18
|
const STATE_FILE = "state.json";
|
|
17
19
|
const OWNER_LOCK = "owner.lock";
|
|
18
20
|
/**
|
|
@@ -108,19 +110,52 @@ export async function serveSharedRuntime(runtime, { dir, idleMs }) {
|
|
|
108
110
|
/**
|
|
109
111
|
* Take the owner lock for `dir`, waiting out a runtime that is starting or
|
|
110
112
|
* stopping. undefined when another live runtime has published its address.
|
|
113
|
+
*
|
|
114
|
+
* A published address that does not answer belongs to a runtime that died
|
|
115
|
+
* without cleaning up, typically in a reboot, which also leaves its owner
|
|
116
|
+
* lock behind. Its pid may since have gone to an unrelated process, so the
|
|
117
|
+
* lock is reclaimed when its owner is that pid, not only when the pid is gone.
|
|
118
|
+
* A runtime that is still starting holds the lock under its own pid and has
|
|
119
|
+
* not published yet, so it is waited for instead.
|
|
111
120
|
*/
|
|
112
121
|
async function claimOwnership(dir) {
|
|
113
122
|
const deadline = Date.now() + CLAIM_WAIT_MS;
|
|
114
123
|
for (;;) {
|
|
124
|
+
const published = await readRuntimeAddress(dir);
|
|
125
|
+
if (published && await answers(published))
|
|
126
|
+
return undefined;
|
|
115
127
|
try {
|
|
116
|
-
return await acquireProcessLock(path.join(dir, OWNER_LOCK), {
|
|
128
|
+
return await acquireProcessLock(path.join(dir, OWNER_LOCK), {
|
|
129
|
+
mode: "exclusive",
|
|
130
|
+
abandoned: (pid) => pid === published?.pid,
|
|
131
|
+
});
|
|
117
132
|
}
|
|
118
133
|
catch (error) {
|
|
119
134
|
if (!(error instanceof AuthenticationInProgressError))
|
|
120
135
|
throw error;
|
|
121
136
|
}
|
|
122
|
-
if (
|
|
137
|
+
if (Date.now() > deadline)
|
|
123
138
|
return undefined;
|
|
124
139
|
await delay(50);
|
|
125
140
|
}
|
|
126
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Whether the runtime at `address` is still serving it. Anything but a
|
|
144
|
+
* refused or timed-out connection, or a 401 from another server now on that
|
|
145
|
+
* port, counts: a runtime that is shutting down (503) is still alive.
|
|
146
|
+
*/
|
|
147
|
+
async function answers(address) {
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(address.url, {
|
|
150
|
+
headers: { Authorization: `Bearer ${address.token}` },
|
|
151
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
152
|
+
});
|
|
153
|
+
await response.body?.cancel();
|
|
154
|
+
// A runtime answers an authorized GET with 405 (it serves POST only). Any
|
|
155
|
+
// other answer is another program that took a dead runtime's port.
|
|
156
|
+
return response.status === 405;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
4
|
* Licensed under MIT — see LICENSE file for details.
|
|
5
5
|
*/
|
|
6
|
+
/** A numeric reference as its character, or left as written when it names no code point (issue #352). */
|
|
7
|
+
function codePoint(whole, code) {
|
|
8
|
+
return code <= 0x10ffff ? String.fromCodePoint(code) : whole;
|
|
9
|
+
}
|
|
6
10
|
/** Decode the character references Brightspace's HTML editor writes into attribute values. */
|
|
7
11
|
export function decodeEntities(value) {
|
|
8
12
|
return value
|
|
9
|
-
.replace(/&#x([0-9a-f]+);/gi, (
|
|
10
|
-
.replace(/&#(\d+);/g, (
|
|
13
|
+
.replace(/&#x([0-9a-f]+);/gi, (whole, hex) => codePoint(whole, parseInt(hex, 16)))
|
|
14
|
+
.replace(/&#(\d+);/g, (whole, dec) => codePoint(whole, parseInt(dec, 10)))
|
|
11
15
|
.replace(/"/g, '"')
|
|
12
16
|
.replace(/</g, "<")
|
|
13
17
|
.replace(/>/g, ">")
|
|
@@ -26,11 +26,11 @@ function decodeEntities(text) {
|
|
|
26
26
|
return ENTITIES[key];
|
|
27
27
|
if (key.startsWith("#x")) {
|
|
28
28
|
const code = Number.parseInt(key.slice(2), 16);
|
|
29
|
-
return Number.isFinite(code) && code > 0 ? String.fromCodePoint(code) : whole;
|
|
29
|
+
return Number.isFinite(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : whole;
|
|
30
30
|
}
|
|
31
31
|
if (key.startsWith("#")) {
|
|
32
32
|
const code = Number.parseInt(key.slice(1), 10);
|
|
33
|
-
return Number.isFinite(code) && code > 0 ? String.fromCodePoint(code) : whole;
|
|
33
|
+
return Number.isFinite(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : whole;
|
|
34
34
|
}
|
|
35
35
|
return whole;
|
|
36
36
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.16",
|
|
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",
|