Use Dango with Claude Code¶
Connect Claude Code (or any MCP-compatible coding agent — Cursor, Windsurf) directly to your Dango project. Once connected, your agent can list your data sources, inspect table schemas and dbt lineage, run read-only SQL, and — with your permission — trigger syncs, run dbt, and scaffold new models, all without you copy-pasting context back and forth.
Overview¶
Dango ships an MCP (Model Context Protocol) server: dango mcp run. You don't run this command yourself — your LLM client spawns it automatically once it's configured. It talks to your client over stdio (standard input/output on the local machine), not over the network.
The server exposes 15 tools:
- 8 read tools — list sources, inspect schemas, browse the catalog, trace lineage, read model SQL, run read-only queries, check sync history
- 7 mutation tools — trigger syncs and dbt runs, create sources/models/schedules
Setup¶
Automatic¶
This detects which supported LLM clients are installed on your machine (by checking whether their config directory exists) and writes a dango entry into each one's MCP config file:
| Client | Config file |
|---|---|
| Claude Code | ~/.claude/settings.json |
| Cursor | ~/.cursor/mcp.json |
| Windsurf | ~/.codeium/windsurf/mcp_config.json |
The write is atomic (temp file + rename) and preserves the file's existing contents and permissions — it merges a dango key into mcpServers rather than overwriting the whole file, so your other client settings are untouched.
Restart your LLM client afterward to pick up the new server.
Verify¶
Reports which supported clients were detected and whether each one's config file actually has the dango entry, e.g.:
Manual configuration¶
If your client isn't auto-detected yet, or you'd rather edit the config yourself, add this to the client's MCP config file under mcpServers:
{
"mcpServers": {
"dango": {
"command": "/path/to/your/project/venv/bin/dango",
"args": ["mcp", "run"]
}
}
}
command should point at the dango executable inside the same virtualenv you installed Dango into (dango mcp setup resolves this automatically from sys.executable; falls back to the bare dango command, relying on PATH, if it can't find a venv-local binary). MCP clients run one server process per project — if you work across multiple Dango projects, dango mcp run picks up whichever project you're in the same way any other dango command does (it walks up from the current directory to find project.yml).
Available tools¶
Read tools¶
| Tool | Purpose |
|---|---|
list_sources() | List all configured data sources with their sync status and row counts. |
get_table_schema(table_name, schema=None) | Get the schema (columns, types) for a table in the warehouse. |
get_catalog(source_filter=None) | Get the data catalog: all tables grouped by schema with row counts. |
get_lineage(model_name=None) | Get dbt lineage from the manifest — source → staging → intermediate → mart flow. |
list_models() | List all dbt models with their layer, schema, and file path. |
get_model_sql(model_name) | Get the SQL source for a dbt model. |
query(sql, row_limit=500) | Run a read-only SQL query against the DuckDB warehouse. Single SELECT (or WITH ... SELECT) only. |
get_sync_history(source_name=None, limit=10) | Get recent sync history for a source, or the most recent entries across all sources. |
Mutation tools¶
| Tool | Purpose |
|---|---|
run_sync(source_name, full_refresh=False) | Sync a data source. Respects the existing lock and queue semantics. |
run_transform(select=None, full_refresh=False) | Run dbt transformations. Equivalent to dango run. |
run_doctor() | Check credential health for all configured sources. Equivalent to dango doctor. |
add_source(source_type, source_name, description="") | Add a new data source to sources.yml. |
list_source_types() | List all available source types in the Dango registry. |
create_model(model_name, layer, upstream_refs, description="") | Create a new dbt model with correct structure and naming conventions. |
add_schedule(schedule_name, cron, sources, timezone="UTC", skip_dbt=False) | Add a new sync schedule. |
Worked examples¶
The tool calls and results below are real output from actually running these tools against a scratch test project (one CSV source, orders_csv, synced once) — not hand-written illustrations.
"What tables do I have?"¶
The agent starts broad with list_sources(), then drills into the catalog:
> list_sources()
[
{
"name": "orders_csv",
"type": "csv",
"enabled": true,
"last_sync": "2026-09-03T08:45:58.971287+00:00",
"rows": 4,
"status": "synced"
}
]
> get_catalog()
{
"tables_by_schema": {
"main": ["_dango_file_metadata"],
"raw_orders_csv": ["orders_csv"],
"staging": ["stg_orders_csv__orders_csv"]
},
"total": 3
}
> get_table_schema("stg_orders_csv__orders_csv")
{
"table_name": "stg_orders_csv__orders_csv",
"schema": "staging",
"columns": [
{"name": "order_id", "type": "BIGINT"},
{"name": "customer_id", "type": "BIGINT"},
{"name": "amount", "type": "DOUBLE"},
{"name": "order_date", "type": "DATE"}
]
}
From here the agent has enough to answer in plain English, or to follow up with query() for actual numbers.
"Build me a marts model for order revenue"¶
The agent traces lineage before writing anything, so the model refs the right upstream table:
> get_lineage()
{
"model_count": 1,
"source_count": 1,
"models": [
{"name": "stg_orders_csv__orders_csv", "schema": "staging", "layer": "staging"}
]
}
> create_model(
model_name="fct_orders",
layer="marts",
upstream_refs=["stg_orders_csv__orders_csv"],
description="Daily order metrics from orders_csv"
)
{
"status": "created",
"file_path": "dbt/models/marts/fct_orders.sql",
"sql_scaffold": "with stg_orders_csv__orders_csv as (select * from {{ ref('stg_orders_csv__orders_csv') }})\n\n-- TODO: add aggregations and business metrics here\n\nselect * from stg_orders_csv__orders_csv\n",
"warnings": [],
"next_steps": [
"Edit fct_orders.sql to add your business logic",
"Run: dango run to test"
]
}
create_model enforces Dango's naming convention as a feature, not an afterthought — staging models must start with stg_, intermediate with int_, and marts with fct_ or dim_. An agent that tries to name a marts model something else gets pushed back immediately instead of writing a model that violates the convention:
> create_model(model_name="orders_summary", layer="marts", upstream_refs=["stg_orders_csv__orders_csv"])
{
"error": "Marts models must be named fct_<metric> or dim_<entity>"
}
It also flags a common anti-pattern — marts models referencing raw tables instead of staging/intermediate ones — as a warning in the response rather than silently allowing it.
"Why did last night's sync fail?"¶
> get_sync_history("orders_csv")
[
{
"timestamp": "2026-09-03T08:45:58.971287+00:00",
"status": "success",
"duration_seconds": 0.59,
"rows_processed": 4,
"full_refresh": true,
"error_message": null,
"source": "orders_csv"
},
{
"timestamp": "2026-09-03T08:45:45.569894+00:00",
"status": "failed",
"duration_seconds": 0.03,
"rows_processed": 0,
"full_refresh": true,
"error_message": "CSV config missing for source: orders_csv",
"source": "orders_csv"
}
]
The error_message field on the failed entry is often enough on its own — here it's a configuration problem, not credentials. For credential-shaped failures (expired OAuth tokens, missing API keys), the agent follows up with run_doctor(), which checks every configured source and reports ok, missing, expired, or expiring_soon per source (the same statuses dango doctor reports on the command line):
> run_doctor()
[
{"source": "orders_csv", "type": "csv", "auth_type": "none", "status": "ok", "detail": ""}
]
Safety¶
Mutation tools are not a separate, less-validated path — most of them call the exact same functions the CLI commands use:
run_synccallsdango.ingestion.run_sync— the identical functiondango syncuses, including its internalDbtLockacquisition around both the data load and the post-sync dbt step. Concurrent syncs and dbt runs are serialized the same way regardless of whether they were triggered from the CLI or from an agent.run_transformacquires the sameDbtLockdango run(CLI) uses before calling dbt, mirroring the CLI command exactly. If the lock is already held by another sync or dbt run, the tool returns a clean{"status": "failed", "error": ...}rather than corrupting the warehouse or raising a raw exception — see the dbt Workflows page for more on theDbtLockmodel.run_doctorcalls the identical credential-health functiondango doctoruses.add_sourceonly writes thesources.ymlentry. It does not create credentials — for OAuth sources you still need to rundango oauth <source_type>yourself, and for API-key sources you still need to add the key to.dlt/secrets.toml. An agent cannot get your data flowing end-to-end without you completing that step.create_modelwrites a.sqlfile and updatesschema.yml; it does not run anything. You (or the agent, viarun_transform) still have to build the model before it materializes.
Auth¶
The MCP server has no separate authentication layer of its own. It's a local stdio process, spawned directly by your LLM client and communicating over stdin/stdout on your machine — there's no network listener to authenticate against. Whatever access the OS user running your LLM client has to your project directory (the same access you'd have running dango commands yourself in a terminal) is the access the agent has. It does not use, and does not need, Dango's web-app API-key mechanism — that mechanism authenticates HTTP requests to the Dango web server, a different surface entirely.
Next Steps¶
- dlt Workflows - Source configuration and sync internals
- dbt Workflows - Direct dbt access and the
DbtLockmodel - Troubleshooting - General troubleshooting guide