Several things a Model Context Protocol (MCP) server can do in 2026 aren’t part of the core protocol at all: interactive user interfaces (UIs), long-running tasks, skills, enterprise authorization. They’re extensions. What surprised me is how little machinery they share: an identifier, a settings object, and a rule for what happens when the other side doesn’t support one.
This is part 23 of my series on what an MCP server does under the 2026-07-28 specification, after part 22 on serving two protocol eras. It opens the arc on extensions, and the facts are as I read them in October 2026.
In brief #
- Extensions are optional additions to MCP, always disabled by default, for functionality that is modular, specialized or experimental.
- Each one has an identifier of the form
{vendor-prefix}/{extension-name}:io.modelcontextprotocolfor the official ones, a reversed domain you own for yours. - Clients declare extensions on every request, and servers in
server/discover, both as a map from identifier to a settings object. - If one side lacks an extension, the other falls back to core behaviour, or, when the extension is mandatory, rejects the request with an error (
-32021from a server whose client didn’t declare it). - My advice: building on official extensions is a reasonable bet; keep experimental ones behind feature flags.
The extensions that exist #
The extensions overview describes extensions as optional additions beyond the core protocol: modular (such as an authentication scheme), specialized (industry-specific logic) or experimental (ideas incubating for possible inclusion in the core). The 2026-07-28 revision added an extensions field to client and server capabilities (part 2), which made them a first-class part of negotiation. Here is the landscape:
The same extensions with their identifiers, where SEP is a Specification Enhancement Proposal:
| Extension | Identifier | Status, October 2026 |
|---|---|---|
| MCP Apps | io.modelcontextprotocol/ui |
Stable (specification 2026-01-26) |
| Tasks | io.modelcontextprotocol/tasks |
Official ( SEP-2663 final) |
| Skills over MCP | io.modelcontextprotocol/skills |
SEP-2640 final, implementations rolling out |
| Enterprise-Managed Authorization | io.modelcontextprotocol/enterprise-managed-authorization |
Stable |
| OAuth Client Credentials | io.modelcontextprotocol/oauth-client-credentials |
Draft |
| Interceptors, Variants | experimental-ext-* repositories |
Experimental (Interceptors: SEP-2624 , an open proposal) |
Vendors also add conventions of their own in _meta, such as anthropic/alwaysLoad. The rest of this arc covers these extensions and conventions in turn.
Identifiers and negotiation #
Identifiers
An extension identifier has the form {vendor-prefix}/{extension-name} and follows the same rules as _meta keys, with a mandatory prefix. Official extensions use io.modelcontextprotocol. If you build your own, use a reversed domain you own, such as com.example/my-extension, to avoid collisions with anyone else’s.
Official extensions live in the MCP GitHub organization, in repositories whose names start with ext- (ext-apps, ext-auth, ext-skills). Experimental ones start with experimental-ext-.
Who declares what
Extensions are always disabled by default and need an explicit opt-in. Software development kits (SDKs) choose which extensions they support, and that choice doesn’t affect protocol conformance. Both sides advertise support in an extensions map. Its keys are identifiers, and its values are settings objects whose shape each extension defines; an empty object means “supported, no settings”.
- Clients declare extensionsper request , in
_meta["io.modelcontextprotocol/clientCapabilities"].extensions. Aspart 5 explains, two requests from the same client can declare different ones. - Servers declare them in the
server/discoverresult, undercapabilities.extensions(part 3 ).
Here a request declares MCP Apps, with the UI type it can render, and Tasks, with no settings:
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] },
"io.modelcontextprotocol/tasks": {}
}
}
}
The server answers each request from what that request declared:
The first call declares the UI extension but not Tasks, so it gets no task handle. The second declares nothing, so it gets a plain core result, or -32021 with the missing extension in data.requiredCapabilities if the operation can’t go without it. In general, the side that supports an extension falls back to core behaviour or, if the extension is mandatory, rejects the request with an appropriate error. Document your fallback.
Declaring and gating an extension in Python
The official Python SDK, mcp 2.3.0, has an extension application programming interface (API). You subclass Extension, give it an identifier and pass it to MCPServer(extensions=[...]). This server advertises a made-up com.example/approvals extension. One tool requires it, and one degrades without it:
from mcp.server import MCPServer
from mcp.server.mcpserver import Context, Extension, require_client_extension
APPROVALS = "com.example/approvals"
class Approvals(Extension):
identifier = APPROVALS # settings() returns {} by default: supported, no settings
mcp = MCPServer("acme-deploy", extensions=[Approvals()])
@mcp.tool()
def request_approval(change: str, ctx: Context) -> str:
"""Send a change for review through the client's approvals UI."""
require_client_extension(ctx.request_context, APPROVALS) # -32021 if undeclared
return f"Sent for review in the client: {change}"
@mcp.tool()
def describe_change(change: str, ctx: Context) -> str:
"""Describe a change, and say how to get it approved."""
caps = ctx.client_capabilities
if caps and caps.extensions and APPROVALS in caps.extensions:
return f"{change}. Call request_approval to send it for review."
return f"{change}. Ask a reviewer to approve it outside this chat."
require_client_extension raises the -32021 error for you. If the identifier isn’t of the form vendor-prefix/name, the SDK raises a TypeError as soon as the class is defined. An extension can also contribute tools, resources, new request methods and a wrapper around tools/call; this one only advertises itself.
Two in-memory clients try it. The first advertises the extension with advertise(), which puts it into every request’s _meta, and calls describe_change; the second declares none and calls both tools:
import anyio
from mcp import Client, MCPError
from mcp.client import advertise
async def main():
change = "Raise the API rate limit"
async with Client(mcp, extensions=[advertise(APPROVALS)]) as client:
print(client.server_capabilities.extensions)
result = await client.call_tool("describe_change", {"change": change})
print("describe_change ->", result.content[0].text)
async with Client(mcp) as client: # declares no extensions
result = await client.call_tool("describe_change", {"change": change})
print("describe_change ->", result.content[0].text)
try:
await client.call_tool("request_approval", {"change": change})
except MCPError as err:
print(err.code, err.data)
if __name__ == "__main__":
anyio.run(main)
It prints:
{'com.example/approvals': {}}
describe_change -> Raise the API rate limit. Call request_approval to send it for review.
describe_change -> Raise the API rate limit. Ask a reviewer to approve it outside this chat.
-32021 {'requiredCapabilities': {'extensions': {'com.example/approvals': {}}}}
The first line is what server/discover advertised. One more reason to keep the fallback: mcp 2.3.0 advertises a server’s extensions only in server/discover, so a client on the older initialize handshake (2025-11-25 and earlier, see part 22) never sees them.
Lifecycle and governance #
From idea to official extension
An extension usually starts as an idea in a Working Group or Interest Group and is tried out in an experimental-ext-* repository. It then becomes a SEP with a reference implementation, passes Core Maintainer review and is published in an ext-* repository, where clients, servers and SDKs adopt it. Some may later be promoted into the core protocol. The rules along the way:
- Every experimental extension must belong to a Working Group or Interest Group and be clearly labelled experimental. Core Maintainers can archive or remove it.
- An official extension needs an Extensions Track SEP written in the MUST, SHOULD and MAY language of Request for Comments (RFC) 2119, plus at least one reference implementation in an official SDK.
- Extensions evolve independently of the core. Prefer capability flags or settings over new identifiers. A truly breaking change (removed or renamed fields, changed types or semantics, new required fields) gets a new identifier, such as
.../my-extension-v2.
Which ones to build on
Tasks shows where some extensions end up. It began as an experimental core feature, was redesigned as an extension so it could incubate and evolve on real-world feedback, and is meant to move back into the core once it’s stable. So my advice is this: building on official extensions now is a reasonable bet. Building on an experimental one is a bet on a working group, so keep it behind a feature flag.
Method and caveats #
- Built from the chapter on extensions in my guide, and written against the 2026-07-28 specification and the extensions overview. The statuses in the table are as of October 2026.
- The Python sample ran against
mcp2.3.0 on 9 October 2026 with the in-memory client; the output is what it printed. The note on the olderinitializehandshake comes from a short probe of the same package. - Where the proposal’s own record is newer than my notes (which SEP specifies Interceptors), this article follows the SEP-2624 pull request, as checked on 8 October 2026.