DuckDB analysis and containerized Python execution, exposed as a single-binary MCP server. Bring your own LLM client.
data-toolbox-mcp lets any MCP client (Claude Desktop, Cursor, ...) load tabular data into a per-workspace DuckDB and run SQL or Python against it inside a Podman sandbox. Nine tools are exposed:
load_data(workspace_id, file_path, table_name)query_data(workspace_id, sql)— auto-LIMIT returnstruncated+total(v0.4.0)execute_code(workspace_id, language, code)list_workspaces()— discover prior workspaces across sessionsdelete_workspace(workspace_id, dry_run?)— irreversible by default;dry_run: trueshows what would be removed (v0.4.0)describe_runtime()— what the container ships (python, packages, fonts, network)attach_files(workspace_id, paths)— return/workfiles as inline MCP image / text contentload_from_work(workspace_id, file_path, table_name)— table-ize a file already in/workdescribe_workspace(workspace_id)— every table's column schema in the workspace (v0.4.0)
The server is LLM-agnostic: it speaks plain MCP over stdio and never talks to any LLM provider itself.
shell-agent-v2 bundles a Wails GUI, an LLM client, and a DuckDB + Podman tool layer in one process. When you want the same data tools from a different LLM client, you'd have to reach inside that bundle. data-toolbox-mcp extracts the tool layer alone and ships it as a reusable MCP server, so any compliant client can use it.
- Three MCP tools for the load → query → analyze loop.
- workspace_id scoping: each workspace owns one container and one DuckDB file; state persists across server restarts. (ADR-0001)
- Podman sandbox with
network=noneby default; CPU / memory / timeout caps configurable. (ADR-0002) - Python runtime (
duckdb,pandas,polars,pyarrowbundled). (ADR-0003) - stdio transport only — no network exposure, no auth needed. (ADR-0004)
- No registry push — the runtime Dockerfile is
go:embed-ed and built locally on first use. (ADR-0005) - Single binary, single version:
serve/build-runtime/doctor/versionsubcommands all ship in one binary. - Structured tool errors: every tool error has a stable
codeLLM clients can branch on (path_not_allowed,unsupported_language,script_failed, ...). - Defense-in-depth path checks:
allowed_pathsis enforced afterEvalSymlinkson both sides, blocking symlink jail-breaks.
- macOS or Linux
- Podman (rootless). On macOS run
podman machine startonce before using. - Go 1.23+ to build from source
# 1. Build the binary (signed with Developer ID on macOS if the cert is in your keychain)
make build
# 2. Build the runtime container image (first time only, ~2 min)
dist/data-toolbox-mcp build-runtime
# 3. Verify the environment
dist/data-toolbox-mcp doctor
# 4. Wire it into your MCP client (Claude Desktop config example)
cat >> ~/Library/Application\ Support/Claude/claude_desktop_config.json <<'JSON'
{
"mcpServers": {
"data-toolbox": {
"command": "/absolute/path/to/dist/data-toolbox-mcp",
"args": ["serve", "--config", "/Users/you/.config/data-toolbox-mcp/config.toml"]
}
}
}
JSONA minimal config.toml:
[workspace]
workspace_dir = "~/.data-toolbox"
allowed_paths = ["~/data", "~/Downloads"]
[container]
image = "localhost/data-toolbox-runtime:latest"
stop_on_exit = true
[container.limits]
cpu = "1.0"
memory = "2GB"
timeout_seconds = 60
network = "none"
[query]
default_row_limit = 20000See config.example.toml for the full schema. Full client setup — Claude Desktop, Cursor, troubleshooting — is in docs/en/reference/client-setup.md.
| Command | Purpose |
|---|---|
serve (default) |
Start the MCP stdio server |
build-runtime |
Unpack the embedded Dockerfile and podman build the runtime image |
doctor |
Diagnose Podman, podman machine (macOS), runtime image, and config |
version |
Show the binary version |
| Tool | Arguments | Returns |
|---|---|---|
load_data |
workspace_id, file_path (host), table_name |
{rows_loaded, schema} |
query_data |
workspace_id, sql |
{rows, row_count, limit_applied, limit_reached, truncated, total, total_unavailable_reason?} |
execute_code |
workspace_id, language: "python", code |
{stdout, stderr, exit_code, host_work_dir} |
list_workspaces |
— | {workspaces: [{id, last_used, container_state, host_work_dir}]} |
delete_workspace |
workspace_id, dry_run? |
dry_run=false: {deleted, workspace_id}; dry_run=true: {would_delete, container_id, container_state, host_paths, disk_usage_bytes} |
describe_runtime |
— | {python_version, container_image, packages, fonts, network, mount_points, notes} |
attach_files |
workspace_id, paths: [string] (1–16, /work/... or relative) |
MCP content array: summary text + image / text / metadata blocks per file |
load_from_work |
workspace_id, file_path (/work/...), table_name |
{rows_loaded, schema} |
describe_workspace |
workspace_id |
{workspace_id, host_work_dir, container_state, tables: [{name, columns: [{name, type}]}]} |
load_data infers the reader from the file extension (.csv → read_csv_auto, .json / .jsonl → read_json_auto, .parquet → read_parquet). query_data auto-appends LIMIT [query] default_row_limit (default 20000) when the SQL has no LIMIT. execute_code only accepts language="python" in this version (ADR-0003); the runtime container ships with duckdb, pandas, polars, pyarrow, matplotlib, and Pillow, plus fonts-noto-cjk so Japanese matplotlib labels render without setup (ADR-0007). Call describe_runtime once at session start to inspect what's actually available.
attach_files (v0.3.0 / ADR-0008) returns files as MCP image content (PNG / JPG / SVG / GIF / WEBP / BMP) or text content (CSV / JSON / MD / etc.) so MCP clients render them inline; files above [attach] max_single_size_bytes (default 10 MiB) or beyond max_total_size_bytes (default 20 MiB) downgrade to metadata-only. load_from_work (v0.3.0 / ADR-0009) table-izes files that already live in /work — typically files written by execute_code — without going through allowed_paths.
describe_workspace (v0.4.0 / ADR-0010) returns every user table's column schema in one call — pair with list_workspaces for cross-session "what's in here?". query_data (v0.4.0) result now includes truncated and total, and a missing-table error carries an actionable hint (available tables + other workspaces). delete_workspace accepts dry_run: true to show what would be removed without acting.
allowed_pathsis enforced for every fileload_datais asked to read. The path is made absolute andEvalSymlinks-resolved before being compared with theEvalSymlinks-resolved allowed entries.- The container runs with
network=noneby default. To enable network access (and thus in-containerpip install), set[container.limits] network = "bridge"— there is intentionally no finer-grained ACL. - The container runs as a non-root user (UID 1000 from the runtime Dockerfile). On rootless Podman the host user is mapped to that UID via
--userns keep-id:uid=1000,gid=1000. - Per-tool timeouts are enforced via
context.WithTimeout; on expiry thepodman execchild is killed and the MCP request still returns (no hung calls). - Tool errors are returned as structured JSON inside the MCP content block; LLM clients can branch on the
codeslug.
Full model: docs/en/reference/architecture.md §6.
samples/ ships with three small datasets — sales.csv (40 rows), products.json (10 rows), logs.jsonl (41 rows) — and samples/README.md walks through a graded end-to-end verification (load → SQL → JOIN → window functions → quantiles → pandas → polars → workspace isolation → security boundaries).
docs/en/data-toolbox-mcp-rfp.md— the original RFPdocs/en/reference/architecture.md— overall architecturedocs/en/reference/phase1-plan.md— Phase 1 (v0.1.0) development plandocs/en/reference/v0.2.0-plan.md— v0.2.0 development plandocs/en/reference/client-setup.md— Claude Desktop / Cursor setupdocs/en/reference/v0.3.0-plan.md— v0.3.0 development plandocs/en/reference/v0.4.0-plan.md— v0.4.0 development plandocs/en/adr/— ten ADRs (0001–0010): workspace_id, Podman, Python-only, stdio, local-build distribution, workspace mgmt + describe_runtime, container package scope,attach_files,load_from_work, and v0.4.0 UX polish
The tool surface and the per-workspace DuckDB + container pattern are derived from shell-agent-v2. data-toolbox-mcp extracts and reshapes those ideas as a standalone MCP server.
MIT.