--- title: low_level sidebarTitle: low_level --- # `fastmcp.server.low_level` ## Functions ### `client_supports_extension` ```python client_supports_extension(session: ServerSession, extension_id: str) -> bool ``` Check whether the connected client supports a given MCP extension. Inspects the ``extensions`` capability on ``ClientCapabilities`` sent by the client during initialization. In v2 the client's initialize params are reachable via ``session.client_params``. SDK v2 declares ``extensions`` as a real field on ``ClientCapabilities``, so a client sending ``ClientCapabilities(extensions={...})`` populates the field directly. We read that field first and fall back to ``model_extra`` only for legacy-serialized clients that carried ``extensions`` as an extra key. ## Classes ### `FastMCPServerMiddleware` Root dispatch for the FastMCP middleware chain, in the SDK's middleware layer. v2 no longer lets FastMCP subclass ``ServerSession`` (the runner constructs it per request), so the old ``MiddlewareServerSession._received_request`` override is replaced by a ``ServerMiddleware`` — an ordinary entry in the SDK's own middleware list. Sitting at the root of dispatch, this is the single entry point through which *every* inbound message flows — requests, notifications, cancellations, ``initialize``, and even malformed or unroutable messages the SDK can still hand us. It binds the FastMCP request-context ContextVar and re-applies the app-scoped ``SharedContext`` for the whole chain, then runs the FastMCP ``Middleware`` chain so ``on_message`` / ``on_request`` / ``on_notification`` observe the message. Dispatch shapes: - Negotiation runs the *whole* FastMCP chain here: ``initialize`` dispatches through ``on_initialize`` and ``server/discover`` through ``on_discover``. Neither has an interior FastMCP handler adapter, and the SDK serializes both results before returning through its middleware seam, so this root adapter restores core results to typed models before FastMCP middleware observes them. - The component methods (``tools/call``, ``tools/list``, ``resources/read``, ...) still run their FastMCP chain *interior*, in the handler adapter, where ``on_call_tool`` receives the typed component result and a tool exception propagates through ``on_message``/``on_request`` exactly where the built-in error/logging/timing middleware expect it. The root dispatch does not re-run the chain for these — it only steps in when such a request fails *before* the interior runs (malformed params, routing), so ``on_message`` still observes the failure. - Every other message — all notifications (including ``notifications/cancelled`` and ``notifications/initialized``), ``ping``, ``logging/setLevel``, and any unroutable/non-component request — has no interior FastMCP dispatch, so the root dispatch runs the ``"outer"`` pass (``on_message`` plus ``on_request``/``on_notification``) here, wrapping the real SDK dispatch. This closes the long-standing gap where these messages were invisible to FastMCP middleware. ### `LowLevelServer` **Methods:** #### `fastmcp` ```python fastmcp(self) -> FastMCP ``` Get the FastMCP instance. #### `create_initialization_options` ```python create_initialization_options(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, extensions: dict[str, dict[str, Any]] | None = None) -> InitializationOptions ``` #### `get_capabilities` ```python get_capabilities(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, extensions: dict[str, dict[str, Any]] | None = None) -> mcp_types.ServerCapabilities ``` Override to advertise registered extensions and the MCP Apps UI extension. ``ServerCapabilities.extensions`` is a real declared field in v2, so we update it directly. The `FastMCP(experimental_capabilities=...)` merge also lives here rather than in `create_initialization_options`: the modern `server/discover` handler calls this directly, without going through `create_initialization_options` at all, so merging there only reached the handshake-era `initialize` response and silently dropped constructor-configured experimental capabilities from `discover`.