TextToSQL MCP Server

TextToSQL MCP Server

A read-only, schema-aware MCP server that connects to SQL databases and provides tools for refining questions, browsing schema, validating, explaining, and running SELECT statements with safety guards.

Category
访问服务器

README

TextToSQL MCP Server

A read-only, schema-aware TextToSQL MCP server. It fronts a SQL warehouse (PostgreSQL or SQLite) and exposes six tools any MCP-capable host or agent can drive: refine a question, browse curated schema knowledge, validate, explain, and run SELECT statements — with independent read-only safety layers in between. Schema knowledge lives as skills-as-markdown: generated drafts you curate once, then freeze. The server ships not yet configured; a bundled SQLite demo database lets you run the full lifecycle end-to-end in about five minutes.

5-minute demo quickstart

The repo bundles the SQL Murder Mystery dataset (demo/murder_mystery.db, 9 tables, ~57k rows — MIT licensed, see THIRD_PARTY_LICENSES.md).

uv sync
cp .env.demo .env                       # SQLite DSN, INTROSPECTION=true
ollama pull gpt-oss:120b                # or set LLM_FALLBACK_API_KEY (see LLM configuration)
uv run python scripts/verify_role.py    # confirms the read-only guard is active
uv run python -m texttosql_mcp.run_http --port 8765

In a second terminal:

curl -s http://localhost:8765/health | python -m json.tool
curl -s -X POST http://localhost:8765/admin/regenerate_skill | python -m json.tool
#   -> 9 tables discovered, 6 FKs; skill drafts under skills/; baseline fixtures written
uv run --extra dev pytest tests/test_fixtures.py -v
#   -> EXPECTED: several fixtures FAIL against the raw drafts (integer dates,
#      lowercase enums, the ssn join). That failure is the point — the drafts
#      contain no semantic knowledge yet.

Now author the curated sections (see The three phases below — fill the <TODO> blocks in skills/ using POST /admin/probe_table samples), then:

uv run --extra dev pytest tests/test_fixtures.py -v   # all demo fixtures pass
# flip INTROSPECTION=false in .env, restart the server
uv run --extra dev pytest tests/test_fixtures.py -v   # still green; admin endpoints now 403
# connect any MCP-capable client/agent to http://localhost:8765/mcp

Architecture

An MCP host (any agent framework, an MCP inspector, or a plain script using the MCP SDK) connects over Streamable HTTP and drives the tool set; the server owns schema knowledge (skills), safety, and DB access. The only server-side LLM call is refine_question — everything else is deterministic.

┌─────────────────────────────────────────────────────────┐
│ MCP host / agent (any MCP-capable client)               │
└──────────────────────────┬──────────────────────────────┘
                           │ Streamable HTTP  POST /mcp/
