brightspace-mcp-server 5.0.8 → 5.0.10
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 +2 -2
- package/build/setup/clients.js +7 -3
- package/build/setup/prepare.js +2 -0
- package/build/setup.js +23 -14
- package/build/utils/mcp-client-cli.js +59 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,7 +129,7 @@ Normally every chat starts its own copy of the server. With many Codex chats ope
|
|
|
129
129
|
url = "http://127.0.0.1:18765/mcp"
|
|
130
130
|
bearer_token_env_var = "D2L_HTTP_TOKEN"
|
|
131
131
|
```
|
|
132
|
-
(`env_http_headers = { "Authorization" = "D2L_HTTP_AUTHORIZATION" }` also works, if that variable holds `Bearer <token>`.) A project's own `.codex/config.toml` overrides this one, so remove any `brightspace` entry there or change it the same way.
|
|
132
|
+
(`env_http_headers = { "Authorization" = "D2L_HTTP_AUTHORIZATION" }` also works, if that variable holds `Bearer <token>`.) A project's own `.codex/config.toml` overrides this one, so remove any `brightspace` entry there or change it the same way. Rerunning `setup` recognizes this entry and keeps it, token settings included.
|
|
133
133
|
4. **Restart Codex.** Chats that were already open keep the old per-chat server until Codex restarts. If you stop or restart the shared server, calls fail until it is back; if they still fail after that, start a new chat or restart Codex.
|
|
134
134
|
|
|
135
135
|
Things that change:
|
|
@@ -138,7 +138,7 @@ Things that change:
|
|
|
138
138
|
- **Stopping it:** press Ctrl+C. Questions already running finish first; press Ctrl+C again to stop at once.
|
|
139
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.
|
|
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 (
|
|
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 (rerun `setup` and say yes when it offers to switch Codex back to its own copy per chat) and stop the shared server.
|
|
142
142
|
|
|
143
143
|
## When it asks you to sign in
|
|
144
144
|
|
package/build/setup/clients.js
CHANGED
|
@@ -163,7 +163,8 @@ export function detectMcpClients(overrides = {}) {
|
|
|
163
163
|
}
|
|
164
164
|
/**
|
|
165
165
|
* Register the server in every given app without asking. An entry that runs
|
|
166
|
-
* something else
|
|
166
|
+
* something else, or connects to the shared server by URL, is left alone
|
|
167
|
+
* unless `replace` is set, and a write that
|
|
167
168
|
* fails (an agent's sandbox may block another app's settings folder) is
|
|
168
169
|
* reported for that app instead of stopping the rest.
|
|
169
170
|
*/
|
|
@@ -174,13 +175,16 @@ export function registerAll(targets, { replace }) {
|
|
|
174
175
|
const existing = target.inspect();
|
|
175
176
|
if (existing.state === "current")
|
|
176
177
|
return { ...base, outcome: "current" };
|
|
178
|
+
if (existing.state === "shared_http" && !replace)
|
|
179
|
+
return { ...base, outcome: "shared_http", url: existing.url };
|
|
177
180
|
if (existing.state === "different" && !replace) {
|
|
178
181
|
return { ...base, outcome: "different_left", current: existing.current };
|
|
179
182
|
}
|
|
180
|
-
|
|
183
|
+
const replacing = existing.state !== "missing";
|
|
184
|
+
if (!target.write(replacing)) {
|
|
181
185
|
return { ...base, outcome: "failed", message: `${target.name} did not accept the registration.` };
|
|
182
186
|
}
|
|
183
|
-
return { ...base, outcome:
|
|
187
|
+
return { ...base, outcome: replacing ? "replaced" : "added" };
|
|
184
188
|
}
|
|
185
189
|
catch (error) {
|
|
186
190
|
return { ...base, outcome: "failed", message: error instanceof Error ? error.message : String(error) };
|
package/build/setup/prepare.js
CHANGED
|
@@ -87,6 +87,8 @@ function appLine(outcome) {
|
|
|
87
87
|
return ok("app", `${outcome.name}: old "brightspace" entry replaced`, false);
|
|
88
88
|
case "current":
|
|
89
89
|
return ok("app", `${outcome.name}: already set up`, false);
|
|
90
|
+
case "shared_http":
|
|
91
|
+
return ok("app", `${outcome.name}: already set up to use the shared server at ${outcome.url}`, false);
|
|
90
92
|
case "different_left":
|
|
91
93
|
return fail("app", `${outcome.name}: "brightspace" already runs ${outcome.current} — left as is (pass --replace to change it)`, false);
|
|
92
94
|
case "failed":
|
package/build/setup.js
CHANGED
|
@@ -151,34 +151,43 @@ export function planQuestions(mode, known) {
|
|
|
151
151
|
// ── Registering with one app ───────────────────────────────────────
|
|
152
152
|
/**
|
|
153
153
|
* Registers Brightspace in one app, never silently replacing an entry
|
|
154
|
-
* that points somewhere else: a matching entry is left alone,
|
|
155
|
-
*
|
|
156
|
-
*
|
|
154
|
+
* that points somewhere else: a matching entry is left alone, an entry using
|
|
155
|
+
* the shared server by URL is kept unless the person asks to switch back, and
|
|
156
|
+
* a different one is printed (so it can be restored) and replaced only on a yes.
|
|
157
|
+
* Resolves true when the app ends up set up.
|
|
157
158
|
*/
|
|
158
|
-
async function registerWithClient(
|
|
159
|
+
export async function registerWithClient(target, askLine, log = console.log) {
|
|
159
160
|
const existing = target.inspect();
|
|
160
161
|
if (existing.state === "current") {
|
|
161
|
-
|
|
162
|
+
log(green(` ${target.name} already has Brightspace configured.`));
|
|
162
163
|
return true;
|
|
163
164
|
}
|
|
165
|
+
if (existing.state === "shared_http") {
|
|
166
|
+
log(green(` ${target.name} uses the shared Brightspace server at ${existing.url}.`));
|
|
167
|
+
const switchBack = await askLine(` Switch it back to its own copy per chat (${serverCommand().join(" ")})? (yes/no): `);
|
|
168
|
+
if (!/^y(es)?$/i.test(switchBack)) {
|
|
169
|
+
log(dim(` Kept ${target.name} on the shared server.`));
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
164
173
|
if (existing.state === "different") {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const replace = await
|
|
174
|
+
log(yellow(` ${target.name} has "brightspace" pointing at:`));
|
|
175
|
+
log(` ${existing.current}`);
|
|
176
|
+
const replace = await askLine(` Replace it with ${serverCommand().join(" ")}? (yes/no): `);
|
|
168
177
|
if (!/^y(es)?$/i.test(replace)) {
|
|
169
|
-
|
|
178
|
+
log(dim(` Left ${target.name} unchanged.`));
|
|
170
179
|
return false;
|
|
171
180
|
}
|
|
172
181
|
}
|
|
173
182
|
try {
|
|
174
|
-
if (target.write(existing.state
|
|
175
|
-
|
|
183
|
+
if (target.write(existing.state !== "missing")) {
|
|
184
|
+
log(green(` ${target.name} configured!`));
|
|
176
185
|
return true;
|
|
177
186
|
}
|
|
178
|
-
|
|
187
|
+
log(yellow(` Could not configure ${target.name}. See README.md for the manual command.`));
|
|
179
188
|
}
|
|
180
189
|
catch (err) {
|
|
181
|
-
|
|
190
|
+
log(yellow(` Could not configure ${target.name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
182
191
|
}
|
|
183
192
|
return false;
|
|
184
193
|
}
|
|
@@ -474,7 +483,7 @@ async function interactive(args) {
|
|
|
474
483
|
if (plan.registerApps) {
|
|
475
484
|
for (const target of detectMcpClients()) {
|
|
476
485
|
const answer = await ask(rl2, target.question);
|
|
477
|
-
if (/^y(es)?$/i.test(answer) && await registerWithClient(rl2,
|
|
486
|
+
if (/^y(es)?$/i.test(answer) && await registerWithClient(target, (question) => ask(rl2, question))) {
|
|
478
487
|
configuredClients.push(target.name);
|
|
479
488
|
}
|
|
480
489
|
console.log("");
|
|
@@ -31,15 +31,41 @@ export function serverCommand(platform = process.platform) {
|
|
|
31
31
|
? ["cmd", "/c", "npx", "-y", `${PACKAGE_NAME}@latest`]
|
|
32
32
|
: ["npx", "-y", `${PACKAGE_NAME}@latest`];
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether a URL is where the shared server listens: plain http on this
|
|
36
|
+
* computer, at the MCP path (see HTTP_HOST and MCP_PATH in http-server.ts,
|
|
37
|
+
* which also accepts "localhost"). Any other URL is some other server.
|
|
38
|
+
*/
|
|
39
|
+
function isSharedServerUrl(url) {
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = new URL(url);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return parsed.protocol === "http:"
|
|
48
|
+
&& (parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost")
|
|
49
|
+
&& parsed.pathname === "/mcp";
|
|
50
|
+
}
|
|
34
51
|
export function classifyRegistration(entry, platform = process.platform) {
|
|
35
52
|
if (entry === undefined)
|
|
36
53
|
return { state: "missing" };
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
const unreadable = { state: "different", current: "(an entry setup could not read)" };
|
|
55
|
+
if (entry === null || typeof entry !== "object")
|
|
56
|
+
return unreadable;
|
|
57
|
+
const { command, args, url } = entry;
|
|
58
|
+
if (typeof command !== "string" && typeof url === "string") {
|
|
59
|
+
// Shown without user-info, query or fragment: a token in the URL must not
|
|
60
|
+
// reach the setup message or the prepare JSON (#295).
|
|
61
|
+
const shown = displayUrl(url);
|
|
62
|
+
if (shown === null)
|
|
63
|
+
return unreadable;
|
|
64
|
+
return isSharedServerUrl(url) ? { state: "shared_http", url: shown } : { state: "different", current: shown };
|
|
39
65
|
}
|
|
40
|
-
const { command, args } = entry;
|
|
41
66
|
if (typeof command !== "string") {
|
|
42
|
-
|
|
67
|
+
const transport = describeTransport(entry);
|
|
68
|
+
return transport === null ? unreadable : { state: "different", current: transport };
|
|
43
69
|
}
|
|
44
70
|
const argv = [command, ...(Array.isArray(args) ? args.map(String) : [])];
|
|
45
71
|
const expected = serverCommand(platform);
|
|
@@ -48,6 +74,35 @@ export function classifyRegistration(entry, platform = process.platform) {
|
|
|
48
74
|
}
|
|
49
75
|
return { state: "different", current: argv.join(" ") };
|
|
50
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* A URL with its user-info, query and fragment removed, which is where a
|
|
79
|
+
* token in a URL rides. Null when it doesn't parse.
|
|
80
|
+
*/
|
|
81
|
+
function displayUrl(url) {
|
|
82
|
+
if (!URL.canParse(url))
|
|
83
|
+
return null;
|
|
84
|
+
const parsed = new URL(url);
|
|
85
|
+
parsed.username = "";
|
|
86
|
+
parsed.password = "";
|
|
87
|
+
parsed.search = "";
|
|
88
|
+
parsed.hash = "";
|
|
89
|
+
return parsed.href;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A commandless entry with no string `url` (Windsurf's `serverUrl`, Gemini's
|
|
93
|
+
* `httpUrl`), reduced to its transport label and its cleaned URL. Everything
|
|
94
|
+
* else (headers, env, credential helpers, unknown fields) can carry secrets,
|
|
95
|
+
* so it never prints.
|
|
96
|
+
*/
|
|
97
|
+
function describeTransport(entry) {
|
|
98
|
+
const { type, serverUrl, httpUrl } = entry;
|
|
99
|
+
const raw = typeof serverUrl === "string" ? serverUrl : typeof httpUrl === "string" ? httpUrl : null;
|
|
100
|
+
const shown = raw === null ? null : displayUrl(raw);
|
|
101
|
+
if (shown === null)
|
|
102
|
+
return null;
|
|
103
|
+
const label = typeof type === "string" && /^[\w-]+$/.test(type) ? { type } : {};
|
|
104
|
+
return JSON.stringify({ ...label, url: shown });
|
|
105
|
+
}
|
|
51
106
|
function readJsonFile(filePath) {
|
|
52
107
|
try {
|
|
53
108
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.10",
|
|
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",
|