af-jupyterlab-mcp
Allows users to create, inspect, list, and delete their own per-user JupyterLab servers, and query GPU availability and supported images.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@af-jupyterlab-mcpCreate a Jupyter server with 2 CPUs and a GPU"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 brokerThis 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.
Related MCP server: rucio-mcp
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 toolsTool 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 a per-user JupyterLab server (pod+service+secret+ingress) | mutating |
| List the caller's own JupyterLab servers | read-only |
| Get rich status for one of the caller's own JupyterLab servers | read-only |
| Delete one of the caller's own JupyterLab servers (all four objects) | destructive |
| Get cluster-wide GPU availability, optionally filtered by product | read-only |
| List the CPU and GPU images allowed by | 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 |
| List files on the notebook server's filesystem | read-only |
| List all running kernels on the notebook server | read-only |
| List notebooks open on the notebook server | read-only |
| Connect to or create a notebook on the notebook server | mutating |
| Disconnect from a notebook on the notebook server | mutating |
| Restart a notebook's kernel on the notebook server | destructive |
| Read a notebook's cells from the notebook server | read-only |
| Read a cell from the active notebook | read-only |
| Insert a cell at a given index in the active notebook | mutating |
| Overwrite the source of a cell in the active notebook | destructive |
| Edit part of a cell's source in the active notebook | mutating |
| Delete one or more cells from the active notebook | destructive |
| Move a cell to a different index in the active notebook | mutating |
| Execute a specific cell in the active notebook | destructive |
| Insert a code cell and immediately execute it | destructive |
| Execute arbitrary code in the notebook server's kernel | destructive |
| 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 ( | Kind |
Notebook |
| read-only |
Notebook |
| mutating |
Notebook |
| destructive |
Console |
| mutating |
Console |
| destructive |
Documents |
| mutating |
File browser |
| read-only |
File browser |
| mutating |
Kernel |
| mutating |
Kernel |
| destructive |
UI |
| read-only |
Search |
| read-only |
Terminal |
| 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 chartThis server cannot be deployed
Maintenance
Related MCP Connectors
Massed Compute MCP — GPU inventory, VM lifecycle, billing, SSH keys, and setup recipes.
GPU cloud platform — create, manage, and monitor instances, snapshots, SSH keys, and billing.
Read GPU instances, types, images, filesystems and firewall rules; launch and terminate instances.
On-demand GPU nodes for agents: create nodes, run commands, and submit jobs, billed by the minute.
Related MCP Servers
- FlicenseAqualityCmaintenanceAn MCP server that enables LLMs to execute Python code on GPU-accelerated compute nodes within SLURM-managed HPC environments. It bridges local clients to remote clusters by launching JupyterLab sessions via SLURM jobs to facilitate high-performance notebook-based computation.7-
- AlicenseNot gradedqualityCmaintenanceAn MCP server that exposes Rucio distributed data management operations as tools for LLMs. Designed for ATLAS physicists working with grid data on analysis facilities, but usable with any Rucio instance.63 PyPI6Apache 2.0
- AlicenseAqualityCmaintenanceEnables code execution in isolated Docker containers with persistent IPython, Node.js, or R kernels, supporting file import/export and cross-session transfers via MCP tools.6MIT
- AlicenseAqualityDmaintenanceAI-powered MCP server for connecting and managing Jupyter Notebooks. Enables interactive code execution, multi-notebook management, and multimodal output for data analysis, visualization, and machine learning.1274 PyPI9MIT