refactor: move to fastmcp, off the SDK's own FastMCP #5

Merged
airaneel merged 1 commit from port-fastmcp into main 2026-08-08 03:31:32 +03:00
Owner

Follows #4, which capped mcp<2 to 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=None replaces structured_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 -> str tool answers twice and Claude Desktop renders {"result": "…"} instead of the output.
  • Context from 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 every run_command was rejected with a validation error before reaching a shell. The tests caught it; nothing short of production would have.
  • Image from fastmcp, so read_image still becomes ImageContent.
  • Host/port move to run() — arguments to serving over HTTP, not properties of a server that also speaks stdio.
  • The lifespan is gone, not ported. Since AppState became a module constant it had nothing to set up or tear down. Tools reach it directly — one fewer place for per-connection state to reappear by accident.
  • auth gets simpler: a RemoteAuthProvider publishes the RFC 9728 metadata, so serving it at the bare path too is a get_routes override rather than a hand-registered route.

Verification

  • 113 passed, mypy --strict and ruff clean.
  • Live streamable-http server: 13 tools listed, babash_initialize and run_command return real output, structured_content is None.
  • Live against the real issuer: 401 carries 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_URL is set.

🤖 Generated with Claude Code

Follows #4, which capped `mcp<2` to 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=None`** replaces `structured_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 `-> str` tool answers twice and Claude Desktop renders `{"result": "…"}` instead of the output. - **`Context` from 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 *every* `run_command` was rejected with a validation error before reaching a shell. The tests caught it; nothing short of production would have. - **`Image` from fastmcp**, so `read_image` still becomes ImageContent. - **Host/port move to `run()`** — arguments to serving over HTTP, not properties of a server that also speaks stdio. - **The lifespan is gone**, not ported. Since AppState became a module constant it had nothing to set up or tear down. Tools reach it directly — one fewer place for per-connection state to reappear by accident. - **auth gets simpler**: a `RemoteAuthProvider` publishes the RFC 9728 metadata, so serving it at the bare path too is a `get_routes` override rather than a hand-registered route. ## Verification - `113 passed`, `mypy --strict` and `ruff` clean. - Live streamable-http server: 13 tools listed, `babash_initialize` and `run_command` return real output, `structured_content` is None. - Live against the real issuer: 401 carries `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_URL` is set. 🤖 Generated with [Claude Code](https://claude.com/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>
airaneel deleted branch port-fastmcp 2026-08-08 03:31:32 +03:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
airaneel/babash!5
No description provided.