@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.
- package/README.md +4 -3
- package/dist/index.js +65 -14
- 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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
49504
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
20
|
+
"inspector": "node ../scripts/run-official-inspector.mjs node dist/index.js"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^25.3.5",
|