{"id":44414637,"url":"https://github.com/lukleh/mcp-read-only-sql","last_synced_at":"2026-02-12T08:20:01.047Z","repository":{"id":316059528,"uuid":"1061773666","full_name":"lukleh/mcp-read-only-sql","owner":"lukleh","description":"Secure MCP server providing read-only SQL access to PostgreSQL and ClickHouse databases","archived":false,"fork":false,"pushed_at":"2026-02-11T13:19:46.000Z","size":297,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-11T21:41:30.440Z","etag":null,"topics":["clickhouse","database","mcp","postgresql","read-only","security","sql"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lukleh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-09-22T11:13:36.000Z","updated_at":"2026-02-11T13:19:50.000Z","dependencies_parsed_at":null,"dependency_job_id":"023eb58e-60d7-42c4-aca7-ec82f1e612fd","html_url":"https://github.com/lukleh/mcp-read-only-sql","commit_stats":null,"previous_names":["lukleh/mcp-read-only-sql"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/lukleh/mcp-read-only-sql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukleh%2Fmcp-read-only-sql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukleh%2Fmcp-read-only-sql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukleh%2Fmcp-read-only-sql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukleh%2Fmcp-read-only-sql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lukleh","download_url":"https://codeload.github.com/lukleh/mcp-read-only-sql/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukleh%2Fmcp-read-only-sql/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29361813,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-12T01:03:07.613Z","status":"online","status_checked_at":"2026-02-12T02:00:06.911Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clickhouse","database","mcp","postgresql","read-only","security","sql"],"created_at":"2026-02-12T08:20:00.257Z","updated_at":"2026-02-12T08:20:01.040Z","avatar_url":"https://github.com/lukleh.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MCP Read-Only SQL Server\n\n[![Tests](https://github.com/lukleh/mcp-read-only-sql/actions/workflows/test.yml/badge.svg)](https://github.com/lukleh/mcp-read-only-sql/actions/workflows/test.yml)\n\nA secure MCP (Model Context Protocol) server that provides **read-only** SQL access to PostgreSQL and ClickHouse databases with built-in safety features.\n\n## Security\n\nThe server implements a **three-layer security model**:\n\n1. **Database-level read-only** - Sessions forced to read-only mode\n2. **Timeout protection** - Connection timeout (5s), query timeout (10s) - configurable per connection\n3. **Result size limits** - Default 5KB, prevents memory exhaustion\n\nAll write operations (INSERT, UPDATE, DELETE, etc.) are blocked at the database level.\n\n### How Read-Only Is Enforced\n\n- **PostgreSQL (Python)** – Connections are opened with `default_transaction_read_only=on`, sessions are set to read-only, and every statement runs with a configurable `statement_timeout`.\n- **PostgreSQL (CLI)** – Queries are wrapped in a transaction that issues `SET TRANSACTION READ ONLY;` before execution. Input is sanitized so only a single statement (plus optional trailing semicolon) is forwarded, transaction-control keywords are rejected up front, and all `psql` invocations include `--single-transaction`, `-v ON_ERROR_STOP=1`, and `PGOPTIONS=-c default_transaction_read_only=on` for defence in depth.\n- **ClickHouse (Python)** – The driver sets `readonly=1` plus connection/query timeouts, forcing the server to reject any write or DDL attempt.\n- **ClickHouse (CLI)** – `clickhouse-client` is invoked with `--readonly=1`, `--max_execution_time`, and connection timeouts, turning the session into a read-only context.\n\nThe shared connector base also applies hard timeouts and result-size ceilings, giving the MCP server deterministic behaviour even if the database misbehaves.\n\nSee [READ_ONLY_ENFORCEMENT_MATRIX.md](READ_ONLY_ENFORCEMENT_MATRIX.md) for a statement-by-statement view of every write-capable command and the tests that enforce it.\n\n## Key Features\n\n- **Read-only enforcement** - Multiple layers of protection against writes\n- **Multi-database support** - PostgreSQL and ClickHouse\n- **Dual implementations** - Choose between Python (pure Python, no dependencies) or CLI (uses `psql`/`clickhouse-client`)\n- **SSH tunnel support** - Both implementations support key authentication; Python uses Paramiko for passwords and CLI uses `sshpass` for password-based tunnels\n- **Security built-in** - Timeouts, size limits, session controls\n- **DBeaver import** - Import existing connections easily\n\n## Prerequisites\n\n- [uv](https://github.com/astral-sh/uv) - Fast Python package installer and resolver\n- [just](https://github.com/casey/just) - Command runner (for project tasks)\n\n## Quick Start\n\n### 1. Install Dependencies\n\n```bash\nuv sync\n```\n\nIf you plan to use CLI connectors with SSH password authentication, install `sshpass` as well (for example, `brew install sshpass` on macOS or `apt-get install sshpass` on Debian-based Linux).\n\n### 2. Configure Database Connections\n\n**Option A: Create from sample**\n```bash\ncp connections.yaml.sample connections.yaml\n# Edit connections.yaml with your database details\n```\n\n**Option B: Import from DBeaver**\n```bash\njust import-dbeaver\n# This creates connections.yaml from your DBeaver workspace\n```\n\n\u003e **Note:** The server reads `connections.yaml` during startup. Restart the MCP\n\u003e process after editing the file so changes take effect.\n\nTo allow a connection to access multiple databases, add an explicit allowlist:\n\n```yaml\n- connection_name: analytics_multi\n  type: postgresql\n  servers:\n    - \"analytics.example.com:5432\"\n  allowed_databases:\n    - analytics\n    - reporting\n  default_database: analytics\n  username: analyst\n```\n\nIf you only set `db`, that single database is implicitly the allowlist.\n\n### 3. Validate and Test Connections\n\n```bash\n# Validate configuration file\njust validate\n\n# Test database connectivity\njust test-connection              # Test all connections\njust test-connection my_postgres  # Test specific connection\n```\n\n### 4. Add MCP Server to Your Client\n\n**For Claude Code:**\n```bash\nclaude mcp add mcp-read-only-sql -- uv --directory {PATH_TO_MCP_READ_ONLY_SQL} run python -m src.server\n```\n\n**For Codex:**\n```bash\ncodex mcp add mcp-read-only-sql -- uv --directory {PATH_TO_MCP_READ_ONLY_SQL} run python -m src.server\n```\n\nReplace `{PATH_TO_MCP_READ_ONLY_SQL}` with the full path to where you cloned this repository (e.g., `/Users/yourname/projects/mcp-read-only-sql`).\n\n## MCP Tools\n\n### `run_query_read_only`\nExecute read-only SQL queries on configured databases.\n\n```json\n{\n  \"connection_name\": \"my_postgres\",\n  \"query\": \"SELECT * FROM users LIMIT 10\",\n  \"database\": \"analytics\",\n  \"server\": \"db2.example.com\",\n  \"file_path\": \"~/Downloads/query.tsv\"\n}\n```\n\n**Parameters:**\n- `connection_name` (required): Identifier returned by list_connections\n- `query` (required): SQL text that must remain read-only\n- `database` (optional): Database to use (must be listed in the connection's allowlist).\n- `server` (optional): Hostname to target a specific server. If not provided, uses the first server in the connection's list.\n- `file_path` (optional): When provided, results are written to this path (parents created if needed) and the tool returns the absolute path string instead of TSV content. The file must not already exist; if it does, the tool returns an error instead of overwriting. The result-size limit is skipped when saving to a file so the full output is streamed to disk.\n\n**Returns:** Tab-separated text (TSV) with a header row followed by data rows.\nThe structured MCP payload mirrors the same TSV string. If results exceed\n`max_result_bytes`, a trailing notice indicates truncation. When `file_path`\nis supplied, the returned value is the absolute path of the written file, the tool refuses to overwrite existing files, and result-size truncation is not applied (full result is written).\n\n### `list_connections`\nList all available database connections.\n\n**Returns:** Tab-separated text with columns `name`, `type`, `description`,\n`servers`, `database`, `databases`, and `user`. `database` is the default database,\nwhile `databases` lists the allowlisted databases (comma-separated). The `servers`\ncolumn lists comma-separated hostnames after resolving SSH/VPN tunnels, so entries\nreflect the endpoints the agent should reference.\n\n## Implementation Matrix\n\n### Database Support by Implementation\n\n| Feature | PostgreSQL CLI | PostgreSQL Python | ClickHouse CLI | ClickHouse Python |\n|---------|---------------|-------------------|----------------|-------------------|\n| **Protocol** | Native PostgreSQL | Native PostgreSQL | Native ClickHouse | HTTP/HTTPS |\n| **Default Port** | 5432 | 5432 | 9000 | 8123 |\n| **Supported Ports** | Any PostgreSQL port | Any PostgreSQL port | 9000, 9440 (native + TLS) | 8123 (HTTP), 8443 (HTTPS) |\n| **TLS/SSL Support** | ✅ Yes | ✅ Yes | ✅ Yes (--secure for 9440) | ✅ Yes (HTTPS on 8443) |\n| **Read-Only Method** | `SET TRANSACTION READ ONLY` | `default_transaction_read_only=on` | `--readonly=1` flag | `readonly=1` setting |\n| **SSH Key Auth** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |\n| **SSH Password Auth** | ✅ Yes (requires `sshpass`) | ✅ Yes (Paramiko) | ✅ Yes (requires `sshpass`) | ✅ Yes (Paramiko) |\n| **Timeout Control** | ✅ Via SQL | ✅ Driver-level | ✅ CLI flags | ✅ Driver-level |\n| **Result Streaming** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |\n| **Binary Required** | `psql` | None | `clickhouse-client` | None |\n\n### ClickHouse Port Compatibility\n\n| Port | Protocol | CLI Support | Python Support | Notes |\n|------|----------|-------------|----------------|-------|\n| 8123 | HTTP | ⚠️ Auto-converts to 9000 | ✅ Native support | Default HTTP interface |\n| 8443 | HTTPS | ⚠️ Auto-converts to 9440 (--secure) | ✅ Native support | Secure HTTP interface |\n| 9000 | Native TCP | ✅ Native support | ⚠️ Auto-converts to 8123 | Default native protocol |\n| 9440 | Native TCP (TLS) | ✅ Native support (--secure) | ⚠️ Auto-converts to 8443 (HTTPS) | Secure native protocol |\n| Custom (e.g., 2650) | Usually HTTP | ❌ No conversion | ✅ Yes | HAProxy/Load balancers - NO auto-conversion |\n\n**Important Notes:**\n- **ClickHouse CLI** (`clickhouse-client`) uses native protocol ports (9000, 9440)\n- **ClickHouse Python** (using `clickhouse-connect`) uses HTTP/HTTPS ports (8123, 8443)\n- Port mismatches are automatically handled - see below\n\n**Automatic Port Handling (Bidirectional):**\n\n*ClickHouse Python Implementation:*\n- **Direct connections**: Port 9000 → automatically uses port 8123 on the same host\n- **SSH tunnels**: Port 9000 → automatically tunnels to remote port 8123\n- **SSH tunnels**: Port 9440 → automatically tunnels to remote port 8443\n\n*ClickHouse CLI Implementation:*\n- **Direct connections**: Port 8123 → automatically uses port 9000 on the same host\n- **SSH tunnels**: Port 8123 → automatically tunnels to remote port 9000\n- **SSH tunnels**: Port 8443 → automatically tunnels to remote port 9440\n\n✨ **This means you can use the same configuration for both CLI and Python implementations, regardless of which port you specify (8123 or 9000) - each implementation will automatically convert to the correct protocol port it needs!**\n\n### Choosing an Implementation\n\n**Use CLI implementation when:**\n- You have the database CLI tools installed (`psql`, `clickhouse-client`)\n- You prefer not to install Python database drivers\n- You're connecting to ClickHouse on native ports (9000, 9440)\n- You want the exact behavior of the official CLI tools\n\n**Use Python implementation when:**\n- You want a pure Python solution with no external dependencies\n- You're connecting to ClickHouse HTTP interface (port 8123, 8443)\n- You need SSH password authentication without installing `sshpass`\n- You want more programmatic control over connections\n\n## Configuration Notes\n\n### HAProxy and Custom Ports\n\nWhen using **HAProxy** or other proxy servers with ClickHouse:\n\n- **HAProxy typically provides HTTP interface** on custom ports (e.g., 2650, 8000, etc.)\n- **Custom ports are NOT auto-converted** - the system only converts standard ports (8123, 8443, 9000, 9440)\n- **For HAProxy connections**: Use `implementation: python` since HAProxy usually proxies HTTP traffic\n- If you get \"Unexpected packet\" errors with CLI on custom ports, switch to Python implementation\n\nExample HAProxy configuration:\n```yaml\n- connection_name: clickhouse_haproxy\n  type: clickhouse\n  servers:\n  - haproxy-server:2650  # Custom HAProxy port\n  implementation: python  # Use Python for HTTP protocol\n  # ... other settings\n```\n\n### Multiple Servers\nWhen multiple servers are specified in a connection's configuration, the system currently uses only the first server in the list. Load balancing across servers is not implemented.\n\n### SSH Authentication\n- **Python implementation**: Supports both SSH password authentication (via `SSH_PASSWORD_\u003cCONNECTION_NAME\u003e` environment variable) and SSH key files\n- **CLI implementation**: Supports key-based authentication and can use passwords when `sshpass` is installed\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukleh%2Fmcp-read-only-sql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flukleh%2Fmcp-read-only-sql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukleh%2Fmcp-read-only-sql/lists"}