# How MCP extensions work

October 24, 2026 · Part 23 of 35

![How MCP extensions work](https://pournasserian.com/_astro/lead.CF-AoP2I_Z1zpNpL.webp)

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:

```mermaid
flowchart LR
    root["MCP extensions"]
    root --> ui["UI"]
    root --> exe["Execution"]
    root --> kn["Knowledge"]
    root --> au["Authorization"]
    root --> xp["Experimental"]
    root --> vd["Vendor-specific"]

    ui --> apps["MCP Apps<br/>io.modelcontextprotocol/ui<br/>stable"]
    exe --> tasks["Tasks<br/>io.modelcontextprotocol/tasks<br/>official"]
    kn --> skills["Skills over MCP<br/>io.modelcontextprotocol/skills<br/>SEP final"]
    au --> ema["Enterprise-Managed Authorization<br/>stable"]
    au --> cc["OAuth Client Credentials<br/>draft"]
    xp --> ic["Interceptors<br/>experimental-ext"]
    xp --> va["Variants<br/>experimental-ext"]
    vd --> an["Client _meta conventions<br/>e.g. anthropic/alwaysLoad"]
```

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:

```json
"_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:

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: server/discover
    S-->>C: capabilities.extensions<br/>(ui, tasks)
    C->>S: tools/call<br/>+ clientCapabilities.extensions (ui)
    Note over S: Client declared ui<br/>but not tasks<br/>on this request
    S-->>C: CallToolResult (no task handle),<br/>tool has _meta.ui for the view
    C->>S: tools/call<br/>+ clientCapabilities.extensions (none)
    alt Extension optional for this operation
        S-->>C: plain core result<br/>(text content)
    else Extension mandatory
        S-->>C: error -32021<br/>MissingRequiredClientCapability<br/>(requiredCapabilities)
    end
```

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:

```text
{'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.
