{
  "markdown": "<p align=\"center\">\n  <h1 align=\"center\">🗄️ a2db</h1>\n  <p align=\"center\">\n    <em>Agent-to-Database</em>\n  </p>\n  <p align=\"center\">\n    <strong>Give AI agents safe, read-only access to your databases. One call, multiple queries, clean results.</strong>\n  </p>\n  <p align=\"center\">\n    5 databases &middot; batch queries &middot; pre-configured connections &middot; SQLGlot read-only\n  </p>\n  <p align=\"center\">\n    <a href=\"https://pypi.org/project/a2db/\"><img src=\"https://img.shields.io/pypi/v/a2db.svg\" alt=\"PyPI\"></a>\n    <a href=\"https://pypi.org/project/a2db/\"><img src=\"https://img.shields.io/pypi/pyversions/a2db.svg\" alt=\"Python versions\"></a>\n    <a href=\"https://github.com/yoselabs/a2db/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/yoselabs/a2db.svg\" alt=\"License\"></a>\n    <a href=\"https://github.com/yoselabs/a2db/actions\"><img src=\"https://github.com/yoselabs/a2db/actions/workflows/publish.yml/badge.svg\" alt=\"CI\"></a>\n    <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>\n  </p>\n  <p align=\"center\">\n    <a href=\"#quick-start\">Quick Start</a> &middot;\n    <a href=\"#mcp-tools\">MCP Tools</a> &middot;\n    <a href=\"#security\">Security</a> &middot;\n    <a href=\"#comparison\">Comparison</a> &middot;\n    <a href=\"#setup-by-environment\">Setup</a>\n  </p>\n</p>\n\n---\n\n```\nAgent: \"Show me active users and their recent orders\"\n  ↓\na2db execute → 2 queries, 1 call, structured results\n  ↓\nAgent: \"Got it — 847 active users, avg order $42.50\"\n```\n\n## Why a2db?\n\nMost 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:\n\n- **Pre-configured connections** — define databases in `.mcp.json` with `--register`, agent queries immediately\n- **Batch queries** — run multiple named queries in a single tool call\n- **Default connection** — set connection once, use it across all queries in a batch\n- **Clean output** — structured JSON envelope with compact TSV data and per-query timing (see [why TSV?](#why-tsv))\n- **Read-only enforced** — SQLGlot AST parsing blocks all write operations\n- **All drivers bundled** — `pip install a2db` and you're done\n- **Secrets stay in env** — `${DB_PASSWORD}` in DSNs, expanded only at connection time\n\n## Supported Databases\n\n| Database | Driver | Async |\n|----------|--------|-------|\n| PostgreSQL | asyncpg | native |\n| SQLite | aiosqlite | native |\n| MySQL / MariaDB | mysql-connector-python | wrapped |\n| Oracle | oracledb | wrapped |\n| SQL Server | pymssql | wrapped |\n\n## Quick Start\n\n```bash\npip install a2db\n```\n\n### As an MCP Server (recommended)\n\n**Claude Code** (with pre-configured connection):\n```bash\nclaude mcp add -s user a2db -- a2db-mcp \\\n  --register myapp/prod/main 'postgresql://user:${DB_PASSWORD}@host/mydb'\n```\n\n**Claude Code** (minimal — agent calls `login` on demand):\n```bash\nclaude mcp add -s user a2db -- a2db-mcp\n```\n\n**Claude Desktop / Cursor / any MCP client** (`.mcp.json`):\n```json\n{\n  \"mcpServers\": {\n    \"a2db\": {\n      \"command\": \"uvx\",\n      \"args\": [\n        \"a2db-mcp\",\n        \"--register\", \"myapp/prod/main\", \"postgresql://user:${DB_PASSWORD}@host/mydb\"\n      ],\n      \"env\": {\n        \"DB_PASSWORD\": \"your-password-here\"\n      }\n    }\n  }\n}\n```\n\n**Multiple databases:**\n```json\n{\n  \"args\": [\n    \"a2db-mcp\",\n    \"--register\", \"myapp/prod/main\", \"postgresql://user:${DB_PASSWORD}@host/maindb\",\n    \"--register\", \"myapp/prod/analytics\", \"postgresql://user:${DB_PASSWORD}@host/analytics\"\n  ]\n}\n```\n\n`--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.\n\n### As a CLI\n\n```bash\n# Save a connection (validates immediately)\na2db login -p myapp -e prod -d main 'postgresql://user:${DB_PASSWORD}@localhost/mydb'\n\n# Query\na2db query -p myapp -e prod -d main \"SELECT * FROM users LIMIT 10\"\n\n# JSON output\na2db query -p myapp -e prod -d main -f json \"SELECT * FROM users LIMIT 10\"\n\n# Explore schema\na2db schema -p myapp -e prod -d main tables\na2db schema -p myapp -e prod -d main columns -t users\n\n# List / remove connections\na2db connections\na2db logout -p myapp -e prod -d main\n```\n\n## MCP Tools\n\n| Tool | Description |\n|------|-------------|\n| `login` | Save a connection — validates by connecting first |\n| `logout` | Remove a saved connection |\n| `list_connections` | List connections (no secrets exposed) |\n| `execute` | Run named batch queries with pagination |\n| `search_objects` | Explore schema — tables, columns, with detail levels |\n\n### `execute` — the core tool\n\n**Named dict with default connection (preferred):**\n```json\n{\n  \"connection\": {\"project\": \"myapp\", \"env\": \"prod\", \"db\": \"main\"},\n  \"queries\": {\n    \"active_users\": {\"sql\": \"SELECT id, name FROM users WHERE active = true\"},\n    \"recent_orders\": {\"sql\": \"SELECT id, total FROM orders ORDER BY created_at DESC LIMIT 5\"}\n  }\n}\n```\n\n**List format (auto-named q1, q2, ...):**\n```json\n{\n  \"connection\": {\"project\": \"myapp\", \"env\": \"prod\", \"db\": \"main\"},\n  \"queries\": [\n    {\"sql\": \"SELECT COUNT(*) AS cnt FROM users\"},\n    {\"sql\": \"SELECT AVG(total) AS avg_order FROM orders\"}\n  ]\n}\n```\n\n**Response (TSV format — default):**\n```json\n{\n  \"active_users\": {\n    \"data\": \"id\\tname\\n1\\tAlice\\n2\\tBob\\n3\\tCharlie\",\n    \"rows\": 3,\n    \"truncated\": false,\n    \"time_ms\": 12\n  },\n  \"recent_orders\": {\n    \"data\": \"id\\ttotal\\n501\\t129.00\\n500\\t49.99\",\n    \"rows\": 2,\n    \"truncated\": false,\n    \"time_ms\": 8\n  }\n}\n```\n\nNo `::text` casts needed — integers, floats, timestamps, arrays, NULLs all work natively.\n\n### Error context\n\nWhen a query fails with a column error, a2db enriches the message:\n\n```\ncolumn \"nme\" does not exist\nDid you mean: name?\nAvailable columns: id (integer), name (text), email (text), active (integer)\n```\n\n### Why TSV?\n\nLLM 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.\n\nFor 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.\n\nSet `format=\"json\"` if you need full structured output with column names on every row.\n\n## Security\n\n### Read-Only Enforcement\n\nEvery query is parsed by [SQLGlot](https://github.com/tobymao/sqlglot) before execution:\n\n- **Blocked:** INSERT, UPDATE, DELETE, DROP, TRUNCATE, ALTER, CREATE, GRANT, REVOKE\n- **Bypass-resistant:** multi-statement attacks and comment-wrapped writes are caught at the AST level, not just keyword matching\n- **Allowed:** SELECT, UNION, EXPLAIN, SHOW, DESCRIBE, PRAGMA\n\nThis 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.\n\n**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](TODO.md).\n\n### Credential Storage\n\nConnections are saved in `~/.config/a2db/connections/` as TOML files.\n\n- **`${DB_PASSWORD}` syntax** — environment variable references are stored literally and expanded only at connection time. Secrets stay in your environment, not on disk.\n- **No secrets in list output** — `list_connections` shows project/env/db and database type, never DSNs or passwords\n- Connection files are local to your machine and outside any repository\n\n### Deployment Scope\n\na2db 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.\n\n**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.\n\n## Comparison\n\n| Feature | a2db | DBHub | Google Toolbox | PGMCP | Supabase MCP |\n|---------|------|-------|----------------|-------|--------------|\n| **Databases** | 5 (PG, SQLite, MySQL, Oracle, MSSQL) | 5 (PG, MySQL, MSSQL, MariaDB, SQLite) | 40+ (cloud + OSS) | PG only | PG (Supabase) |\n| **Batch queries** | Named dict + list | Semicolon-separated | No | No | No |\n| **Default connection** | Set once, use for all | Per-query | N/A | Single DB | Single project |\n| **Read-only** | SQLGlot AST (enforced) | Keyword check (config) | Hint/annotation | Read-only tx + regex | Config flag |\n| **Write support** | Planned (per-connection) | Config flag | Via tool definition | No | Config flag |\n| **Output** | JSON + TSV data | Structured text | MCP protocol | Table / JSON / CSV | JSON |\n| **Schema discovery** | 3 detail levels | Dedicated tool | Prebuilt tools | Via NL-to-SQL | Dedicated tools |\n| **Pre-configured** | `--register` in MCP config | Config file | YAML config | Env var | Cloud-managed |\n| **Credentials** | `${ENV_VAR}` in DSN | DSN strings | Env vars + GCP IAM | Env var | OAuth 2.1 |\n| **Drivers bundled** | All included | All included | Varies | Built-in | Managed |\n| **CLI** | Yes | No | Yes | Yes | No |\n| **Error context** | Column suggestions + types | No | No | No | No |\n| **License** | Apache 2.0 | MIT | Apache 2.0 | Apache 2.0 | Apache 2.0 |\n\n**When to use what:**\n\n- **a2db** — multi-DB batch queries with clean output, agent-first design, fast setup\n- **DBHub** — custom tools via TOML config, web workbench UI\n- **Google Toolbox** — GCP ecosystem, IAM integration, 40+ sources\n- **PGMCP** — natural-language-to-SQL for PostgreSQL (requires OpenAI key)\n- **Supabase MCP** — full Supabase platform management (edge functions, branching, storage)\n\n## Setup by Environment\n\n### Local (macOS / Linux)\n\n```bash\npip install a2db\n\n# CLI\na2db login -p myapp -e dev -d main 'postgresql://user:pass@localhost/mydb'\n\n# Or add as MCP server (see Quick Start)\n```\n\n### Docker\n\n```dockerfile\nFROM python:3.12-slim\nRUN pip install a2db\nCMD [\"a2db-mcp\", \"--register\", \"myapp/prod/main\", \"postgresql://user:${DB_PASSWORD}@host/mydb\"]\n```\n\n```bash\ndocker run -e DB_PASSWORD=secret -i my-a2db-image\n```\n\nSecrets are injected as environment variables at runtime — never baked into the image.\n\n### CI / Automation\n\n```bash\npip install a2db\n\n# Pre-configured — no login needed\na2db-mcp --register myapp/ci/main \"postgresql://ci_user:${CI_DB_PASSWORD}@db-host/mydb\"\n\n# Or use CLI directly\na2db login -p myapp -e ci -d main \"postgresql://ci_user:${CI_DB_PASSWORD}@db-host/mydb\"\na2db query -p myapp -e ci -d main \"SELECT COUNT(*) FROM migrations\"\n```\n\n## Development\n\n```bash\nmake bootstrap   # Install deps + hooks\nmake check       # Lint + test + security (full gate)\nmake test        # Tests with coverage (90% minimum)\nmake lint        # Lint only (never modifies files)\nmake fix         # Auto-fix + lint\n```\n\n## License\n\nApache 2.0\n\n---\n\n<p align=\"center\">\n  <sub>🗄️ Agent-first database access since 2025.</sub>\n</p>\n<p align=\"center\">\n  <sub>Built by <a href=\"https://github.com/iorlas\">Denis Tomilin</a></sub>\n</p>\n\n<!-- mcp-name: io.github.yoselabs/a2db -->\n",
  "bytes": 11323,
  "sha": "f579e618290307d23d05c5e4db845c5b167a39d0d000fb218d5da0435eff6d49",
  "repo_slug": "agentic-eng/a2db",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_agentic_eng_a2db_00ebfb0e/readme"
}