brightspace-mcp-server 5.0.6 → 5.0.8
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 +1 -0
- package/build/index.js +1 -1
- package/build/update.js +96 -38
- package/build/utils/update-checker.js +42 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -136,6 +136,7 @@ Things that change:
|
|
|
136
136
|
- **Settings go to the server, not to Codex.** A URL entry has no process to pass an `env` table to, so `D2L_*` settings (the [table above](#settings-you-might-want)) are set in the terminal that runs `http`, in a `.env` file in the directory you start it from, or saved by `setup`.
|
|
137
137
|
- **Sign-in messages show up in the server's terminal.** The number to approve still comes back in the chat's answer, and, in clients that show tool progress, while the question is running.
|
|
138
138
|
- **Stopping it:** press Ctrl+C. Questions already running finish first; press Ctrl+C again to stop at once.
|
|
139
|
+
- **Updating it:** restart the server, not Codex. Restarting a client only reconnects it to the server that is already running the old version. Stop the server and start the same `http` command again with the same `D2L_HTTP_TOKEN`, port, and settings; from a source checkout, run `npm run update` first. Update notices from a shared server say this too.
|
|
139
140
|
|
|
140
141
|
Any other MCP client that can connect to a Streamable HTTP URL with a bearer token can share the same server. To go back to one copy per chat, put the `command`/`args` entry back (rerunning `setup` does it) and stop the shared server.
|
|
141
142
|
|
package/build/index.js
CHANGED
|
@@ -75,7 +75,7 @@ else {
|
|
|
75
75
|
// Check for updates now, then every few hours. A stdio server can stay
|
|
76
76
|
// alive for days, so a boot-only check would never see a release. The
|
|
77
77
|
// interval is unref'd and cannot hold the process open.
|
|
78
|
-
startUpdateChecks();
|
|
78
|
+
startUpdateChecks({ transport: http ? "http" : "stdio" });
|
|
79
79
|
// Log active course filter config if any filter is set
|
|
80
80
|
if (config.courseFilter.includeCourseIds || config.courseFilter.excludeCourseIds || !config.courseFilter.activeOnly) {
|
|
81
81
|
log("DEBUG", "Course filter config", {
|
package/build/update.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* https://github.com/rohanmuppa/brightspace-mcp-server
|
|
8
8
|
*/
|
|
9
9
|
import { execSync } from "node:child_process";
|
|
10
|
-
import { readFileSync, realpathSync } from "node:fs";
|
|
10
|
+
import { existsSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { dirname, join, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import dotenv from "dotenv";
|
|
@@ -15,6 +15,7 @@ dotenv.config({ quiet: true });
|
|
|
15
15
|
// ANSI color helpers
|
|
16
16
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
17
17
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
18
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
18
19
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
19
20
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
20
21
|
const projectRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -27,10 +28,50 @@ function run(cmd, opts) {
|
|
|
27
28
|
}).trim();
|
|
28
29
|
}
|
|
29
30
|
catch (err) {
|
|
31
|
+
// tsc reports compile errors on stdout and npm adds its summary on
|
|
32
|
+
// stderr, so a failure keeps both.
|
|
30
33
|
const error = err;
|
|
31
|
-
|
|
34
|
+
const output = [error.stdout, error.stderr].filter((s) => s?.trim()).join("\n");
|
|
35
|
+
throw new Error(output || error.message || "Command failed");
|
|
32
36
|
}
|
|
33
37
|
}
|
|
38
|
+
/** How many trailing lines of a failed command's output are worth showing. */
|
|
39
|
+
const FAILURE_TAIL_LINES = 20;
|
|
40
|
+
/**
|
|
41
|
+
* The end of a failed command's output, fit for a terminal: escape sequences
|
|
42
|
+
* and control characters stripped, blank lines dropped, long logs cut to the
|
|
43
|
+
* last few lines (where npm and tsc put the actual error).
|
|
44
|
+
*/
|
|
45
|
+
export function failureDetail(err) {
|
|
46
|
+
if (err === undefined || err === null)
|
|
47
|
+
return [];
|
|
48
|
+
const text = err instanceof Error ? err.message : String(err);
|
|
49
|
+
const lines = text
|
|
50
|
+
.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "")
|
|
51
|
+
.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "")
|
|
52
|
+
.split("\n")
|
|
53
|
+
.map((line) => line.trimEnd())
|
|
54
|
+
.filter((line) => line.trim() !== "");
|
|
55
|
+
return lines.slice(-FAILURE_TAIL_LINES);
|
|
56
|
+
}
|
|
57
|
+
/** Print a failed step with its cause and how to recover, then exit. */
|
|
58
|
+
function fail(summary, err, recovery) {
|
|
59
|
+
console.error(red(` Error: ${summary}`));
|
|
60
|
+
const detail = failureDetail(err);
|
|
61
|
+
if (detail.length > 0) {
|
|
62
|
+
console.error("");
|
|
63
|
+
for (const line of detail)
|
|
64
|
+
console.error(dim(` ${line}`));
|
|
65
|
+
console.error("");
|
|
66
|
+
}
|
|
67
|
+
for (const line of recovery)
|
|
68
|
+
console.error(` ${line}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const RETRY_HINT = [
|
|
72
|
+
"The update is not finished. Fix the problem above, then run `npm run update` again;",
|
|
73
|
+
"it will redo the install and build even though the new code is already pulled.",
|
|
74
|
+
];
|
|
34
75
|
function getVersion() {
|
|
35
76
|
const pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf-8"));
|
|
36
77
|
return pkg.version || "unknown";
|
|
@@ -117,12 +158,6 @@ export function chooseRelease(registry, nodeVersion) {
|
|
|
117
158
|
}
|
|
118
159
|
return { ok: true, version, commit: gitHead };
|
|
119
160
|
}
|
|
120
|
-
function fail(...lines) {
|
|
121
|
-
console.error(red(` Error: ${lines[0]}`));
|
|
122
|
-
for (const line of lines.slice(1))
|
|
123
|
-
console.error(` ${line}`);
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
126
161
|
export function main() {
|
|
127
162
|
console.log("");
|
|
128
163
|
console.log(bold("=== Brightspace MCP Server — Update ==="));
|
|
@@ -172,12 +207,12 @@ export function main() {
|
|
|
172
207
|
registry = JSON.parse(run("npm view brightspace-mcp-server@latest version gitHead engines --json", { silent: true }));
|
|
173
208
|
}
|
|
174
209
|
catch {
|
|
175
|
-
fail("Could not read the latest release from npm.", "Check your network connection and try again.");
|
|
210
|
+
fail("Could not read the latest release from npm.", undefined, ["Check your network connection and try again."]);
|
|
176
211
|
return;
|
|
177
212
|
}
|
|
178
213
|
const release = chooseRelease(registry, process.version);
|
|
179
214
|
if (!release.ok) {
|
|
180
|
-
fail("No release this checkout can safely update to.", release.reason);
|
|
215
|
+
fail("No release this checkout can safely update to.", undefined, [release.reason]);
|
|
181
216
|
return;
|
|
182
217
|
}
|
|
183
218
|
const { version: targetVersion, commit } = release;
|
|
@@ -186,8 +221,8 @@ export function main() {
|
|
|
186
221
|
try {
|
|
187
222
|
run("git fetch origin main", { silent: true });
|
|
188
223
|
}
|
|
189
|
-
catch {
|
|
190
|
-
fail("Failed to fetch from remote.", "Check your network connection and try again.");
|
|
224
|
+
catch (err) {
|
|
225
|
+
fail("Failed to fetch from remote.", err, ["Check your network connection and try again."]);
|
|
191
226
|
return;
|
|
192
227
|
}
|
|
193
228
|
try {
|
|
@@ -197,7 +232,7 @@ export function main() {
|
|
|
197
232
|
throw new Error("version mismatch");
|
|
198
233
|
}
|
|
199
234
|
catch {
|
|
200
|
-
fail(`Could not verify release ${targetVersion} (commit ${commit.slice(0, 7)}) on origin/main.`, "Nothing was changed.");
|
|
235
|
+
fail(`Could not verify release ${targetVersion} (commit ${commit.slice(0, 7)}) on origin/main.`, undefined, ["Nothing was changed."]);
|
|
201
236
|
return;
|
|
202
237
|
}
|
|
203
238
|
// Local commits on main that origin/main lacks would need a merge or rebase.
|
|
@@ -205,51 +240,69 @@ export function main() {
|
|
|
205
240
|
if (aheadCount !== "0") {
|
|
206
241
|
refuse(`Your main has ${aheadCount} commit${aheadCount === "1" ? "" : "s"} that origin/main does not.`, "Move them to another branch or reconcile main with origin/main yourself, then run the update again.");
|
|
207
242
|
}
|
|
208
|
-
//
|
|
243
|
+
// A marker in the git directory records that the code was updated but the
|
|
244
|
+
// install or build has not yet succeeded. Without it a retry would see no
|
|
245
|
+
// new commits and report "Already up to date" over a broken build.
|
|
246
|
+
const marker = resolve(projectRoot, run("git rev-parse --git-path brightspace-update-incomplete", { silent: true }));
|
|
247
|
+
const unfinished = existsSync(marker);
|
|
209
248
|
const behindCount = run(`git rev-list --count HEAD..${commit}`, { silent: true });
|
|
210
|
-
if (behindCount === "0") {
|
|
249
|
+
if (behindCount === "0" && !unfinished) {
|
|
211
250
|
console.log("");
|
|
212
251
|
console.log(green(" Already up to date!"));
|
|
213
252
|
console.log("");
|
|
214
253
|
return;
|
|
215
254
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
console.log(bold(` ${behindCount} new commit${behindCount === "1" ? "" : "s"} in ${targetVersion}:`));
|
|
219
|
-
console.log("");
|
|
220
|
-
const log = run(`git log HEAD..${commit} --oneline`, { silent: true });
|
|
221
|
-
for (const line of log.split("\n")) {
|
|
222
|
-
console.log(` ${dim("•")} ${line}`);
|
|
223
|
-
}
|
|
224
|
-
console.log("");
|
|
225
|
-
// Fast-forward only, to the verified release: never a merge or rebase,
|
|
226
|
-
// whatever git's pull settings say, and no pull fallback if it fails.
|
|
227
|
-
console.log(dim(` Updating to ${targetVersion}...`));
|
|
228
|
-
try {
|
|
229
|
-
run(`git merge --ff-only ${commit}`, { silent: true });
|
|
255
|
+
if (behindCount === "0") {
|
|
256
|
+
console.log(yellow(" The last update did not finish. Finishing it now."));
|
|
230
257
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
258
|
+
else {
|
|
259
|
+
// Show what's new
|
|
260
|
+
console.log("");
|
|
261
|
+
console.log(bold(` ${behindCount} new commit${behindCount === "1" ? "" : "s"} in ${targetVersion}:`));
|
|
262
|
+
console.log("");
|
|
263
|
+
const log = run(`git log HEAD..${commit} --oneline`, { silent: true });
|
|
264
|
+
for (const line of log.split("\n")) {
|
|
265
|
+
console.log(` ${dim("•")} ${line}`);
|
|
266
|
+
}
|
|
267
|
+
console.log("");
|
|
268
|
+
// Recorded before HEAD moves, and cleared only after the build succeeds.
|
|
269
|
+
writeFileSync(marker, `${new Date().toISOString()}\n`);
|
|
270
|
+
// Fast-forward only, to the verified release: never a merge or rebase,
|
|
271
|
+
// whatever git's pull settings say, and no pull fallback if it fails.
|
|
272
|
+
console.log(dim(` Updating to ${targetVersion}...`));
|
|
273
|
+
try {
|
|
274
|
+
run(`git merge --ff-only ${commit}`, { silent: true });
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
// HEAD did not move, so this run left nothing unfinished.
|
|
278
|
+
if (!unfinished)
|
|
279
|
+
rmSync(marker, { force: true });
|
|
280
|
+
fail(`Could not fast-forward to ${targetVersion}.`, err, ["Nothing was changed. Check 'git status' and try again."]);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
234
283
|
}
|
|
235
284
|
// Install dependencies
|
|
236
285
|
console.log(dim(" Installing dependencies..."));
|
|
237
286
|
try {
|
|
238
287
|
run("npm install", { silent: true });
|
|
239
288
|
}
|
|
240
|
-
catch {
|
|
241
|
-
|
|
242
|
-
|
|
289
|
+
catch (err) {
|
|
290
|
+
fail("Failed to install dependencies.", err, [
|
|
291
|
+
"If a file is locked (EBUSY/EPERM), close every MCP client using this server first.",
|
|
292
|
+
...RETRY_HINT,
|
|
293
|
+
]);
|
|
294
|
+
return;
|
|
243
295
|
}
|
|
244
296
|
// Build
|
|
245
297
|
console.log(dim(" Building..."));
|
|
246
298
|
try {
|
|
247
299
|
run("npm run build", { silent: true });
|
|
248
300
|
}
|
|
249
|
-
catch {
|
|
250
|
-
|
|
251
|
-
|
|
301
|
+
catch (err) {
|
|
302
|
+
fail("Build failed.", err, RETRY_HINT);
|
|
303
|
+
return;
|
|
252
304
|
}
|
|
305
|
+
rmSync(marker, { force: true });
|
|
253
306
|
// Show new version
|
|
254
307
|
const newVersion = getVersion();
|
|
255
308
|
console.log("");
|
|
@@ -259,6 +312,11 @@ export function main() {
|
|
|
259
312
|
}
|
|
260
313
|
console.log("");
|
|
261
314
|
console.log(" Restart your MCP client to use the latest version.");
|
|
315
|
+
console.log(" If you run the shared HTTP server, restart the server instead: a client restart only");
|
|
316
|
+
console.log(" reconnects to the old process. Stop it, start node build/index.js http again with the");
|
|
317
|
+
console.log(" same D2L_HTTP_TOKEN, port, and D2L_* settings, and reconnect any client that does not.");
|
|
318
|
+
console.log(dim(" A chat already connected over stdio keeps its own copy and cannot move onto the shared"));
|
|
319
|
+
console.log(dim(" server mid-session; restart its client to load the new build."));
|
|
262
320
|
console.log("");
|
|
263
321
|
}
|
|
264
322
|
// `npm run update` runs this file directly. VITEST is set only by the test
|
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
*
|
|
21
21
|
* A source checkout gets different advice: installing the npm package would
|
|
22
22
|
* update some other copy, and for a fork it would mean leaving its changes
|
|
23
|
-
* behind. Such a checkout is told to
|
|
23
|
+
* behind. Such a checkout is told to run its own updater, or to opt out.
|
|
24
|
+
*
|
|
25
|
+
* What to restart depends on the transport: a stdio server dies with its
|
|
26
|
+
* client, but the shared HTTP server outlives every client and must itself be
|
|
27
|
+
* restarted.
|
|
24
28
|
*
|
|
25
29
|
* Set D2L_NO_UPDATE_CHECK to any value to switch the check off entirely.
|
|
26
30
|
*/
|
|
@@ -162,13 +166,37 @@ export async function fetchLatestVersion(fetchImpl = fetch) {
|
|
|
162
166
|
return null;
|
|
163
167
|
}
|
|
164
168
|
}
|
|
169
|
+
/** The command that starts the shared HTTP server from each kind of install. */
|
|
170
|
+
const HTTP_START_COMMAND = {
|
|
171
|
+
"npx-cache": `npx -y ${PACKAGE_NAME}@latest http`,
|
|
172
|
+
"npm-install": `${PACKAGE_NAME} http`,
|
|
173
|
+
"source-checkout": "node build/index.js http",
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* What to restart so the new version actually runs. A stdio server lives as
|
|
177
|
+
* long as its client, so restarting the client is enough. The shared HTTP
|
|
178
|
+
* server outlives every client: restarting one only reconnects it to the same
|
|
179
|
+
* old process, so the server itself has to be restarted, with the token and
|
|
180
|
+
* port its clients already use.
|
|
181
|
+
*/
|
|
182
|
+
function reloadStep(installKind, transport, to, afterAStep) {
|
|
183
|
+
// "Then" only follows an update step; an npx install with no stale cache to
|
|
184
|
+
// clear has none, so the restart is the first thing to do.
|
|
185
|
+
const restart = afterAStep ? "Then restart" : "Restart";
|
|
186
|
+
if (transport === "stdio")
|
|
187
|
+
return `${restart} your MCP client to pick up v${to}.`;
|
|
188
|
+
return `${restart} the shared HTTP server to pick up v${to}: restarting an MCP client only reconnects it ` +
|
|
189
|
+
`to this running server. Stop it (Ctrl+C where it runs, or stop its service), then start it again with ` +
|
|
190
|
+
`${HTTP_START_COMMAND[installKind]}, using the same D2L_HTTP_TOKEN, port, and D2L_* settings, ` +
|
|
191
|
+
`and reconnect any client that does not reconnect on its own.`;
|
|
192
|
+
}
|
|
165
193
|
/**
|
|
166
194
|
* Run one update check. Never throws and never blocks the caller for long:
|
|
167
195
|
* the registry request is bounded by a timeout and every failure is
|
|
168
196
|
* swallowed, because a version check must not affect the server.
|
|
169
197
|
*/
|
|
170
198
|
export async function initUpdateChecker(deps = {}) {
|
|
171
|
-
const { fetchImpl = fetch, env = process.env, installedVersion = getInstalledVersion(), installKind = installKindOf(), clearCaches = () => clearAllNpxCaches(), } = deps;
|
|
199
|
+
const { fetchImpl = fetch, env = process.env, installedVersion = getInstalledVersion(), installKind = installKindOf(), clearCaches = () => clearAllNpxCaches(), transport = "stdio", } = deps;
|
|
172
200
|
if (env.D2L_NO_UPDATE_CHECK)
|
|
173
201
|
return;
|
|
174
202
|
try {
|
|
@@ -180,24 +208,27 @@ export async function initUpdateChecker(deps = {}) {
|
|
|
180
208
|
const from = safeVersionLabel(installedVersion);
|
|
181
209
|
const to = safeVersionLabel(latest);
|
|
182
210
|
const headline = `Update available: v${from} to v${to}.`;
|
|
183
|
-
let
|
|
211
|
+
let update;
|
|
184
212
|
if (installKind === "npx-cache") {
|
|
185
213
|
const count = await clearCaches();
|
|
186
|
-
|
|
214
|
+
update = count > 0
|
|
187
215
|
? `Cleared ${count} stale npx cache director${count === 1 ? "y" : "ies"} for ${PACKAGE_NAME} ` +
|
|
188
|
-
`(kept the one this server is running from)
|
|
216
|
+
`(kept the one this server is running from).`
|
|
189
217
|
: "";
|
|
190
|
-
text = `${headline} ${cleanup}Restart your MCP client to pick up v${to}.`;
|
|
191
218
|
}
|
|
192
219
|
else if (installKind === "source-checkout") {
|
|
193
|
-
|
|
194
|
-
`(
|
|
195
|
-
`If it is a fork you maintain, set D2L_NO_UPDATE_CHECK=1 to turn off upstream update notices.`;
|
|
220
|
+
update = `This server runs from a source checkout, so update that checkout with npm run update ` +
|
|
221
|
+
`(it pulls, installs dependencies, and rebuilds).`;
|
|
196
222
|
}
|
|
197
223
|
else {
|
|
198
|
-
|
|
199
|
-
`, then ${CLEAR_NPX_CACHE_COMMAND} and restart your MCP client.`;
|
|
224
|
+
update = `Run: ${GLOBAL_INSTALL_COMMAND}, then ${CLEAR_NPX_CACHE_COMMAND}.`;
|
|
200
225
|
}
|
|
226
|
+
const optOut = installKind === "source-checkout"
|
|
227
|
+
? " If it is a fork you maintain, set D2L_NO_UPDATE_CHECK=1 to turn off upstream update notices."
|
|
228
|
+
: "";
|
|
229
|
+
const text = [headline, update, `${reloadStep(installKind, transport, to, update !== "")}${optOut}`]
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.join(" ");
|
|
201
232
|
// Preserve lastShownAt when the text is unchanged, so a periodic re-check
|
|
202
233
|
// does not reset the throttle and start repeating the same line.
|
|
203
234
|
state = state?.text === text ? state : { text, lastShownAt: null };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.8",
|
|
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",
|