refactor: move to fastmcp, off the SDK's own FastMCP #5
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "port-fastmcp"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Follows #4, which capped
mcp<2to stop the crash-loop. A floor is not a destination — this moves babash onto fastmcp, where that lineage continued and where the rest of the MCP fleet here already runs.What moved
Most of the surface did not. Tools keep registering through
text_tool; the port is one line inside it.output_schema=Nonereplacesstructured_output=False. The default is a sentinel meaning derive one from the return annotation, so omitting it is a question, not an answer — without this every-> strtool answers twice and Claude Desktop renders{"result": "…"}instead of the output.Contextfrom fastmcp. A real break, not a rename: the SDK Context is not a type fastmcp injects, so it was treated as an ordinary required argument and everyrun_commandwas rejected with a validation error before reaching a shell. The tests caught it; nothing short of production would have.Imagefrom fastmcp, soread_imagestill becomes ImageContent.run()— arguments to serving over HTTP, not properties of a server that also speaks stdio.RemoteAuthProviderpublishes the RFC 9728 metadata, so serving it at the bare path too is aget_routesoverride rather than a hand-registered route.Verification
113 passed,mypy --strictandruffclean.babash_initializeandrun_commandreturn real output,structured_contentis None.resource_metadata, PRM served at both the RFC 9728 path and the bare one nginx asks for, a bogus bearer is refused after an actual introspection call.Deployment is unchanged and still off by default — auth engages only when
MCP_AUTH_URLis set.🤖 Generated with Claude Code
The mcp SDK removed `mcp.server.fastmcp` in 2.0 — the class this server was built on — replacing it with a different API under a new name. Pinning below that major stopped the crash-loop it caused, but a floor is not a destination: it left babash on a major nothing else will follow, still importing a module that no longer exists upstream. fastmcp is where that lineage continued, and it is already what the rest of the MCP fleet here runs, so this also ends babash being the odd one out. It keeps mcp as a dependency because the tool annotations and the auth types still come from there directly. Most of the surface did not move. Tools go on registering through `text_tool`, which is what that wrapper was for — the port is one line inside it. The rest: - Structured output is suppressed with `output_schema=None` rather than `structured_output=False`. The default is a sentinel meaning "derive one from the return annotation", so omitting it is a question, not the answer; without this every `-> str` tool would again answer twice and Claude Desktop would render `{"result": "…"}` instead of the output. - `Context` now comes from fastmcp. This was a real break, not a rename: the SDK's Context is not a type fastmcp injects, so it was treated as an ordinary required argument and *every* run_command was rejected with a validation error before reaching a shell. The tests caught it; nothing else would have, short of production. - `Image` likewise comes from fastmcp, so read_image keeps returning something the framework turns into ImageContent. - Host and port move from the constructor to `run()`. They are arguments to serving over HTTP, not properties of a server that also speaks stdio. - The lifespan is gone rather than ported. Since AppState became a module constant it had nothing left to set up or tear down: it yielded the constant and get_app read it back out of a contextvar. Tools reach it directly now — the same object by a shorter route, and one fewer place for per-connection state to reappear by accident. - The auth module gets simpler for the same reason its sibling MCPs are: a RemoteAuthProvider publishes the RFC 9728 metadata, so serving it at the bare path too is an override of `get_routes` rather than a hand-registered route, and AccessToken carries claims. Tests follow the same distinction. test_tool_surface now reads tools through an in-memory client instead of the server's registry, because the wire form is what it is actually asserting about; test_process_app states the process-wide-registry invariant without reference to a lifespan; test_no_loop_blocking drops a fake request context that get_app would now ignore. 113 tests pass, mypy --strict and ruff are clean, and a live streamable-http server answers tools/list, babash_initialize and run_command with no structuredContent wrapper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>