Repository navigation
events: four wire divergences from the merged design sketch, found by the new conformance suite #1380
Description
Why
The Events conformance suite (#1378, panyam/mcpconformance#23) scored examples/events/kitchen-sink against the design sketch that merged upstream on 2026-09-08. Six divergences came back. Two were already tracked: nextPollSeconds after spec commit 197c32b4 (G31, in flight on feat/events-spec-wire-alignment) and the events.topology substitute for the missing list_changed (G34).
The other four were not. Each is self-consistent and covered by our own tests, which is exactly why they survived: every test we wrote already knows what our implementation does.
Measured against kitchen-sink at main on 2026-09-15. make testconf-events reproduces.
The four
1. events/list answers, but capabilities.events is never declared
The most consequential of the four. initialize returns {"tools":…,"resources":…,"logging":…,"completions":…} and no events key, while events/list serves a full catalog. A client that reads capabilities to decide what to call, which is what the spec tells it to do, never discovers the surface at all.
This is also why the suite's capability gate probes events/list before skipping. The conventional "undeclared optional capability → SKIP" would have reported us green here.
- Declare
capabilities.eventswhen any event source is registered, withlistChangedreflecting whether we send the notification (today: we do not, see G34).
2. No descriptor carries inputSchema
Every descriptor has payloadSchema; none has inputSchema. The sketch defines inputSchema as the JSON Schema for subscription arguments, mirroring the inputSchema/arguments pairing on tools. Without it a client cannot validate arguments before subscribing, and cannot tell which filters an event type accepts.
Knock-on: sep-9999-poll-invalid-arguments is untestable against us, because there is no declared constraint left to violate.
- Surface the per-source argument schema on the descriptor.
3. events/poll omits events instead of returning an empty array
{"cursor":…,"hasMore":false,…} with no events key when nothing happened. The sketch's response shape has events always present, and "empty events array means nothing happened" is called out as the common case. Reads like a Go omitempty on a nil slice.
A client doing for (const e of result.events) breaks on the quiet path, which is the path that runs almost every time.
- Always marshal
events, empty array when there is nothing to return.
4. events/poll answers for event types that do not advertise poll
presence.changed declares delivery: ["push","webhook"]. Polling it returns a result rather than -32014 Unsupported. The delivery array is currently descriptive rather than enforced.
events.topology is the same shape from the other side: it sends delivery: null, where the sketch requires a non-empty subset of poll/push/webhook. That one is entangled with G34, since the source only exists as the substitute for list_changed, so it belongs with that work rather than here.
- Enforce
deliveryon theevents/pollpath, answering-32014 Unsupportedwithdatanaming the mode.
Not in scope here
- G31 (
nextPollSeconds/maxAgein seconds) — already in flight. - G30 (event-type removal and breaking-change termination) — declared in the suite's yaml but not yet exercised; driving it needs a server that can drop an event type mid-run, which waits on the push scenario.
events.topology'sdelivery: null— G34.
Verification
make testconf-events today reports events-discovery 8/11 and events-poll 18/28. Closing all four should take those to 11/11 and 19/28, the remainder being checks that need a delivered event or a stale cursor the harness cannot mint. Update the recorded counts in conformance/local-suites.yaml when they move.
Refs #1374
🤖 Generated with Claude Code
Activity
panyam commentedon Sep 15, 2026
The suite that found these is now a draft upstream: modelcontextprotocol/conformance#504. The fork PR is closed; review happens there.
feat(conformance): wire make testconf-events against the Events suite
panyam commentedon Sep 15, 2026
Re-ran against
mainatc9160cff, after #1379 and #1381 landed. Two of the four have changed, so updating before anyone picks this up.Item 2 narrowed. "No descriptor carries
inputSchema" is no longer accurate — #1381 added it to the three real sources. Of the four descriptorsevents/listnow returns, onlyevents.topologyis missing it.Items 2, 3 and 4 collapse into one cause. With the real sources fixed, every remaining descriptor-level failure traces to the
events.topologymeta-source, which does not keep the descriptor contract at all:delivery: null, noinputSchema, and it answersevents/polldespite advertising no poll delivery. That reads less like three bugs and more like one question — whether a meta-source should appear inevents/listas a peer of real event types, or be modelled some other way. Worth deciding that before fixing the three symptoms individually.Items 1 and 3 stand unchanged.
capabilities.eventsis still never declared, which keeps the whole surface undiscoverable and is still the one I would fix first.events/pollstill omitseventsrather than returning[].No longer in scope here: G31 (
nextPollSeconds) closed by #1379; G34 and G30 (list_changed, termination) by #1381. The suite confirms all three.Current:
events-discovery8/11,events-poll19/29.feat(conformance): wire make testconf-events against the Events suite
panyam commentedon Sep 22, 2026
All six closed by #1416 (merged
02fea1bd). Verified on mergedmain:capability-events-objectcapabilities.events,listChanged: true, on both wirespoll-events-arrayeventsalways an array, guaranteed inMarshalJSONpoll-mode-unsupportedevents/pollgates on the declareddelivery,-32014descriptor-delivery-subsetdescriptor-input-schemaevents.topologypoll-invalid-argumentsTwo of these were scoped out of this issue and could not stay out
events.topology'sdelivery: nulland missinginputSchemawere filed here as entangled with G34. Enforcingdeliveryon the poll path made that impossible to defer: a source advertisingdelivery: nullstops answering polls the moment the gate lands. Exempting the library's own descriptor from the contract it enforces on everyone else's was the worse option.Three things this issue did not know about
events/listranged a map, so its order flapped between calls. Invisible while the one unpollable source was never a selection candidate; the momentevents.topologybecame poll-eligible, scenarios that pick "the first type offering poll" started choosing differently per run. Same bug as fix(ui): InProcessAppBridge tools/list ranged a map, so the order flapped #1408. Now constraint C10.inputSchemaat all. This issue and the suite's own yaml both attributed that row toevents.topology; the failure was onchat.message. Argument validation had never been graded by anything.sep-9999-poll-invalid-argumentsreports untestable, not failing, when the selected type declares no typed property. Reads like a defect in the report and is a fixture gap.On the delivery derivation
An author's declared array is never widened or narrowed. An absent one is derived from what the source can actually serve rather than defaulted to all three: 68 of the 121
EventDefliterals in the package's tests leave it unset, so strict enforcement would have broken them wholesale, and a fixed default would have claimed push for sources that cannot stream. ReadRegistry.Def(name), notsrc.Def(), anywhere the answer has to match whatevents/listpublished.Verified it still grades
Disabling the poll gate turns
sep-9999-poll-mode-unsupportedred naming the source. Worth recording that the first attempt at this check was useless: removingpollfrom a fixture source left the suite green, because the scenarios discover a poll-capable type at runtime and simply picked another. That lesson is now inconformance/NOTES.md.Follow-ups: #1417 (poll and stream gate delivery on different bases; webhook has no gate), #1418 (kitchen-sink
WALKTHROUGH.mdnever committed). Stage 8i staysINFOuntil the spec text stabilises rather than because it is red.