{"id":"5616fbd1-c8cf-486c-93f1-284bb0c36cfd","slug":"db-mcp-server","kind":"mcp","name":"db-mcp-server","summary":"A powerful multi-database server implementing the Model Context Protocol (MCP) to provide AI assistants with structured access to databases.","owner":"FreePeak","repoUrl":"https://github.com/FreePeak/db-mcp-server","license":"MIT","licenseTier":"permissive","trust":"community","stars":465,"match":null,"fewStars":false,"format":"mcp","createdAt":null,"addedAt":"2026-10-14T03:43:01.403Z","logoUrl":"https://avatars.githubusercontent.com/u/192848417?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra","databases","memory-knowledge"],"tags":["database-mcp-server"],"runtime":"node","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-14T03:49:07.383Z","sample":false,"url":"https://conduits.dev/tools/db-mcp-server","language":"Go","transports":["stdio"],"install":{"npm":"@freepeak/db-mcp-server"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"<div align=\"center\">\n\n<img src=\"assets/logo.svg\" alt=\"DB MCP Server Logo\" width=\"300\" />\n\n# Multi Database MCP Server\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n[![Go Report Card](https://goreportcard.com/badge/github.com/FreePeak/db-mcp-server)](https://goreportcard.com/report/github.com/FreePeak/db-mcp-server)\n[![Go Reference](https://pkg.go.dev/badge/github.com/FreePeak/db-mcp-server.svg)](https://pkg.go.dev/github.com/FreePeak/db-mcp-server)\n[![Contributors](https://img.shields.io/github/contributors/FreePeak/db-mcp-server)](https://github.com/FreePeak/db-mcp-server/graphs/contributors)\n\n<h3>A powerful multi-database server implementing the Model Context Protocol (MCP) to provide AI assistants with structured access to databases.</h3>\n\n<div class=\"toc\">\n  <a href=\"#overview\">Overview</a> •\n  <a href=\"#core-concepts\">Core Concepts</a> •\n  <a href=\"#features\">Features</a> •\n  <a href=\"#supported-databases\">Supported Databases</a> •\n  <a href=\"#deployment-options\">Deployment Options</a> •\n  <a href=\"#configuration\">Configuration</a> •\n  <a href=\"#available-tools\">Available Tools</a> •\n  <a href=\"#examples\">Examples</a> •\n  <a href=\"#troubleshooting\">Troubleshooting</a> •\n  <a href=\"#contributing\">Contributing</a>\n</div>\n\n<div align=\"center\">\n  <a href=\"docs/PRD.md\"><b>📄 Product Requirements — current capabilities, roadmap & decisions</b></a>\n</div>\n\n</div>\n\n## Overview\n\nThe DB MCP Server provides a standardized way for AI models to interact with multiple databases simultaneously. Built on the [FreePeak/cortex](https://github.com/FreePeak/cortex) framework, it enables AI assistants to execute SQL queries, manage transactions, explore schemas, and analyze performance across different database systems through a unified interface.\n\n## Core Concepts\n\n### Multi-Database Support\n\nUnlike traditional database connectors, DB MCP Server can connect to and interact with multiple databases concurrently:\n\n```json\n{\n  \"connections\": [\n    {\n      \"id\": \"mysql1\",\n      \"type\": \"mysql\",\n      \"host\": \"localhost\",\n      \"port\": 3306,\n      \"name\": \"db1\",\n      \"user\": \"user1\",\n      \"password\": \"password1\"\n    },\n    {\n      \"id\": \"postgres1\",\n      \"type\": \"postgres\",\n      \"host\": \"localhost\",\n      \"port\": 5432,\n      \"name\": \"db2\",\n      \"user\": \"user2\",\n      \"password\": \"password2\"\n    },\n    {\n      \"id\": \"oracle1\",\n      \"type\": \"oracle\",\n      \"host\": \"localhost\",\n      \"port\": 1521,\n      \"service_name\": \"XEPDB1\",\n      \"user\": \"user3\",\n      \"password\": \"password3\"\n    }\n  ]\n}\n```\n\n### Dynamic Tool Generation\n\nFor each connected database, the server automatically generates specialized tools:\n\n```go\n// For a database with ID \"mysql1\", these tools are generated:\nquery_mysql1       // Execute SQL queries\nexecute_mysql1     // Run data modification statements\ntransaction_mysql1 // Manage transactions\nschema_mysql1      // Explore database schema\nperformance_mysql1 // Analyze query performance\n```\n\n### Clean Architecture\n\nThe server follows Clean Architecture principles with these layers:\n\n1. **Domain Layer**: Core business entities and interfaces\n2. **Repository Layer**: Data access implementations\n3. **Use Case Layer**: Application business logic\n4. **Delivery Layer**: External interfaces (MCP tools)\n\n## Features\n\n- **Simultaneous Multi-Database Support**: Connect to multiple MySQL, PostgreSQL, SQLite, and Oracle databases concurrently\n- **Lazy Loading Mode**: Defer connection establishment until first use - perfect for setups with 10+ databases (enable with `--lazy-loading` flag)\n- **Database-Specific Tool Generation**: Auto-creates specialized tools for each connected database\n- **Clean Architecture**: Modular design with clear separation of concerns\n- **OpenAI Agents SDK Compatibility**: Full compatibility for seamless AI assistant integration\n- **Dynamic Database Tools**: Execute queries, run statements, manage transactions, explore schemas, analyze performance\n- **Unified Interface**: Consistent interaction patterns across different database types\n- **Connection Management**: Simple configuration for multiple database connections\n- **Health Check**: Automatic validation of database connectivity on startup\n- **Production Guardrails**: Per-database `read_only` enforcement (blocks writes through both `query_*` and `execute_*` tools), `max_rows` result truncation with explicit notices, and per-query timeouts\n\n### Production Guardrails\n\nProtect agent sessions against runaway queries and accidental writes:\n\n| Setting | Scope | Effect |\n|---------|-------|--------|\n| `\"read_only\": true` | per database | Blocks write statements (`INSERT`, `UPDATE`, `DELETE`, DDL, data-modifying CTEs, stacked writes) through **both** query and execute tools, **and** enforces rejection at the database engine itself on PostgreSQL/TimescaleDB (`default_transaction_read_only=on`) and MySQL (`transaction_read_only=1`); SQLite opens `mode=ro`. Classification strips comments and string literals and defaults to deny for unrecognized statements. |\n| `\"max_rows\": 1000` | per database | Truncates result sets at N rows and appends an explicit `[Truncated]` notice so the model knows to refine its query instead of losing context. `0` (default) means unlimited. |\n| `\"masking_rules\": [...]` | per database | Masks values of result columns whose **name** matches a rule's regex before they leave the server — applies to every query shape including `SELECT *`. Strategies: `\"fixed_string\"` (replace with `value`), `\"null\"`, and `\"partial\"` (`keep_last` trailing characters visible; shorter values fully masked). First matching rule wins; invalid patterns or unknown strategies abort config load (fail closed); masked-cell counts are reported in the result footer. Renaming a column with an alias bypasses name matching by design. See [docs/design/column-masking-scoping.md](docs/design/column-masking-scoping.md). |\n| `\"mask_pii\": true` | per database | Redacts PII in query results by **content shape**, not just column name: emails, phone numbers, credit-card numbers (Luhn-checked), SSNs, IPv4 addresses and 19+ digit identifiers are replaced with `[EMAIL]`/`[PHONE]`/`[CREDIT_CARD]`/`[SSN]`/`[IP_ADDRESS]`/`[LONG_NUMBER]` wherever they appear — including inside free-text columns no rule covers. Column names like `email`/`card_number`/`iban` are redacted whole. Operator-controlled: once set, an agent **cannot** turn it off (the per-query `mask_pii` parameter is additive only). Composes with `masking_rules`. |\n| `\"query_timeout\": 30` | per database | Cancels statements that exceed the timeout in seconds; enforced at the repository layer for every tool (queries, statements, transactions, explain, schema inspection). Unset defaults to 30s; `-1` disables. Env-only deployments can set `QUERY_TIMEOUT_SECONDS` to fill connections without an explicit value (JSON keeps precedence). |\n\n| `DB_MCP_AUDIT_LOG=/path/audit.jsonl` | process | Appends one JSONL record per executed statement — timestamp, op (`query`/`execute`/`tx_*`), database, statement (capped at 10k chars), duration, error. Includes rejected attempts against read-only databases. Best-effort writes never fail a query; file is created with `0600`. |\n\n> **Defense in depth**: read-only is enforced in three layers — application classifier, engine session defaults, and (recommended) least-privilege database users. Oracle currently relies on the classifier plus user privileges.\n\n## Supported Databases\n\n| Database   | Status                    | Features                                                     |\n| ---------- | ------------------------- | ------------------------------------------------------------ |\n| MySQL      | ✅ Full Support           | Queries, Transactions, Schema Analysis, Performance Insights |\n| PostgreSQL | ✅ Full Support (v9.6-17) | Queries, Transactions, Schema Analysis, Performance Insights |\n| SQLite     | ✅ Full Support           | File-based & In-memory databases, SQLCipher encryption support |\n| Oracle     | ✅ Full Support (10g-23c) | Queries, Transactions, Schema Analysis, RAC, Cloud Wallet, TNS |\n| TimescaleDB| ✅ Full Support           | Time-Series Queries, Hypertable Discovery (write policies via SQL) |\n\n## Deployment and transport status\n\nThe safest local integration is **STDIO**. HTTP transports are still under hardening; do not expose the current server directly to a shared network.\n\n### Personal/local STDIO (recommended)\n\n```bash\n# Build from the audited source revision\nmake build\n\n# Use a private, single-connection config\n./bin/server -t stdio -c /absolute/path/to/config.local.json\n```\n\nFor Cursor, point the client at the actual build output and use absolute paths:\n\n```json\n{\n  \"mcpServers\": {\n    \"local-db\": {\n      \"command\": \"/absolute/path/to/db-mcp-server/bin/server\",\n      \"args\": [\"-t\", \"stdio\", \"-c\", \"/absolute/path/to/config.local.json\"]\n    }\n  }\n}\n```\n\nUse a read-only database account and a bounded `query_timeout` for a personal\nPostgreSQL/MySQL connection. For SQLite, use an absolute `database_path` and\n`\"read_only\": true` when the agent only needs to inspect data.\n\n### Legacy SSE (compatibility only)\n\nThe historical Cortex SSE transport has no built-in authentication. It is\n**disabled by default** and must only be enabled explicitly behind a trusted\nprivate reverse proxy:\n\n```bash\n./bin/server -t sse -h 127.0.0.1 -p 9092 \\\n  -allow-insecure-sse -c /absolute/path/to/config.local.json\n```\n\nDo not use this mode for a shared deployment. The legacy endpoint does not\napply `DB_MCP_API_KEY`; terminate TLS, authentication, and network policy at the\nproxy. A future release will replace it with a maintained Streamable HTTP\nimplementation.\n\n### Streamable HTTP (experimental)\n\nThe `streamable` endpoint is backed by the maintained Go MCP SDK and supports\nstateful sessions, JSON or SSE responses, notifications, GET/DELETE session\nlifecycle, and JSON-RPC dispatch. The process refuses to start it without\n`DB_MCP_API_KEY`; keep it behind a private TLS gateway until the identity and\nconformance rollout is complete.\n\nFor browser-origin requests, set an explicit comma-separated allowlist:\n\n```bash\nMCP_ALLOWED_ORIGINS=https://agent.example.com\n```\n\nThe built-in static bearer key is a bounded pilot mechanism. It grants\n`db:read` by default; set `DB_MCP_API_SCOPES=db:read,db:write` only for a\ntrusted write client. `db:admin` is reserved for controlled administration.\nSet `DB_MCP_OIDC_ISSUER` and `DB_MCP_OIDC_AUDIENCE` to use OIDC discovery,\nJWKS signature validation, issuer/audience checks, and subject identity for\nshared deployments; database-specific scopes are still intentionally not\nenabled until that policy is defined.\n\n### Docker development image\n\nThe published image is a development/demo artifact, not an enterprise\nhardening recommendation. The current Dockerfile runs as an unprivileged\nuser, omits an embedded config, and includes the legacy\n`-allow-insecure-sse` behavior for local Compose compatibility. It exposes a\nseparate health port (`9093`) and is not suitable for public publication.\n\n```bash\n# Build a local image from this source revision\ndocker build -t db-mcp-server:local .\n\n# Keep it private; the image's default SSE mode is compatibility-only\ndocker run --rm -p 127.0.0.1:9092:9092 \\\n  -p 127.0.0.1:9093:9093 \\\n  -v \"$(pwd)/config.local.json:/app/config.json:ro\" \\\n  db-mcp-server:local\n```\n\nFor a team deployment, wait for the authenticated Streamable transport and use\na non-root, read-only-filesystem image with a private network, external secret\nstore, and a TLS/OIDC gateway.\n\n### Source Code Installation\n\n```bash\n# Clone the repository\ngit clone https://github.com/FreePeak/db-mcp-server.git\ncd db-mcp-server\n\n# Build the server\nmake build\n\n# Run the server\n./bin/server -t stdio -c config.json\n```\n\n## Configuration\n\n### Database Configuration File\n\nCreate a `config.json` file with your database connections. `schema_version: 1`\nenables strict unknown-field validation:\n\n```json\n{\n  \"schema_version\": 1,\n  \"connections\": [\n    {\n      \"id\": \"mysql1\",\n      \"type\": \"mysql\",\n      \"host\": \"mysql1\",\n      \"port\": 3306,\n      \"name\": \"db1\",\n      \"user\": \"user1\",\n      \"password_env\": \"DB_MCP_MYSQL_PASSWORD\",\n      \"query_timeout\": 60,\n      \"max_open_conns\": 20,\n      \"max_idle_conns\": 5,\n      \"conn_max_lifetime_seconds\": 300,\n      \"conn_max_idle_time_seconds\": 60,\n      \"read_only\": false,\n      \"max_rows\": 1000,\n      \"masking_rules\": [\n        { \"pattern\": \"(?i)email\", \"strategy\": \"fixed_string\", \"value\": \"***MASKED***\" },\n        { \"pattern\": \"(?i)(ssn|tax_id)\", \"strategy\": \"null\" }\n      ]\n    },\n    {\n      \"id\": \"postgres1\",\n      \"type\": \"postgres\",\n      \"host\": \"postgres1\",\n      \"port\": 5432,\n      \"name\": \"db1\",\n      \"user\": \"user1\",\n      \"password_env\": \"DB_MCP_POSTGRES_PASSWORD\"\n    },\n    {\n      \"id\": \"sqlite_app\",\n      \"type\": \"sqlite\",\n      \"database_path\": \"./data/app.db\",\n      \"journal_mode\": \"WAL\",\n      \"cache_size\": 2000,\n      \"read_only\": false,\n      \"use_modernc_driver\": true,\n      \"query_timeout\": 30,\n      \"max_open_conns\": 1,\n      \"max_idle_conns\": 1\n    },\n    {\n      \"id\": \"sqlite_encrypted\",\n      \"type\": \"sqlite\",\n      \"database_path\": \"./data/secure.db\",\n      \"encryption_key\": \"your-secret-key-here\",\n      \"journal_mode\": \"WAL\",\n      \"use_modernc_driver\": false\n    },\n    {\n      \"id\": \"sqlite_memory\",\n      \"type\": \"sqlite\",\n      \"database_path\": \":memory:\",\n      \"cache_size\": 1000,\n      \"use_modernc_driver\": true\n    }\n  ]\n}\n```\n\n#### Secret references\n\nDo not put production credentials directly in JSON. Use either an\nenvironment variable name or a mounted secret file per connection:\n\n```json\n{\n  \"id\": \"prod\",\n  \"type\": \"postgres\",\n  \"host\": \"db.internal\",\n  \"port\": 5432,\n  \"name\": \"app\",\n  \"user\": \"mcp_reader\",\n  \"password_env\": \"DB_MCP_PROD_PASSWORD\"\n}\n```\n\n`password_file` and `encryption_key_env`/`encryption_key_file` are also\nsupported. Relative file paths resolve from the config file directory; an\ninline value cannot be combined with a reference. The resolved value stays in\nprocess memory and is not printed by the config loader.\n\n### Command-Line Options\n\n```bash\n# Basic syntax\n./bin/server -t <transport> -c <config-file>\n\n# HTTP transport options (legacy SSE requires an explicit opt-in)\n./bin/server -t sse -h <hostname> -p <port> -allow-insecure-sse -c <config-file>\n\n# Lazy loading mode (recommended for 10+ databases)\n./bin/server -t stdio -c <config-file> --lazy-loading\n\n# Customize log directory (useful for multi-project setups)\n./bin/server -t stdio -c <config-file> -log-dir /tmp/db-mcp-logs\n\n# Inline database configuration\n./bin/server -t stdio -db-config '{\"connections\":[...]}'\n\n# Environment variable configuration\nexport DB_CONFIG='{\"connections\":[...]}'\n./bin/server -t stdio\n\n# Print the build version\n./bin/server --version\n\n# Upgrade to the latest published release (verifies SHA-256, swaps the binary)\n./bin/server update\n./bin/server update -check          # only report whether a newer release exists\n./bin/server update -version 1.13.0 # install a specific release\n```\n\n**Available Flags:**\n- `-t, -transport`: Transport mode (`stdio`, legacy `sse`, or experimental `streamable`)\n- `-c, -config`: Path to database configuration file\n- `-p, -port`: HTTP port (default: 9092)\n- `-h, -host`: HTTP bind host (default: `127.0.0.1`)\n- `--version`: Print the build version and exit\n- `-allow-insecure-sse`: Explicitly allow legacy SSE for a private development proxy\n- `-api-key`: Bearer key for the experimental Streamable HTTP entrypoint (or use `DB_MCP_API_KEY`)\n- `-log-level`: Log level (`debug`, `info`, `warn`, `error`)\n- `-log-dir`: Directory for log files (default: `./logs` in current directory)\n- `-db-config`: Inline JSON database configuration\n- `-lazy-loading`: Establish connections on first use\n- `-unified-tools`: Register one tool set with a `database` parameter\n\n**Updating:** `db-mcp-server update` resolves the latest GitHub release, checks\nthe download against the release's `SHA256SUMS` manifest, and replaces the\nrunning binary in place (the previous binary is left untouched if verification\nfails). It installs the asset matching your OS and architecture — restart the\nserver afterwards. On Windows a running executable cannot be replaced, so update\nthrough the npm installer there. `-check` reports without downloading; `-force`\nreinstalls the current version.\n\n### Environment Variables\n\nValues in a `.env` file are loaded first; real environment variables take precedence. A JSON config file (`CONFIG_PATH`/`DB_CONFIG_FILE`) takes precedence over inline and legacy database variables.\n\n| Variable | Default | Purpose |\n|---|---|---|\n| `CONFIG_PATH` / `DB_CONFIG_FILE` | `config.json` | Path to the multi-database JSON config |\n| `DB_CONFIG` | — | Inline JSON database configuration (used when no file is present) |\n| `TRANSPORT_MODE` | `stdio` | Transport mode when `-t` is not passed |\n| `SERVER_PORT` | `9092` | HTTP port |\n| `MCP_ALLOWED_ORIGINS` | empty | Comma-separated browser origins allowed by experimental Streamable HTTP |\n| `DB_MCP_API_KEY` | empty | Bearer key for experimental Streamable HTTP; keep it in a secret manager |\n| `DB_MCP_API_SCOPES` | `db:read` | Required/granted scopes: `db:read`, `db:write`, and/or `db:admin` |\n| `DB_MCP_OIDC_ISSUER` | empty | OIDC issuer URL; when set, OIDC/JWT verification replaces the static pilot key |\n| `DB_MCP_OIDC_AUDIENCE` | empty | Required OIDC audience/client ID |\n| `LOG_LEVEL` | `info` | Log verbosity (`debug`, `info`, `warn`, `error`) |\n| `DISABLE_LOGGING` | `false` | `true`/`1` silences logging entirely |\n| `DB_TYPE`, `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` | engine defaults | Single-database fallback when no JSON config exists |\n| `QUERY_TIMEOUT_SECONDS` | unset | Fills connections that don't set their own `query_timeout`; negative disables the cap. JSON configs keep precedence. |\n| `DB_MCP_AUDIT_LOG` | unset | Optional append-only JSONL audit path |\n| `VERBOSE` | unset | `true`/`1` sets the startup log level to `debug` (the default is `error`, i.e. silent unless `LOG_LEVEL` is set) |\n| `MCP_DISABLE_LOGGING` | unset | `true`/`1` discards MCP protocol logs (writer-level switch, independent of `LOG_LEVEL`/`DISABLE_LOGGING`) |\n\n\n## SQLite Configuration Options\n\nWhen using SQLite databases, you can leverage these additional configuration options:\n\n### SQLite Connection Parameters\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `database_path` | string | Required | Path to SQLite database file or `:memory:` for in-memory |\n| `encryption_key` | string | - | Key for SQLCipher encrypted databases |\n| `read_only` | boolean | false | Open database in read-only mode |\n| `max_rows` | integer | unlimited | Maximum rows returned per query; larger results are truncated with an explicit notice. Works on all database types |\n| `cache_size` | integer | 2000 | SQLite cache size in pages |\n| `journal_mode` | string | \"WAL\" | Journal mode: DELETE, TRUNCATE, PERSIST, WAL, OFF |\n| `use_modernc_driver` | boolean | true | Use modernc.org/sqlite (CGO-free) or mattn/go-sqlite3 |\n\n### SQLite Examples\n\n#### Basic File Database\n```json\n{\n  \"id\": \"my_sqlite_db\",\n  \"type\": \"sqlite\",\n  \"database_path\": \"./data/myapp.db\",\n  \"journal_mode\": \"WAL\",\n  \"cache_size\": 2000\n}\n```\n\n#### Encrypted Database (SQLCipher)\n```json\n{\n  \"id\": \"encrypted_db\",\n  \"type\": \"sqlite\",\n  \"database_path\": \"./data/secure.db\",\n  \"encryption_key\": \"your-secret-encryption-key\",\n  \"use_modernc_driver\": false\n}\n```\n\n#### In-Memory Database\n```json\n{\n  \"id\": \"memory_db\",\n  \"type\": \"sqlite\",\n  \"database_path\": \":memory:\",\n  \"cache_size\": 1000\n}\n```\n\n#### Read-Only Database\n```json\n{\n  \"id\": \"reference_data\",\n  \"type\": \"sqlite\",\n  \"database_path\": \"./data/reference.db\",\n  \"read_only\": true,\n  \"journal_mode\": \"DELETE\"\n}\n```\n\n## Oracle Configuration Options\n\nWhen using Oracle databases, you can leverage these additional configuration options:\n\n### Oracle Connection Parameters\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `host` | string | Required | Oracle database host |\n| `port` | integer | 1521 | Oracle listener port |\n| `service_name` | string | - | Service name (recommended for RAC) |\n| `sid` | string | - | System identifier (legacy, use service_name instead) |\n| `user` | string | Required | Database username |\n| `password` | string | Required | Database password |\n| `wallet_location` | string | - | Path to Oracle Cloud wallet directory |\n| `tns_admin` | string | - | Path to directory containing tnsnames.ora |\n| `tns_entry` | string | - | Named entry from tnsnames.ora |\n| `edition` | string | - | Edition-Based Redefinition edition name |\n| `pooling` | boolean | false | Enable driver-level connection pooling |\n| `standby_sessions` | boolean | false | Allow queries on standby databases |\n| `nls_lang` | string | AMERICAN_AMERICA.AL32UTF8 | Character set configuration |\n\n### Oracle Examples\n\n#### Basic Oracle Connection (Development)\n```json\n{\n  \"id\": \"oracle_dev\",\n  \"type\": \"oracle\",\n  \"host\": \"localhost\",\n  \"port\": 1521,\n  \"service_name\": \"XEPDB1\",\n  \"user\": \"testuser\",\n  \"password\": \"testpass\",\n  \"max_open_conns\": 50,\n  \"max_idle_conns\": 10,\n  \"conn_max_lifetime_seconds\": 1800\n}\n```\n\n#### Oracle with SID (Legacy)\n```json\n{\n  \"id\": \"oracle_legacy\",\n  \"type\": \"oracle\",\n  \"host\": \"oracledb.company.com\",\n  \"port\": 1521,\n  \"sid\": \"ORCL\",\n  \"user\": \"app_user\",\n  \"password\": \"app_password\"\n}\n```\n\n#### Oracle Cloud Autonomous Database (with Wallet)\n```json\n{\n  \"id\": \"oracle_cloud\",\n  \"type\": \"oracle\",\n  \"user\": \"ADMIN\",\n  \"password\": \"your-cloud-password\",\n  \"wallet_location\": \"/path/to/wallet_DBNAME\",\n  \"service_name\": \"dbname_high\"\n}\n```\n\n#### Oracle RAC (Real Application Clusters)\n```json\n{\n  \"id\": \"oracle_rac\",\n  \"type\": \"oracle\",\n  \"host\": \"scan.company.com\",\n  \"port\": 1521,\n  \"service_name\": \"production\",\n  \"user\": \"app_user\",\n  \"password\": \"app_password\",\n  \"max_open_conns\": 100,\n  \"max_idle_conns\": 20\n}\n```\n\n#### Oracle with TNS Entry\n```json\n{\n  \"id\": \"oracle_tns\",\n  \"type\": \"oracle\",\n  \"tns_admin\": \"/opt/oracle/network/admin\",\n  \"tns_entry\": \"PROD_DB\",\n  \"user\": \"app_user\",\n  \"password\": \"app_password\"\n}\n```\n\n#### Oracle with Edition-Based Redefinition\n```json\n{\n  \"id\": \"oracle_ebr\",\n  \"type\": \"oracle\",\n  \"host\": \"oracledb.company.com\",\n  \"port\": 1521,\n  \"service_name\": \"production\",\n  \"user\": \"app_user\",\n  \"password\": \"app_password\",\n  \"edition\": \"v2_0\"\n}\n```\n\n### Oracle Connection String Priority\n\nWhen multiple connection methods are configured, the following priority is used:\n\n1. **TNS Entry** (if `tns_entry` and `tns_admin` are configured)\n2. **Wallet** (if `wallet_location` is configured) - for Oracle Cloud\n3. **Standard** (host:port/service_name) - default method\n\n## Available Tools\n\nFor each connected database, DB MCP Server automatically generates these specialized tools:\n\n### Query Tools\n\n| Tool Name | Description |\n|-----------|-------------|\n| `query_<db_id>` | Execute SELECT queries and get results as a tabular dataset. Optional `mask_pii` boolean redacts PII by content shape (see the guardrails table); it is additive — a database configured with `\"mask_pii\": true` masks regardless. |\n| `execute_<db_id>` | Run data manipulation statements (INSERT, UPDATE, DELETE). Optional `dry_run` boolean classifies the statement offline — kind, risk level, advisories — and executes nothing; destructive statements that do run carry a `⚠ Risk notice` in their result. |\n| `transaction_<db_id>` | Begin, commit, and rollback transactions. Also carries the undo actions: `list_snapshots` and `rollback_snapshot` (with `snapshot_id`). Every `DELETE`/`UPDATE` run through `execute_<db_id>` records a pre-mutation snapshot of the rows it will affect — best-effort, bounded to the last 25 per database — and reports its ID in the result. |\n\n### Schema Tools\n\n| Tool Name | Description |\n|-----------|-------------|\n| `schema_<db_id>` | Get information about tables, columns, indexes, and foreign keys |\n| `generate_schema_<db_id>` | Generate SQL or code from database schema |\n\n### Performance Tools\n\n| Tool Name | Description |\n|-----------|-------------|\n| `performance_<db_id>` | Analyze query performance via actions: `stats` / `slow_queries` (in-process tracker), `engine_slow_queries` (pg_stat_statements / MySQL digest tables / Oracle v$sqlarea), `suggest` (static SQL lint), `suggest_indexes` (heuristic CREATE INDEX advice for one statement, equality-first composites, verify with EXPLAIN), `validate_suggestions` (PostgreSQL: installs the same suggestions as cost-free hypothetical indexes via the hypopg extension and reports whether the planner actually picks each one — ground-truth validation instead of manual EXPLAIN), `workload_suggestions` (same analysis across the top-N expensive workload statements, weighted by executions), `index_health` (duplicate/redundant/unused/invalid indexes and table bloat findings from catalogs; usage evidence where engine statistics exist), `db_health` (everything index_health covers plus connection-pressure utilization vs max_connections), `reset` |\n| `explain_<db_id>` | Show the execution plan for a SQL statement without running it; `analyze: true` executes with timing/buffer stats (PostgreSQL/MySQL). Writes stay blocked on read-only databases |\n| `describe_<db_id>` | Inspect one table's columns, indexes, and row estimate via engine catalog queries |\n| `health_<db_id>` | Report connectivity, ping latency, connection-pool state, and engine stats (PostgreSQL buffer-cache hit ratio, MySQL InnoDB buffer efficiency) |\n\n### TimescaleDB Tools\n\nFor PostgreSQL databases with the `timescaledb` extension installed, these additional\nspecialized tools are registered automatically at startup (registration is config-driven,\nso it also works under `--lazy-loading`; each handler verifies the extension at call time\nand returns an actionable error when it is absent):\n\n| Tool Name | Description |\n|-----------|-------------|\n| `timescaledb_timeseries_query_<db_id>` | Execute optimized time-series queries with time bucketing (`time_bucket`), filtering, and window functions |\n| `timescaledb_analyze_timeseries_<db_id>` | Analyze time-series patterns (trend, seasonality summary) for one table/column |\n| `timescaledb_list_hypertables_<db_id>` | List hypertables with their time column and dimension count (read-only) |\n| `timescaledb_compression_settings_<db_id>` | Show compression configuration for hypertables (read-only) |\n| `timescaledb_retention_policy_<db_id>` | Show configured retention policies (read-only) |\n| `timescaledb_list_continuous_aggregates_<db_id>` | List continuous aggregates with bucket interval and refresh policy (read-only) |\n| `timescaledb_continuous_aggregate_info_<db_id>` | Inspect one continuous aggregate in detail (read-only) |\n\nIn unified mode the same seven tools appear once as `timescaledb_timeseries_query`,\n`timescaledb_analyze_timeseries`, `timescaledb_list_hypertables`,\n`timescaledb_compression_settings`, `timescaledb_retention_policy`,\n`timescaledb_list_continuous_aggregates`, and `timescaledb_continuous_aggregate_info`,\neach taking a required `database` parameter.\n\n> **Scope note**: read-only discovery above goes through the query pipeline and therefore\n> stays usable on `read_only` databases; each handler checks for the `timescaledb`\n> extension first. Write-policy operations (hypertable creation, compression toggles,\n> add/remove retention or refresh policies) remain unexposed — use plain SQL through\n> the query/execute tools in the meantime. For detailed documentation, see\n> [TIMESCALEDB_TOOLS.md](docs/TIMESCALEDB_TOOLS.md).\n\n### Unified Tool Mode\n\nIf you connect many databases (5+), the per-database tool naming generates a large\nnumber of tools (5 × N). Some MCP clients — Claude in particular — apply strict\nlimits on the total number of tools and tool description size that can cause the\nagent to fail to load the server, ignore tools, or refuse to call them. Issue #18\ndocuments this exact symptom: \"the db-mcp-server does not function properly with\nClaude, even though it works fine with OpenAI\".\n\nFor these clients, launch the server with the `--unified-tools` flag to register six consolidated tools (`query`, `execute`, `transaction`, `performance`, `explain`, `describe`, `schema`, `filter_tables`) instead of per-database tools:\n\n```bash\n./bin/server -t stdio -c config.json --unified-tools\n```\n\n**Context-window cost (measured, `TestToolTokenBenchmark`, re-verified 2026-08 via `scripts/token-benchmark.sh`):** unified mode costs ~1.25–1.6k tokens **regardless of how many databases are connected**, while per-database mode costs ~800 tokens per database (7 tools each) — 10 connected databases ≈ 8k tokens, an 80% wire-payload saving with unified. With one database only, per-database naming is slightly cheaper; unified wins from two databases onward and scales flat thereafter. Re-measure the real wire payload yourself with `scripts/token-benchmark.sh`; methodology and results in [docs/benchmark-token-efficiency.md](docs/benchmark-token-efficiency.md).\n```\n\nIn unified mode, each tool accepts a required `database` parameter that names\nwhich database the call should target. See the [Configuration](#configuration)\nsection for the full list of available databases. This dramatically reduces the\ntool count and the cumulative description size, which resolves the Claude\ncompatibility issues.\n\nFor very large configurations, also enable `--lazy-loading` so that startup\ndoesn't open connections to databases that may never be queried during the\nsession.\n\n## Examples\n\n### Querying Multiple Databases\n\n```sql\n-- Query the MySQL database\nquery_mysql1(\"SELECT * FROM users LIMIT 10\")\n\n-- Query the PostgreSQL database in the same context\nquery_postgres1(\"SELECT * FROM products WHERE price > 100\")\n\n-- Query the SQLite database\nquery_sqlite_app(\"SELECT * FROM local_data WHERE created_at > datetime('now', '-1 day')\")\n\n-- Query the Oracle database\nquery_oracle_dev(\"SELECT * FROM employees WHERE hire_date > SYSDATE - 30\")\n```\n\n### Managing Transactions\n\nThe `transaction_<db_id>` tool supports `begin`, `execute`, `commit`, and `rollback` actions. Each `begin` returns a `transactionId`; pass it back to stage statements and to commit or roll back:\n\n```json\n// 1. Start a transaction\n{ \"action\": \"begin\" }\n// → { \"transactionId\": \"tx_mysql1_1730000000000000000\" }\n\n// 2. Execute statements within the transaction\n{\n  \"action\": \"execute\",\n  \"transactionId\": \"tx_mysql1_1730000000000000000\",\n  \"statement\": \"INSERT INTO orders (customer_id, product_id) VALUES (1, 2)\"\n}\n\n// 3a. Commit — persists all staged statements\n{ \"action\": \"commit\", \"transactionId\": \"tx_mysql1_1730000000000000000\" }\n\n// 3b. OR rollback — discards all staged statements\n{ \"action\": \"rollback\", \"transactionId\": \"tx_mysql1_1730000000000000000\" }\n```\n\nUnknown or already-retired transaction IDs return a clear error instead of a silent success, so agents can detect and recover from lost-transaction situations.\n\n### Exploring Database Schema\n\n```sql\n-- Get all tables in the database\nschema_mysql1(\"tables\")\n\n-- Get columns for a specific table\nschema_mysql1(\"columns\", \"users\")\n\n-- Get constraints\nschema_mysql1(\"constraints\", \"orders\")\n```\n\n### Working with SQLite-Specific Features\n\n```sql\n-- Create a table in SQLite\nexecute_sqlite_app(\"CREATE TABLE IF NOT EXISTS local_cache (key TEXT PRIMARY KEY, value TEXT, timestamp DATETIME)\")\n\n-- Use SQLite-specific date functions\nquery_sqlite_app(\"SELECT * FROM events WHERE date(created_at) = date('now')\")\n\n-- Query SQLite master table for schema information\nquery_sqlite_app(\"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'\")\n\n-- Performance optimization with WAL mode\nexecute_sqlite_app(\"PRAGMA journal_mode = WAL\")\nexecute_sqlite_app(\"PRAGMA synchronous = NORMAL\")\n```\n\n### Working with Oracle-Specific Features\n\n```sql\n-- Query user tables (excludes system schemas)\nquery_oracle_dev(\"SELECT table_name FROM user_tables ORDER BY table_name\")\n\n-- Use Oracle-specific date functions\nquery_oracle_dev(\"SELECT employee_id, hire_date FROM employees WHERE hire_date >= TRUNC(SYSDATE, 'YEAR')\")\n\n-- Oracle sequence operations\nexecute_oracle_dev(\"CREATE SEQUENCE emp_seq START WITH 1000 INCREMENT BY 1\")\nquery_oracle_dev(\"SELECT emp_seq.NEXTVAL FROM DUAL\")\n\n-- Oracle-specific data types\nquery_oracle_dev(\"SELECT order_id, TO_CHAR(order_date, 'YYYY-MM-DD HH24:MI:SS') FROM orders\")\n\n-- Get schema metadata from Oracle data dictionary\nquery_oracle_dev(\"SELECT column_name, data_type, nullable FROM user_tab_columns WHERE table_name = 'EMPLOYEES'\")\n\n-- Use Oracle analytic functions\nquery_oracle_dev(\"SELECT employee_id, salary, RANK() OVER (ORDER BY salary DESC) as salary_rank FROM employees\")\n```\n\n## Troubleshooting\n\n### Common Issues\n\n- **Connection Failures**: Verify network connectivity and database credentials\n- **Permission Errors**: Ensure the database user has appropriate permissions\n- **Timeout Issues**: Check the `query_timeout` setting in your configuration\n\n### Logs\n\nEnable verbose logging for troubleshooting:\n\n```bash\n./bin/server -t sse -c config.json -v\n```\n\n## Testing\n\n### Running Tests\n\nThe project includes comprehensive unit and integration tests for all supported databases.\n\n#### Unit Tests\n\nRun unit tests (no database required):\n\n```bash\nmake test\n# or\ngo test -short ./...\n```\n\n#### Integration Tests\n\nIntegration tests require running database instances. We provide Docker Compose configurations for easy setup.\n\n**Test All Databases:**\n\n```bash\n# Start test databases\ndocker-compose -f docker-compose.test.yml up -d\n\n# Run all integration tests\ngo test ./... -v\n\n# Stop test databases\ndocker-compose -f docker-compose.test.yml down -v\n```\n\n**Test Oracle Database:**\n\n```bash\n# Start Oracle test environment\n./oracle-test.sh start\n\n# Run Oracle tests\n./oracle-test.sh test\n# or manually\nORACLE_TEST_HOST=localhost go test -v ./pkg/db -run TestOracle\nORACLE_TEST_HOST=localhost go test -v ./pkg/dbtools -run TestOracle\n\n# Stop Oracle test environment\n./oracle-test.sh stop\n\n# Full cleanup (removes volumes)\n./oracle-test.sh cleanup\n```\n\n**Test TimescaleDB:**\n\n```bash\n# Start TimescaleDB test environment\n./timescaledb-test.sh start\n\n# Run TimescaleDB tests\nTIMESCALEDB_TEST_HOST=localhost go test -v ./pkg/db/timescale ./internal/delivery/mcp\n\n# Stop TimescaleDB test environment\n./timescaledb-test.sh stop\n```\n\n#### Regression Tests\n\nRun comprehensive regression tests across all database types:\n\n```bash\n# Ensure all test databases are running\ndocker-compose -f docker-compose.test.yml up -d\n./oracle-test.sh start\n\n# Run regression tests\nMYSQL_TEST_HOST=localhost \\\nPOSTGRES_TEST_HOST=localhost \\\nORACLE_TEST_HOST=localhost \\\ngo test -v ./pkg/db -run TestRegression\n\n# Run connection pooling tests\ngo test -v ./pkg/db -run TestConnectionPooling\n```\n\n### Continuous Integration\n\nAll tests run automatically on every pull request via GitHub Actions. The CI pipeline includes:\n\n- **Unit Tests**: Fast tests that don't require database connections\n- **Integration Tests**: Tests against MySQL, PostgreSQL, SQLite, and Oracle databases\n- **Regression Tests**: Comprehensive tests ensuring backward compatibility\n- **Linting**: Code quality checks with golangci-lint\n\n## Contributing\n\nWe welcome contributions to the DB MCP Server project! To contribute:\n\n1. Fork the repository\n2. Create a feature branch (`git checkout -b feature/amazing-feature`)\n3. Commit your changes (`git commit -m 'feat: add amazing feature'`)\n4. Push to the branch (`git push origin feature/amazing-feature`)\n5. Open a Pull Request\n\nPlease see our [CONTRIBUTING.md](docs/CONTRIBUTING.md) file for detailed guidelines.\n\n### Testing Your Changes\n\nBefore submitting a pull request, please ensure:\n\n1. All unit tests pass: `go test -short ./...`\n2. Integration tests pass for affected databases\n3. Code follows the project's style guidelines: `golangci-lint run ./...`\n4. New features include appropriate test coverage\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n"}