# Jupyter MCP Server A focused [Model Context Protocol](https://modelcontextprotocol.io/) server for safely reading, editing, and executing one live Jupyter notebook. The implementation deliberately stays small: one notebook, one kernel, and the standard `stdio` transport. Operations share a session lock, code is appended and executed exactly once, and the server waits for the kernel's final execution reply—not the first visible output. ## What it exposes | MCP primitive | Name | Purpose | | --- | --- | --- | | Tool | `add_markdown_cell` | Append and verify one markdown cell | | Tool | `add_execute_code_cell` | Append once, execute once, and return structured outputs | | Tool | `read_notebook_content` | Read a consistent notebook snapshot | | Tool | `kernel_restart` | Replace the current kernel/session cleanly | | Resource | `jupyter://notebook` | Read-only notebook JSON | | Prompt | `analyse_notebook` | Analyze before changing the notebook | HTML, image, JSON, stream, result, and traceback data are retained in structured output models. Execution failures, timeouts, and unknown outcomes are MCP errors rather than successful strings. ## Requirements - Python 3.10 or newer (3.12 recommended) - A JupyterLab server with real-time collaboration enabled - [`uv`](https://docs.astral.sh/uv/) (recommended) or `pip` ## Local setup Install the server and development tools: ```bash uv sync --extra dev ``` Create a valid notebook document (an empty file is not a notebook): ```bash uv run python -c "import nbformat; nbformat.write(nbformat.v4.new_notebook(), 'notebook.ipynb')" ``` In another environment or terminal, install and start Jupyter: ```bash uv run --with jupyterlab --with jupyter-collaboration \ jupyter lab \ --port 8888 \ --ServerApp.port_retries 0 \ --ServerApp.root_dir . \ --IdentityProvider.token MY_TOKEN \ --no-browser ``` Run the MCP server: ```bash NOTEBOOK_PATH=notebook.ipynb \ SERVER_URL=http://localhost:8888 \ TOKEN=MY_TOKEN \ uv run jupyter-mcp ``` The server opens Jupyter connections on first use, so importing the package or listing MCP tools does not require Jupyter to be running. ## MCP client configuration Use the following shape for clients that launch local `stdio` servers, replacing the repository path with your checkout: ```json { "mcpServers": { "jupyter": { "command": "uv", "args": [ "--directory", "/absolute/path/to/jupyter_mcp_server_kshitij", "run", "jupyter-mcp" ], "env": { "NOTEBOOK_PATH": "notebook.ipynb", "SERVER_URL": "http://localhost:8888", "TOKEN": "MY_TOKEN" } } } } ``` ## Configuration | Variable | Default | Description | | --- | --- | --- | | `NOTEBOOK_PATH` | `notebook.ipynb` | Path relative to the Jupyter server root | | `SERVER_URL` | `http://localhost:8888` | Jupyter server URL | | `TOKEN` | empty | Jupyter authentication token; set it at runtime | | `CONNECTION_TIMEOUT_SECONDS` | `15` | Timeout for startup, cleanup, and interruption | | `EXECUTION_TIMEOUT_SECONDS` | `30` | Maximum kernel execution time | | `CONNECTION_ATTEMPTS` | `2` | Pre-mutation connection attempts | Connection retries happen only before a mutation. Once execution starts, a disconnect produces an explicit “outcome unknown” error and the cell is never automatically repeated. ## Docker Build and run the non-root image. The token has no image default and must be supplied at runtime: ```bash docker build -f dockerfile -t jupyter-mcp . docker run --rm -i \ -e NOTEBOOK_PATH=notebook.ipynb \ -e SERVER_URL=http://host.docker.internal:8888 \ -e TOKEN=MY_TOKEN \ jupyter-mcp ``` ## Development ```bash uv run pytest uv run ruff check . ``` The unit tests use fake notebook and kernel clients; they do not require a live Jupyter server. For an end-to-end check, start Jupyter as shown above and exercise the tools with an MCP client or the MCP Inspector. ## Safety and lifecycle guarantees - Importing modules performs no network I/O. - MCP lifespan cleanup closes both notebook and kernel clients on normal or exceptional shutdown. - An `asyncio.Lock` prevents concurrent notebook mutation, execution, reads, and restarts. - The notebook client's public sequence/export API is used; private `_doc` and `_ycells` internals are not accessed. - Kernel errors and timeouts become MCP errors. A timed-out kernel is interrupted, and an uncertain session is discarded before reuse. - Only standard `stdio` transport is enabled; there is no silent raw-TCP fallback.