brightspace-mcp-server 5.1.7 → 5.1.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 CHANGED
@@ -165,7 +165,10 @@ How often you're asked is up to your school, not this tool. Everything else abou
165
165
 
166
166
  ## Something not working?
167
167
 
168
- - Run `npx -y brightspace-mcp-server@latest doctor` first — it checks your Node version, the sign-in browser, saved setup, credential store, Brightspace connectivity, saved sign-in, and installed version, and tells you exactly what to fix.
168
+ - Run `npx -y brightspace-mcp-server@latest doctor` first — it checks your Node version, the sign-in browser, saved setup, credential store, Brightspace connectivity, saved sign-in, and installed version, and tells you exactly what to fix. For a global installation, use `brightspace-mcp-server doctor` to check the copy your client launches.
169
+ - **Browser removed during disk cleanup?** Sign-in restores a missing Chromium download once and retries automatically. It uses the installed Playwright dependency's browser revision and preserves saved credentials, MFA enrollment and sessions. Downloads have a two-minute limit; offline or failed repairs return an actionable installation error instead of asking you to reset credentials.
170
+ - **Repair without signing in or changing client configuration:** run `brightspace-mcp-server repair` for a global install, or `npx -y brightspace-mcp-server@latest repair` for an npx installation. Add `--json` for automation. Diagnostics print an absolute command for the exact installation being checked. Repair verifies both the full browser executable and a real headless launch and close. It does not register clients, update school settings, or start SSO/MFA.
171
+ - Browser downloads are disposable; files inside an installed package's `node_modules` are dependency code. If those files were deleted, diagnostics give an installation-scoped npm command to restore the package's declared dependencies first. Browser repair does not run npm or upgrade packages automatically.
169
172
  - **`Authentication failed: The native credential store is locked or unavailable.`** Your password store is locked or hasn't let this server read the saved password. Unlock it (on a Mac, log in or unlock Keychain Access), then run `npx -y brightspace-mcp-server@latest auth` again, or `setup` if that still fails, and choose **Always Allow** when Keychain asks.
170
173
  - **It works in the terminal but not in the app:** the app starts it separately and may not see your password store yet — see [docs/troubleshooting.md](docs/troubleshooting.md).
171
174
  - **The app says the server timed out while starting:** open the app's MCP server log. Each startup step prints a timestamped `Startup: ...` line before it begins and `done in N ms` when it ends, and a step still running after 10 seconds prints `Startup: still ...` once. The last such line names the step it was stuck in, often the saved-password lookup in your password store. These lines never include your username, password, or cookies.
@@ -10,6 +10,7 @@ import * as path from "node:path";
10
10
  import { log } from "../utils/logger.js";
11
11
  import { AuthError } from "../utils/errors.js";
12
12
  import { AUTH_COMMAND } from "../utils/commands.js";
13
+ import { dependencyRepairHint, PLAYWRIGHT_INSTALL_HINT } from "../utils/browser-install.js";
13
14
  import { devActivity, newRecoveryId, RECOVERY_ID_ENV } from "../utils/dev-activity.js";
14
15
  import { parsePhaseMarker } from "./auth-phases.js";
15
16
  import { AUTOMATIC_PENDING_MARKER, RELAYED_CHALLENGE_MARKER, RELAYED_STARTING_MARKER, authLockPath, challengeRelayedAt, relayChallenge } from "./mfa-challenge.js";
@@ -969,6 +970,8 @@ export class AuthRunner {
969
970
  }
970
971
  else {
971
972
  const failures = {
973
+ 8: ["browserInstall", PLAYWRIGHT_INSTALL_HINT],
974
+ 9: ["dependencyMissing", dependencyRepairHint()],
972
975
  2: ["busy", `Authentication already in progress in another process. Complete that attempt, then retry, or run \`${AUTH_COMMAND}\` in a terminal to take over that background attempt.`],
973
976
  3: ["cooldown", `Automatic MFA is paused after an unsuccessful attempt. Run ${AUTH_COMMAND} to retry immediately.`],
974
977
  4: ["unsupported", "This identity provider cannot complete headless authentication. See the authentication logs."],
@@ -14,7 +14,7 @@ import { acquireProcessLock, AuthenticationInProgressError, inspectLockOwner } f
14
14
  import { authLockPath, publishAutomaticProgress, publishChallenge, relayChallenge } from "./mfa-challenge.js";
15
15
  import { AuthCooldown } from "./auth-cooldown.js";
16
16
  import { mintAccessToken } from "./token-mint.js";
17
- import { isMissingBrowserError, PLAYWRIGHT_INSTALL_HINT } from "../utils/browser-install.js";
17
+ import { launchWithBrowserRecovery } from "../utils/browser-install.js";
18
18
  import { signInCommand } from "../utils/commands.js";
19
19
  const SILENT_SSO_TIMEOUT_MS = 30000;
20
20
  const SILENT_SSO_POLL_MS = 1000;
@@ -164,22 +164,14 @@ export class BrowserAuth {
164
164
  };
165
165
  try {
166
166
  state = await this.stateStore.load();
167
- const { chromium } = await import("playwright");
168
167
  const args = ["--disable-blink-features=AutomationControlled"];
169
168
  if (BrowserAuth.isWSLOrDocker())
170
169
  args.push("--no-sandbox", "--disable-setuid-sandbox");
171
170
  // Use Playwright's own timeout, which cleans up an unsuccessful launch.
172
- try {
173
- browser = await this.timed("launch", () => chromium.launch({ headless: this.config.headless, timeout: 60000, args }));
174
- }
175
- catch (launchError) {
176
- // Keep the remedy attached to the failure. Without this the hint is
177
- // lost when the auth runner flattens errors into "Authentication failed".
178
- if (isMissingBrowserError(launchError)) {
179
- throw new BrowserAuthError(PLAYWRIGHT_INSTALL_HINT, "browser_missing", launchError);
180
- }
181
- throw launchError;
182
- }
171
+ browser = await this.timed("launch", () => launchWithBrowserRecovery(async () => {
172
+ const { chromium } = await import("playwright");
173
+ return chromium.launch({ headless: this.config.headless, timeout: 60000, args });
174
+ }));
183
175
  process.once("SIGINT", closeOnSignal);
184
176
  process.once("SIGTERM", closeOnSignal);
185
177
  context = await browser.newContext({ viewport: { width: 1280, height: 720 }, storageState: state });
package/build/auth-cli.js CHANGED
@@ -19,6 +19,7 @@ import { AUTH_COMMAND, SETUP_COMMAND } from "./utils/commands.js";
19
19
  import { initUpdateChecker, peekUpdateNotice } from "./utils/update-checker.js";
20
20
  import { reexecLatestIfStale } from "./utils/self-update.js";
21
21
  import { detectSkew } from "./utils/install-sites.js";
22
+ import { BrowserInstallError } from "./utils/browser-install.js";
22
23
  dotenv.config({ quiet: true });
23
24
  const pkg = JSON.parse(readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"));
24
25
  async function requestMfaCode() {
@@ -118,15 +119,18 @@ async function main() {
118
119
  for (const line of relayedLockLines(error.challenge, error.ownerMode))
119
120
  console.log(line);
120
121
  }
121
- process.exitCode = error instanceof NativeCredentialStoreError ? 5
122
- : code === "AUTH_IN_PROGRESS" ? 2
123
- : code === "AUTH_COOLDOWN" ? 3
124
- : code === "AUTH_UNSUPPORTED" ? 4
125
- : code === "AUTH_TRANSPORT" ? 6
126
- : error instanceof MfaApprovalError ? 7 : 1;
122
+ process.exitCode = error instanceof BrowserInstallError ? (error.dependencyMissing ? 9 : 8)
123
+ : error instanceof NativeCredentialStoreError ? 5
124
+ : code === "AUTH_IN_PROGRESS" ? 2
125
+ : code === "AUTH_COOLDOWN" ? 3
126
+ : code === "AUTH_UNSUPPORTED" ? 4
127
+ : code === "AUTH_TRANSPORT" ? 6
128
+ : error instanceof MfaApprovalError ? 7 : 1;
127
129
  console.error("\nAuthentication failed:", error instanceof Error ? error.message : "Unknown authentication error");
128
- console.error(`Run \`${SETUP_COMMAND}\` to update saved credentials.`);
129
- console.error(`Run \`${AUTH_COMMAND}\` to retry explicitly. This bypasses the automatic MFA cooldown.`);
130
+ if (!(error instanceof BrowserInstallError)) {
131
+ console.error(`Run \`${SETUP_COMMAND}\` to update saved credentials.`);
132
+ console.error(`Run \`${AUTH_COMMAND}\` to retry explicitly. This bypasses the automatic MFA cooldown.`);
133
+ }
130
134
  }
131
135
  finally {
132
136
  // The failure path is exactly where knowing you are out of date matters
package/build/doctor.js CHANGED
@@ -29,7 +29,7 @@ import { discoverVersions } from "./api/version-discovery.js";
29
29
  import { D2LApiClient } from "./api/client.js";
30
30
  import { fetchLatestVersion, isNewerVersion, safeVersionLabel, installKindOf, } from "./utils/update-checker.js";
31
31
  import { SETUP_COMMAND, AUTH_COMMAND, GLOBAL_INSTALL_COMMAND, CLEAR_NPX_CACHE_COMMAND, prepareCommand, signInCommand, } from "./utils/commands.js";
32
- import { chromiumReady, chromiumStatus } from "./utils/browser-install.js";
32
+ import { chromiumReady, chromiumStatus, dependencyRepairHint, BROWSER_REPAIR_COMMAND } from "./utils/browser-install.js";
33
33
  import { nodeVersionSupported, readOnlyTokenManager, readSavedSessionToken, sessionDirFor } from "./utils/saved-session.js";
34
34
  // Tests import this from doctor.ts.
35
35
  export { readOnlySessionStore } from "./utils/saved-session.js";
@@ -213,7 +213,11 @@ async function checkBrowser(deps) {
213
213
  }
214
214
  if (chromiumReady(status))
215
215
  return pass("browser", "Chromium is installed for sign-in");
216
- return problem("browser", `Chromium is missing, so sign-in can't start — run: ${prepareCommand()}`);
216
+ if (status.headless === "dependency_missing")
217
+ return problem("browser", dependencyRepairHint());
218
+ if (status.headless === "unknown")
219
+ return problem("browser", `Chromium's test launch failed. Check operating system permissions and browser system dependencies — run: ${BROWSER_REPAIR_COMMAND}`);
220
+ return problem("browser", `Chromium is missing, so sign-in can't start — run: ${BROWSER_REPAIR_COMMAND}`);
217
221
  }
218
222
  async function checkNetwork(deps, baseUrl) {
219
223
  try {
package/build/index.js CHANGED
@@ -26,7 +26,7 @@ const PKG_VERSION = (() => {
26
26
  })();
27
27
  // ── Subcommand routing (before any MCP initialization) ──────────────
28
28
  const subcommand = process.argv[2];
29
- const COMMANDS = ['setup', 'auth', 'doctor', 'http', 'runtime'];
29
+ const COMMANDS = ['setup', 'auth', 'doctor', 'repair', 'http', 'runtime'];
30
30
  // A mistyped command (`htp`) must not quietly start a stdio server that sits
31
31
  // waiting on stdin. Flags alone still mean the default stdio server.
32
32
  if (subcommand !== undefined && !subcommand.startsWith('-') && !COMMANDS.includes(subcommand)) {
@@ -37,7 +37,8 @@ if (subcommand !== undefined && !subcommand.startsWith('-') && !COMMANDS.include
37
37
  ' http run one shared MCP server on localhost\n' +
38
38
  ' setup configure your school and sign-in\n' +
39
39
  ' auth sign in to Brightspace now\n' +
40
- ' doctor check the install and configuration\n');
40
+ ' doctor check the install and configuration\n' +
41
+ ' repair restore and verify this installation\'s Chromium\n');
41
42
  process.exit(2);
42
43
  }
43
44
  if (subcommand === 'setup') {
@@ -49,6 +50,9 @@ else if (subcommand === 'auth') {
49
50
  else if (subcommand === 'doctor') {
50
51
  await import('./doctor.js');
51
52
  }
53
+ else if (subcommand === 'repair') {
54
+ await (await import('./repair.js')).main();
55
+ }
52
56
  else {
53
57
  // ── MCP Server ──────────────────────────────────────────────────────
54
58
  // default: stdio, relayed to the one shared runtime, started on demand;
@@ -0,0 +1,37 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { resolve } from "node:path";
3
+ import { chromiumStatus, chromiumReady, installChromium, dependencyRepairHint, BROWSER_REPAIR_COMMAND } from "./utils/browser-install.js";
4
+ /** Browser maintenance only: no config, credential-store, client or sign-in imports. */
5
+ export async function runRepair(deps = { status: chromiumStatus, install: installChromium }) {
6
+ let status = await deps.status();
7
+ if (status.headless === "dependency_missing")
8
+ return { ok: false, message: dependencyRepairHint() };
9
+ if (status.headless === "missing" || !status.visible) {
10
+ const installed = await deps.install();
11
+ if (!installed.ok)
12
+ return { ok: false, message: `Chromium installation failed. Check your connection and retry: ${BROWSER_REPAIR_COMMAND}` };
13
+ status = await deps.status();
14
+ }
15
+ return chromiumReady(status)
16
+ ? { ok: true, message: "Chromium is installed and a headless launch and close succeeded." }
17
+ : { ok: false, message: "Chromium could not launch. Check operating system permissions and browser system dependencies, then run repair again." };
18
+ }
19
+ export async function main() {
20
+ const args = process.argv.slice(3);
21
+ if (args.some(arg => arg !== "--json")) {
22
+ console.error("Usage: brightspace-mcp-server repair [--json]");
23
+ process.exitCode = 2;
24
+ return;
25
+ }
26
+ try {
27
+ const result = await runRepair();
28
+ console.log(args.includes("--json") ? JSON.stringify(result) : result.message);
29
+ process.exitCode = result.ok ? 0 : 1;
30
+ }
31
+ catch {
32
+ console.error(`Browser repair failed. Run: ${BROWSER_REPAIR_COMMAND}`);
33
+ process.exitCode = 1;
34
+ }
35
+ }
36
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url))
37
+ void main();
@@ -13,7 +13,7 @@ import { nodeVersionSupported, readSavedSessionToken, sessionDirFor } from "../u
13
13
  import { discoverVersions } from "../api/version-discovery.js";
14
14
  import { getConfigStorePath } from "../utils/config-store.js";
15
15
  import { saveSecureConfig } from "../utils/secure-config.js";
16
- import { chromiumReady, chromiumStatus, installChromium, PLAYWRIGHT_INSTALL_HINT, } from "../utils/browser-install.js";
16
+ import { chromiumReady, chromiumStatus, dependencyRepairHint, installChromium, PLAYWRIGHT_INSTALL_HINT, } from "../utils/browser-install.js";
17
17
  import { DOCTOR_COMMAND, prepareCommand, signInCommand } from "../utils/commands.js";
18
18
  import { runCommandWithoutPrompts } from "../utils/mcp-client-cli.js";
19
19
  import { buildConfigToSave, isValidUrl, normalizeUrl, readExistingConfig, sameSchool } from "./config.js";
@@ -135,7 +135,7 @@ export async function runPrepare(args, overrides = {}) {
135
135
  steps.push(store.ok ? ok("credential-store", store.line) : fail("credential-store", store.line));
136
136
  // ── Chromium ──
137
137
  let browser = await deps.chromiumStatus();
138
- if (!chromiumReady(browser) && args.installBrowser) {
138
+ if (!chromiumReady(browser) && args.installBrowser && browser.headless !== "dependency_missing" && browser.headless !== "unknown") {
139
139
  deps.progress(" Downloading Chromium for sign-in (this can take a few minutes)...");
140
140
  const installed = await deps.installChromium();
141
141
  browser = installed.ok ? await deps.chromiumStatus() : browser;
@@ -143,14 +143,14 @@ export async function runPrepare(args, overrides = {}) {
143
143
  deps.progress(` ${installed.message}`);
144
144
  }
145
145
  if (chromiumReady(browser)) {
146
- steps.push(ok("browser", browser.headless === "unknown"
147
- ? `Chromium is installed (a test launch didn't work here: ${browser.detail ?? "unknown reason"}; sign-in in a terminal normally still works)`
148
- : "Chromium is installed for sign-in"));
146
+ steps.push(ok("browser", "Chromium is installed for sign-in"));
149
147
  }
150
148
  else {
151
- steps.push(fail("browser", args.installBrowser
152
- ? `Chromium could not be installed. ${PLAYWRIGHT_INSTALL_HINT}`
153
- : "Chromium is missing — run prepare again without --no-browser-install"));
149
+ steps.push(fail("browser", browser.headless === "dependency_missing" ? dependencyRepairHint()
150
+ : browser.headless === "unknown" ? "Chromium's test launch failed. Check operating system permissions and browser system dependencies."
151
+ : args.installBrowser
152
+ ? `Chromium could not be installed. ${PLAYWRIGHT_INSTALL_HINT}`
153
+ : "Chromium is missing — run prepare again without --no-browser-install"));
154
154
  }
155
155
  // ── Network, settings, session (need a school) ──
156
156
  let signedIn = false;
@@ -9,6 +9,7 @@ import { TokenRefreshError } from "../api/errors.js";
9
9
  import { AuthProcessError } from "../auth/auth-runner.js";
10
10
  import { log } from "../utils/logger.js";
11
11
  import { AUTH_COMMAND } from "../utils/commands.js";
12
+ import { dependencyRepairHint, PLAYWRIGHT_INSTALL_HINT } from "../utils/browser-install.js";
12
13
  import { getUpdateNotice } from "../utils/update-checker.js";
13
14
  import { mfaShortcutTip } from "../utils/two-factor-shortcuts.js";
14
15
  import { DownloadError, isSafeDetail, } from "../utils/download-errors.js";
@@ -94,6 +95,8 @@ export function errorResponse(message, kind) {
94
95
  const MFA_SOLO_CALL = "Don't run other Brightspace calls in parallel until the sign-in completes: a batched " +
95
96
  "answer can reach the user too late to approve. Make one call at a time.";
96
97
  const AUTH_FAILURE_GUIDANCE = {
98
+ browserInstall: PLAYWRIGHT_INSTALL_HINT,
99
+ dependencyMissing: dependencyRepairHint(),
97
100
  busy: "A sign-in is already running in another process. Let it finish, then try again, " +
98
101
  `or run \`${AUTH_COMMAND}\` in a terminal to take over that background attempt.`,
99
102
  cooldown: "Automatic sign-in is paused because an MFA prompt went unanswered. " +
@@ -1,39 +1,46 @@
1
1
  /**
2
2
  * Brightspace MCP Server. Copyright (c) 2026 Rohan Muppa. MIT licensed.
3
3
  *
4
- * Recognising a missing Playwright browser.
5
- *
6
- * This package's postinstall downloads Chromium. npm skips postinstall under
7
- * `--ignore-scripts` and under the newer allow-scripts policy, both of which
8
- * are common on global installs, so the package can land without a browser and
9
- * fail only later, at first sign-in.
10
- *
11
- * Playwright's own message does say what to run, but by the time it has been
12
- * wrapped in BrowserAuthError and then flattened into a generic
13
- * "Authentication failed" by the auth runner, the actionable command is gone.
14
- * Matching the error lets us keep the fix attached to the failure.
15
- *
16
- * Sign-in matches reactively, so it never reports a missing browser unless a
17
- * launch really failed. `setup --prepare` and `doctor` check up front instead
18
- * (chromiumStatus), through Playwright's public API only, and prepare installs
19
- * a missing browser with the package's own Playwright (installChromium).
4
+ * Browser downloads are disposable; installed dependency code is not.
5
+ * Repair downloads through this installation's own Playwright CLI. Sign-in
6
+ * repairs only a missing executable and retries once; it never runs npm.
20
7
  */
21
- import { spawn } from "node:child_process";
8
+ import { spawn, execFileSync } from "node:child_process";
22
9
  import * as fs from "node:fs";
23
10
  import * as path from "node:path";
24
11
  import { createRequire } from "node:module";
25
- import { CLEAR_NPX_CACHE_COMMAND } from "./commands.js";
26
- export const PLAYWRIGHT_INSTALL_HINT = "Chromium is not installed, so sign-in cannot start. Your package manager " +
27
- "skipped this package's postinstall script, which is what normally downloads it " +
28
- "(common with --ignore-scripts or an allow-scripts policy). Run: " +
29
- "npx playwright install chromium" +
30
- `. If that does not help, reinstall and clear caches with ${CLEAR_NPX_CACHE_COMMAND}.`;
12
+ import { fileURLToPath } from "node:url";
13
+ /** Run the installation that reported the error, even from another project. */
14
+ export const BROWSER_REPAIR_COMMAND = `node "${fileURLToPath(new URL("../index.js", import.meta.url)).replaceAll("\\", "/")}" repair`;
15
+ export function dependencyRepairHint() {
16
+ const root = fileURLToPath(new URL("../../", import.meta.url)).replaceAll("\\", "/");
17
+ const reinstall = root.split("/").includes("node_modules")
18
+ ? `npm install --prefix "${root}" --omit=dev`
19
+ : `npm ci --prefix "${root}", then npm run build in that directory`;
20
+ return "Playwright's package code is missing. Restore this installation's declared dependencies with " +
21
+ `${reinstall}, then run: ${BROWSER_REPAIR_COMMAND}`;
22
+ }
23
+ export class BrowserInstallError extends Error {
24
+ dependencyMissing;
25
+ constructor(dependencyMissing = false) {
26
+ super(dependencyMissing ? dependencyRepairHint() : PLAYWRIGHT_INSTALL_HINT);
27
+ this.dependencyMissing = dependencyMissing;
28
+ this.name = "BrowserInstallError";
29
+ }
30
+ }
31
+ export function isMissingPlaywrightError(error) {
32
+ const code = error?.code;
33
+ return (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") &&
34
+ /playwright/.test(error instanceof Error ? error.message : "");
35
+ }
36
+ export const PLAYWRIGHT_INSTALL_HINT = "Chromium is missing or could not be restored. It may have been removed after installation, " +
37
+ `or the install script may have been skipped. Run: ${BROWSER_REPAIR_COMMAND}`;
31
38
  /** True when an error is Playwright reporting that no browser binary exists. */
32
39
  export function isMissingBrowserError(error) {
33
40
  const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
34
41
  if (!message)
35
42
  return false;
36
- return /Executable doesn't exist|playwright install|Failed to launch.*because executable doesn't exist/i.test(message);
43
+ return /Executable doesn't exist|Failed to launch.*because executable doesn't exist/i.test(message);
37
44
  }
38
45
  const defaultProbe = {
39
46
  async launchHeadless() {
@@ -56,7 +63,10 @@ export async function chromiumStatus(overrides = {}) {
56
63
  await probe.launchHeadless();
57
64
  }
58
65
  catch (error) {
59
- if (isMissingBrowserError(error)) {
66
+ if (isMissingPlaywrightError(error)) {
67
+ headless = "dependency_missing";
68
+ }
69
+ else if (isMissingBrowserError(error)) {
60
70
  headless = "missing";
61
71
  }
62
72
  else {
@@ -75,7 +85,42 @@ export async function chromiumStatus(overrides = {}) {
75
85
  }
76
86
  /** True when sign-in has every browser it might need. */
77
87
  export function chromiumReady(status) {
78
- return status.headless !== "missing" && status.visible;
88
+ return status.headless === "present" && status.visible;
89
+ }
90
+ /** One repair and one retry; unrelated launch failures never download anything.
91
+ * Authentication already holds its process lock; Playwright's installer also
92
+ * locks the shared browser cache across installations and accounts.
93
+ */
94
+ export async function launchWithBrowserRecovery(launch, install = installChromium, progress = () => console.error("Restoring Chromium for Brightspace sign-in...")) {
95
+ try {
96
+ return await launch();
97
+ }
98
+ catch (error) {
99
+ if (isMissingPlaywrightError(error))
100
+ throw new BrowserInstallError(true);
101
+ if (!isMissingBrowserError(error))
102
+ throw error;
103
+ }
104
+ progress();
105
+ let installed;
106
+ try {
107
+ installed = await install();
108
+ }
109
+ catch {
110
+ throw new BrowserInstallError();
111
+ }
112
+ if (!installed.ok)
113
+ throw new BrowserInstallError();
114
+ try {
115
+ return await launch();
116
+ }
117
+ catch (error) {
118
+ if (isMissingPlaywrightError(error))
119
+ throw new BrowserInstallError(true);
120
+ if (isMissingBrowserError(error))
121
+ throw new BrowserInstallError();
122
+ throw error;
123
+ }
79
124
  }
80
125
  /** The CLI of the Playwright this package depends on, so the browser revision matches. */
81
126
  export function playwrightCliPath() {
@@ -99,7 +144,30 @@ export function installChromium(options = {}) {
99
144
  return Promise.resolve({ ok: false, message: error instanceof Error ? error.message : String(error) });
100
145
  }
101
146
  return new Promise((resolve) => {
102
- const child = spawnImpl(execPath, [cliPath, "install", "chromium"], { stdio: ["ignore", 2, 2] });
147
+ const child = spawnImpl(execPath, [cliPath, "install", "chromium"], {
148
+ stdio: ["ignore", 2, 2], detached: process.platform !== "win32",
149
+ });
150
+ const timer = setTimeout(() => {
151
+ // Include Playwright's download worker, so a timeout cannot leave an
152
+ // orphan downloading into the cache or holding its installation lock.
153
+ try {
154
+ if (child.pid && process.platform === "win32") {
155
+ execFileSync("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore", timeout: 5000 });
156
+ }
157
+ else if (child.pid) {
158
+ process.kill(-child.pid, "SIGKILL");
159
+ }
160
+ else
161
+ child.kill("SIGKILL");
162
+ }
163
+ catch {
164
+ child.kill("SIGKILL");
165
+ }
166
+ resolve({ ok: false, message: "Chromium installation timed out. Check your connection and run repair again." });
167
+ }, options.timeoutMs ?? 120_000);
168
+ timer.unref();
169
+ child.once("close", () => clearTimeout(timer));
170
+ child.once("error", () => clearTimeout(timer));
103
171
  child.once("error", (error) => resolve({ ok: false, message: error.message }));
104
172
  child.once("close", (code) => resolve(code === 0 ? { ok: true } : { ok: false, message: `Playwright's installer exited with code ${code}.` }));
105
173
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "5.1.7",
3
+ "version": "5.1.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",
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "files": [
14
14
  "build",
15
+ "scripts/install-browser.cjs",
15
16
  "README.md",
16
17
  "LICENSE"
17
18
  ],
@@ -24,7 +25,8 @@
24
25
  "dev": "tsc --watch",
25
26
  "test": "vitest",
26
27
  "test:run": "vitest run",
27
- "postinstall": "npx playwright install chromium"
28
+ "test:install": "node scripts/install-smoke.mjs",
29
+ "postinstall": "node scripts/install-browser.cjs"
28
30
  },
29
31
  "dependencies": {
30
32
  "@modelcontextprotocol/sdk": "^1.26.0",
@@ -0,0 +1,18 @@
1
+ // Works in both a source checkout (before build) and the published tarball.
2
+ // Resolve this package's dependency, never a Playwright from the caller's cwd.
3
+ const { spawnSync } = require("node:child_process");
4
+ const { dirname, join } = require("node:path");
5
+ const cli = join(dirname(require.resolve("playwright/package.json")), "cli.js");
6
+ const result = spawnSync(process.execPath, [cli, "install", "chromium"], {
7
+ stdio: "inherit",
8
+ });
9
+ if (result.error || result.status !== 0) {
10
+ console.error("Chromium installation failed. Retry with brightspace-mcp-server repair after installation completes.");
11
+ process.exit(1);
12
+ }
13
+ require("playwright").chromium.launch({ headless: true, timeout: 30_000 })
14
+ .then(browser => browser.close())
15
+ .catch(() => {
16
+ console.error("Chromium was downloaded but could not launch. Check operating system permissions and browser system dependencies.");
17
+ process.exitCode = 1;
18
+ });