Add rationale for three delivery modes to Key Design Decisions

Records why poll, push, and webhook all exist as protocol-level modes
rather than picking one: the deployment topologies are disjoint and no
mode subsumes another. Acknowledges the departure from the usual
one-way-to-do-it stance and the spec/SDK cost it carries, and notes the
rejected alternative (one mode + proxy synthesis) and why.
This commit is contained in:
Peter Alexander 2026-05-14 18:36:25 +01:00
commit 754be6ce47
No known key found for this signature in database

View file

@ -882,6 +882,7 @@ These are the choices where reasonable alternatives existed; recorded so reviewe
| Decision | Choice | Rationale |
|---|---|---|
| Number of delivery modes | Three — poll, push, webhook | This goes against MCP's usual stance of offering one way to do a thing, and the cost is real: roughly 3× spec and SDK surface, two cursor placements, two subscription lifecycles. We pay it because the deployment topologies events must reach are disjoint, and dropping any mode strands a real population. **Poll** is the floor: it works for stateless/serverless servers with no outbound HTTP and no long-lived connections, and for clients behind NAT with no public endpoint — but at interval-bounded latency. **Push** gets low latency for stdio servers, local agents, and HTTP clients that can hold a stream open — but a server must keep a connection per subscription, which scale-to-zero and connection-capped deployments cannot. **Webhook** gets low latency without a held connection — but requires the server to make outbound HTTP and the client (or a proxy acting for it) to expose a reachable endpoint. No mode subsumes another: a serverless server can't push, a NAT'd client can't receive webhooks, a battery- or cost-sensitive client shouldn't poll. The alternative — define one mode and let proxies synthesize the rest — was rejected because it forces an always-on, trusted intermediary (holding cursors and webhook secrets) into every deployment that doesn't fit the chosen mode, which is most of them. May revisit if real deployments converge on a subset. |
| Mandatory delivery mode | None — servers advertise any non-empty subset of poll/push/webhook | Universal compatibility would be nice, but mandating any single mode places an unacceptable burden on implementers (poll needs a history or buffer; push needs a held connection; webhook needs outbound HTTP + SSRF hardening). The extension is already opt-in. May revisit if deployments converge on a common subset in practice. |
| Webhook signature scheme | Adopt Standard Webhooks as a profile (`webhook-id`/`-timestamp`/`-signature`, `v1,` HMAC, `whsec_` secrets, multi-sig rotation) | Gets off-the-shelf verifier libraries in most languages and a defined asymmetric (`v1a,`) path for free. Trade-off is a dependency on an external community spec, but it's small, stable, pinnable, and not a one-way door. |
| Batched poll/stream | One subscription per `events/poll`/`events/stream` request | Batching is a transport-level concern; HTTP/2 multiplexing makes per-subscription requests cheap. Avoids the protocol complexity batching introduces (coalescing heterogeneous `nextPollMs`, partial-failure result shapes, per-entry error routing, duplicate-id handling). Clients with many subscriptions on HTTP/1.1 should expect to coalesce at the transport, not the protocol. |