Skip to main content
Glama
IBeanCN

mysql-readonly-mcp

by IBeanCN
README.md
# MySQL Readonly MCP Server

A Python MCP (Model Context Protocol) server that provides **readonly** access to a MySQL database.

- Chinese documentation: `README.zh-CN.md`

- Transport: **stdio** (recommended for MCP clients) and **HTTP/SSE** (standalone service)
- Safety: Only `SELECT`, `SHOW`, `DESCRIBE`, `DESC`, and `EXPLAIN` statements are permitted
- Config: **stdio** — MySQL settings via `mcp.json` `env`; **SSE** — environment at process start, optional `MCP_BEARER_TOKEN` for HTTP auth
- Table blacklist: `QUERY_TABLE_BLACKLIST` blocks **data** access via `query`; **`describe_table` still works** for schema on listed tables

---

## Requirements

- **Installer and MCP runtime:** Python **3.10+**. `install.py` checks the interpreter
  that runs it **before** any other step; if the version is too low, it exits immediately
  (use `python3.12 install.py`, `py -3.12 install.py`, etc.).
- **Optional:** `install.py --python /path/to/python3.12` creates `.venv` with that
  binary instead of `sys.executable` (that binary must also be 3.10+).
- A reachable MySQL instance.

## Installation

### First-time install (recommended)

Run `install.py` with a **3.10+** interpreter (`python`, `python3`, or `py -3`).

The script creates `.venv` with `python -m venv`, then `python -m pip install -r requirements.txt`.

After a successful install, an **interactive wizard** (if stdin is a TTY) asks for:
transport (**stdio** or **sse**), core MySQL fields, **QUERY_TABLE_BLACKLIST** (always),
optional timeouts / **QUERY_DEFAULT_LIMIT** / TLS paths, and for SSE **MCP_HOST** /
**MCP_PORT** / **MCP_BEARER_TOKEN**. It prints a **complete `mcp.json` snippet** (and for
SSE, shell `export` lines plus the server command). Use **`--no-wizard`** to skip (CI /
automation). Non-interactive stdin skips the wizard automatically.

**Windows** (CMD or double-click in Explorer):

```bat
cd \path\to\MySQL_MCP
install.bat
```

**macOS / Linux**:

```bash
cd /path/to/MySQL_MCP
python3 install.py
```

Useful flags:

| Flag | Meaning |
|------|---------|
| `--recreate` | Delete `.venv` and reinstall |
| `--dry-run` | Show the planned venv/pip steps only (still requires 3.10+ to run the script) |
| `--no-wizard` | Do not run the post-install configuration wizard |
| `--python EXE` | Create `.venv` with this 3.10+ interpreter (`EXE` on PATH or full path) |

