NL-to-SQL MCP

NL-to-SQL MCP

MCP server enabling natural-language querying of SQLite databases via schema discovery, GraphRAG retrieval, and safely guarded read-only SQL execution.

Category
访问服务器

README

NL-to-SQL — MCP + GraphRAG

Natural-language querying over a SQLite database (the Chinook music-store sample: artists, albums, tracks, genres, customers, invoices, employees, playlists — 11 tables, 11 FK relationships), built around two independent guardrail layers, a schema-graph retrieval step, and an LLM-driven generation pipeline.

Two ways to use this

This repo is actually two separate products sharing the same guardrail and retrieval code:

  1. MCP server (server/main.py) — exposes list_schemas, get_table_metadata, retrieve_relevant_schema, and execute_safe_query as tools for any MCP client (Claude Desktop, Claude Code). In this mode, the connected LLM does its own reasoning about which tables/SQL to use — our code only provides schema discovery, GraphRAG-assisted retrieval, and guardrailed execution. No OpenAI calls happen in this path.

  2. Standalone pipeline (pipeline/pipeline.py) — a fully self-contained 5-stage NL-to-SQL system (Plan → Retrieve → Generate → Validate → Execute) that uses the OpenAI API directly. This is what the web app and CLI script call; Claude Desktop is not involved at all in this path.

These don't talk to each other — pick whichever fits how you want to interact with the database.

Architecture

                    ┌─────────────────────────────────────┐
                    │         Schema Graph (NetworkX)      │
                    │  tables + columns as nodes,          │
                    │  FKs as edges — built once,          │
                    │  persisted to GraphML                │
                    └───────────────┬───────────────────────┘
                                    │
              ┌─────────────────────┼─────────────────────┐
              │                     │                     │
   ┌──────────▼─────────┐  ┌────────▼────────┐  ┌─────────▼──────────┐
   │     MCP Server      │  │  Pipeline (CLI)  │  │   Web App (FastAPI) │
   │  4 tools, stdio      │  │  scripts/        │  │  web/app.py          │
   │  Claude Desktop/Code │  │  run_pipeline.py │  │  + static HTML/JS    │
   └──────────┬───────────┘  └────────┬─────────┘  └─────────┬────────────┘
              │                        └──────────┬───────────┘
              │                                    │
              │                          pipeline/pipeline.py
              │                    Plan → Retrieve → Generate → Validate → Execute
              │                                    │
              └──────────────────┬─────────────────┘
                                   │
                      ┌────────────▼────────────┐
                      │   Guardrail layer         │
                      │  sql_guard.py (AST allow-  │
                      │  list) + read-only conn +  │
                      │  timeout + row cap         │
                      └────────────┬────────────┘
                                   │
                            db/chinook.db (SQLite)

Guardrails

Security is layered, not a single check:

  • AST-based SQL validation (server/sql_guard.py) — allowlist, not keyword blocklist. Parses generated SQL with sqlglot; only accepts a single SELECT/UNION/INTERSECT/EXCEPT statement, every table reference must exist in the live schema (CTE aliases correctly excluded), load_extension/pragma_* calls blocked, and a LIMIT is always injected/clamped to 1000 rows by rewriting the AST — never trusting whatever the caller or LLM wrote.
  • Read-only connection (server/query_executor.py) — SQLite opened via mode=ro URI, an independent backstop at the driver level even if a write somehow passed AST validation.
  • Timeout — a progress-handler wall-clock timeout aborts long-running scans instead of blocking.
  • Plan-stage scope filtering (pipeline/plan_stage.py) — an LLM call classifies whether a question is in-scope before any SQL generation is attempted. This is a quality/cost filter, not the security boundary — the AST validator is what actually stops unsafe SQL regardless of what Plan decides.

Multi-database support

The web app isn't limited to the bundled Chinook demo. Switch to "Upload your own" and provide either:

  • a SQLite .db file — used as-is, no conversion
  • a MySQL or Postgres SQL dump (.sql) — transpiled to SQLite statement-by-statement via sqlglot (web/db_import.py) and imported into a fresh, isolated per-session database

Each upload gets its own session (web/sessions.py): its own SQLite file, its own schema graph, expiring after 24h, never touching the shared demo DB or another session's data. You must supply your own OpenAI API key to query an uploaded database — it's used only for that session's requests and never persisted to disk; the bundled demo continues to use the server's own key from .env, unaffected.

