-
Notifications
You must be signed in to change notification settings - Fork 10
extensions
Verdict: ServerExtension = bootstrap hook (register features + custom JSON-RPC methods through ExtensionContext) + negotiation hook. Methods an extension registers are owned by it. negotiation() policy: REQUIRED (default) ⇒ client must declare ext on current context, else -32021 (2026) / -32003 (2025); OPTIONAL ⇒ dispatched regardless, nothing synthesized.
| Member | Default | Proof |
|---|---|---|
extensionId() |
— | Extension |
advertiseMode() |
— (ALWAYS / NEVER / NEGOTIATED) |
AdvertiseMode |
serverSettings() |
empty | ServerExtension#serverSettings |
methods() |
empty set (pre-declared owned methods) | ServerExtension#methods |
negotiation() |
REQUIRED (REQUIRED / OPTIONAL) |
ServerExtension#negotiation, .../server/extensions/ExtensionNegotiation.java
|
requiresMetaEnvelope() |
true | ServerExtension#requiresMetaEnvelope |
bootstrap(ExtensionContext) |
no-op | ServerExtension#bootstrap |
onConnectionInit(ctx, clientSettings) |
no-op | ServerExtension#onConnectionInit |
onConnectionClose(ctx), shutdown()
|
no-op | Extension#onConnectionClose, Extension#shutdown |
ExtensionContext = tools/resources/prompts/completions/tasks, executor, runtime, registerHandler(method, ExtensionMethodHandler) ExtensionContext. DefaultTachyonServer implements it DefaultTachyonServer.
ExtensionMethodHandler.handle(InteractionContext, JsonObject params) → Object (null ⇒ protocol empty result) ExtensionMethodHandler, adapter DefaultTachyonServer#getHandler.
Spring extensions still bootstrap during server construction. Discovered annotated beans register later, after singleton initialization, through the existing registries (ServerConfiguration#tachyonServer, TachyonBeanRegistrar#afterSingletonsInstantiated); see spring-boot.
-
withExtensions(...)— duplicate id ⇒ IAEDefaultServerBuilder.javaaddExtension. - Ctor
bootstrapExtensions: recordmethods()owners, setbootstrappingExtensionId, callbootstrap(this); anyregisterHandlerduring bootstrap also ownedDefaultTachyonServer#getHandler,DefaultTachyonServer#toJsonObject. - Features registered with
descriptor.extensionId(...)hidden from lists/calls unless enabled on ctx (ToolMethodHandlers,PromptMethodHandlers,ResourceMethodHandlersfilters). - Negotiation →
ExtensionNegotiator.negotiate(extensions, ctx, declared): enable matching ids +onConnectionInitExtensionNegotiator#negotiate.- 2025-11-25: once in
InitializeHandlerfrominitializeparams, enabled ids stored on session (persisted)InitializeHandler#handle,DefaultDispatchContext#enableExtension. - 2026-07-28: every POST,
ExtensionNegotiationHandlerin pipeline, on fresh channel ctxExtensionNegotiationHandler#channelRead. Non-object params ⇒RequestMappingExceptionswallowed, no extension enabled, request passes on so dispatcher answersinvalid_paramsRequestMappingException.
- 2025-11-25: once in
- Advertise in
initialize/server/discover:ALWAYSalways,NEGOTIATEDif enabled,NEVERneverExtensionNegotiator#registeredExtensions. -
close()→shutdownExtensions(deadline)each on own VT, joined with remaining grace; slow one logged + abandonedDefaultTachyonServer#bootstrapExtensions.
Order: resolve handler first (server.getHandler, none ⇒ methodNotFound), then McpDispatcher.extensionNegotiationRejection McpDispatcher#dispatchTrackedRequestAsync, McpDispatcher#dispatchTrackedRequestAsync, McpDispatcher#extensionNegotiationRejection:
| Owner | Policy | Declared on ctx |
_meta.<extId> (if requiresMetaEnvelope) |
Result |
|---|---|---|---|---|
| none | — | — | — | dispatch (core) |
| ext | OPTIONAL |
any | skipped | dispatch |
| ext | REQUIRED |
no | — |
ServerErrors.missingRequiredExtension(id) ⇒ -32021 / HTTP 400 (2026), -32003 (2025), data.requiredCapabilities.extensions.<id>:{}
|
| ext | REQUIRED |
yes | missing | invalidParams("Missing required client capability: <id>") |
| ext | REQUIRED |
yes | present / not required | dispatch |
Policy snapshot at bootstrap into optionalNegotiationExtensionIds DefaultTachyonServer#optionalNegotiationExtensionIds, DefaultTachyonServer#bootstrapExtensions, DefaultTachyonServer#extensionNegotiationOptional. "Declared on ctx" = session (2025) or per-request channel ctx (2026) — no leak across 2026 requests. OPTIONAL never enables the ext: handler sees isExtensionEnabled=false, no onConnectionInit. Fix 582f9c52: Skills sets requiresMetaEnvelope=false.
Capability requirement inside a handler: throw MissingRequiredClientCapabilityException(msg, requiredCaps) ⇒ -32021 (2026) MissingRequiredClientCapabilityException. Tasks gate helper TasksExtension.requireDeclared (shares ServerErrors.missingRequiredExtension ServerErrors#missingRequiredExtension) returns error only for session-less protocols TasksExtension#requireDeclared.
| Id | Impl | Mode | Page |
|---|---|---|---|
io.modelcontextprotocol/tasks |
TasksExtension (core) |
ALWAYS | tasks |
io.modelcontextprotocol/skills |
SkillsExtension |
ALWAYS, no meta, negotiation via builder (default REQUIRED, same as SPI default; OPTIONAL opt-in) |
tachyon-extensions |
dev.tachyonmcp/kotlin-coroutines |
CoroutineRuntime (internal, lifecycle only) |
NEVER | tachyon-kotlin |
Related: feature-registries, protocol-versions.
📄 source .llm-wiki/concepts/extensions.md · updated 2026-09-17 · verified at 1011a627 · tags [concept, extensions, spi]
🧭 Start
⚙️ Concepts (cross-cutting)
- request-lifecycle
- netty-pipeline
- protocol-versions
- sessions
- sse-streams
- feature-registries
- tasks
- extensions
- json-layer
- errors
- concurrency
- declarative-configuration
- configuration
- security-guards
- observability
- api-stability
📦 Modules