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
32 changes: 32 additions & 0 deletions .github/workflows/integration-scripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ on:
- "sql/**"
- "scripts/test-*.py"
- "scripts/test-mcp-guard.sh"
- "scripts/test-mcp-endpoint.sh"
- "scripts/test-mcp-tool-pack.sh"
- "scripts/test-mysql-mcp-image.sh"
- "docker/mysql-mcp/**"
Expand All @@ -60,6 +61,7 @@ on:
- "sql/**"
- "scripts/test-*.py"
- "scripts/test-mcp-guard.sh"
- "scripts/test-mcp-endpoint.sh"
- "scripts/test-mcp-tool-pack.sh"
- "scripts/test-mysql-mcp-image.sh"
- "docker/mysql-mcp/**"
Expand Down Expand Up @@ -210,6 +212,36 @@ jobs:
chmod +x scripts/test-mysql-mcp-image.sh
scripts/test-mysql-mcp-image.sh "mysql-mcp-smoke:$MYSQL_VERSION"

mcp-endpoint:
# component_mcp (RFC 0001 Phase 4 spike): built in the MySQL 26.10 tree with the Router
# HTTP libraries, then tested end to end with component_mcp_guard and the tool pack
# (scripts/test-mcp-endpoint.sh): bearer auth, the MCP handshake, both tools, and the
# guard's policy in the endpoint's impersonated sessions.
name: mcp-endpoint (26.10.0)
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Build component_mcp_guard and component_mcp
env:
MYVECTOR_BUILD_MCP: "ON"
MYVECTOR_BUILD_MCP_ENDPOINT: "ON"
run: |
chmod +x scripts/build-component-26.10-docker.sh
scripts/build-component-26.10-docker.sh mysql-26.10.0 dist/mcp-26.10.0
ls -la dist/mcp-26.10.0
test -f dist/mcp-26.10.0/component_mcp.so
test -f dist/mcp-26.10.0/component_mcp_guard.so

