deploy/docker-compose.yml was unedited docs-mcp-template boilerplate — untouched since the scaffold commit, still carrying <product>, <registry> and <owner> placeholders — describing a standalone stack that has never existed. crop-chem-docs runs as the `chem-mcp` service inside Drawbar's parent compose. It also set MCP_ALLOWED_HOSTS, which no code in this repo reads. The knob is MCP_DISABLE_DNS_REBINDING_PROTECTION. Anyone who trusted the old file and set an allowlist would have gotten a 421 on every request with nothing in the logs to explain it. - deploy/docker-compose.yml: replaced with the real chem-mcp block, a copy of what runs in Drawbar/drawbar-backend. Verified structurally identical to the parent (image, environment, expose, extra_hosts, restart, labels all equal). Carries the why for each setting: the :latest-vs-corpus-tag Watchtower trap (#339), the rebind-protection rationale, and that the OLLAMA_URL override is load-bearing because Drawbar's own ollama service is commented out — the image default http://ollama:11434 does not resolve in that stack, so without the override every search_docs call fails to embed its query. - deploy/drawbar-compose-snippet.md: deleted. It was a second, differently-wrong copy (service name `crop-chem-docs`, ports 8001:8000, and "No environment block needed — the image's defaults handle it", which is false on both the rebind and Ollama counts). Its still-true content (verification commands) moved into the compose file; the tag scheme and deploy chain were already in the README. - deploy/rerank-docker.md: RERANK_URL said http://10.10.1.65:8082. In production the MCP reaches the sidecar by compose service name (http://llama-rerank:8080, baked into the image). Documents the network-attach gotcha that makes rerank fail silently, and keeps the host-IP form for local dev. - README.md: file tree updated for the deleted file; Watchtower poll interval corrected 5 min -> 60s (WATCHTOWER_POLL_INTERVAL=60, as configured on trashpanda). Verified: no <product>/<registry>/<owner> placeholders remain in deploy/ or README, and all six env vars set in the block are ones the server actually reads. Closes #5 Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01FFBDnRWHispovmJVK9rXc9
120 lines
5.9 KiB
YAML
120 lines
5.9 KiB
YAML
# crop-chem-docs service block to MERGE into Drawbar's parent compose
|
|
# file at /home/justin/drawbar/drawbar-backend/docker-compose.yml on
|
|
# trashpanda (10.10.1.65).
|
|
#
|
|
# This is NOT a standalone stack — do not `docker compose up` this file
|
|
# on its own. The MCP is one service inside the Drawbar backend stack,
|
|
# where it is reached over the internal docker network as
|
|
# `chem-mcp:8080` by drawbar-backend-api (CHEM_MCP_BASE_URL). Its tools
|
|
# land in the advisor's catalog under the `chem:` prefix via the
|
|
# mcp_client multiplex. Sibling seed-mcp sits alongside it.
|
|
#
|
|
# Keep this block in sync with the parent compose. It is a copy of what
|
|
# actually runs, so that this repo's deploy/ documents reality rather
|
|
# than an aspiration.
|
|
|
|
services:
|
|
|
|
# crop-chem-docs — ~4.1K US row-crop pesticide / herbicide labels
|
|
# (EPA PPLS + Bayer), ~219K chunks. The advisor consults it for label
|
|
# rates, REI/PHI and rotation restrictions. Chroma + BM25 indexes are
|
|
# baked into the image, so there's no cold-start corpus build, no DB
|
|
# and no auth.
|
|
chem-mcp:
|
|
# :latest, NOT a corpus- tag. Watchtower only re-pulls the tag the
|
|
# container is already running, and corpus-YYYY.MM.DD tags are minted
|
|
# once and never re-pushed — so pinning one while keeping the
|
|
# watchtower label makes the opt-in silently inert. That is how prod
|
|
# sat on the May 2026 corpus through two refreshes until 2026-09-10
|
|
# (Drawbar/drawbar-backend#339). To freeze a snapshot deliberately,
|
|
# pin the corpus tag AND drop the watchtower label.
|
|
image: git.jpaul.io/justin/crop-chem-docs:latest
|
|
environment:
|
|
MCP_TRANSPORT: streamable-http
|
|
MCP_HOST: 0.0.0.0
|
|
MCP_PORT: "8080"
|
|
# DNS-rebinding protection rejects any Host header that isn't in
|
|
# its (empty by default) allowlist, with a 421. On an internal
|
|
# docker network the caller's Host is `chem-mcp:8080` — exactly
|
|
# what gets rejected. Safe to disable here: the container is
|
|
# `expose`d only, never published to a host port, so it is only
|
|
# reachable from inside the compose network.
|
|
#
|
|
# NOTE: this is the only knob this server has for it. There is no
|
|
# MCP_ALLOWED_HOSTS — the code does not read such a variable.
|
|
MCP_DISABLE_DNS_REBINDING_PROTECTION: "1"
|
|
# Query-time embeddings hit Ollama. Drawbar's own `ollama` compose
|
|
# service is commented out, so the image default
|
|
# (OLLAMA_URL=http://ollama:11434) does NOT resolve in this stack —
|
|
# this override is load-bearing, not cosmetic. Without it every
|
|
# search_docs call fails to embed its query.
|
|
OLLAMA_URL: ${CHEM_OLLAMA_URL:-http://host.docker.internal:11434}
|
|
EMBED_MODEL: ${CHEM_EMBED_MODEL:-nomic-embed-text}
|
|
# Not set here on purpose — these come from the image's ENV
|
|
# defaults (see Dockerfile) and are correct for this stack:
|
|
# PRODUCT_NAME=crop_chem
|
|
# HYBRID_SEARCH=true
|
|
# RERANK_URL=http://llama-rerank:8080
|
|
# Override any of them here if the stack's service names differ.
|
|
# Hybrid + rerank is the eval-validated config (MRR 0.672 vs 0.544
|
|
# for BM25 alone; see eval/results/with_rerank.md). Hybrid WITHOUT
|
|
# rerank is worse than BM25 alone — don't ship that combination.
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
expose:
|
|
- "8080"
|
|
restart: unless-stopped
|
|
labels:
|
|
# Watchtower auto-pulls :latest on push from CI. The label is
|
|
# required because the Drawbar stack's watchtower runs in
|
|
# label-mode (WATCHTOWER_LABEL_ENABLE=true); it polls every 60s.
|
|
com.centurylinklabs.watchtower.enable: "true"
|
|
|
|
|
|
# ─── llama-rerank ────────────────────────────────────────────────────
|
|
#
|
|
# The reranker is a SHARED sidecar (chem-mcp and seed-mcp both use it),
|
|
# and it is not declared in the parent compose — it runs as a standalone
|
|
# container. See deploy/rerank-docker.md for how to stand it up.
|
|
#
|
|
# The gotcha: it must be attached to the `drawbar-backend_default`
|
|
# network or `RERANK_URL=http://llama-rerank:8080` resolves via public
|
|
# DNS to an unrelated IP and connection-refuses. The MCP then falls back
|
|
# to dense+BM25 SILENTLY — retrieval quality craters with no error in
|
|
# the log. This bit chem-mcp through 2026-05-25. To fix or re-fix:
|
|
#
|
|
# docker network connect drawbar-backend_default llama-rerank
|
|
#
|
|
# It is idempotent, but it does NOT survive the container being
|
|
# recreated. Better: bring llama-rerank into the parent compose so the
|
|
# attachment is declarative.
|
|
#
|
|
# Confirm it is actually engaged — the header says which mode ran:
|
|
#
|
|
# docker exec drawbar-backend-chem-mcp-1 python -c \
|
|
# "from docs_mcp.server import search_docs; \
|
|
# print(search_docs('soybean herbicide for waterhemp', k=2))"
|
|
#
|
|
# Expect `mode=hybrid-rrf+rerank`. If it reads `mode=hybrid-rrf`, the
|
|
# sidecar is unreachable and you are serving degraded results.
|
|
|
|
# ─── verifying a deploy ──────────────────────────────────────────────
|
|
#
|
|
# docker exec drawbar-backend-chem-mcp-1 python -c \
|
|
# "from docs_mcp.server import corpus_status; print(corpus_status())"
|
|
#
|
|
# Expect the label/chunk counts and the active feature flags. To confirm
|
|
# the transport itself, from the api container:
|
|
#
|
|
# docker exec drawbar-backend-api-1 python -c \
|
|
# "import urllib.request, json; \
|
|
# req=urllib.request.Request('http://chem-mcp:8080/mcp', \
|
|
# data=json.dumps({'jsonrpc':'2.0','id':1,'method':'initialize', \
|
|
# 'params':{'protocolVersion':'2025-06-18','capabilities':{}, \
|
|
# 'clientInfo':{'name':'smoke','version':'0'}}}).encode(), \
|
|
# headers={'Content-Type':'application/json', \
|
|
# 'Accept':'application/json, text/event-stream'}); \
|
|
# print(urllib.request.urlopen(req, timeout=15).status)"
|
|
#
|
|
# Expect 200.
|