Summary
On a connection that negotiated a modern (2026-07-28) protocol version, Client drops every inbound JSON-RPC request — including consumer-owned extension methods (e.g. ui/*) that the client has explicitly registered a handler for via the documented 3-arg setRequestHandler(method, { params, result? }, handler) form. The handler silently never fires, no error response is written, and the sender hangs until its request timeout. The only signal is a local onerror on the receiving side.
This makes the documented custom-methods path (#1974, docs/advanced/custom-methods.md) unusable for any extension whose protocol includes peer→client requests, unless the consumer overrides the protected _shouldDropInbound hook — i.e. relies on non-public API.
Where this bites: MCP Apps (ext-apps)
I'm working on migrating modelcontextprotocol/ext-apps to v2 (modelcontextprotocol/ext-apps#702). Following the guidance from #1974 ("build custom behavior on Client or Server instead of subclassing Protocol") and the role assignment from earlier composition port (ext-apps#612), the iframe view composes a Client on the postMessage wire and the host composes a Server. The apps protocol legitimately sends host→view (i.e. server→client) requests such as ui/resource-teardown, which the view registers with the 3-arg setRequestHandler. On a modern-era connection these are dropped before handler lookup.
Code
The drop landed as part of the 2026-07-28 implementation (#2286):
packages/client/src/client/client.ts — Client._shouldDropInbound returns 'drop' for any JSONRPCRequest once isModernProtocolVersion(this._negotiatedProtocolVersion) holds.
packages/core-internal/src/shared/protocol.ts — _onrequest consults the hook before handler lookup, so registered custom handlers are unreachable.
The rationale in the code comments is clear and sound for spec methods: the 2026-07-28 era has no server→client request channel (SEP-2322 moved sampling/elicitation/roots into in-band input_required results), and on stdio a client must never write JSON-RPC responses.
The inconsistency
Elsewhere the SDK deliberately treats non-spec methods as era-blind: the era gates in protocol.ts only apply to spec methods, and wire/codec.ts describes anything outside the spec universe as "a consumer-owned extension method (era-blind, schema-explicit)". The inbound drop is the one place where extension traffic is subjected to a spec-era direction rule. Meanwhile setRequestHandler("ui/foo", …) on a Client succeeds without warning, producing a handler that can never run — arguably a footgun independent of the extension use case.
Possible approaches
- Consult registered handlers before dropping: in
Client._shouldDropInbound (or in _onrequest), dispatch inbound requests whose method has an explicitly registered non-spec handler; keep the drop for spec methods and unhandled traffic. This preserves the direction discipline the drop was added for while making the documented custom-methods API self-consistent.
- Opt-in surface on
ClientOptions: e.g. an inboundRequestMethods: string[] | (method: string) => boolean allowlist, defaulting to the current drop-everything behavior.
- Bless the override point: document
_shouldDropInbound as a supported protected extension seam for protocol dialects.
- Spec-level clarification: state whether extensions (SEP-2133 /
capabilities.extensions) may define peer→client requests on the modern era at all — if the answer is no, ext-apps needs a different shape (notifications + view-initiated requests only), and that's worth knowing before the port.
Option 1 seems most aligned with how the rest of v2 treats extension methods, and I'm happy to put up a PR for whichever direction maintainers prefer.
References
Summary
On a connection that negotiated a modern (2026-07-28) protocol version,
Clientdrops every inbound JSON-RPC request — including consumer-owned extension methods (e.g.ui/*) that the client has explicitly registered a handler for via the documented 3-argsetRequestHandler(method, { params, result? }, handler)form. The handler silently never fires, no error response is written, and the sender hangs until its request timeout. The only signal is a localonerroron the receiving side.This makes the documented custom-methods path (#1974,
docs/advanced/custom-methods.md) unusable for any extension whose protocol includes peer→client requests, unless the consumer overrides the protected_shouldDropInboundhook — i.e. relies on non-public API.Where this bites: MCP Apps (ext-apps)
I'm working on migrating
modelcontextprotocol/ext-appsto v2 (modelcontextprotocol/ext-apps#702). Following the guidance from #1974 ("build custom behavior on Client or Server instead of subclassing Protocol") and the role assignment from earlier composition port (ext-apps#612), the iframe view composes aClienton the postMessage wire and the host composes aServer. The apps protocol legitimately sends host→view (i.e. server→client) requests such asui/resource-teardown, which the view registers with the 3-argsetRequestHandler. On a modern-era connection these are dropped before handler lookup.Code
The drop landed as part of the 2026-07-28 implementation (#2286):
packages/client/src/client/client.ts—Client._shouldDropInboundreturns'drop'for anyJSONRPCRequestonceisModernProtocolVersion(this._negotiatedProtocolVersion)holds.packages/core-internal/src/shared/protocol.ts—_onrequestconsults the hook before handler lookup, so registered custom handlers are unreachable.The rationale in the code comments is clear and sound for spec methods: the 2026-07-28 era has no server→client request channel (SEP-2322 moved sampling/elicitation/roots into in-band
input_requiredresults), and on stdio a client must never write JSON-RPC responses.The inconsistency
Elsewhere the SDK deliberately treats non-spec methods as era-blind: the era gates in
protocol.tsonly apply to spec methods, andwire/codec.tsdescribes anything outside the spec universe as "a consumer-owned extension method (era-blind, schema-explicit)". The inbound drop is the one place where extension traffic is subjected to a spec-era direction rule. MeanwhilesetRequestHandler("ui/foo", …)on aClientsucceeds without warning, producing a handler that can never run — arguably a footgun independent of the extension use case.Possible approaches
Client._shouldDropInbound(or in_onrequest), dispatch inbound requests whose method has an explicitly registered non-spec handler; keep the drop for spec methods and unhandled traffic. This preserves the direction discipline the drop was added for while making the documented custom-methods API self-consistent.ClientOptions: e.g. aninboundRequestMethods: string[] | (method: string) => booleanallowlist, defaulting to the current drop-everything behavior._shouldDropInboundas a supported protected extension seam for protocol dialects.capabilities.extensions) may define peer→client requests on the modern era at all — if the answer is no, ext-apps needs a different shape (notifications + view-initiated requests only), and that's worth knowing before the port.Option 1 seems most aligned with how the rest of v2 treats extension methods, and I'm happy to put up a PR for whichever direction maintainers prefer.
References
ext-appsto@modelcontextprotocol/serverv2 SDK ext-apps#702 (ext-apps v2 migration)packages/client/test/client/modernEraInboundDrop.test.ts(pins current behavior)