Extension mechanisms

The four extension surfaces and how mcpkit organizes core / ext / experimental.

Extension mechanisms

How MCP grows. What counts as an extension, how new things get into the protocol, how mcpkit organizes them, and how you write your own without forking core. Six questions.

Kind: root (FAQ-style) · Prerequisites: bring-up, transport-mechanics, notifications, request-anatomy
Reachable from: README, bring-up Leads-to, notifications Leads-to
Branches into: tasks (stub), auth deep-dive (stub), apps (stub), experimental events
Spec: Lifecycle / capabilities · SEP-2663 (tasks v2) · SEP-2549 (list-TTL) · SEP-2322 (MRTR) · Code: core/protocol.go, core/typed_tool.go, server/registration.go, server/middleware.go, server/dispatch.go, server/mrtr.go, ext/auth/, ext/ui/, experimental/ext/

Prerequisites

  • You understand what a capability is and that capabilities are negotiated at bring-up. → If not, read bring-up.
  • You can read JSON-RPC messages and know that methods are named strings. → If not, read transport mechanics.
  • You know how notifications are gated by capability. → If not, read notifications.
  • You know what handler context, registries, and middleware are (referenced in Q4). → If not, read per-request anatomy.

Context

MCP grows by extension. Tasks, auth, apps, events, list-TTL, MRTR — everything beyond the core protocol — works through a small set of shared mechanisms (capability flags, method namespaces, _meta, notifications), the capabilities.extensions map that advertises them, and mcpkit’s code-level extension points (registries, middleware, sub-modules). Naming the mechanisms once gives you the vocabulary to read “this extension uses SEP-X” or “this is a _meta-only extension” and immediately know what’s going on.

Q1 — What counts as an “extension” in MCP?

The protocol has four extension surfaces. Any extension uses one or more of them.

