mcp-server-mysql
A Model Context Protocol (MCP) server for MySQL. Lets Cursor and other MCP clients list tables, describe schema, and run SQL against any MySQL database you configure, over stdio, with read-only protection for production connections.
Published as mysql-mcp-toolkit on npm.
Quick start (config file)
-
Generate a starter config:
npx -y mysql-mcp-toolkit init # writes ~/.config/mcp-server-mysql/config.json -
Replace the placeholder credentials in that file. Any field still containing
your-is treated as a placeholder and the connection is rejected at startup. -
Add the server to Cursor's
~/.cursor/mcp.json:{ "mcpServers": { "mysql": { "command": "npx", "args": [ "-y", "mysql-mcp-toolkit@latest", "--config", "/absolute/path/to/config.json" ] } } }The
@latesttag matters; see Updating. Without it, npx caches the first version it installs and keeps reusing it. -
Restart Cursor. You should see
mysqlshow up under MCP tools.
Quick start (env vars only)
If you only need one connection, skip the config file entirely:
{
"mcpServers": {
"mysql": {
"command": "npx",
"args": ["-y", "mysql-mcp-toolkit@latest"],
"env": {
"MYSQL_HOST": "127.0.0.1",
"MYSQL_PORT": "3306",
"MYSQL_USER": "root",
"MYSQL_PASSWORD": "secret",
"MYSQL_DATABASE": "myapp",
"MYSQL_READ_ONLY": "true"
}
}
}
}
Or supply a single connection URL instead of the four discrete variables:
{
"mcpServers": {
"mysql": {
"command": "npx",
"args": ["-y", "mysql-mcp-toolkit@latest"],
"env": {
"MYSQL_URL": "mysql://root:[email protected]:3306/myapp",
"MYSQL_READ_ONLY": "true"
}
}
}
}
User, password, and database in the URL are URL-decoded, so reserved
characters (e.g. @ or / in a password) can be percent-encoded. Add
?ssl=true|false|amazon-rds or use the mysqls:// scheme to enable TLS
(MYSQL_SSL still overrides). MYSQL_CONNECTION_NAME, MYSQL_READ_ONLY,
and MYSQL_QUERY_TIMEOUT_MS apply to either form.
Updating
npx caches each package spec under ~/.npm/_npx/<hash>/. If your mcp.json
points at mysql-mcp-toolkit with no version tag, npx installs whatever is
current on the first run and keeps reusing that cached copy forever,
even after new versions ship to npm. That's why the examples above pin
@latest: it forces npx to resolve against the registry on every spawn, so
new releases get picked up the next time Cursor restarts the server.
After any of these, restart Cursor to respawn the MCP child process.
Verify the upgrade worked by checking the tool list: table_stats,
list_foreign_keys, and search_columns arrived in 1.3.0, so seeing them
in Cursor's MCP panel confirms you're current.
Tools
Every schema tool (list_tables, describe_table, show_create_table,
get_schema, table_stats, list_foreign_keys, search_columns,
sample_rows) accepts an optional database argument to inspect a schema
other than the one the connection is attached to. Use list_schemas to
discover the available names.
All tools advertise MCP tool annotations
(readOnlyHint, destructiveHint, idempotentHint), so clients can relax
approval prompts for the safe ones.
Read-only enforcement
Connections with readOnly: true use a default-deny allowlist: only
statements starting with SELECT, WITH, TABLE, VALUES, SHOW,
EXPLAIN, DESCRIBE/DESC, HELP, or CHECKSUM are accepted, and
SELECT/WITH bodies are additionally scanned for embedded writes (CTE
writes like WITH x AS (...) DELETE FROM t, INTO OUTFILE/DUMPFILE,
FOR UPDATE locks). Everything else, including LOAD DATA, SET,
KILL, FLUSH, EXPLAIN ANALYZE (which executes), and transaction
control, is rejected before any SQL leaves the process. Detection is
comment-, string-literal-, and backtick-identifier-aware, so neither
/* DELETE */ comments nor a column named `update` confuse it.
MySQL executable comments (/*! ... */, /*!50000 ... */), whose
contents the server actually runs, are inspected rather than ignored, and
-- is only treated as a comment when followed by whitespace — so
/*! DELETE FROM t */ and SELECT 1--1;DELETE FROM t are both rejected.
Each tool accepts an optional connection argument. If omitted, the default
from defaults.connection (or the literal "default") is used. Tools that
run user-supplied SQL also accept an optional timeoutMs (defaults to the
connection's queryTimeoutMs, then defaults.queryTimeoutMs, then 30000).
On SELECT, the timeout is enforced server-side via MySQL's
MAX_EXECUTION_TIME optimizer hint. On non-SELECT statements the timeout
only closes the client socket; the server may keep the query running.
Parameterized queries
// execute_query
{
"query": "SELECT id, email FROM users WHERE org_id = ? AND active = ?",
"params": [42, true]
}
Driver-side escaping prevents SQL injection. Always prefer this over
interpolating values into the query string.
Large result sets
execute_query accepts two options that keep responses small enough for
an agent's context window:
format: "compact"returns{ columns: [...], rows: [[...]] }instead of one JSON object per row, much smaller for wide tables.maxResponseBytes(default1000000, configurable viadefaults.maxResponseBytes) caps the serialized size of returned rows. When exceeded, rows are truncated and the response includestruncated: true, the fetched row count, and a note.offsetpaginates a bareSELECT/WITH…SELECTthat has no explicitLIMIT: the server appendsLIMIT n OFFSET m. Combine withlimitto page through a large result set. Ignored when the query already has aLIMIT.
Session state
Each tool call may run on a different pooled connection. Transactions
(BEGIN/COMMIT), SET statements, USE, and temporary tables therefore
do not carry over between calls; don't try to span a transaction
across multiple execute_query invocations.
BIGINT values above 2^53 are returned as strings to avoid silent
precision loss, and DATE/DATETIME/TIMESTAMP columns are returned as
plain strings (no timezone conversion).
Resources
For each configured connection the server exposes a resource:
- URI:
database://<connectionName> - MIME type:
application/json - Body:
{ connectionName, host, database, port, readOnly }(no password)
Config file reference
{
"defaults": {
"connection": "default",
"queryLimit": 100,
"queryTimeoutMs": 30000,
"maxResponseBytes": 1000000
},
"connections": [
{
"connectionName": "default",
"host": "127.0.0.1",
"port": 3306,
"user": "root",
"password": "secret",
"database": "myapp",
"readOnly": false,
"connectionLimit": 5,
"queueLimit": 20
},
{
"connectionName": "production",
"host": "db.example.com",
"port": 3306,
"user": "readonly_user",
"password": "prod-secret",
"database": "myapp",
"readOnly": true,
"ssl": "Amazon RDS",
"queryTimeoutMs": 15000
}
]
}
Config resolution order
The first source that exists wins:
--config <path>flagMCP_MYSQL_CONFIGenvironment variable./mcp-mysql.config.json(current working directory)~/.config/mcp-server-mysql/config.jsonMYSQL_*environment variables (single connection)
Environment variables (only when no config file is found)
mcp-server-mysql init
npx -y mysql-mcp-toolkit init [--output <path>]
Copies the bundled config.example.json to the target path. Default target
is ~/.config/mcp-server-mysql/config.json. The command refuses to
overwrite an existing file. Pick a different --output or delete the
target first.
--check
Validate config and connectivity without starting the MCP server:
npx -y mysql-mcp-toolkit --check [--config <path>]
Loads config, pings every connection with SELECT 1, prints a per-
connection status report to stderr, and exits non-zero if any connection
fails. Useful before wiring the server into an MCP client.
Security
- Never commit your config file. The included
.gitignoreexcludesmcp-mysql.config.jsonandconfig.jsonat the repo root. - Set
readOnly: truefor any connection that points at a shared or production database. The server enforces this at the tool layer before any SQL leaves the process. - If a password leaks into git history or logs, rotate it.
list_databasesanddatabase://resources never include passwords.- All MCP server logging goes to stderr; stdout is reserved for the protocol.
Development
npm install
npm run dev -- --config ./config.example.json # expect a placeholder error
npm test
npm run build
Publishing
For maintainers only:
npm run build
npm test
npm publish # access defaults to public via publishConfig
The prepublishOnly script runs npm run build automatically. Bump the
version in package.json and add a CHANGELOG.md entry before publishing.
License
MIT