From 4ec2757b6d4a7b1f54bc680a3af2c7d96a72e68e Mon Sep 17 00:00:00 2001 From: "marvin-context-protocol[bot]" <225465937+marvin-context-protocol[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:44:54 -0400 Subject: [PATCH] chore: Update SDK documentation (#4569) Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com> --- docs/python-sdk-pages.json | 1 + docs/python-sdk/fastmcp-mcp_config.mdx | 32 ++-- .../fastmcp-utilities-asgi_transport.mdx | 92 ++++++++++++ .../fastmcp-utilities-async_utils.mdx | 23 ++- docs/python-sdk/fastmcp-utilities-inspect.mdx | 10 +- docs/python-sdk/fastmcp-utilities-tests.mdx | 137 ++++++++++++++++-- 6 files changed, 261 insertions(+), 34 deletions(-) create mode 100644 docs/python-sdk/fastmcp-utilities-asgi_transport.mdx diff --git a/docs/python-sdk-pages.json b/docs/python-sdk-pages.json index 6d71d0d53..eb525313d 100644 --- a/docs/python-sdk-pages.json +++ b/docs/python-sdk-pages.json @@ -35,6 +35,7 @@ "group": "fastmcp.utilities", "pages": [ "python-sdk/fastmcp-utilities-__init__", + "python-sdk/fastmcp-utilities-asgi_transport", "python-sdk/fastmcp-utilities-async_utils", "python-sdk/fastmcp-utilities-auth", "python-sdk/fastmcp-utilities-authorization", diff --git a/docs/python-sdk/fastmcp-mcp_config.mdx b/docs/python-sdk/fastmcp-mcp_config.mdx index 8dbfb102c..44e287d46 100644 --- a/docs/python-sdk/fastmcp-mcp_config.mdx +++ b/docs/python-sdk/fastmcp-mcp_config.mdx @@ -42,7 +42,7 @@ infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse'] Infer the appropriate transport type from the given URL. -### `update_config_file` +### `update_config_file` ```python update_config_file(file_path: Path, server_name: str, server_config: CanonicalMCPServerTypes) -> None @@ -57,7 +57,7 @@ worry about transforming server objects here. ## Classes -### `StdioMCPServer` +### `StdioMCPServer` MCP server configuration for stdio transport. @@ -67,19 +67,19 @@ This is the canonical configuration format for MCP servers using stdio transport **Methods:** -#### `to_transport` +#### `to_transport` ```python to_transport(self) -> StdioTransport ``` -### `TransformingStdioMCPServer` +### `TransformingStdioMCPServer` A Stdio server with tool transforms. -### `RemoteMCPServer` +### `RemoteMCPServer` MCP server configuration for HTTP/SSE transport. @@ -89,19 +89,19 @@ This is the canonical configuration format for MCP servers using remote transpor **Methods:** -#### `to_transport` +#### `to_transport` ```python to_transport(self) -> StreamableHttpTransport | SSETransport ``` -### `TransformingRemoteMCPServer` +### `TransformingRemoteMCPServer` A Remote server with tool transforms. -### `MCPConfig` +### `MCPConfig` A configuration object for MCP Servers that conforms to the canonical MCP configuration format @@ -113,7 +113,7 @@ For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class. **Methods:** -#### `wrap_servers_at_root` +#### `wrap_servers_at_root` ```python wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any] @@ -122,7 +122,7 @@ wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any] If there's no mcpServers key but there are server configs at root, wrap them. -#### `add_server` +#### `add_server` ```python add_server(self, name: str, server: MCPServerTypes) -> None @@ -131,7 +131,7 @@ add_server(self, name: str, server: MCPServerTypes) -> None Add or update a server in the configuration. -#### `from_dict` +#### `from_dict` ```python from_dict(cls, config: dict[str, Any]) -> Self @@ -140,7 +140,7 @@ from_dict(cls, config: dict[str, Any]) -> Self Parse MCP configuration from dictionary format. -#### `to_dict` +#### `to_dict` ```python to_dict(self) -> dict[str, Any] @@ -149,7 +149,7 @@ to_dict(self) -> dict[str, Any] Convert MCPConfig to dictionary format, preserving all fields. -#### `write_to_file` +#### `write_to_file` ```python write_to_file(self, file_path: Path) -> None @@ -158,7 +158,7 @@ write_to_file(self, file_path: Path) -> None Write configuration to JSON file. -#### `from_file` +#### `from_file` ```python from_file(cls, file_path: Path) -> Self @@ -167,7 +167,7 @@ from_file(cls, file_path: Path) -> Self Load configuration from JSON file. -### `CanonicalMCPConfig` +### `CanonicalMCPConfig` Canonical MCP configuration format. @@ -178,7 +178,7 @@ The format is designed to be client-agnostic and extensible for future use cases **Methods:** -#### `add_server` +#### `add_server` ```python add_server(self, name: str, server: CanonicalMCPServerTypes) -> None diff --git a/docs/python-sdk/fastmcp-utilities-asgi_transport.mdx b/docs/python-sdk/fastmcp-utilities-asgi_transport.mdx new file mode 100644 index 000000000..25fd6e2f8 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-asgi_transport.mdx @@ -0,0 +1,92 @@ +--- +title: asgi_transport +sidebarTitle: asgi_transport +--- + +# `fastmcp.utilities.asgi_transport` + + +An in-process, full-duplex HTTP transport for driving ASGI applications from httpx. + +Ported from the MCP Python SDK's test suite (`tests/interaction/transports/_bridge.py`, +MIT licensed). + +`httpx2.ASGITransport` runs the application to completion and only then hands the buffered +response to the caller, so a server that streams its response — as the streamable HTTP +transport's SSE responses do — can never converse with the client mid-request: a +server-initiated request nested inside a still-open call deadlocks. +`StreamingASGITransport` removes that limitation by running the application as a background +task and forwarding every `http.response.body` chunk to the client the moment it is sent. +Everything happens on the one event loop: no sockets, no threads, no sleeps. + +The behavioural contract: + +- The request body is buffered before the application is invoked (MCP requests are small + JSON documents); the response streams chunk by chunk. +- Closing the response — or the whole client — delivers `http.disconnect` to the + application, exactly as a real server sees when its peer goes away. +- An exception the application raises before sending `http.response.start` fails the + originating request with that same exception. After the response has started, a failure + is visible to the client only through the response itself (status code, truncated body) — + the same signal a real server over a real socket would give. + +The transport owns an anyio task group for the application tasks; it is opened and closed by +`httpx2.AsyncClient`'s own context manager, so the client must be used as a context manager. +Closing the transport cancels every running application task by default; set +`cancel_on_close=False` to wait for the application's own disconnect handling instead, which +is what the legacy SSE transport relies on for resource cleanup. + + +## Functions + +### `run_asgi_lifespan` + +```python +run_asgi_lifespan(app: ASGIApp) -> AsyncIterator[None] +``` + + +Run an ASGI application's lifespan, driving the protocol as a real server does. + +The application's lifespan runs inside a dedicated task for the whole duration of +the context. This matters because a lifespan typically owns cancel scopes and task +groups — anyio requires those to be exited by the task that entered them, which +rules out entering the lifespan on one task and leaving it on another (as a pytest +fixture's setup and teardown phases may do). + +**Args:** +- `app`: The ASGI application whose lifespan should run. + +**Raises:** +- `RuntimeError`: If the application reports `lifespan.startup.failed`, or reports +`lifespan.shutdown.failed` (or crashes during shutdown) while the context +body itself completed successfully. A failure inside the body takes +precedence and propagates unchanged. + + +## Classes + +### `StreamingASGITransport` + + +Drive an ASGI application in-process, streaming each response as it is produced. + +This is an `httpx2` transport, so it plugs into anything that accepts an +`httpx2.AsyncClient` — including FastMCP's client transports via their +`httpx_client_factory` argument. + +**Args:** +- `app`: The ASGI application to drive (e.g. `FastMCP.http_app()`). +- `cancel_on_close`: When True (the default), closing the transport cancels every +application task still running, so harness teardown can never hang. Set to +False to wait for the application's own disconnect handling to complete +instead, which the legacy SSE server transport relies on for cleanup. + + +**Methods:** + +#### `handle_async_request` + +```python +handle_async_request(self, request: httpx2.Request) -> httpx2.Response +``` diff --git a/docs/python-sdk/fastmcp-utilities-async_utils.mdx b/docs/python-sdk/fastmcp-utilities-async_utils.mdx index 361400e1c..4d6a996e9 100644 --- a/docs/python-sdk/fastmcp-utilities-async_utils.mdx +++ b/docs/python-sdk/fastmcp-utilities-async_utils.mdx @@ -37,10 +37,10 @@ Uses anyio.to_thread.run_sync which properly propagates contextvars, making this safe for functions that depend on context (like dependency injection). -### `gather` +### `gather` ```python -gather(*awaitables: Awaitable[T]) -> list[T] | list[T | BaseException] +gather(awaitables: Iterable[Awaitable[T]]) -> list[T] | list[T | BaseException] ``` @@ -48,8 +48,25 @@ Run awaitables concurrently and return results in order. Uses anyio TaskGroup for structured concurrency. +``awaitables`` is consumed lazily, one item at a time, right before each +is handed to the task group. Callers with a dynamic number of awaitables +should pass a generator expression (e.g. ``gather(f(x) for x in xs)``) +rather than a list or list comprehension: a list comprehension calls +every ``f(x)`` up front, creating a batch of coroutine objects before +this function even starts, whereas a generator expression creates each +coroutine only as this function's own scheduling loop asks for it. That +matters because coroutine creation and scheduling can be interrupted +between any two bytecode instructions by a synchronous signal handler +(for example pytest-timeout's SIGALRM-based per-test timeout). If that +happens while a whole batch of coroutines is sitting unscheduled, they +are silently abandoned and eventually trigger a "coroutine was never +awaited" warning attributed to whatever unrelated code happens to be +running when the garbage collector gets to them. Lazy consumption keeps +the window in which a created-but-unscheduled coroutine can exist as +small as possible. + **Args:** -- `*awaitables`: Awaitables to run concurrently +- `awaitables`: Iterable of awaitables to run concurrently. - `return_exceptions`: If True, exceptions are returned in results. If False, first exception cancels all and raises. diff --git a/docs/python-sdk/fastmcp-utilities-inspect.mdx b/docs/python-sdk/fastmcp-utilities-inspect.mdx index c92f62bcd..fca4da868 100644 --- a/docs/python-sdk/fastmcp-utilities-inspect.mdx +++ b/docs/python-sdk/fastmcp-utilities-inspect.mdx @@ -42,7 +42,7 @@ Extract information from a FastMCP v1.x instance using a Client. - FastMCPInfo dataclass containing the extracted information -### `inspect_fastmcp` +### `inspect_fastmcp` ```python inspect_fastmcp(mcp: FastMCP[Any] | SDKServer) -> FastMCPInfo @@ -61,7 +61,7 @@ and uses the appropriate extraction method. - FastMCPInfo dataclass containing the extracted information -### `format_fastmcp_info` +### `format_fastmcp_info` ```python format_fastmcp_info(info: FastMCPInfo) -> bytes @@ -73,7 +73,7 @@ Format FastMCPInfo as FastMCP-specific JSON. This includes FastMCP-specific fields like tags, enabled, annotations, etc. -### `format_mcp_info` +### `format_mcp_info` ```python format_mcp_info(mcp: FastMCP[Any] | SDKServer) -> bytes @@ -86,7 +86,7 @@ Uses Client to get the standard MCP protocol format with camelCase fields. Includes version metadata at the top level. -### `format_info` +### `format_info` ```python format_info(mcp: FastMCP[Any] | SDKServer, format: InspectFormat | Literal['fastmcp', 'mcp'], info: FastMCPInfo | None = None) -> bytes @@ -136,7 +136,7 @@ Information about a resource template. Information extracted from a FastMCP instance. -### `InspectFormat` +### `InspectFormat` Output format for inspect command. diff --git a/docs/python-sdk/fastmcp-utilities-tests.mdx b/docs/python-sdk/fastmcp-utilities-tests.mdx index bb2b3fd2f..6c1ce3e92 100644 --- a/docs/python-sdk/fastmcp-utilities-tests.mdx +++ b/docs/python-sdk/fastmcp-utilities-tests.mdx @@ -7,7 +7,7 @@ sidebarTitle: tests ## Functions -### `temporary_settings` +### `temporary_settings` ```python temporary_settings(**kwargs: Any) @@ -20,7 +20,7 @@ Temporarily override FastMCP setting values. - `**kwargs`: The settings to override, including nested settings. -### `run_server_in_process` +### `run_server_in_process` ```python run_server_in_process(server_fn: Callable[..., None], *args: Any, **kwargs: Any) -> Generator[str, None, None] @@ -43,18 +43,20 @@ not pickleable, so we need a function that creates and runs one. - The server URL. -### `run_server_async` +### `run_server_async` ```python run_server_async(server: FastMCP, port: int | None = None, transport: Literal['http', 'streamable-http', 'sse'] = 'http', path: str = '/mcp', host: str = '127.0.0.1') -> AsyncGenerator[str, None] ``` -Start a FastMCP server as an asyncio task for in-process async testing. +Start a FastMCP server on a real port as an asyncio task. -This is the recommended way to test FastMCP servers. It runs the server -as an async task in the same process, eliminating subprocess coordination, -sleeps, and cleanup issues. +This runs a real uvicorn server in the current process, bound to a real TCP port, +and yields its URL. Use it when the behaviour under test is genuinely about the +network — real sockets, TLS, or a server that must be reachable by something other +than an in-process client. Otherwise prefer `asgi_client` or `asgi_server`, which +exercise the same HTTP stack without binding a port. **Args:** - `server`: FastMCP server instance @@ -64,9 +66,124 @@ sleeps, and cleanup issues. - `host`: Host to bind to (default\: "127.0.0.1") +### `asgi_server` + +```python +asgi_server(server: FastMCP, transport: Literal['http', 'streamable-http', 'sse'] = 'http', path: str | None = None, **http_app_kwargs: Any) -> AsyncGenerator[ASGIServer, None] +``` + + +Serve a FastMCP server's HTTP app in-process, with no socket and no uvicorn. + +This is the fastest way to test a FastMCP server over HTTP. The server's real +Starlette app is built with `http_app()` and its lifespan is started, then every +request is dispatched directly into the app on the current event loop. That skips +port binding, uvicorn startup and connection setup entirely, while still exercising +the full HTTP stack: middleware, authentication, session management and SSE +streaming all run exactly as they do in production. + +Use this as a fixture when several tests share one server but each needs its own +client. For a single test, `asgi_client` hands you a connected client in one step. + +**Args:** +- `server`: FastMCP server instance. +- `transport`: Transport type ("http", "streamable-http", or "sse"). +- `path`: URL path for the server (defaults to "/mcp", or "/sse" for SSE). +- `**http_app_kwargs`: Additional arguments forwarded to `server.http_app()`. + + +### `asgi_client` + +```python +asgi_client(server: FastMCP, transport: Literal['http', 'streamable-http', 'sse'] = 'http', path: str | None = None, **client_kwargs: Any) -> AsyncGenerator[Client, None] +``` + + +Serve a FastMCP server over HTTP in-process and yield a connected `Client`. + +This is the shortest path to testing a server over a real HTTP stack. The server's +Starlette app is built and started, and requests are dispatched straight into it on +the current event loop — no port, no uvicorn, no subprocess — but middleware, +authentication, session management and SSE streaming all behave as in production. + +Reach for `asgi_server` instead when a fixture must serve several tests that each +build their own client, or when a test needs raw HTTP access to the app. + +**Args:** +- `server`: FastMCP server instance. +- `transport`: Transport type ("http", "streamable-http", or "sse"). +- `path`: URL path for the server (defaults to "/mcp", or "/sse" for SSE). +- `headers`: HTTP headers to send with every request. +- `auth`: Client authentication, as accepted by the HTTP transports. +- `**client_kwargs`: Additional arguments forwarded to `Client`. + + ## Classes -### `HeadlessOAuth` +### `ASGIServer` + + +A FastMCP server's real HTTP app, reachable in-process with no sockets. + +Yielded by `asgi_server`. The `url` looks like an ordinary server URL and the app +behind it is the genuine article — auth middleware, session manager, SSE framing and +redirects all run — but every request is dispatched straight into the ASGI +application on the current event loop. + +Because nothing is listening on the network, a plain `httpx2.AsyncClient()` cannot +reach this server. Use `client()` for a FastMCP client, `http_client()` for raw HTTP +assertions, and `transport()` when you need to build the client transport yourself. + + +**Methods:** + +#### `http_client` + +```python +http_client(self, headers: dict[str, str] | None = None, timeout: httpx2.Timeout | None = None, auth: httpx2.Auth | None = None, **kwargs: Any) -> httpx2.AsyncClient +``` + +An `httpx2.AsyncClient` bound to the in-process app, for raw HTTP assertions. + +Relative URLs resolve against the server's base URL, and absolute URLs on the +same origin work too, so `client.get(f"{server.url}/health")` reads the same as +it would against a real server. + +The signature matches `McpHttpClientFactory`, so this method can also be handed +to anything that takes an `httpx_client_factory`. + + +#### `transport` + +```python +transport(self, **kwargs: Any) -> StreamableHttpTransport | SSETransport +``` + +A FastMCP client transport wired to the in-process app. + +Accepts the same keyword arguments as the underlying transport (`headers`, +`auth`, ...); `httpx_client_factory` is supplied automatically. + + +#### `client` + +```python +client(self, **client_kwargs: Any) -> Client +``` + +An unconnected FastMCP `Client` pointed at the in-process app. + +`headers` and `auth` configure the underlying HTTP transport; every other +keyword argument is passed to `Client` (`timeout`, `elicitation_handler`, ...). +Use it as a context manager, exactly like any other client. + +**Args:** +- `headers`: HTTP headers to send with every request. +- `auth`: Client authentication, as accepted by the HTTP transports. +- `**client_kwargs`: Additional arguments forwarded to `Client`. + + +### `HeadlessOAuth` OAuth provider that bypasses browser interaction for testing. @@ -77,7 +194,7 @@ instead of opening a browser and running a callback server. Useful for automated **Methods:** -#### `redirect_handler` +#### `redirect_handler` ```python redirect_handler(self, authorization_url: str) -> None @@ -86,7 +203,7 @@ redirect_handler(self, authorization_url: str) -> None Make HTTP request to authorization URL and store response for callback handler. -#### `callback_handler` +#### `callback_handler` ```python callback_handler(self) -> AuthorizationCodeResult