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 three 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; seventeen 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;
and 52 nb_ui_* tools that drive the user's open JupyterLab tab through the
jupyter-mcp-tools extension.
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 seventeen nb_* jupyter-mcp-server proxy @mcp.tool() functions
└── nb_ui.py # the 52 nb_ui_* JupyterLab frontend-command proxy tools
Tool surface
75 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 |
nb_clear_cell_output |
Clear a code cell's outputs, keeping the cell | 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.
Cell-addressing tools accept either a positional index or the cell's stable
cell_id (given both, the id wins); prefer ids, since an index goes stale as
soon as a cell is inserted above it.
JupyterLab UI proxy (tools/nb_ui.py, upstream: jupyter-mcp-tools)
Every nb_ui_* tool proxies one JupyterLab command through the same
ownership/readiness/token path as the nb_* tools, but the command runs
inside the user's open JupyterLab browser tab (relayed over a websocket by
the jupyter-mcp-tools extension) and acts on the active notebook, console, or
selection there. With no tab open, the call fails. The notebook image must
allowlist each command
(maniaclab/ml_platform#14).
Tool names are nb_ui_ + the upstream id with - → _ (upstream ids are
JupyterLab command ids with : → _, e.g. notebook:run-all-cells →
notebook_run-all-cells → nb_ui_notebook_run_all_cells). Tools that take
arguments beyond notebook_server_id are noted.
| Area | Tools (nb_ui_ prefix omitted) |
Kind |
|---|---|---|
| Notebook | notebook_get_selected_cell, notebook_move_cursor_down, notebook_move_cursor_up, notebook_extend_marked_cells_below, notebook_extend_marked_cells_above, notebook_copy_cell |
read-only |
| Notebook | notebook_insert_cell_below, notebook_insert_cell_above, notebook_paste_cell_below, notebook_paste_cell_above, notebook_move_cell_up, notebook_move_cell_down, notebook_split_cell_at_cursor, notebook_change_cell_to_code, notebook_change_cell_to_markdown, notebook_change_cell_to_raw |
mutating |
| Notebook | notebook_delete_cell, notebook_cut_cell, notebook_merge_cell_above, notebook_merge_cell_below, notebook_run_all_cells, notebook_run_cell, notebook_run_cell_and_select_next, notebook_run_cell_and_insert_below, notebook_append_execute (source, cell_type) |
destructive |
| Console | console_create (path, insert_mode, activate) |
mutating |
| Console | console_clear, console_interrupt_kernel, console_inject (code, path, activate) |
destructive |
| Documents | docmanager_open (path, factory), docmanager_new_untitled (content_type, path, ext), docmanager_save, docmanager_duplicate |
mutating |
| File browser | filebrowser_go_to_path (path), filebrowser_refresh, filebrowser_toggle_hidden_files |
read-only |
| File browser | filebrowser_create_new_directory |
mutating |
| Kernel | kernelmenu_reconnect_to_kernel |
mutating |
| Kernel | kernelmenu_interrupt, kernelmenu_shutdown |
destructive |
| UI | application_toggle_left_area, application_toggle_right_area, application_toggle_presentation_mode, apputils_change_theme (theme), editmenu_open, filemenu_open, helpmenu_open |
read-only |
| Search | documentsearch_start (search_text), documentsearch_highlightNext, documentsearch_highlightPrevious |
read-only |
| Terminal | terminal_create_new, terminal_refresh |
mutating |
"Read-only" here includes commands that only change view state (cursor,
selection, layout, search, theme). Commands that execute code are destructive,
like the nb_execute_* tools.
Deliberately not proxied (and not allowlisted in the image):
filebrowser_upload/filebrowser_download— they open a file picker or save into the human's browser; no bytes ever reach the MCP client.docmanager_delete/docmanager_rename/docmanager_save-as,kernelmenu_change,kernelmenu_restart,console_restart-kernel— they block on a modal dialog that needs a human click. Usenb_restart_notebookto restart a kernel without the UI.
Images with different tool sets
The tool list above is fixed, but the notebook behind a call can run any
allowlisted image, and older images offer fewer tools (e.g. ml-platform:2026.3
has no jupyter-mcp-server at all; images with jupyter-mcp-server 1.x lack
clear_cell_output and all but two UI commands). The chart value
notebook.images.toolOverrides maps an exact image string to the upstream
jupyter-mcp-server tool ids that image offers (e.g. read_cell,
notebook_run-all-cells). Images without an entry are assumed to support every
tool, matching the latest ml-platform image. Calling a tool a notebook's image
does not offer returns a clear "not supported by image" error naming the image,
without contacting the notebook.
On a jupyter-mcp-server 1.x image, the 2.x cell_id-style arguments are not
understood: 1.x silently drops unknown arguments, so passing both
cell_index and cell_id uses the index (2.x would use the id), and passing
cell_id alone fails upstream because cell_index is required there.
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.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 | |
|---|---|---|---|
| af_jupyterlab_mcp-0.4.0.tar.gz | 162.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| af_jupyterlab_mcp-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 210.9 kB
Release files / af_jupyterlab_mcp-0.4.0.tar.gz
| Download URL | af_jupyterlab_mcp-0.4.0.tar.gz |
|---|---|
| Size | 162.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0a6463e0654a2f52290bf5221f9fa5a8db11138fded72ff5381081d7fb8eba1d
|
|
BLAKE2b-256 checksum How to use checksums |
fd9c932a8657de9d33cb1cab8180d21e03a0196cb76bf029219ec490dd71550f
|
| 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 Oct 9, 2026.
Transparency logRelease files / af_jupyterlab_mcp-0.4.0-py3-none-any.whl
| Download URL | af_jupyterlab_mcp-0.4.0-py3-none-any.whl |
|---|---|
| Size | 48.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
611e7eb85fe54ac12a3ba894da7c19277d9d510f5b4a3e970323ce87083b37a3
|
|
BLAKE2b-256 checksum How to use checksums |
7c2e3490b8e29214ff8ff36a0aa8a0ff0fe92fd5986336906658353838bd696a
|
| 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 Oct 9, 2026.
Transparency log