# 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.