Skip to content

feat: server and client extensions ({ id, setup(ctx) }), request middleware (use), acceptResultType - #2820

Open
mattzcarey wants to merge 10 commits into
mainfrom
feat/server-extensions
Open

mattzcarey wants to merge 10 commits into
mainfrom
feat/server-extensions

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Sep 16, 2026 •

Copy link
Copy Markdown
Member

Servers and clients take MCP extensions as { id, setup(ctx) } objects: new McpServer(info, { extensions: [skills, tasks] }). What is required and declarative sits on the object, which today is just the id. Everything else happens in setup through ctx, so the SDK can check ids and method names for each extension. First of a stack: #3066 ports Skills, and #2782 adds Tasks with an example.

const gate: ServerExtension = {
    id: 'com.example/gate',
    setup(ctx) {
        ctx.capability({ exampleData: true });   // capabilities.extensions[id]; {} by default
        ctx.require('com.example/audit');        // another extension this one can't work without
        ctx.setRequestHandler('gate/status', { params }, () => ({ armed: true }));
        ctx.use('tools/call', (request, requestCtx, next) => next(request, requestCtx));
        ctx.mcp.registerResource(/* … */);       // anything McpServer registers
    }
};

Skills (#3047, merged) registers a resource template through McpServer, and Tasks (#2782) needs middleware on tools/call plus its own result kind on the client. One context covers both, and it maps onto SEP-3371's registration, dependencies, custom methods and middleware:

  • ctx.setRequestHandler takes the same arguments as setRequestHandler. It refuses a core method name, and a method another handler already serves, so two extensions can't silently replace each other.
  • ctx.require(extensionId) names another extension. The full list is known before any setup runs, so order doesn't matter, and a missing one fails construction, naming both. Optional behaviour stays in the extension's own code.
  • Extensions are set up in array order. A duplicate id fails construction before any setup runs. Extension middleware runs outside middleware the application adds afterwards with server.use.
  • extensions takes only { id, setup(ctx) }; there are no adapters. It is an McpServer option, and the low-level Server is unchanged.
  • The client mirrors the shape, with ctx.client, ctx.setRequestHandler for server-to-client requests and ctx.acceptResultType, which lets tools/call carry resultType: "task".
  • The id, dependency and handler rules live once in core-internal (shared/extensions.ts), so server and client refuse the same things with the same messages.
  • Protocol.use and Protocol.acceptResultType stay public for applications. McpServer re-throws MissingRequiredClientCapabilityError (-32021) from a tool as a JSON-RPC error.
  • The extension shape, both extensions options, the contexts, use, acceptResultType and RequestMiddleware are @experimental. Docs are in docs/advanced/extensions.md.

pnpm -r typecheck, pnpm -r lint, pnpm docs:check and the core-internal, client and server suites ran. #2782 adds an end-to-end test with Skills and Tasks on one server.

@changeset-bot

changeset-bot Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9f97b9d

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/client Minor
@modelcontextprotocol/server 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: 9f97b9d

@mattzcarey mattzcarey changed the title feat(server): server extensions and Protocol.overrideRequestHandler feat: server and client extensions, Protocol.overrideRequestHandler Sep 17, 2026
@mattzcarey mattzcarey changed the title feat: server and client extensions, Protocol.overrideRequestHandler feat: server and client extensions, overrideRequestHandler, acceptResultType Sep 17, 2026
@mattzcarey mattzcarey changed the title feat: server and client extensions, overrideRequestHandler, acceptResultType feat: server and client extensions, request middleware (use), acceptResultType Sep 17, 2026
@mattzcarey
mattzcarey added this pull request to stack #2826 September 17, 2026 14:18
@mattzcarey
mattzcarey force-pushed the feat/server-extensions branch from e056cf6 to af8889b Compare September 17, 2026 14:23
@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Sep 23, 2026
@mattzcarey
mattzcarey force-pushed the feat/server-extensions branch from af8889b to e17337b Compare October 8, 2026 21:41
@mattzcarey
mattzcarey marked this pull request as ready for review October 8, 2026 21:41
@mattzcarey
mattzcarey requested a review from a team as a code owner October 8, 2026 21:41
@mattzcarey
mattzcarey force-pushed the feat/server-extensions branch from e17337b to be40c1e Compare October 8, 2026 21:46

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 optional notes from this repository's REVIEW.md or CLAUDE.md checks were not posted as comments, over this review's limit for such notes; they are on this commit's check card.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🔴 packages/core-internal/src/shared/protocol.ts — Client extensions that wrap elicitation/create (or sampling/createMessage, roots/list) with use silently get no middleware on a 2026-07-28 connection. On the modern era those requests arrive embedded in input_required results and are dispatched through _getRequestHandler at protocol.ts:747, which reads _requestHandlers directly and skips the middleware stack. Fix: resolve every dispatch path through _resolveRequestHandler so use middleware composes around embedded input requests too, which also covers the getRequestHandler host hook in client.ts:752. Only legacy _onrequest traffic composes middleware today, and Client._shouldDropInbound drops modern top-level requests, so on 2026-07-28 the middleware never runs. [also at: packages/core-internal/src/shared/protocol.ts:1896 - Client extension middleware on elicitation/create is skipped for embedded input requests on 2026-07-28 connections, so a gate or rewrite it installs silently does not run there.]

    Why this was flagged

    A user installs a ClientExtension that calls client.use('elicitation/create', mw) as the JSDoc in packages/client/src/client/extension.ts:15 describes, and connects on protocol revision 2026-07-28. The server answers tools/call with an input_required result carrying an embedded elicitation/create request; the Client's _resolveNonCompleteResult (packages/client/src/client/client.ts:749-752) runs runInputRequiredFlow, which calls host.getRequestHandler(method) which is Protocol._getRequestHandler (protocol.ts:746-747) returning this._requestHandlers.get(method). Only _onrequest at protocol.ts:1037 uses the new _resolveRequestHandler, so the middleware array is never consulted here and mw never runs; the raw registered handler answers. On the modern era Client._shouldDropInbound (client.ts:690-696) drops any top-level inbound request, so there is no path on which the middleware applies at all. The base branch has no use, so the regression is that the new API advertises a hook that is a no-op on the default modern negotiation.

    Verification: The pre-existing _getRequestHandler at protocol.ts:746-748 is untouched and still does return this._requestHandlers.get(method);. dispatchInputRequest in packages/core-internal/src/shared/inputRequiredEngine.ts:145-163 calls that handler directly, so middleware registered via use for those methods is never invoked on the embedded path. On 2026-07-28 this is the only path for those methods.

Comment thread packages/core-internal/src/shared/protocol.ts
Comment thread packages/server/src/server/server.ts Outdated
Comment thread packages/core-internal/src/shared/protocol.ts Outdated
Comment thread packages/core-internal/src/shared/protocol.ts Outdated
Comment thread packages/server/src/server/mcp.ts
Comment thread packages/core-internal/src/shared/protocol.ts Outdated
Comment thread packages/core-internal/src/shared/protocol.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no new issues

No new issues were found in this update; 7 findings from earlier reviews are still open above.

1 optional suggestion (a nit or a note on pre-existing code) was found and not posted.

Still open from earlier reviews (7):

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no new issues

No new issues were found in this update; 7 findings from earlier reviews are still open above.

1 optional suggestion (a nit or a note on pre-existing code) was found and not posted.

Still open from earlier reviews (7):

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

@mattzcarey
mattzcarey force-pushed the feat/server-extensions branch from 09c01f2 to 96868ae Compare October 20, 2026 00:17
@mattzcarey
mattzcarey removed this pull request from stack #2826 October 20, 2026 00:17
@mattzcarey
mattzcarey changed the base branch from main to feature/ext-skills-sep-2640 October 20, 2026 00:17
@mattzcarey
mattzcarey added this pull request to stack #3054 October 20, 2026 00:17
@mattzcarey mattzcarey changed the title feat: server and client extensions, request middleware (use), acceptResultType feat: server and client extensions (install and attach), request middleware (use), acceptResultType Oct 20, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline finding, I also checked the new filesystem surface the Skills extension adds in packages/server/src/ext/skills/fsPath.ts and node.ts for path traversal: splitRelativePath rejects .. and empty segments, fsPathWithin re-checks containment with path.relative after the join, and node.ts opens with O_NOFOLLOW so a final-component symlink cannot escape the skills folder.

Extended reasoning...

Since the last review the PR grew from the generic extension hooks to also include the full Skills extension (SEP-2640), which serves files from a server-side folder over skill:// URIs and resources/directory/read; that path-containment guard was the new security-sensitive surface examined this run and it holds, so this note records only that ruled-out check.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (7):

  • 🔴 packages/core-internal/src/shared/protocol.ts:1901 — Extension middleware that reads ctx.mcpReq.requestState() gets the raw, unverified wire string, not the value the reque…
  • Also unresolved: 5 minor or pre-existing, 1 blocking on lines changed since (possibly already fixed).

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Comment thread packages/server/src/index.ts Outdated
@mattzcarey
mattzcarey force-pushed the feat/server-extensions branch from 96868ae to 55c03c8 Compare October 20, 2026 13:52
@mattzcarey mattzcarey changed the title feat: server and client extensions (install and attach), request middleware (use), acceptResultType feat: server and client extensions ({ id, setup(ctx) }), request middleware (use), acceptResultType Oct 20, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 other optional suggestions (nits or notes on pre-existing code) were found and not posted.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (10):

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Comment thread packages/client/src/client/extension.ts Outdated
@mattzcarey
mattzcarey force-pushed the feat/server-extensions branch from ed2a77c to 3cfe7ec Compare October 21, 2026 15:14

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 other optional suggestions (nits or notes on pre-existing code) were found and not posted.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (11):

  • 🔴 packages/core-internal/src/shared/protocol.ts:1901 — Extension middleware that reads ctx.mcpReq.requestState() gets the raw, unverified wire string, not the value the reque…
  • Also unresolved: 8 minor or pre-existing, 2 blocking on lines changed since (possibly already fixed).

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Comment thread docs/advanced/extensions.md Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 other optional suggestion (a nit or a note on pre-existing code) was found and not posted.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (12):

  • 🔴 packages/core-internal/src/shared/protocol.ts:1905 — Extension middleware that reads ctx.mcpReq.requestState() gets the raw, unverified wire string, not the value the reque…
  • Also unresolved: 9 minor or pre-existing, 2 blocking on lines changed since (possibly already fixed).

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Comment thread packages/server/src/server/extension.ts Outdated
@mattzcarey
mattzcarey force-pushed the feat/server-extensions branch from 39b0911 to 92954d8 Compare October 27, 2026 12:17
On a 2026-07-28 connection the client fulfils elicitation/create,
sampling/createMessage and roots/list embedded in input_required results
through _getRequestHandler, which read the stored handler directly. Middleware
an extension installed with ctx.use on those methods never ran there, though
it is the only path for them on that era. Compose the same middleware stack
around that handler.
…ion path

ctx.setRequestHandler only checked the 2026-07-28 method registry, which
holds no client-served methods and drops several 2025-11-25 ones. A client
extension could register elicitation/create, sampling/createMessage or
roots/list, and a server extension logging/setLevel or resources/subscribe,
though the JSDoc and docs say a core name throws. Check both eras, keeping
the tasks/get and tasks/cancel names SEP-2663 reuses for the Tasks extension.
- use() middleware receives the request as it arrived, params not yet
  validated, not a parsed request.
- Typed calls such as callTool are also routed past the codec by
  acceptResultType, and reject the kind as an invalid result.
- Application middleware goes on mcp.server.use; McpServer has no use().
- Client custom request handlers are served on 2025-11-25 connections only.
- The gate example refuses every 2025-11-25 request as written.
- Move the acceptResultType comment off the codec decode comment, and the
  ServerExtension export off the requestStateCodec comment.
…path only

The check sat in the request funnel every typed spec call shares, so once a
client extension accepted resultType "task" on tools/call, a plain
client.callTool also skipped the codec. McpServer adds content: [] to a task
handle, so the handle then parsed as an empty CallToolResult and the task was
lost to the caller. Gate the bypass on the explicit-schema overload (and its
retry legs), as the JSDoc already said; typed calls answer
UnsupportedResultType again. Restores that JSDoc and corrects the guide.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 other optional suggestions (nits or notes on pre-existing code) were found and not posted.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (6):

  • 🔴 packages/core-internal/src/shared/protocol.ts:1917 — Extension middleware that reads ctx.mcpReq.requestState() gets the raw, unverified wire string, not the value the reque…
  • Also unresolved: 4 minor or pre-existing, 1 blocking on lines changed since (possibly already fixed).

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Comment thread packages/core-internal/src/shared/protocol.ts Outdated
The export sat under the requestStateCodec comment, then under the OAuth
metadata comment once sorted; it now has its own.
…through middleware

- setup may return a promise. The setups after it wait for it, so they still
  run in array order, and connect (Protocol, and Server before its
  one-connection check) waits for all of them and rejects with the error if
  one fails. Nothing is processed with an extension partly set up, and a
  failure that nobody connects for is not an unhandled rejection.
- Extension middleware (ctx.use) is kept in its own tier, composed outside
  middleware the application adds with use(), so an async setup that
  registers after the application cannot end up inside it.
- An extension's capabilities.extensions entry belongs to it: an entry for an
  installed id in the capabilities option fails construction, and
  registerCapabilities refuses it later. It was silently replaced by {}.
- Middleware on a method with no handler: when the MethodNotFound that next
  throws is passed through, the per-request HTTP transport answers 404, as
  for a method with no handler and as Hono answers notFound through route
  middleware. A middleware answer keeps its own status.
- Documented that middleware sees the request before params validation and
  before the requestState verify hook, and that an answer without next skips
  result validation. Both installers now share installExtensions.
…brief comments

- With acceptResultType in force, a typed call such as callTool on a
  2025-11-25 connection had the codec strip resultType and returned a task
  handle as an empty tool result. Typed calls now reject an accepted kind
  with UnsupportedResultType on both eras.
- ctx.mcpReq.send with an explicit schema now accepts extension result
  kinds like request() with one.
- The middleware docs say the request arrives with wire-only material
  lifted onto ctx.mcpReq, not as it was received.
- Cut the new comments to one line where they are not public API.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 other optional suggestion (a nit or a note on pre-existing code) was found and not posted.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (4):

  • Unresolved: 4 minor or pre-existing.

Comment thread packages/server/src/server/server.ts
Comment thread docs/advanced/extensions.md Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (2):

  • 🔴 packages/server/src/server/server.ts:362 — Servers hosted with createMcpHandler skip a tool's scopeChallenge and Mcp-Param-* check when an async extension setup r…
  • Also unresolved: 1 minor or pre-existing.

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Comment thread packages/core-internal/src/shared/protocol.ts
…'s instance

createMcpHandler's modern route and serveStdio read the instance the
factory returned before connecting it: the tool input schema for the
Mcp-Param-* check, the tool's scope challenge, and the capabilities that
narrow subscriptions/listen filters. Only connect waited for async setups,
so a tool an async setup registered skipped the scope preflight and was
called with a token lacking the scope. Both now wait right after the
factory returns; a failed setup fails the request or connection.
… with accepted kinds

- A ctx.require or other throw from an async setup, or from a setup after
  one, rejects connect rather than failing the constructor.
- Typed calls reject an acceptResultType kind with UnsupportedResultType on
  either protocol version; the JSDoc, an inline comment and the changeset
  still said they ignore the declaration.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the latest push and found no new bugs. Because this adds new public API across three packages and changes request dispatch, a human look at the design is still worthwhile.

What was reviewed

  • The two newest commits: createMcpHandler and serveStdio now await extensionsSettled right after the factory returns, before the capability, Mcp-Param-* and scope-challenge reads.
  • The docs, changeset and JSDoc wording on async setup failures rejecting connect, and on typed calls rejecting an accepted result kind with UnsupportedResultType, now match the code in shared/extensions.ts and protocol.ts.
  • Middleware composition in Protocol (extension tier outermost, the no-handler MethodNotFound mapped to 404 in perRequestTransport.ts) and capability ownership in registerCapabilities on both sides.
Extended reasoning...

The change is 21 files, +1655/-17, adding experimental extensions, Protocol.use middleware and acceptResultType across core-internal, client and server, with docs, a changeset and unit tests. It touches security-relevant surface: middleware runs before params validation and the requestState.verify hook, and the HTTP entry's scope-challenge preflight now depends on async extension setups having settled. This run found no bugs, and the commits pushed after the last review cover the hosting-entry readiness wait and the wording items. It is deferred, not approved, because of the size, the new public API surface, and the McpServer tool-dispatch behaviour change (-32021 re-thrown as a JSON-RPC error) shipped as a minor, which are design decisions for a maintainer.

This branch has not been deployed

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

Labels

v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant