mysql-mcp
mysql-mcp
Query MySQL tables from Claude Code — read-only by default.
An MCP server that gives Claude Code (and any MCP client) read access to a MySQL database through structured, parameterized query tools. No raw SQL passthrough by default — the tool surface is designed so the worst possible outcome is a SELECT that returns too many rows.
Modeled on Agent360's browser-mcp (same MCP server patterns: stdio transport, lifecycle handling, LLM-facing tool descriptions).
Testing
npm testThree layers, all under node --test (no test framework dependency):
File | Layer | Needs DB? |
SQL builder security contract (identifiers, params, clamps, footgun defense) | No | |
Error → LLM-hint mapping + result truncation | No | |
Full MCP protocol against a real MySQL, via test/helpers/mcp-client.js | Yes — suites skip gracefully when unreachable |
Integration env (defaults match the local Docker dev setup; override freely):
TEST_DB_HOST / TEST_DB_PORT / TEST_DB_USER / TEST_DB_PASSWORD / TEST_DB_NAME
TEST_DB_RW_USER / TEST_DB_RW_PASSWORD → write suite (skipped if unset)
TEST_DB_ADMIN_PASSWORD → cleanup of write-test rowsStatus — M5
Five read tools plus two opt-in write tools, all integration-tested against a real MySQL:
Tool | What it does |
| Verify MCP + MySQL connectivity, returns guiding errors |
|
|
| Column names/types/keys/defaults/comments via information_schema |
| Structured queries — key/value WHERE, column lists, order, limit (hard cap 200) |
| Shows the account's grants and whether it is read-only |
| INSERT + read-back of the inserted row |
| UPDATE with REQUIRED where, default LIMIT 1 (cap 100) + read-back of the new state |
Writing — opt-in at two levels
Write tools exist only when both hold:
Server: started with
ALLOW_WRITES=1— otherwise the tools are not even advertised intools/list, and dispatch refuses them anyway.Database: the connected account holds INSERT/UPDATE grants (verify with
check_permissions).
Safety defaults on update_rows: where is required (a where-less UPDATE is
rejected — the classic footgun), limit defaults to 1 and caps at 100.
Every write reads the affected rows back and returns the verified new state.
Security — three tiers, all enforced
Tier | Defense | Where |
1. Database | SELECT-only account ( | Your DBA work — verified by |
2. Connection |
| |
3. Application | No raw SQL passthrough; identifier allowlist + | db.js |
Every tool call is audited to stderr — shape only (tool, table name, row count), never values. Same secret-hygiene contract as browser-mcp's action log.
Reliability & LLM experience
Errors are LLM-actionable. Raw MySQL/network errors are classified and shipped with a hint that says what to do next:
Error: Table 'x' doesn't exist→Hint: Call list_tables...,Unknown column→Hint: Call describe_table..., connection failures →Hint: Check DB_HOST / DB_PORT...(marked retryable).Retry policy. Transient connection failures retry once automatically (SELECTs are idempotent); SQL errors never retry. Same read-only/retryable split as browser-mcp's CDP whitelist.
Result truncation. Results are capped at 50,000 serialized chars (on top of the 200-row cap). When truncated, the response carries
truncated: true,fetched, and a hint telling the model to narrow (WHERE filters, fewer columns, offset paging) instead of re-querying bigger.
Roadmap
✅ M1–M6 core done: skeleton, query tools, security, reliability, opt-in writes, test suite
⏸️ npm publishing — skipped by decision; add
@scopename, LICENSE, registry metadata (server.json/glama.json) if you ever publish
Install (local — no npm publish needed)
cd /path/to/mysql-mcp
node bin/cli.js installThis writes the mysql-mcp server into ~/.claude/mcp.json, pointing at this
checkout's index.js by absolute path (dev-mode install, same pattern as
browser-mcp's install.sh). Credentials use ${VAR} placeholders — Claude Code
resolves them from your environment, never stored in the config file:
{
"mcpServers": {
"mysql-mcp": {
"command": "node",
"args": ["/path/to/mysql-mcp/index.js"],
"env": {
"DB_HOST": "${DB_HOST}",
"DB_PORT": "${DB_PORT}",
"DB_USER": "${DB_USER}",
"DB_PASSWORD": "${DB_PASSWORD}",
"DB_NAME": "${DB_NAME}"
}
}
}
}Database account — read-only, always
CREATE USER 'mcp_ro'@'%' IDENTIFIED BY '<strong-password>';
GRANT SELECT ON <your_db>.* TO 'mcp_ro'@'%';Point it at a read replica if you have one.
Dev
npm ci
node index.js # → [mysql-mcp] MySQL MCP server running (stdio)Environment variables
Var | Default | Description |
|
| MySQL host |
|
| MySQL port |
| (required) | MySQL user |
| (empty) | MySQL password |
| (none) | Default database |
|
| Per-query timeout in ms |
| (unset) | Set to |
License
MIT