sql_tool
Provides a read-only SQL tool server backed by DuckDB: fetches dataset schemas and executes generated SQL queries against the data, with server-side guardrails enforcing read-only access, blocked keywords, and single-statement execution.
Provides a Python analysis tool server backed by pandas for statistics SQL expresses poorly, exposing a fixed set of safe operations such as compute_correlation and compute_stat on the dataset's columns.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@sql_toolget the schema for sales.csv"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
DataPilot — AI Data Analyst Agent
Multi-agent system: ask a question about a dataset in plain language. Agents plan, generate/execute SQL (or route to a Python tool when SQL genuinely doesn't fit), validate the result, optionally chart it, and explain it back honestly — including saying "I can't compute that" instead of guessing. Built with LangGraph orchestration, real MCP tool servers, guardrails proven independent of LLM behavior, a self-correction retry loop, ChromaDB memory, and an eval harness with ground-truth checks.
Builds on the same orchestration/MCP/guardrails skeleton as the Job/Resume Intel Agent project — domain swapped, agent pattern reused.
All 10 steps of the build are complete. See TESTING.md for the
full run-by-run debugging journal (12 real test runs, every bug found
and how it was fixed) — that document is arguably more informative than
this one about how the system actually behaves.
Architecture
┌─────────────┐
│ Planner │ keyword routing (correlation → Python,
│ │ chart request → Chart Agent) + memory
└──────┬──────┘ retrieval (ChromaDB business terms)
│
┌─────────────┴─────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ SQL Agent │ │ Python Agent │
│ (MCP sql_tool) │ │(MCP python_tool)│
└───────┬───────┘ └───────┬───────┘
│ │
└─────────────┬──────────────┘
▼
┌─────────────┐
│ Validator │──── retry (SQL path only,
│ │ capped, on genuine errors
└──────┬──────┘ or column mismatches)
│
validated?│
┌────────────┴────────────┐
▼ ▼
┌───────────────┐ ┌─────────────┐
│ Chart Agent │ │ Interpreter │
│(MCP chart_tool)│─────────▶│ │
└───────────────┘ └─────────────┘Planner — decides SQL vs Python (deterministic keyword check, not LLM judgment — see Known Limitations for why), whether a chart was asked for, and retrieves relevant business-term context from memory.
SQL Agent — fetches schema + generates + executes SQL, entirely through MCP tool calls. Can respond
NOT_COMPUTABLEwhen business context says a question isn't answerable from this data (e.g. profit).Python Agent — handles correlation questions SQL fights with (aggregate + raw columns "in one row"). No arbitrary code execution — fixed, safe pandas operations only.
Validator — catches SQL errors, zero-row/NULL-only results, and column-mismatches (SQL that runs fine but ignores a column the question asked about). Drives the retry loop.
Chart Agent — generates a bar chart when asked, picking axes by data type (not SQL column order — a real bug found and fixed in testing). Fails gracefully on shapes that don't fit.
Interpreter — compiles the final plain-language answer, or explains a failure honestly instead of hallucinating from bad data.
MCP layer: sql_tool, python_tool, chart_tool — three real MCP
servers over stdio, not hardcoded functions. Agents talk to them the
same way they'd talk to any MCP-compliant tool.
Guardrails: read-only SQL enforcement lives server-side in
sql_tool_server.py (blocks DROP/DELETE/UPDATE/INSERT/ALTER, rejects
multi-statement SQL) — and is proven to work independent of the LLM's
behavior via tests/test_sql_guardrail.py (7/7 malicious queries
blocked, 3/3 safe queries pass), not just by an adversarial test
question that the model might happen to dodge on its own.
Memory: app/memory.py — ChromaDB-backed retrieval of 5 business-term
definitions (revenue, profit, discount, region, order), filtered by a
distance threshold so irrelevant questions get no injected context.
Concrete payoff: this dataset has no cost data, so the SQL Agent can
correctly decline profit questions instead of guessing.
Observability: app/observability.py — every run's full state is
logged to outputs/traces/*.json (always works, no external account).
Langfuse is wired too, centrally in get_llm(), but only activates if
credentials are set in .env — never required to run the project.
Eval harness: tests/eval_harness.py checks numeric results against
ground truth computed independently with pandas (not the agent's own
output), plus a row-count consistency check that scans the Interpreter's
answers for stated counts and flags mismatches against the real data.
Related MCP server: mcp-sql
Build order
✅ Domain + sample dataset (
data/sales.csv) + fixed test queries✅ State schema + baseline graph
✅ MCP
sql_toolserver (DuckDB, read-only enforced)✅ Guardrail proven (
tests/test_sql_guardrail.py, LLM-bypassing)✅ Self-correction retry loop (bad SQL → re-plan → re-execute)
✅ Python analysis tool (MCP-wrapped) for non-SQL-expressible questions
✅ Chart tool (MCP-wrapped) + visualization logic
✅ Memory (ChromaDB for business-term RAG) + observability (Langfuse)
✅ Eval harness
✅ UI (Streamlit)
Setup
Two terminals — Ollama needs to stay running while anything else runs.
Terminal 1 (leave running):
ollama serveTerminal 2:
pip install -r requirements.txt
cp .env.example .env
ollama pull llama3.2:3b # only needed oncePinned to
mcp<2.0.0— MCP 2.x renamedFastMCPtoMCPServerand broke this project's server code. If you seeModuleNotFoundError: No module named 'mcp.server.fastmcp', runpip install "mcp<2.0.0" --force-reinstall.
Running it
CLI — runs the fixed test queries and prints full trace/debug info:
python -m app.mainUI — chat-style interface, example questions in the sidebar, charts rendered inline, an expandable panel per answer showing the SQL/Python used, business context, and full agent trace:
streamlit run streamlit_app.pyGuardrail proof — bypasses the LLM entirely, sends known-malicious SQL straight to the MCP tool:
python -m tests.test_sql_guardrailEval harness — ground-truth + row-count consistency checks:
python -m tests.eval_harnessSet DEBUG_MEMORY=1 before any of the above to print ChromaDB's actual
retrieval distances per question (useful for tuning _MAX_DISTANCE in
app/memory.py).
Fixed test queries
Simple aggregation — total revenue
Group + filter — sales by region, Electronics only
Correlation — discount vs quantity (routes to Python Agent)
Adversarial — SQL-injection-style instruction embedded in the question
Nonsense / no-match — question the dataset can't answer
SQL-error-prone — tests the retry loop directly
Chart request — grouped data, should produce a real bar chart
Chart on a shape that may or may not reduce to a single aggregate
Business-term memory — profit margin (not computable from this data)
Project structure
data/
sales.csv # sample dataset used by all test queries
app/
state.py # shared GraphState
llm.py # centralized LLM client (Ollama default) + Langfuse wiring point
mcp_client.py # generalized MCP client (works with any server script)
memory.py # ChromaDB business-term retrieval
observability.py # local trace logging + optional Langfuse handler
utils.py # shared helpers (e.g. mentioned_columns)
graph.py # LangGraph wiring
main.py # CLI entrypoint, runs fixed test queries
agents/
planner.py # routing + memory retrieval
sql_agent.py # calls MCP sql_tool
python_agent.py # calls MCP python_tool (correlation questions)
validator.py
chart_agent.py # calls MCP chart_tool
interpreter.py
mcp_servers/
sql_tool_server.py # FastMCP server, DuckDB-backed, read-only enforced
python_tool_server.py # FastMCP server, pandas-backed, fixed safe stats only
chart_tool_server.py # FastMCP server, matplotlib-backed, fixed bar chart only
tests/
test_sql_guardrail.py # bypasses the LLM, proves the read-only guardrail directly
eval_harness.py # benchmark: ground-truth checks + row-count consistency check
streamlit_app.py # UI entrypointGenerated charts are written to outputs/ and run traces to
outputs/traces/ (both gitignored — regenerate by running the app).
ChromaDB's local data lives in chroma_data/ (also gitignored,
regenerated automatically on first run).
Known limitations (honest)
Small-model self-correction is limited. With
llama3.2:3b, a genuinely malformed query sometimes gets retried with the identical broken SQL rather than a fix — the retry cap still kicks in and the system fails honestly rather than hallucinating, but it doesn't always self-correct. A larger model would likely do better; this is a model-capability limit, not a guardrail or architecture gap.No arbitrary Python code execution. By design — a real sandboxed
run_python(code, df)tool with Docker isolation is future work. exec()-ing LLM-generated code without a sandbox in place was deliberately avoided rather than "temporarily" allowed.Routing between SQL and Python is keyword-based, not learned. Deliberately conservative (SQL is the default; only a correlation keyword match routes to Python) after the LLM's own routing judgment proved unreliable twice in testing — once nearly skipping SQL entirely with no fallback. A broader set of "SQL can't express this well" cases (regression, forecasting) isn't covered.
Chart Agent and Validator assume this dataset's specific shape. Chart Agent only produces one shape (category + numeric value → bar chart); the Validator's checks are written against this table's structure. Generalizing to arbitrary uploaded datasets would need both to reason about shape/schema dynamically.
Python Agent's column discovery is dynamic (fixed from an earlier hardcoded list) — fetched from the SQL tool's schema at runtime, filtered to numeric types, so a different dataset's numeric columns work without editing the file.
The Interpreter's narration isn't fully fact-checked against the data. Seen once in testing: a correct 20-row SQL result got summarized as "13 unique orders." Partly addressed by
tests/eval_harness.py's row-count consistency check, which hasn't yet caught a live violation on this benchmark — only proven to not false-positive.Business-term memory covers 5 fixed, hand-written terms. Adding new ones means editing
_SEED_TERMSinapp/memory.pydirectly. Retrieval distance threshold (_MAX_DISTANCE) is an empirical starting point, confirmed reasonable viaDEBUG_MEMORY=1runs but not rigorously tuned.Langfuse integration is wired but not verified against real credentials — only local trace logging has been confirmed working.
This is a single-dataset prototype, not a general data-analyst tool. It demonstrates the architecture pattern — multi-agent orchestration, MCP tool boundaries, guardrails proven independent of LLM behavior, retry/self-correction, memory, eval — on one fixed CSV. Supporting arbitrary uploaded datasets (the original project plan's Phase 5) would need the points above addressed, not just more test questions against this one.
Setup notes for the UI specifically
The Streamlit app is a thin layer over the same build_graph() used by
the CLI — no separate agent logic. Each question still spawns fresh MCP
subprocess calls per tool use (same as the CLI), so expect the same
per-query latency you'd see running python -m app.main, just rendered
in a browser instead of a terminal.
This server cannot be deployed
Maintenance
Related MCP Connectors
Query, join, profile, clean and convert CSV/JSON/Parquet with server-side DuckDB over MCP.
Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.
Query your org's data in natural language — read-only MCP access to SQL, NoSQL, files & warehouses.
Query your warehouse or a CSV with Claude/ChatGPT over MCP, governed by table-level ACL + audit.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables DuckDB database interaction through MCP, supporting SQL queries, table creation, and schema inspection with optional read-only mode.1MIT
- AlicenseNot gradedqualityDmaintenanceAn extensible read-only MCP server for SQL databases, enabling schema exploration and safe SELECT queries via tools like list_schemas, list_tables, describe_table, and execute_query.MIT
- AlicenseNot gradedqualityDmaintenanceA read-only MCP server that enables LLMs to safely explore and query any SQLite database via natural language. It exposes tools for listing tables, describing schemas, and executing SELECT/WITH queries with built-in safety guards like write prevention and row limits.MIT
- AlicenseNot gradedqualityCmaintenanceRead-only MCP server for SQL databases (SQLite/PostgreSQL) that enables listing tables, describing schemas, and executing SELECT queries with safety guardrails.MIT