Skip to content

Repository files navigation

Deepnote MCP — Project Report

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.


1. The vision (Deepnote's words)

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.


2. Demo flow

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."

  1. load_csv("popular_cities_weather.csv", "cities") → table + DataFrame in session
  2. run_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 bound
  3. create_chart(df_var="hottest_may", x="city", y="mean_tmax", chart_type="bar") → PNG returned inline
  4. (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.


3. Architecture

┌──────────────────┐    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.


4. Tools

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.


5. Safety model

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_python is 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.

6. Test results

Two harnesses, both green.

smoke_test.py — does the basic pipeline work?

== 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

stress_test.py — 26/26 passed

# 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 suite

Selected stress-test highlights:

  • SQL row cap injectionSELECT * FROM range(20000) is rewritten as SELECT * FROM (...) LIMIT 5000 and the response includes "row_cap_applied": true. An explicit LIMIT 7 is preserved untouched.
  • Path traversalload_csv("../../../etc/passwd") returns {"error": "path … is outside DATA_DIR …"} rather than reading the file.
  • Output truncationprint('x' * 50000) returns ~2000 chars + a …[truncated 48000 chars] marker.
  • Block cap — a synthetic 25-block .deepnote runs 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_python and run_notebook refuse; run_sql still works.

7. Setup

Prerequisites

  • 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

Install

python3.12 -m venv ~/.deepnote/venv
~/.deepnote/venv/bin/pip install deepnote-toolkit mcp duckdb pandas pyyaml \
    nbformat vl-convert-python

Wire up Claude Desktop

Merge 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."

Run for any other MCP client

~/.deepnote/venv/bin/python /path/to/deepnote-mcp/src/server.py

The server speaks stdio. Point any MCP client at the command above.


8. Layout

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

9. What this proves to Deepnote

  1. 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.
  2. The native .deepnote project format Just Works as the input surface — sortable blocks, multi-notebook projects, explicit executionMode. Parsing it took ~10 lines.
  3. The right primitive is "stateful session + tools." Claude composes load_csvrun_sqlcreate_chart the way an analyst would. A single answer_question(prompt) RPC would be both less powerful and less debuggable.
  4. 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.

10. Limitations & next steps

  • No wall-clock timeout on run_python (see §5). Production needs process isolation — that's the cloud kernel's job.
  • DuckDB only for run_sql in this v0. deepnote-toolkit ships Postgres / Snowflake / BigQuery / Redshift / Databricks / MongoDB / Trino / Athena / ClickHouse / MSSQL connectors — wiring run_sql to 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_provider for OAuth.

Prototype built in one session against the toolkit Deepnote already ships. Not affiliated with Deepnote.

About

prototype of the **Deepnote-as-a-data-agent**

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages