mysql-mcp-plugin
by Efilkucf
README.md
# mysql-mcp-plugin
MySQL access for Claude Code. Each user configures their own connections, and each
connection grants a specific set of capabilities — so a production replica can be
read-only while a scratch database is writable.
## Install
```bash
/plugin marketplace add <your-github-user>/mysql-mcp-plugin
/plugin install mysql
```
Nothing to build and no paths to edit: `dist/` ships prebuilt and self-contained.
Node 20+ on `PATH` is the only requirement.
## Add a connection
**The settings form is the quickest route.** Claude Code prompts for these fields
when you enable the plugin, and you can reopen them any time:
```
/plugin configure mysql
```
| Field | Notes |
|---|---|
| Connection name | Leave blank to skip the form entirely |
| MySQL host / port / user / database | With a bastion set, the host is as resolved *from* the bastion |
| MySQL password | Masked as you type; stored in your OS keychain, never in `settings.json` |
| SSH bastion host / user / key | Blank key means agent auth, so no key material is stored |
| Allow read/write | Adds `INSERT`, `UPDATE`, `DELETE`. Off by default |
| Allow schema changes | Adds `CREATE`, `ALTER`, `DROP`, `TRUNCATE`. Off by default |
| Allow all queries | Everything, including `GRANT` and `SET GLOBAL`. Off by default |
Nothing is typed into the conversation, and the password is masked at entry.
**The form holds one connection.** It is a flat list of fields, so it cannot express
several. For more than one — each with its own capabilities — use the commands:
```
/mysql:setup guided, explains the model first
/mysql:add straight to adding one
```
Those never ask for a password in the conversation either: they hand you a single
line to run yourself, and the CLI prompts on your own terminal so the secret stays
out of the transcript.
Both sources are merged, so you can use the form for your everyday database and
`/mysql:add` for the rest. If a name appears in both, the file wins.
Then check the grants line up:
```
/mysql:test staging-replica
```
## Commands
| Command | Purpose |
|---|---|
| `/mysql:setup` | Guided first-time setup — explains the model, then adds a connection |
| `/mysql:add` | Add a connection (prompts for password out-of-band) |
| `/mysql:list` | Show connections and their capabilities |
| `/mysql:test <name>` | Connect, show `SHOW GRANTS`, compare against declared caps |
| `/mysql:grant <name> <caps>` | Widen what Claude may do |
| `/mysql:revoke <name> <caps>` | Narrow it; `--all` disables the connection |
| `/mysql:remove <name>` | Delete the connection and its stored password |
| `/mysql:import` | Migrate a legacy `MYSQL_SERVERS` env setup |
## Capabilities
| Cap | Allows |
|---|---|
| `select` | `SELECT`, `WITH … SELECT`, `TABLE`, `VALUES` |
| `schema_read` | `SHOW`, `DESCRIBE`, `EXPLAIN`, `USE` |
| `insert` | `INSERT`, `REPLACE`, `LOAD DATA` |
| `update` | `UPDATE` |
| `delete` | `DELETE` |
| `ddl` | `CREATE`, `ALTER`, `RENAME` |
| `drop` | `DROP`, `TRUNCATE` |
| `admin` | `GRANT`, `REVOKE`, `SET GLOBAL`, user management, `CALL` |
A connection with no capabilities is revoked: still configured, still
credentialed, every query refused.
## How enforcement works
```
1. PreToolUse hook Claude Code only. Denies before the tool runs, so the model
never sees a result. Cannot be bypassed from context.
2. The server Checks before executing. Works in ANY MCP client, so the
guarantee does not depend on Claude Code.
3. MySQL GRANTs The real security boundary.
```
Layers 1 and 2 call the same `authorize()`, so their verdicts cannot drift apart.
Layer 2 exists because the server is portable: point Cursor or any other MCP client
at `dist/index.js` and capability checking still applies. The check runs *before*
connecting, so a refused statement costs no handshake and opens no SSH channel.
**Layer 3 is the one that actually protects the database.** Layers 1 and 2 catch
mistakes — a wrong connection name, a model error, something injected into the
context — before they reach a database whose GRANTs might be wider than you
intended. Give Claude a MySQL account holding only the privileges you are willing
for it to have, and treat the capability list as a second pair of eyes rather than
the lock.
Both fail closed: unreadable config, unparseable SQL, or anything unclassifiable is
refused.
Batches are checked statement by statement. `multipleStatements` is enabled on the
connection, so `SELECT 1; DROP TABLE users` really would run both — it needs
`select` **and** `drop`, and is refused without them.
## Connecting through a bastion
Set an `ssh` block and the plugin opens the tunnel itself — `host`/`port` then
refer to the database as seen *from the bastion*:
```json
{
"version": 1,
"defaultConnection": "staging-replica",
"connections": {
"staging-replica": {
"host": "db.internal",
"port": 3306,
"database": "appdb",
"user": "claude_ro",
"password": { "keychain": "staging-replica" },
"caps": ["select", "schema_read"],
"ssh": {
"host": "bastion.example.com",
"port": 22,
"user": "you",
"auth": "agent"
}
}
}
}
```
The tunnel is passed to the driver as a stream, so no local port is allocated and
two people cannot collide on the same forwarded port. `auth: "agent"` uses
`SSH_AUTH_SOCK`, which means the plugin never holds key material; `auth: "key"`
with `privateKeyPath` is available where an agent is not.
If you would rather run your own tunnel, point the connection at `127.0.0.1` and
set `tunnelHint` to the command that starts it — connection failures then tell you
what to run.
## Where configuration lives
Two sources, merged, with the file winning on a name clash:
| Source | Holds | Secrets |
|---|---|---|
| Settings form (`/plugin configure mysql`) | one connection | password in OS keychain, via Claude Code |
| `~/.config/claude-mysql/connections.json` | unlimited connections | keychain or env reference only |
The file path is overridable with `MYSQL_MCP_CONFIG`.
It contains no secrets, so a team can share a skeleton and each member populates
their own keychain. Passwords are referenced as `{ "keychain": "name" }` or
`{ "env": "VARNAME" }` — the latter for machines without an OS keychain, such as
CI.
## Tools
Two, unchanged from the pre-plugin server:
- `list_servers` — configured connections, their capabilities, and the config path
- `execute_sql(server?, sql, database?)` — run SQL on a connection
## Development
```bash
npm install
npm run build # typecheck + bundle to dist/
npm test # 97 tests
```
Test coverage: SQL classification (19), authorization decisions (10), hook verdicts
(21), the server driven over stdio as a bare MCP client (10), settings-form parsing
and its capability toggles (17), onboarding notices (7), and the plugin manifests (14).
The stdio group verifies the portable guarantee — no Claude Code, no hook, and no
database required, since refusals happen before the connection is opened. The
manifest group exists because a published version once shipped `author` as a string
instead of an object, which made the plugin uninstallable while every other test
passed and CI was green.
Verified against a real environment: `/mysql:import` migrating connections out of
`~/.claude.json`, and the keychain write-then-read path via `/mysql:list`.
Still unverified, because both need a reachable database: a query executing through
an SSH tunnel, and `/mysql:test`'s comparison against live `SHOW GRANTS` output.
`dist/` is committed. Bundling (rather than plain `tsc`) is what makes it
self-contained — `tsc` output would still import `mysql2`, `ssh2`, and the MCP SDK
at runtime, and `node_modules` is not shipped. `npm run verify-dist` checks the
committed output matches `src/`.
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). One thing to know up front: `dist/` is
committed, so any `src/` change needs `npm run build` committed alongside it — CI
fails otherwise, and the tests alone won't catch it.
Design notes and rationale: [`docs/specs/`](docs/specs/).
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues