MCPInstall
mcp-database-read-prototype logo

mcp-database-read-prototype

by riefer02 1 stars riefer02/mcp-database-read-prototype

Model context protocol for reading information from a database.

READMEmain

Database Read MCP Server

A Model Context Protocol (MCP) server for read-only PostgreSQL access over stdio.

What you get

  • Read-only DB access over MCP: list tables, inspect schemas, run SELECT/WITH queries, EXPLAIN plans
  • Parse-based write protection (rejects INSERT/UPDATE/DELETE/MERGE/DROP/CREATE/ALTER/TRUNCATE and multi-statement payloads)
  • Per-transaction READ ONLY + statement/lock/idle timeouts + hard row caps + streamed batched fetches
  • Per-environment connection pools (local/staging/production/...) selectable per call
  • Schema allowlist (default: public)

Requirements

  • Python 3.10+
  • PostgreSQL
  • An MCP-compatible client (Cursor, Claude Desktop, Codex, ...)
  • uv for project management

The official Python MCP SDK is constrained to its maintained v1 line (mcp[cli]>=1.30.0,<2); v2 is a breaking upgrade and is intentionally excluded until this server is migrated.

Install

uv sync

Connection environment variables

VariablePurpose
DATABASE_URLDefault connection string
DATABASE_URL_<ENV>Per-environment connection (e.g. DATABASE_URL_LOCAL, DATABASE_URL_STAGING, DATABASE_URL_PRODUCTION)
DATABASE_TARGET_ENVSelects active environment (aliases: DATABASE_ENV, DB_ENV; values like dev/prod/stage normalize to local/production/staging)

Every MCP tool also accepts an environment argument to override per-call without restarting the server.

Safety / tuning environment variables

VariableDefaultPurpose
DB_STATEMENT_TIMEOUT_MS60000Per-query timeout
DB_MAX_STATEMENT_TIMEOUT_MS300000 (or DB_STATEMENT_TIMEOUT_MS if higher)Maximum per-call timeout; must be at least DB_STATEMENT_TIMEOUT_MS
DB_LOCK_TIMEOUT_MS15000Lock acquisition timeout
DB_IDLE_IN_TRANSACTION_TIMEOUT_MS60000Kills idle-in-txn sessions
DB_MAX_ROWS10000Hard row cap (truncation flagged in response)
DB_TOOL_CALLS_PER_MINUTE120Per-process sliding-window limit for MCP tool calls
DB_FETCHMANY_SIZE1000Batch fetch size while streaming
DB_POOL_SIZE5Connections per environment
DB_MAX_OVERFLOW2Pool overflow
DB_POOL_TIMEOUT30Pool wait timeout (s)
DB_POOL_RECYCLE1800Recycle connections after (s)
DB_ALLOWED_SCHEMASpublicComma-separated schemas exposed to tools

Available tools

ToolPurpose
health_checkDatabase + server connectivity check
database_queryRun a read-only SQL query (SELECT/WITH); supports bound params, max_rows, offset, statement_timeout_ms, environment
explain_queryEXPLAIN [ANALYZE] for a query, JSON plan
list_tablesTables in the chosen schema
get_table_schemaColumns + primary keys for one table
get_all_schemasBulk dump: columns + primary keys (2 queries total); sample_data is opt-in via include_samples=true

database_query response shape:

{
  "status": "success",
  "results": [...],
  "count": 42,
  "truncated": false,
  "offset": 0,
  "max_rows": 10000,
  "statement_timeout_ms": 60000,
  "environment": "default"
}

max_rows is clamped to DB_MAX_ROWS. Per-call statement_timeout_ms must be positive and no greater than DB_MAX_STATEMENT_TIMEOUT_MS.

Pass query values separately in params rather than interpolating them into SQL:

{
  "query": "SELECT id, name FROM users WHERE email = :email",
  "params": {"email": "user@example.com"}
}

The internal bind names _row_limit and _row_offset are reserved for server pagination.

Client setup

Cursor

Cursor reads MCP config from .cursor/mcp.json (project) or ~/.cursor/mcp.json (global).

{
  "mcpServers": {
    "database-reader": {
      "command": "uv",
      "args": ["--directory", "${workspaceFolder}", "run", "database_read.py"],
      "env": {
        "DATABASE_TARGET_ENV": "local",
        "DATABASE_URL_LOCAL": "${env:DATABASE_URL_LOCAL}",
        "DATABASE_URL_STAGING": "${env:DATABASE_URL_STAGING}",
        "DATABASE_URL_PRODUCTION": "${env:DATABASE_URL_PRODUCTION}"
      }
    }
  }
}

Claude Desktop

Config path: ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %AppData%\Claude\claude_desktop_config.json (Windows). Use absolute paths.

{
  "mcpServers": {
    "database-reader": {
      "command": "uv",
      "args": ["--directory", "/ABSOLUTE/PATH/TO/mcp-prototype", "run", "database_read.py"],
      "env": {
        "DATABASE_TARGET_ENV": "local",
        "DATABASE_URL_LOCAL": "postgresql://user:password@localhost:5432/db_name"
      }
    }
  }
}

OpenAI Codex

codex mcp add database-reader \
  --env DATABASE_TARGET_ENV=local \
  --env DATABASE_URL_LOCAL='postgresql://user:password@localhost:5432/db_name' \
  -- uv --directory /ABSOLUTE/PATH/TO/mcp-prototype run database_read.py

Or ~/.codex/config.toml:

[mcp_servers.database-reader]
command = "uv"
args = ["--directory", "/ABSOLUTE/PATH/TO/mcp-prototype", "run", "database_read.py"]

[mcp_servers.database-reader.env]
DATABASE_TARGET_ENV = "local"
DATABASE_URL_LOCAL = "postgresql://user:password@localhost:5432/db_name"

Other MCP clients

Any stdio MCP client accepts the same fields: command = "uv", args = ["--directory", "<repo>", "run", "database_read.py"], plus env entries for connection URLs.

Switching environments per call

{
  "name": "database_query",
  "arguments": {
    "query": "SELECT * FROM users LIMIT 5",
    "environment": "staging"
  }
}

The server keeps a separate connection pool per environment, so switching does not require a restart.

Hardening: dedicated read-only role

The MCP enforces read-only at the application layer (parse-time validator, SET TRANSACTION READ ONLY, dangerous-function blacklist). The strongest fourth layer is a Postgres role with SELECT-only grants — the database itself refuses writes even if every line of Python were replaced. See docs/database-role-setup.md for the minimal setup, triage query examples, and the optional view-based pattern for redacting sensitive columns without changing MCP code.

Tests

See AGENTS.md for the regression suite (unit + integration).

Notes

  • SQLAlchemy requires postgresql:// (not postgres://).
  • Restart the MCP client after editing its config.
  • Never commit real credentials; use shell env vars or a secret manager.