A working prototype of the Deepnote-as-a-data-agent vision: an MCP server
that lets any MCP-compatible client (Claude Desktop, Cursor, Windsurf, etc.)
drive a Deepnote-style notebook session — running SQL, executing Python,
ingesting CSVs, rendering charts, and executing native .deepnote projects
end-to-end, with state that persists across calls.
Built on deepnote-toolkit (Deepnote's own execution surface) + DuckDB +
Vega-Lite. ~385 LOC of server code; no cloud dependency for v0.
Using the Model Context Protocol (MCP) developed by Anthropic, we'll enable Deepnote to be called directly by AI assistants to generate, analyze, and return answers powered by Deepnote AI. Imagine: Claude gets a question like "What were Q4 sales in Germany?", calls Deepnote, which spins up a notebook, runs the analysis, and returns the result (chart and all) in seconds.
This repo demonstrates that vision with the toolkit Deepnote already ships — no closed cloud APIs required for the proof.
Demo dataset: examples/popular_cities_weather.csv — 7 056 rows of monthly
weather (tavg, tmin, tmax, precipitation, wind, pressure, sunshine) for
96 Indian cities, 2020–2025.
Claude: "Which Indian cities are hottest in May, and chart the top 10."
load_csv("popular_cities_weather.csv", "cities")→ table + DataFrame in sessionrun_sql("SELECT city, ROUND(AVG(tmax),1) AS mean_tmax FROM cities WHERE month(date)=5 GROUP BY city ORDER BY mean_tmax DESC LIMIT 10", "hottest_may")→ 10 rows boundcreate_chart(df_var="hottest_may", x="city", y="mean_tmax", chart_type="bar")→ PNG returned inline- (optional)
run_notebook("cities_weather.deepnote")→ runs the canonical 4-block analysis (summer hottest, Delhi YoY trend, wettest cities) and exposes its variables for follow-up questions
smoke_chart.png is the rendered output of step 3.
┌──────────────────┐ stdio (MCP) ┌────────────────────────────────────┐
│ Claude Desktop / │ ◀───────────────▶ │ deepnote-mcp (FastMCP) │
│ Cursor / agent │ JSON-RPC tools │ │
└──────────────────┘ │ ┌──────────────┐ ┌────────────┐ │
│ │ IPython shell│ │ DuckDB │ │
│ │ (stateful) │ │ in-proc │ │
│ └──────┬───────┘ └─────┬──────┘ │
│ │ shared user_ns │ │
│ ▼ ▼ │
│ ┌──────────────────────────────┐ │
│ │ vl-convert (Vega-Lite → PNG) │ │
│ │ deepnote-toolkit (formatters,│ │
│ │ connectors, dataframe util)│ │
│ │ nbformat / PyYAML (.deepnote)│ │
│ └──────────────────────────────┘ │
└────────────────────────────────────┘
Key design choice: shared session state across tools. run_sql binds a
DataFrame; run_python and create_chart see it. This matches how a human
analyst works in a notebook and how an LLM reasons iteratively — and is the
right primitive, not one-shot RPCs.
| Tool | Purpose | Returns |
|---|---|---|
run_python(code) |
Execute Python in stateful IPython kernel; pd and duck pre-imported |
JSON: stdout, stderr, last-expr repr, error |
run_sql(query, bind_to="df") |
Run SQL via DuckDB; bind result to session DataFrame | JSON: row count, schema, 50-row preview |
load_csv(path, table_name="data") |
Ingest CSV into DuckDB + session (path-checked, size-capped) | JSON: rows, columns, byte size |
create_chart(df_var, x, y, chart_type, color?, title?) |
Render Vega-Lite (bar/line/point/area) |
PNG Image (binary) |
run_notebook(path, notebook?) |
Execute every code block of a .deepnote project (or .ipynb) |
JSON: per-block stdout/stderr/error |
list_session_vars() |
Introspect session state | JSON: name, type, shape |
Native Deepnote format. run_notebook parses the YAML .deepnote
project schema (multiple notebooks, sortable blocks[]), executing
type: code blocks in sortingKey order. .ipynb is supported as a
fallback for compatibility with the broader Jupyter ecosystem.
The threat model for a local stdio MCP is "the LLM agent might do something dumb or get prompt-injected via the data it ingests" — not a remote attacker on the wire. Controls focus there. All limits are env-tunable.
| Control | Default | Env var | What it prevents |
|---|---|---|---|
| Path containment | examples/ only |
DEEPNOTE_MCP_DATA_DIR, DEEPNOTE_MCP_ALLOW_ABS |
../../etc/passwd-style traversal, reading arbitrary files |
| File size cap (load_csv) | 100 MB | DEEPNOTE_MCP_MAX_FILE_MB |
Memory blowup from oversized inputs |
| SQL row cap | 100 000 | DEEPNOTE_MCP_MAX_ROWS |
SELECT * from huge tables — auto-injects LIMIT if missing, leaves user-supplied LIMIT alone |
| Chart row cap | 5 000 (sampled) | DEEPNOTE_MCP_MAX_CHART_ROWS |
Vega-Lite payload bloat |
| Output truncation | 16 000 chars/stream | DEEPNOTE_MCP_MAX_OUTPUT_CHARS |
A runaway print exhausting the LLM context |
| Notebook block cap | 200 blocks | DEEPNOTE_MCP_MAX_BLOCKS |
Pathological / adversarial notebooks |
| Identifier validation | [A-Za-z_][A-Za-z0-9_]* |
— | SQL identifier injection via table_name |
| Chart type validation | enum | — | Arbitrary Vega-Lite mark types |
| Safe mode | off | DEEPNOTE_MCP_SAFE_MODE=1 |
Disables run_python and run_notebook — leaves SQL + load + chart for read-only deployments |
What is not enforced (and why):
- Wall-clock timeout / true sandboxing.
run_pythonis arbitrary code execution by design — that's what makes the agent useful. A real cloud deployment would replace the in-process IPython shell with a Deepnote cloud kernel, which already has process isolation and resource quotas. Locally, this server inherits the user's permissions; treat it that way. - Network egress controls. Same reasoning. Add at the OS layer (e.g. firewall rules) if needed.
Two harnesses, both green.
== load_csv ✅ 7 056 rows from examples/popular_cities_weather.csv
== run_sql ✅ top 5 hottest May tmax → 5 rows bound as hottest_may
== run_python ✅ Delhi summer (Apr–Jun) mean tmax computed in-session
== list_session_vars ✅ pd, duck, cities, hottest_may, d
== create_chart ✅ 141 KB PNG written to smoke_chart.png
== run_notebook ✅ all 4 blocks of cities_weather.deepnote executed
ALL OK
| # | Section | Tests | Result |
|---|---|---|---|
| 1 | Happy path | 6 | ✅ all pass |
| 2 | Persistence (state survives across tool calls) | 2 | ✅ all pass |
| 3 | Error recovery (bad SQL / bad Python don't kill session) | 3 | ✅ all pass |
| 4 | Limits enforcement (row, output, file size, block, chart) | 6 | ✅ all pass |
| 5 | Path safety (../, abs paths, identifier injection, bad cols/types) |
6 | ✅ all pass |
| 6 | Safe mode (run_python + run_notebook refuse; SQL still works) | 3 | ✅ all pass |
Reproduce locally:
~/.deepnote/venv/bin/python smoke_test.py # basic pipeline
~/.deepnote/venv/bin/python stress_test.py # 26-test safety suiteSelected stress-test highlights:
- SQL row cap injection —
SELECT * FROM range(20000)is rewritten asSELECT * FROM (...) LIMIT 5000and the response includes"row_cap_applied": true. An explicitLIMIT 7is preserved untouched. - Path traversal —
load_csv("../../../etc/passwd")returns{"error": "path … is outside DATA_DIR …"}rather than reading the file. - Output truncation —
print('x' * 50000)returns ~2000 chars + a…[truncated 48000 chars]marker. - Block cap — a synthetic 25-block
.deepnoteruns only the first 10 with"block_cap_applied": true. - Safe mode — verified by re-importing the server in a subprocess with
DEEPNOTE_MCP_SAFE_MODE=1.run_pythonandrun_notebookrefuse;run_sqlstill works.
- macOS / Linux
- Python 3.10–3.13 (3.14 not yet supported by
deepnote-toolkit) - A
.deepnote-aware editor is not required — this server speaks MCP
python3.12 -m venv ~/.deepnote/venv
~/.deepnote/venv/bin/pip install deepnote-toolkit mcp duckdb pandas pyyaml \
nbformat vl-convert-pythonMerge claude_desktop_config.example.json
into ~/Library/Application Support/Claude/claude_desktop_config.json,
adjusting paths if needed. Restart Claude Desktop.
Then ask: "Load examples/popular_cities_weather.csv as cities and chart the 10 hottest Indian cities by average May max temperature."
~/.deepnote/venv/bin/python /path/to/deepnote-mcp/src/server.pyThe server speaks stdio. Point any MCP client at the command above.
deepnote-mcp/
├── src/server.py # MCP server (~385 LOC)
├── examples/
│ ├── popular_cities_weather.csv # 96 Indian cities × monthly weather, 2020–2025
│ └── cities_weather.deepnote # native Deepnote project (YAML)
├── smoke_test.py # 6-step pipeline check
├── stress_test.py # 26-test safety + edge-case suite
├── smoke_chart.png # last rendered chart
├── claude_desktop_config.example.json # MCP wiring
└── README.md # this report
- Your toolkit is already enough for v1.
deepnote-toolkit+nbformat+ a Vega-Lite renderer covers the full execute-and-return loop. No new infra needed to ship MCP support. - The native
.deepnoteproject format Just Works as the input surface — sortable blocks, multi-notebook projects, explicitexecutionMode. Parsing it took ~10 lines. - The right primitive is "stateful session + tools." Claude composes
load_csv→run_sql→create_chartthe way an analyst would. A singleanswer_question(prompt)RPC would be both less powerful and less debuggable. - Drop-in path to cloud. Swap the local IPython shell for a Deepnote cloud kernel + project context, keep the exact same six-tool surface. Reusable modules and shared notebooks become callable as MCP tools by name — your workspace becomes a queryable data API.
- No wall-clock timeout on
run_python(see §5). Production needs process isolation — that's the cloud kernel's job. - DuckDB only for
run_sqlin this v0.deepnote-toolkitships Postgres / Snowflake / BigQuery / Redshift / Databricks / MongoDB / Trino / Athena / ClickHouse / MSSQL connectors — wiringrun_sqlto a configurable engine is mechanical. - No streaming. Long notebook runs return one final blob. MCP supports progress notifications; adding them is a small change.
- No auth. Local stdio only. For HTTP/SSE deployment, FastMCP exposes
auth_server_providerfor OAuth.
Prototype built in one session against the toolkit Deepnote already ships. Not affiliated with Deepnote.