The short version: The 2026-07-28 Model Context Protocol spec froze a small core and pushed evolution to a negotiated edge. If you want your MCP server to do something the core doesn't — an audit log, a domain-specific approval flow, a proprietary capability — the sanctioned way is now an extension, not a forked core. You give it a reverse-DNS ID you own, advertise it in the extensions capability map, and degrade gracefully on peers that don't have it. This is the whole build. Here's each step, and the one habit that separates an extension that survives from one that breaks strangers' clients.
Why an extension, not a core patch#
Before 2026-07-28, "add a feature" meant growing the core, and "does this feature exist" was a question about the protocol version both sides spoke. The final spec ends that. The core is now deliberately small — tools, resources, prompts, and a stateless request model — and even MCP's own first-class capabilities live outside it: Tasks is now the io.modelcontextprotocol/tasks extension, and MCP Apps is an extension too. If the protocol's own maintainers put their new work in extensions, so should you. Forking the core to add a capability isn't just discouraged — it's the thing the framework exists to make unnecessary.
1. Name it with a reverse-DNS ID you control#
An extension is identified by a reverse-DNS ID, the same trick Java packages and Android intents use: a name is globally unique because it's rooted in a domain you own, with no central registry handing out slots. Use a domain you actually control so it can never collide with someone else's:
com.yourco/audit-log
That string is your extension's identity everywhere — in the capability map, in your docs, in any future SEP. Pick it once and treat it as stable.
2. Advertise it in the extensions capability map#
Capability in MCP is now a property of the handshake, not the spec version. Both peers list the extensions they support in an extensions map inside the capabilities they exchange, and the active set for the connection is the overlap. Your server advertises yours:
// server capabilities (shape is illustrative — see the 2026-07-28 spec
// for the authoritative schema)
{
"capabilities": {
"tools": {},
"resources": {},
"extensions": {
"com.yourco/audit-log": { "version": "1.0" }
}
}
}
Because the 2026-07-28 transport is stateless — no initialize handshake, no Mcp-Session-Id header — the protocol version, client identity, and client capabilities now ride in _meta on each request. So don't cache an assumption about what a client supports; read what the peer actually brought.
3. Negotiate: check before you use#
The rule that changes how you build: negotiate capability, don't assume it. Before your server does anything extension-specific, confirm the peer advertised your extension for this connection.
function clientHasAuditLog(peerCapabilities) {
const ext = peerCapabilities?.extensions || {};
return Boolean(ext["com.yourco/audit-log"]);
}
// in your tool handler:
if (clientHasAuditLog(peer)) {
await emitAuditRecord(call); // the extension path
} else {
// core-only path — see step 4
}
4. Degrade gracefully — this is the part people skip#
Because opt-in is per connection, you will always meet clients that don't have your extension. A server that hard-depends on one is out of step with the framework and will break for strangers. So every extension needs a defined answer to "what happens when the peer doesn't support it":
- Prefer a reduced core-only result. If your audit-log extension can't fire, still complete the underlying tool call — just without the extra record.
- If the capability is genuinely required, fail loudly in the core. Return a normal tool error the client already knows how to handle ("this operation requires the
com.yourco/audit-logextension"), not a protocol-level surprise. - Never assume symmetry. A client supporting your extension doesn't mean it implements every method you added; check per feature if they're independent.
The 2026-07-28 spec turned "supports MCP" into a less complete sentence. Build for the handshake, not the version number — and always ship the fallback.
5. Version it independently, and take the official path if you want it#
An extension lives in its own repository (ext-*, mirroring the official convention) with its own maintainers, and it versions independently of the spec — yours can reach v3 while the base spec is still on one date-stamped revision, and neither drags the other. Put the version in your capability entry ("version": "1.0" above) so peers can range-check it.
If your extension is generally useful and you want it to become official, there's a defined route: the Extensions Track in the SEP (Specification Enhancement Proposal) process, which promotes an extension from experimental to official without it ever entering the frozen core. Either way, extensions inherit MCP's 12-month deprecation guarantee — a stable contract you can build a product on, provided you version deliberately.
The whole loop#
Name it (reverse-DNS), advertise it (extensions map), negotiate it (check the peer, read _meta), degrade when it's absent, and version it in its own repo. Do those five things and you've added a proprietary capability to your MCP server that costs the ecosystem nothing: old clients keep working, new clients light up the extra behavior, and you never touched the core. That's the trade the framework was designed to make cheap — take it.



