MCP Database Connector + Agentic Retrieval
Provides tools to interact with a PostgreSQL database, enabling schema discovery and read-only queries with a dedicated read-only role.
Provides tools to interact with a SQLite database, enabling schema discovery and read-only queries.
Click on "Install 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., "@MCP Database Connector + Agentic RetrievalShow me the top 3 employees with the most open issues."
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.
MCP Database Connector + Agentic Retrieval
An LLM answers natural-language questions about a company database — but it can only reach that database through the Model Context Protocol (MCP). No connection strings in the agent, no arbitrary SQL execution, read-only by construction, and a plan → act → observe loop with real error recovery.
See docs/PLAN.md for the full architecture diagram and design decisions. This README covers setup, the connector flow, and the required conceptual write-up.
1. Quick start
# 1. Install dependencies
pip install -r requirements.txt
# 2. Build the dummy database (SQLite, 3 related tables, realistic data)
python db/init_db.py --force
# 3. Configure once: copy the example env file and put your key in it.
# Every entry point loads .env automatically — no per-shell exports needed.
copy .env.example .env # then edit .env: set OPENAI_API_KEY (or ANTHROPIC_API_KEY)
# 4. Run the full demo (4 scenarios) — or one at a time
python demos/demo.py
python demos/demo.py 2 # just the JOIN scenarioThe demo auto-detects which key is present (AGENT_PROVIDER=openai|anthropic
overrides). Both providers drive the same MCP server, prompt rules and
guardrails — agent/agent.py (Anthropic) and agent/agent_openai.py
(OpenAI) differ only in the LLM client, which is itself a demonstration of
the decoupling argument in §8.
Prefer a UI? Run the web application — a React (Vite) chat interface over a FastAPI backend, with a live agent-trace panel (every tool call, error and recovery visible) and the server-side audit log in the sidebar:
# The built React app (frontend/dist) is committed, so no Node is needed to run:
run.bat # one-click → http://localhost:8000
# ...or equivalently:
python -m uvicorn backend.api:api --port 8000 # open http://localhost:8000To rebuild the frontend after changing frontend/src (needs Node):
cd frontend; npm install; npm run build. Dev mode with hot reload:
cd frontend; npm run dev → http://localhost:5173 (proxies /api to :8000).
No API key yet? Run the offline demo — it drives the same MCP server through the same plan → act → observe steps with a scripted planner standing in for the LLM, so the whole architecture (schema discovery, JOIN, guardrail rejection, error recovery) is demonstrable with zero credentials:
python demos/demo_offline.pyYou can also ask ad-hoc questions:
python agent/agent.py "Who has the most open critical issues?"Related MCP server: MCP DB Server
2. What's in the box
Path | What it is |
| Builds |
| The MCP server. Exposes exactly 3 tools; holds the DB path; opens SQLite read-only |
| The read-only enforcement: SELECT-only allow-list, keyword block-list, single-statement check, hard row LIMIT |
| The agent (Anthropic/Claude): MCP client + tool-use loop (discover schema → plan SQL → execute → recover → answer) |
| Same agent, OpenAI driver — shares the prompt/rules from |
| 4 end-to-end LLM scenarios: simple, JOIN, clarification, error recovery |
| Same MCP round-trip with a scripted planner — runs with no API key |
| Interactive terminal chat: ask anything live; |
| React app (Vite): chat UI with live agent-trace panel, sample-question chips, audit-log sidebar, guardrail-test button — components in |
| FastAPI backend: |
| Engine abstraction: SQLite (default) or PostgreSQL ( |
| PostgreSQL variant: same data plus a SELECT-only |
| Tamper-proof server-side audit trail of every tool call (auto-created) |
| Example MCP host config — note credentials sit in the server's env block |
| Architecture diagram + design decisions |
3. The connector flow (step list)
User question
│
▼
Agent (agent/agent.py) ── holds ONLY the Anthropic API key
│ 1. connects to MCP server (subprocess, stdio JSON-RPC)
│ 2. discovers available tools dynamically (list_tools)
│ 3. Claude calls: list_tables ──────────────┐
│ 4. Claude calls: describe_schema(table) │ runtime schema discovery
│ 5. Claude plans SQL from what it just saw ──┘ (nothing hard-coded)
│ 6. Claude calls: run_query(sql)
│ 7. on error → error text returned as tool result → Claude repairs SQL → retry
│ 8. on ambiguity → ask_user tool → human answers → loop continues
▼
MCP server (mcp_server/server.py) ── holds the DB path (env: COMPANY_DB_PATH)
│ 9. sql_guard validates: SELECT-only, no blocked keywords, one statement,
│ row LIMIT clamped to ≤500
▼
SQLite opened as file:company.db?mode=ro ← engine-level read-only backstop
│ 10. rows (or a structured error) travel back up the same path
▼
Claude writes a natural-language answer grounded in the returned rowsEvery hop is observable: the demo prints each tool call, its arguments, and a preview of each observation, so you can watch the loop think.
4. How credentials stay out of the agent
The agent process (
agent/agent.py) contains no DB driver import, no connection string, no file path to the database. Its only secret isANTHROPIC_API_KEY. Grep it — there is nosqlite3in the agent.The MCP server process reads the DB location from its own environment (
COMPANY_DB_PATH, with a safe default). Inmcp_config.json, note that theenvblock belongs to the server entry: the MCP host passes it to the server subprocess, and it is never part of any message the LLM sees.The protocol itself only carries tool names, JSON arguments, and JSON results. Even a fully prompt-injected model cannot exfiltrate a credential that never enters its context window.
Swapping SQLite for Postgres would mean changing only the server: put the DSN in the server's env, keep the same three tools, and the agent code does not change at all. That decoupling is the point of the connector layer.
5. How read-only is enforced (defense in depth)
Safety lives at three layers, on purpose — the strongest answer is that no single layer is trusted alone:
Layer | Mechanism | What it catches | Why it's insufficient alone |
1. Prompt | System prompt: "READ-ONLY: only SELECT queries" | Keeps a well-behaved model from even trying | Prompts are guidance, not enforcement — models err and can be adversarially steered |
2. MCP tool (primary) |
| Any write, DDL, multi-statement injection, or unbounded scan — rejected before the DB sees it | A validator can have bugs |
3. Database engine | SQLite opened via | Any write that somehow survived layer 2 — the engine itself refuses | Doesn't bound read cost; that's layer 2's LIMIT clamp |
How MCP helps enforce this boundary: the guard lives in the server
process, on the other side of a process boundary from the LLM. The model
cannot patch, bypass, or "talk its way past" code it can't reach — its entire
universe of action is three named tools whose implementations we control.
Rejections come back as structured tool results ({"error": ..., "rejected_by": "sql_guard"}), which doubles as useful feedback for the agent's recovery loop.
The row/cost limit: every query is either wrapped as
SELECT * FROM (<query>) LIMIT n or has an oversized LIMIT clamped, with a
hard max of 500 rows, plus a connection-level timeout — so neither a
hallucinated SELECT * on a big table nor a pathological query can blow up
cost or latency.
Known trade-off (deliberate): the guard is keyword-based, not a full SQL
parser. To keep that honest it strips comments and blanks the contents of
string literals before matching, so a blocked word or a semicolon that is
merely data — e.g. WHERE title LIKE '%training set%' or = 'a;b' — is
not a false positive; only real SQL syntax is checked. The residual
trade-off is that a keyword-based check can still over-block an exotic query
(e.g. a blocked word used as an unquoted identifier) — never under-block,
so the failure mode stays safe: it can only reject, never let a write through.
The agent reads any rejection and rephrases automatically, the guard stays
~130 lines of plain Python anyone can review, and the DB-permission layer
(SELECT-only role) is the backstop. Swapping in an AST validator (e.g.
sqlglot) later would remove the residual over-blocking without touching any
other component.
6. Agentic depth — what to look for in the demo
Plan → act → observe: the agent always starts with
list_tables+describe_schema— the system prompt forbids guessing names, and neither the agent code nor the prompt contains any table/column name. Rename a column ininit_db.pyand rebuild: the agent adapts with zero code change.Error recovery (scenario 4): the demo feeds the agent a false claim that
issueshas aseveritycolumn and tells it to use that name unchecked. The first query fails with the database's no such column / column "severity" does not exist error (the exact wording depends on the engine); the error comes back as anis_errortool result; the agent re-inspects the schema, finds a real column (e.g.priority), and retries successfully. The trace prints the whole cycle.Clarification (scenario 3): "How many open issues are left on the AI project?" is deliberately ambiguous — two projects belong to the AI department (
Project PhoenixandProject Atlas), so the agent can't know which one is meant. It looks up the candidate projects, then uses itsask_usertool to ask one targeted question, and resumes with the answer.Multi-table JOIN (scenario 2): "Which AI-team members have open issues on Project Phoenix?" requires
employees ⋈ issues ⋈ projects— three tables, two foreign keys.Bounded loops: a hard
MAX_ITERATIONScap terminates a question that genuinely can't converge, instead of looping forever.
7. Judgment call: how much schema does the LLM see?
We chose on-demand introspection (list_tables → describe_schema)
over dumping the full schema into every prompt.
Cost of our choice: 1–2 extra tool round-trips per question.
What we get: token cost that scales with the question, not the database — a 300-table production schema would bloat every request if inlined, but costs nothing here until a table is actually relevant. It also makes the agent schema-change-resilient and keeps the agent code fully generic (it works unchanged on any database the server points at).
When we'd flip it: for a tiny stable schema and a high-QPS workload, inlining the schema once (with prompt caching) is cheaper. For a 3-table demo either works; we deliberately demonstrate the pattern that scales.
8. Required write-up: MCP as the connector layer between LLM and DB
What MCP is doing here. MCP standardizes how an LLM host discovers and
invokes external capabilities: the server declares typed tools
(list_tables, describe_schema, run_query), the client discovers them
at runtime, and every invocation is a structured JSON-RPC message. In this
project the protocol is the only bridge between the model and the data —
an abstraction layer in exactly the sense that a REST API is one between a
frontend and a database.
Why this beats handing the LLM a raw connection string:
Credential isolation. A connection string in the model's context can be leaked — echoed into a transcript, logged, or extracted by prompt injection. Here the secret lives in a different process, configured via that process's environment; there is no message type in the protocol that could carry it to the model.
A narrow, enumerable capability surface. With a raw connection the model can do anything the driver allows. With MCP it can do exactly three things, each with a typed input schema. That surface is small enough to review, test, and audit — "what can the model do to our database?" has a complete three-line answer.
Why this beats letting the LLM execute arbitrary SQL directly:
Enforcement outside the model's control. "Please only run SELECT" in a prompt is a request, not a control. The MCP server validates every query in server-side code and opens the database read-only at the engine level. A hallucinating or adversarially-prompted model still physically cannot write, because the enforcement point is on the other side of a process boundary it cannot reach.
Cost and blast-radius bounds. The server clamps every query to a row limit and a timeout — so one bad query can't dump a table or hang a connection pool.
Auditability and observability. Every access is a discrete tool call with a name and JSON arguments — trivially loggable and rate-limitable, unlike free-form SQL strings buried in a chat transcript.
Decoupling and portability. The agent depends on tool names, not on a database engine. SQLite → Postgres is a server-side change only; the agent and its prompts stay identical. The same agent could be pointed at a different MCP server (a CRM, a ticket system) with no structural change.
The honest trade-off: MCP adds a hop (latency), a server to run, and the tool surface constrains the model to what we anticipated (a genuinely novel analysis might need a tool we didn't build). For database access those costs are small and the security/portability gains are decisive — which is why "tools, not connection strings" is the right default for LLM ↔ data systems.
9. Deploying it live
The repo ships a multi-stage Dockerfile (builds the React app, then runs
FastAPI serving both the API and the frontend from one container) and a
render.yaml blueprint.
Render.com (free):
Dashboard → New → Blueprint → select this repo (it reads
render.yaml).Set the
OPENAI_API_KEYenv var in the dashboard.Optional: create a free PostgreSQL DB at neon.tech, run
db/init_db_postgres.pyagainst it once, and setCOMPANY_DB_DSN— the app switches engines automatically. Without it, the container rebuilds the demo SQLite DB on every start (fine for a demo).Deploy → you get a public
https://…onrender.comURL serving the full app.
The same Dockerfile also works on Hugging Face Spaces (Docker space), Railway,
and Fly.io. Secrets stay platform-side env vars; .dockerignore excludes
.env so a key can never be baked into an image.
⚠️ A public URL means strangers can spend your OpenAI credits. For a demo, share the link only with your team, keep a usage cap on the key, and take the service down after the review.
10. MCP configuration reference
The agent launches the server itself as a stdio subprocess (see
DatabaseAgent.__init__), so no separate process management is needed for
the demo. To use the same server from any other MCP host (e.g. Claude
Desktop), point the host at mcp_config.json-style config:
{
"mcpServers": {
"company-database": {
"command": "python",
"args": ["mcp_server/server.py"],
"env": { "COMPANY_DB_PATH": "db/company.db" }
}
}
}Environment variables:
Variable | Read by | Purpose |
| agent | LLM access — the agent's only secret |
| agent factory / web backend | Force |
| MCP server | SQLite file path — never visible to the agent/LLM |
| MCP server | Set to a PostgreSQL DSN to switch engines (e.g. |
| MCP server | Audit log location (default |
| agent (Anthropic) | Optional model override (default |
| agent (OpenAI) | Optional model override (default |
Switching to PostgreSQL
# one-time setup (needs your postgres superuser password):
$env:PGADMIN_PASSWORD="<your postgres password>"
python db/init_db_postgres.py # creates DB 'company' + SELECT-only role 'mcp_readonly'
# then point the MCP SERVER at it (the agent doesn't change at all):
$env:COMPANY_DB_DSN="postgresql://mcp_readonly:readonly_pass@localhost:5432/company"
python demos/demo.pyWith Postgres, the third defense layer becomes a real database role with
only GRANT SELECT — even a bug in every software layer above could not
produce a write, because the engine's permission system refuses it.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/keerthivanan/MCP-Database-connector-Agentic-retrievel'
If you have feedback or need assistance with the MCP directory API, please join our Discord server