mysqlpeek
A read-only MySQL / MariaDB MCP server that lets an agent explore your schema, cost a query before running it, and pick which of several instances it is talking to — per call, never by a mode it can forget it is in.
Most MySQL MCP servers give a model one blunt instrument, "run this SQL", on one host, with a
regular expression standing between it and DROP TABLE. mysqlpeek is different on three
counts:
- Writes are refused by the engine, not only by a parser. Every connection runs inside a
READ ONLYtransaction with server-side caps on execution time, result rows and rows examined. A statement that gets past the parser still cannot write, and cannot run away. - Cost before execution.
EXPLAINtells you how many rows a query would examine before a single one is read, andmax_join_sizemakes the server refuse a query the optimiser expects to be too big — before it starts. - Several instances, one server. A profiles file names your prod replica, staging and
local boxes; every tool takes
instance=, and every response says which one answered.
Works with MySQL 5.7.8+, MySQL 8 and 9, MariaDB 10.1+, and the managed flavours that keep
the standard session variables (RDS, Cloud SQL, Azure Database for MySQL). MariaDB reports
no query_cost and no tree-shaped plan; everything else behaves the same.
Install
As a Claude Code plugin (recommended)
/plugin marketplace add sgr-xd/mysqlpeek
/plugin install mysqlpeek
Claude Code then asks for four things:
| Field | Example |
|---|---|
| Server | db.internal, host:3307, or mysqls://db.example.com for TLS |
| Username | readonly_user |
| Password | masked; goes to your operating system's secure storage, never to a settings file |
| Database | optional — leave empty and every call names its own |
Port and TLS come from whatever you put in Server, so there is nothing else to set.
This also installs two skills: mysql-query-craft, which teaches the
explore → estimate → run discipline the tools are built around, and
mysql-instance-health, a one-shot sweep of replication, lock waits, long transactions,
connections, buffer pool, temp tables and top statements that reports whether the
instance is healthy right now.
As a standalone MCP server
claude mcp add mysqlpeek --scope user \
-e MYSQL_HOST=db.internal \
-e MYSQL_USER=readonly_user \
-e MYSQL_PASSWORD_FILE=$HOME/.config/mysqlpeek/password \
-e MYSQL_DATABASE=shop \
-- uvx mysqlpeek
Works with any MCP client, not only Claude Code — point it at uvx mysqlpeek over stdio.
From source
git clone https://github.com/sgr-xd/mysqlpeek && cd mysqlpeek
uv venv && uv pip install -e ".[dev]"
pytest -m "not integration" # unit tests, no database needed
pytest # adds live tests against a real server
Configure
Only needed for the standalone and from-source paths; the plugin asks instead.
export MYSQL_HOST=db.internal # or host:port, or mysqls://host
export MYSQL_USER=readonly_user
export MYSQL_PASSWORD_FILE=~/.config/mysqlpeek/password # preferred over MYSQL_PASSWORD
export MYSQL_DATABASE=shop # optional
| You enter | Host | Port | TLS |
|---|---|---|---|
db.internal |
db.internal | 3306 | no |
db.internal:3307 |
db.internal | 3307 | no |
mysql://db.internal:3306 |
db.internal | 3306 | no |
mysqls://db.example.com |
db.example.com | 3306 | yes |
MYSQL_PORT, MYSQL_SSL, MYSQL_SSL_CA and MYSQL_SSL_VERIFY still work and take
precedence if you set them. The password is never accepted as a command-line argument —
arguments leak through shell history and ps.
| Variable | Purpose |
|---|---|
MYSQLPEEK_PROFILES |
Path to a multi-instance profiles file — see below |
MYSQLPEEK_AUDIT_LOG |
Path to a JSONL record of every query decision |
Tools
Discovery — schema exploration, no user SQL accepted:
| Tool | Purpose |
|---|---|
list_instances |
Configured instances, which is default, and each one's limits |
list_databases |
Schemas visible to your user, system ones flagged |
list_tables |
Tables with engine, approximate rows, size, primary key, partitioning |
describe_table |
Columns, types, nullability, key membership, charset, comments |
list_indexes |
Every index with its column order and cardinality |
show_create_table |
Full CREATE TABLE / CREATE VIEW DDL |
Query:
| Tool | Purpose |
|---|---|
run_select_query |
Execute a SELECT under enforced caps; reports rows_examined and the optimiser's query_cost |
sample_rows |
Preview rows from a table (SQL built server-side) |
profile_column |
One column's nulls, distinct count, range and most common values, over a bounded sample |
Operations — what the server is doing, and whether it is keeping up:
| Tool | Purpose |
|---|---|
list_running_queries |
Statements executing now, longest first (needs PROCESS to see other sessions) |
top_statements |
Statement digests from performance_schema ranked by time, count, rows examined or missing index |
table_storage_stats |
Tables by size with fragmentation, index-to-data ratio and auto-increment headroom |
replication_status |
Replica threads, lag, last error, read_only / super_read_only |
lock_waits |
Sessions blocked on row locks and who blocks them |
long_transactions |
Transactions open longer than N seconds, idle-in-transaction included |
server_health |
Connections, buffer pool hit rate, temp tables on disk, lock waits, history list |
Cost & validation — these read no table data:
| Tool | Purpose |
|---|---|
validate_query |
EXPLAIN — is the SQL legal, and do the names resolve? |
estimate_query_cost |
EXPLAIN FORMAT=JSON — per table: access type, index chosen, rows it would examine, share of the table; a full_scan / heavy / selective verdict |
explain_plan |
EXPLAIN FORMAT=TREE (MySQL 8.0.16+) or the classic table, plus whether every base table is reached through an index |
Every response names the instance that answered and the SQL actually executed, so a
rewritten LIMIT is visible rather than silent.
Safety
mysqlpeek is read-only, enforced in three independent layers:
-
Statement policy — single statement only; must open with
SELECT,WITH…SELECT,SHOW,DESCRIBEorEXPLAIN; DML/DDL keywords,INTO OUTFILE, locking reads (FOR UPDATE,FOR SHARE),LOAD_FILE(),SLEEP(), lock functions, executable comments (/*! … */) and cap-raising hints (SET_VAR,MAX_EXECUTION_TIME) are rejected.mysql.userand the other credential tables are refused, backticked or not. A missingLIMITis appended; an oversized one is clamped. -
Session guards — every connection is put into this state the moment it opens, and a connection where any guard fails is refused outright:
Guard Statement no writes SET SESSION TRANSACTION READ ONLYtime cap SET SESSION max_execution_time = …(MariaDB:max_statement_time)result cap SET SESSION sql_select_limit = …examined-rows cap SET SESSION max_join_size = …withsql_big_selects = 0— the optimiser refuses a statement it expects to examine more rows than this, before reading anyMulti-statement execution is off at the protocol level, so
SELECT 1; DROP …is a syntax error to the server. -
Database grants — connect as a user with only
SELECT. This is the layer that cannot be argued with, and the one you should not skip. It is also the only layer that stops server-state statements such asSET GLOBAL: the parser refuses them, but aREAD ONLYtransaction does not, so an account holdingSUPERorSYSTEM_VARIABLES_ADMINis one parser bug away from changing the server. Do not point mysqlpeek atroot:
CREATE USER 'readonly_user'@'%' IDENTIFIED BY '…';
GRANT SELECT ON shop.* TO 'readonly_user'@'%';
-- and, if you want the ops tools:
GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'readonly_user'@'%';
Tuning the caps
| Variable | Default |
|---|---|
MYSQLPEEK_MAX_EXECUTION_TIME |
30 (seconds) |
MYSQLPEEK_MAX_RESULT_ROWS |
10000 |
MYSQLPEEK_MAX_JOIN_SIZE |
100000000 (rows the optimiser may plan to examine) |
MYSQLPEEK_DEFAULT_LIMIT |
100 |
MYSQLPEEK_MAX_LIMIT |
10000 |
Several instances
Point MYSQLPEEK_PROFILES at a JSON file, and every tool gains an optional instance
argument.
The file holds references, never values — so it is safe to commit:
{
"default": "prod-replica",
"limits": { "max_limit": 5000 },
"instances": {
"prod-replica": {
"description": "read replica of the primary",
"host": "mysqls://db-ro.example.com",
"user": "${env:PROD_MYSQL_USER:-readonly}",
"password": "${cmd:vault kv get -field=password secret/mysql-prod}",
"database": "shop",
"limits": { "default_limit": 25, "max_limit": 200, "max_join_size": 5000000 }
},
"staging": {
"host": "db-staging.example.com:3307",
"password": "${file:~/.config/mysqlpeek/staging.pw}"
},
"local": {
"host": "127.0.0.1",
"user": "root",
"password": "${env:LOCAL_MYSQL_PW:-}"
}
}
}
| Scheme | Example | Notes |
|---|---|---|
env |
${env:PROD_MYSQL_PASSWORD} |
${env:VAR:-default} supplies a fallback |
file |
${file:~/.config/mysqlpeek/prod.pw} |
Trailing newline stripped |
cmd |
${cmd:op read op://vault/mysql-prod/password} |
stdout of a command, run without a shell |
A literal password in the file is rejected with an error pointing at the reference syntax.
A profiles file writable by anyone but its owner is refused, because a ${cmd:…} reference
means the file decides what gets executed.
Limits layer: environment defaults → file-wide limits → per-instance limits. Connections
are lazy, so an unreachable instance does not stop the server starting. There is
deliberately no use_instance tool: selection is an argument on every call, and every
response carries the instance that answered it.
Releasing
Three files carry the version and must agree: .claude-plugin/plugin.json,
.claude-plugin/marketplace.json (plugins[0].version) and pyproject.toml. Bump on
every shipped change, documentation included — the plugin cache is keyed by version, so an
unchanged version means installed copies silently keep the previous build.
License
Apache-2.0
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mysqlpeek-0.1.0.tar.gz.
File metadata
- Download URL: mysqlpeek-0.1.0.tar.gz
- Upload date:
- Size: 67.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.8.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de126fc1e54ca9cdf7a2b5e1e3efe66b2c00a271880e0bf07f1868d8cdb013e3
|
|
| MD5 |
a215c56bb99bce1cad050da2ec47fd8c
|
|
| BLAKE2b-256 |
8a5de732239c817a0668080a98ce72db3427e9e17ae07726255c3cfe3d901b51
|
File details
Details for the file mysqlpeek-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mysqlpeek-0.1.0-py3-none-any.whl
- Upload date:
- Size: 58.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.8.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c07da6c19586e89c49fe803f71af77a6e5b15a8f045bc47b7d8c9a22b4c5f06f
|
|
| MD5 |
6c7acce36d76b0f553bdc19f97a4c020
|
|
| BLAKE2b-256 |
0ef59d3d69e89f93b168371e52f646ada352aa2a6fc05b6c38f1753c798a859c
|