If the Python Launcher is missing on Windows, install Python 3.10+ from [python.org](https://www.python.org/downloads/) and enable “Add to PATH”.

### Manual install (without the script)

```bash
cd /path/to/MySQL_MCP
python3 -m venv .venv
# Windows: .venv\Scripts\pip install -r requirements.txt
# Unix:    .venv/bin/pip install -r requirements.txt
```

After either method, set `mcp.json` `command` to the **venv** `python` / `python.exe`
absolute path — not a bare `python` on PATH.

---

## Configuration via mcp.json

All MySQL connection parameters are passed through the standard MCP configuration file.
Choose **Option A** (stdio) or **Option B** (SSE) depending on your client.

### Option A — stdio transport (recommended)

The MCP client launches `server.py` as a subprocess and injects MySQL credentials via the
`env` block.  No separate server process is needed.

Copy the following block into your client's MCP settings (e.g. Cursor `mcp.json`,
Claude Desktop `claude_desktop_config.json`, or a project-level `.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "mysql-readonly": {
      "command": "/absolute/path/to/MySQL_MCP/.venv/bin/python",
      "args": ["/absolute/path/to/MySQL_MCP/server.py"],
      "env": {
        "MYSQL_HOST": "127.0.0.1",
        "MYSQL_PORT": "3306",
        "MYSQL_USER": "your_mysql_user",
        "MYSQL_PASSWORD": "your_mysql_password",
        "MYSQL_DATABASE": "your_database_name",
        "MYSQL_CONNECT_TIMEOUT": "10",
        "MYSQL_SSL": "false",
        "QUERY_DEFAULT_LIMIT": "100",
        "QUERY_TABLE_BLACKLIST": "sensitive_table,internal_audit_log"
      }
    }
  }
}
```

`args` must contain the **absolute path** to `server.py`.  
On Windows, use `"command": "C:\\path\\to\\MySQL_MCP\\.venv\\Scripts\\python.exe"` (escape backslashes in JSON).

Replace the `env` values with your actual MySQL credentials.

### Option B — HTTP/SSE transport

Start the server as a standalone HTTP service first. MySQL settings come from the
process environment (or your shell / systemd / Docker `environment` block).

```bash
export MYSQL_HOST=127.0.0.1
export MYSQL_PORT=3306
export MYSQL_USER=your_mysql_user
export MYSQL_PASSWORD=your_mysql_password
export MYSQL_DATABASE=your_database_name

# Default bind is 127.0.0.1 (safer). Use 0.0.0.0 only on trusted networks or
# behind a reverse proxy; set MCP_BEARER_TOKEN so clients must send
# Authorization: Bearer <token> on SSE and message requests.
export MCP_BEARER_TOKEN=your-long-random-secret   # optional but recommended if exposed

# Use the venv interpreter (from repo root after install):
# Unix/macOS:  .venv/bin/python server.py --transport sse --port 8000
# Windows:     .venv\Scripts\python.exe server.py --transport sse --port 8000
.venv/bin/python server.py --transport sse --port 8000
```

Then point your MCP client at the SSE endpoint (and configure the client to send
the Bearer token if `MCP_BEARER_TOKEN` is set):

```json
{
  "mcpServers": {
    "mysql-readonly": {
      "url": "http://localhost:8000/sse"
    }
  }
}
```

---

## Environment Variables Reference

| Variable                    | Required | Default      | Description |
|-----------------------------|----------|--------------|-------------|
| `MYSQL_HOST`                | No       | `127.0.0.1`  | MySQL hostname or IP |
| `MYSQL_PORT`                | No       | `3306`       | MySQL port |
| `MYSQL_USER`                | **Yes**  | —            | MySQL username |
| `MYSQL_PASSWORD`            | No       | `""`         | MySQL password |
| `MYSQL_DATABASE`            | **Yes**  | —            | Target database name |
| `MYSQL_CONNECT_TIMEOUT`     | No       | `10`         | TCP connect timeout (seconds) |
| `MYSQL_READ_TIMEOUT`        | No       | `30`         | Socket read timeout (seconds) |
| `MYSQL_WRITE_TIMEOUT`       | No       | `30`         | Socket write timeout (seconds) |
| `MYSQL_MAX_EXECUTION_TIME`  | No       | `30000`      | Per-query server limit (milliseconds); `SET SESSION MAX_EXECUTION_TIME` |
| `MYSQL_SSL`                 | No       | `false`      | Enable TLS to MySQL: `"true"` / `"false"` |
| `MYSQL_SSL_CA`              | No       | `""`         | Path to CA certificate (when using TLS) |
| `MYSQL_SSL_CERT`            | No       | `""`         | Path to client certificate |
| `MYSQL_SSL_KEY`             | No       | `""`         | Path to client private key |
| `MYSQL_SSL_VERIFY_CERT`     | No       | `true`       | Set `"false"` to skip server cert verification (not recommended) |
| `QUERY_DEFAULT_LIMIT`       | No       | `100`        | Upper bound on rows for `SELECT`; explicit `LIMIT` is also capped to this (after applying the `limit` tool argument) |
| `QUERY_TABLE_BLACKLIST`     | No       | `""`         | Comma-separated table names. The **`query` tool** rejects SQL that references them (including `JOIN`); **`describe_table` still returns schema** for those tables. Not a substitute for DB grants. |
| `MCP_HOST`                  | No       | `127.0.0.1`  | Bind address for SSE transport (`CLI --host` overrides at runtime) |
| `MCP_PORT`                  | No       | `8000`       | Bind port for SSE (`CLI --port` overrides) |
| `MCP_BEARER_TOKEN`          | No       | `""`         | If non-empty, SSE HTTP requests require `Authorization: Bearer <token>` |

---

## Available Tools

### `query`

Execute a readonly SQL statement and return results as structured JSON.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `sql`     | str  | —       | SQL statement (`SELECT` / `SHOW` / `DESCRIBE` / `EXPLAIN`) |
| `limit`   | int  | `QUERY_DEFAULT_LIMIT` | Capped at `QUERY_DEFAULT_LIMIT`; combined with server rewrite so `SELECT` never returns more rows than this effective cap (even if SQL contains a larger `LIMIT`) |

**Returns:**
```json
{
  "columns": ["id", "name", "email"],
  "rows": [
    {"id": 1, "name": "Alice", "email": "alice@example.com"}
  ],
  "row_count": 1
}
```

### `list_tables`

List all tables in the configured database.

**Returns:**
```json
{
  "database": "mydb",
  "tables": ["users", "orders", "products"],
  "count": 3
}
```

### `describe_table`

Get the column schema of a specific table.

For tables in `QUERY_TABLE_BLACKLIST`, use this tool for schema — the `query` tool
rejects any SQL (including `DESCRIBE`) that references those tables.

| Parameter    | Type | Description         |
|--------------|------|---------------------|
| `table_name` | str  | Table name (letters, digits, underscores only) |

**Returns:**
```json
{
  "table": "users",
  "columns": [
    {"Field": "id",    "Type": "int",          "Null": "NO",  "Key": "PRI", "Default": null, "Extra": "auto_increment"},
    {"Field": "name",  "Type": "varchar(255)", "Null": "YES", "Key": "",    "Default": null, "Extra": ""},
    {"Field": "email", "Type": "varchar(255)", "Null": "YES", "Key": "UNI", "Default": null, "Extra": ""}
  ]
}
```

---

## Security — Readonly Enforcement

The server enforces readonly access at the application layer with a two-stage guard:

1. **Whitelist** — the first keyword must be one of `SELECT`, `SHOW`, `DESCRIBE`, `DESC`, `EXPLAIN`.
2. **Blacklist** — the full statement is scanned for forbidden patterns: `INSERT`, `UPDATE`,
   `DELETE`, `DROP`, `ALTER`, `CREATE`, `TRUNCATE`, `REPLACE`, `GRANT`, `REVOKE`,
   `COMMIT`, `ROLLBACK`, `LOAD DATA`, `INTO OUTFILE`, `SLEEP`, `BENCHMARK`, and more.
3. **Multi-statement rejection** — any SQL containing `;` (after stripping a single trailing
   semicolon) is rejected.
4. **Identifier validation** — table names passed to `describe_table` are validated to contain
   only `[A-Za-z0-9_]` characters before being interpolated into the query.
5. **Table blacklist** — tables listed in `QUERY_TABLE_BLACKLIST` cannot be used for **data**
   access through the `query` tool (including subqueries / `JOIN`s that reference them).
   The **`describe_table` tool is still allowed** for those names so agents can inspect schema.
   Errors use a bilingual JSON payload (`message_en` / `message_zh`).

> For production use, also configure the MySQL user with `SELECT`-only privileges at the
> database level as an additional layer of defense. Treat the app-level blacklist as a
> convenience, not the primary authorization boundary.

---

## Verification

After configuration, test the server with these queries:

```sql
-- Should succeed
SHOW TABLES
SELECT * FROM your_table LIMIT 5
DESCRIBE your_table
EXPLAIN SELECT id FROM your_table

-- Should be rejected with an error
DELETE FROM your_table WHERE id = 1
INSERT INTO your_table (name) VALUES ('x')
SELECT 1; DROP TABLE your_table
SELECT SLEEP(5)
```

---

## Running Locally (without an MCP client)

You can test the server directly from the command line using the MCP CLI:

```bash
# Install dev dependency
pip install "mcp[cli]"

# stdio mode — interactive inspector
MYSQL_HOST=127.0.0.1 MYSQL_USER=root MYSQL_PASSWORD=secret MYSQL_DATABASE=mydb \
  mcp dev server.py

# SSE mode — start server, then open http://localhost:8000/sse in a browser or curl
MYSQL_HOST=127.0.0.1 MYSQL_USER=root MYSQL_PASSWORD=secret MYSQL_DATABASE=mydb \
  .venv/bin/python server.py --transport sse
```

---

## Project Structure

```
MySQL_MCP/
├── server.py            # MCP server: tools, SQL guard, MySQL connector
├── install.py           # First-time setup: venv + pip install (all platforms)
├── install.bat          # Windows launcher for install.py
├── mcp.json             # MCP configuration template (stdio + SSE examples)
├── requirements.txt     # Python dependencies
├── pyproject.toml       # Package metadata
├── Dockerfile           # Container image definition
├── docker-compose.yml   # Compose file (MCP server + optional local MySQL)
└── README.md            # This file
```

---

## Docker Deployment

### Build and run with Docker Compose

Create a `.env` file in the project root with your MySQL credentials:

```bash
MYSQL_HOST=host.docker.internal   # use host.docker.internal to reach the host machine
MYSQL_PORT=3306
MYSQL_USER=your_mysql_user
MYSQL_PASSWORD=your_mysql_password
MYSQL_DATABASE=your_database_name
```

Uncomment the `ports` block in [`docker-compose.yml`](docker-compose.yml) (see the
`127.0.0.1:${MCP_PORT:-8000}:8000` example) so the host can reach the container; it is
commented out by default for safety.

Then start the container:

```bash
docker compose up -d
```

Set `MCP_BEARER_TOKEN` in `.env` when exposing SSE. The MCP SSE endpoint is then
`http://localhost:8000/sse` (or the mapped host/port you chose).

Point your MCP client at it:

```json
{
  "mcpServers": {
    "mysql-readonly": {
      "url": "http://localhost:8000/sse"
    }
  }
}
```

### Build and run manually

```bash
docker build -t mysql-mcp-server .

docker run -d \
  --name mysql-mcp-server \
  -p 8000:8000 \
  -e MYSQL_HOST=host.docker.internal \
  -e MYSQL_USER=your_user \
  -e MYSQL_PASSWORD=your_password \
  -e MYSQL_DATABASE=your_db \
  mysql-mcp-server
```

### Using with a local MySQL container

Uncomment the `mysql` service block in `docker-compose.yml` to spin up a local MySQL
alongside the MCP server.  The service uses a `healthcheck` so the MCP server only
starts after MySQL is ready.

---

## Extending

The following improvements are recommended before production use:

- **Connection pooling** — replace per-request connections with `DBUtils` or `SQLAlchemy` pool
- **SQL AST validation** — use `sqlglot` or `sqlparse` for structural analysis instead of regex
- **Audit logging** — log every executed query with timestamp, client identity, and row count
- **Row-level rate limiting** — enforce per-client query frequency limits
- **TLS / mTLS at the edge** — terminate HTTPS and optional client certificates in a reverse
  proxy in front of SSE; combine with `MCP_BEARER_TOKEN` for defense in depth

---

## License

This project is licensed under the MIT License. See `LICENSE`.