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.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues