@fre4x/jupyter 1.0.46 → 1.0.49

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.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/dist/index.js +65 -14
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -8,7 +8,7 @@ Jupyter Notebook MCP server for reading, writing, executing code in `.ipynb` fil
8
8
  - **Execute**: Run code cells in a real Jupyter kernel over the Jupyter WebSocket protocol.
9
9
  - **Control**: Launch or connect to a local Jupyter server, list kernels, and open notebooks in the browser.
10
10
  - **Mock Mode**: Development without a real Jupyter server using `MOCK=true`.
11
- - **Auto Start**: In non-mock mode the MCP process starts or connects to Jupyter before accepting requests.
11
+ - **Auto Start**: In non-mock mode, the Jupyter server starts during MCP server startup.
12
12
 
13
13
  ## Tools
14
14
 
@@ -55,7 +55,8 @@ MOCK=true npx @fre4x/jupyter
55
55
  - `MOCK=true` keeps the old fixture-only behavior.
56
56
  - Without `MOCK=true`, the server first tries `JUPYTER_SERVER_URL` if provided.
57
57
  - Otherwise it launches a local Jupyter process (`jupyter lab`, then notebook fallbacks) with `--no-browser`.
58
- - After the server is ready, it opens the Jupyter UI in your default browser.
58
+ - The Jupyter runtime is initialized before the MCP server finishes startup, so `list_tools` and later tool calls share the same ready runtime.
59
+ - After the server is ready, it opens the Jupyter UI in your default browser (if `JUPYTER_AUTO_OPEN` is not `false`).
59
60
  - If `JUPYTER_START_NOTEBOOK_PATH` is set, that notebook is opened directly; otherwise the root Jupyter UI is opened.
60
61
 
61
62
  ## Environment Variables
@@ -63,6 +64,6 @@ MOCK=true npx @fre4x/jupyter
63
64
  - `JUPYTER_SERVER_URL`: Use an already-running Jupyter server instead of launching one.
64
65
  - `JUPYTER_TOKEN`: Optional token for an external Jupyter server.
65
66
  - `JUPYTER_ROOT_DIR`: Root directory for managed Jupyter startup. Defaults to the current working directory.
66
- - `JUPYTER_AUTO_OPEN`: Set to `false` to skip browser launch on MCP startup.
67
+ - `JUPYTER_AUTO_OPEN`: Set to `false` to skip browser launch when the server starts.
67
68
  - `JUPYTER_START_NOTEBOOK_PATH`: Notebook to open automatically on startup.
68
69
  - `JUPYTER_KERNEL_NAME`: Kernel name used when code execution needs to create a kernel. Defaults to `python3`.
package/dist/index.js CHANGED
@@ -49277,6 +49277,10 @@ var STARTUP_TIMEOUT_MS = Number.parseInt(
49277
49277
  process.env.JUPYTER_STARTUP_TIMEOUT_MS || "30000",
49278
49278
  10
49279
49279
  );
49280
+ var STARTUP_LOG_TAIL_MAX_CHARS = Number.parseInt(
49281
+ process.env.JUPYTER_STARTUP_LOG_TAIL_MAX_CHARS || "16384",
49282
+ 10
49283
+ );
49280
49284
  var EXECUTION_TIMEOUT_MS = Number.parseInt(
49281
49285
  process.env.JUPYTER_EXECUTION_TIMEOUT_MS || "30000",
49282
49286
  10
@@ -49351,6 +49355,13 @@ function parseServerUrlFromOutput(output) {
49351
49355
  }
49352
49356
  return void 0;
49353
49357
  }
