Skip to content

refactor(core): _wrapHandler hook so subclasses don't redeclare setRequestHandler#1976

Merged
felixweinberger merged 2 commits intomainfrom
fweinberger/1974a-wraphandler
Apr 29, 2026
Merged

refactor(core): _wrapHandler hook so subclasses don't redeclare setRequestHandler#1976
felixweinberger merged 2 commits intomainfrom
fweinberger/1974a-wraphandler

Conversation

@felixweinberger
Copy link
Copy Markdown
Contributor

Replace Client/Server setRequestHandler overrides with a protected _wrapHandler hook.

Motivation and Context

On main, Client and Server each override setRequestHandler to wrap registered handlers with validation (sampling/createMessage, elicitation/create, tools/call). That means redeclaring the public method's signature and JSDoc in two subclasses, which goes stale when the base signature changes.

This PR adds protected _wrapHandler(method, handler): handler to Protocol (default: identity), called inside setRequestHandler before storing. Subclasses override _wrapHandler instead of the public method. Same behavior, no public API change. Base PR for #1974.

How Has This Been Tested?

pnpm test:all green. New wrapHandler.test.ts asserts the hook is called for registered handlers. Existing client/server validation tests pass unchanged.

Breaking Changes

None.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Pure refactor extracted from #1974 so the custom-method overload PR can stack on it without subclasses redeclaring the overload set.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 28, 2026

🦋 Changeset detected

Latest commit: e69c6a7

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

This PR includes changesets to release 7 packages
Name Type
@modelcontextprotocol/core Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/server Patch
@modelcontextprotocol/node Patch
@modelcontextprotocol/express Patch
@modelcontextprotocol/fastify Patch
@modelcontextprotocol/hono Patch

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
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 28, 2026

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/server

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 19e32d7

@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

@felixweinberger felixweinberger marked this pull request as ready for review April 28, 2026 20:17
@felixweinberger felixweinberger requested a review from a team as a code owner April 28, 2026 20:17
Comment thread packages/core/src/shared/protocol.ts
Comment thread packages/server/src/server/server.ts Outdated
@felixweinberger felixweinberger force-pushed the fweinberger/1974a-wraphandler branch from 8f12176 to e69c6a7 Compare April 28, 2026 20:42
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — both earlier points are addressed (base return type tightened to ResultTypeMap[M], out-of-scope string widenings reverted), and the remaining diff is a behavior-preserving move of the wrapping logic into the new hook.

Extended reasoning...

Overview

This PR introduces a protected _wrapHandler(method, handler) hook on Protocol (identity by default) and has setRequestHandler route the stored closure through it. Client and Server now override _wrapHandler instead of redeclaring the public setRequestHandler signature; the validation bodies for elicitation/create, sampling/createMessage, and tools/call are moved verbatim (modulo reindent and await handler(...) replacing await Promise.resolve(handler(...)), which is equivalent since stored already returns a Promise). A new wrapHandler.test.ts asserts the hook is invoked per registration, and a changeset is included.

Follow-up on prior review

My two earlier comments are resolved in e69c6a7:

  • The base setRequestHandler handler return type is now ResultTypeMap[M] | Promise<ResultTypeMap[M]>, restoring the compile-time contract that the removed subclass overrides previously provided.
  • The four assertCapabilityForMethod / assertNotificationCapability parameter widenings to string were reverted; both subclasses again match the abstract declarations in Protocol.

I traced the one ordering difference: previously the subclass wrapper received the already-schema.parsed request and re-parsed it; now _wrapHandler receives the raw JSONRPCRequest and parses it, then stored parses again before calling the user handler. Since getRequestSchema(method) and the explicit CallToolRequestSchema/ElicitRequestSchema/CreateMessageRequestSchema are the same loose schemas, the user handler still receives an identically-shaped parsed object, and the internal params.mode defaulting was never visible to the user handler before either (it mutated a separate parse result). Net runtime behavior is unchanged.

Security risks

None. No transport, auth, or I/O paths are touched; this only reorganizes where in-process handler wrapping happens. The validation that was applied before is still applied, on the same inputs, with the same error codes.

Level of scrutiny

Moderate — Protocol.setRequestHandler is public API on the core package, but the only public-facing change is the tightening of the handler return type on the base class to match what Client/Server already enforced, which is strictly safer for consumers. _wrapHandler is protected, so it's not new public surface. The rest is mechanical code movement covered by existing client/server validation tests plus the new hook test.

Other factors

The Client class JSDoc absorbs the example that used to live on the removed override, so doc coverage is preserved. No bugs from the bug-hunting pass. This is now a tightly-scoped base PR for #1974 as described.

Copy link
Copy Markdown
Contributor

@KKonstantinov KKonstantinov left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — both earlier comments addressed (base return type tightened to ResultTypeMap[M], capability-assert widenings reverted), and the remaining diff is a scoped hook refactor with identical runtime behavior.

Extended reasoning...

Overview

Introduces protected Protocol._wrapHandler(method, handler) (default: identity) and routes setRequestHandler through it before storing. Client and Server override _wrapHandler instead of redeclaring setRequestHandler; their validation bodies for elicitation/create / sampling/createMessage / tools/call are moved verbatim into the hook. The base setRequestHandler handler return type is tightened from Result to ResultTypeMap[M] to preserve the type contract previously enforced by the deleted overrides. A small vitest asserts the hook is invoked, plus a changeset.

Follow-up on prior review

I raised two issues on the first revision; both are fixed in the current diff and the threads are resolved:

  • 🔴 Handler return-type loosening → base signature now uses ResultTypeMap[M] | Promise<ResultTypeMap[M]> (protocol.ts), restoring the prior compile-time contract for Client/Server callers.
  • 🟡 Out-of-scope widening of assertCapabilityForMethod/assertNotificationCapability to string → reverted; both subclasses again take RequestMethod/NotificationMethod, matching the base abstract declarations.

Security risks

None. No transport, auth, or I/O changes; this only restructures where the existing validation closures are assembled. The wrapped handler still runs the same Zod request/result parses with the same ProtocolError codes.

Level of scrutiny

Medium — it touches the core request-handler registration path used by every server/client, but the change is mechanical (override → hook) and the validation bodies are byte-for-byte moves apart from await handler(...) replacing await Promise.resolve(handler(...)) (equivalent, since stored already returns a Promise). The execution order of base-schema parse vs. role-specific parse is inverted relative to before, but both still run on the same raw request and the user handler still receives the base-parsed payload, so observable behavior is unchanged.

Other factors

  • _wrapHandler is protected and underscore-prefixed, so no new public export surface.
  • Constructor-time setRequestHandler('ping' | 'initialize' | 'logging/setLevel') calls now flow through the subclass _wrapHandler, which correctly returns identity for those methods.
  • Tightening the base return type is a strict improvement (direct Protocol callers now get the same checking subclasses had) and existing internal callsites (ping, initialize, logging/setLevel, the new test) all satisfy it.
  • Bug-hunting pass found nothing on this revision; pnpm test:all reported green by the author.

@felixweinberger felixweinberger merged commit 55b1f06 into main Apr 29, 2026
22 of 23 checks passed
@felixweinberger felixweinberger deleted the fweinberger/1974a-wraphandler branch April 29, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2-bc v2 backwards-compatibility series

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants