You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
MCP revision 2026-07-28 is a breaking, stateless redesign of the protocol (changelog). The initialize handshake and sessions are gone: every request self-describes via reserved _meta fields (io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientCapabilities), a new server/discover RPC is mandatory, every result carries a required resultType, list results carry required cache hints (ttlMs, cacheScope), and ping, sampling, roots, and the MCP logging feature are removed or deprecated. Server-initiated requests no longer exist — server→client interaction (elicitation) happens via the new MRTR pattern (resultType:"input_required"). Error codes -32020..-32099 are now spec-reserved (e.g. -32022 UnsupportedProtocolVersionError).
Our server (internal/mcpserver/, wired by cmd/mcp.go) is a hand-rolled stdio JSON-RPC server that still advertises 2024-11-05 — and echoes back any version a client requests without validating it. Goal of this issue: 100% compliance with 2026-07-28, keeping legacy clients working (dual-era).
Already compliant (no work needed)
Much of the current design survives the new spec untouched:
stdio framing: newline-delimited JSON-RPC, one UTF-8 message per line, no embedded newlines; stdout carries only protocol messages, diagnostics on stderr (which is also the spec's recommended replacement for the now-deprecated Logging feature).
Server never initiates JSON-RPC requests and never responds to notifications — both hard requirements in the MRTR world.
Accidentally stateless already: tools/call works without initialize; no per-connection context is accumulated; tools/list is deterministic and identical for every client.
Two-tier error model (tool failures as isError content blocks vs. protocol failures as JSON-RPC errors) is exactly the required split; no custom codes minted in the newly reserved -32020..-32099 range.
Tool names and inputSchemas conform (valid JSON Schema, no $ref, so the new network-dereference prohibitions are trivially met).
Does not implement the deprecated Roots, Sampling, or Logging features, nor the deprecated HTTP+SSE transport.
Shutdown (EOF + WaitGroup drain + SIGINT/SIGTERM), concurrent request handling with serialized stdout writes, and env-based credentials for stdio all match spec guidance.
Phase 1 — Modern-era core (MUST, breaking)
Implement server/discover: return resultType:"complete", supportedVersions, capabilities ({tools:{}}), ttlMs/cacheScope, and io.modelcontextprotocol/serverInfo ({name:"buttons", version}) in result._meta (SEP-2575). Currently returns -32601.
Replace the protocol-version echo in initializeResult (server.go) with an explicit supported-versions list; reject undeclared versions with -32022 UnsupportedProtocolVersionError, data: {"supported":[...], "requested":"..."}. Remove the echo assertion in server_test.go.
Parse params._meta on every request. For modern-era requests, require both io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities; reject with -32602 when missing. Capability gating is per-request, never per-connection.
Add required resultType:"complete" to every result (server/discover, tools/list, tools/call) (SEP-2322). Legacy-era initialize/ping results may stay bare.
Add required ttlMs + cacheScope to tools/list and server/discover results (CacheableResult, SEP-2549). The meta-tool surface is fixed for the process lifetime, so a generous ttlMs with cacheScope:"private" is right — document the choice.
Honor notifications/cancelled on stdio: track in-flight requests by ID with per-request context.CancelFunc; cancel the press (existing process-group kill path), suppress the response, handle the cancel-after-complete race gracefully, ignore unknown IDs. Today all notifications are ignored and a 120s press can't be aborted.
Dual-era gating: keep initialize / notifications/initialized / ping working for legacy clients only; serve any request carrying modern _meta statelessly under the new rules. Never require initialize before tools/* in either era (already true — pin with a test).
Return -32600 Invalid Request for structurally invalid requests (missing method). The constant exists but is never used; these currently fall through to -32601.
Phase 2 — SHOULDs
Include io.modelcontextprotocol/serverInfo in every modern-era result's _meta via a shared result-wrapping helper.
Unknown tool name in tools/call → -32602 "Unknown tool: " (currently -32601, which is for unknown RPC methods).
tools/list with a non-empty cursor → -32602 (we never issue cursors, so any cursor is invalid; today params are silently ignored).
Phase 3 — Conditional: Streamable HTTP (only if an --http mode ever ships)
Stdio-only is fully spec-compliant; no action while we stay there. But any future HTTP mode (plausible given the webhook listener) is gated on the 2026-07-28 Streamable HTTP bundle:
Single POST-only endpoint; 405 for GET/DELETE; 202 for notifications; JSON or request-scoped SSE responses; SSE stream close = cancellation; no SSE event IDs/resumability.
MCP-Protocol-Version header must match body _meta (mismatch = 400 + -32020 HeaderMismatch); validate mirrored Mcp-Method/Mcp-Name headers.
Origin validation (403 on invalid) against DNS rebinding; bind localhost only.
Never mint or echo Mcp-Session-Id (sessions are gone).
Auth, if any, per OAuth 2.1 + RFC 9728 Protected Resource Metadata, audience-bound tokens (RFC 8707), no token passthrough.
Phase 4 — Opportunities (MAY, not required for compliance)
structuredContent + declared outputSchema on the meta-tools (SEP-2106) — we already build JSON payloads, they're just stuffed into text blocks; keep the text block for backwards compat.
Tasks extension (io.modelcontextprotocol/tasks, SEP-2663) for long-running presses: advertise in server/discovercapabilities.extensions, return a task handle (resultType:"task") backed by existing pressed/ history + $BUTTONS_PROGRESS_PATH JSONL, add tasks/get / tasks/update / tasks/cancel, high-entropy task IDs, gated on the client declaring the extension per-request. Would lift the 120s wall for MCP clients.
MRTR elicitation for missing buttons_press args: resultType:"input_required" with a flat-primitive requestedSchema, HMAC-protected requestState (treat echoes as attacker-controlled), capability-gated, and never elicit $ENV{...}-style secrets via form mode.
subscriptions/listen + toolsListChanged only if the tool surface ever becomes dynamic (e.g. a 1:1 button:tool mode). Static meta-tool surface correctly declares nothing today.
OTel trace-context _meta passthrough (SEP-414): thread traceparent/tracestate/baggage into stderr logs and press history for correlation.
Summary
MCP revision 2026-07-28 is a breaking, stateless redesign of the protocol (changelog). The
initializehandshake and sessions are gone: every request self-describes via reserved_metafields (io.modelcontextprotocol/protocolVersion,io.modelcontextprotocol/clientCapabilities), a newserver/discoverRPC is mandatory, every result carries a requiredresultType, list results carry required cache hints (ttlMs,cacheScope), andping, sampling, roots, and the MCP logging feature are removed or deprecated. Server-initiated requests no longer exist — server→client interaction (elicitation) happens via the new MRTR pattern (resultType:"input_required"). Error codes-32020..-32099are now spec-reserved (e.g.-32022UnsupportedProtocolVersionError).Our server (
internal/mcpserver/, wired bycmd/mcp.go) is a hand-rolled stdio JSON-RPC server that still advertises2024-11-05— and echoes back any version a client requests without validating it. Goal of this issue: 100% compliance with 2026-07-28, keeping legacy clients working (dual-era).Already compliant (no work needed)
Much of the current design survives the new spec untouched:
tools/callworks withoutinitialize; no per-connection context is accumulated;tools/listis deterministic and identical for every client.isErrorcontent blocks vs. protocol failures as JSON-RPC errors) is exactly the required split; no custom codes minted in the newly reserved-32020..-32099range.$ref, so the new network-dereference prohibitions are trivially met).mcp_enabledopt-in.Phase 1 — Modern-era core (MUST, breaking)
server/discover: returnresultType:"complete",supportedVersions,capabilities({tools:{}}),ttlMs/cacheScope, andio.modelcontextprotocol/serverInfo({name:"buttons", version}) inresult._meta(SEP-2575). Currently returns-32601.initializeResult(server.go) with an explicit supported-versions list; reject undeclared versions with-32022UnsupportedProtocolVersionError,data: {"supported":[...], "requested":"..."}. Remove the echo assertion inserver_test.go.params._metaon every request. For modern-era requests, require bothio.modelcontextprotocol/protocolVersionandio.modelcontextprotocol/clientCapabilities; reject with-32602when missing. Capability gating is per-request, never per-connection.resultType:"complete"to every result (server/discover,tools/list,tools/call) (SEP-2322). Legacy-erainitialize/pingresults may stay bare.ttlMs+cacheScopetotools/listandserver/discoverresults (CacheableResult, SEP-2549). The meta-tool surface is fixed for the process lifetime, so a generousttlMswithcacheScope:"private"is right — document the choice.notifications/cancelledon stdio: track in-flight requests by ID with per-requestcontext.CancelFunc; cancel the press (existing process-group kill path), suppress the response, handle the cancel-after-complete race gracefully, ignore unknown IDs. Today all notifications are ignored and a 120s press can't be aborted.initialize/notifications/initialized/pingworking for legacy clients only; serve any request carrying modern_metastatelessly under the new rules. Never requireinitializebeforetools/*in either era (already true — pin with a test).-32600Invalid Request for structurally invalid requests (missingmethod). The constant exists but is never used; these currently fall through to-32601.Phase 2 — SHOULDs
io.modelcontextprotocol/serverInfoin every modern-era result's_metavia a shared result-wrapping helper.tools/call→-32602"Unknown tool: " (currently-32601, which is for unknown RPC methods).tools/listwith a non-emptycursor→-32602(we never issue cursors, so any cursor is invalid; today params are silently ignored).Phase 3 — Conditional: Streamable HTTP (only if an
--httpmode ever ships)Stdio-only is fully spec-compliant; no action while we stay there. But any future HTTP mode (plausible given the webhook listener) is gated on the 2026-07-28 Streamable HTTP bundle:
405for GET/DELETE;202for notifications; JSON or request-scoped SSE responses; SSE stream close = cancellation; no SSE event IDs/resumability.MCP-Protocol-Versionheader must match body_meta(mismatch =400+-32020HeaderMismatch); validate mirroredMcp-Method/Mcp-Nameheaders.403on invalid) against DNS rebinding; bind localhost only.Mcp-Session-Id(sessions are gone).Phase 4 — Opportunities (MAY, not required for compliance)
structuredContent+ declaredoutputSchemaon the meta-tools (SEP-2106) — we already build JSON payloads, they're just stuffed into text blocks; keep the text block for backwards compat.io.modelcontextprotocol/tasks, SEP-2663) for long-running presses: advertise inserver/discovercapabilities.extensions, return a task handle (resultType:"task") backed by existingpressed/history +$BUTTONS_PROGRESS_PATHJSONL, addtasks/get/tasks/update/tasks/cancel, high-entropy task IDs, gated on the client declaring the extension per-request. Would lift the 120s wall for MCP clients.buttons_pressargs:resultType:"input_required"with a flat-primitiverequestedSchema, HMAC-protectedrequestState(treat echoes as attacker-controlled), capability-gated, and never elicit$ENV{...}-style secrets via form mode.subscriptions/listen+toolsListChangedonly if the tool surface ever becomes dynamic (e.g. a 1:1 button:tool mode). Static meta-tool surface correctly declares nothing today._metapassthrough (SEP-414): threadtraceparent/tracestate/baggageinto stderr logs and press history for correlation.Testing & docs
server_test.gowire tests: modern-era_metaround-trip,-32022/-32602/-32600paths, mid-press cancellation + cancel-after-complete race, dual-era coexistence,server/discovershape.docs/buttons/mcp.mdxfor the 2026-07-28 surface; regeneratedocs/cli/buttons_mcp.mdif flags change.References
_meta)internal/mcpserver/server.go,internal/mcpserver/tools.go,cmd/mcp.go