diff --git a/deploy/README.md b/deploy/README.md index f051f82..d20c398 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,36 +1,42 @@ # Deploying `ag-bids-mcp` behind MetaMCP -This runs on the **MetaMCP host (192.168.0.2)** alongside `zerto-docs-mcp`. -It joins the same `mcp` Docker network so MetaMCP can proxy to it by container -DNS name (`http://ag-bids-mcp:8000/mcp`). +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`). -## One-time setup (on 192.168.0.2) +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. -### 1. Add env vars +## One-time setup (already done — kept for re-runs) -Edit `/home/justin/zerto-docs-rag/deploy/.env`: +### 1. Pull the deploy snippet + +`docker-compose.snippet.yml` in this directory is the ENTIRE compose file +for the deployment. Copy it to `/home/justin/ag-bids-mcp/docker-compose.yml` +on `.0.2`. + +### 2. Write the `.env` + +Create `/home/justin/ag-bids-mcp/.env` (mode 600) on `.0.2` with: ```ini -# Copy this from ag-monitor's .env on 192.168.0.126 (the BRIEF_API_KEY value). -# Same key powers /api/data/* and /api/brief/*. -AG_BIDS_API_KEY= - -# Credentials MetaMCP will send to the MCP via Basic auth. Generate two -# random values — they're not user-facing, only MetaMCP knows them. -AG_BIDS_MCP_USER=agbids-svc -AG_BIDS_MCP_PASS= +AG_BIDS_API_URL=https://agbids.paul.farm +AG_BIDS_API_KEY= +AG_BIDS_API_TIMEOUT_SECS=20 +AG_BIDS_MCP_USER= +AG_BIDS_MCP_PASS=<32+ random bytes> ``` -### 2. Paste the service block into compose - -Open `/home/justin/zerto-docs-rag/deploy/docker-compose.yml` and append the -`ag-bids-mcp:` block from [docker-compose.snippet.yml](docker-compose.snippet.yml) -inside `services:`, alongside `zerto-docs-mcp:`. Keep the same indentation -(2 spaces). +Generate a password locally: +```bash +python3 -c "import secrets; print(secrets.token_urlsafe(32))" +``` ### 3. Build + push the image -On any dev machine with `docker` and the Gitea registry login: +On any dev machine with the Gitea registry login: ```bash cd ~/github/ag-bids-mcp @@ -39,18 +45,16 @@ docker build -t git.jpaul.io/justin/ag-bids-mcp:latest . docker push git.jpaul.io/justin/ag-bids-mcp:latest ``` -### 4. Start the container +### 4. Start the container on .0.2 ```bash -cd /home/justin/zerto-docs-rag/deploy -docker compose pull ag-bids-mcp -docker compose up -d ag-bids-mcp -docker compose logs -f ag-bids-mcp # confirm "starting ag-bids MCP on streamable-http://0.0.0.0:8000 (Basic auth enforced)" +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)" ``` -Watchtower (already running on the host) will auto-pull updates every 5 -minutes from this point forward. - ### 5. Register the namespace in MetaMCP In the MetaMCP web UI at `https://mcp.jpaul.io`: @@ -67,18 +71,34 @@ In the MetaMCP web UI at `https://mcp.jpaul.io`: Public endpoint becomes: **`https://mcp.jpaul.io/metamcp/ag-bids/mcp`** -### 6. Smoke test from the LAN +## Smoke test + +From inside the `mcp-servers_mcp` network (e.g. the `metamcp` container) +the MCP should 401 anonymous, 200 authenticated: ```bash -# 401 without creds -curl -i http://192.168.0.2:8000/mcp 2>&1 | head -3 -# 200 with creds (Initialize handshake will succeed; the MCP doesn't have -# a plain GET, so a curl probe just confirms auth) -curl -i -u "$AG_BIDS_MCP_USER:$AG_BIDS_MCP_PASS" http://192.168.0.2:8000/mcp 2>&1 | head -3 +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 in a real MCP client (Claude Desktop / OpenWebUI / etc.) configured against -`https://mcp.jpaul.io/metamcp/ag-bids/mcp`, try: +Then, with creds, an MCP `initialize` handshake should return capability +metadata: + +```bash +# from the .0.2 host +USER= PASS= +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' \ + -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"}}}' +# expect: event: message ... "serverInfo":{"name":"ag-bids", ...} +``` + +Then in a real MCP client (Claude Desktop / OpenWebUI / etc.) configured +against `https://mcp.jpaul.io/metamcp/ag-bids/mcp`, try: - **"What's the best place to sell corn today?"** → calls `best_local_bid("corn")` - **"What's the current price of lime?"** → calls `current_lime_price()` @@ -86,16 +106,16 @@ Then in a real MCP client (Claude Desktop / OpenWebUI / etc.) configured against ## Rotating credentials -To rotate the Basic password: change `AG_BIDS_MCP_PASS` in the `.env` on -192.168.0.2 → `docker compose up -d ag-bids-mcp` to restart with the new -value → update the MetaMCP namespace's upstream Basic password to match. +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. To rotate the upstream API key: change `BRIEF_API_KEY` in ag-monitor's `.env` -on 192.168.0.126 + restart `api` there, then update `AG_BIDS_API_KEY` on -192.168.0.2 + restart `ag-bids-mcp`. +on `.0.126` + restart `api` there, then update `AG_BIDS_API_KEY` in +`~/ag-bids-mcp/.env` on `.0.2` + `docker compose up -d`. ## Observability -- Per-tool-call usage logs: `/home/justin/zerto-docs-rag/deploy/ag-bids-mcp-logs/usage-YYYY-MM-DD.jsonl` +- 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 diff --git a/deploy/docker-compose.snippet.yml b/deploy/docker-compose.snippet.yml index 35e706f..946016f 100644 --- a/deploy/docker-compose.snippet.yml +++ b/deploy/docker-compose.snippet.yml @@ -1,18 +1,23 @@ -# Paste this service block into /home/justin/zerto-docs-rag/deploy/docker-compose.yml -# on 192.168.0.2 (the MetaMCP host), alongside zerto-docs-mcp. It joins the -# same `mcp` Docker network so MetaMCP can reach it by container DNS name. +# Standalone docker-compose for ag-bids-mcp. # -# Required env vars (set in the same .env that already powers the rest of the -# MetaMCP stack): -# AG_BIDS_API_KEY — copy from ag-monitor's .env (BRIEF_API_KEY) +# 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: +# 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`. +services: ag-bids-mcp: container_name: ag-bids-mcp image: git.jpaul.io/justin/ag-bids-mcp:latest pull_policy: always restart: unless-stopped + env_file: .env environment: MCP_TRANSPORT: streamable-http MCP_HOST: 0.0.0.0 @@ -20,14 +25,6 @@ # Behind a Docker DNS name, FastMCP's localhost-only rebinding-protection # would 421 every call from MetaMCP. Disable it; the mcp network is private. MCP_DISABLE_DNS_REBINDING_PROTECTION: "1" - # --- upstream ag-monitor (Cloudflare Tunnel from .0.126) --- - AG_BIDS_API_URL: https://agbids.paul.farm - AG_BIDS_API_KEY: ${AG_BIDS_API_KEY} - AG_BIDS_API_TIMEOUT_SECS: "20" - # --- HTTP Basic auth in front of this MCP --- - AG_BIDS_MCP_USER: ${AG_BIDS_MCP_USER} - AG_BIDS_MCP_PASS: ${AG_BIDS_MCP_PASS} - # --- per-tool-call JSONL usage log --- USAGE_LOG_DIR: /app/var/logs USAGE_LOG_KEEP_DAYS: "90" volumes: @@ -35,4 +32,13 @@ - ./ag-bids-mcp-logs:/app/var/logs networks: [mcp] labels: + # Watchtower (on the same daemon) auto-pulls new images for any container + # with this label set to "true". com.centurylinklabs.watchtower.enable: "true" + +networks: + mcp: + external: true + # Confirmed on 192.168.0.2 — joined by metamcp, metamcp-pg, zerto-docs-mcp, + # jina-rerank. Created by the compose project rooted at ~/mcp-servers/. + name: mcp-servers_mcp