- name: Endpoint test
run: |
chmod +x scripts/test-mcp-endpoint.sh
scripts/test-mcp-endpoint.sh mysql:26.10.0 dist/mcp-26.10.0

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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **`component_mcp` (RFC 0001 Phase 4 spike, MySQL 26.10).** An MCP Streamable HTTP endpoint inside `mysqld` on MySQL Router's HTTP server (the libraries `component_mysql_rest_service` links), built in the server tree with `MYVECTOR_BUILD_MCP_ENDPOINT=ON ./scripts/build-component-26.10-docker.sh`. It is not for production.
- **Endpoint:** loopback, plain HTTP, one bearer token (`mcp_http.token_sha256`) mapped to one account (`mcp_http.account`).
- **Tools:** `list_databases` (tool pack) and `run_query` (one statement).
- **Sessions:** each call runs in an impersonated `"local"` session, announced with a connection audit event so that `component_mcp_guard` enforces its policy there.
- **Phase 4 exit criterion met:** through the endpoint, an `MCP_USER` account's write and `mysql` read are refused, and an account without `MCP_USER` is not restricted.
- **Measured:** 95 s build, 3.1 MB, 3 threads (MRS: 265 s, 21 MB).
- **Tests:** `scripts/test-mcp-endpoint.sh` (30 checks) and the `mcp-endpoint` CI job. `docs/MCP_COMPONENTS.md` has the details and the Phase 5 facts (locked accounts, `max_user_connections`, Basic-only Router auth).
- **`component_mcp_guard` tests for views, multi-statement packets and parity** (RFC 0001 §5.2 item 7, Q3). `scripts/test-mcp-guard.sh` now has 102 checks on 8.4.11, 9.7.2 and 26.10.0.
- **Views:** a view over another schema or over `mysql` is refused, nested views included (the rules see base tables).
- **Multi-statement packets:** a write or disallowed read in a later statement of one packet is refused, and nothing is written.
Expand Down
80 changes: 80 additions & 0 deletions docs/MCP_COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Server-side components for [mysql-mcp-server](https://github.com/askdba/mysql-mc
|---|---|---|
| `component_mcp_guard` | Phase 2: identity, read-only and schema enforcement, time limit, error log, status variables | MySQL 8.4, 9.7, 26.10 |
| `mysql_mcp` SQL tool pack ([`sql/mcp/`](https://github.com/askdba/myvector/tree/main/sql/mcp)) | 1.0.0 | MySQL 8.0 to 26.10, MariaDB 11.4, 12.3 |
| `component_mcp` | **Phase 4 spike** (not for production): an MCP endpoint inside `mysqld` | MySQL 26.10 |

## component_mcp_guard

Expand Down Expand Up @@ -182,3 +183,82 @@ The option is `mcp.read_only`, `mcp.deny_system_schemas`, `mcp.allowed_schemas`
`scripts/test-mcp-guard.sh <mysql-tag> <artifact-dir>` runs install, restart, persistence, uninstall and the enforcement cases, the tool-call table and its labels, views, multi-statement packets, statements without table access events, sessions opened by components and the parity table below against the official image (123 checks, including a `lower_case_table_names=1` server), and `src/mcp/guard/test_mcp_guard_policy.cc` unit-tests the classification and the SQL scanner. CI (`integration-scripts.yml`, job `mcp-guard`) runs both on MySQL 8.4.11, 9.7.2 and 26.10.0.

**Parity with mysql-mcp-server** (RFC 0001 §5.2 item 7): [`sql/mcp/guard-parity.tsv`](https://github.com/askdba/myvector/blob/main/sql/mcp/guard-parity.tsv) lists statements a `run_query` call might send, with the verdict of mysql-mcp-server's validator (`go`) and of the guard under the default settings (`guard`). Every row where they differ says why. The test checks the guard column, sending each statement as one packet. mysql-mcp-server vendors the file and checks the go column. Both reject every write and locking read. The guard allows a few things the validator does not (session `SET`, `information_schema`, `CALL mysql_mcp.*`) and catches what text checks cannot (a write inside a function called from `SELECT`).

## component_mcp (Phase 4 spike)

An MCP **Streamable HTTP** endpoint (JSON-RPC 2.0 over `POST /mcp`) inside `mysqld`. It runs on MySQL Router's HTTP server, the libraries `component_mysql_rest_service` links (RFC 0001 §5.3). This is the Phase 4 build spike and is **not for production**. It is limited to the following:

- **Network and auth:** loopback only (`127.0.0.1`) and plain HTTP, with no TLS yet. One bearer token, mapped to one account.
- **Tools:** `list_databases` (through the `mysql_mcp` tool pack) and `run_query` (one statement).
- **Results:** `run_query` returns at most 1000 rows and 4 MB of serialized rows (escapes counted), as `{"rows": [{"col": value, ...}], "truncated": true|false}`. A value is a JSON string, `null`, or `{"base64": "..."}` when its bytes are not valid UTF-8 (binary data). The session's `sql_select_limit` stops the server at 1001 rows for statements without their own `LIMIT`.
- **Known spike limit:** the command service's default consumer collects every row while a statement runs, so a statement with a large explicit `LIMIT` is buffered in full in `mysqld` before the reply is cut. Bounding that needs a custom row consumer (`mysql_command_consumer` services), which is Phase 5 work.
- **Missing:** the `account_locked` check, a per-account session cap and column masking. All are in Phase 5.

### Build and install

It links the Router HTTP libraries, so it is built in the MySQL 26.10 source tree, like MRS:

```sh
MYVECTOR_BUILD_MCP=ON MYVECTOR_BUILD_MCP_ENDPOINT=ON \
./scripts/build-component-26.10-docker.sh mysql-26.10.0 dist/mcp-26.10.0
```

Use a random token and hash it outside MySQL, so the plaintext never appears in statement text (general and slow logs, audit logs, `performance_schema` history):

```sh
MCP_TOKEN=$(openssl rand -hex 32)
printf '%s' "$MCP_TOKEN" | sha256sum | cut -d' ' -f1
```

```sql
INSTALL COMPONENT 'file://component_mcp_guard'; -- required: install refuses without it
INSTALL COMPONENT 'file://component_mcp'
SET GLOBAL mcp_http.port = 33480,
GLOBAL mcp_http.account = 'mcp@%',
GLOBAL mcp_http.token_sha256 = '<hex digest from above>';
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

`component_mcp` requires the guard's query consumer (`event_tracking_query.mcp_guard`). The server therefore refuses to install it without `component_mcp_guard`, and refuses to uninstall the guard while it is installed. The guard is what enforces the policy on its sessions.

| Variable | Meaning |
|---|---|
| `mcp_http.port` | TCP port on `127.0.0.1` (read-only, default 33480) |
| `mcp_http.account` | The account (`user@host`) every request runs as (read-only) |
| `mcp_http.token_sha256` | Hex SHA-256 of the bearer token (read-only, not persisted, sensitive: readable only with `SENSITIVE_VARIABLES_OBSERVER`). Requests without the matching `Authorization: Bearer <token>` get 401 |

### How a tool call runs

Each `tools/call` opens a `"local"` command-service session that **impersonates** `mcp_http.account`, with no password, on one of the endpoint's I/O threads. It runs one statement, `CALL mysql_mcp.list_databases()` or the `run_query` SQL, then closes the session. Neither `CLIENT_MULTI_STATEMENTS` nor anything similar is set, so `a; b` is a parse error.

The guard enforces its policy on these sessions only because of two things, both checked by `scripts/test-mcp-endpoint.sh`:

- The endpoint emits `MYSQL_AUDIT_CONNECTION_CONNECT` for each session. The server starts such sessions **unaudited**, so without this event no statement event would reach the guard.
- The guard classifies a session at its first statement. Such a session raises no connection event the guard could use.

So an account with `MCP_USER` is read-only through the endpoint, and its schema rules and time limit apply. An account without it is not treated as an MCP session.

Facts recorded for Phase 5:

- **Locked accounts:** impersonation skips the login checks, so a locked account (`ACCOUNT LOCK`) can be impersonated. The endpoint must check `account_locked` itself (26.10 has the lookup).
- **Connection limits:** `max_user_connections` is not enforced for these sessions, so the endpoint needs its own per-account cap. They do count against `max_connections`.
- **Auth backends:** Router's `http_auth_realm` / `http_auth_backend` support Basic auth only, so the endpoint checks bearer tokens itself (SHA-256, constant-time compare).

### Spike measurements (MySQL 26.10.0, arm64, 16 cores)

| | `component_mcp` | `component_mysql_rest_service` |
|---|---|---|
| Build of the one target, fresh tree (after a 36 s configure) | 95 s | 265 s |
| `.so` size | 3.1 MB | 21 MB |
| Threads added to `mysqld` (one I/O worker) | 3, released on uninstall | — |

### Tests

`scripts/test-mcp-endpoint.sh <mysql-image> <artifact-dir>` (30 checks) installs the guard, the tool pack and `component_mcp` on the official image, then checks:

- bearer auth, `initialize`, `notifications/initialized`, `tools/list` and the JSON-RPC errors;
- both tools, and that `run_query` runs as the mapped account;
- that through the endpoint an `MCP_USER` account's write and a `mysql` read are refused by the guard, and a multi-statement is a parse error;
- that nothing is written and no session leaks;
- that remapped to an account without `MCP_USER`, the same write is not restricted.

CI (`integration-scripts.yml`, job `mcp-endpoint`) builds and runs it on 26.10.0.
18 changes: 18 additions & 0 deletions scripts/build-component-26.10-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ docker run --rm \
-e HOST_GID="$HOST_GID" \
-e OUTPUT_DIR="$OUTPUT_DIR" \
-e MYVECTOR_BUILD_MCP="${MYVECTOR_BUILD_MCP:-OFF}" \
-e MYVECTOR_BUILD_MCP_ENDPOINT="${MYVECTOR_BUILD_MCP_ENDPOINT:-OFF}" \
oraclelinux:9 \
bash -c '
set -e
Expand Down Expand Up @@ -174,6 +175,23 @@ docker run --rm \
fi
echo "==> Built: /workspace/$OUTPUT_DIR/libmyvector_component.so"

# component_mcp (RFC 0001 Phase 4 spike), built when MYVECTOR_BUILD_MCP_ENDPOINT=ON. It
# links MySQL Router'"'"'s HTTP libraries, so it is built in the server tree like
# component_mysql_rest_service: copy src/mcp/endpoint to components/mcp, re-run cmake
# (components are globbed), build that one target.
rm -f "/workspace/$OUTPUT_DIR/component_mcp.so"
case "$(printf "%s" "$MYVECTOR_BUILD_MCP_ENDPOINT" | tr "[:lower:]" "[:upper:]")" in
1 | ON | YES | TRUE | Y)
echo "==> Building component_mcp in the server tree..."
rm -rf "$MYSQL_SRC/components/mcp"
cp -r /workspace/src/mcp/endpoint "$MYSQL_SRC/components/mcp"
cmake -S "$MYSQL_SRC" -B "$MYSQL_BLD" >/dev/null
cmake --build "$MYSQL_BLD" --target component_mcp -j"$(nproc)"
cp "$MYSQL_BLD/plugin_output_directory/component_mcp.so" "/workspace/$OUTPUT_DIR/"
echo "==> Built: /workspace/$OUTPUT_DIR/component_mcp.so"
;;
esac

# Restore host ownership of the MySQL source workspace so the runner user
# can save it via actions/cache@v4 (container runs as root).
chown -R "${HOST_UID}:${HOST_GID}" "/workspace/mysql-server-${MYSQL_TAG}" 2>/dev/null || true
Expand Down
Loading
Loading