Import is deliberately best-effort, not all-or-nothing: dump syntax with no SQLite equivalent (MySQL's inline KEY/INDEX clauses, Postgres's CREATE SEQUENCE/CREATE EXTENSION, session-config SET statements, schema qualifiers like public.customers which SQLite would otherwise misparse as a cross-database reference) is detected and skipped, with a report of what was skipped — not an opaque failure over one unsupported statement. Capped at 20MB per upload; larger/async imports are a v2 concern.

Setup

python -m venv .venv
.venv/Scripts/pip install -r requirements.txt      # Windows
# .venv/bin/pip install -r requirements.txt         # macOS/Linux

cp .env.example .env      # then edit .env with your real OPENAI_API_KEY

python scripts/build_graph.py    # build graphrag/schema_graph.graphml from db/chinook.db

Running it

MCP server (for Claude Desktop/Code — see .mcp.json, already configured for this project):

python server/main.py

Web app:

uvicorn web.app:app --reload
# open http://127.0.0.1:8000

CLI:

python scripts/run_pipeline.py "Which artist has the most albums?"
python scripts/query_graph.py "your question"     # GraphRAG retrieval only, no LLM

Testing

# Free, deterministic (no API calls):
python tests/test_sql_guard.py              # 21 adversarial/legit SQL cases
python tests/test_query_executor.py         # timeout, read-only backstop, row cap
python tests/test_retrieval.py              # GraphRAG retrieval regression cases
python tests/test_pipeline_stages_offline.py

python tests/test_db_import.py              # SQLite/MySQL/Postgres import + transpilation

# Real API calls (needs OPENAI_API_KEY):
python tests/test_plan_stage.py
python tests/test_generate_stage.py
python eval/run_eval.py                     # full 40-question golden set

Eval harness

eval/run_eval.py grades on executed results, not SQL text — two differently-written queries can both be correct, so it compares the pipeline's output rows against a hand-verified reference query's output (value-subset matching, tolerant of extra descriptive columns and float precision). 30 legitimate questions (counts, sums, joins, self-joins, nullable-FK handling, literal lookups) plus 10 adversarial (out-of-scope, prompt injection, destructive intent) — currently 40/40.

The first real eval run caught two genuine bugs (not eval-harness artifacts): Plan being too conservative about a self-referencing FK relationship it had no schema access to verify, and Generate grouping by a non-unique display column (Playlist.Name — two different playlists share that name in Chinook) instead of the primary key, silently merging distinct rows. Both are fixed in the current prompts.

Wired into .github/workflows/eval.yml: deterministic tests run first (fail fast, free), then the eval harness (needs an OPENAI_API_KEY repo secret), with the report uploaded as a CI artifact.

Deployment

docker build -t nl2sql-mcp-graphrag .
docker run -p 8000:8000 --env-file .env nl2sql-mcp-graphrag

render.yaml is a Render blueprint (Docker runtime, free tier) — set OPENAI_API_KEY in the Render dashboard after connecting the repo, it's intentionally not committed. Note: the Docker build hasn't been verified in this environment (no Docker available) — test docker build . locally before deploying.

Known limitations / v2

  • No multi-tenancy — no row-level access control; deferred deliberately to keep v1 scoped.
  • No schema-drift detection — scripts/build_graph.py must be re-run manually after a schema change; no polling/webhook invalidation.
  • Lexical/fuzzy schema matching, not embeddings — free and deterministic, but can't bridge true vocabulary gaps beyond the small synonym map in graphrag/matching.py (e.g. "revenue" → Invoice.Total is hardcoded, not learned). Swap-in point is match_schema_nodes() if eval ever shows this as a bottleneck.
  • Retrieval is high-recall, not high-precision — an FK column like Invoice.CustomerId legitimately contains the word "customer", so simple questions can pull in more tables than strictly needed. Generate has so far proven robust to this noise (see Phase 4 testing), but it's a known tradeoff, not a solved problem.
  • Dump transpilation isn't guaranteed complete — best-effort, with a skip report for unsupported syntax (stored procedures, triggers, engine-specific types beyond what's already handled). Complex enterprise dumps may import partially. No background/async import, so upload is capped at 20MB and blocks the request until done.
  • No accounts — sessions are anonymous and expire after 24h; there's no way to return to an uploaded database later or share it across devices.

Project structure

db/            chinook.db (SQLite sample data)
server/        MCP server + AST guardrails (sql_guard.py, query_executor.py)
graphrag/      NetworkX schema graph, fuzzy matching, retrieval
pipeline/      5-stage LLM pipeline (plan/retrieve/generate/validate/execute)
web/           FastAPI app + static frontend + multi-DB upload/import/sessions
eval/          golden question set, grader, eval runner
scripts/       CLI entrypoints (build_graph, query_graph, run_pipeline, inspect_schema)
tests/         deterministic test suites

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选