How MCP extensions work Under the 2026-07-28 Model Context Protocol specification, MCP extensions such as interactive UIs, long-running tasks, skills and enterprise authorization are optional additions that are always disabled by default and declared through an `extensions` map keyed by identifiers of the form `{vendor-prefix}/{extension-name}`, according to a technical explainer published in October 2026. Official extensions use the `io.modelcontextprotocol` prefix, including MCP Apps (`io.modelcontextprotocol/ui`, stable since specification 2026-01-26), Tasks (SEP-2663 final), Skills over MCP (SEP-2640 final, implementations rolling out) and Enterprise-Managed Authorization (stable), while OAuth Client Credentials remains a draft. The explainer states that when one side lacks an extension the other falls back to core behaviour, or, if the extension is mandatory, the server rejects the request with error `-32021`, and advises building on official extensions while keeping experimental ones behind feature flags. How MCP extensions work 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 https://pournasserian.com/writing/mcp-2026-migration-two-eras . It opens the arc on extensions, and the facts are as I read them in October 2026. In brief 1. Extensions are optional additions to MCP, always disabled by default, for functionality that is modular, specialized or experimental. 2. Each one has an identifier of the form {vendor-prefix}/{extension-name} : io.modelcontextprotocol for the official ones, a reversed domain you own for yours. 3. Clients declare extensions on every request, and servers in server/discover , both as a map from identifier to a settings object. 4. 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 -32021 from a server whose client didn’t declare it . 5. My advice: building on official extensions is a reasonable bet; keep experimental ones behind feature flags. The extensions that exist The extensions overview https://modelcontextprotocol.io/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 https://pournasserian.com/writing/mcp-2026-what-changed , 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 https://modelcontextprotocol.io/seps/2663-tasks-extension final | | Skills over MCP | io.modelcontextprotocol/skills | SEP-2640 https://modelcontextprotocol.io/seps/2640-skills-extension 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 https://github.com/modelcontextprotocol/modelcontextprotocol/pull/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 extensions per request , in meta "io.modelcontextprotocol/clientCapabilities" .extensions . As part 5 https://pournasserian.com/writing/mcp-2026-stateless-requests explains, two requests from the same client can declare different ones. - Servers declare them in the server/discover result, under capabilities.extensions part 3 https://pournasserian.com/writing/mcp-2026-server-discover . 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 https://py.sdk.modelcontextprotocol.io/advanced/extensions/ . 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: python 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: python 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: php {'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 https://pournasserian.com/writing/mcp-2026-migration-two-eras 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 mcp 2.3.0 on 9 October 2026 with the in-memory client; the output is what it printed. The note on the older initialize handshake 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.