MySQL Read-Only MCP Server
An MCP (Model Context Protocol) server and CLI for one MySQL 8.0.46 target. It provides metadata discovery and a strict read-only query tool. It does not support writes, administration, backups, replication, or dynamic targets.
Quick start
# Requires Go 1.25+ (see .tool-versions)
make build
./bin/mysql-mcp-server version
# Set these only in the runtime environment. Do not commit the password.
export MYSQL_MCP_HOST='127.0.0.1'
export MYSQL_MCP_DB_PORT='3306'
export MYSQL_MCP_USERNAME='readonly'
export MYSQL_MCP_PASSWORD='<runtime-secret>'
export MYSQL_MCP_DATABASE='inventory'
# Start MCP server (stdio, default)
./bin/mysql-mcp-server mcp
# List / call tools from the CLI (no MCP client required)
./bin/mysql-mcp-server tools list
./bin/mysql-mcp-server tools call mysql_list_tables --params '{"database":"inventory"}'
./bin/mysql-mcp-server tools call mysql_query --params '{"sql":"SELECT 1"}'
Features
- Six MySQL discovery and query tools are available through both MCP and CLI.
- JSON parameter validation occurs at the shared tool-handler boundary.
- A Vitess AST query boundary and
database/sqlread-only transaction protect accepted queries. - Stdio, Streamable HTTP, and SSE transports are supported.
MySQL tools
Successful calls use a JSON envelope with schema_version: 1. Unknown,
missing, empty, and non-string parameters are rejected before any database call.
mysql_query accepts one parsed read query only. It rejects multiple or
non-query statements, INTO, locking reads, user-variable assignments, and
blocked session/file functions. Accepted queries run in a read-only transaction
that is always rolled back.
This is not proof that stored functions, UDFs, views, or plugins have no side
effects. Use a least-privilege account; do not grant EXECUTE, FILE,
LOCK TABLES, or administrative privileges.
CLI commands
Tools examples
./bin/mysql-mcp-server tools list
./bin/mysql-mcp-server tools list --json
./bin/mysql-mcp-server tools describe mysql_query
./bin/mysql-mcp-server tools describe mysql_query --json
./bin/mysql-mcp-server tools call mysql_list_tables --params '{"database":"inventory"}'
echo '{"sql":"SELECT 1"}' | ./bin/mysql-mcp-server tools call mysql_query --params-file -
MCP transports
Stdio (default)
./bin/mysql-mcp-server mcp
Cursor / Claude Desktop style config:
{
"mcpServers": {
"mysql-mcp-server": {
"command": "/absolute/path/to/bin/mysql-mcp-server",
"args": ["mcp"],
"env": {
"MYSQL_MCP_HOST": "127.0.0.1",
"MYSQL_MCP_DB_PORT": "3306",
"MYSQL_MCP_USERNAME": "readonly",
"MYSQL_MCP_PASSWORD": "<runtime-secret>",
"MYSQL_MCP_DATABASE": "inventory"
}
}
}
}
Streamable HTTP
./bin/mysql-mcp-server mcp --port 8080
curl -s http://127.0.0.1:8080/healthz
Client config example:
{
"mcpServers": {
"mysql-mcp-server": {
"url": "http://127.0.0.1:8080/mcp"
}
}
}
SSE
Same process as HTTP mode. Endpoints:
./bin/mysql-mcp-server mcp --port 8080 --sse-base-url http://127.0.0.1:8080
Docker
# Build
make docker
# Stdio (default ENTRYPOINT is `mysql-mcp-server mcp`)
docker run -i --rm \
-e MYSQL_MCP_HOST -e MYSQL_MCP_DB_PORT -e MYSQL_MCP_USERNAME \
-e MYSQL_MCP_PASSWORD -e MYSQL_MCP_DATABASE \
ghcr.io/futuretea/mysql-mcp-server:dev
# HTTP
docker run --rm \
-e MYSQL_MCP_HOST -e MYSQL_MCP_DB_PORT -e MYSQL_MCP_USERNAME \
-e MYSQL_MCP_PASSWORD -e MYSQL_MCP_DATABASE \
-p 8080:8080 ghcr.io/futuretea/mysql-mcp-server:dev --port 8080 --listen 0.0.0.0
HTTP / SSE have no auth and no TLS. Default --listen 127.0.0.1. Use only on trusted networks; put a reverse proxy in front if you expose the port.
Configuration
Priority: flags > environment variables > config file > defaults.
Environment variables
Config file
See config.example.yaml:
port: 0
listen: 127.0.0.1
sse_base_url: ""
log_level: info
enabled_tools: []
disabled_tools: []
enabled_domains: []
disabled_domains: []
./bin/mysql-mcp-server mcp --config config.example.yaml --port 8080
Project layout
cmd/mcp-server/ # binary entrypoint
internal/cmd/ # cobra CLI (mcp / tools / version / completion)
internal/mysql/ # connection, discovery, query boundary, result encoding
pkg/core/ # config / logging / version
pkg/server/mcp/ # MCP registration and transports
pkg/server/http/ # HTTP / SSE / healthz
pkg/toolset/ # Toolset interface and filters
pkg/toolset/mysql/ # six MySQL tool definitions and shared parameter decoder
Operational limits
This slice deliberately does not impose query timeouts, row limits, response size limits, or concurrency budgets. Keep the server on trusted networks; HTTP and SSE have no built-in authentication or TLS termination.
Development
make tidy
make format
make lint
make test
make coverage
make ci
make build
make docker
Local MySQL E2E
The real MySQL E2E suite is opt-in and never runs in GitHub Actions or through
make ci. It starts and removes its own disposable mysql:8.0.46 container,
then exercises the compiled CLI and MCP stdio transport:
MYSQL_MCP_INTEGRATION=1 go test ./internal/mysql -run '^TestMySQL8046Integration$' -count=1
Use released artifacts
Release tags use vX.Y.Z. Use X.Y.Z for npm and vX.Y.Z for Docker.
npx -y @futuretea/mysql-mcp-server@<version>
docker run --rm -i ghcr.io/futuretea/mysql-mcp-server:v<version>
Both launch the MCP server's mcp command. Arguments after the package or image are forwarded to that command.
Configure an MCP client that supports stdio with npm:
{
"mcpServers": {
"mysql-mcp-server": {
"command": "npx",
"args": ["-y", "@futuretea/mysql-mcp-server@<version>"],
"env": {
"MYSQL_MCP_HOST": "127.0.0.1",
"MYSQL_MCP_DB_PORT": "3306",
"MYSQL_MCP_USERNAME": "readonly",
"MYSQL_MCP_PASSWORD": "<runtime-secret>",
"MYSQL_MCP_DATABASE": "inventory"
}
}
}
}
Supported release platforms are recorded in .github/ci/release-platforms.json.