Skip to main content
Glama
nufelroknidev

Jupiter Controller MCP

README.md
# Jupiter Controller MCP (`jkb-mcp`)

[![CI](https://github.com/nufelroknidev/jupyter-kernel-bridge-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/nufelroknidev/jupyter-kernel-bridge-mcp/actions/workflows/ci.yml)
![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)
![Platform: Windows](https://img.shields.io/badge/platform-Windows-lightgrey)

Connects Claude Code to local Jupyter kernels as a set of native tools:
start/restart/shutdown kernels, run code against a live kernel (including one
backing a VS Code notebook), get stdout, results, errors and plots back, and
execute whole notebooks with outputs saved into the `.ipynb`. The client never
needs to know the kernel's connection details.

Built and tested with Claude Code on Windows. It is a standard MCP server
over stdio, so other MCP clients should work too, but they are untested.

> **Security warning:** this tool executes arbitrary code inside your live
> Jupyter kernel process, with that process's full OS privileges, exactly
> as if you typed it into the notebook yourself. Execution ships **disabled
> by default**; see "Enabling execution" below.
>
> The realistic threat is not just a misbehaving client. It is **untrusted
> data flowing through a trusted client** (prompt injection): a notebook,
> CSV, or web page containing adversarial text can steer the model into
> running exfiltration or destructive code. Mitigations built in: the
> tool-based opt-in is **session-scoped** (dies with the server process;
> persistent enablement requires you to edit the config file yourself), and
> a `disable_execution` tool can always turn it off. Still: be deliberate
> about which files and data you let the model read while execution is
> enabled.

## Requirements

- Windows
- Python 3.11+
- Optional: VS Code with an active Jupyter kernel, if you want to attach to
  an existing notebook session. `start_kernel` can otherwise run everything
  standalone.

## Install

```
pip install git+https://github.com/nufelroknidev/jupyter-kernel-bridge-mcp.git
```

Or from a local clone:

```
git clone https://github.com/nufelroknidev/jupyter-kernel-bridge-mcp.git
pip install ./jupyter-kernel-bridge-mcp
```

Verify it installed correctly:

```
jkb-mcp --help
```

If `jkb-mcp` isn't found, your Python Scripts directory isn't on PATH. Use
the full path shown by `pip show -f jkb-mcp`, or activate the venv you
installed into.

## Enabling execution

The server ships with code execution **off**. `list_kernels` and `get_config`
always work; every tool that executes code or mutates kernel state refuses
until you opt in, one of two ways:

1. **Via the client (session-scoped)**: call the `enable_execution` tool
   with `confirm="I understand the risk"`. This enables execution **only
   until the MCP server process restarts**. It is deliberately never
   persisted, so the model (or injected instructions in data it read) can't
   quietly enable execution forever.
2. **Manually (persistent)**: edit `%APPDATA%\jkb-mcp\config.json` and set
   `"execution_enabled": true`, then restart the MCP server (config is
   cached at startup).

`disable_execution` turns it off again at any time. It clears the session
flag *and* the config-file flag.

## Register with Claude Code

```
claude mcp add jkb-mcp -- jkb-mcp
```

Add `-s user` to make it available in every project on this machine, not just
the current one:

```
claude mcp add jkb-mcp -s user -- jkb-mcp
```

No absolute paths are involved; `jkb-mcp` resolves through whatever Python
environment you installed the package into.

## Teaching Claude Code about this server

The server ships its own usage guide as MCP *server instructions* (the
recommended workflow, kernel-addressing semantics, and security rules), which
Claude Code surfaces to the model automatically, so no context-file setup is
required. If you want the model to reach for these tools more proactively,
add a note like this to your project's `CLAUDE.md`:

> All Jupyter-notebook work goes through the `jkb-mcp` MCP server: enable
> execution once per session (`enable_execution`), start or attach to a
> kernel (`start_kernel` / `list_kernels`), iterate with `run_python`, and
> persist results into the `.ipynb` with `execute_notebook`. Do not run
> notebook code through plain `python` subprocesses.

The "Tool reference" table below covers every tool and its parameters.

## Tool reference

| Tool | Gated? | Description |
|---|---|---|
| `list_kernels()` | No | Lists detected kernel connection files with age and status: `alive`, `busy` (executing), or `dead` (stale file). |
| `get_config()` | No | Shows current settings (execution enabled?, output limits, retention). |
| `enable_execution(confirm)` | - | Enables execution **for this session only** (resets on server restart). Requires `confirm="I understand the risk"`. |
| `disable_execution()` | - | Disables execution: clears the session opt-in and the config-file flag. |
| `run_python(code, kernel=0, timeout=60)` | Yes | Executes code in a live kernel; returns text output and inline images. |
| `run_file(path, kernel=0, timeout=60)` | Yes | Reads a local `.py` file and executes it the same way. |
| `interrupt_kernel(kernel=0, force=False)` | Yes | Stops a stuck/runaway cell and confirms whether the interrupt landed. `force=True` hard-restarts a server-owned kernel if it doesn't (blocking C calls ignore Windows interrupts); state is lost. |
| `start_kernel(kernel_name="python3", cwd="")` | Yes | Starts a fresh kernel owned by this server, no VS Code needed. Supports real restart/shutdown. |
| `restart_kernel(kernel=0)` | Yes | Real process restart for server-owned kernels; best-effort protocol restart request for external (VS Code) kernels. |
| `shutdown_kernel(kernel=0)` | Yes | Shuts down a server-owned kernel. Refuses kernels owned by other apps. |
| `execute_notebook(path, kernel=0, timeout_per_cell=120, stop_on_error=True, cells=None)` | Yes | Runs a `.ipynb`'s code cells and **saves outputs (incl. embedded plot images) into the file**, like nbconvert `--execute`; VS Code shows them on reload. |
| `list_variables(kernel=0)` | Yes | Lists user variables in the kernel namespace with type, shape/length, size. |
| `get_variable(name, kernel=0, max_chars=4000)` | Yes | Deep preview of one variable: type, shape/dtypes, DataFrame columns + head(), capped repr. |
| `list_kernelspecs()` | No | Lists installed kernelspecs usable with `start_kernel(kernel_name=...)`. |
| `list_notebook_cells(path)` | No | Shows cell indices/types/first lines + saved-output status for `execute_notebook(cells=[...])`. |

**`kernel` parameter**: `0` (the default) auto-selects the most recent kernel
that responds to a health check, silently falling back to the next-most-recent
if it's unresponsive. Any other explicit index is honored exactly: it fails
with a clear message rather than silently substituting a different kernel.
Because indices are positions in the newest-first list, they **shift when new
kernels appear**; to target a specific kernel reliably across calls, pass its
connection-file name (or a unique substring of it) from `list_kernels`
instead, e.g. `kernel="kernel-jkb-1a2b3c.json"`.

## Troubleshooting

| Message | Cause | Fix |
|---|---|---|
| "No Jupyter kernel connection files found..." | No `.ipynb` has ever run a cell | Open a notebook in VS Code and run a cell. |
| "Found N kernel connection file(s) but none responded..." | The kernel process(es) behind those files are closed | Restart the notebook's kernel (run a cell again). |
| "Kernel index N ... did not respond to a health check" | You asked for a *specific* kernel index and it's dead | Use `list_kernels` to find a live index, or omit `kernel` to auto-select. |
| "Kernel N is currently busy running another request..." | Two calls hit the same kernel at once | Wait for the first to finish, or use a different kernel index. |
| "... is malformed or incomplete..." | A connection file was partially written or corrupted | It's skipped automatically; this is informational, not fatal. |
| "Execution exceeded the timeout..." | Your cell ran past the `timeout` you passed | Partial output (if any) is included. The kernel was interrupted; on Windows this can take a few seconds to land. |
| "Code execution is disabled..." | The safety gate is off (the default) | See "Enabling execution" above. |

## Data locations

| What | Where |
|---|---|
| Config (execution gate, limits) | `%APPDATA%\jkb-mcp\config.json` |
| Logs | `%LOCALAPPDATA%\jkb-mcp\logs\server.log` (rotates at 5MB, 5 backups) |
| Captured images | `%LOCALAPPDATA%\jkb-mcp\outputs\` (auto-cleaned: 7-day retention, 500MB cap by default, both configurable in `config.json`) |

Nothing is ever logged above DEBUG level for the actual code/output content,
since kernel output can contain secrets. Only short previews and outcomes
are logged at INFO.

## Uninstall

```
pip uninstall jkb-mcp
claude mcp remove jkb-mcp
```

Optionally also delete `%APPDATA%\jkb-mcp` and `%LOCALAPPDATA%\jkb-mcp`.

## License

**PolyForm Noncommercial 1.0.0**: free for personal, research, educational,
and other noncommercial use. **Commercial use requires a separate paid
license** from the copyright holder: nufel.rokni.dev@gmail.com. See `LICENSE`
for full terms.