Skip to content

feat(server): server extensions and Protocol.overrideRequestHandler - #2820

Draft
mattzcarey wants to merge 1 commit into
mainfrom
feat/server-extensions
Draft

mattzcarey wants to merge 1 commit into
mainfrom
feat/server-extensions

Conversation

@mattzcarey

Copy link
Copy Markdown
Contributor

Draft, first of a stack. This PR adds the seam; #2782 (rebased on top of it) adds the Tasks extension as its first consumer.

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:

const server = new McpServer({ name: 'gated', version: '1.0.0' }, { extensions: [tasks] });
  • ServerOptions.extensions takes ServerExtension objects: { id, capability?, install(server) }. Each is advertised under capabilities.extensions[id] ({} by default) and installed at construction, in order, after the built-in handlers exist. Same option on the low-level Server.
  • Protocol.overrideRequestHandler(method, (request, ctx, next) => …) is the new seam an extension uses to intercept a spec method. Overrides compose around the registered handler at dispatch time, so an override on tools/call installed at construction still applies even though McpServer registers that handler on the first tool registration. With no underlying handler, next throws MethodNotFound. Later overrides nest outside earlier ones; the returned function removes the override. A thrown ProtocolError becomes the JSON-RPC error response.
  • Custom methods keep using the existing explicit-schema setRequestHandler(method, { params, result }, handler); nothing new there.
  • McpServer tool dispatch re-throws MissingRequiredClientCapabilityError (-32021) as a JSON-RPC error instead of converting it into an isError tool result, the same passthrough UrlElicitationRequiredError already has. Extensions that gate tools/call on a client capability need the client to see the error as an error.

The SDK owns the seams 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/overrideRequestHandler.test.ts — wraps and transforms, answers without next, throws become JSON-RPC errors, applies to a later-registered handler, MethodNotFound with nothing underneath, nesting order, removal.
  • packages/server/test/server/extensions.test.ts — advertised capability and install order, {} default through McpServer, extension custom method served, tools/call override refusing with -32021 and passing through with the capability, -32021 from inside a tool handler is a JSON-RPC error.
  • Full suites: core-internal 1462, server 487, client 877, all green.

Docs

docs/advanced/extensions.md, linked from the Advanced nav next to Custom methods.

Not in this PR

  • No dispatch middleware or plugin registry beyond the override seam; kept deliberately small.
  • Client-side extensions (none needed yet).

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.
@changeset-bot

changeset-bot Bot commented Sep 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f195b40

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/core-internal Minor
@modelcontextprotocol/server Minor
@modelcontextprotocol/client Minor
@modelcontextprotocol/codemod Minor
@modelcontextprotocol/core Minor
@modelcontextprotocol/server-legacy Minor

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

@pkg-pr-new

pkg-pr-new Bot commented Sep 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2820

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2820

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2820

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2820

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2820

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2820

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2820

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2820

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2820

commit: f195b40

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant