Skip to content

events: four wire divergences from the merged design sketch, found by the new conformance suite #1380

Closed

Description

@panyam

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.events when any event source is registered, with listChanged reflecting 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 delivery on the events/poll path, answering -32014 Unsupported with data naming the mode.

Not in scope here

  • G31 (nextPollSeconds / maxAge in 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's delivery: 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

https://claude.ai/code/session_017kyEZdgh1zjdhURgdcZ4nL

Activity

  1. panyam commented on Sep 15, 2026

    @panyam
    OwnerAuthor

    The suite that found these is now a draft upstream: modelcontextprotocol/conformance#504. The fork PR is closed; review happens there.

  2. panyam commented on Sep 15, 2026

    @panyam
    OwnerAuthor

    Re-ran against main at c9160cff, 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 descriptors events/list now returns, only events.topology is 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.topology meta-source, which does not keep the descriptor contract at all: delivery: null, no inputSchema, and it answers events/poll despite advertising no poll delivery. That reads less like three bugs and more like one question — whether a meta-source should appear in events/list as 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.events is still never declared, which keeps the whole surface undiscoverable and is still the one I would fix first. events/poll still omits events rather 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-discovery 8/11, events-poll 19/29.

  3. panyam commented on Sep 22, 2026

    @panyam
    OwnerAuthor

    All six closed by #1416 (merged 02fea1bd). Verified on merged main:

    events-discovery: Passed: 12/12, 0 failed, 1 warnings
    events-poll:      Passed: 29/29, 0 failed, 4 warnings
    
    Check Closed by
    capability-events-object top-level capabilities.events, listChanged: true, on both wires
    poll-events-array events always an array, guaranteed in MarshalJSON
    poll-mode-unsupported events/poll gates on the declared delivery, -32014
    descriptor-delivery-subset delivery derived at registration when a source declares none
    descriptor-input-schema schemas on the three kitchen-sink sources and on events.topology
    poll-invalid-arguments the same schemas, which made argument validation gradeable at all

    Two of these were scoped out of this issue and could not stay out

    events.topology's delivery: null and missing inputSchema were filed here as entangled with G34. Enforcing delivery on the poll path made that impossible to defer: a source advertising delivery: null stops 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/list ranged a map, so its order flapped between calls. Invisible while the one unpollable source was never a selection candidate; the moment events.topology became 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.
    • No kitchen-sink source declared an inputSchema at all. This issue and the suite's own yaml both attributed that row to events.topology; the failure was on chat.message. Argument validation had never been graded by anything.
    • sep-9999-poll-invalid-arguments reports 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 EventDef literals 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. Read Registry.Def(name), not src.Def(), anywhere the answer has to match what events/list published.

    Verified it still grades

    Disabling the poll gate turns sep-9999-poll-mode-unsupported red naming the source. Worth recording that the first attempt at this check was useless: removing poll from 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 in conformance/NOTES.md.

    Follow-ups: #1417 (poll and stream gate delivery on different bases; webhook has no gate), #1418 (kitchen-sink WALKTHROUGH.md never committed). Stage 8i stays INFO until the spec text stabilises rather than because it is red.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    protocolMCP protocol features and compliance

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions