Observed since 20 Oct, 21:07 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
MCP Server for per-user JupyterLab server management on the ATLAS AF Kubernetes cluster
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.
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.
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
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.
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.
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.
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.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.
pixi run test # quick tests
pixi run lint # pre-commit + pylint
pixi run helm-lint # lint + smoke-render the Helm chart
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.4.0 | 9 Oct 2026 | 6 |
This describes our collected data; it is not a security assessment.
Shared repository data · GitHub
Observed since 20 Oct, 21:07 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:8120d36c8deee5143767b448e8e7940f48bb9204ec54337390cd8d608313b2fc