from mcp.server.mcpserver import MCPServer (+ TransportSecuritySettings)
constructor
FastMCP(name, stateless_http=True)
MCPServer(name) — no transport options
transport config
mcp.settings.host/.port/.transport_security…
run() kwargs; mcp.settings no longer exists
requirement
mcp[fastmcp]>=1.0.0,<2
mcp>=2,<3 (2.x has no [fastmcp] extra)
@mcp.tool() decorators and every handler signature are unchanged. This server never called mcp.get_context(), so nothing there to port.
CI gains an import docs_mcp.server smoke in both workflows — once after pip install -r requirements.txt, once inside the built image (docker run --rm --entrypoint python …). That's the guard that would have caught the seed-mcp and zerto-docs outages: a green build can no longer ship a container that doesn't import.
CLAUDE.md was corrected where it told future sessions to put stateless_http=True on the constructor.
Verification
All on python:3.12-slim, matching the production image.
tools/list diff — byte-for-byte identical between mcp 1.30.0 and 2.2.0, all 10 tools, dumped in wire format (model_dump(by_alias=True, mode="json", exclude_none=True), sorted). The tool-routing risk did not materialize.
Booted for real on streamable-http: initialize → HTTP 200, from localhost and with a container-DNS Host: hvm-docs-mcp:8000 header. Passing host= to run() keeps DNS-rebinding protection off, so the 421 trap that bit git-mcp's self-hosted-ASGI port doesn't apply here.
stateless_http confirmed in effect: no mcp-session-id response header.
stdio still works: initialize + tools/list → 10 tools.
Production image builds and imports under mcp 2.2.0; httpx 0.28.1 and httpx2 2.12.0 coexist, opentelemetry-api 1.44.0 pulled in as a transitive dep.
Retrieval is untouched, so there are no eval numbers to report.
One visible change to clients
serverInfo.version now comes back as "" instead of the SDK version — this server passes no version= to the constructor. Cosmetic, and it matches what morpheus-docs and seed-mcp report; flagging it since clients can see it.
Deploy note
Merging alone doesn't ship this — run Image rebuild (skip scrape) (image-only.yml) to build and push, then Watchtower picks it up. The image smoke runs inside that job, so a broken image can't reach the registry tag unnoticed.
Closes #12
The `mcp<2` pin already landed here in 6723829, so this PR is the second half: the port that lets the pin be lifted. Same mechanical change as justin/seed-mcp#23 and justin/morpheus-docs#10.
## What changed
| | before | after |
|---|---|---|
| import | `from mcp.server.fastmcp import FastMCP` | `from mcp.server.mcpserver import MCPServer` (+ `TransportSecuritySettings`) |
| constructor | `FastMCP(name, stateless_http=True)` | `MCPServer(name)` — no transport options |
| transport config | `mcp.settings.host/.port/.transport_security…` | `run()` kwargs; `mcp.settings` no longer exists |
| requirement | `mcp[fastmcp]>=1.0.0,<2` | `mcp>=2,<3` (2.x has no `[fastmcp]` extra) |
`@mcp.tool()` decorators and every handler signature are **unchanged**. This server never called `mcp.get_context()`, so nothing there to port.
CI gains an `import docs_mcp.server` smoke in **both** workflows — once after `pip install -r requirements.txt`, once inside the built image (`docker run --rm --entrypoint python …`). That's the guard that would have caught the seed-mcp and zerto-docs outages: a green build can no longer ship a container that doesn't import.
`CLAUDE.md` was corrected where it told future sessions to put `stateless_http=True` on the constructor.
## Verification
All on `python:3.12-slim`, matching the production image.
1. **`tools/list` diff — byte-for-byte identical** between mcp 1.30.0 and 2.2.0, all 10 tools, dumped in wire format (`model_dump(by_alias=True, mode="json", exclude_none=True)`, sorted). The tool-routing risk did not materialize.
2. **Booted for real on streamable-http**: `initialize` → HTTP 200, from localhost *and* with a container-DNS `Host: hvm-docs-mcp:8000` header. Passing `host=` to `run()` keeps DNS-rebinding protection off, so the 421 trap that bit git-mcp's self-hosted-ASGI port doesn't apply here.
3. **`stateless_http` confirmed in effect**: no `mcp-session-id` response header.
4. **stdio still works**: `initialize` + `tools/list` → 10 tools.
5. **Production image builds and imports** under mcp 2.2.0; `httpx` 0.28.1 and `httpx2` 2.12.0 coexist, `opentelemetry-api` 1.44.0 pulled in as a transitive dep.
Retrieval is untouched, so there are no eval numbers to report.
## One visible change to clients
`serverInfo.version` now comes back as `""` instead of the SDK version — this server passes no `version=` to the constructor. Cosmetic, and it matches what morpheus-docs and seed-mcp report; flagging it since clients can see it.
## Deploy note
Merging alone doesn't ship this — run **Image rebuild (skip scrape)** (`image-only.yml`) to build and push, then Watchtower picks it up. The image smoke runs inside that job, so a broken image can't reach the registry tag unnoticed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QBx2P1VhKQEzJWoZ96QcnH
mcp 2.0.0 removed `mcp.server.fastmcp`. This repo was pinned to <2
(6723829) to stop a rebuild from crash-looping the container the way
seed-mcp (2026-09-01) and zerto-docs (2026-08-11) did. This lifts the
pin by doing the port — the same mechanical change as
justin/seed-mcp#23 and justin/morpheus-docs#10.
- import: FastMCP -> MCPServer, plus TransportSecuritySettings
- constructor takes no transport options: MCPServer(f"{PRODUCT_NAME}-docs")
- host/port/stateless_http/transport_security are run() kwargs;
`mcp.settings` no longer exists
- requirement becomes plain `mcp>=2,<3` (2.x has no [fastmcp] extra)
@mcp.tool() decorators and all handler signatures are unchanged.
CI now smokes `import docs_mcp.server` twice in both workflows — once
after pip install, once inside the built image — so a green build can
never ship a non-importing container again.
Verified on python:3.12-slim (matches the image):
- tools/list dumped in wire format is BYTE-FOR-BYTE IDENTICAL between
mcp 1.30.0 and 2.2.0, all 10 tools
- streamable-http boots; initialize returns HTTP 200, both from
localhost and with a container-DNS `Host: hvm-docs-mcp:8000` header
(passing host= to run() keeps DNS-rebinding protection off)
- no mcp-session-id response header, i.e. stateless_http is in effect
- stdio boots; initialize + tools/list return 10 tools
- production image builds and imports under mcp 2.2.0; httpx 0.28.1 and
httpx2 2.12.0 coexist as expected
Retrieval is untouched, so no eval numbers are included.
Closes#12
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01QBx2P1VhKQEzJWoZ96QcnH
justin
changed title from feat(mcp): migrate to mcp 2.x (MCPServer), lift the <2 pin to feat(mcp): migrate to mcp 2.x (MCPServer), lift the mcp<2 pin2026-09-10 22:37:31 -04:00
justin
merged commit 8e2b678464 into main2026-09-11 12:20:34 -04:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #12
The
mcp<2pin already landed here in6723829, so this PR is the second half: the port that lets the pin be lifted. Same mechanical change as justin/seed-mcp#23 and justin/morpheus-docs#10.What changed
from mcp.server.fastmcp import FastMCPfrom mcp.server.mcpserver import MCPServer(+TransportSecuritySettings)FastMCP(name, stateless_http=True)MCPServer(name)— no transport optionsmcp.settings.host/.port/.transport_security…run()kwargs;mcp.settingsno longer existsmcp[fastmcp]>=1.0.0,<2mcp>=2,<3(2.x has no[fastmcp]extra)@mcp.tool()decorators and every handler signature are unchanged. This server never calledmcp.get_context(), so nothing there to port.CI gains an
import docs_mcp.serversmoke in both workflows — once afterpip install -r requirements.txt, once inside the built image (docker run --rm --entrypoint python …). That's the guard that would have caught the seed-mcp and zerto-docs outages: a green build can no longer ship a container that doesn't import.CLAUDE.mdwas corrected where it told future sessions to putstateless_http=Trueon the constructor.Verification
All on
python:3.12-slim, matching the production image.tools/listdiff — byte-for-byte identical between mcp 1.30.0 and 2.2.0, all 10 tools, dumped in wire format (model_dump(by_alias=True, mode="json", exclude_none=True), sorted). The tool-routing risk did not materialize.initialize→ HTTP 200, from localhost and with a container-DNSHost: hvm-docs-mcp:8000header. Passinghost=torun()keeps DNS-rebinding protection off, so the 421 trap that bit git-mcp's self-hosted-ASGI port doesn't apply here.stateless_httpconfirmed in effect: nomcp-session-idresponse header.initialize+tools/list→ 10 tools.httpx0.28.1 andhttpx22.12.0 coexist,opentelemetry-api1.44.0 pulled in as a transitive dep.Retrieval is untouched, so there are no eval numbers to report.
One visible change to clients
serverInfo.versionnow comes back as""instead of the SDK version — this server passes noversion=to the constructor. Cosmetic, and it matches what morpheus-docs and seed-mcp report; flagging it since clients can see it.Deploy note
Merging alone doesn't ship this — run Image rebuild (skip scrape) (
image-only.yml) to build and push, then Watchtower picks it up. The image smoke runs inside that job, so a broken image can't reach the registry tag unnoticed.🤖 Generated with Claude Code
https://claude.ai/code/session_01QBx2P1VhKQEzJWoZ96QcnH
feat(mcp): migrate to mcp 2.x (MCPServer), lift the <2 pinto feat(mcp): migrate to mcp 2.x (MCPServer), lift the mcp<2 pin