Flavors¶
--flavor (and build_server(layer, flavor)) chooses which implementation speaks MCP. The
layer, the policy and the results are identical across flavors; what differs is the wire code
and which serve options apply.
| Flavor | Implementation | Needs | Pick it when |
|---|---|---|---|
auto |
sdk if import mcp.server.lowlevel works, else native |
You do not care | |
native |
cloudg.mcp.native, standard library only |
nothing | Minimal installs, air-gapped hosts, or when you want every option below (paging, CORS, /healthz) |
sdk |
the official SDK's low-level Server, uvicorn for HTTP |
cloudg[mcp] |
You want the reference implementation on the wire, or the SDK's OAuth support when you build the app yourself |
fastmcp |
fastmcp.FastMCP with cloudg components |
fastmcp |
You want fastmcp's middleware, auth providers or mount |
Which serve options each flavor honors:
| Option | native | sdk | fastmcp |
|---|---|---|---|
--transport stdio / http / sse |
yes | yes | yes |
--host, --port, --path |
yes | yes | yes (--path is not used for sse) |
--auth-token |
yes | yes (ASGI middleware) | yes (ASGI middleware) |
--allowed-origin, --allowed-host |
yes | yes (cloudg's OriginHostGuard in front of the SDK app) |
yes (same guard) |
--cors-origin |
yes | refused at start-up | refused at start-up |
--json-response |
yes | yes | yes for http; refused with sse |
--stateless |
yes | yes (affects handshake-era sessions only) | yes for http; refused with sse |
--page-size |
yes | no (one page) | no |
/healthz |
yes | no | no |
| Session id bound to the principal that opened it (another principal presenting it gets 404) | yes | yes (streamable HTTP; ids issued over the legacy SSE transport are not bound) | yes (same) |
Per-principal session cap, connection cap, header and body timeouts (HTTPConfig) |
yes | no (uvicorn's own limits) | no (uvicorn's own limits) |
The flavors share one DNS-rebinding check, cloudg.mcp.native.http.OriginHostGuard. The
native server runs it itself; for the SDK and fastmcp flavors it sits in
TokenAuthASGIMiddleware in front of the framework's app, and the frameworks' own checks are
switched off so requests are not filtered twice. The rules, identical everywhere:
Originis checked on every bind. A request withoutOrigin(non-browser clients) passes; otherwise the origin must match--allowed-origin(default: loopback origins on any port) or be same-origin, meaning itshost:portequals theHostheader. Same-origin counts only when theHostheader itself was validated (a loopback bind, or--allowed-host): on a public bind without--allowed-hosta DNS-rebinding attacker controls both headers. Failure: 403.Hostis checked against--allowed-hostwhen given. Without it, a loopback bind accepts only loopback names (and refuses a missingHost), and any other bind does not check it, since the server cannot know which names it is reached by. Failure: 421.
Against the SDK flavor bound to 0.0.0.0 with a token and no --allowed-host, an
Origin: https://evil.example got 403 Forbidden with
{"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Forbidden: invalid Origin"}},
Host: whatever.example got 200 OK, and a request without the token got 401. The fastmcp
flavor (fastmcp 4.0.11) with --json-response answered initialize with
content-type: application/json and refused a bad Origin with 403 and a bad Host with 421.
Behind a reverse proxy on a public name, pass --allowed-host with that name.
Versions checked for this release:
| Package | Version | Checked with |
|---|---|---|
| mcp | 2.3.0 | the main dev environment: SDK flavor over stdio and HTTP, MCPServer and low-level Server mounting, both eras |
| mcp | 1.30.0 | scratch venv: FastMCP and low-level Server mounting, adapter, native, middleware and CLI tests |
| fastmcp | 4.0.11 (on mcp 2.3.0) | scratch venv: mounting, adapter tests, the fastmcp flavor over HTTP |
| fastmcp | 3.4.8 (on mcp 1.30.0) | scratch venv: mounting, adapter tests |
| fastmcp | 2.14.7 (on mcp 1.30.0) | scratch venv: mounting, adapter tests |
The mcp extra is mcp>=1.30,<3, the range those versions cover. How tightly to pin beyond
that depends on how you use the SDK:
- Mounting into the SDK's high-level servers reaches into private attributes:
MCPServer._lowlevel_server(mcp 2.x),FastMCP._mcp_server(mcp 1.x, and fastmcp's inner server), andMCPServer._subscriptionsfor the 2026 notification bus. A minor SDK release can rename them;installthen raises aTypeErrorthat says so. For this path, pin the minor version you tested. - Mounting into a low-level
Server, and the SDK flavor (which builds one), use the request-handler API (get_request_handler/add_request_handleron 2.x, which the SDK marks provisional, andrequest_handlerson 1.x). The extra's range is enough there. The one private attribute on this path is the 2.x session's_connection, read only to track handshake-era sessions for change notifications. - The native flavor depends on no SDK at all.