How do MCP peers declare extension support in the extensions capability map?
answer
- both sides have to say yes
- a map, not a list
- key is the identifier, value is settings
- an empty object still means yes
basics
~20 sEach 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 sIn 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{
"elicitation": {},
"extensions": {
"io.modelcontextprotocol/tasks": {},
"com.example/replay": {
"maxAttempts": 3
}
}
}go deeper
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.
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.
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.
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