Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ jobs:
# existing users depending on them, so component images for those versions get
# a "-component" tag suffix; mysql26.10 has no plugin equivalent to collide with
# (component-only), so it gets the plain tag.
# The same job also builds component_mcp_guard and publishes the separate mysql-mcp
# image family (docker/mysql-mcp/Dockerfile, RFC 0001 in askdba/mysql-mcp-server) as
# ghcr.io/<owner>/mysql-mcp:<mysql-version>.
build-and-publish-component:
# Publish only from a dispatch that targets a VERSION tag (refs/tags/v*).
# Without the version-tag guard, a workflow_dispatch off any branch — or any non-version tag — would
Expand Down Expand Up @@ -251,6 +254,8 @@ jobs:
uses: docker/setup-buildx-action@v3

- name: Build MyVector component (amd64/arm64)
env:
MYVECTOR_BUILD_MCP: "ON"
run: |
set -euo pipefail
chmod +x ${{ matrix.build-script }}
Expand All @@ -263,6 +268,8 @@ jobs:
cp build/component-amd64/libmyvector_component.so myvector-component-amd64.so
cp build/component-arm64/libmyvector_component.so myvector-component-arm64.so
cp build/component-amd64/myvector.json myvector.json
cp build/component-amd64/component_mcp_guard.so component_mcp_guard-amd64.so
cp build/component-arm64/component_mcp_guard.so component_mcp_guard-arm64.so
cp sql/myvector_install_component.sql myvector_install_component.sql
cp sql/myvector_uninstall_component.sql myvector_uninstall_component.sql

Expand Down Expand Up @@ -311,3 +318,45 @@ jobs:
tags: ghcr.io/${{ github.repository }}:${{ matrix.image-tag }}
build-args: |
MYSQL_VERSION=${{ matrix.mysql-version }}

