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