Jupyter Interpreter MCP
A remote Jupyter-based MCP (Model Context Protocol) server for code interpretation. This server connects to a remote Jupyter server (e.g. running in a Docker container or cloud instance) and provides a persistent, sandboxed code execution environment similar to Jupyter notebooks. Supports both Python and bash command execution.
Architecture
MCP Server → RemoteJupyterClient → Jupyter REST API → Remote Kernel
↓
WebSocket Connection
↓
Jupyter server Filesystem
All code executes within the remote Jupyter server. Session history files are stored in the server's filesystem, not on the host. You can execute both Python code and bash commands (e.g., ls, pwd, cat file.txt). Requirements
Requirements
- Python 3.10 or higher
- uv package manager
- Network access to a Jupyter server
Quick Start
1. (Optional) Start Jupyter Container
This is only necessary if you don't use any other remote instance of Jupyter. Run a Jupyter container with the required port mappings, e.g.:
docker run -d \
--name jupyter-notebook \
-p 8889:8888 \
jupyter/minimal-notebook:latest
2. Get Authentication Token
Create a new token for accessing the Jupyter server or use an existing token.
3. Run the MCP server
Using uvx
Start the server using uvx:
uvx jupyter-interpreter-mcp --jupyter-base-url http://localhost:8889 --jupyter-token abc123def456... --sessions-dir /home/jovyan/sessions --session-ttl 3600
or to add it to e.g. Claude Code:
{
"mcpServers": {
"jupyter-interpreter-mcp": {
"command": "uvx",
"args": [
"jupyter-interpreter-mcp",
"--jupyter-base-url",
"http://localhost:8889",
"--jupyter-token",
"abc123def456...",
"--sessions-dir",
"/home/jovyan/sessions",
"--session-ttl",
"3600",
"--restore-sessions-on-startup"
]
}
}
}
From source
Create a .env file in the project root:
JUPYTER_BASE_URL=http://localhost:8889
JUPYTER_TOKEN=abc123def456...
SESSIONS_DIR=/home/jovyan/sessions
SESSION_TTL=3600 # Optional: session expiry in seconds (0 = never expire)
RESTORE_SESSIONS_ON_STARTUP=false # Optional: eager restore all sessions on startup
See .env.example for full configuration options and Docker setup instructions.
You can then install and run the server using uv:
uv pip install .
uv run jupyter-interpreter-mcp
The server will validate the connection to Jupyter on startup and fail with a clear error message if the connection cannot be established.
Session-Based Workflow
The server uses a session-based architecture where each session has:
- A unique UUID-based session ID
- An isolated directory on the Jupyter server at
{sessions-dir}/{session-id}/ - A persistent Jupyter kernel that maintains execution state (variables, imports)
- On-demand restoration on access after restart (with optional eager restore at startup)
Typical Workflow
- Create a session using
create_session - Execute code in the session using
execute_code - Upload/download files within the session directory using
upload_file_pathanddownload_file - List files in the session directory using
list_dir - Read/write/edit text files directly using
read_file,write_file, andedit_file
Sessions automatically expire after the configured TTL (time-to-live) period.
Tools
create_session
Creates a new isolated session with a dedicated directory and Jupyter kernel.
Parameters: None
Returns: A dictionary containing:
session_id(string): UUID identifier for the session
Example usage:
result = create_session()
# Returns: {
# "session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
# }
execute_code
Executes code (Python or bash) within a persistent session, retaining past results (e.g., variables, imports). Similar to a Jupyter notebook.
Parameters:
code(string, required): The code to execute (Python or bash commands)session_id(string, required): The session ID fromcreate_session
Returns: A dictionary containing:
result(list of strings): Output from the code executionerror(list of strings): Any errors that occurred during executionsession_id(string): The session ID used
Example usage:
# Create a session first
session = create_session()
session_id = session["session_id"]
# Execute code in the session
result = execute_code(code="x = 42\nprint(x)", session_id=session_id)
# Returns: {"result": ["42"], "error": [], "session_id": "a1b2c3d4-..."}
# Subsequent execution - reuses the session state
result = execute_code(code="print(x * 2)", session_id=session_id)
# Returns: {"result": ["84"], "error": [], "session_id": "a1b2c3d4-..."}
# Bash commands
result = execute_code(code="ls -la", session_id=session_id)
download_file
Download a file from the session directory.
Parameters:
session_id(string, required): The session IDpath(string, required): Relative path within session directory
Returns: A dictionary containing:
content(string): File content (base64-encoded for binary, plain text otherwise)encoding(string): Either"base64"or"text"filename(string): The basename of the downloaded file
Example usage:
# Download text file
result = download_file(session_id=session_id, path="script.py")
# Returns: {"content": "print('Hello, World!')", "encoding": "text", "filename": "script.py"}
# Download binary file
result = download_file(session_id=session_id, path="images/logo.png")
# Returns: {"content": "iVBORw0KGgo...", "encoding": "base64", "filename": "logo.png"}
list_dir
List files and directories within the session directory.
Parameters:
session_id(string, required): The session IDpath(string, optional): Subdirectory path within session (defaults to session root)
Returns: A dictionary containing:
error(string): Empty string on success, error message on failureresult(list of strings): Formatted file/directory listing
Example usage:
# List session root
result = list_dir(session_id=session_id)
# List subdirectory
result = list_dir(session_id=session_id, path="images")
read_file
Reads a text file from the session directory and returns its content with 1-indexed line numbers. Use offset and limit to page through large files. Binary files are not supported; use download_file for those.
Parameters:
session_id(string, required): The session IDpath(string, required): File path relative to the session directoryoffset(integer, optional): 1-indexed line number to start reading from (default:1)limit(integer, optional): Maximum number of lines to return (default:200)
Returns: A dictionary containing:
lines(list of strings): Lines prefixed with their line number (e.g."42: content")total_lines(integer): Total number of lines in the fileoffset(integer): The offset that was usedlimit(integer): The limit that was usedtruncated(boolean): Whether the result was truncatedpath(string): The file path that was read
Example usage:
# Read first 200 lines
result = read_file(session_id=session_id, path="script.py")
# Returns: {"lines": ["1: import os", "2: ..."], "total_lines": 42, "offset": 1, "limit": 200, "truncated": False, "path": "script.py"}
# Page through a large file
result = read_file(session_id=session_id, path="data.csv", offset=201, limit=200)
write_file
Writes text content to a file in the session directory, creating or overwriting it. Parent directories are created automatically. Use upload_file_path for binary files.
Parameters:
session_id(string, required): The session IDpath(string, required): Destination file path relative to the session directorycontent(string, required): Text content to write
Returns: A dictionary containing:
status(string):"ok"on successpath(string): The file path that was writtenbytes_written(integer): Number of bytes written
Example usage:
result = write_file(
session_id=session_id,
path="scripts/hello.py",
content="print('Hello, World!')\n"
)
# Returns: {"status": "ok", "path": "scripts/hello.py", "bytes_written": 23}
edit_file
Edits a text file in the session directory by replacing an exact substring (old_string) with new_string. Three matching strategies are tried in order: exact, line-trimmed, and indentation-flexible. By default the match must be unique; set replace_all=True to replace every occurrence. Use read_file first to confirm the exact content.
Parameters:
session_id(string, required): The session IDpath(string, required): File path relative to the session directoryold_string(string, required): The text to find and replace (must be non-empty)new_string(string, required): The replacement textreplace_all(boolean, optional): Replace all occurrences instead of requiring a unique match (default:false)
Returns: A dictionary containing:
status(string):"ok"on successpath(string): The file path that was editedreplacements(integer): Number of replacements made
Example usage:
result = edit_file(
session_id=session_id,
path="scripts/hello.py",
old_string="Hello, World!",
new_string="Hello, Python!",
)
# Returns: {"status": "ok", "path": "scripts/hello.py", "replacements": 1}
# Replace all occurrences
result = edit_file(
session_id=session_id,
path="data.txt",
old_string="foo",
new_string="bar",
replace_all=True,
)
upload_file_path
Upload a file from the host filesystem to the session directory by providing its absolute path. Only files within allowed directories are permitted, and sensitive files (.env, .ssh/, credentials, etc.) are blocked.
Parameters:
session_id(string, required): The session IDhost_path(string, required): Absolute path to the file on the host filesystemdestination_path(string, required): Relative path within session directoryoverwrite(boolean, optional): Whether to overwrite an existing file (default:true)
Returns: A dictionary containing:
status(string):"success"if the upload succeededsandbox_path(string): Absolute path inside the sandboxsize(string): File size in bytes
Example usage:
result = upload_file_path(
session_id=session_id,
host_path="/home/user/data/dataset.csv",
destination_path="data/dataset.csv",
overwrite=False
)
Path Security Configuration
The upload_file_path tool restricts which host filesystem paths can be uploaded for security. You can configure allowed directories using either a command-line argument or an environment variable.
Configuration Methods (in order of precedence)
1. --allowed-dir CLI Argument
Pass one or more --allowed-dir arguments when starting the server:
# Allow uploads from a single directory
jupyter-interpreter-mcp --allowed-dir /home/user/projects
# Allow uploads from multiple directories
jupyter-interpreter-mcp \
--allowed-dir /home/user/projects \
--allowed-dir /home/user/data
2. ALLOWED_UPLOAD_DIRS Environment Variable
Set the ALLOWED_UPLOAD_DIRS environment variable to a colon-separated list of absolute directory paths:
# Allow uploads from multiple directories
export ALLOWED_UPLOAD_DIRS=/home/user/projects:/home/user/data
# Or in your .env file
ALLOWED_UPLOAD_DIRS=/home/user/projects:/home/user/data
3. Allow all
Using the --allow-all flag will allow uploads from any directory on the host filesystem.
4. Default Behavior
When neither --allowed-dir nor ALLOWED_UPLOAD_DIRS is set, uploads are allowed only from the current working directory on the host filesystem. Sensitive file protection (see below) is always active regardless of this setting.
MCP Client Configuration Examples
OpenCode
In your opencode.jsonc (global config at ~/.config/opencode/opencode.json or per-project):
{
"mcp": {
"jupyter-interpreter": {
"type": "local",
"command": [
"uv", "run", "--project", "/path/to/jupyter-interpreter-mcp",
"jupyter-interpreter-mcp",
"--allowed-dir", "/home/user/projects",
"--jupyter-base-url", "http://localhost:8888",
// ... other args
],
// Or use environment variables:
"environment": {
"ALLOWED_UPLOAD_DIRS": "/home/user/projects:/home/user/data"
}
}
}
}
For most use cases, either use the environment variable approach with hardcoded paths, or rely on the CWD-only default with sensitive file protection.
Claude Desktop / Cursor
In claude_desktop_config.json or .cursor/mcp.json:
{
"mcpServers": {
"jupyter-interpreter": {
"command": "jupyter-interpreter-mcp",
"args": [
"--allowed-dir", "/home/user/projects",
"--jupyter-base-url", "http://localhost:8888"
],
"env": {
"JUPYTER_TOKEN": "your-token-here"
}
}
}
}
Sensitive File Protection
Regardless of allowed directories, the following file patterns are always blocked from upload:
.envfiles (e.g.,.env,.env.local)- SSH keys and configuration (
.ssh/) - GPG keys (
.gnupg/) - AWS credentials (
.aws/) - Docker credentials (
.docker/config.json) - Generic credential files (
credentials.json,credentials.yaml) - Netrc files (
.netrc) - NPM/PyPI tokens (
.npmrc,.pypirc) - Secret/token files (
secret.json,tokens.yaml, etc.) - Git credentials (
.git-credentials)
Development
Installing Development Dependencies
uv pip install -e ".[dev,test]"
Testing
Tests can be run using pytest. If you're using mcpo you can start the server using e.g. the following command:
uvx mcpo --port 8000 -- uv run --directory /path/to/jupyter-interpreter-mcp jupyter-interpreter-mcp
For this, a configured .env file is required.
You can then test the MCP server endpoint at http://localhost:8000/docs.
License
MIT
Metadata
Release files for jupyter-interpreter-mcp 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| jupyter_interpreter_mcp-0.4.0.tar.gz | 60.7 kB | With provenance | Details for jupyter_interpreter_mcp-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| jupyter_interpreter_mcp-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for jupyter_interpreter_mcp-0.4.0-py3-none-any.whl |
Total release size: 92.9 kB
Release files / jupyter_interpreter_mcp-0.4.0.tar.gz
| Download URL | jupyter_interpreter_mcp-0.4.0.tar.gz |
|---|---|
| Size | 60.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e04e4967fdbeaab24fa71eb1ef2765b36bc5c8146275ec7edce7e009ed2edfa8
|
|
BLAKE2b-256 checksum How to use checksums |
94d234fdf153bfe9024abe59bc9a2b78d204eac1fef07ebb9f030290c6b3ac4e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 23, 2026.
Transparency logRelease files / jupyter_interpreter_mcp-0.4.0-py3-none-any.whl
| Download URL | jupyter_interpreter_mcp-0.4.0-py3-none-any.whl |
|---|---|
| Size | 32.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
443d352f4c231204bb6b73fe6e230e0ffe75e663dc14c6ca9ed6d3273a694555
|
|
BLAKE2b-256 checksum How to use checksums |
3180d2d8fb3dd54403f8d35c7237660d8117351068b3f62b5fdf3cc6d5c7cc7f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 23, 2026.
Transparency logSecurity for jupyter-interpreter-mcp 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for jupyter_interpreter_mcp-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.