# A component works only on the patch release it was built against (the 8.4.8 guard
# crashes 8.4.11), so the mysql-mcp image is based on that exact release.
- name: Test mysql-mcp image
env:
MYSQL_TAG: ${{ matrix.mysql-tag }}
run: |
MYSQL_VERSION=${MYSQL_TAG#mysql-}
for arch in amd64 arm64; do
docker buildx build \
--platform "linux/${arch}" \
--file docker/mysql-mcp/Dockerfile \
--build-arg MYSQL_VERSION="$MYSQL_VERSION" \
-t "mysql-mcp-smoke:${MYSQL_VERSION}-${arch}" \
--load \
.
done
# Full first-start test natively; arm64 under QEMU checks the contents only.
chmod +x scripts/test-mysql-mcp-image.sh
scripts/test-mysql-mcp-image.sh "mysql-mcp-smoke:${MYSQL_VERSION}-amd64"
docker run --rm --platform linux/arm64 --entrypoint /bin/sh "mysql-mcp-smoke:${MYSQL_VERSION}-arm64" -c \
"test -f /usr/lib64/mysql/plugin/component_mcp_guard.so && test -f /usr/share/mysql-mcp/mysql_mcp.sql && test -f /docker-entrypoint-initdb.d/50-mysql-mcp.sh"

- name: mysql-mcp base release
id: mcp-version
env:
MYSQL_TAG: ${{ matrix.mysql-tag }}
run: echo "version=${MYSQL_TAG#mysql-}" >> "$GITHUB_OUTPUT"

- name: Build and push mysql-mcp image
uses: docker/build-push-action@v5
with:
context: .
file: docker/mysql-mcp/Dockerfile
platforms: linux/amd64,linux/arm64
# Never push on a dry_run dispatch — build only, to prove the pipeline.
push: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
tags: |
ghcr.io/${{ github.repository_owner }}/mysql-mcp:${{ matrix.mysql-version }}
ghcr.io/${{ github.repository_owner }}/mysql-mcp:${{ steps.mcp-version.outputs.version }}
build-args: |
MYSQL_VERSION=${{ steps.mcp-version.outputs.version }}
16 changes: 16 additions & 0 deletions .github/workflows/integration-scripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ on:
- "scripts/test-*.py"
- "scripts/test-mcp-guard.sh"
- "scripts/test-mcp-tool-pack.sh"
- "scripts/test-mysql-mcp-image.sh"
- "docker/mysql-mcp/**"
- "scripts/build-component*"
- "scripts/build-plugin*"
- "CMakeLists.txt"
Expand All @@ -59,6 +61,8 @@ on:
- "scripts/test-*.py"
- "scripts/test-mcp-guard.sh"
- "scripts/test-mcp-tool-pack.sh"
- "scripts/test-mysql-mcp-image.sh"
- "docker/mysql-mcp/**"
- "scripts/build-component*"
- "scripts/build-plugin*"
- "CMakeLists.txt"
Expand Down Expand Up @@ -194,6 +198,18 @@ jobs:
chmod +x scripts/test-mcp-guard.sh
scripts/test-mcp-guard.sh "$MYSQL_VERSION" "dist/mcp-guard-${MYSQL_VERSION}"

- name: mysql-mcp image test
# The image docker-publish.yml publishes as ghcr.io/<owner>/mysql-mcp, built on the
# official image of the same patch release.
env:
MYSQL_VERSION: ${{ matrix.mysql-version }}
run: |
cp "dist/mcp-guard-${MYSQL_VERSION}/component_mcp_guard.so" component_mcp_guard-amd64.so
docker build -f docker/mysql-mcp/Dockerfile --build-arg MYSQL_VERSION="$MYSQL_VERSION" \
--build-arg TARGETARCH=amd64 -t "mysql-mcp-smoke:$MYSQL_VERSION" .
chmod +x scripts/test-mysql-mcp-image.sh
scripts/test-mysql-mcp-image.sh "mysql-mcp-smoke:$MYSQL_VERSION"

mcp-tool-pack:
# The mysql_mcp SQL tool pack (sql/mcp/, RFC 0001 Phase 1 in askdba/mysql-mcp-server):
# install, re-install, and parity of every procedure with mysql-mcp-server's own query,
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **`mysql-mcp` Docker images** (Phase 3 of RFC 0001). `ghcr.io/askdba/mysql-mcp:<8.4|9.7|26.10>` (also tagged with the exact release) is the official MySQL image of the release the component is built for, with `component_mcp_guard` and the `mysql_mcp` tool pack installed on first start. `MYSQL_MCP_USER`/`MYSQL_MCP_PASSWORD` create an MCP account (`MCP_USER`, tool pack, read access to `MYSQL_MCP_DATABASES`), and `MYSQL_MCP_ALLOWED_SCHEMAS`/`MYSQL_MCP_MAX_EXECUTION_TIME_MS` persist guard settings. The images are a separate family from the MyVector images. They are published by `docker-publish.yml` with releases and tested in CI by `scripts/test-mysql-mcp-image.sh` (22 checks on 8.4.11, 9.7.2 and 26.10.0).
- **`component_mcp_guard` tool calls and audit messages** (Phase 3 of RFC 0001, server side).
- **`performance_schema.mcp_tool_calls`:** one row per mysql-mcp-server tool call (account, tool, request id, statements, errors, refusals, latency, first and last seen). The guard groups the MCP session statements that start with the label `/* mcp:tool=... req=... */`, keeping the last `mcp.tool_calls_history_size` calls (new read-only variable, default 1000).
- **Audit messages:** each refusal is emitted through `mysql_audit_api_message` (component `mcp_guard`, message `refused`, with the option, connection id, redacted statement and the tool call), so audit log consumers record refusals.
Expand Down
28 changes: 28 additions & 0 deletions docker/mysql-mcp/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# mysql-mcp: the official MySQL image with the MCP components of RFC 0001
# (askdba/mysql-mcp-server): component_mcp_guard and the mysql_mcp SQL tool pack.
# Published as ghcr.io/askdba/mysql-mcp:<8.4|9.7|26.10> by docker-publish.yml.
# Build context: the repository root, with component_mcp_guard-<arch>.so staged there.
# MYSQL_VERSION (required, no default) must be the exact release the component was built
# against, for example 8.4.8: a component built for another patch release can crash the
# server.
ARG MYSQL_VERSION

FROM mysql:${MYSQL_VERSION}
ARG TARGETARCH

LABEL org.opencontainers.image.title="mysql-mcp" \
org.opencontainers.image.description="MySQL with component_mcp_guard and the mysql_mcp tool pack for mysql-mcp-server" \
org.opencontainers.image.source="https://github.com/askdba/myvector" \
org.opencontainers.image.documentation="https://github.com/askdba/myvector/blob/main/docs/MCP_COMPONENTS.md" \
org.opencontainers.image.licenses="GPL-2.0-only AND Apache-2.0"

# Statically linked against libstdc++/libgcc and built in oraclelinux:9, this image's base.
COPY component_mcp_guard-${TARGETARCH}.so /usr/lib64/mysql/plugin/component_mcp_guard.so
COPY sql/mcp/mysql_mcp.sql sql/mcp/uninstall.sql sql/mcp/LICENSE /usr/share/mysql-mcp/
COPY docker/mysql-mcp/mysql-mcp-init.sh /docker-entrypoint-initdb.d/50-mysql-mcp.sh
COPY docker/mysql-mcp/mysql-mcp-healthcheck.sh /usr/local/bin/mysql-mcp-healthcheck

# checkov:skip=CKV_DOCKER_3:MySQL entrypoint needs root for initialization
# Healthy only after first-start setup, on the server's port, without touching root's
# login (see the script).
HEALTHCHECK --interval=30s --timeout=5s --retries=5 CMD ["/usr/local/bin/mysql-mcp-healthcheck"]
16 changes: 16 additions & 0 deletions docker/mysql-mcp/mysql-mcp-healthcheck.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/sh
# Health check of the mysql-mcp image: is mysqld up and done with first-start setup?
#
# - TCP: the entrypoint's temporary setup server does not listen on TCP, so the check
# passes only once the real server runs.
# - The port mysqld was started with (--port=N or --port N on its command line; else 3306).
# - As a user that does not exist, with no password: ping reports a running server even
# when access is denied, and a nonexistent account cannot be locked by failed logins
# (probing as root could lock out administrators with FAILED_LOGIN_ATTEMPTS).
port=$(tr '\0' '\n' </proc/1/cmdline 2>/dev/null | awk '
prev == "--port" || prev == "-P" { p = $0 }
/^--port=/ { p = substr($0, 8) }
{ prev = $0 }
END { print p }')
exec mysqladmin ping --protocol=TCP -h127.0.0.1 -P"${port:-3306}" \
-umysql-mcp-healthcheck --connect-timeout=3 --silent
70 changes: 70 additions & 0 deletions docker/mysql-mcp/mysql-mcp-init.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/bin/bash
# First-start setup of the mysql-mcp image. Sourced by the official entrypoint (it is not
# executable), so docker_process_sql and mysql_note are available.
#
# - installs component_mcp_guard and gives root MCP_ADMIN (to change mcp.* settings);
# - installs the mysql_mcp SQL tool pack;
# - MYSQL_MCP_USER / MYSQL_MCP_PASSWORD: creates an account for mysql-mcp-server that
# holds MCP_USER (so the guard applies to it), EXECUTE on mysql_mcp and SELECT, SHOW
# VIEW on MYSQL_MCP_DATABASES (comma-separated; default MYSQL_DATABASE);
# - MYSQL_MCP_ALLOWED_SCHEMAS, MYSQL_MCP_MAX_EXECUTION_TIME_MS: persisted as
# mcp.allowed_schemas and mcp.max_execution_time_ms.

# Batches with literals start with this, so _mcp_sql_literal's escaping holds even when
# the server runs with NO_BACKSLASH_ESCAPES.
_mcp_backslash_escapes="SET SESSION sql_mode = REPLACE(@@SESSION.sql_mode, 'NO_BACKSLASH_ESCAPES', '');"
_mcp_sql_literal() { # a single-quoted SQL string literal (with backslash escapes)
local s=${1//\\/\\\\}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
printf "'%s'" "${s//\'/\'\'}"
}
_mcp_sql_ident() { # a backquoted identifier
printf '`%s`' "${1//\`/\`\`}"
}

mysql_note "mysql-mcp: installing component_mcp_guard"
docker_process_sql --database=mysql <<<"INSTALL COMPONENT 'file://component_mcp_guard'"
# Dynamic privileges registered after root was created: grant them to every root account.
_mcp_roots=$(docker_process_sql --database=mysql -N -B <<<"SELECT CONCAT(QUOTE(user), '@', QUOTE(host)) FROM mysql.user WHERE user = 'root'")
for _mcp_root in $_mcp_roots; do
docker_process_sql --database=mysql <<<"GRANT MCP_ADMIN ON *.* TO $_mcp_root"
done

mysql_note "mysql-mcp: installing the mysql_mcp tool pack"
docker_process_sql --database=mysql </usr/share/mysql-mcp/mysql_mcp.sql

_mcp_settings=$_mcp_backslash_escapes
if [ -n "${MYSQL_MCP_ALLOWED_SCHEMAS:-}" ]; then
_mcp_settings+="SET PERSIST mcp.allowed_schemas = $(_mcp_sql_literal "$MYSQL_MCP_ALLOWED_SCHEMAS");"
fi
if [ -n "${MYSQL_MCP_MAX_EXECUTION_TIME_MS:-}" ]; then
case "$MYSQL_MCP_MAX_EXECUTION_TIME_MS" in
*[!0-9]*) mysql_error "MYSQL_MCP_MAX_EXECUTION_TIME_MS must be a number of milliseconds" ;;
esac
_mcp_settings+="SET PERSIST mcp.max_execution_time_ms = $MYSQL_MCP_MAX_EXECUTION_TIME_MS;"
fi
[ "$_mcp_settings" = "$_mcp_backslash_escapes" ] || docker_process_sql --database=mysql <<<"$_mcp_settings"

if [ -n "${MYSQL_MCP_USER:-}" ]; then
[ -n "${MYSQL_MCP_PASSWORD:-}" ] || mysql_error "MYSQL_MCP_USER is set but MYSQL_MCP_PASSWORD is not"
[ "$MYSQL_MCP_USER" != root ] || mysql_error "MYSQL_MCP_USER cannot be root"
_mcp_account="$(_mcp_sql_literal "$MYSQL_MCP_USER")@'%'"
mysql_note "mysql-mcp: creating MCP account $MYSQL_MCP_USER"
_mcp_grants="$_mcp_backslash_escapes
CREATE USER $_mcp_account IDENTIFIED BY $(_mcp_sql_literal "$MYSQL_MCP_PASSWORD");
GRANT MCP_USER ON *.* TO $_mcp_account;
GRANT EXECUTE ON mysql_mcp.* TO $_mcp_account;"
IFS=',' read -r -a _mcp_dbs <<<"${MYSQL_MCP_DATABASES:-${MYSQL_DATABASE:-}}"
for _mcp_db in "${_mcp_dbs[@]}"; do
_mcp_db=${_mcp_db#"${_mcp_db%%[![:space:]]*}"} # trim
_mcp_db=${_mcp_db%"${_mcp_db##*[![:space:]]}"}
[ -n "$_mcp_db" ] || continue
# Escape _ and % so the grant names exactly this schema (as the entrypoint does).
_mcp_pattern=${_mcp_db//\\/\\\\}
_mcp_pattern=${_mcp_pattern//_/\\_}
_mcp_pattern=${_mcp_pattern//%/\\%}
_mcp_grants+="
GRANT SELECT, SHOW VIEW ON $(_mcp_sql_ident "$_mcp_pattern").* TO $_mcp_account;"
done
docker_process_sql --database=mysql <<<"$_mcp_grants"
fi
unset _mcp_backslash_escapes _mcp_roots _mcp_root _mcp_settings _mcp_account _mcp_grants _mcp_dbs _mcp_db _mcp_pattern
24 changes: 23 additions & 1 deletion docs/MCP_COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ Enforces a policy on the sessions of mysql-mcp-server, inside `mysqld`, so a cli
### Install

```sql
-- copy component_mcp_guard.so into @@plugin_dir first
-- copy component_mcp_guard.so into @@plugin_dir first: the build for this server's
-- exact release (a build for another patch release, even 8.4.8 on 8.4.11, can crash it)
INSTALL COMPONENT 'file://component_mcp_guard';
GRANT MCP_ADMIN ON *.* TO 'dba'@'%'; -- who may change mcp.* settings
GRANT MCP_USER ON *.* TO 'mcp'@'%'; -- the account mysql-mcp-server uses
Expand All @@ -23,6 +24,27 @@ GRANT EXECUTE ON mysql_mcp.* TO 'mcp'@'%'; -- if the SQL tool pack is install

`UNINSTALL COMPONENT 'file://component_mcp_guard'` removes the privileges, variables and status variables. Grants of `MCP_USER` and `MCP_ADMIN` stay in the grant tables until revoked.

### Docker images (mysql-mcp)

`ghcr.io/askdba/mysql-mcp:<8.4|9.7|26.10>` (amd64, arm64; also tagged with the exact release, such as `8.4.8`) is the official `mysql` image of the release the component was built for, with `component_mcp_guard` and the `mysql_mcp` SQL tool pack. It is separate from the MyVector images. On first start, with an empty data directory, it installs both, grants `MCP_ADMIN` to the root accounts, and reads these settings:

| Variable | Effect |
|---|---|
| `MYSQL_MCP_USER`, `MYSQL_MCP_PASSWORD` | Create this account (host `%`) for mysql-mcp-server, with `MCP_USER`, `EXECUTE` on `mysql_mcp` and `SELECT, SHOW VIEW` on the databases below. |
| `MYSQL_MCP_DATABASES` | Comma-separated databases the account may read. The default is `MYSQL_DATABASE`. |
| `MYSQL_MCP_ALLOWED_SCHEMAS` | Persisted as `mcp.allowed_schemas`. |
| `MYSQL_MCP_MAX_EXECUTION_TIME_MS` | Persisted as `mcp.max_execution_time_ms`. |

The other settings keep their defaults (read-only on). The official image's own variables (`MYSQL_ROOT_PASSWORD`, `MYSQL_DATABASE`, ...) work as usual.

```sh
docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=shop \
-e MYSQL_MCP_USER=mcp -e MYSQL_MCP_PASSWORD=mcp-secret ghcr.io/askdba/mysql-mcp:9.7
MYSQL_DSN='mcp:mcp-secret@tcp(127.0.0.1:3306)/shop' mysql-mcp-server
```

The images are built and published with MyVector releases (`docker-publish.yml`). `scripts/test-mysql-mcp-image.sh <image>` tests the first-start setup, and CI (`integration-scripts.yml`, job `mcp-guard`) runs it on each version.

### MCP sessions

A session is an **MCP session** when its account holds `MCP_USER` and not `MCP_ADMIN`. This is decided at connect and at `COM_CHANGE_USER`, from privileges only; connection attributes and statement comments cannot change it.
Expand Down
Loading
Loading