49358
+ function appendOutputTail(current, chunk, maxChars = STARTUP_LOG_TAIL_MAX_CHARS) {
49359
+ if (maxChars <= 0) {
49360
+ return "";
49361
+ }
49362
+ const next = current + chunk;
49363
+ return next.length > maxChars ? next.slice(-maxChars) : next;
49364
+ }
49354
49365
  function buildLaunchCandidates(rootDir) {
49355
49366
  return [
49356
49367
  {
@@ -49425,11 +49436,17 @@ async function launchManagedServer() {
49425
49436
  });
49426
49437
  let output = "";
49427
49438
  let settled = false;
49439
+ let readinessCheckInFlight = false;
49440
+ const cleanupListeners = () => {
49441
+ child.stdout?.off("data", handleChunk);
49442
+ child.stderr?.off("data", handleChunk);
49443
+ };
49428
49444
  const timeout = setTimeout(() => {
49429
49445
  if (settled) {
49430
49446
  return;
49431
49447
  }
49432
49448
  settled = true;
49449
+ cleanupListeners();
49433
49450
  child.kill("SIGTERM");
49434
49451
  reject(
49435
49452
  new Error(
@@ -49444,15 +49461,21 @@ ${output.trim()}`
49444
49461
  }
49445
49462
  settled = true;
49446
49463
  clearTimeout(timeout);
49464
+ cleanupListeners();
49447
49465
  reject(error48);
49448
49466
  };
49449
49467
  const maybeResolve = async () => {
49468
+ if (settled || readinessCheckInFlight) {
49469
+ return;
49470
+ }
49450
49471
  const parsed = parseServerUrlFromOutput(output);
49451
- if (!parsed || settled) {
49472
+ if (!parsed) {
49452
49473
  return;
49453
49474
  }
49475
+ readinessCheckInFlight = true;
49454
49476
  settled = true;
49455
49477
  clearTimeout(timeout);
49478
+ cleanupListeners();
49456
49479
  const runtime2 = {
49457
49480
  ...parsed,
49458
49481
  frontend: candidate.frontend,
@@ -49469,10 +49492,18 @@ ${output.trim()}`
49469
49492
  } catch (error48) {
49470
49493
  child.kill("SIGTERM");
49471
49494
  reject(error48);
49495
+ } finally {
49496
+ readinessCheckInFlight = false;
49472
49497
  }
49473
49498
  };
49474
49499
  const handleChunk = (chunk) => {
49475
- output += chunk.toString("utf8");
49500
+ if (settled) {
49501
+ return;
49502
+ }
49503
+ output = appendOutputTail(
49504
+ output,
49505
+ chunk.toString("utf8")
49506
+ );
49476
49507
  void maybeResolve();
49477
49508
  };
49478
49509
  child.stdout.on("data", handleChunk);
@@ -49500,9 +49531,16 @@ ${output.trim()}`
49500
49531
  failures.push(`${candidate.label}: ${message}`);
49501
49532
  }
49502
49533
  }
49503
- throw new Error(
49504
- `Unable to start a local Jupyter server. Tried: ${failures.join(" | ")}`
49505
- );
49534
+ const allMissing = failures.every((f) => f.includes("ENOENT"));
49535
+ const combinedMessage = `Unable to start a local Jupyter server. Tried: ${failures.join(" | ")}`;
49536
+ if (allMissing) {
49537
+ throw new Error(
49538
+ `${combinedMessage}
49539
+
49540
+ [Hint] Jupyter seems to be missing. Please install it with: pip install jupyterlab`
49541
+ );
49542
+ }
49543
+ throw new Error(combinedMessage);
49506
49544
  }
49507
49545
  async function connectToExternalServer() {
49508
49546
  const configuredUrl = process.env.JUPYTER_SERVER_URL?.trim();
@@ -49526,7 +49564,10 @@ async function connectToExternalServer() {
49526
49564
  async function ensureJupyterRuntime(options) {
49527
49565
  if (runtimeState.runtime) {
49528
49566
  if (options?.openBrowser && !runtimeState.startupBrowserOpened) {
49529
- await openJupyterInBrowser(options.notebookPath);
49567
+ await openRuntimeInBrowser(
49568
+ runtimeState.runtime,
49569
+ options.notebookPath
49570
+ );
49530
49571
  runtimeState.startupBrowserOpened = true;
49531
49572
  }
49532
49573
  return runtimeState.runtime;
@@ -49542,8 +49583,11 @@ async function ensureJupyterRuntime(options) {
49542
49583
  }
49543
49584
  const runtime = await runtimeState.startupPromise;
49544
49585
  runtimeState.startupPromise = void 0;
49586
+ console.error(
49587
+ `[jupyter] Connected to ${runtime.source} Jupyter server at ${runtime.baseUrl}`
49588
+ );
49545
49589
  if (options?.openBrowser && !runtimeState.startupBrowserOpened) {
49546
- await openJupyterInBrowser(options.notebookPath);
49590
+ await openRuntimeInBrowser(runtime, options.notebookPath);
49547
49591
  runtimeState.startupBrowserOpened = true;
49548
49592
  }
49549
49593
  return runtime;
@@ -49584,12 +49628,15 @@ function buildJupyterOpenUrl(runtime, notebookPath) {
49584
49628
  );
49585
49629
  return appendToken(url3, runtime.token);
49586
49630
  }
49587
- async function openJupyterInBrowser(notebookPath, overrideUrl) {
49588
- const runtime = await ensureJupyterRuntime();
49631
+ async function openRuntimeInBrowser(runtime, notebookPath, overrideUrl) {
49589
49632
  const target = overrideUrl || buildJupyterOpenUrl(runtime, notebookPath);
49590
49633
  await openTarget(target);
49591
49634
  return target;
49592
49635
  }
49636
+ async function openJupyterInBrowser(notebookPath, overrideUrl) {
49637
+ const runtime = await ensureJupyterRuntime();
49638
+ return openRuntimeInBrowser(runtime, notebookPath, overrideUrl);
49639
+ }
49593
49640
  async function listJupyterKernels() {
49594
49641
  const runtime = await ensureJupyterRuntime();
49595
49642
  const response = await createHttpClient(runtime).get("/api/kernels");
@@ -49798,7 +49845,14 @@ async function executeCode(code, requestedKernelId) {
49798
49845
 
49799
49846
  // src/api.ts
49800
49847
  function createApiError(message, statusCode) {
49801
- const hint = statusCode === 404 ? "The requested resource was not found." : "Check your inputs and retry.";
49848
+ let hint = statusCode === 404 ? "The requested resource was not found." : "Check your inputs and retry.";
49849
+ if (message.includes("Unable to start a local Jupyter server")) {
49850
+ hint = "Jupyter is not installed or not in your PATH. Please install it (e.g., pip install jupyterlab).";
49851
+ } else if (message.includes("Timed out waiting for Jupyter server")) {
49852
+ hint = "The Jupyter server started but is not responding. Check your firewall or resource usage.";
49853
+ } else if (message.includes("ECONNREFUSED")) {
49854
+ hint = "Connection refused. Ensure the Jupyter server is running and accessible.";
49855
+ }
49802
49856
  const detail = statusCode ? ` (HTTP ${statusCode})` : "";
49803
49857
  return {
49804
49858
  isError: true,
@@ -50099,13 +50153,10 @@ async function main() {
50099
50153
  "[jupyter] Running in MOCK mode \u2014 no real local actions or API calls."
50100
50154
  );
50101
50155
  } else {
50102
- const runtime = await ensureJupyterRuntime({
50156
+ await ensureJupyterRuntime({
50103
50157
  openBrowser: shouldAutoOpenOnStartup(),
50104
50158
  notebookPath: getStartupNotebookPath()
50105
50159
  });
50106
- console.error(
50107
- `[jupyter] Connected to ${runtime.source} Jupyter server at ${runtime.baseUrl}`
50108
- );
50109
50160
  }
50110
50161
  const transport = new StdioServerTransport();
50111
50162
  await server.connect(transport);
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@fre4x/jupyter",
3
- "version": "1.0.46",
3
+ "version": "1.0.49",
4
4
  "description": "Jupyter Notebook MCP server",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
- "jupyter": "dist/index.js"
7
+ "fre4x-jupyter": "dist/index.js"
8
8
  },
9
9
  "files": [
10
10
  "dist"
@@ -17,7 +17,7 @@
17
17
  "prepublishOnly": "npm run typecheck && npm run build",
18
18
  "test": "vitest run --exclude dist",
19
19
  "test:watch": "vitest",
20
- "inspector": "cross-env MOCK=true npx @modelcontextprotocol/inspector node dist/index.js"
20
+ "inspector": "node ../scripts/run-official-inspector.mjs node dist/index.js"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^25.3.5",