brightspace-mcp-server 3.13.2 → 3.13.4
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 -0
- package/build/auth/browser-state-store.js +3 -1
- package/build/auth/logout.js +122 -0
- package/build/auth/microsoft-session.js +4 -3
- package/build/auth/session-store.js +28 -1
- package/build/auth-cli.js +7 -0
- package/build/utils/commands.js +2 -0
- package/build/utils/config.js +42 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,6 +124,8 @@ sits beside your password, so on **this** computer your two factors become one.
|
|
|
124
124
|
anyone who only has your password, and it changes nothing for your other devices. Full enrollment
|
|
125
125
|
steps: [docs/sign-in.md](docs/sign-in.md#answering-code-challenges-without-your-phone).
|
|
126
126
|
|
|
127
|
+
**To sign out early** (on a shared computer, say), run `npx -y brightspace-mcp-server@latest auth --logout`. It deletes this computer's saved Brightspace session, so the next question does a full sign-in. Your saved password, authenticator key, and settings are kept. An AI app that is already running can keep answering until its current access token expires; restart the app to drop it right away. It only clears this computer; it does not sign you out of Microsoft or Brightspace anywhere else. If a sign-in is in progress, it refuses and deletes nothing.
|
|
128
|
+
|
|
127
129
|
How often you're asked is up to your school, not this tool. Everything else about sign-in — Duo, authenticator codes, visible-browser mode, and running without a browser at all (Docker, WSL, hardware keys) — is in [docs/sign-in.md](docs/sign-in.md).
|
|
128
130
|
|
|
129
131
|
## Something not working?
|
|
@@ -9,6 +9,8 @@ import { SessionStoreError } from "../utils/errors.js";
|
|
|
9
9
|
import { NativeCredentialStoreError } from "./credential-store.js";
|
|
10
10
|
import { readEncryptedRecord, saveEncryptedRecord, trashFile } from "./encrypted-store.js";
|
|
11
11
|
export const BROWSER_STATE_FILE = "storage-state.encrypted.json";
|
|
12
|
+
/** Plaintext browser state from before encryption; load() still adopts it if present. */
|
|
13
|
+
export const LEGACY_BROWSER_STATE_FILE = "storage-state.json";
|
|
12
14
|
function validState(value) {
|
|
13
15
|
const state = value;
|
|
14
16
|
return !!state && Array.isArray(state.cookies) && Array.isArray(state.origins)
|
|
@@ -27,7 +29,7 @@ export class BrowserStateStore {
|
|
|
27
29
|
this.sessionDir = sessionDir;
|
|
28
30
|
this.options = options;
|
|
29
31
|
this.file = path.join(sessionDir, BROWSER_STATE_FILE);
|
|
30
|
-
this.legacyFile = path.join(sessionDir,
|
|
32
|
+
this.legacyFile = path.join(sessionDir, LEGACY_BROWSER_STATE_FILE);
|
|
31
33
|
}
|
|
32
34
|
storeError(action, error) {
|
|
33
35
|
if (error instanceof NativeCredentialStoreError)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brightspace MCP Server. Copyright (c) 2026 Rohan Muppa. MIT licensed.
|
|
3
|
+
*
|
|
4
|
+
* `auth --logout`: end the saved Brightspace session on this computer early.
|
|
5
|
+
*
|
|
6
|
+
* This is a local sign-out only. It deletes the files that let the next tool
|
|
7
|
+
* call skip the browser, so that call does a full sign-in. It does not log out
|
|
8
|
+
* of Microsoft or D2L on their servers, and it leaves the saved password,
|
|
9
|
+
* config.json, and every credential-store entry (including the session
|
|
10
|
+
* encryption key, which the next sign-in reuses) exactly as they were.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs/promises";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { acquireProcessLock, AuthenticationInProgressError } from "./auth-lock.js";
|
|
15
|
+
import { BROWSER_STATE_FILE, LEGACY_BROWSER_STATE_FILE } from "./browser-state-store.js";
|
|
16
|
+
import { MICROSOFT_SESSION_FILE } from "./microsoft-session.js";
|
|
17
|
+
import { authLockPath } from "./mfa-challenge.js";
|
|
18
|
+
import { SESSION_FILE, SessionStore } from "./session-store.js";
|
|
19
|
+
import { AUTH_COMMAND } from "../utils/commands.js";
|
|
20
|
+
import { resolveSessionLocation } from "../utils/config.js";
|
|
21
|
+
/** Browser state files other than the session token itself, in the order they are deleted. */
|
|
22
|
+
const BROWSER_STATE_FILES = [BROWSER_STATE_FILE, LEGACY_BROWSER_STATE_FILE, MICROSOFT_SESSION_FILE];
|
|
23
|
+
class RemoveError extends Error {
|
|
24
|
+
file;
|
|
25
|
+
constructor(file, cause) {
|
|
26
|
+
super(`Could not remove ${file}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
27
|
+
this.file = file;
|
|
28
|
+
this.name = "RemoveError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function removeIfPresent(file) {
|
|
32
|
+
try {
|
|
33
|
+
await fs.unlink(file);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === "ENOENT")
|
|
38
|
+
return false;
|
|
39
|
+
throw new RemoveError(path.basename(file), error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Delete the saved session files in one account's session directory.
|
|
44
|
+
*
|
|
45
|
+
* `sessionDir` is the resolved directory: `accounts/<hash>/` when a username
|
|
46
|
+
* is configured, the session root otherwise. Siblings, other accounts, and
|
|
47
|
+
* the pre-account files above `accounts/` are never this account's session
|
|
48
|
+
* and are not touched. The plaintext `storage-state.json` a version 1 install
|
|
49
|
+
* left behind is included because loading browser state would re-adopt it.
|
|
50
|
+
*
|
|
51
|
+
* Holds the sign-in lock for the duration, in a mode that neither takes over
|
|
52
|
+
* a live sign-in nor can be taken over, and throws AuthenticationInProgressError
|
|
53
|
+
* without deleting anything if one is running. It also waits out any session
|
|
54
|
+
* write already in flight rather than racing it.
|
|
55
|
+
*/
|
|
56
|
+
export async function clearSavedSession(sessionDir) {
|
|
57
|
+
// Taking the lock creates its parent directory; there is nothing to clear
|
|
58
|
+
// (and nothing to create) when there is no directory at all.
|
|
59
|
+
try {
|
|
60
|
+
await fs.access(sessionDir);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code === "ENOENT")
|
|
64
|
+
return { removed: [] };
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
const release = await acquireProcessLock(authLockPath(sessionDir), { mode: "exclusive" });
|
|
68
|
+
try {
|
|
69
|
+
const removed = [];
|
|
70
|
+
if (await new SessionStore(sessionDir).remove())
|
|
71
|
+
removed.push(SESSION_FILE);
|
|
72
|
+
for (const name of BROWSER_STATE_FILES) {
|
|
73
|
+
if (await removeIfPresent(path.join(sessionDir, name)))
|
|
74
|
+
removed.push(name);
|
|
75
|
+
}
|
|
76
|
+
return { removed };
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await release();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const KEPT = "Kept: your saved password, config.json, and any authenticator or credential-store entries.";
|
|
83
|
+
const MEMORY_NOTE = "A running Brightspace MCP server may still hold an access token in memory until it expires. Restart your AI client to drop it right away.";
|
|
84
|
+
const LOCAL_ONLY = "This only clears files on this computer. It does not sign you out of Microsoft or Brightspace on their servers.";
|
|
85
|
+
/**
|
|
86
|
+
* The `auth --logout` command. Resolves to the process exit code: 0 when the
|
|
87
|
+
* session was cleared or there was nothing to clear, 2 when a sign-in is in
|
|
88
|
+
* progress (the same code `auth` uses for that), 1 for anything else.
|
|
89
|
+
*/
|
|
90
|
+
export async function runLogout(options = {}) {
|
|
91
|
+
const print = options.print ?? ((line) => console.error(line));
|
|
92
|
+
try {
|
|
93
|
+
const sessionDir = options.sessionDir ?? resolveSessionLocation().sessionDir;
|
|
94
|
+
const { removed } = await clearSavedSession(sessionDir);
|
|
95
|
+
if (removed.length === 0) {
|
|
96
|
+
print("No saved Brightspace session was found on this computer, so there is nothing to clear.");
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
print("Signed out of Brightspace on this computer.");
|
|
100
|
+
print(`Removed: ${removed.join(", ")}`);
|
|
101
|
+
print(KEPT);
|
|
102
|
+
print(`The next Brightspace request, or \`${AUTH_COMMAND}\`, will do a full sign-in.`);
|
|
103
|
+
}
|
|
104
|
+
print(MEMORY_NOTE);
|
|
105
|
+
print(LOCAL_ONLY);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof AuthenticationInProgressError) {
|
|
110
|
+
print("Not signed out: a sign-in is in progress. Nothing was deleted.");
|
|
111
|
+
print("Wait for it to finish (or cancel it), then run this command again.");
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
if (error instanceof RemoveError) {
|
|
115
|
+
print(`Sign-out is incomplete. ${error.message}`);
|
|
116
|
+
print("Close anything that may be using that file, then run this command again to finish.");
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
print(`Could not clear the saved session: ${error instanceof Error ? error.message : String(error)}`);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -7,7 +7,8 @@ import * as fs from "node:fs/promises";
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { writeFileAtomic } from "../utils/atomic-write.js";
|
|
9
9
|
import { BROWSER_STATE_FILE } from "./browser-state-store.js";
|
|
10
|
-
|
|
10
|
+
/** The plain summary, relative to its session directory. */
|
|
11
|
+
export const MICROSOFT_SESSION_FILE = "microsoft-session.json";
|
|
11
12
|
const ENTRA_HOST = "login.microsoftonline.com";
|
|
12
13
|
const ENTRA_COOKIES = new Set(["ESTSAUTH", "ESTSAUTHPERSISTENT", "ESTSAUTHLIGHT"]);
|
|
13
14
|
const OUTCOMES = ["ticked", "already", "absent", "unknown", "off"];
|
|
@@ -41,7 +42,7 @@ function validSummary(value) {
|
|
|
41
42
|
}
|
|
42
43
|
async function readSummary(sessionDir) {
|
|
43
44
|
try {
|
|
44
|
-
const summary = JSON.parse(await fs.readFile(path.join(sessionDir,
|
|
45
|
+
const summary = JSON.parse(await fs.readFile(path.join(sessionDir, MICROSOFT_SESSION_FILE), "utf8"));
|
|
45
46
|
return validSummary(summary) ? summary : undefined;
|
|
46
47
|
}
|
|
47
48
|
catch {
|
|
@@ -59,7 +60,7 @@ export async function recordMicrosoftSession(sessionDir, state, rememberMfa) {
|
|
|
59
60
|
rememberMfa: rememberMfa ?? (await readSummary(sessionDir))?.rememberMfa ?? null,
|
|
60
61
|
};
|
|
61
62
|
await fs.mkdir(sessionDir, { recursive: true, mode: 0o700 });
|
|
62
|
-
await writeFileAtomic(path.join(sessionDir,
|
|
63
|
+
await writeFileAtomic(path.join(sessionDir, MICROSOFT_SESSION_FILE), JSON.stringify(summary), { mode: 0o600 });
|
|
63
64
|
}
|
|
64
65
|
/** The summary for saved browser state, or undefined when there is none. */
|
|
65
66
|
export async function readMicrosoftSession(sessionDir, now = Date.now()) {
|
|
@@ -12,6 +12,8 @@ import { acquireProcessLock, AuthenticationInProgressError } from "./auth-lock.j
|
|
|
12
12
|
import { hasSessionEncryptionKey, NativeCredentialStoreError } from "./credential-store.js";
|
|
13
13
|
import { decrypt, readEncryptedRecord, saveEncryptedRecord, trashFile } from "./encrypted-store.js";
|
|
14
14
|
const DEFAULT_SESSION_DIR = path.join(os.homedir(), ".d2l-session");
|
|
15
|
+
/** The saved access token, relative to its session directory. */
|
|
16
|
+
export const SESSION_FILE = "session.json";
|
|
15
17
|
function validTenantOrigin(origin) {
|
|
16
18
|
if (origin === undefined)
|
|
17
19
|
return true;
|
|
@@ -47,7 +49,7 @@ export class SessionStore {
|
|
|
47
49
|
constructor(sessionDir = DEFAULT_SESSION_DIR, options = {}) {
|
|
48
50
|
this.options = options;
|
|
49
51
|
this.sessionDir = sessionDir;
|
|
50
|
-
this.sessionFilePath = path.join(sessionDir,
|
|
52
|
+
this.sessionFilePath = path.join(sessionDir, SESSION_FILE);
|
|
51
53
|
}
|
|
52
54
|
async withWriteLock(operation) {
|
|
53
55
|
await fs.mkdir(this.sessionDir, { recursive: true, mode: 0o700 });
|
|
@@ -212,6 +214,31 @@ export class SessionStore {
|
|
|
212
214
|
this.storeError("clear", error);
|
|
213
215
|
}
|
|
214
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Delete the saved session without decrypting it, so a damaged file or a
|
|
219
|
+
* locked credential store cannot keep someone signed in. Permanent rather
|
|
220
|
+
* than moved to Trash: a session that was signed out on purpose should not
|
|
221
|
+
* stay recoverable. The encryption key stays in the credential store for the
|
|
222
|
+
* next sign-in. Resolves true when a file was removed.
|
|
223
|
+
*/
|
|
224
|
+
async remove() {
|
|
225
|
+
try {
|
|
226
|
+
return await this.withWriteLock(async () => {
|
|
227
|
+
try {
|
|
228
|
+
await fs.unlink(this.sessionFilePath);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error.code === "ENOENT")
|
|
233
|
+
return false;
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
this.storeError("remove", error);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
215
242
|
/** Prevent a slow mint from replacing credentials saved by a newer login. */
|
|
216
243
|
async saveIfCurrent(token, expected) {
|
|
217
244
|
try {
|
package/build/auth-cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { retireLegacyProfile } from "./auth/legacy-profile.js";
|
|
|
13
13
|
import { formatPhaseMarker } from "./auth/auth-phases.js";
|
|
14
14
|
import { AuthenticationInProgressError } from "./auth/auth-lock.js";
|
|
15
15
|
import { AUTOMATIC_PENDING_MARKER, challengeMarker } from "./auth/mfa-challenge.js";
|
|
16
|
+
import { runLogout } from "./auth/logout.js";
|
|
16
17
|
import { configureDevActivity } from "./utils/dev-activity.js";
|
|
17
18
|
import { AUTH_COMMAND, SETUP_COMMAND } from "./utils/commands.js";
|
|
18
19
|
import { initUpdateChecker, peekUpdateNotice } from "./utils/update-checker.js";
|
|
@@ -35,6 +36,12 @@ async function requestMfaCode() {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
async function main() {
|
|
39
|
+
// Local and offline: no banner, no update check, no hand-off to a newer
|
|
40
|
+
// release. It only deletes this machine's saved session files.
|
|
41
|
+
if (process.argv.includes("--logout")) {
|
|
42
|
+
process.exitCode = await runLogout();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
38
45
|
const automatic = process.argv.includes("--automatic");
|
|
39
46
|
// If this copy is stale, hand off to the current release instead of running
|
|
40
47
|
// our own out-of-date sign-in code. No prompt, no user action.
|
package/build/utils/commands.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
export const PACKAGE_NAME = "brightspace-mcp-server";
|
|
18
18
|
/** Re-authenticate. Always runs the current published release. */
|
|
19
19
|
export const AUTH_COMMAND = `npx -y ${PACKAGE_NAME}@latest auth`;
|
|
20
|
+
/** End the saved Brightspace session on this computer. Always runs the current published release. */
|
|
21
|
+
export const LOGOUT_COMMAND = `npx -y ${PACKAGE_NAME}@latest auth --logout`;
|
|
20
22
|
/** Re-run the setup wizard. Always runs the current published release. */
|
|
21
23
|
export const SETUP_COMMAND = `npx -y ${PACKAGE_NAME}@latest setup`;
|
|
22
24
|
/** Diagnose a broken setup. Always runs the current published release. */
|
package/build/utils/config.js
CHANGED
|
@@ -12,13 +12,15 @@ import { resolveStoredPassword } from "./secure-config.js";
|
|
|
12
12
|
import { getStoredTotpUri } from "../auth/credential-store.js";
|
|
13
13
|
import { normalizeTotpEnrollment } from "../auth/totp.js";
|
|
14
14
|
import { migrateLegacyState } from "../auth/legacy-state.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The saved settings, or null when there are none to use. A corrupt or
|
|
17
|
+
* permission-denied config.json must not take the whole server down at
|
|
18
|
+
* startup: env vars alone are a complete, if less convenient, configuration.
|
|
19
|
+
* The failure is still surfaced loudly (never silent) so a broken file doesn't
|
|
20
|
+
* masquerade as "no config.json was ever created".
|
|
21
|
+
* Idea from lmgveerhoek's fork (MIT).
|
|
22
|
+
*/
|
|
23
|
+
function readConfigStore() {
|
|
22
24
|
let store = null;
|
|
23
25
|
let storeLoadError;
|
|
24
26
|
if (configStoreExists()) {
|
|
@@ -39,12 +41,43 @@ export async function loadConfig() {
|
|
|
39
41
|
else {
|
|
40
42
|
console.error("[config] No config.json found, using environment variables");
|
|
41
43
|
}
|
|
42
|
-
|
|
44
|
+
return store;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Which school and account this configuration signs in as, and where that
|
|
48
|
+
* account's session lives (env > store > default for each). Shared by
|
|
49
|
+
* loadConfig and by commands that need only the location, such as
|
|
50
|
+
* `auth --logout`, so they can never disagree about where a session is.
|
|
51
|
+
*/
|
|
52
|
+
function resolveAccountLocation(store) {
|
|
43
53
|
const sessionRoot = process.env.D2L_SESSION_DIR
|
|
44
54
|
? expandTilde(process.env.D2L_SESSION_DIR)
|
|
45
55
|
: store?.sessionDir
|
|
46
56
|
? expandTilde(store.sessionDir)
|
|
47
57
|
: path.join(os.homedir(), ".d2l-session");
|
|
58
|
+
const configuredUrl = new URL(process.env.D2L_BASE_URL || store?.baseUrl || "https://purdue.brightspace.com");
|
|
59
|
+
if (configuredUrl.protocol !== "https:" || configuredUrl.username || configuredUrl.password) {
|
|
60
|
+
throw new Error("The Brightspace URL must be an HTTPS school URL without embedded credentials.");
|
|
61
|
+
}
|
|
62
|
+
const baseUrl = configuredUrl.origin;
|
|
63
|
+
const username = process.env.D2L_USERNAME || store?.username;
|
|
64
|
+
// A new account must never inherit another account's cookies, even at the same school.
|
|
65
|
+
return { baseUrl, username, sessionRoot, sessionDir: accountSessionDirectory(sessionRoot, baseUrl, username) };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Where the configured account keeps its saved session. Unlike loadConfig it
|
|
69
|
+
* reads no password and migrates nothing, so it works when the credential
|
|
70
|
+
* store is locked. `sessionDir` holds the account's files; `sessionRoot` is
|
|
71
|
+
* the directory above any `accounts/<hash>/` layer.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveSessionLocation() {
|
|
74
|
+
dotenv.config({ quiet: true });
|
|
75
|
+
const { sessionDir, sessionRoot } = resolveAccountLocation(readConfigStore());
|
|
76
|
+
return { sessionDir, sessionRoot };
|
|
77
|
+
}
|
|
78
|
+
export async function loadConfig() {
|
|
79
|
+
dotenv.config({ quiet: true });
|
|
80
|
+
const store = readConfigStore();
|
|
48
81
|
// Code-entry and other interactive MFA methods need a visible browser.
|
|
49
82
|
const headless = envBoolean(process.env.D2L_HEADLESS, "D2L_HEADLESS")
|
|
50
83
|
?? store?.headless
|
|
@@ -89,12 +122,7 @@ export async function loadConfig() {
|
|
|
89
122
|
const envAccessToken = readEnvSecret(process.env.D2L_ACCESS_TOKEN, "D2L_ACCESS_TOKEN");
|
|
90
123
|
const rawSessionCookie = readEnvSecret(process.env.D2L_SESSION_COOKIE, "D2L_SESSION_COOKIE");
|
|
91
124
|
const envSessionCookie = rawSessionCookie ? parseSessionCookieEnv(rawSessionCookie) : undefined;
|
|
92
|
-
const
|
|
93
|
-
if (configuredUrl.protocol !== "https:" || configuredUrl.username || configuredUrl.password) {
|
|
94
|
-
throw new Error("The Brightspace URL must be an HTTPS school URL without embedded credentials.");
|
|
95
|
-
}
|
|
96
|
-
const baseUrl = configuredUrl.origin;
|
|
97
|
-
const username = process.env.D2L_USERNAME || store?.username;
|
|
125
|
+
const { baseUrl, username, sessionRoot, sessionDir } = resolveAccountLocation(store);
|
|
98
126
|
const password = passwordless ? undefined : await resolveStoredPassword(baseUrl, username, store);
|
|
99
127
|
// Opt-in: only an enrollment the user deliberately saved makes the Entra
|
|
100
128
|
// sign-in answer a verification-code challenge itself. With none, every MFA
|
|
@@ -110,8 +138,6 @@ export async function loadConfig() {
|
|
|
110
138
|
// that reports a locked or missing store, loudly, for the credential the
|
|
111
139
|
// server genuinely cannot work without.
|
|
112
140
|
: username ? (await getStoredTotpUri(baseUrl, username).catch(() => null)) ?? undefined : undefined;
|
|
113
|
-
// A new account must never inherit another account's cookies, even at the same school.
|
|
114
|
-
const sessionDir = accountSessionDirectory(sessionRoot, baseUrl, username);
|
|
115
141
|
const legacyMigration = sessionDir !== sessionRoot ? await migrateLegacyState(sessionRoot) : undefined;
|
|
116
142
|
return {
|
|
117
143
|
baseUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.13.
|
|
3
|
+
"version": "3.13.4",
|
|
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",
|