skip to content

How do MCP peers declare extension support in the extensions capability map?

level: middleimportance: must knowfreq 58%

answer

  1. both sides have to say yes
  2. a map, not a list
  3. key is the identifier, value is settings
  4. an empty object still means yes

basics

~20 s

Each side lists supported extensions in the extensions field of its capabilities: a map from a prefixed identifier to a settings object. An empty object means supported with no settings, not unsupported. An extension is live only if both sides list it.

solid answer

~40 s

In MCP revision 2026-07-28, `extensions` is a field on both `ClientCapabilities` and `ServerCapabilities`. It is a map, not a list: the key is the prefixed extension identifier, such as `io.modelcontextprotocol/tasks`, and the value is that extension's settings object. An **empty object is a positive declaration** — it means "supported, with nothing to configure" — which is the shape most declarations take. Opt-in is mutual and explicit: an extension is only in play when it appears in **both** peers' maps, so a client declaring it against a server that does not is simply a client with an unused capability. Because MCP is stateless in 2026-07-28, capabilities accompany the request rather than being established once, so the opt-in is re-asserted on each request and a server must not carry an earlier one forward.

code

json · 9 lines
json
{
  "elicitation": {},
  "extensions": {
    "io.modelcontextprotocol/tasks": {},
    "com.example/replay": {
      "maxAttempts": 3
    }
  }
}

go deeper

for a junior

Recall that support is declared in an extensions map keyed by the extension identifier, and that an empty object under a key still means the extension is supported.

for a middle

Explain the map's key/value shape and state the mutual opt-in rule plainly: an extension is only active when it appears in both peers' maps, and absence of a key is what means non-support.

for a senior

Demonstrate that you read the opt-in off the request in front of you rather than caching it, and that your code tolerates extension identifiers it has never seen without erroring.

for a principal

Be ready to discuss what belongs in an extension's settings object versus what should be a separate extension, and how you keep a fleet's support matrix observable once several extensions are in play.

## The shape The declaration lives in one field, `extensions`, present on both `ClientCapabilities` and `ServerCapabilities` in MCP revision 2026-07-28. Its type is a map: - **key** — the extension identifier, namespace-prefixed in the same style MCP uses for prefixed `_meta` keys: dot-separated reverse-DNS labels, a slash, then a name (`io.modelcontextprotocol/tasks`, `io.modelcontextprotocol/ui`, or a vendor's own `com.example/replay`). - **value** — a settings object belonging to that extension. Its contents are defined by the extension, not by the core spec. Being a map rather than a list is the design point: it gives every extension a slot for configuration without the core protocol having to know what any of it means. ## The empty object is not nothing The single most common misreading is treating `{}` as "no support" or "unset". It is the opposite. `"io.modelcontextprotocol/tasks": {}` means **I support Tasks and have no settings to pass**. Absence of the key is what means no support. Implementations that fall for this either fail to advertise a feature they actually implement, or refuse a peer that legitimately advertised one. ## Opt-in is mutual and explicit An extension exists for a client-server pair only when it appears in **both** maps. That has some blunt consequences: - A client listing an extension the server has never heard of has not committed anyone to anything. The server treats the unknown key as inactive, not as an error. - A server that implements an extension does not get to use it against a client that did not list it. - There is no "default on" tier. Nothing is implied by silence except non-support. This symmetry is why the mechanism is called opt-in negotiation rather than advertisement: the intersection of the two maps is the working feature set. ## Where the maps travel A client's capabilities, including its extensions map, accompany each request rather than being registered once — revision 2026-07-28 made MCP stateless, and its own words are that every request is self-contained and carries its own protocol version and capabilities. A server's capabilities, including its extensions map, come back from `server/discover`. The carrier mechanics belong to per-request metadata and discovery respectively; what matters for negotiation is the consequence: **the opt-in is a property of the request in front of you, not of the connection.** A server that remembers "this client supports Tasks" from an earlier call and applies it to a later one that did not declare it has broken the statelessness rule. Conversely, a client must not assume a server's extension support survives a `ttlMs` expiry of its discovery result without re-checking. ## Practical implementation notes - **Read the map, do not scan for a boolean.** Extension support is key presence, and settings live under that key. Code that checks `capabilities.extensions != null` and stops has learned nothing. - **Namespace your own.** If you define an extension, put it under a prefix you control. `io.modelcontextprotocol` is the specification's own namespace. - **Emit an empty object rather than omitting the key** when you support an extension with default settings. Omission is a claim of non-support. - **Unknown keys are inert, not fatal.** A peer must be able to receive an extensions map containing identifiers it has never seen and carry on with core behaviour. - **Do not conflate with versioning.** The protocol revision (`YYYY-MM-DD`) says which spec you speak; the extensions map says which optional features you have on top of it. A peer can be perfectly current on revision and declare an empty extensions map. ## What good answers sound like The answer an interviewer is listening for names the field, gets the map-of-identifier-to-settings shape right, states that `{}` is a yes, and closes with the mutual-opt-in rule. If you also point out that statelessness makes the declaration per-request rather than per-connection, you have shown you understand the 2026-07-28 model rather than reciting the handshake era it replaced.

  • What does MCP's statelessness in 2026-07-28 change about an extension opt-in?
    It makes the opt-in per request rather than per connection. Capabilities accompany the request, so a server evaluates the extensions map it was just given and must not carry an earlier request's declaration forward. Practically, an implementation behind a load balancer needs no shared memory of who opted into what — whatever node handles a request has everything it needs in that request.
  • If a client's extensions map lists an identifier the server has never heard of, what should the server do?
    Nothing special — treat it as inactive and proceed with core behaviour. Unknown extension identifiers are not a protocol error; the mutual opt-in simply fails to complete, so the extension is not in play. Rejecting the request because an unrecognised key was present would break forward compatibility with clients that support newer extensions.
  • Can a peer declare an extension and then not use it?
    Yes. The declaration advertises capability; it does not commit either side to exercising it. Most requests over a connection where both sides support an extension will not involve that extension at all.

saying these in an interview costs you the question

  • Reads an empty settings object as 'not supported'
  • Declares an extension on one side only and expects it to engage
  • Uses an unprefixed identifier like 'tasks' as the map key
  • Treats an unknown extension identifier as a protocol error
  • Believes one declaration holds for the life of the connection

context