┌──────────────────────────▼──────────────────────────────┐
│ TextToSQL MCP server (FastAPI + FastMCP)                 │
│                                                          │
│  refine_question ──► LLM (translate + vocabulary map)    │
│  list_tables / get_table_schema ──► skills/*.md (cached) │
│  validate_query ──► sqlglot AST guard                    │
│  explain_query ──► EXPLAIN guard (cost / plan hints)     │
│  run_query ──► validate + explain + execute (row-capped) │
│  introspect_schema ──► Phase 1 only (INTROSPECTION=true) │
│                                                          │
│  read-only connection (RO role / PRAGMA query_only)      │
└──────────────────────────┬──────────────────────────────┘
                           │ async SQLAlchemy
                ┌──────────▼──────────┐
                │ PostgreSQL / SQLite │
                └─────────────────────┘

The three phases

Phase 1 — bootstrap skills (INTROSPECTION=true). POST /admin/regenerate_skill sweeps the schema (tables, columns, PKs, FKs, indexes, low-cardinality enums, sample rows) and renders draft skill files: skills/sql_schema.skill.md (master) plus skills/tables/<table>.md. Drafts auto-fill everything mechanical and leave <TODO> markers in the semantic sections. A baseline fixture file (row counts per table) is also written.

Phase 2 — curate and iterate. Fill the curated sections — the master's OVERVIEW, VOCABULARY, CROSS_TABLE_EXAMPLES and PITFALLS, and each table's PURPOSE, EXAMPLES, PITFALLS — using POST /admin/probe_table?name=<table> to inspect real rows. Iterate with the NL→SQL fixture harness (pytest tests/test_fixtures.py): each fixture asks a question, lets the LLM draft SQL against your skills, runs it, and asserts tables used, clauses, values, and row counts. Re-runs of Phase 1 preserve curated sections (merge_preserving_curated).

Phase 3 — freeze. Set INTROSPECTION=false and restart. The introspection tool and admin endpoints disappear (tools/list shows exactly six tools; admin returns 403). Skill files are the frozen contract; edits to them hot-reload on the next call (mtime cache), no restart needed.

Pointing at your own database

Configuration is env-only — no DSN, schema name, or threshold lives in source. Copy .env.example to .env and set:

Variable Meaning
DATABASE_URL Async SQLAlchemy DSN (required)
SCHEMA_NAME Schema to introspect/query (required; SQLite: main)
INTROSPECTION true during Phases 1–2, false in production
INTROSPECTION_TABLE_PREFIXES CSV of table-name prefixes to limit scope (empty = all)
ALLOWED_QUERY_SCHEMAS Extra schemas SELECTs may reference (empty = SCHEMA_NAME only)
SAMPLE_ROW_LIMIT Sample rows per table in drafts
SKILL_DIR / FIXTURE_DIR Where skills / fixtures live
EXPLAIN_WARN_COST / EXPLAIN_HARD_COST Cost guard thresholds (PostgreSQL only)
DEV_DB_VERSION Bump to invalidate the harness result cache
MCP_HTTP_PORT HTTP port (default 8765)

PostgreSQL — use a read-only role:

CREATE ROLE warehouse_read LOGIN PASSWORD '...';
GRANT USAGE ON SCHEMA analytics TO warehouse_read;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO warehouse_read;
ALTER ROLE warehouse_read SET default_transaction_read_only = on;
ALTER ROLE warehouse_read SET statement_timeout = '30s';
DATABASE_URL=postgresql+psycopg://warehouse_read:...@host:5432/db
SCHEMA_NAME=analytics

SQLite — point at any .db file; every connection gets PRAGMA query_only=ON, so writes are refused at the connection level:

DATABASE_URL=sqlite+aiosqlite:///./path/to/your.db
SCHEMA_NAME=main

Bring your own CSVs — build a demo DB from flat files (stdlib only):

python scripts/csv_to_sqlite.py my.db --csv orders=./orders.csv --csv customers=./customers.csv

PKs/FKs are not inferred; add them by hand if you want the JOINS section of the master skill populated (convention-only joins work too — the renderer handles the no-FK case).

Run uv run python scripts/verify_role.py after any connection change — it fails loudly if the connection can write.

LLM configuration

The LLM drives the refine_question tool and the pytest harness; the other five tools never call one. Any OpenAI-compatible endpoint works.

# Primary (default: local Ollama)
LLM_BASE_URL=http://localhost:11434/v1
LLM_MODEL=gpt-oss:120b            # or gpt-oss:20b on smaller machines
LLM_API_KEY=ignored

# Automatic fallback when the primary is unreachable (probe once per process).
# Enabled by setting the key; default target is OpenRouter's automatic router.
LLM_FALLBACK_BASE_URL=https://openrouter.ai/api/v1
LLM_FALLBACK_MODEL=openrouter/auto
LLM_FALLBACK_API_KEY=
LLM_PROBE_TIMEOUT=2

On first LLM use, the server probes GET {LLM_BASE_URL}/models; if unreachable and a fallback key is set, all calls transparently use the fallback (a warning is logged once). GET /health surfaces the resolved endpoint and fallback_active so a dead primary is a one-line diagnosis, not a mid-run timeout.

Safety model

Three independent layers on PostgreSQL; two layers plus plan hints on SQLite:

  1. Read-only connection. PostgreSQL: RO role with default_transaction_read_only=on + statement_timeout. SQLite: PRAGMA query_only=ON on every pooled connection.
  2. AST validation (validate_query). sqlglot rejects anything that is not exactly one SELECT — including CTE-hidden writes — plus references to schemas outside the allowlist.
  3. EXPLAIN guard (explain_query). PostgreSQL: two-tier cost thresholds (warn / reject) with rewrite hints derived from the plan. SQLite: EXPLAIN QUERY PLAN has no cost estimates — the cost guard is a PostgreSQL feature; SQLite mode still catches invalid statements pre-run and degrades to plan-shape hints (full scans, temp B-trees, automatic indexes).

run_query re-runs layers 2–3 internally and enforces a server-side row cap (default 1,000, max 10,000) regardless of any LIMIT in the SQL.

Tracing (Langfuse)

Optional, off by default. Set LANGFUSE_ENABLED=true plus LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_BASE_URL to emit one span per MCP tool call, a generation span for refine_question, and harness spans per fixture. Clients that pass session_id into tool calls get their trace stitched with the server's spans under one session. When disabled, the SDK is never imported.

Tests

# Unit layer — no DB, no LLM required
uv run --extra dev pytest tests/test_config_env_only.py tests/test_safety.py \
    tests/test_tools.py tests/test_refine_question.py -v

# Fixture harness — needs the configured DB and a live LLM endpoint
uv run --extra dev pytest tests/test_fixtures.py -v

Fixture runs write reports to tests/reports/<ISO>/ (gitignored) and cache verified SQL results in tests/.db_cache/ keyed on (normalized_sql, db_fingerprint) — the LLM call is never cached.

Operational notes

  • Skill hot-reload: skill files are cached by mtime; edits are picked up on the next MCP call. POST /admin/skill_reload force-invalidates (Phase 1–2).
  • Phase 1 backups: re-running introspection backs up prior skills to skills/.bak-<ISO>/ (retention INTROSPECTION_BACKUP_RETENTION).
  • Table menu is authoritative: a table missing from the master file's TABLES section is invisible to clients even if it exists in the DB.
  • Windows: the launcher (texttosql_mcp.run_http) pins a SelectorEventLoop, required by psycopg async.

Troubleshooting

Symptom Likely cause Fix
Tool calls return HTTP 500 "Task group is not initialized" Mounted app without the FastAPI lifespan Start via texttosql_mcp.run_http, not a bare uvicorn app:...
validate_query rejects with forbidden_schema SQL references a schema outside the allowlist Use unqualified/SCHEMA_NAME tables, or opt in via ALLOWED_QUERY_SCHEMAS
Every query rejected with high cost Thresholds too low for your warehouse Raise EXPLAIN_WARN_COST / EXPLAIN_HARD_COST (PostgreSQL only)
Accented/case-variant text doesn't match Case-sensitive = comparison Use case-insensitive matching (ILIKE on PostgreSQL, LIKE on SQLite)
refine_question returns the question unchanged LLM endpoint down and no fallback key Check GET /healthllm.fallback_active; set LLM_FALLBACK_API_KEY
Fixtures re-execute all SQL after a DB reload Fingerprint changed (expected) Bump DEV_DB_VERSION only when you want invalidation

Repository layout

src/texttosql_mcp/
  server.py           # FastMCP tool registration (six tools)
  server_http.py      # FastAPI app: /mcp mount, /health, /admin/* (Phase 1)
  server_stdio.py     # stdio transport alternative
  run_http.py         # launcher (Windows-safe event loop)
  config.py           # env-only Settings (pydantic-settings)
  db.py               # async engine + read-only guard + db_fingerprint
  llm.py              # ChatOpenAI factory with automatic fallback
  tools/              # refine_question, list_tables, get_table_schema,
                      # validate_query, explain_query, run_query, introspect_schema
  safety/             # sqlglot AST guard, EXPLAIN guard, plan hints
  introspection/      # schema sweep, skill renderer, fixture stub
  skill/              # mtime-cached loader + section parser
skills/               # curated knowledge (generated locally; gitignored)
tests/                # unit tests + NL→SQL fixture harness
demo/                 # bundled SQLite demo DB + dataset license
scripts/              # verify_role.py, csv_to_sqlite.py

License

MIT — see LICENSE. Bundled demo dataset attribution: THIRD_PARTY_LICENSES.md.

推荐服务器

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 模型以安全和受控的方式获取实时的网络信息。

官方
精选