Surface What it lets you add Gated by Example
Method namespace new JSON-RPC methods, typically under a prefix (tasks/*, elicitation/*) a capability declared at bring-up tasks/create (SEP-2663), elicitation/create
Capability flags a core capability slot (fixed, spec-defined set) or an entry in the open-ended capabilities.extensions map core slots: the core spec. extensions map: keyed by extension ID, carries {specVersion, stability} — see Q2 core slot: tools.listChanged. extension: capabilities.extensions["io.modelcontextprotocol/tasks"]
Notification methods new fire-and-forget messages, capability-gated usually a <area>.listChanged-style flag declared by the emitter notifications/resources/updated
_meta fields opt-in metadata on existing requests/responses; doesn’t change the method’s semantics per-call opt-in by the requester _meta.progressToken (progress), _meta cache-TTL hint (SEP-2549)

Most extensions combine multiple surfaces. Useful styles to recognize:

Style Combines Examples
Method-namespace extension new prefix + new capability + (often) new notifications tasks (SEP-2663), elicitation, sampling
Capability-only extension new capability flag + reuses an existing notification method logging (logging cap + notifications/message)
_meta-only extension one or more _meta fields, no new methods or capabilities list-TTL (SEP-2549)
Bring-up extension extends the connection-establishment phase, not the message exchange auth — WWW-Authenticate / OAuth dance lives at the HTTP layer, before initialize
Library-architecture extension mostly around the protocol; thin protocol surface apps (ext/ui/) — host-side AppHost / Bridge / Registry; the protocol part is small

Worth internalizing: the four surfaces are knobs, not categories. A new feature picks which knobs to turn. Tasks turns three (methods + capability + notifications); list-TTL turns one (_meta); auth turns none of the four because it extends the layer below MCP.

Q2 — How does a new capability get declared and negotiated?

Two shapes of capability, one governance process.

Core capability slots vs. the extensions map

The capabilities object exchanged at bring-up has two kinds of entry:

  1. Core capability slots — a fixed, spec-defined set of keys. Server-side: tools, resources, prompts, logging, completions. Client-side: sampling, roots, elicitation, fileInputs. The core MCP spec owns this list; adding a slot means changing the core spec. Each slot is a typed struct (tools.listChanged, resources.subscribe, …).

  2. The extensions mapcapabilities.extensions, a map keyed by extension ID. This is the open-ended slot: a new extension lands here without touching the core capability schema. Extension IDs are reverse-DNS strings — io.modelcontextprotocol/tasks, io.modelcontextprotocol/ui, io.mcpkit/auth.

The map’s values differ by side:

  • Server → client: ExtensionCapability { specVersion, stability } — “I implement extension X at spec version Y, maturity Z.”
  • Client → server: an extension-specific shape (ClientExtensionCap). MCP Apps, for instance, has the client declare the mimeTypes it can render. What’s inside is the extension’s business.

stability is the maturity marker — in-band, per-extension

stability is an enum (core.Stability): experimental, stable, deprecated. It travels inside each extension’s map entry. An extension graduating from experimental to stable flips its stability value — the extension ID never changes. There is no experimental.-prefixed capability name; maturity is a field, not a namespace.

Who populates the map (server side)

Step What Code
Declare an extension is a core.Extension { ID, SpecVersion, Stability, Config }, surfaced via the core.ExtensionProvider interface (Extension() Extension) core/auth.goExtension, ExtensionProvider, Stability live here
Register WithExtension(provider) at construction, or Server.RegisterExtension(provider) at runtime server/server.go
Advertise registered extensions are serialized into capabilities.extensions on the initialize response the dispatcher’s extensions map

mcpkit extensions self-register: RegisterTasks internally calls RegisterExtension(tasksExtensionProvider{}), so wiring up the feature advertises the extension automatically — the caller doesn’t thread a separate option.

How it’s negotiated

Both sides advertise their extensions in initialize. A server handler then gates extension-specific behaviour on what the client declared:

if core.ClientSupportsExtension(ctx, core.TasksExtensionID) {
    // client negotiated the tasks extension — safe to use the tasks/* surface
}

bc.ClientSupportsExtension(id) is the handler-context form. Each extension usually ships a typed helper — ClientSupportsTasks(ctx) wraps ClientSupportsExtension(ctx, TasksExtensionID). Receivers ignore extension IDs they don’t recognize — that’s the forward-compatibility guarantee that lets a new extension ship without breaking old peers.

The SEP governance process

SEPs (Standard Enhancement Proposals) are MCP’s RFC process. A SEP specifies the methods / capabilities / notifications / _meta an extension adds. It gets prototyped, conformance-tested (mcpkit runs per-SEP suites — just testconf-tasks-v2 for SEP-2663, testconf-mrtr for SEP-2322, …), and iterated. The extension ID and its capabilities.extensions entry exist from day one; what changes as the SEP matures is the stability value and specVersion.

graph LR
    sep["SEP filed<br/>(design proposal)"] --> reg["extension registered<br/>stability: experimental"]
    reg --> conf["conformance + iteration"]
    conf --> stable["stability: stable"]
    conf -. design abandoned .-> dep["stability: deprecated → removed"]

Worked example: tasks moved from core to extension

mcpkit’s tasks v1 was a core capabilityServerCapabilities.Tasks, a fixed slot sitting right next to tools / resources / prompts. It was added speculatively, before the long-running-operations design had settled.

SEP-2663 (tasks v2) moves tasks into the extensions map. It’s now extension ID io.modelcontextprotocol/tasks, advertised via capabilities.extensions. SEP-2663 merged Final on 2026-05-15.

Why move a feature out of core:

  • Opt-in vs. forced. A core slot makes every implementation reason about tasks. An extension is opt-in — a server that doesn’t do long-running work carries zero task surface, and a client that doesn’t understand io.modelcontextprotocol/tasks simply ignores the map entry.
  • Versionable. A core slot is binary — present or absent. An extension entry carries specVersion + stability, so tasks can iterate its design and signal maturity without a core protocol-version bump.
  • Keeps core small. The core capability set stays short and stable; speculative features live in the extensions map until proven.

mcpkit keeps both surfaces alive during the transition via two entry points: server.RegisterTasksV1 (frozen — advertises the legacy core capabilities.tasks slot) and tasks.Register in the github.com/panyam/mcpkit/ext/tasks sub-module (v2 — advertises the io.modelcontextprotocol/tasks extension). Servers needing both surfaces during a rolling upgrade install them independently. The deep v1↔v2 wire-shape and registration detail lives in tasks.

Q3 — What does an extension look like in mcpkit’s code organization?

mcpkit’s package layout maps directly to the extension lifecycle:

core/                         ← protocol types; everything depends on this
server/                       ← server impl (dispatch, middleware, transports, tasks)
client/                       ← client impl (transports, reconnect, auth retry)

ext/auth/                     ← stable extension, separate go.mod
ext/ui/                       ← stable extension (Apps), separate go.mod

experimental/ext/protogen/    ← in-flux: proto → MCP codegen
experimental/ext/events/      ← in-flux: events as first-class

Three tiers, by stability:

Location Stability go.mod Promotion path
core/ stable, foundational, breaks rarely shared with root (would only move if relocating something)
ext/<name>/ stable enough for production separate could fold into core if a hard dependency, otherwise stays here
experimental/ext/<name>/ in flux, may break, may not become standard separate promote to ext/ when the API stabilizes; drop if abandoned

Why separate go.mod for ext / experimental. Three reasons:

  1. No forced dependencies — a consumer using only the core MCP server doesn’t pull in OAuth machinery, Apps Bridge JS dependencies, or experimental codegen. Smaller binary, fewer CVEs to track.
  2. Independent versioningext/auth can iterate without forcing a root version bump.
  3. Smaller blast radius — a breaking change in experimental/ext/events doesn’t touch consumers of core/ or server/.

[!NOTE]
The cost is module discipline. Adding a new core/ import that an extension needs requires just tidy-all to update sub-module go.sum files. CLAUDE.md flags this as a recurring gotcha.

Q4 — How do you write a server-side feature without forking core?

mcpkit’s extension points, roughly in order of how often you’ll reach for them:

Extension point What you plug in Where
Tool / prompt / resource registries a function or struct that handles one named item RegisterTool, RegisterPrompt, RegisterResource in server/registration.go
Typed binding Go structs with tags; mcpkit handles JSON↔Go marshal + schema validation core/typed_tool.go
Middleware wrap any method or all methods on either receive-side or send-side server/middleware.go, client/middleware.go
Custom transports implement the transport interface server/transport.go (interfaces); existing examples in stdio_transport.go, streamable_transport.go, memory_transport.go
Extension registration declare a protocol extension (ID, specVersion, stability) so it’s advertised in capabilities.extensions at initialize WithExtension / Server.RegisterExtension in server/server.go; core.Extension / ExtensionProvider in core/auth.go
Method handlers bypass typed binding for raw JSON-RPC handling server/method_handler.go
Tasks (RegisterTasks / hybrid) register long-running operation handlers server/tasks_v2.go, server/tasks_hybrid.go

The general shape: you add registrations at server start (or client side), the runtime picks them up via dispatch, and middleware wraps the call path. You never need to modify core/ or server/ to add functionality.

[!NOTE]
The dispatch + middleware + handler-context internals that make these extension points work are covered in per-request anatomy — read that first.

Q5 — Case studies: how tasks, auth, apps, events, list-TTL, MRTR, elicitation each map to the mechanisms above

Brief — each gets its own full page later.

Extension Style Surfaces used Where
Tasks (SEP-2663) method-namespace tasks/* methods + the io.modelcontextprotocol/tasks entry in capabilities.extensions (v1 used a core tasks slot — see Q2 worked example) + reuses progress notifications + own task store server/tasks_v2.go, server/task_store.go · v1/v2/hybrid coexistence is mcpkit-specific
Auth bring-up extension none of the four MCP surfaces — extends the HTTP layer below: WWW-Authenticate + OAuth + bearer token per request core/auth.go, core/www_authenticate.go, ext/auth/ (separate go.mod)
Apps library-architecture thin protocol surface; bulk is host-architecture (AppHost lifecycle, Bridge JS runtime, ServerRegistry tracking live servers) ext/ui/ (separate go.mod) · docs in docs/APPS_DESIGN.md, docs/APPS_HOST.md, docs/APPS_ONBOARDING.md
Events method-namespace, target-shape registered as an extension with stability: experimental; explores events as first-class beyond raw SSE event-id replay experimental/ext/events/
List-TTL (SEP-2549) _meta-only a *int field with explicit-zero semantics on list responses; no new methods or capabilities hooked into existing list responses; conformance originally via a dedicated testconf-list-ttl suite, now folded into just testconf
MRTR (SEP-2322) method-namespace + _meta extends tools/call — server can return InputRequiredResult with inputRequests + signed requestState; client retries with inputResponses + echoed token; stateless across rounds. Default InputHandler bridges to sampling/elicitation/roots server/mrtr.go, client/mrtr.go
Elicitation method-namespace elicitation/create method + elicitation capability declared by client; Form mode + URL mode core/elicitation.go · server-originates, client receives

Reading this table is the fastest way to see the shape of MCP’s extension landscape. Each extension turns one or more of the four knobs from Q1.

Q6 — Where’s the boundary between protocol extension and host/client policy?

Easy to conflate. Things that are not extensions, even though they look like new behavior:

  • Multi-server orchestration — a host config has Jira + Slack + Glean; the host decides which session a tool call goes to. The protocol is point-to-point (one client ↔ one server); routing across servers is host-side.
  • Tool selection by the model — the model picks jira_search from the tool list it was given. The pick happens above MCP entirely.
  • Caching policies — the client may cache tools/prompts/resources catalogs; the server may cache its own state. The protocol gives hints (list_changed, list-TTL) but doesn’t dictate cache shape.
  • UI surface — how the host shows progress to the user, how it lays out elicitation forms, how it displays log notifications. All policy.
  • Auth UXwho the user is and how the OAuth dance is presented to them is host policy. Auth itself (WWW-Authenticate / token issuance / refresh) is protocol-relevant — that’s the bring-up extension. The line is “what’s on the wire vs. what the user sees.”
  • Server discovery and lifecycle — eager-vs-lazy connect, when to terminate a session, host config files. All policy.
  • Logging back-end — server emits notifications/message; what the host does with the messages (display, ship to a logging service, drop) is policy.

The rule of thumb: if it can be observed on the wire between client and server, it’s protocol; if it only manifests in the host’s behavior, it’s policy. Extensions live on the protocol side. Many seemingly-new features turn out to be policy in disguise — useful to spot early so you don’t try to spec them.

End-state (what downstream pages can assume)

After reading this page, downstream pages can assume:

  • You know the four extension surfaces (method namespace, capability flags, notifications, _meta) and the common styles that combine them.
  • You know the capabilities.extensions map — keyed by reverse-DNS extension ID, server-side values carry {specVersion, stability} — and that WithExtension / RegisterExtension populate it. You know stability (experimental / stable / deprecated) is an in-band per-extension marker, not a capability-name prefix. You know the SEP process and can read “this extension uses SEP-XXXX” with a sense of where it sits on the maturity curve.
  • You know why a feature can move from a core capability slot to the extensions map (tasks v1 → v2): opt-in vs. forced, versionable via specVersion/stability, and keeping the core capability set small.
  • You know mcpkit’s three-tier organization (core/ext/experimental/ext/) and why some extensions get their own go.mod.
  • You know the mcpkit extension points — registries, middleware, custom transports, capability advertisement — and that you don’t need to fork core to add a server-side feature.
  • You can read the case-study table and tell at a glance which surfaces each extension uses.
  • You can distinguish protocol extension from host/client policy — both are “new behavior,” but only the former is an extension in the technical sense.

Next to read

  • Tasks v1/v2 (stub, root) — the deep walk on the largest method-namespace extension, including the v1→v2 migration and the side-by-side registration pattern that replaced the removed RegisterTasksHybrid.
  • Auth deep-dive (stub, root, off-mainline) — the bring-up extension; full OAuth/PRM/JWT/fine-grained-auth.
  • Apps (stub, root) — the library-architecture extension; AppHost/Bridge JS/ServerRegistry. Mostly mcpkit-side, thin protocol surface.
  • Reverse-call mechanics — concretizes elicitation, sampling, roots/list as the same method-namespace pattern.
  • MRTR — Multi Round-Trip Requests; the stateless-server alternative to synchronous reverse calls during a tools/call.
  • List-TTL (SEP-2549) (stub, leaf off notifications) — the canonical _meta-only extension; orthogonal to list_changed.
  • Tracing (SEP-414) (root, FAQ-style) — the canonical _meta-field-plus-middleware extension; W3C trace context over _meta, spans in middleware, the ext/otel adapter.
  • experimental/ext/events/ (branch, target-shape) — events as first-class.