af-jupyterlab-mcp
MCP server that lets AF users create, inspect, and delete their own per-user JupyterLab servers on the UChicago ATLAS Analysis Facility Kubernetes cluster — the same notebooks af-portal deploys today, exposed as tools for LLMs.
Architecture
LLM <--MCP/HTTP--> af-jupyterlab-mcp <--k8s API--> notebook namespace (Pod/Service/Secret/Ingress)
^
| Authorization: Bearer <broker-issued JWT>
|
af-mcp-platform credential broker
This repo ships two groups of tools: six that manage the Pod/Service/
Secret/Ingress quadruple for a notebook, ported from af-portal's
portal/jupyterlab.py and its four Jinja templates, and sixteen nb_* tools
that proxy calls into the Datalayer jupyter-mcp-server running inside the
notebook itself (4ea435f), so a session can drive code execution inside the
user's own notebook without the notebook token ever entering LLM context — see
maniaclab/af-mcp-platform#189.
Project layout
src/af_jupyterlab_mcp/
├── cli.py # argparse: `af-jupyterlab-mcp serve` (HTTP only)
├── config.py # env-driven Settings: namespace, domain, image allowlist, quotas
├── server.py # FastMCP setup, lifespan (k8s client + broker verifier), tool registration
├── auth/
│ └── broker.py # extract_bearer(), get_broker_claims() -- broker-issued JWT verification
├── k8s/
│ ├── errors.py # GuardrailError, NameConflictError, NotFoundOrNotYoursError, ...
│ ├── guardrails.py # CPU/memory/duration range + image allowlist validation
│ ├── names.py # sanitize_k8s_pod_name, name availability, name generation
│ ├── templates.py # Jinja rendering of the four ported manifests
│ ├── notebooks.py # create/get/list/delete notebook (ported portal logic)
│ ├── gpu.py # get_gpu_availability (ported portal logic)
│ ├── proxy.py # call_notebook_tool -- MCP client that proxies into jupyter-mcp-server
│ └── templates/ # pod.yaml.j2, service.yaml.j2, secret.yaml.j2, ingress.yaml.j2
│ # (ported verbatim from af-portal/portal/templates/jupyterlab/)
└── tools/
├── _helpers.py # format_error(), append_next_actions(), format_notebook[_list]()
├── jupyterlab.py # the six CRD-management @mcp.tool() functions
└── nb_proxy.py # the sixteen nb_* jupyter-mcp-server proxy @mcp.tool() functions
Tool surface
22 tools total. Every tool's MCP annotations declare its
read-only/mutating/destructive status (see CLAUDE.md's "Tool registration
pattern"); the column below mirrors that.
Notebook server management (k8s/notebooks.py, k8s/gpu.py)
| Tool | Does | Kind |
|---|---|---|
create_jupyter_server |
Create a per-user JupyterLab server (pod+service+secret+ingress) | mutating |
list_jupyter_servers |
List the caller's own JupyterLab servers | read-only |
get_jupyter_server |
Get rich status for one of the caller's own JupyterLab servers | read-only |
delete_jupyter_server |
Delete one of the caller's own JupyterLab servers (all four objects) | destructive |
get_gpu_availability |
Get cluster-wide GPU availability, optionally filtered by product | read-only |
list_supported_images |
List the CPU and GPU images allowed by create_jupyter_server |
read-only |
The owner of every server is always claims.unixname from the verified broker
JWT — no tool takes an owner/username argument.
Notebook content proxy (k8s/proxy.py, upstream: jupyter-mcp-server)
Every nb_* tool takes notebook_server_id first, verifies the caller owns
that pod, checks it is Ready, and forwards the call to the notebook's own
jupyter-mcp-server with the notebook token injected server-side (never
returned to the caller).
| Tool | Does | Kind |
|---|---|---|
nb_list_files |
List files on the notebook server's filesystem | read-only |
nb_list_kernels |
List all running kernels on the notebook server | read-only |
nb_list_notebooks |
List notebooks open on the notebook server | read-only |
nb_use_notebook |
Connect to or create a notebook on the notebook server | mutating |
nb_unuse_notebook |
Disconnect from a notebook on the notebook server | mutating |
nb_restart_notebook |
Restart a notebook's kernel on the notebook server | destructive |
nb_read_notebook |
Read a notebook's cells from the notebook server | read-only |
nb_read_cell |
Read a cell from the active notebook | read-only |
nb_insert_cell |
Insert a cell at a given index in the active notebook | mutating |
nb_overwrite_cell_source |
Overwrite the source of a cell in the active notebook | destructive |
nb_edit_cell_source |
Edit part of a cell's source in the active notebook | mutating |
nb_delete_cell |
Delete one or more cells from the active notebook | destructive |
nb_move_cell |
Move a cell to a different index in the active notebook | mutating |
nb_execute_cell |
Execute a specific cell in the active notebook | destructive |
nb_insert_execute_code_cell |
Insert a code cell and immediately execute it | destructive |
nb_execute_code |
Execute arbitrary code in the notebook server's kernel | destructive |
The three code-execution tools (nb_execute_cell,
nb_insert_execute_code_cell, nb_execute_code) are marked destructive even
though "execute" isn't literally a delete: they can mutate anything the kernel
can reach, which is what the annotation communicates to a client.
nb_get_selected_cell and nb_run_all_cells are intentionally absent — they
require the jupyter-mcp-tools JupyterLab frontend extension, not installed in
the current notebook images.
Build and test commands
pixi run test # quick tests
pixi run lint # pre-commit + pylint
pixi run helm-lint # lint + smoke-render the Helm chart
Metadata
Release files for af-jupyterlab-mcp 0.3.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 | |
|---|---|---|---|
| af_jupyterlab_mcp-0.3.0.tar.gz | 150.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| af_jupyterlab_mcp-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 192.0 kB
Release files / af_jupyterlab_mcp-0.3.0.tar.gz
| Download URL | af_jupyterlab_mcp-0.3.0.tar.gz |
|---|---|
| Size | 150.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4920fb865ac8b7297d3a8f994a40c4d6a1da1ac3a82c29b88c747d8ce8f0f73d
|
|
BLAKE2b-256 checksum How to use checksums |
588bf5369194773d07cfefe0a949706e74a8bb00b0673c2629a26f7ea945ff31
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 18, 2026.
Transparency logRelease files / af_jupyterlab_mcp-0.3.0-py3-none-any.whl
| Download URL | af_jupyterlab_mcp-0.3.0-py3-none-any.whl |
|---|---|
| Size | 41.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4a487da3b9c1fc64832cf04fc0f8ec834c14c8c95d0d7b9a8741d3d68ec89b76
|
|
BLAKE2b-256 checksum How to use checksums |
effb1d7a734df91ddd69f6b9114e5fbe2208eb61f0d91183c054fbf141809676
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 18, 2026.
Transparency log