Af Jupyterlab
FreeNot checkedMCP Server for per-user JupyterLab server management on the ATLAS AF Kubernetes cluster
About
MCP Server for per-user JupyterLab server management on the ATLAS AF Kubernetes cluster
README
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
Install Af Jupyterlab in Claude Desktop, Claude Code & Cursor
unyly install af-jupyterlabInstalls into Claude Desktop, Claude Code, Cursor & VS Code — handles npx, uvx and build-from-source repos for you.
First time? Get the CLI: curl -fsSL https://unyly.org/install | sh
Or configure manually
Run in your terminal:
claude mcp add af-jupyterlab -- uvx af-jupyterlab-mcpStep-by-step: how to install Af Jupyterlab
FAQ
Is Af Jupyterlab MCP free?
Yes, Af Jupyterlab MCP is free — one-click install via Unyly at no cost.
Does Af Jupyterlab need an API key?
No, Af Jupyterlab runs without API keys or environment variables.
Is Af Jupyterlab hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Af Jupyterlab in Claude Desktop, Claude Code or Cursor?
Open Af Jupyterlab on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
GitHub
PRs, issues, code search, CI status
by GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Unyly Deploy
Deploy sites and MCP servers to Unyly straight from your AI agent
by UnylyAmap Maps Mcp Server
MCP server for using the AMap Maps API
by duxiaohuiPayMCP
Lightweight payments layer for MCP servers: turn tools into paid endpoints with a two-line decorator. [PyPI](https://pypi.org/project/paymcp/) · [npm](https://w
by blustAIServer For Power BI
Enables exploring Microsoft Fabric / Power BI workspaces and semantic models, and executing ad-hoc DAX queries.
by Mitsubishi-FusoGpt Image 2 Mcp
RunAPI GPT Image 2 MCP server for image generation: create tasks, poll results, and check pricing across 1 model from Claude Code, Codex, Cursor, and VS Code.
by runapi-aiTemplate MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
by mcpdotdirectSupabase
Database, auth and storage
by SupabaseCompare Af Jupyterlab with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All development MCPs
