feat(core): export Protocol class + ProtocolSpec generic for typed custom vocabularies#1917
Conversation
🦋 Changeset detectedLatest commit: 04d9133 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/server
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
2f34630 to
daab2e2
Compare
33209d0 to
ae50e77
Compare
|
@claude review |
There was a problem hiding this comment.
No correctness issues spotted beyond the doc-drift nit, but this promotes Protocol (plus four new types) into the curated public surface and adds a second class-level generic — that's exactly the kind of API-surface commitment REVIEW.md says needs a human maintainer's sign-off, so deferring.
Extended reasoning...
Overview
This PR (third in the v2-bc stack) does two things: (1) re-exports the abstract Protocol class from core/public along with four new type-level helpers (ProtocolSpec, McpSpec, SpecRequests, SpecNotifications), and (2) extends Protocol with a second generic parameter SpecT extends ProtocolSpec = McpSpec plus a new spec-typed overload at the front of the setRequestHandler / setNotificationHandler overload sets. ContextT also gains a = BaseContext default. A new vitest file covers the type inference, and a changeset is included.
Security risks
None identified — purely type-level additions and an export-barrel change; no new runtime code paths, I/O, auth, or parsing logic.
Level of scrutiny
High, on API-surface grounds rather than correctness. REVIEW.md is explicit that the burden of proof is on addition and that every new public export must be intentional; CLAUDE.md §Public API Exports until now used Protocol as the canonical internal-only example. Promoting it (and committing to ProtocolSpec as a public extension contract) is a deliberate policy reversal that a maintainer should explicitly approve — even if the motivation (v1 deep-import parity, ext-apps subclassing) is sound.
Other factors
- I verified
ClientandServerboth re-declare their ownsetRequestHandleroverload sets, so the new SpecT-typed overload on the base class is shadowed there and shouldn't perturb existing user-facing inference; the change is effectively scoped to directProtocolsubclasses. - The defaults (
ContextT = BaseContext,SpecT = McpSpec) keep existingextends Protocol<X>references compiling, andSpecRequests<ProtocolSpec> = nevercorrectly disables the typed overload for the unconstrained default — both are exercised in the new test. - One nit (posted inline): the file-header JSDoc in
exports/public/index.tsand CLAUDE.md still citeProtocolas internal-only, contradicting the new export. - Stacked on #1916 → #1891; reviewer should confirm this lands in the intended order.
daab2e2 to
324ed12
Compare
ae50e77 to
b25d3a5
Compare
324ed12 to
3961553
Compare
b25d3a5 to
5fce199
Compare
e75e0b0 to
9da988e
Compare
5fce199 to
9da988e
Compare
|
@claude review |
9da988e to
05a01a1
Compare
e1af6ec to
ccb49d3
Compare
|
@claude review |
2a23915 to
59f44bb
Compare
b7402d8 to
a0158a2
Compare
59f44bb to
c61962c
Compare
a0158a2 to
829ed7f
Compare
c61962c to
d3f9f5a
Compare
829ed7f to
eca508d
Compare
eca508d to
cb7c4e5
Compare
cb7c4e5 to
e0c7fe6
Compare
|
@claude review |
There was a problem hiding this comment.
All prior doc nits are addressed and the type-level machinery looks correct, but since this adds Protocol + the ProtocolSpec/McpSpec/SpecRequests family to the public API surface, a maintainer should sign off on the export decision.
Extended reasoning...
Overview
This PR promotes the abstract Protocol class from internal-only to the curated public barrel (core/public), adds a second generic parameter SpecT extends ProtocolSpec = McpSpec to Protocol, and introduces four new exported type aliases (ProtocolSpec, McpSpec, SpecRequests, SpecNotifications). It also adds spec-typed overload signatures to setRequestHandler/setNotificationHandler. The implementation body of those methods is unchanged — the new overloads are pure type-level declarations that resolve to the existing 3-arg dispatch. Supporting changes: a changeset, CLAUDE.md prose update, the public/index.ts file-header/section-comment updates, and a new protocolSpec.test.ts with type-level assertions and one runtime round-trip.
Review history
I raised three rounds of doc-only nits (stale "internal-only" prose in CLAUDE.md and the file header; ambiguous "the default" wording in the ProtocolSpec JSDoc; misleading "params inferred from SpecT" wording on the handler JSDocs; and a "concrete" vs "abstract" regression in the section comment). All four are now resolved in the current diff — commit 00306b5 fixed the last one. The bug-hunting pass on this revision found nothing.
Security risks
None. No runtime control-flow, auth, transport, or wire-format changes. The only runtime-visible delta is that Protocol is now reachable from @modelcontextprotocol/client/server re-exports; the class itself is unchanged.
Level of scrutiny
Medium. Correctness risk is low (type-only additions, defaulted generic, unchanged implementation). But this is a deliberate public-API expansion in a repo whose REVIEW.md leads with "burden of proof is on addition" and "removing something from the public API is far harder than not adding it." Exporting Protocol commits the SDK to supporting third-party subclassing, and ProtocolSpec/SpecRequests/SpecNotifications become a stable type-level contract. That's a maintainer-level call, not something to auto-approve.
Other factors
The PR is part of the labeled v2-bc series with a linked reviewer guide and stacks on #1916/#1891, so the design has presumably been discussed in that context — but the stacked-PR base and the guide aren't visible here, so I can't independently verify the rationale beyond "v1 exposed this via deep imports." Test coverage is adequate for what's added (typed-SpecT inference, SpecRequests<ProtocolSpec> = never, params-typed-by-schema regression). No outstanding reviewer comments remain.
e0c7fe6 to
99794a8
Compare
00306b5 to
99794a8
Compare
|
@claude review |
54a89b3 to
e042fae
Compare
|
@claude review |
cdc022b to
d353e17
Compare
e042fae to
56ce347
Compare
d353e17 to
ddd6776
Compare
56ce347 to
7051b7c
Compare
…stom vocabularies Exports the abstract Protocol class (was reachable in v1 via deep imports) and adds Protocol<ContextT, SpecT extends ProtocolSpec = McpSpec>. Subclasses supplying a concrete ProtocolSpec get method-name autocomplete and params/result correlation on the typed setRequestHandler/setNotificationHandler overloads.
…verload fallthrough) The SpecT-typed overload's P constraint (StandardSchemaV1<SpecParams>) was too tight to ever match real schemas, so calls always fell through to the loose (method: string, schema, h: => Result) overload, silently accepting any return type. Loosen overload 1's P to bare StandardSchemaV1 (params type still inferred from the schema; only the result type is SpecT-constrained) and never-guard the loose overload's method param against SpecRequests<SpecT> so spec-method calls that don't satisfy the typed overload error instead of falling through. Same guard on setNotificationHandler for symmetry. JSDoc updated; type tests added.
…arify ProtocolSpec.params is informational
8eab636 to
1c4736b
Compare
b141b90 to
04d9133
Compare
| * Supplying a concrete `ProtocolSpec` as `Protocol`'s second type argument gives method-name | ||
| * autocomplete and result-type correlation on the typed overloads of `setRequestHandler` | ||
| * and `setNotificationHandler`. `Protocol` defaults to {@linkcode McpSpec}; using the bare |
There was a problem hiding this comment.
🟡 Nit: after 7051b7c changed "params/result correlation" → "result-type correlation", this JSDoc (and .changeset/export-protocol-spec.md:6) still says SpecT gives "method-name autocomplete and result-type correlation on the typed overloads of setRequestHandler and setNotificationHandler". Notification handlers return void — there's no result type to correlate, so for setNotificationHandler SpecT only contributes method-name autocomplete. Suggest: "method-name autocomplete on both, plus result-type correlation on setRequestHandler" (or just drop setNotificationHandler from the result-correlation clause).
Extended reasoning...
What's overstated. The ProtocolSpec JSDoc at protocol.ts:311-313 and the changeset at .changeset/export-protocol-spec.md:6 both read: "method-name autocomplete and result-type correlation on the typed overloads of setRequestHandler and setNotificationHandler". The natural English parse distributes both features ("method-name autocomplete" and "result-type correlation") across both methods. But notification handlers have no result type — the SpecT-typed setNotificationHandler overload (protocol.ts:1209-1213) declares the handler as (params: StandardSchemaV1.InferOutput<P>) => void | Promise<void>, identical to the never-guarded string overload below it. There is nothing for "result-type correlation" to correlate.
How it got here. Commit 7051b7c addressed the prior review comment (inline 3100080328) that flagged "params/result correlation" as overstating what SpecT does, since e042fae had dropped the params constraint. The fix changed "params/result" → "result-type" in both locations — correct for setRequestHandler — but left setNotificationHandler in the conjunction. After dropping "params/", setNotificationHandler's only remaining SpecT contribution is the K extends SpecNotifications<SpecT> method-name constraint; the "result-type" half no longer has anything to point at for notifications.
Step-by-step proof.
- Define
type Spec = { notifications: { 'ui/ping': { params: { n: number } } } }andconst p = new TestProtocol<Spec>(). - The SpecT-typed overload at protocol.ts:1209 resolves
K = 'ui/ping'(method-name autocomplete ✓). - The handler signature is
(params: InferOutput<P>) => void | Promise<void>— no_Notifications<SpecT>[K]['result']lookup exists (notifications have noresultfield inProtocolSpec, line 321:notifications?: Record<string, { params?: unknown }>). - Compare to
setRequestHandler's SpecT overload at protocol.ts:1100-1107, which types the return as_Requests<SpecT>[K]['result'] | Promise<…>— that's the result-type correlation the prose describes. - So for
setNotificationHandler, SpecT contributes only step 2 (autocomplete); the "result-type correlation" claim is vacuous there.
Partial mitigation, and why it's still worth fixing. The very next paragraph (protocol.ts:316-317: "Only requests[K].result is enforced by the type system") implicitly scopes result enforcement to requests, so a careful reader can reconcile. But the headline sentence is what readers skim, and the changeset prose lands verbatim in published CHANGELOGs where the mitigating paragraph won't follow it. Per REVIEW.md §Documentation & Changesets ("flag any claim the diff doesn't back"), this is the textbook case.
Fix. One-clause reword in both locations, e.g.: "gives method-name autocomplete on the typed overloads of setRequestHandler and setNotificationHandler, plus result-type correlation on setRequestHandler" — or simply drop setNotificationHandler from the sentence and let the next paragraph cover it. Doc-only; no runtime or type-level impact.
Part of the v2 BC series — see reviewer guide. Stacks on #1916 (which stacks on #1891).
Exports the abstract
Protocolclass (was reachable in v1 via deep imports; ext-apps subclasses it) and addsProtocol<ContextT, SpecT extends ProtocolSpec = McpSpec>. Supplying a concreteProtocolSpecgives method-name autocomplete and params/result correlation on the typedsetRequestHandler/setNotificationHandleroverloads.Motivation and Context
v1 exposed
Protocolvia deep imports (sdk/shared/protocol.js). MCP-dialect protocols (e.g., MCP Apps) subclass it. v2 omitted it from the public surface — this restores it and adds aProtocolSpectype-level vocabulary.How Has This Been Tested?
packages/core/test/shared/protocolSpec.test.ts— typed-SpecT inference,SpecRequests<ProtocolSpec> = never, params typed by passed schema (not SpecT).Breaking Changes
None — additive.
Protocolstays abstract; the second generic defaults so existingProtocol<ContextT>references work unchanged.Types of changes
Additional context
Stacks on #1916 → #1891.