deps: migrate to mcp 2.x (lift the <2 pin) #6

Merged
claude merged 1 commits from mcp-2x-migration into main 2026-09-10 19:50:11 -04:00
6 changed files with 52 additions and 25 deletions
+7
View File
@@ -92,6 +92,13 @@ jobs:
-t "${REGISTRY_PUSH}/${IMAGE}:${SHA_TAG}" \ -t "${REGISTRY_PUSH}/${IMAGE}:${SHA_TAG}" \
-t "${REGISTRY_PUSH}/${IMAGE}:${CORPUS_TAG}" \ -t "${REGISTRY_PUSH}/${IMAGE}:${CORPUS_TAG}" \
. .
# Smoke: the image must at least import its server module before we
# publish it. mcp 2.x renamed mcp.server.fastmcp -> mcp.server.mcpserver,
# so an unpinned dep produced a green build that crash-looped in prod.
# Import is side-effect-free here (lazy singletons), so this needs no
# Ollama/Chroma.
docker run --rm --entrypoint python \
"${REGISTRY_PUSH}/${IMAGE}:latest" -c "import docs_mcp.server"
docker push "${REGISTRY_PUSH}/${IMAGE}:latest" docker push "${REGISTRY_PUSH}/${IMAGE}:latest"
docker push "${REGISTRY_PUSH}/${IMAGE}:${SHA_TAG}" docker push "${REGISTRY_PUSH}/${IMAGE}:${SHA_TAG}"
docker push "${REGISTRY_PUSH}/${IMAGE}:${CORPUS_TAG}" docker push "${REGISTRY_PUSH}/${IMAGE}:${CORPUS_TAG}"
+7
View File
@@ -156,6 +156,13 @@ jobs:
-t "${REGISTRY_PUSH}/${IMAGE}:${SHA_TAG}" \ -t "${REGISTRY_PUSH}/${IMAGE}:${SHA_TAG}" \
-t "${REGISTRY_PUSH}/${IMAGE}:${CORPUS_TAG}" \ -t "${REGISTRY_PUSH}/${IMAGE}:${CORPUS_TAG}" \
. .
# Smoke: the image must at least import its server module before we
# publish it. mcp 2.x renamed mcp.server.fastmcp -> mcp.server.mcpserver,
# so an unpinned dep produced a green build that crash-looped in prod.
# Import is side-effect-free here (lazy singletons), so this needs no
# Ollama/Chroma.
docker run --rm --entrypoint python \
"${REGISTRY_PUSH}/${IMAGE}:latest" -c "import docs_mcp.server"
docker push "${REGISTRY_PUSH}/${IMAGE}:latest" docker push "${REGISTRY_PUSH}/${IMAGE}:latest"
docker push "${REGISTRY_PUSH}/${IMAGE}:${SHA_TAG}" docker push "${REGISTRY_PUSH}/${IMAGE}:${SHA_TAG}"
docker push "${REGISTRY_PUSH}/${IMAGE}:${CORPUS_TAG}" docker push "${REGISTRY_PUSH}/${IMAGE}:${CORPUS_TAG}"
+14 -7
View File
@@ -68,7 +68,7 @@ Cursor, etc.).
│ ├── index.py # Builds Chroma + BM25 │ ├── index.py # Builds Chroma + BM25
│ └── bm25.py # SQLite FTS5 lexical index │ └── bm25.py # SQLite FTS5 lexical index
├── docs_mcp/ # Phase 3+ — MCP server ├── docs_mcp/ # Phase 3+ — MCP server
│ ├── server.py # FastMCP + tool definitions │ ├── server.py # MCPServer + tool definitions
│ └── usage.py # TimedCall telemetry │ └── usage.py # TimedCall telemetry
├── eval/ # Phase 7 — golden-query harness ├── eval/ # Phase 7 — golden-query harness
│ ├── queries.jsonl.example │ ├── queries.jsonl.example
@@ -124,7 +124,8 @@ need:
- **Lexical store**: SQLite FTS5 (stdlib) - **Lexical store**: SQLite FTS5 (stdlib)
- **Fusion**: Reciprocal Rank Fusion with k=60 - **Fusion**: Reciprocal Rank Fusion with k=60
- **Transport**: streamable-HTTP in prod, stdio for local dev - **Transport**: streamable-HTTP in prod, stdio for local dev
- **MCP framework**: FastMCP with `stateless_http=True` - **MCP framework**: `mcp.server.mcpserver.MCPServer` (mcp 2.x),
run with `stateless_http=True`
- **Container deploy**: Watchtower auto-pull on `:latest`, rollback - **Container deploy**: Watchtower auto-pull on `:latest`, rollback
via `:<sha12>` pin via `:<sha12>` pin
@@ -133,7 +134,7 @@ need:
The template uses `PRODUCT_NAME` env var (defaults to `"myproduct"`) The template uses `PRODUCT_NAME` env var (defaults to `"myproduct"`)
throughout. Set it on first build. References show up in: throughout. Set it on first build. References show up in:
- `docs_mcp/server.py` — `FastMCP(f"{PRODUCT_NAME}-docs", ...)` - `docs_mcp/server.py` — `MCPServer(f"{PRODUCT_NAME}-docs")`
- Collection name (`<product>_docs`) - Collection name (`<product>_docs`)
- BM25 db filename - BM25 db filename
- Tool names that include the product name (e.g., the `_api_lessons` - Tool names that include the product name (e.g., the `_api_lessons`
@@ -200,10 +201,16 @@ python -m scrape.changelog --history-out corpus/.digest/history.jsonl --history-
ENTIRE batch if any doc exceeds `n_ctx_train=1024`. Truncate docs ENTIRE batch if any doc exceeds `n_ctx_train=1024`. Truncate docs
to ~2000 chars before sending to rerank. Full chunk text still to ~2000 chars before sending to rerank. Full chunk text still
goes back to the user; truncation is reranking-only. goes back to the user; truncation is reranking-only.
- **FastMCP `stateless_http=True`**: critical for production - **`stateless_http=True`**: critical for production hosting
hosting behind Watchtower auto-updates. Without it, every behind Watchtower auto-updates. Without it, every container
container recreate produces a 404 storm from clients with recreate produces a 404 storm from clients with stale session
stale session IDs. IDs. Under mcp 2.x this is a `mcp.run()` kwarg, not a
constructor arg — see `main()` in `docs_mcp/server.py`.
- **mcp 2.x, not 1.x**: `mcp.server.fastmcp` was removed in 2.0.0.
This server uses `mcp.server.mcpserver.MCPServer`; `mcp.settings`
no longer exists (host/port/transport_security are `run()`
kwargs). PLAN.md still describes the 1.x API — it tracks the
upstream template, not this repo.
- **Runner shell is `/bin/sh` (dash)**: no `${VAR::N}` substring - **Runner shell is `/bin/sh` (dash)**: no `${VAR::N}` substring
expansion in workflow scripts. Use `cut`/`awk`/`printf`. expansion in workflow scripts. Use `cut`/`awk`/`printf`.
- **Cloudflare 100 MB body cap**: if pushing through a Cloudflare- - **Cloudflare 100 MB body cap**: if pushing through a Cloudflare-
+1 -1
View File
@@ -109,7 +109,7 @@ PRODUCT_NAME=crop_chem python -m docs_mcp.server --transport stdio
│ ├── index.py # Chroma + BM25 builder │ ├── index.py # Chroma + BM25 builder
│ └── bm25.py # FTS5 lexical index │ └── bm25.py # FTS5 lexical index
├── docs_mcp/ ├── docs_mcp/
│ ├── server.py # FastMCP — hybrid+rerank │ ├── server.py # MCPServer (mcp 2.x) — hybrid+rerank
│ ├── lessons.md # Curated knowledge layer │ ├── lessons.md # Curated knowledge layer
│ └── usage.py # TimedCall + JSONL telemetry │ └── usage.py # TimedCall + JSONL telemetry
├── eval/ ├── eval/
+21 -9
View File
@@ -22,9 +22,10 @@ import logging
import os import os
import re import re
from pathlib import Path from pathlib import Path
from typing import Annotated from typing import Annotated, Any
from mcp.server.fastmcp import FastMCP from mcp.server.mcpserver import MCPServer
from mcp.server.transport_security import TransportSecuritySettings
from pydantic import Field from pydantic import Field
from .usage import TimedCall from .usage import TimedCall
@@ -61,9 +62,9 @@ RRF_K = int(os.environ.get("RRF_K", "60"))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# FastMCP setup. # MCPServer setup.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
mcp = FastMCP(f"{PRODUCT_NAME}-docs", stateless_http=True) mcp = MCPServer(f"{PRODUCT_NAME}-docs")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -719,12 +720,23 @@ def main() -> None:
if args.transport == "stdio": if args.transport == "stdio":
mcp.run() mcp.run()
else: return
mcp.settings.host = args.host
mcp.settings.port = args.port # mcp 2.x: transport options are run() kwargs, and `mcp.settings` is gone.
run_kwargs: dict[str, Any] = {"host": args.host, "port": args.port}
if args.transport == "streamable-http":
# Was a constructor arg under 1.x. Required in prod — see CLAUDE.md
# (without it, every Watchtower recreate is a 404 storm from clients
# holding stale session IDs).
run_kwargs["stateless_http"] = True
if os.environ.get("MCP_DISABLE_DNS_REBINDING_PROTECTION") in {"1", "true", "yes"}: if os.environ.get("MCP_DISABLE_DNS_REBINDING_PROTECTION") in {"1", "true", "yes"}:
mcp.settings.transport_security.enable_dns_rebinding_protection = False # Deployed as `chem-mcp:8080` on Drawbar's internal docker network
mcp.run(transport=args.transport) # and never published to a host port, so the Host header the rebind
# check rejects is the only one we ever see.
run_kwargs["transport_security"] = TransportSecuritySettings(
enable_dns_rebinding_protection=False,
)
mcp.run(transport=args.transport, **run_kwargs)
if __name__ == "__main__": if __name__ == "__main__":
+1 -7
View File
@@ -1,11 +1,5 @@
# MCP server # MCP server
# Pinned below 2.0: mcp 2.0.0 removed `mcp.server.fastmcp`, which this mcp>=2,<3 # 2.x: no [fastmcp] extra; FastMCP -> MCPServer
# server imports. The unpinned floor pulled 2.0.0 into a rebuild on
# 2026-08-11 and crash-looped zerto-docs with
# "ModuleNotFoundError: No module named 'mcp.server.fastmcp'".
# 2.0 also drops the [fastmcp] extra. Migrating to the 2.x API is a
# deliberate piece of work — do not lift this pin without it.
mcp[fastmcp]>=1.0.0,<2
pydantic>=2.0 pydantic>=2.0
httpx>=0.27 httpx>=0.27