Drop in-container auth — MetaMCP guards the user-facing edge

The MCP's port 8000 isn't exposed outside the private mcp-servers_mcp
Docker network, so only the MetaMCP gateway can ever reach it. MetaMCP
itself enforces auth at the gateway → MCP-client edge (bearer token in
its UI), which is the right layer for it. In-container Basic/Bearer was
defense-in-depth that turned out to be friction-in-depth.

Removed:
  - ag_bids_mcp/auth.py (HTTP Basic middleware)
  - tests/test_auth.py (3 tests covering the middleware)
  - AG_BIDS_MCP_USER / AG_BIDS_MCP_PASS env vars from .env.example, README,
    docker-compose.snippet.yml, and deploy/README.md

Server.py simplified — direct `mcp.run(transport=...)` like zerto-docs-mcp,
no Starlette wrapping. 21 tests passing.

Live on 192.168.0.2: container recreated, real MCP initialize handshake
returns 200 + capability metadata over the mcp-servers_mcp network with
no auth header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 16:05:41 -04:00
parent 8aa4cc0ef3
commit e78733d55e
7 changed files with 46 additions and 294 deletions
+21 -41
View File
@@ -1,13 +1,18 @@
# Deploying `ag-bids-mcp` behind MetaMCP
This runs on the **MetaMCP host (`192.168.0.2`)** as its **own standalone
docker-compose project** — independent from the zerto-docs-rag stack but
joining the same `mcp-servers_mcp` Docker network so MetaMCP can proxy to it
by container DNS name (`http://ag-bids-mcp:8000/mcp`).
docker-compose project** — independent from the MetaMCP/zerto-docs stack
but joining the same `mcp-servers_mcp` Docker network so MetaMCP can proxy
to it by container DNS name (`http://ag-bids-mcp:8000/mcp`).
The deploy lives in `/home/justin/ag-bids-mcp/` on `.0.2`. Watchtower (already
running on the host) auto-pulls a new image every 5 min whenever a fresh
`git.jpaul.io/justin/ag-bids-mcp:latest` is pushed.
The deploy lives in `/home/justin/ag-bids-mcp/` on `.0.2`. Watchtower
(already running on the host) auto-pulls a new image every 5 min whenever
a fresh `git.jpaul.io/justin/ag-bids-mcp:latest` is pushed.
**No in-container auth.** Port 8000 is never exposed outside the private
`mcp-servers_mcp` Docker network — only MetaMCP can reach it. MetaMCP
enforces auth at the gateway → MCP-client edge (bearer token in its UI),
which is the right layer for it.
## One-time setup (already done — kept for re-runs)
@@ -25,13 +30,6 @@ Create `/home/justin/ag-bids-mcp/.env` (mode 600) on `.0.2` with:
AG_BIDS_API_URL=https://agbids.paul.farm
AG_BIDS_API_KEY=<copy BRIEF_API_KEY from ag-monitor .env on 192.168.0.126>
AG_BIDS_API_TIMEOUT_SECS=20
AG_BIDS_MCP_USER=<pick a username; non-user-facing>
AG_BIDS_MCP_PASS=<32+ random bytes>
```
Generate a password locally:
```bash
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
```
### 3. Build + push the image
@@ -52,7 +50,7 @@ ssh justin@192.168.0.2
cd ~/ag-bids-mcp
docker compose pull
docker compose up -d
docker compose logs -f ag-bids-mcp # expect "starting ag-bids MCP on streamable-http://0.0.0.0:8000 (Basic auth enforced)"
docker compose logs -f ag-bids-mcp # expect "starting ag-bids MCP on streamable-http://0.0.0.0:8000"
```
### 5. Register the namespace in MetaMCP
@@ -63,9 +61,8 @@ In the MetaMCP web UI at `https://mcp.jpaul.io`:
2. **Add upstream MCP server** to that namespace:
- **Transport:** Streamable HTTP
- **URL:** `http://ag-bids-mcp:8000/mcp`
- **Authentication:** Basic
- **Username:** matches `AG_BIDS_MCP_USER`
- **Password:** matches `AG_BIDS_MCP_PASS`
- **Bearer token:** leave blank (the upstream has no auth; MetaMCP
enforces at the user-facing endpoint instead)
3. Save.
Public endpoint becomes:
@@ -73,27 +70,15 @@ Public endpoint becomes:
## Smoke test
From inside the `mcp-servers_mcp` network (e.g. the `metamcp` container)
the MCP should 401 anonymous, 200 authenticated:
From inside the `mcp-servers_mcp` network the MCP should respond to a real
MCP `initialize` handshake:
```bash
ssh justin@192.168.0.2 'docker exec metamcp wget -qS -O- --tries=1 http://ag-bids-mcp:8000/mcp 2>&1 | head -3'
# expect: HTTP/1.1 401 Unauthorized
```
Then, with creds, an MCP `initialize` handshake should return capability
metadata:
```bash
# from the .0.2 host
USER=<env value> PASS=<env value>
CREDS=$(printf '%s:%s' "$USER" "$PASS" | base64 -w0)
docker run --rm --network mcp-servers_mcp curlimages/curl:latest \
-s -H "Authorization: Basic $CREDS" \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
ssh justin@192.168.0.2 'docker run --rm --network mcp-servers_mcp curlimages/curl:latest \
-s -H "content-type: application/json" \
-H "accept: application/json, text/event-stream" \
-X POST http://ag-bids-mcp:8000/mcp \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"1"}}}'
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"smoke\",\"version\":\"1\"}}}"'
# expect: event: message ... "serverInfo":{"name":"ag-bids", ...}
```
@@ -104,11 +89,7 @@ against `https://mcp.jpaul.io/metamcp/ag-bids/mcp`, try:
- **"What's the current price of lime?"** → calls `current_lime_price()`
- **"Are any sources down?"** → calls `source_health()`
## Rotating credentials
To rotate the Basic password: change `AG_BIDS_MCP_PASS` in `~/ag-bids-mcp/.env`
on `.0.2``docker compose up -d` to restart with the new value → update
the MetaMCP namespace's upstream Basic password to match.
## Rotating the upstream API key
To rotate the upstream API key: change `BRIEF_API_KEY` in ag-monitor's `.env`
on `.0.126` + restart `api` there, then update `AG_BIDS_API_KEY` in
@@ -118,4 +99,3 @@ on `.0.126` + restart `api` there, then update `AG_BIDS_API_KEY` in
- Per-tool-call usage logs: `/home/justin/ag-bids-mcp/ag-bids-mcp-logs/usage-YYYY-MM-DD.jsonl`
- Container stdout: `docker compose logs ag-bids-mcp`
- Successful auth → no log line; failed auth → INFO line with the offending path
+9 -8
View File
@@ -1,15 +1,16 @@
# Standalone docker-compose for ag-bids-mcp.
#
# This file is the ENTIRE compose project — it does NOT belong inside
# zerto-docs-rag's compose. Put it in /home/justin/ag-bids-mcp/ on the
# MetaMCP host (192.168.0.2) alongside a .env file with three secrets:
# This file is the ENTIRE compose project — it does NOT belong inside the
# MetaMCP compose. Put it in /home/justin/ag-bids-mcp/ on the MetaMCP host
# (192.168.0.2) alongside a .env file with ONE secret:
# AG_BIDS_API_KEY — copy from ag-monitor's .env (BRIEF_API_KEY) on .0.126
# AG_BIDS_MCP_USER — username MetaMCP will send in Basic auth
# AG_BIDS_MCP_PASS — password MetaMCP will send in Basic auth
#
# Joins the EXISTING `mcp-servers_mcp` network (created by the MetaMCP
# compose project at /home/justin/mcp-servers/) as external, so MetaMCP
# can reach this container by DNS name `ag-bids-mcp`.
# No in-container auth is needed because port 8000 is never exposed outside
# the private `mcp-servers_mcp` Docker network — the only client that can
# reach it is the MetaMCP gateway, which handles auth at the user-facing
# edge. Joins the EXISTING `mcp-servers_mcp` network (created by the
# MetaMCP compose project at /home/justin/mcp-servers/) as external so
# MetaMCP can reach this container by DNS name `ag-bids-mcp`.
services:
ag-bids-mcp: