Skip to main content
An MCP extension is a protocol feature that lives outside the core spec, named by a reverse-DNS identifier and negotiated as a capability. A server advertises the extensions it implements, and a client advertises the ones it understands. That negotiation is per request: a client repeats its extension capabilities in every request’s _meta, so a handler can always tell whether the caller opted in to this particular call. Honoring that opt-in is the extension’s job, not the framework’s. FastMCP advertises your capability and routes your methods, but it does not filter callers for you, so an extension that changes behavior must check before it acts. The tool-call interceptor below shows the check. FastMCP 4 makes extensions a first-class surface. FastMCP.add_extension() takes an object that can advertise a capability, serve new request methods, wrap every tools/call, and own resources for the life of the server. Background tasks are built this way, on the same public interface available to you, so a cross-cutting protocol feature becomes a plugin rather than a change to FastMCP itself. Providers can also bundle the extensions their components need. Adding or mounting the provider registers those extensions automatically when they opt into that behavior, so the protocol support travels with the components that use it.

Writing an extension

Subclass ServerExtension and set an identifier. The identifier must carry a reverse-DNS prefix in vendor-prefix/name form, which FastMCP validates when the class is defined, so a malformed one fails immediately rather than at connection time. Everything else is optional: each contribution method has a working default, and a useful extension often overrides just one. Registering the extension binds it to the server and advertises its capability. The capability is advertised only while the extension is registered. Two explicit registrations with the same identifier are an error; an explicit registration can replace a provider-bundled extension.
Register extensions before the server starts. Adding one after the lifespan is running raises, because the extension’s own lifespan could no longer run and it would end up silently half-active. An extension reaches the rest of the server through self.server, which is the FastMCP instance it was registered on. That is how handlers and interceptors get at the component registry, the request Context, and the authenticated caller.

Advertising settings

Some extensions need to tell the client how they are configured: a size limit, a supported mode, a flag. Override settings() to return a JSON-serializable dict, and it appears on the wire under capabilities.extensions[identifier]. The default is an empty dict, which advertises the extension with no settings attached.
A client reads these alongside the capability itself, so it can adapt before making a single call.

Provider extensions

A provider can bundle the extensions its components need, so adding the provider also enables the protocol methods clients use to access those components. The bundles follow providers through aggregates, namespaces, transforms, and mounted servers. You can compose a provider without knowing which extensions it needs. An extension opts into this behavior with auto_register = True; the default is False. Choose this for extensions whose behavior can safely apply to the receiving server’s entire component registry. Extensions that change unrelated tools or start infrastructure, such as the tasks extension, stay explicitly registered by default. A provider bundles extension instances by returning them from required_extensions(). Each receiving server registers its own copy, and you can configure that copy with add_extension(). An explicit registration with the same identifier takes precedence whether it happens before or after adding the provider. In this example, mounting the catalog server automatically registers its extension on the parent. The parent advertises includeDetails=False, while the child keeps includeDetails=True for clients that connect to it directly.

Configuration

Extensions are identified by their protocol identifier, so several providers can share one registration. An explicit registration on the receiving server controls the extension’s configuration for all of those providers. Otherwise the first bundled instance wins. FastMCP warns once per identifier when bundles have different extension types or advertise different settings; configure the extension explicitly to choose the settings you want. Automatic registrations and explicit precedence are logged at the debug level.

Server ownership

The receiving server owns the advertised capability and request handlers. When you mount a child, the parent’s extension sees the parent’s full component registry, including the mounted components. Mounted servers contribute both provider bundles and their explicitly registered extensions that allow automatic registration. Custom composite providers should return their children’s bundles in provider order; AggregateProvider, FastMCPProvider, and transform wrappers already forward them. For propagated extensions, a tool call through the parent runs the parent’s interceptor once. Mounted children use their own interceptor for standalone requests and separate programmatic calls. Extension lifespans run at the root of the runtime tree. Extensions use clone() to create an independent, unbound instance for each receiving server. The default deep-copies instance attributes and clears the server binding. Override clone() to reconstruct configuration if your extension holds objects that cannot be copied or runtime state that should start fresh. Use a separate instance for each explicit registration as well.

Startup

Finish composing providers that need new extensions before serving. FastMCP registers bundles when you add a provider and checks again at startup, after the root’s user lifespan and before extension lifespans begin. This discovers bundles added to mounted children or aggregates during that setup. Provider lifespans run after extension lifespans. During provider lifespans and after startup, you can add providers whose extensions are already registered on the root, or providers that need no extensions. Introducing a new extension raises RuntimeError, including through nested mounts and aggregates. An aggregate shared by several running servers can accept a bundle only when every server already has its extensions. If a bundled extension has not opted into automatic registration, register it explicitly on the receiving server before adding the provider.

Adding request methods

An extension can serve request methods the core spec does not define. Return a MethodBinding from methods() naming the wire method, the Pydantic model its params validate against, and the handler to run. Extension methods are strictly additive. Binding a spec-defined method like tools/call raises at construction, because doing so would silently shadow the server’s own handler. To change how a core method behaves, use middleware or the tool-call interceptor below. The params model should subclass RequestParams so _meta parses uniformly, and the handler receives the request context and the validated params.
Setting protocol_versions on a binding restricts the method to specific wire versions, and a request at any other version is rejected as METHOD_NOT_FOUND. Leaving it unset, the default, serves the method on every version.

Intercepting tool calls

Override intercept_tool_call() to wrap every tools/call the server handles. The interceptor runs after the FastMCP middleware chain and immediately before the tool body, making it the last gate before execution. Await call_next() to let the call proceed, or return a result without awaiting it to short-circuit. Interceptors run for clients that never advertised your extension as well. FastMCP does not gate this for you, so an interceptor that changes what the caller gets back must first confirm the caller opted in. context.client_extension_settings(identifier) returns the settings the client declared for this request, or None when it declared nothing.
Counting is harmless either way, so this example passes unaware callers straight through. The check becomes essential the moment an interceptor short-circuits: returning an extension-specific result to a client that never negotiated the extension hands it a shape it has no way to understand. Request methods have the same requirement, and self.client_settings(ctx) is the equivalent inside a handler. params holds the validated tools/call params, and context is the FastMCP Context, so the tool being invoked is reachable as context.fastmcp.get_tool(params.name) along with auth scope and the server itself. When several extensions intercept, they nest with the first-registered outermost. Reach for middleware when you want to observe or modify requests generally; reach for an interceptor when the behavior belongs to a negotiated capability and should exist only while that extension is registered.

Owning resources

An extension that owns something with a lifecycle, such as a connection pool or a background worker, overrides lifespan() to return an async context manager. FastMCP enters it with the server’s own lifespan and exits it on shutdown, so setup and teardown stay with the extension that needs them rather than leaking into the application’s startup code. The lifespan is entered once per runtime tree, at the root. The root server owns the wire, so its registered extensions advertise capabilities and answer methods. Auto-registerable extensions propagate through mounts as separate instances on the root; register other extensions on the server you actually run.

Client extensions

The client half of an extension is what makes negotiation two-sided. Pass ClientExtension instances to Client(extensions=...) and each contributes its capability advertisement, its result claims, and its notification bindings to the underlying session. A claimed call_tool result is then resolved transparently through the extension that owns it. When a client needs only to say it understands an extension, without implementing behavior for it, advertise() produces an advertise-only entry.
Advertise only what you genuinely support: the advertisement asserts wire compatibility, and claiming an extension you have not implemented invites the server to use a feature you cannot answer. For anything behavioral, construct the real extension instead. Claimed result shapes are a modern-protocol feature and stay inert on a legacy connection, so an extension-aware client is still safe to point at an older server.