Skip to content

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.

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. Use nb_restart_notebook to 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.


See Contributing for development setup.