feat: server and client extensions, request middleware (use), acceptResultType - #2820
Draft
mattzcarey wants to merge 9 commits into
Draft
mattzcarey wants to merge 9 commits into
mattzcarey wants to merge 9 commits into
Conversation
🦋 Changeset detectedLatest commit: af8889b The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
mattzcarey
added this pull request to stack #2826
September 17, 2026 14:18
ServerOptions.extensions takes ServerExtension objects ({ id, capability?,
install(server) }). Each is advertised under capabilities.extensions[id]
and installed at construction, after the built-in handlers exist.
Extensions register custom methods with the explicit-schema
setRequestHandler and intercept spec methods with the new
Protocol.overrideRequestHandler(method, (request, ctx, next) => ...).
Overrides compose around the registered handler at dispatch time, so an
override on tools/call applies even though McpServer registers that
handler lazily; the returned function removes the override.
McpServer tool dispatch re-throws MissingRequiredClientCapabilityError
(-32021) as a JSON-RPC error instead of an isError tool result, matching
the UrlElicitationRequiredError passthrough.
ClientOptions.extensions takes ClientExtension objects ({ id, capability?,
install(client) }), the client half of the server extensions seam. Each is
advertised under the client's capabilities.extensions[id] (in initialize
on a legacy connection, in every request's client-capabilities envelope on
2026-07-28) and installed with the Client at construction.
…inds A client extension declares that results of a method may carry its own resultType (the Tasks extension answers tools/call with "task"). A raw response with that discriminator skips the era codec's closed vocabulary and is validated against the caller's explicit result schema as-is. Also replaces the word 'seam' with 'hook' in the text this stack added.
…ddleware in registration order setRequestHandler is the route handler; use(method, middleware) is the middleware around it, Koa-shaped. Middleware now composes in registration order (first installed outermost), matching Hono and Koa. No wildcard.
…ddleware
setRequestHandler is the route handler; use(method, middleware) is the
middleware around it, Koa-shaped. Middleware composes in one registration
order across methods (first installed outermost), matching Hono and Koa,
and use(middleware) / use('*', middleware) runs on every request so an
extension can stamp its own _meta key on every result.
…stall
The optional capability settings field is gone from ServerExtension and
ClientExtension. The constructor advertises {} under the extension id;
an extension with settings calls registerCapabilities in install, where
everything else it does to the protocol already happens.
What every peer must know about an extension goes through registerCapabilities, not per-request middleware.
mattzcarey
force-pushed
the
feat/server-extensions
branch
from
September 17, 2026 14:23
e056cf6 to
af8889b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
A way to package protocol behaviour outside the core spec — an MCP extension such as
io.modelcontextprotocol/tasks, or a vendor feature — as one object the server installs:ServerOptions.extensionstakesServerExtensionobjects:{ id, install(server) }. Each is advertised undercapabilities.extensions[id]as{}and installed at construction, in order, after the built-in handlers exist. Everything else an extension does to the protocol, its capability settings included (registerCapabilities), happens ininstall: the interface is deliberately code, not declaration. Same option on the low-levelServer.Protocol.use(method, (request, ctx, next) => …)is request middleware, the new hook an extension uses to intercept a spec method.setRequestHandleris the route handler;useis the middleware around it, Koa-shaped:await next(request, ctx)yields the result and the middleware returns what goes on the wire. Middleware composes at dispatch time, so middleware ontools/callinstalled at construction still applies even thoughMcpServerregisters that handler on the first tool registration. With no underlying handler,nextthrowsMethodNotFound. Middleware runs in one registration order, first installed outermost; the returned function removes it. A thrownProtocolErrorbecomes the JSON-RPC error response. Method names are exact, no wildcard: what every peer must know about an extension goes throughregisterCapabilities, not per-request_meta.Protocol.acceptResultType(method, resultType)declares an extension result kind for a method. A raw response carrying thatresultTypebypasses the era codec's closed vocabulary and is validated against the caller's explicit result schema as-is, discriminator included. This is how a client extension receives shapes such as the Tasks extension'sresultType: "task"ontools/call([v2] Tasks extension: tools/call rejects CreateTaskResult but accepts an omitted discriminator as complete #2637) without the codec rejecting them; typed spec calls keep the closed vocabulary.ClientOptions.extensionsis the symmetric client half:ClientExtensionobjects{ id, install(client) }, advertised under the client'scapabilities.extensions[id]— ininitializeon a legacy connection, in every request's_metaclient-capabilities envelope on 2026-07-28, which is where a server extension reads it — and installed with theClient(handlers for server-to-client requests, or middleware around the SDK's own).setRequestHandler(method, { params, result }, handler); nothing new there.McpServertool dispatch re-throwsMissingRequiredClientCapabilityError(-32021) as a JSON-RPC error instead of converting it into anisErrortool result, the same passthroughUrlElicitationRequiredErroralready has. Extensions that gatetools/callon a client capability need the client to see the error as an error.The SDK owns the hooks and the capability advertisement. How an extension stores state or where its work runs is the extension's own.
Tests
packages/core-internal/test/shared/requestMiddleware.test.ts— wraps and transforms, answers withoutnext, throws become JSON-RPC errors, applies to a later-registered handler,MethodNotFoundwith nothing underneath, one registration order across methods with a_metastamp throughnext, removal.packages/client/test/client/extensions.test.ts— install at construction,{}default, the extension ininitializeon a legacy connection, stamped into every request envelope on 2026-07-28, an accepted extension result kind reaching the caller's schema, and a server-to-client request served by the handler the extension installed.packages/server/test/server/extensions.test.ts— advertised capability with settings set frominstall,{}when install sets none (throughMcpServer), extension custom method served,tools/callmiddleware refusing with-32021and passing through with the capability,-32021from inside a tool handler is a JSON-RPC error.Docs
docs/advanced/extensions.md, linked from the Advanced nav next to Custom methods.Not in this PR