From d33dba6cc8b8380d352009cfac6258c16d8926a3 Mon Sep 17 00:00:00 2001 From: Alkin Tezuysal Date: Sat, 31 Oct 2026 09:48:32 +0000 Subject: [PATCH 1/4] feat(docker): mysql-mcp images with component_mcp_guard and the tool pack (RFC 0001 Phase 3) A separate image family, ghcr.io//mysql-mcp:<8.4|9.7|26.10>: the official mysql image with component_mcp_guard and the mysql_mcp tool pack, installed on first start by docker/mysql-mcp/mysql-mcp-init.sh. Optional MYSQL_MCP_USER/PASSWORD create an MCP account (MCP_USER, tool pack, read access to MYSQL_MCP_DATABASES); MYSQL_MCP_ALLOWED_SCHEMAS and MYSQL_MCP_MAX_EXECUTION_TIME_MS persist guard settings. - docker-publish.yml: the component job builds the guard (MYVECTOR_BUILD_MCP=ON), tests the image and pushes it with releases (dry_run builds only). - integration-scripts.yml (mcp-guard): builds the image on 8.4.11, 9.7.2, 26.10.0 and runs scripts/test-mysql-mcp-image.sh (17 checks). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docker-publish.yml | 38 ++++++++++++ .github/workflows/integration-scripts.yml | 16 +++++ CHANGELOG.md | 1 + docker/mysql-mcp/Dockerfile | 25 ++++++++ docker/mysql-mcp/mysql-mcp-init.sh | 66 ++++++++++++++++++++ docs/MCP_COMPONENTS.md | 21 +++++++ scripts/test-mysql-mcp-image.sh | 73 +++++++++++++++++++++++ 7 files changed, 240 insertions(+) create mode 100644 docker/mysql-mcp/Dockerfile create mode 100644 docker/mysql-mcp/mysql-mcp-init.sh create mode 100755 scripts/test-mysql-mcp-image.sh diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 6bd7d46..6f6ddc3 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -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//mysql-mcp:. 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 @@ -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 }} @@ -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 @@ -311,3 +318,34 @@ jobs: tags: ghcr.io/${{ github.repository }}:${{ matrix.image-tag }} build-args: | MYSQL_VERSION=${{ matrix.mysql-version }} + + - name: Test mysql-mcp image + env: + MYSQL_VERSION: ${{ matrix.mysql-version }} + run: | + 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: 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 }} + build-args: | + MYSQL_VERSION=${{ matrix.mysql-version }} diff --git a/.github/workflows/integration-scripts.yml b/.github/workflows/integration-scripts.yml index e533a51..65e1c0e 100644 --- a/.github/workflows/integration-scripts.yml +++ b/.github/workflows/integration-scripts.yml @@ -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" @@ -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" @@ -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//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, diff --git a/CHANGELOG.md b/CHANGELOG.md index e404d56..7ec19f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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>` is the official MySQL image 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` (17 checks on 8.4.11, 9.7.2 and 26.10.0). - **`component_mcp_guard` time limit and refusal log** (Phase 2 of RFC 0001, completed). - **`mcp.max_execution_time_ms`:** sets an MCP session's `max_execution_time` at connect. Attempts to raise it are refused with error 1290: via `SET`, or via the `MAX_EXECUTION_TIME(n)` or `SET_VAR(max_execution_time=n)` hints. Lowering it is allowed. - **`mcp.log_denials` (default ON):** writes each refusal to the error log as `[MY-011071] [mcp_guard]`, with string literals redacted, at most 10 lines a minute. diff --git a/docker/mysql-mcp/Dockerfile b/docker/mysql-mcp/Dockerfile new file mode 100644 index 0000000..312ee1b --- /dev/null +++ b/docker/mysql-mcp/Dockerfile @@ -0,0 +1,25 @@ +# 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-.so staged there. +ARG MYSQL_VERSION=9.7 + +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 + +# checkov:skip=CKV_DOCKER_3:MySQL entrypoint needs root for initialization +HEALTHCHECK --interval=30s --timeout=5s --retries=5 CMD \ + if [ -n "$MYSQL_RANDOM_ROOT_PASSWORD" ] && [ -z "${MYSQL_ROOT_PASSWORD:-}" ]; then exit 0; fi; \ + if [ -n "${MYSQL_ROOT_PASSWORD:-}" ]; then mysqladmin ping -uroot -p"$MYSQL_ROOT_PASSWORD" --silent; \ + else mysqladmin ping -uroot --silent; fi diff --git a/docker/mysql-mcp/mysql-mcp-init.sh b/docker/mysql-mcp/mysql-mcp-init.sh new file mode 100644 index 0000000..8a57b6a --- /dev/null +++ b/docker/mysql-mcp/mysql-mcp-init.sh @@ -0,0 +1,66 @@ +#!/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. + +_mcp_sql_literal() { # a single-quoted SQL string literal + local s=${1//\\/\\\\} + 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 ` (amd64, arm64) is the official `mysql` image 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 ` 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. diff --git a/scripts/test-mysql-mcp-image.sh b/scripts/test-mysql-mcp-image.sh new file mode 100755 index 0000000..af52dd2 --- /dev/null +++ b/scripts/test-mysql-mcp-image.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Tests a mysql-mcp image (docker/mysql-mcp/Dockerfile): first-start setup of +# component_mcp_guard, the mysql_mcp tool pack and the MYSQL_MCP_* account and settings, +# and that they survive a restart. +# +# Usage: scripts/test-mysql-mcp-image.sh +# e.g. scripts/test-mysql-mcp-image.sh mysql-mcp-smoke:9.7 +# Requires Docker. bash 3.2+. +set -u + +IMAGE=${1:?image, e.g. mysql-mcp-smoke:9.7} +CNAME=mysql-mcp-image-test-$$ +CNAME_PLAIN=$CNAME-plain +PASS=0 +FAIL=0 +ok() { PASS=$((PASS + 1)); printf ' PASS %s\n' "$1"; } +bad() { FAIL=$((FAIL + 1)); printf ' FAIL %s -- %s\n' "$1" "$2"; } +cleanup() { docker rm -f -v "$CNAME" "$CNAME_PLAIN" >/dev/null 2>&1; } +trap cleanup EXIT +expect() { + if printf '%s\n' "$2" | grep -Eq -- "$3"; then ok "$1"; else bad "$1" "got: $(printf '%s' "$2" | head -c 300)"; fi +} + +# as CONTAINER USER PASS SQL +as() { docker exec -i "$1" mysql -h127.0.0.1 -u"$2" -p"$3" -N --batch -e "$4" 2>&1 | grep -v "Using a password"; } +wait_ready() { + for _ in $(seq 1 120); do + docker exec "$1" mysql -h127.0.0.1 -uroot -p"$2" -e 'select 1' >/dev/null 2>&1 && return 0 + sleep 2 + done + return 1 +} + +echo "== $IMAGE" +# A database name with _ and a password with quotes check the init script's quoting. +docker run -d --name "$CNAME" -e MYSQL_ROOT_PASSWORD=testpass -e MYSQL_DATABASE=app_db \ + -e MYSQL_MCP_USER=mcp -e "MYSQL_MCP_PASSWORD=it's\"a\\pass" \ + -e MYSQL_MCP_ALLOWED_SCHEMAS=app_db -e MYSQL_MCP_MAX_EXECUTION_TIME_MS=5000 "$IMAGE" >/dev/null || exit 1 +wait_ready "$CNAME" testpass || { docker logs "$CNAME" 2>&1 | tail -30; echo "server did not start"; exit 1; } +root() { as "$CNAME" root testpass "$1"; } +mcp() { as "$CNAME" mcp "it's\"a\\pass" "$1"; } +echo " server: $(root 'select version()')" + +expect "init script ran without errors" "$(docker logs "$CNAME" 2>&1 | grep -E 'mysql-mcp:|ERROR' | tr '\n' ' ')" "mysql-mcp: creating MCP account mcp" +expect "no init errors" "$(docker logs "$CNAME" 2>&1 | grep -c '\[ERROR\] \[Entrypoint\]')" "^0$" +expect "component_mcp_guard installed" "$(root "SELECT component_urn FROM mysql.component")" "component_mcp_guard" +expect "mysql_mcp tool pack installed" "$(root "CALL mysql_mcp.version()")" "^[0-9]+\.[0-9]+\.[0-9]+$" +expect "root has MCP_ADMIN" "$(root "SHOW GRANTS FOR root@'%'")" "MCP_ADMIN" +expect "settings persisted" "$(root "SELECT CONCAT_WS(',', @@mcp.allowed_schemas, @@mcp.max_execution_time_ms, @@mcp.read_only)")" "^app_db,5000,1$" +expect "MCP account grants" "$(root "SHOW GRANTS FOR mcp@'%'" | tr '\n' ' ')" "MCP_USER.*SELECT, SHOW VIEW ON \`app[\\]+_db\`.*EXECUTE ON \`mysql_mcp\`" # --batch doubles the backslash +root "CREATE TABLE app_db.t (i INT); INSERT INTO app_db.t VALUES (1), (2)" >/dev/null +expect "MCP account reads its database" "$(mcp "SELECT COUNT(*) FROM app_db.t")" "^2$" +expect "MCP account uses the tool pack" "$(mcp "CALL mysql_mcp.list_tables('app_db')")" "^t" +expect "MCP account gets the time limit" "$(mcp "SELECT @@max_execution_time")" "^5000$" +expect "guard applies to the MCP account" "$(mcp "SELECT * FROM app_db.t FOR UPDATE")" "ERROR 1290 .*mcp.read_only" +expect "root can change mcp.* settings" "$(root "SET GLOBAL mcp.read_only = ON")" "^$" + +docker restart "$CNAME" >/dev/null && wait_ready "$CNAME" testpass +expect "after restart: guard loaded, settings kept" "$(root "SELECT CONCAT_WS(',', @@mcp.allowed_schemas, @@mcp.max_execution_time_ms)")" "^app_db,5000$" +expect "after restart: setup not run again" "$(docker logs "$CNAME" 2>&1 | grep -c 'mysql-mcp: installing component_mcp_guard')" "^1$" + +# Without MYSQL_MCP_* settings: component and tool pack only. +docker run -d --name "$CNAME_PLAIN" -e MYSQL_ROOT_PASSWORD=testpass "$IMAGE" >/dev/null +if wait_ready "$CNAME_PLAIN" testpass; then + expect "plain: component and tool pack" "$(as "$CNAME_PLAIN" root testpass "SELECT COUNT(*) FROM mysql.component WHERE component_urn LIKE '%mcp_guard'; CALL mysql_mcp.version()" | tr '\n' ' ')" "^1 [0-9]+\.[0-9]+\.[0-9]+ $" + expect "plain: default settings" "$(as "$CNAME_PLAIN" root testpass "SELECT CONCAT_WS(',', @@mcp.enabled, @@mcp.read_only, IFNULL(@@mcp.allowed_schemas, ''))")" "^1,1,$" + expect "plain: no MCP account" "$(as "$CNAME_PLAIN" root testpass "SELECT COUNT(*) FROM mysql.user WHERE user = 'mcp'")" "^0$" +else + bad "plain container" "did not start" +fi + +echo "== $IMAGE: $PASS passed, $FAIL failed" +[ "$FAIL" = 0 ] From 177b70b59210934ca413a967905fc5e4d1a4bdd9 Mon Sep 17 00:00:00 2001 From: Alkin Tezuysal Date: Sat, 31 Oct 2026 11:37:55 +0000 Subject: [PATCH 2/4] fix(docker): base mysql-mcp images on the guard's exact build release The dry-run publish showed component_mcp_guard built against 8.4.8 crashing mysql:8.4 (8.4.11) at its first refusal (signal 11); the same build on mysql:8.4.8 passes. Reproduced locally. The publish job now builds the image FROM mysql: (8.4.8, 9.7.0, 26.10.0) and also tags it with that release; docs say a component must match the server's exact release. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docker-publish.yml | 17 ++++++++++++++--- CHANGELOG.md | 2 +- docker/mysql-mcp/Dockerfile | 4 +++- docs/MCP_COMPONENTS.md | 5 +++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 6f6ddc3..515a226 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -319,10 +319,13 @@ jobs: 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_VERSION: ${{ matrix.mysql-version }} + MYSQL_TAG: ${{ matrix.mysql-tag }} run: | + MYSQL_VERSION=${MYSQL_TAG#mysql-} for arch in amd64 arm64; do docker buildx build \ --platform "linux/${arch}" \ @@ -338,6 +341,12 @@ jobs: 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: @@ -346,6 +355,8 @@ jobs: 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 }} + 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=${{ matrix.mysql-version }} + MYSQL_VERSION=${{ steps.mcp-version.outputs.version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec19f0..5ab81a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +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>` is the official MySQL image 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` (17 checks on 8.4.11, 9.7.2 and 26.10.0). +- **`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` (17 checks on 8.4.11, 9.7.2 and 26.10.0). - **`component_mcp_guard` time limit and refusal log** (Phase 2 of RFC 0001, completed). - **`mcp.max_execution_time_ms`:** sets an MCP session's `max_execution_time` at connect. Attempts to raise it are refused with error 1290: via `SET`, or via the `MAX_EXECUTION_TIME(n)` or `SET_VAR(max_execution_time=n)` hints. Lowering it is allowed. - **`mcp.log_denials` (default ON):** writes each refusal to the error log as `[MY-011071] [mcp_guard]`, with string literals redacted, at most 10 lines a minute. diff --git a/docker/mysql-mcp/Dockerfile b/docker/mysql-mcp/Dockerfile index 312ee1b..ec607c7 100644 --- a/docker/mysql-mcp/Dockerfile +++ b/docker/mysql-mcp/Dockerfile @@ -2,7 +2,9 @@ # (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-.so staged there. -ARG MYSQL_VERSION=9.7 +# MYSQL_VERSION 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=9.7.2 FROM mysql:${MYSQL_VERSION} ARG TARGETARCH diff --git a/docs/MCP_COMPONENTS.md b/docs/MCP_COMPONENTS.md index 73e2f75..3adb53c 100644 --- a/docs/MCP_COMPONENTS.md +++ b/docs/MCP_COMPONENTS.md @@ -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 @@ -25,7 +26,7 @@ GRANT EXECUTE ON mysql_mcp.* TO 'mcp'@'%'; -- if the SQL tool pack is install ### Docker images (mysql-mcp) -`ghcr.io/askdba/mysql-mcp:<8.4|9.7|26.10>` (amd64, arm64) is the official `mysql` image 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: +`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 | |---|---| From f888106617bf8239c9c26cd189f8f14b91505bd0 Mon Sep 17 00:00:00 2001 From: Alkin Tezuysal Date: Sat, 31 Oct 2026 12:16:10 +0000 Subject: [PATCH 3/4] =?UTF-8?q?fix(docker):=20review=20of=20#244=20?= =?UTF-8?q?=E2=80=94=20required=20release,=20TCP=20healthcheck,=20NO=5FBAC?= =?UTF-8?q?KSLASH=5FESCAPES?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfile: MYSQL_VERSION has no default, so an image cannot pair the guard with a different release by accident. - HEALTHCHECK: mysqladmin ping over TCP without credentials (ping succeeds on access denied), so MYSQL_RANDOM_ROOT_PASSWORD is probed too and the container is healthy only after first-start setup (the temporary server has no TCP). - Init script: batches with string literals clear NO_BACKSLASH_ESCAPES for the session, so backslashes in MYSQL_MCP_PASSWORD and settings are kept as given. - test-mysql-mcp-image.sh: NO_BACKSLASH_ESCAPES server with a backslash password and setting, health probe (20 checks). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- docker/mysql-mcp/Dockerfile | 15 ++++++++------- docker/mysql-mcp/mysql-mcp-init.sh | 14 +++++++++----- scripts/test-mysql-mcp-image.sh | 13 +++++++++++++ 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab81a4..a77f0e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +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` (17 checks on 8.4.11, 9.7.2 and 26.10.0). +- **`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` (20 checks on 8.4.11, 9.7.2 and 26.10.0). - **`component_mcp_guard` time limit and refusal log** (Phase 2 of RFC 0001, completed). - **`mcp.max_execution_time_ms`:** sets an MCP session's `max_execution_time` at connect. Attempts to raise it are refused with error 1290: via `SET`, or via the `MAX_EXECUTION_TIME(n)` or `SET_VAR(max_execution_time=n)` hints. Lowering it is allowed. - **`mcp.log_denials` (default ON):** writes each refusal to the error log as `[MY-011071] [mcp_guard]`, with string literals redacted, at most 10 lines a minute. diff --git a/docker/mysql-mcp/Dockerfile b/docker/mysql-mcp/Dockerfile index ec607c7..c0b1a1f 100644 --- a/docker/mysql-mcp/Dockerfile +++ b/docker/mysql-mcp/Dockerfile @@ -2,9 +2,10 @@ # (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-.so staged there. -# MYSQL_VERSION 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=9.7.2 +# 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 @@ -21,7 +22,7 @@ COPY sql/mcp/mysql_mcp.sql sql/mcp/uninstall.sql sql/mcp/LICENSE /usr/share/mysq COPY docker/mysql-mcp/mysql-mcp-init.sh /docker-entrypoint-initdb.d/50-mysql-mcp.sh # checkov:skip=CKV_DOCKER_3:MySQL entrypoint needs root for initialization -HEALTHCHECK --interval=30s --timeout=5s --retries=5 CMD \ - if [ -n "$MYSQL_RANDOM_ROOT_PASSWORD" ] && [ -z "${MYSQL_ROOT_PASSWORD:-}" ]; then exit 0; fi; \ - if [ -n "${MYSQL_ROOT_PASSWORD:-}" ]; then mysqladmin ping -uroot -p"$MYSQL_ROOT_PASSWORD" --silent; \ - else mysqladmin ping -uroot --silent; fi +# Over TCP, which the entrypoint's temporary server does not listen on, so the container is +# healthy only after first-start setup. ping succeeds even when access is denied, so it +# needs no password (MYSQL_RANDOM_ROOT_PASSWORD included). +HEALTHCHECK --interval=30s --timeout=5s --retries=5 CMD mysqladmin ping -h127.0.0.1 --silent diff --git a/docker/mysql-mcp/mysql-mcp-init.sh b/docker/mysql-mcp/mysql-mcp-init.sh index 8a57b6a..579bc96 100644 --- a/docker/mysql-mcp/mysql-mcp-init.sh +++ b/docker/mysql-mcp/mysql-mcp-init.sh @@ -10,7 +10,10 @@ # - MYSQL_MCP_ALLOWED_SCHEMAS, MYSQL_MCP_MAX_EXECUTION_TIME_MS: persisted as # mcp.allowed_schemas and mcp.max_execution_time_ms. -_mcp_sql_literal() { # a single-quoted SQL string literal +# 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//\\/\\\\} printf "'%s'" "${s//\'/\'\'}" } @@ -29,7 +32,7 @@ done mysql_note "mysql-mcp: installing the mysql_mcp tool pack" docker_process_sql --database=mysql /dev/null && wait_ready "$CNAME" testpass expect "after restart: guard loaded, settings kept" "$(root "SELECT CONCAT_WS(',', @@mcp.allowed_schemas, @@mcp.max_execution_time_ms)")" "^app_db,5000$" expect "after restart: setup not run again" "$(docker logs "$CNAME" 2>&1 | grep -c 'mysql-mcp: installing component_mcp_guard')" "^1$" +expect "healthy once setup is done" "$(docker exec "$CNAME" mysqladmin ping -h127.0.0.1 --silent 2>&1; echo "rc=$?")" "rc=0" + # Without MYSQL_MCP_* settings: component and tool pack only. docker run -d --name "$CNAME_PLAIN" -e MYSQL_ROOT_PASSWORD=testpass "$IMAGE" >/dev/null if wait_ready "$CNAME_PLAIN" testpass; then @@ -68,6 +70,17 @@ if wait_ready "$CNAME_PLAIN" testpass; then else bad "plain container" "did not start" fi +docker rm -f -v "$CNAME_PLAIN" >/dev/null 2>&1 + +# NO_BACKSLASH_ESCAPES: the password with a backslash still works as configured. +docker run -d --name "$CNAME_PLAIN" -e MYSQL_ROOT_PASSWORD=testpass -e MYSQL_MCP_USER=mcp \ + -e 'MYSQL_MCP_PASSWORD=a\b'"'"'c' -e 'MYSQL_MCP_ALLOWED_SCHEMAS=x\y' "$IMAGE" --sql-mode=NO_BACKSLASH_ESCAPES >/dev/null +if wait_ready "$CNAME_PLAIN" testpass; then + expect "NO_BACKSLASH_ESCAPES: MCP account logs in with its password" "$(as "$CNAME_PLAIN" mcp 'a\b'"'"'c' "SELECT CURRENT_USER()")" "^mcp@%$" + expect "NO_BACKSLASH_ESCAPES: settings keep the backslash" "$(as "$CNAME_PLAIN" root testpass "SELECT @@mcp.allowed_schemas")" '^x\\?\\y$' +else + bad "NO_BACKSLASH_ESCAPES container" "did not start" +fi echo "== $IMAGE: $PASS passed, $FAIL failed" [ "$FAIL" = 0 ] From 5badedfb49d8a98bee4c712b36bb07cbbf2a813c Mon Sep 17 00:00:00 2001 From: Alkin Tezuysal Date: Sat, 31 Oct 2026 16:59:28 +0000 Subject: [PATCH 4/4] fix(docker): health check without root logins, on the server's port Review of #244 (CodeRabbit on f888106): - The probe pinged as root without a password; with FAILED_LOGIN_ATTEMPTS on root it locked root out (reproduced: error 3955 after 3 probes). It now pings as a user that does not exist (ping reports a running server even when access is denied, and a nonexistent account cannot be locked). - It follows the port mysqld was started with (--port=N / --port N), not 3306. - docker/mysql-mcp/mysql-mcp-healthcheck.sh holds the probe. - test-mysql-mcp-image.sh: probes do not lock root; probe follows --port (22). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- docker/mysql-mcp/Dockerfile | 8 ++++---- docker/mysql-mcp/mysql-mcp-healthcheck.sh | 16 ++++++++++++++++ scripts/test-mysql-mcp-image.sh | 20 +++++++++++++++----- 4 files changed, 36 insertions(+), 10 deletions(-) create mode 100755 docker/mysql-mcp/mysql-mcp-healthcheck.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index a77f0e4..aa5aeda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +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` (20 checks on 8.4.11, 9.7.2 and 26.10.0). +- **`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` time limit and refusal log** (Phase 2 of RFC 0001, completed). - **`mcp.max_execution_time_ms`:** sets an MCP session's `max_execution_time` at connect. Attempts to raise it are refused with error 1290: via `SET`, or via the `MAX_EXECUTION_TIME(n)` or `SET_VAR(max_execution_time=n)` hints. Lowering it is allowed. - **`mcp.log_denials` (default ON):** writes each refusal to the error log as `[MY-011071] [mcp_guard]`, with string literals redacted, at most 10 lines a minute. diff --git a/docker/mysql-mcp/Dockerfile b/docker/mysql-mcp/Dockerfile index c0b1a1f..90907fb 100644 --- a/docker/mysql-mcp/Dockerfile +++ b/docker/mysql-mcp/Dockerfile @@ -20,9 +20,9 @@ LABEL org.opencontainers.image.title="mysql-mcp" \ 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 -# Over TCP, which the entrypoint's temporary server does not listen on, so the container is -# healthy only after first-start setup. ping succeeds even when access is denied, so it -# needs no password (MYSQL_RANDOM_ROOT_PASSWORD included). -HEALTHCHECK --interval=30s --timeout=5s --retries=5 CMD mysqladmin ping -h127.0.0.1 --silent +# 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"] diff --git a/docker/mysql-mcp/mysql-mcp-healthcheck.sh b/docker/mysql-mcp/mysql-mcp-healthcheck.sh new file mode 100755 index 0000000..4865e75 --- /dev/null +++ b/docker/mysql-mcp/mysql-mcp-healthcheck.sh @@ -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' /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 diff --git a/scripts/test-mysql-mcp-image.sh b/scripts/test-mysql-mcp-image.sh index cb18154..9929fd9 100755 --- a/scripts/test-mysql-mcp-image.sh +++ b/scripts/test-mysql-mcp-image.sh @@ -22,10 +22,12 @@ expect() { } # as CONTAINER USER PASS SQL -as() { docker exec -i "$1" mysql -h127.0.0.1 -u"$2" -p"$3" -N --batch -e "$4" 2>&1 | grep -v "Using a password"; } +# PORT: the server's port (default 3306). +as() { docker exec -i "$1" mysql -h127.0.0.1 -P"${PORT:-3306}" -u"$2" -p"$3" -N --batch -e "$4" 2>&1 | grep -v "Using a password"; } +health() { docker exec "$1" /usr/local/bin/mysql-mcp-healthcheck >/dev/null 2>&1; echo "rc=$?"; } wait_ready() { for _ in $(seq 1 120); do - docker exec "$1" mysql -h127.0.0.1 -uroot -p"$2" -e 'select 1' >/dev/null 2>&1 && return 0 + docker exec "$1" mysql -h127.0.0.1 -P"${PORT:-3306}" -uroot -p"$2" -e 'select 1' >/dev/null 2>&1 && return 0 sleep 2 done return 1 @@ -59,7 +61,12 @@ docker restart "$CNAME" >/dev/null && wait_ready "$CNAME" testpass expect "after restart: guard loaded, settings kept" "$(root "SELECT CONCAT_WS(',', @@mcp.allowed_schemas, @@mcp.max_execution_time_ms)")" "^app_db,5000$" expect "after restart: setup not run again" "$(docker logs "$CNAME" 2>&1 | grep -c 'mysql-mcp: installing component_mcp_guard')" "^1$" -expect "healthy once setup is done" "$(docker exec "$CNAME" mysqladmin ping -h127.0.0.1 --silent 2>&1; echo "rc=$?")" "rc=0" +expect "healthy once setup is done" "$(health "$CNAME")" "rc=0" +# With failed-login locking on root, probes must not lock it (they do not log in as root). +root "ALTER USER root@'%' FAILED_LOGIN_ATTEMPTS 2 PASSWORD_LOCK_TIME 1" >/dev/null +health "$CNAME" >/dev/null; health "$CNAME" >/dev/null; health "$CNAME" >/dev/null +expect "health probes do not lock root" "$(root "SELECT 1")" "^1$" +root "ALTER USER root@'%' FAILED_LOGIN_ATTEMPTS 0 PASSWORD_LOCK_TIME 0" >/dev/null # Without MYSQL_MCP_* settings: component and tool pack only. docker run -d --name "$CNAME_PLAIN" -e MYSQL_ROOT_PASSWORD=testpass "$IMAGE" >/dev/null @@ -72,10 +79,13 @@ else fi docker rm -f -v "$CNAME_PLAIN" >/dev/null 2>&1 -# NO_BACKSLASH_ESCAPES: the password with a backslash still works as configured. +# NO_BACKSLASH_ESCAPES: the password with a backslash still works as configured. This +# server also runs on another port, which the health check must follow. +PORT=3307 docker run -d --name "$CNAME_PLAIN" -e MYSQL_ROOT_PASSWORD=testpass -e MYSQL_MCP_USER=mcp \ - -e 'MYSQL_MCP_PASSWORD=a\b'"'"'c' -e 'MYSQL_MCP_ALLOWED_SCHEMAS=x\y' "$IMAGE" --sql-mode=NO_BACKSLASH_ESCAPES >/dev/null + -e 'MYSQL_MCP_PASSWORD=a\b'"'"'c' -e 'MYSQL_MCP_ALLOWED_SCHEMAS=x\y' "$IMAGE" --sql-mode=NO_BACKSLASH_ESCAPES --port=3307 >/dev/null if wait_ready "$CNAME_PLAIN" testpass; then + expect "health check follows --port" "$(health "$CNAME_PLAIN")" "rc=0" expect "NO_BACKSLASH_ESCAPES: MCP account logs in with its password" "$(as "$CNAME_PLAIN" mcp 'a\b'"'"'c' "SELECT CURRENT_USER()")" "^mcp@%$" expect "NO_BACKSLASH_ESCAPES: settings keep the backslash" "$(as "$CNAME_PLAIN" root testpass "SELECT @@mcp.allowed_schemas")" '^x\\?\\y$' else