D129 — Report the protocol era and client identity of connected clients¶
Decision. Each *Server keeps a bounded in-memory tally of the clients that reach it, keyed by
client name, client version, protocol version and era (internal/mcpserver/clients.go). Identity is
read from _meta.io.modelcontextprotocol/clientInfo in the 2026-07-28 era and from
initialize's params.clientInfo in the handshake era — one shared parseClientInfo, called from
resolveEra and handleInitialize. A handshake-era request that is not initialize carries no
identity and is counted under the literal client name unknown. Recording happens in
dispatchMethod after the metadata authorization check, so a denied caller records nothing, and for
initialize after the handler has parsed clientInfo and settled the negotiated version. The tally
is capped at 64 distinct keys with each identity field truncated to 64 bytes (rune-safe); beyond the
cap a single overflow counter increments and existing keys keep counting. It is exposed by
GET /clients on all three handler constructors (HTTPHandler, FullHTTPHandler, the multi-KB
Handler), as one flat array across mounted KBs sorted by (kb, client name, version), 405 on any
method other than GET, {"clients":[]} and 200 on a fresh server. /clients is not added to
isPublicPath, so it inherits the bearer requirement.
Rationale. D128 kept the handshake era alive precisely because it is not established which
revision the real clients speak, and D130 cannot be scheduled on a guess: retiring an era needs
evidence that every client of a deployment has moved. That evidence was unobtainable — initialize
read protocolVersion into a local variable and dropped clientInfo entirely, and the audit log
covers only tools/call, so the one signal needed was structurally outside it. The audit log was
rejected as the home for this anyway: it is a signed hash chain, not the place for a soft
operational counter. The endpoint is separate from /health because /health is deliberately
exempt from auth for k8s probes, and client names and versions are deployment topology; extending it
would have published them unauthenticated. No MCP tool was added: this is operator-facing, an agent
has no use for the client roster of its own server, and every tool costs context in every KB's
tools/list.
Consequences. The question "which clients talk to this server, on which protocol version" is now
answerable on any deployment, which is what unblocks D130 — and it recurs on every future revision,
not just this one. The data is process-local and lost on restart, deliberately: durable history is
the audit log's job. Identity stays self-reported and unverified, and must never become an
authorization input — stated here so nothing is later built on it. D128's "no session state"
invariant holds: the tally is aggregate observability keyed by client identity, never by connection;
it mints no identifier and is never echoed to a client, so two requests from the same client remain
indistinguishable to the protocol. Recording is a side effect on the request path with no failure
path — hitting the cap is invisible to the caller. A hostile client can burn the 64 key slots, which
degrades the roster to an overflow count but cannot grow memory; an operator who sees overflow
climb should read it as noise, not as topology.