Skip to main content
Glama
TheManchineel

Jupyter Terminal MCP

README.md
# Jupyter Terminal MCP

This MCP server lets an agent run commands through a JupyterLab/JupyterHub-hosted terminal when SSH is not available. Originally designed for man-in-the-loop debugging of NPU kernels on AMD's AUP Learning Cloud, it talks to a Jupyter Server terminal REST API and websocket endpoint using a Selenium WebDriver to keep a valid session and perform the requests. You need access to the JupyterLab machine from the system hosting the MCP server. You can also use this server without a browser, if you have a valid Jupyter token and the server is not behind Cloudflare or other login challenges.

> [!WARNING]
> You must have permission from the system administrator to use the JupyterLab environment for research/development. Do not use this software to break rules or bypass security measures. The author is not responsible for any misuse of this software. Not affiliated with Jupyter or any other org. **Always verify AI-generated commands before accepting and executing them.**

## Install

```bash
UV_CACHE_DIR=.uv-cache uv sync
```

## Configure

Keep your token in environment variables or your agent's MCP config JSON.

```bash
export JUPYTER_URL="https://myjupyterserver.example.com/user/my-user/lab"
export JUPYTER_TOKEN="<your Jupyter token>" # only supported in non-browser mode
export JUPYTER_USERNAME="<your Jupyter username>"
```

Optional settings:

```bash
export JUPYTER_CONNECT_TIMEOUT="10"
export JUPYTER_VERIFY_TLS="true"
export JUPYTER_WEBSOCKET_RETRIES="3"
export JUPYTER_WEBSOCKET_RETRY_DELAY="1"
```

`JUPYTER_URL` may point at `/lab`; the server automatically converts it to the Jupyter API base URL.
The server opens the Lab URL, follows the Hub login/OAuth flow, and submits `JUPYTER_USERNAME` plus `JUPYTER_TOKEN` as the login credentials. If `JUPYTER_USERNAME` is omitted, it is derived from `/user/<name>/` in `JUPYTER_URL`.

Websocket connection failures with transient statuses such as `503 Service Unavailable` are retried. Before each retry, the server refreshes the JupyterHub session cookies.

In my experience, custom terminal names can sometimes remain listed after their websocket route has gone stale. Leaving `JUPYTER_TERMINAL_NAME` unset is usually more robust: the server reuses existing numeric terminals first, starting from the lowest name (`1`, then `2`, and so on). If a selected terminal returns a 502/503/504 websocket handshake, the server tries the next existing numeric terminal before creating a fresh unnamed terminal.

Browser-backed mode does not require `JUPYTER_TOKEN`; the logged-in browser session supplies the cookies. Useful browser/socket settings:

```bash
export JUPYTER_MCP_SOCKET_HOST="127.0.0.1"
export JUPYTER_MCP_SOCKET_PORT="8765"
export CHROMEDRIVER="/path/to/chromedriver"                  # optional if chromedriver is already on PATH
export JUPYTER_CHROME_BINARY="/path/to/Google Chrome"         # optional
export JUPYTER_CHROME_USER_DATA_DIR="/path/to/user-data-dir"  # optional; reuse a Chrome user data dir
export JUPYTER_CHROME_PROFILE_DIRECTORY="Default"             # optional; profile inside the user data dir
export JUPYTER_BROWSER_KEEP_OPEN="true"                       # optional; leave Chrome open when the daemon exits
```

## Run

For a quick local config check:

```bash
UV_CACHE_DIR=.uv-cache uv run jupyter-terminal-mcp --check-config
```

For browser-backed mode, run the browser daemon in a regular terminal first:

```bash
export JUPYTER_URL="https://myjupyterserver.example.com/user/my-user/lab"
UV_CACHE_DIR=.uv-cache uv run jupyter-terminal-mcp --browser-server
```

Chrome opens. Complete any login procedure, Cloudflare challenge etc. manually, wait until JupyterLab is ready, then press Enter in the daemon terminal. The daemon listens for MCP JSON-RPC messages on `127.0.0.1:8765` by default.

Then configure MCP clients to launch the stdio socket proxy:

```bash
UV_CACHE_DIR=.uv-cache uv run jupyter-terminal-mcp --socket-client
```

Example browser-backed MCP configuration:

```json
{
  "mcpServers": {
    "jupyter-terminal": {
      "command": "uv",
      "args": [
        "--directory",
        "~/Projects/jupyter-terminal-mcp",
        "run",
        "jupyter-terminal-mcp",
        "--socket-client"
      ],
      "env": {
        "UV_CACHE_DIR": "~/Projects/jupyter-terminal-mcp/.uv-cache",
        "JUPYTER_MCP_SOCKET_HOST": "127.0.0.1",
        "JUPYTER_MCP_SOCKET_PORT": "8765"
      }
    }
  }
}
```

In non-browser-backed mode, clients should launch:

```bash
UV_CACHE_DIR=.uv-cache uv run jupyter-terminal-mcp
```

Example non-browser-backed MCP configuration:

```json
{
  "mcpServers": {
    "jupyter-terminal": {
      "command": "uv",
      "args": [
        "--directory",
        "~/Projects/jupyter-terminal-mcp",
        "run",
        "jupyter-terminal-mcp"
      ],
      "env": {
        "UV_CACHE_DIR": "~/Projects/jupyter-terminal-mcp/.uv-cache",
        "JUPYTER_URL": "https://myjupyterserver.example.com/user/my-user/lab",
        "JUPYTER_TOKEN": "<your Jupyter token>",
        "JUPYTER_USERNAME": "my-user"
      }
    }
  }
}
```


## Tools

Your coding agent can then use the following tools:

| Tool | Description |
| --- | --- |
| `jupyter_terminal_run` | Run a shell command and return JSON with `output`, `exit_code`, `terminal_name`, `timed_out`, and elapsed time. |
| `jupyter_terminal_write` | Send raw text to the terminal for interactive prompts. |
| `jupyter_terminal_list` | List Jupyter terminals visible to the server. |


`jupyter_terminal_run` wraps each command with unique begin/end markers so the agent receives only the command's output and exit status. If a command times out, the server sends `^C` to the terminal and returns the output collected so far.

> [!WARNING]
> Make sure to gate access to `jupyter_terminal_run` and `jupyter_terminal_write` appropriately. These tools can be used to run arbitrary shell commands on the JupyterLab host, which is a security risk.