<p align="center"> <h1 align="center">🗄️ a2db</h1> <p align="center"> <em>Agent-to-Database</em> </p> <p align="center"> <strong>Give AI agents safe, read-only access to your databases. One call, multiple queries, clean results.</strong> </p> <p align="center"> 5 databases · batch queries · pre-configured connections · SQLGlot read-only </p> <p align="center"> <a href="https://pypi.org/project/a2db/"><img src="https://img.shields.io/pypi/v/a2db.svg" alt="PyPI"></a> <a href="https://pypi.org/project/a2db/"><img src="https://img.shields.io/pypi/pyversions/a2db.svg" alt="Python versions"></a> <a href="https://github.com/yoselabs/a2db/blob/main/LICENSE"><img src="https://img.shields.io/github/license/yoselabs/a2db.svg" alt="License"></a> <a href="https://github.com/yoselabs/a2db/actions"><img src="https://github.com/yoselabs/a2db/actions/workflows/publish.yml/badge.svg" alt="CI"></a> <a href="https://registry.modelcontextprotocol.io/servers/io.github.yoselabs/a2db"><img src="https://img.shields.io/badge/MCP-registry-blue" alt="MCP Registry"></a> </p> <p align="center"> <a href="#quick-start">Quick Start</a> · <a href="#mcp-tools">MCP Tools</a> · <a href="#security">Security</a> · <a href="#comparison">Comparison</a> · <a href="#setup-by-environment">Setup</a> </p> </p>
---
Agent: "Show me active users and their recent orders"
↓
a2db execute → 2 queries, 1 call, structured results
↓
Agent: "Got it — 847 active users, avg order $42.50"
Why a2db?
Most database MCP servers make you run one query at a time, repeat connection details on every call, and return results double-encoded inside JSON strings. a2db fixes all of that:
- Pre-configured connections — define databases in
.mcp.jsonwith--register, agent queries immediately - Batch queries — run multiple named queries in a single tool call
- Default connection — set connection once, use it across all queries in a batch
- Clean output — structured JSON envelope with compact TSV data and per-query timing (see why TSV?)
- Read-only enforced — SQLGlot AST parsing blocks all write operations
- All drivers bundled —
pip install a2dband you're done - Secrets stay in env —
${DB_PASSWORD}in DSNs, expanded only at connection time
Supported Databases
| Database | Driver | Async | |----------|--------|-------| | PostgreSQL | asyncpg | native | | SQLite | aiosqlite | native | | MySQL / MariaDB | mysql-connector-python | wrapped | | Oracle | oracledb | wrapped | | SQL Server | pymssql | wrapped |
Quick Start
pip install a2db
As an MCP Server (recommended)
Claude Code (with pre-configured connection): ``bash claude mcp add -s user a2db -- a2db-mcp \ --register myapp/prod/main 'postgresql://user:${DB_PASSWORD}@host/mydb' ``
Claude Code (minimal — agent calls login on demand): ``bash claude mcp add -s user a2db -- a2db-mcp ``
Claude Desktop / Cursor / any MCP client (.mcp.json): ``json { "mcpServers": { "a2db": { "command": "uvx", "args": [ "a2db-mcp", "--register", "myapp/prod/main", "postgresql://user:${DB_PASSWORD}@host/mydb" ], "env": { "DB_PASSWORD": "your-password-here" } } } } ``
Multiple databases: ``json { "args": [ "a2db-mcp", "--register", "myapp/prod/main", "postgresql://user:${DB_PASSWORD}@host/maindb", "--register", "myapp/prod/analytics", "postgresql://user:${DB_PASSWORD}@host/analytics" ] } ``
--register pre-registers connections at server startup — the agent can query immediately. Passwords use ${ENV_VAR} syntax and are expanded at connection time, never stored in plaintext.
As a CLI
# Save a connection (validates immediately)
a2db login -p myapp -e prod -d main 'postgresql://user:${DB_PASSWORD}@localhost/mydb'
# Query
a2db query -p myapp -e prod -d main "SELECT * FROM users LIMIT 10"
# JSON output
a2db query -p myapp -e prod -d main -f json "SELECT * FROM users LIMIT 10"
# Explore schema
a2db schema -p myapp -e prod -d main tables
a2db schema -p myapp -e prod -d main columns -t users
# List / remove connections
a2db connections
a2db logout -p myapp -e prod -d main
MCP Tools
| Tool | Description | |------|-------------| | login | Save a connection — validates by connecting first | | logout | Remove a saved connection | | list_connections | List connections (no secrets exposed) | | execute | Run named batch queries with pagination | | search_objects | Explore schema — tables, columns, with detail levels |
execute — the core tool
Named dict with default connection (preferred): ``json { "connection": {"project": "myapp", "env": "prod", "db": "main"}, "queries": { "active_users": {"sql": "SELECT id, name FROM users WHERE active = true"}, "recent_orders": {"sql": "SELECT id, total FROM orders ORDER BY created_at DESC LIMIT 5"} } } ``
List format (auto-named q1, q2, ...): ``json { "connection": {"project": "myapp", "env": "prod", "db": "main"}, "queries": [ {"sql": "SELECT COUNT(*) AS cnt FROM users"}, {"sql": "SELECT AVG(total) AS avg_order FROM orders"} ] } ``
Response (TSV format — default): ``json { "active_users": { "data": "id\tname\n1\tAlice\n2\tBob\n3\tCharlie", "rows": 3, "truncated": false, "time_ms": 12 }, "recent_orders": { "data": "id\ttotal\n501\t129.00\n500\t49.99", "rows": 2, "truncated": false, "time_ms": 8 } } ``
No ::text casts needed — integers, floats, timestamps, arrays, NULLs all work natively.
Error context
When a query fails with a column error, a2db enriches the message:
column "nme" does not exist
Did you mean: name?
Available columns: id (integer), name (text), email (text), active (integer)
Why TSV?
LLM context windows are expensive. JSON row data is verbose — every row repeats every column name, adds braces, commas, and quotes. TSV is a flat grid: one header row, then just values separated by tabs.
For a 100-row, 5-column result set, TSV typically uses 40-60% fewer tokens than JSON row format. The structured JSON envelope still gives you metadata (row count, truncation status) — only the row payload is TSV.
Set format="json" if you need full structured output with column names on every row.
Security
Read-Only Enforcement
Every query is parsed by SQLGlot before execution:
- Blocked: INSERT, UPDATE, DELETE, DROP, TRUNCATE, ALTER, CREATE, GRANT, REVOKE
- Bypass-resistant: multi-statement attacks and comment-wrapped writes are caught at the AST level, not just keyword matching
- Allowed: SELECT, UNION, EXPLAIN, SHOW, DESCRIBE, PRAGMA
This is defense-in-depth — you should also use a read-only database user, but a2db won't let writes through even if the user has write permissions.
Write support is implemented in the core but not yet exposed via MCP. Planned: per-connection write permissions, explicitly enabled by the human operator — not the agent. See TODO.md.
Credential Storage
Connections are saved in ~/.config/a2db/connections/ as TOML files.
${DB_PASSWORD}syntax — environment variable references are stored literally and expanded only at connection time. Secrets stay in your environment, not on disk.- No secrets in list output —
list_connectionsshows project/env/db and database type, never DSNs or passwords - Connection files are local to your machine and outside any repository
Deployment Scope
a2db currently runs as a local stdio MCP server. It inherits environment variables from the process that launches it (your shell, Claude Code, Docker). This is the standard model for local MCP servers — the same approach used by DBHub, Google Toolbox, and others.
Planned: remote HTTP transport with OAuth 2.1 per the MCP spec. For now, if running in Docker, inject secrets via environment variables at container runtime.
Comparison
| Feature | a2db | DBHub | Google Toolbox | PGMCP | Supabase MCP | |---------|------|-------|----------------|-------|--------------| | Databases | 5 (PG, SQLite, MySQL, Oracle, MSSQL) | 5 (PG, MySQL, MSSQL, MariaDB, SQLite) | 40+ (cloud + OSS) | PG only | PG (Supabase) | | Batch queries | Named dict + list | Semicolon-separated | No | No | No | | Default connection | Set once, use for all | Per-query | N/A | Single DB | Single project | | Read-only | SQLGlot AST (enforced) | Keyword check (config) | Hint/annotation | Read-only tx + regex | Config flag | | Write support | Planned (per-connection) | Config flag | Via tool definition | No | Config flag | | Output | JSON + TSV data | Structured text | MCP protocol | Table / JSON / CSV | JSON | | Schema discovery | 3 detail levels | Dedicated tool | Prebuilt tools | Via NL-to-SQL | Dedicated tools | | Pre-configured | --register in MCP config | Config file | YAML config | Env var | Cloud-managed | | Credentials | ${ENV_VAR} in DSN | DSN strings | Env vars + GCP IAM | Env var | OAuth 2.1 | | Drivers bundled | All included | All included | Varies | Built-in | Managed | | CLI | Yes | No | Yes | Yes | No | | Error context | Column suggestions + types | No | No | No | No | | License | Apache 2.0 | MIT | Apache 2.0 | Apache 2.0 | Apache 2.0 |
When to use what:
- a2db — multi-DB batch queries with clean output, agent-first design, fast setup
- DBHub — custom tools via TOML config, web workbench UI
- Google Toolbox — GCP ecosystem, IAM integration, 40+ sources
- PGMCP — natural-language-to-SQL for PostgreSQL (requires OpenAI key)
- Supabase MCP — full Supabase platform management (edge functions, branching, storage)
Setup by Environment
Local (macOS / Linux)
pip install a2db
# CLI
a2db login -p myapp -e dev -d main 'postgresql://user:pass@localhost/mydb'
# Or add as MCP server (see Quick Start)
Docker
FROM python:3.12-slim
RUN pip install a2db
CMD ["a2db-mcp", "--register", "myapp/prod/main", "postgresql://user:${DB_PASSWORD}@host/mydb"]
docker run -e DB_PASSWORD=secret -i my-a2db-image
Secrets are injected as environment variables at runtime — never baked into the image.
CI / Automation
pip install a2db
# Pre-configured — no login needed
a2db-mcp --register myapp/ci/main "postgresql://ci_user:${CI_DB_PASSWORD}@db-host/mydb"
# Or use CLI directly
a2db login -p myapp -e ci -d main "postgresql://ci_user:${CI_DB_PASSWORD}@db-host/mydb"
a2db query -p myapp -e ci -d main "SELECT COUNT(*) FROM migrations"
Development
make bootstrap # Install deps + hooks
make check # Lint + test + security (full gate)
make test # Tests with coverage (90% minimum)
make lint # Lint only (never modifies files)
make fix # Auto-fix + lint
License
Apache 2.0
---
<p align="center"> <sub>🗄️ Agent-first database access since 2025.</sub> </p> <p align="center"> <sub>Built by <a href="https://github.com/iorlas">Denis Tomilin</a></sub> </p>
<!-- mcp-name: io.github.yoselabs/a2db -->











