AskData MCP Server
Allows using a local Ollama model as an optional LLM planner for semantic query generation, enabling offline LLM-powered planning without API keys.
Allows using OpenAI models as an optional LLM planner to translate natural language questions into a governed SemanticQuery JSON, with fallback to the rule-based planner on invalid output or errors.
Provides read-only access to a SQLite sales database, executing deterministic SQL compiled from semantic queries with guardrails and bound parameters.
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., "@AskData MCP ServerWhat were the top 3 cities by revenue in 2025?"
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.
AskData: a semantic-layer text-to-SQL agent with guardrails, evals and an MCP server
Ask a sales database questions in plain English ("Top 3 cities by revenue in 2025") and get back an answer, the SQL that produced it, a chart and a step-by-step trace. It runs fully offline with no API key. An LLM planner (OpenAI or a local Ollama model) is optional.
I built this as a final-year B.Tech (AI & ML) project to learn how "AI for analytics" works in practice: how to stop an LLM from writing SQL that is plausible but wrong, how to keep an agent read-only, and how to measure all of it with evals instead of eyeballing outputs.

The problem
Text-to-SQL demos look great until you check the numbers. A model that writes raw SQL against raw tables can
silently use the wrong definition of revenue (cancelled orders? discounts?), make up columns, or run a
DELETE. Recent write-ups (LinkedIn Engineering's SQL Bot, dbt's 2026 semantic-layer benchmark) point the
same way: let the model choose governed metrics and dimensions, and let deterministic code write the SQL.
AskData is a small, end-to-end version of that idea:
Semantic layer (
askdata/semantic_layer.yaml): 6 governed metrics (revenue, orders, AOV, units, unique customers, return rate) and 8 dimensions, each defined once.Planner: turns the question into a small JSON
SemanticQuery(names only, never SQL). The default is an offline rule-based planner. An LLM planner can be switched on with an env var.Compiler: deterministic
SemanticQuery → SQL, with values passed as bound parameters.Guardrails in three layers: a write-intent check before any planner, a static SQL checker, and a read-only SQLite connection with an authorizer callback.
Evals: 47 golden questions with hand-written gold SQL, plus a 20-query SQL attack suite. CI runs them on every push.
MCP server: exposes the agent as tools (
ask,run_sql,list_metrics,describe_schema) for any MCP client.
Related MCP server: mcp-analytics
Architecture
flowchart LR
U["User question"] --> I{"Write-intent check"}
I -- "delete / drop / update" --> R["Refuse"]
I -- "read-only" --> P["Planner: rule-based (default)<br/>or LLM via OpenAI / Ollama"]
SL[("semantic_layer.yaml<br/>metrics + dimensions")] --> P
P -- "SemanticQuery JSON, names only" --> V["Validate against semantic layer"]
V -- "invalid" --> F["Fallback to rule-based or refuse"]
V --> C["Deterministic compiler<br/>SQL + bound params"]
C --> G["Static SQL guardrail<br/>SELECT-only, table allowlist"]
G --> X[("SQLite read-only<br/>mode=ro + authorizer + row cap")]
X --> S["Template summarizer<br/>no invented numbers"]
S --> O["Answer + SQL + table + chart + trace"]
O --> CLI["CLI"]
O --> ST["Streamlit app"]
O --> MCP["MCP server"]
E["Eval harness<br/>47 golden Qs + 20 SQL attacks"] -. "runs in CI" .-> PFeatures
Governed metrics. Revenue always means net of discount, completed orders only. The definition lives in one YAML file.
No SQL from the model. The planner outputs JSON (
metrics,dimensions,filters, dates,order_by,limit), and code writes the SQL.Defense-in-depth guardrails. Write intent is refused before planning. The static checker blocks multiple statements, DDL/DML,
ATTACH,PRAGMA,load_extension, system tables and unknown tables. The SQLite connection is openedmode=rowithPRAGMA query_onlyand an authorizer that denies anything except reads. Results are capped at 200 rows.Injection-safe filters. Filter values are bound parameters (there's a test that passes
Hyderabad' OR 1=1 --as a city name).Optional LLM, works offline. With
OPENAI_API_KEYorOPENAI_BASE_URLset, the LLM planner is used. Invalid LLM output (unknown metric, bad JSON, network error) falls back to the rule-based planner, and the trace records why.Observability. Every answer carries a step trace (
intent_check → plan → compile → guardrail → execute → summarize) with timings.Eval harness. Execution accuracy against hand-written gold SQL, split into dev (used while building) and holdout (written up front, never tuned on), plus refusal accuracy, a guardrail attack suite and latency percentiles.
Three interfaces. CLI, Streamlit app and MCP server, all built on the same
AskDataAgent.
Quickstart
git clone https://github.com/Shibukedang27/askdata-semantic-agent.git
cd askdata-semantic-agent
python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
python -m askdata ask "Top 3 cities by revenue in 2025" # builds data/shop.db from the CSVs on first run
python -m askdata eval # golden evals + guardrail suite
python -m pytest # test suite
streamlit run app/streamlit_app.py # web demo
python -m askdata mcp # MCP server over stdioOptional: use an LLM planner
# OpenAI
export OPENAI_API_KEY=sk-... # never commit this; see .env.example
export ASKDATA_LLM_MODEL=gpt-4o-mini
# or a local model with Ollama (no key, nothing leaves your machine)
export OPENAI_BASE_URL=http://localhost:11434/v1
export ASKDATA_LLM_MODEL=qwen2.5:3bASKDATA_PLANNER=rule forces the offline planner (CI does this).
Use it from an MCP client
Add this to your MCP client config (Claude Desktop, Cursor, VS Code and others):
{
"mcpServers": {
"askdata": {
"command": "/path/to/askdata-semantic-agent/.venv/bin/python",
"args": ["-m", "askdata.mcp_server"],
"cwd": "/path/to/askdata-semantic-agent"
}
}
}Tools: ask(question), run_sql(sql) (guarded), list_metrics(), describe_schema().
Demo output (real terminal output)
$ python -m askdata ask "Top 3 cities by revenue in 2025"
Q: Top 3 cities by revenue in 2025
Status: ok | planner: rule-based | 9.2 ms
A: Top 3 by revenue (city) (2025-01-01 to 2025-12-31): Hyderabad (₹3,857,858), Bengaluru (₹3,070,323), Mumbai (₹2,018,628).
SQL:
SELECT c.city AS city, SUM(CASE WHEN o.status = 'completed' THEN oi.quantity * oi.unit_price * (1 - oi.discount) ELSE 0 END) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.order_date >= '2025-01-01' AND o.order_date <= '2025-12-31'
GROUP BY c.city
ORDER BY revenue DESC
LIMIT 3
city revenue
Hyderabad 3857857.65
Bengaluru 3070323.35
Mumbai 2018627.75
Trace: intent_check 0.09ms -> plan 3.43ms -> compile 0.01ms -> guardrail 0.05ms -> execute 4.97ms -> summarize 0.61ms
$ python -m askdata sql "SELECT * FROM sqlite_master"
Q: SELECT * FROM sqlite_master
Status: refused | planner: raw-sql | 0.0 ms
A: Blocked by guardrail: system tables are not allowed
$ python -m askdata eval
planner: rule-based
dev execution accuracy: 28/28 = 100.0%
holdout execution accuracy: 13/19 = 68.4%
by category: simple 1/1, time_filter 3/3, group_by 7/7, time_series 6/6, top_n 8/8, filter 8/9, multi_metric 1/1, refusal 7/7, paraphrase 0/5
latency per question: p50 3.92 ms, p95 8.39 ms
failed: h07, h10, h11, h12, h13, h15
guardrails: 15/15 attacks blocked, 5/5 benign queries allowed; DB layer alone blocks 14/15Top-N question | Write request refused |
|
|
Results (measured, python -m askdata eval)
All numbers come from running this repo on my machine (16 Oct 2026). Raw per-question outputs are in
evals/results.json (rule-based) and
evals/results_llm_qwen2.5-3b.json (local LLM).
Execution accuracy means the agent's result rows match the result of the hand-written gold SQL (floats rounded to 2 decimals; row order checked only for top-N and time-series questions).
Planner | Dev (28) | Holdout (19) | Refusals (7) | Latency p50 / p95 |
Rule-based (offline, default) | 28/28 (100%) | 13/19 (68.4%) | 7/7 | 3.8 / 8.0 ms |
LLM: Qwen2.5-3B-Instruct Q4 via Ollama, CPU only | 17/28 (60.7%) | 5/19 (26.3%) | 6/7 | 8.2 / 11.0 s |
The LLM row was measured on an 8-vCPU machine with no GPU, using the prompt in askdata/planners/llm.py as-is (no prompt
tuning on the eval set). In that run, 2 of the 47 questions fell back to the rule planner because the LLM's JSON failed validation,
and "Delete all orders from 2024" was refused by the intent check before the LLM was called.
Guardrail suite | Result |
Attack queries blocked (all layers) | 15/15 |
Benign queries allowed (e.g. | 5/5 |
Attacks blocked by the read-only DB layer alone | 14/15 (the one it lets through is |

(Latency numbers move by a millisecond or two between runs. The table uses evals/results.json.)
What I learned from the numbers
Dev at 100% doesn't mean the system is good. I iterated on the dev questions, so they overfit by design. The holdout set (written before the planner, never used to tune it) shows the real picture: 68%. All 5 paraphrase questions fail ("make the most money", "share of orders got returned", "how many books did students buy"). "Revenue per weekday" is worse: there's no weekday dimension, so the rule planner silently drops it and returns the total instead of refusing.
A small local LLM was not a drop-in fix. Qwen2.5-3B on CPU was about 2,000× slower (8.2 s vs 3.8 ms median) and less accurate than the rule planner on this set. Most failures were about the shape of the query: grouping by a dimension it was only meant to filter on, using "Q4" as a filter value, or adding extra columns. Without the semantic layer, those mistakes would have been invented SQL. With it, they are at least valid, inspectable queries.
Evals caught a safety bug. On the first LLM run, the model answered "Delete all orders from 2024" with an order count instead of refusing. The DB was read-only so nothing happened, but I moved the write-intent check before every planner and added a regression test (
test_write_intent_refused_before_llm_is_called). The same run also surfaced a crash onnullfilter values from the LLM, which is now handled inSemanticQuery.from_dict.
Dataset
data/*.csv is a synthetic retail dataset made by askdata/data_gen.py (seed 27): 800 customers in 6 Indian
cities, 32 products in 5 categories, 6,107 orders and 9,136 order lines from Jan 2024 to Dec 2025. It has
festive-season seasonality and discounts. None of it is real company or customer data.
python -m askdata build-db regenerates it.
Project structure
askdata/
semantic_layer.yaml # governed metrics + dimensions
semantic.py # SemanticModel, SemanticQuery (+ validation of untrusted JSON)
planners/ # rule_based.py (offline), llm.py (OpenAI/Ollama), get_planner()
compiler.py # SemanticQuery -> parameterised SQL
guardrails.py # static SQL checks
executor.py # read-only SQLite + authorizer + row cap
agent.py # orchestration + trace
summarize.py # template answers
evals.py # golden + guardrail evals
mcp_server.py # MCP tools
cli.py
app/streamlit_app.py # demo UI
evals/ # golden.yaml, sql_attacks.yaml, results*.json
scripts/ # make_charts.py, screenshots.py
tests/ # pytest suite (69 tests)Tech stack
Python 3.11+, SQLite, pandas, PyYAML, the MCP Python SDK (v2), the OpenAI Python SDK (optional, also used for Ollama's OpenAI-compatible API), Streamlit, matplotlib, pytest, Playwright (screenshots only) and GitHub Actions.
Testing and CI
python -m pytest runs 69 tests covering guardrails (every attack in the suite), the read-only executor,
compiler and validation, the rule planner, the LLM planner (with a fake client, so no network), the agent
(totals cross-checked against an independent pandas calculation from the CSVs), the MCP tools, the CLI
and data generation. The GitHub Actions workflow runs the tests on Python 3.11 and 3.12, then an eval
gate: the build fails if dev-split accuracy drops below 100% or any refusal case regresses.
Limitations and future work
Paraphrases. The rule planner is keyword-based. Next I want a hybrid router: rule planner first, LLM only when it can't map a metric, plus a better prompt with few-shot examples, measured on the same holdout.
Unknown dimensions should refuse. "Revenue per weekday" should say "weekday isn't a dimension" instead of answering something else.
Bigger and stronger models. Re-run the LLM column with a hosted model and with larger local models, and report cost per question.
Period-over-period metrics (growth %, YoY) in the semantic layer.
OpenTelemetry export of the trace, so runs can be viewed in Langfuse, Phoenix and similar tools.
More data backends (DuckDB, Postgres) behind the same compiler.
License
MIT, see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Read-only Google Analytics 4 MCP server for AI assistants.
Governed data discovery, exact queries, decisions, simulations, and runtime utilities over MCP.
The BigQuery remote MCP server is a fully managed service that uses the Model Context Protocol to connect AI applications and LLMs to BigQuery data sources. It provides secure, standardized tools for AI agents to list datasets and tables, retrieve schemas, generate and execute SQL queries through natural language, and analyze data—enabling direct access to enterprise analytics data without requiring manual SQL coding.
Read-only Google Search Console MCP server for AI assistants.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables querying Google Analytics 4 properties using natural language through MCP clients. Supports customizable reports with any dimensions and metrics, listing properties, and real-time data.4MIT
- FlicenseNot gradedqualityCmaintenanceExposes product analytics metrics from an internal data warehouse via MCP, enabling agents to query metrics, product summaries, and event breakdowns.-
- AlicenseNot gradedqualityAmaintenanceEnables MCP clients to serve and query semantic models, providing tools for entity descriptions, metric lookups, context resolution, and operation validation for AI agents.MIT
- AlicenseAqualityAmaintenanceEnables MCP clients to query Google Analytics 4 reporting and configuration data through 27 read-only tools, including pivots, funnels, realtime reports, Admin API inspection, and diagnostics, with identifiers redacted by default.2762 npmApache 2.0

