Notebooks¶
The built-in notebook MCP server lets agents create, execute, and manage Jupyter notebooks and Quarto documents (.qmd). Agents interact through MCP tools; humans view results in JupyterLab.
Installation¶
The notebook server runs as a Docker container. The image is built automatically on deploy (src/pynchy/agent/build.sh) or on first use by the MCP manager. To build manually:
How it works¶
The notebook server is a first-party plugin that runs as a Docker container, sandboxing all kernel execution. It provides:
- MCP tools for agents — start kernels, execute code cells, add markdown, save/load notebooks
- JupyterLab for humans — a web frontend on port 8888 for viewing and interacting with notebooks
- IPython kernels managed directly via
jupyter_client— nojupyter_serveroverhead
Agents interact with notebooks exclusively through MCP tools, which handle kernel lifecycle, cell execution, output collection, and auto-saving. Agents can also read and edit .qmd files directly from the workspace, since notebooks live in groups/<workspace>/notebooks/ (mounted at /workspace/group/notebooks/ inside the container).
Enabling notebooks¶
Add "notebook" to a workspace's MCP server list:
No per-workspace config required. The server automatically scopes notebooks to groups/<workspace>/notebooks/ and sets the kernel's working directory to groups/<workspace>/, so the agent can reference workspace files naturally (e.g., pd.read_csv("mydata.csv")).
Each workspace gets its own server instance — no cross-workspace contamination.
Default format: Quarto (.qmd)¶
Notebooks default to .qmd (Quarto markdown) rather than .ipynb. Quarto documents use plain text with code fences, making them more readable for agents and easier to diff in version control:
## Sales Analysis
Loaded the Q4 sales data and filtered for the US region.
```{python}
import pandas as pd
df = pd.read_csv("sales.csv")
df[df.region == "US"].head()
```
The US region accounts for 62% of total revenue.
To work with .ipynb files instead, include the extension in the notebook name (e.g., start_kernel(name="analysis.ipynb")).
MCP tools¶
All tools become available once the workspace includes "notebook" in its server list.
Kernel lifecycle¶
start_kernel(name?) — Start an IPython kernel. If name refers to an existing notebook, load and re-execute all code cells to restore kernel state (session rehydration). If omitted, generate a name like 2026-02-20-ailing-amoeba.
shutdown_kernel(kernel_id) — Save and shut down a kernel.
list_kernels() — List active kernels with their notebook names and cell counts.
Working with cells¶
execute_cell(kernel_id, code) — Execute Python code. Returns outputs (text, image file paths, errors). The cell and its outputs append to the notebook and auto-save to disk. Images save automatically to <notebook>_files/ alongside the notebook.
add_markdown(kernel_id, content) — Add a markdown cell. Auto-saves to disk.
File operations¶
save_as(kernel_id, name) — Save under a different name. Use .qmd or .ipynb extension to control format.
read_notebook(name) — Read an existing notebook without starting a kernel. Returns structured cell contents.
list_notebooks() — List saved notebooks with sizes and modification times.
Agent workflow¶
A typical agent session:
start_kernel()
→ kernel_id: "a1b2c3d4", notebook: "2026-02-20-ailing-amoeba.qmd"
execute_cell(kernel_id="a1b2c3d4", code="import pandas as pd\ndf = pd.read_csv('sales.csv')\ndf.head()")
→ outputs: [{"type": "result", "text": " date revenue\n0 ..."}]
add_markdown(kernel_id="a1b2c3d4", content="## Sales Analysis\nLoaded sales data for Q4.")
execute_cell(kernel_id="a1b2c3d4", code="df.describe()")
→ outputs: [{"type": "result", "text": " revenue\ncount ..."}]
save_as(kernel_id="a1b2c3d4", name="q4-sales-analysis")
→ notebook: "q4-sales-analysis.qmd", cells: 4
shutdown_kernel(kernel_id="a1b2c3d4")
Session rehydration¶
When an agent calls start_kernel(name="q4-sales-analysis") for an existing notebook, the server:
- Starts a fresh IPython kernel
- Loads the notebook from disk
- Re-executes all code cells sequentially to restore kernel state
- Returns a summary: cell count, any errors during replay
This lets agents resume work across sessions without losing state. The kernel starts fresh, but replaying the cells restores all variables and imports.
Agent-friendly output¶
The kernel auto-configures libraries for text-friendly output at startup:
- Pandas — wide column display, increased row/column limits for readable tables
- Matplotlib — non-interactive
Aggbackend (avoids GUI window attempts) - Images — all
image/pngoutputs (matplotlib plots, PIL images, etc.) save automatically to<notebook>_files/cell_N.png. The agent receives the file path instead of raw base64.
Finding saved images¶
execute_cell returns image paths relative to the notebook directory. From the container filesystem, images are at:
For example, a notebook named q4-sales-analysis that produces a plot in cell 3:
- Relative path (returned by tool):
q4-sales-analysis_files/cell_3.png - Container path:
/workspace/group/notebooks/q4-sales-analysis_files/cell_3.png
If a single cell produces multiple images, they're suffixed: cell_3_1.png, cell_3_2.png, etc.
Installing dependencies¶
The container ships with common data-science libraries pre-installed (pandas, matplotlib, numpy). When you need a package that isn't available, install it at runtime from a notebook cell using uv:
Use --system because the container runs without a virtual environment. Installed packages persist for the lifetime of the container but are lost when it restarts (idle timeout, deploy, manual stop). Add frequently needed packages to src/pynchy/agent/mcp/notebook.Dockerfile instead.
Direct file access¶
Since notebooks live inside the workspace folder (/workspace/group/notebooks/), agents can also:
- Read
.qmdfiles from previous sessions directly with filesystem tools - Edit earlier cells by modifying the
.qmdfile, then re-executing withstart_kernel(name=...) - Include notebook files in git commits
Viewing notebooks¶
JupyterLab runs alongside the MCP server on port 8888 (no authentication — designed for Tailscale access). Open http://pynchy-server:8888 to browse and interact with notebooks.
Notebooks auto-save on every execute_cell and add_markdown call, so JupyterLab always reflects the latest state.
Idle timeout¶
The MCP manager stops the container after 30 minutes of no MCP tool calls. Notebook files persist on the host (bind-mounted workspace), so no data is lost. The next agent tool call starts a fresh container automatically.
Want to customize this? Write your own plugin — see the Plugin Authoring Guide. Have an idea but don't want to build it? Open a feature request.