Lifecycle and versioning
created: 2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T08:00:00-05:00
increment: 1
status: standard
tags: area:compare
description: How peers agree on a revision: the legacy initialize handshake, 2026 per-request versioning, server/discover, dual-era rules.
Revisions 2024-11-05 through 2025-11-25 open every connection with an initialize handshake that fixes the version
and capabilities for the connection’s life; 2026-07-28 removes the handshake, puts version, capabilities and identity
in every request’s _meta, and adds server/discover. mcpx has to speak both eras in both directions, and two things
matter most: its modern path agrees with itself but not with the schema (protocolVersions where the field is
supportedVersions), so it cannot complete discovery with a conformant 2026 peer in either direction; and it refuses
legacy clients that the legacy rules say it must answer with a counter-offer.
Status column: mcpx @ 05c78b2. Legend and citation conventions: ../README.md.
| ID | Difference | Labels | Where it exists | mcpx @ 05c78b2 | Value | Effort | Risk |
|---|---|---|---|---|---|---|---|
| LV-01 | initialize / notifications/initialized handshake, removed in 2026 | 2024-11-05 has 2026-07-28 removes has better replacement opencode v1 has opencode v2 has | 24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 — · opencode v1 ✓ · opencode v2 ✓ (default) | ✓ — served and spoken; legacy tried first as a client | + high | S | low |
| LV-02 | InitializeRequest.params: protocolVersion, capabilities, clientInfo | 2024-11-05 has 2025-11-25 has 2026-07-28 removes | 24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ (+_meta) · 26-07 — | ✓ — all three sent; missing protocolVersion accepted | + low | S | low |
| LV-03 | 2024-11-05 is not served; the floor is 2025-03-26 | impl deferred 2024-11-05 has | 24-11 ✓ hosts exist | ✗ — deliberate floor at 2025-03-26 | + med | M | med |
| LV-04 | Server echoes a supported requested version | 2024-11-05 has 2026-07-28 removes | 24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 — | ✓ — 2025-03-26 and 2025-06-18 agreed as asked | + high | S | low |
| LV-05 | Counter-offer for an older legacy version (2024-11-05) | mcpx missing 2024-11-05 has specs conflict | 24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 — | ✗ — refused with -32022 instead of a counter-offer (#202) | + high | S | high |
| LV-06 | Counter-offer for an unknown newer version | mcpx missing 2024-11-05 has | 24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 — | ✗ — 9999-01-01 refused; future dates count as modern (#202) | + med | S | med |
| LV-07 | initialize asking for 2026-07-28 | 2026-07-28 has mcpx missing | 26-07 ✓ (dual-era rule) | ✗ — refused -32022, legacy list only; rule says counter-offer (#202) | + low | S | low |
| LV-08 | Client checks the version the server answered | specs conflict mcpx missing opencode v1 has opencode v2 has | 24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 — · opencode v1 ✓ · opencode v2 ✓ | ✗ — stores any version the server returns (#203) | + med | S | med |
| LV-09 | Version each client offers in initialize | 2025-11-25 has opencode v1 has opencode v2 has lootbox has | mcpx 2025-11-25 · opencode v1/v2 2025-11-25 · lootbox 2025-06-18 | partial — always 2025-11-25; documented override does not exist | + low | S | low |
| LV-10 | Pre-initialization restrictions (only ping, logging) | 2024-11-05 has 2026-07-28 removes | 24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 — | acc. — any method served before initialize, as 2025-03-26 | + low | S | low |
| LV-11 | “Respect negotiated version and capabilities”: SHOULD → MUST | 2025-06-18 has | 24-11 SHOULD · 25-03 SHOULD · 25-06 MUST · 25-11 MUST · 26-07 per request | partial — results downgraded; some declarations leak | + high | M | med |
| LV-12 | initialize MUST NOT be batched | 2025-03-26 has 2025-06-18 removes | 25-03 ✓ only | n/a — mcpx accepts no batches at all | + low | S | low |
| LV-13 | notifications/initialized arriving at a 2026 server | 2026-07-28 removes | 24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 — | ✓ — swallowed silently, bare initialized too | + low | S | low |
| LV-14 | instructions: InitializeResult → DiscoverResult | 2024-11-05 has 2026-07-28 has specs conflict opencode v1 has opencode v2 has | 24-11 ✓(schema) · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ (discover) · opencode v1/v2 system prompt | ✓ — sent on both; client stores both | + high | S | low |
| LV-15 | Implementation.title | 2025-06-18 has | 24-11 — · 25-03 — · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ | ✗ — not sent (optional) | + low | S | low |
| LV-16 | Implementation.description | 2025-11-25 has | 25-11 ✓ · 26-07 ✓ | ✗ — not sent (optional) | + low | S | low |
| LV-17 | Implementation.icons | 2025-11-25 has | 25-11 ✓ · 26-07 ✓ | ✗ — not sent, not read | + low | S | low |
| LV-18 | Implementation.websiteUrl | 2025-11-25 has | 25-11 ✓ · 26-07 ✓ | ✗ — not sent (optional) | + low | S | low |
| LV-19 | Per-request _meta io.modelcontextprotocol/protocolVersion | 2026-07-28 has opencode v2 has | 26-07 ✓ · opencode v2 (modern era) | ✓ — read and checked per request; client sends it | + high | S | low |
| LV-20 | Per-request _meta io.modelcontextprotocol/clientCapabilities | 2026-07-28 has opencode v2 has | 26-07 ✓ · opencode v2 (modern era) | partial — read per request; a missing key means “none” | + high | S | med |
| LV-21 | Per-request _meta io.modelcontextprotocol/clientInfo | 2026-07-28 has opencode v2 has | 26-07 ✓ (SHOULD) · opencode v2 (modern era) | ✓ — client sends it; server does not read it | + low | S | low |
| LV-22 | io.modelcontextprotocol/serverInfo in every result’s _meta | 2026-07-28 has mcpx missing | 26-07 ✓ (SHOULD) | ✗ — never emitted; client never reads it (#199) | + med | S | low |
| LV-23 | DiscoverResult has no top-level serverInfo | 2026-07-28 has mcpx missing | 26-07 (field absent) | ✗ — sends undefined field; client reads it there (#199) | + low | S | low |
| LV-24 | A 2026 request missing required _meta is malformed | 2026-07-28 has mcpx missing | 26-07 ✓ | partial — no version ⇒ served as 2025-03-26; no caps ⇒ none (#199) | + med | S | med |
| LV-25 | server/discover MUST be implemented | 2026-07-28 has opencode v2 has | 26-07 ✓ · opencode v2 (protocol auto/pinned) | partial — answered in any era; field names wrong | + high | S | high |
| LV-26 | DiscoverResult.supportedVersions | 2026-07-28 has mcpx missing | 26-07 ✓ | ✗ — protocolVersions on both server and client (#199) | + high | S | high |
| LV-27 | stdio probe order: server/discover before initialize | 2026-07-28 has impl deferred opencode v2 has | 26-07 SHOULD · opencode v2 auto | partial — legacy first by default; modern first is opt-in | + med | S | med |
| LV-28 | Which probe errors mean “modern server” | 2026-07-28 has mcpx missing | 26-07 ✓ | ✗ — only a -32022 substring counts (#200) | + med | S | med |
| LV-29 | Modern-only server refusing initialize; mcpx client aborts | 2026-07-28 has mcpx missing | 26-07 SHOULD name versions | ✗ — client aborts on -32022; never tries discover (#200) | + high | S | high |
| LV-30 | HTTP era detection from the 400 body | 2026-07-28 has | 26-07 MAY/SHOULD | partial — status ignored; body matched by substring | + med | S | med |
| LV-31 | Era is a property of the server; cache it | 2026-07-28 has | 26-07 SHOULD cache, MAY persist | partial — per connection; not persisted across restarts | + low | S | low |
| LV-32 | Per-server era setting | opencode v2 has | mcpx four values · opencode v2 three values | partial — typos silently fall back to legacy | + low | S | low |
| LV-33 | Dual-era server: era chosen per opening message | 2026-07-28 has | 26-07 MAY | ✓ — per-request _meta first, else connection handshake | + high | S | low |
| LV-34 | ping received from a modern client | 2026-07-28 removes has better replacement | 24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 — | acc. — answered {resultType:"complete"} | + low | S | low |
| LV-35 | ping sent by a client | 2024-11-05 has 2026-07-28 removes lootbox has | 24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 — · lootbox (fork) | partial — Ping() era-blind but unused; answers server pings | + low | S | low |
| LV-36 | resultType required on every 2026 result | 2026-07-28 has | 26-07 ✓ | ✓ — stamped for modern peers, stripped for legacy | + high | S | low |
| LV-37 | Request-id uniqueness: per session → among in-flight | 2026-07-28 has | 24-11..25-11 session · 26-07 in flight | ✓ — server-initiated ids are negative | + low | S | low |
| LV-38 | stdio server exits promptly on stdin EOF | 2026-07-28 has | 26-07 SHOULD | ✓ — mcpx serve exits at EOF | + med | S | low |
| LV-39 | Client stdio shutdown: close stdin, wait, SIGTERM, SIGKILL | 2024-11-05 has mcpx missing opencode v2 has | 24-11 ✓ · 25-03 ✓ · 25-06 ✓ · 25-11 ✓ · 26-07 ✓ · opencode v2 ✓ | partial — SIGTERM immediately after closing stdin (#203) | + low | S | low |
| LV-40 | Restart a stdio server that exited unexpectedly | 2026-07-28 has lootbox has | 26-07 SHOULD · lootbox (fork) · opencode v1/v2 — | ✓ — lazy respawn on next call, backoff after failures | + med | S | low |
LV-01 initialize / notifications/initialized handshake, removed in 2026
- What. In the four legacy revisions every connection opens with an
initializerequest, itsInitializeResult, and anotifications/initializednotification, and that exchange MUST be the first interaction. 2026-07-28 deletes both message types; per-request_meta(LV-19..LV-21) and the optionalserver/discover(LV-25) replace them. - Where. 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25. Not in 2026-07-28:
ClientRequestlistsDiscoverRequestand noInitializeRequest, andClientNotificationisCancelledNotificationalone. opencode v1 always initializes; opencode v2 does unless its per-serverprotocoloption says otherwise (LV-32). - mcpx @ 05c78b2. The server answers
initialize(internal/mcpserver/server.go:505-527) and swallowsnotifications/initialized(internal/mcpserver/server.go:540-541). The client initializes first by default (internal/mcpclient/client.go:231-232) and sendsnotifications/initializedafter the result (internal/mcpclient/client.go:277). - Value to mcpx. + high: every published legacy client and server uses it, and 2026 explicitly allows a server to keep answering it (LV-33).
- Effort. S — done in both directions.
- Risk. Removing it would cut off the whole legacy ecosystem; keeping it costs nothing.
- Detail. mcpx also accepts the non-standard bare method name
initialized, an accept-liberally extra. What a modern-only server does with a straynotifications/initializedis LV-13. - Sources.
2024-11-05/basic/lifecycle.mdx:40“The initialization phase MUST be the first interaction”;2026-07-28/changelog.mdx:14“remove theinitialize/notifications/initializedhandshake”;schema/2026-07-28/schema.ts:3153“export type ClientRequest =”;schema/2026-07-28/schema.ts:3166“export type ClientNotification = CancelledNotification;”;v1:patches/@modelcontextprotocol%2Fsdk@1.29.0.patch:36;v2:packages/core/src/mcp/client.ts:312;internal/mcpserver/server.go:540.
LV-02 InitializeRequest.params: protocolVersion, capabilities, clientInfo
- What.
protocolVersion: string,capabilities: ClientCapabilitiesandclientInfo: Implementationare required and unchanged in every legacy revision. 2025-11-25 moves them intoInitializeRequestParams extends RequestParams, which formally allows_metaoninitialize. - Where. 2024-11-05..2025-11-25. 2026-07-28 moves each into a per-request
_metakey (LV-19, LV-20, LV-21). - mcpx @ 05c78b2. The client sends all three (
internal/mcpclient/client.go:262-267). The server readsprotocolVersionandcapabilities(internal/mcpserver/server.go:506-517) and ignoresclientInfo. Aninitializewith noprotocolVersion, or with params that do not parse, is accepted and answered with 2025-11-25 (internal/mcpserver/server.go:848). - Value to mcpx. + low: nothing to add.
- Effort. S — done.
- Risk. None.
- Detail. The schema describes
protocolVersionas the latest version the client supports and adds “The client MAY decide to support older versions as well.” The 2026 mapping isprotocolVersion→io.modelcontextprotocol/protocolVersion(required),capabilities→io.modelcontextprotocol/clientCapabilities(required),clientInfo→io.modelcontextprotocol/clientInfo(optional, SHOULD). Accepting a missingprotocolVersionis accept-liberally: the field is required in every legacy schema. - Sources.
schema/2024-11-05/schema.ts:151“protocolVersion: string;”;schema/2025-11-25/schema.ts:255“export interface InitializeRequestParams extends RequestParams {”;2026-07-28/basic/index.mdx:375;internal/mcpserver/server.go:848.
LV-03 2024-11-05 is not served; the floor is 2025-03-26
- What. mcpx serves 2025-03-26, 2025-06-18, 2025-11-25 and 2026-07-28. A 2024-11-05 host cannot connect, and a request that declares nothing is assumed to be 2025-03-26.
- Where. 2024-11-05 hosts exist (older desktop builds, early SDKs); the spec repo still carries its schema and its HTTP+SSE transport.
- mcpx @ 05c78b2.
Supportedlists four revisions (internal/mcpserver/server.go:778) andOldestis 2025-03-26 (internal/mcpserver/revisions.go:14).docs/protocol.md:51-59records the floor as a deliberate choice. As a client mcpx still accepts a 2024-11-05 answer from an upstream (LV-08). - Value to mcpx. + med: reach to the oldest hosts, and “every revision” is the stated goal. − another
downgrade()column to maintain. - Effort. M — the version, a downgrade column (no audio, no tool annotations, no
completionscapability), and the HTTP+SSE transport if those hosts are to be reached remotely (transports.mdTR-10). - Risk. Not done: the “speak every revision” goal stays unmet for 2024-11-05.
- Detail.
docs/protocol.md:51-53says the 2025-03-26 schema “was not among” those read; it is available now and confirms the doc’s assumptions (audio, annotations andcompletionspresent; nostructuredContent,resource_link, elicitation or tasks). Serving 2024-11-05 is a separate decision from counter-offering to a 2024-11-05 client (LV-05), which needs no new column. - Sources.
internal/mcpserver/revisions.go:14“const Oldest = "2025-03-26"”;docs/protocol.md:56“as the floor”;internal/mcpserver/server.go:778.
LV-04 Server echoes a supported requested version
- What. In every legacy revision, if the server supports the version the client asked for, it MUST answer with that same version.
- Where. 2024-11-05..2025-11-25; no negotiation exists in 2026-07-28 (LV-19).
- mcpx @ 05c78b2.
negotiatereturns the requested version when it is a supported legacy one (internal/mcpserver/server.go:855) and the reply echoes it (internal/mcpserver/server.go:523), with capabilities shaped for that revision. Wire W2 agreed 2025-03-26; the stdio run S1 agreed 2025-06-18. - Value to mcpx. + high: this is the common path for every legacy host.
- Effort. S — done.
- Risk. None.
- Detail. The agreed version is stored on the connection and governs everything sent to that peer afterwards
(
internal/mcpserver/conn.go:101-105). - Sources.
2024-11-05/basic/lifecycle.mdx:126“If the server supports the requested protocol version, it MUST respond with the same”;internal/mcpserver/server.go:855; wire W2.
LV-05 Counter-offer for an older legacy version (2024-11-05)
- What. Every legacy revision says that when the server does not support the requested version it MUST still
answer
initializesuccessfully, with another version it supports (SHOULD be its latest); the client then decides whether to disconnect. Negotiation is a counter-offer, not an error. - Where. 2024-11-05..2025-11-25, identical text.
- mcpx @ 05c78b2.
negotiate()returns""for any version not inSupported(internal/mcpserver/server.go:844-859) and theinitializebranch answersunsupportedVersion(..., legacyOnly=true)(internal/mcpserver/server.go:506-509). Wire W1 (HTTP) and S1 (stdio):-32022withdata.requested: "2024-11-05"andsupported= the three legacy versions. The same failed handshake also mints anMcp-Session-Id(transports.mdTR-27).docs/protocol.md:76claimsinitializeaccepts “any legacy version”. - Value to mcpx. + high: a 2024-11-05 host would be offered 2025-11-25 and could proceed if it tolerates it; today it gets a code it has never heard of.
- Effort. S — return
Latestinstead of""for a well-formed version mcpx does not implement. - Risk. Not done: every 2024-11-05-only host fails the handshake. Done: such a host may disconnect (spec-intended), or proceed and meet 2025-era shapes it did not ask for, which is its decision to make.
- Detail. The same lifecycle pages show an “initialization error” example with
-32602anddata: {supported, requested}(errors.mdERR-10), which is why the texts conflict. The example’srequestedvalue is"1.0.0", not a date, which is one way to reconcile them: counter-offer for a well-formed version, error for garbage. - Sources.
2024-11-05/basic/lifecycle.mdx:126;2025-03-26/basic/lifecycle.mdx:132“Otherwise, the server MUST respond with another protocol version it”;2025-11-25/basic/lifecycle.mdx:170;internal/mcpserver/server.go:858return "";docs/protocol.md:76; wire W1, S1.
LV-06 Counter-offer for an unknown newer version
- What. A legacy handshake from a client newer than mcpx (for example
9999-01-01) gets the same counter-offer rule as LV-05: answer with mcpx’s own latest legacy version. - Where. Required by every legacy revision.
- mcpx @ 05c78b2. Same refusal path as LV-05. Wire W4: HTTP 200,
-32022,supported= the legacy list, and a newMcp-Session-Idheader. - Value to mcpx. + med: forward compatibility with the next client that still starts with
initialize. - Effort. S — the same change as LV-05, plus the
Modern()check below. - Risk. Not done: a future dual-era client that probes with
initializefirst falls over at the handshake. - Detail.
Modern()is a string comparison,version >= "2026-07-28"(internal/mcpserver/server.go:793), so every future date counts as “modern” and would still be refused overinitializeafter LV-05 is fixed, unless that check changes too. - Sources.
2025-11-25/basic/lifecycle.mdx:170;internal/mcpserver/server.go:793“func Modern(version string) bool { return version >= "2026-07-28" }”; wire W4.
LV-07 initialize asking for 2026-07-28
- What. 2026 has no
initialize. A dual-era server treats aninitializerequest as selecting legacy semantics, so aninitializecarryingprotocolVersion: "2026-07-28"falls under the legacy counter-offer rule and should be answered with 2025-11-25. - Where. 2026-07-28 versioning page (dual-era rules).
- mcpx @ 05c78b2. The
initializebranch passeslegacyOnly=true(internal/mcpserver/server.go:237-242,:508). Wire W3:-32022,supported: ["2025-11-25","2025-06-18","2025-03-26"].docs/protocol.md:76records the refusal as intended. - Value to mcpx. + low: either answer is consistent with “
initializemeans legacy”. A counter-offer lets a confused client proceed; the refusal’s data omits 2026-07-28, so it gives no hint to tryserver/discover. - Effort. S — the same change as LV-05.
- Risk. Low.
- Detail. The refusal code is itself 2026-only (
errors.mdERR-10). A dual-era client that receives it concludes “modern server” and retries withserver/discover, which mcpx answers, so the exchange still ends in the modern era after one wasted round trip. - Sources.
2026-07-28/basic/versioning.mdx:178“- Aninitializerequest selects legacy semantics, scoped to the stdio”;internal/mcpserver/server.go:508;docs/protocol.md:76; wire W3.
LV-08 Client checks the version the server answered
- What. Legacy prose says the client SHOULD disconnect if it does not support the version in the server’s
InitializeResult; the schema doc comment onInitializeResult.protocolVersionsays MUST. - Where. Both texts in all four legacy revisions. opencode v1 and v2 accept only a known list (2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07) and fail the connection on anything else.
- mcpx @ 05c78b2. The client stores
ir.ProtocolVersionasNegotiatedwith no check (internal/mcpclient/client.go:275)./v1/protocolshowed the test upstream atnegotiated: 2025-06-18(wire, “mcpx as client”). A 2024-11-05 answer would be accepted although mcpx implements neither that revision’s shapes nor its HTTP+SSE transport. - Value to mcpx. + med: prevents silently misparsing a server that picked a revision mcpx cannot speak, while accepting known lower versions stays accept-liberally.
- Effort. S — check against
Supportedand disconnect otherwise. - Risk. Not done: silent misparse against an old or unknown revision.
- Detail. The schema is the source of truth for a revision, so treat it as MUST. Nothing below the handshake adapts
to the accepted version either: the HTTP version header stays 2025-11-25 (
transports.mdTR-34). - Sources.
2024-11-05/basic/lifecycle.mdx:130“If the client does not support the version in the server’s response, it SHOULD”;schema/2024-11-05/schema.ts:162“If the client cannot support this version, it MUST disconnect.”;schema/2025-11-25/schema.ts:281;internal/mcpclient/client.go:275; https://unpkg.com/@modelcontextprotocol/sdk@1.29.0/dist/esm/types.js L4; https://unpkg.com/@modelcontextprotocol/core@2.0.0/dist/auth-CUe6YdwF.mjs L4–L12.
LV-09 Version each client offers in initialize
- What. Which revision a client proposes when it opens a legacy connection.
- Where. mcpx: 2025-11-25. opencode v1 (
@modelcontextprotocol/sdk1.29.0 plus a 647-line patch) and opencode v2 (@modelcontextprotocol/client2.0.0, unlessprotocolis set): 2025-11-25. lootbox (@modelcontextprotocol/sdk1.22.0): 2025-06-18 as its latest, with client capabilities{}. - mcpx @ 05c78b2.
ProtocolVersion = "2025-11-25"(internal/mcpclient/client.go:22), sent atinternal/mcpclient/client.go:263; the modern list is["2026-07-28"](internal/mcpclient/client.go:26).docs/protocol.md:39-40says 2025-03-26 and 2025-06-18 are spoken upstream “on request (protocol: force-legacy)”, butforce-legacyonly removes the fallback; it never chooses a version. - Value to mcpx. + low: offering the newest legacy revision is right. A per-server pin would matter only for servers that mishandle counter-offers.
- Effort. S — correct the doc, or add the pin it describes.
- Risk. A documented knob that does not exist is a declared-but-not-delivered bug.
- Detail. lootbox’s
capabilities: {}means an upstreamelicitation/createorroots/listgets the SDK’s default method-not-found. opencode v1’s patch moves the handshake into_initializeso it can be re-run on session expiry (transports.mdTR-29). - Sources.
internal/mcpclient/client.go:22“const ProtocolVersion = "2025-11-25"”;docs/protocol.md:39“on request (protocol: force-legacy)”;v1:packages/opencode/package.json:83;v2:packages/core/package.json:113;v2:packages/core/src/mcp/client.ts:312;.lootbox/deno.json:44;.lootbox/src/lib/external-mcps/mcp_client_manager.ts:150-153;@modelcontextprotocol/sdk@1.22.0/dist/esm/types.js:2“export const LATEST_PROTOCOL_VERSION = ‘2025-06-18’;” (the copy lootbox resolves from the local Deno npm cache).
LV-10 Pre-initialization restrictions (only ping, logging)
- What. Legacy: the client SHOULD NOT send requests other than
pingbefore theinitializeresponse, and the server SHOULD NOT send requests other thanpingand logging beforenotifications/initialized. - Where. 2024-11-05..2025-11-25; meaningless in 2026-07-28.
- mcpx @ 05c78b2. The server processes any method on a connection that never initialized and treats it as
Oldest, 2025-03-26 (internal/mcpserver/conn.go:140-146). - Value to mcpx. + low: SHOULD NOT, so serving is permitted.
- Effort. S — nothing to do.
- Risk. A modern client that forgot
_metais silently served legacy shapes (LV-24). - Detail. 2026’s stdio page notes that legacy servers which do not validate ordering answer an era-ambiguous
method under legacy semantics, which is why the modern probe treats any non-modern answer as “legacy” (LV-27,
LV-28). The dual-era case of a request with neither
_metanor a prior handshake is covered in the process-model register. - Sources.
2024-11-05/basic/lifecycle.mdx:113“The client SHOULD NOT send requests other than”;2024-11-05/basic/lifecycle.mdx:116;2026-07-28/basic/transports/stdio.mdx:144“some legacy servers do not validate that a”;internal/mcpserver/conn.go:144“v = Oldest”.
LV-11 “Respect negotiated version and capabilities”: SHOULD → MUST
- What. 2024-11-05 and 2025-03-26 say both parties SHOULD respect the negotiated version and use only negotiated capabilities; 2025-06-18 makes it MUST. 2026-07-28 restates it per request: a server MUST NOT rely on capabilities the client has not declared on that request.
- Where. MUST from 2025-06-18.
- mcpx @ 05c78b2.
Defines()anddowngrade()gate result shapes per revision (internal/mcpserver/revisions.go:115-123,:132-170). Declarations are not covered by that machinery; the capability leaks to 2025-11-25 and 2026 peers are in the capabilities register. - Value to mcpx. + high: this is mcpx’s “send conservatively” rule.
- Effort. M — the remaining leaks are spread across capabilities, lists and requests.
- Risk. Strict peers reject or misread fields they never negotiated.
- Detail. The 2025-06-18 changelog records the change as item 9.
- Sources.
2024-11-05/basic/lifecycle.mdx:162“Both parties SHOULD:”;2025-06-18/basic/lifecycle.mdx:175“Both parties MUST:”;2025-06-18/changelog.mdx:31;internal/mcpserver/revisions.go:132-170.
LV-12 initialize MUST NOT be batched
- What. With JSON-RPC batching introduced, 2025-03-26 forbids putting
initializein a batch; the sentence goes away with batching in 2025-06-18. - Where. 2025-03-26 only.
- mcpx @ 05c78b2. mcpx does not accept batches at all (
transports.mdTR-09). - Value to mcpx. + low: only relevant if 2025-03-26 batching is implemented.
- Effort. S.
- Risk. None.
- Detail. The stated reason was compatibility with 2024-11-05 peers that cannot read batches.
- Sources.
2025-03-26/basic/lifecycle.mdx:72“The initialize request MUST NOT be part of a JSON-RPC”.
LV-13 notifications/initialized arriving at a 2026 server
- What. 2026 defines no such notification, so a modern-only receiver sees an unknown notification. Notifications never get a response; on HTTP the server returns 202 if it accepts the POST or a 4xx status.
- Where. Required after
initializein 2024-11-05..2025-11-25; absent in 2026-07-28. - mcpx @ 05c78b2. Accepted silently, as are bare
initializednotifications (internal/mcpserver/server.go:540-541). The mcpx client sends it only after a legacyinitializesucceeded, so it never sends it to a modern-only server. - Value to mcpx. + low: correct for a dual-era server.
- Effort. S — done.
- Risk. None.
- Detail. 2026 HTTP says header requirements for notification POSTs are “not defined by this revision”, so a
modern-only HTTP server might 400 it for a missing
Mcp-Method; a legacy client would already have failed atinitialize. - Sources.
schema/2026-07-28/schema.ts:3166;2026-07-28/basic/index.mdx:160“The receiver MUST NOT send a response.”;2026-07-28/basic/transports/streamable-http.mdx:102;internal/mcpserver/server.go:540.
LV-14 instructions: InitializeResult → DiscoverResult
- What. Optional natural-language guidance from the server about how to use it. Legacy servers put it in
InitializeResult.instructions; 2026 servers inDiscoverResult.instructions. - Where. Present in every legacy schema including 2024-11-05, although the lifecycle prose first shows it in
2025-03-26. 2026-07-28 moves it to
DiscoverResult. opencode v1 and v2 render server instructions inside<mcp_instructions>in the system prompt; v2 also reads them fromserver/discoverand adds a code-mode hint. - mcpx @ 05c78b2. Sent in both handshakes (
internal/mcpserver/server.go:526,:537); the client stores either (internal/mcpclient/client.go:274,:322). - Value to mcpx. + high: mcpx’s whole pitch (use
mcpx_exec, not a hundred tools) reaches the model this way, and opencode puts it in front of the model on every turn. - Effort. S — done.
- Risk. Long instructions cost tokens on every opencode turn. A 2026 client that never calls
server/discover(it is optional for clients) never sees them. - Detail. 2026 tightens the wording: instructions “should not duplicate information already in tool descriptions”. opencode v1 drops servers whose tools are all permission-disabled; v2 delivers instruction changes as diffs. The schema-vs-prose gap for 2024-11-05 matters only to someone implementing from prose.
- Sources.
schema/2024-11-05/schema.ts:172“instructions?: string;”;2025-03-26/basic/lifecycle.mdx:103;schema/2026-07-28/schema.ts:696;internal/mcpserver/server.go:526;internal/mcpclient/client.go:322;v1:packages/opencode/src/session/system.ts:129;v2:packages/core/src/mcp/instructions.ts:23;v2:packages/core/src/mcp/client.ts:249.
LV-15 Implementation.title
- What.
Implementation(the type ofclientInfoandserverInfo) gains an optional displaytitlebeside the programmaticname, throughBaseMetadata. - Where.
{name, version}only in 2024-11-05 and 2025-03-26;{name, title?, version}from 2025-06-18. - mcpx @ 05c78b2. Sends
{name, version}as server and client (internal/mcpserver/server.go:525,internal/mcpclient/modern.go:84). - Value to mcpx. + low: a UI nicety.
- Effort. S.
- Risk. None; optional.
- Detail. Send conservatively: no
titleto a 2024-11-05 or 2025-03-26 peer (harmless in practice, since the object is open). - Sources.
schema/2025-06-18/schema.ts:331“export interface Implementation extends BaseMetadata {”;schema/2025-03-26/schema.ts:277“export interface Implementation {”;2025-06-18/basic/lifecycle.mdx:69.
LV-16 Implementation.description
- What. Optional free-text
descriptiononImplementation. - Where. 2025-11-25 and 2026-07-28.
- mcpx @ 05c78b2. Not sent (
internal/mcpserver/server.go:525); the client does not surface an upstream’s. - Value to mcpx. + low: an upstream’s description would improve mcpx’s catalog output.
- Effort. S.
- Risk. None.
- Detail. Of the three
Implementationfields added in 2025-11-25, this is the only one the changelog names. - Sources.
2025-11-25/changelog.mdx:25“Add optionaldescriptionfield toImplementation”;schema/2025-11-25/schema.ts:550.
LV-17 Implementation.icons
- What.
Implementationextends theIconsmixin, so a client or server can publish icons for itself. - Where. 2025-11-25 and 2026-07-28.
- mcpx @ 05c78b2. Not sent; not read from upstreams.
- Value to mcpx. + low.
- Effort. S to send; more to render safely.
- Risk. Rendering upstream icons without the icon rules below.
- Detail. Not named in the changelog; it arrives through the mixin. Consumers MUST reject icon URIs with unsafe
schemes (
javascript:,file:,ftp:,ws:, local app schemes), should fetch without credentials, and should check that icons come from the server’s origin. - Sources.
schema/2025-11-25/schema.ts:550“export interface Implementation extends BaseMetadata, Icons {”;2025-11-25/basic/index.mdx:247;2025-11-25/basic/index.mdx:251;2025-11-25/basic/index.mdx:252.
LV-18 Implementation.websiteUrl
- What. Optional
websiteUrl(@format uri) onImplementation. - Where. 2025-11-25 and 2026-07-28.
- mcpx @ 05c78b2. Not sent (
internal/mcpserver/server.go:525). - Value to mcpx. + low.
- Effort. S.
- Risk. None.
- Detail. The 2025-11-25 changelog never mentions it.
- Sources.
schema/2025-11-25/schema.ts:567“websiteUrl?: string;”.
LV-19 Per-request _meta io.modelcontextprotocol/protocolVersion
- What. Every 2026 request MUST carry its protocol version in
params._meta["io.modelcontextprotocol/protocolVersion"]. There is no negotiation step: the server accepts or rejects each request on its own, and the version of the request is the version of the response. - Where. 2026-07-28. opencode v2’s SDK stamps it on every request of a modern connection.
- mcpx @ 05c78b2. The server reads it (
requestVersion,internal/mcpserver/server.go:813-822) and refuses an unsupported one before dispatch (internal/mcpserver/server.go:469-471; the code iserrors.mdERR-09). The client adds it to every modern request (internal/mcpclient/modern.go:82). - Value to mcpx. + high: the core of the modern era.
- Effort. S — done.
- Risk. None.
- Detail. The key is non-optional in the schema. On HTTP it MUST equal the
MCP-Protocol-Versionheader: the schema doc says “400 Bad Request”, the transport page says 400 with-32020(errors.mdERR-07). Absent means malformed (LV-24). - Sources.
2026-07-28/basic/versioning.mdx:12“There is no negotiation handshake. Every request carries its protocol”;schema/2026-07-28/schema.ts:76;schema/2026-07-28/schema.ts:71-72;internal/mcpserver/server.go:469;internal/mcpclient/modern.go:82; https://unpkg.com/@modelcontextprotocol/core@2.0.0/dist/auth-CUe6YdwF.mjs L24;v2:packages/core/src/mcp/client.ts:248.
LV-20 Per-request _meta io.modelcontextprotocol/clientCapabilities
- What. Client capabilities are declared on every 2026 request;
{}means “no optional capabilities”, and the server MUST NOT carry them over from earlier requests. - Where. 2026-07-28 (legacy declares once, in
initialize). opencode v2’s SDK sends it in the modern era. - mcpx @ 05c78b2. The server builds a per-request
Peerfrom_meta(internal/mcpserver/conn.go:133-136,:150-166); if the key is missing the capabilities are nil and the request is served rather than rejected (LV-24). The client always sends its own (internal/mcpclient/modern.go:83). - Value to mcpx. + high: one connection can carry many agents with different capabilities.
- Effort. S.
- Risk. A proxy that caches capabilities per connection breaks the MUST NOT.
- Detail. The mcpx client declares
roots: {listChanged: false}in the modern era (internal/mcpclient/modern.go:52) although 2026rootsis{}and deprecated; the shape of what mcpx declares is in the capabilities register. Because capabilities are per request, mcpx could declare upstream only what the originating host declared (for example url-mode elicitation); today its declaration is fixed per client. - Sources.
schema/2026-07-28/schema.ts:96“Servers MUST NOT infer capabilities from prior requests.”;2026-07-28/basic/index.mdx:377;internal/mcpserver/conn.go:133;internal/mcpclient/modern.go:83.
LV-21 Per-request _meta io.modelcontextprotocol/clientInfo
- What. Client identity moves to optional per-request
_meta; clients SHOULD send it on every request unless configured not to. It is self-reported and display-only (meta.mdMETA-08). - Where. 2026-07-28. opencode v2’s SDK sends it in the modern era.
- mcpx @ 05c78b2. The client sends it on every modern request (
internal/mcpclient/modern.go:84). The server defines the key (internal/mcpserver/conn.go:174) but never reads it. - Value to mcpx. + low: attributing upstream calls per agent in logs is allowed; changing behaviour on it is not.
- Effort. S.
- Risk. Using it for authorization or isolation would break the SHOULD NOT.
- Detail. SEP-2575 had it required; PR #3002 made it optional after the SEP reached Final.
- Sources.
2026-07-28/basic/index.mdx:384“Clients SHOULD includeio.modelcontextprotocol/clientInfoon every request”;seps/2575-stateless-mcp.md:791;internal/mcpclient/modern.go:84;internal/mcpserver/conn.go:174.
LV-22 io.modelcontextprotocol/serverInfo in every result’s _meta
- What. A 2026 server SHOULD identify itself on every result under
result._meta["io.modelcontextprotocol/serverInfo"],DiscoverResultincluded. - Where. 2026-07-28.
- mcpx @ 05c78b2. Never emitted:
HandleOnadds onlyresultType(internal/mcpserver/server.go:432-436; wire W6, W7, W8). The client reads identity only from a top-level field (LV-23). - Value to mcpx. + med: a modern client that wants to show which server answered finds nothing, and with LV-23 it has no identity from mcpx at all.
- Effort. S — stamp it where
resultTypeis stamped, for modern peers only. - Risk. Low (SHOULD).
- Detail. Legacy results must not get it. PR #3002 removed
DiscoverResult.serverInfo“to avoid duplicate representations”. The 2026 reserved-key table omits this key although the schema defines it (meta.mdMETA-06). - Sources.
2026-07-28/basic/index.mdx:396“Servers SHOULD include the followingio.modelcontextprotocol/*field in”;schema/2026-07-28/schema.ts:157;seps/2575-stateless-mcp.md:795;2026-07-28/changelog.mdx:14;internal/mcpserver/server.go:432-436; wire W6.
LV-23 DiscoverResult has no top-level serverInfo
- What. 2026’s
DiscoverResultdefinessupportedVersions,capabilities,instructionsand the cache fields. Server identity belongs in result_meta(LV-22), not in aserverInfofield. - Where. 2026-07-28.
- mcpx @ 05c78b2. The server sends a top-level
serverInfo(internal/mcpserver/server.go:535; wire W5, W9) and the client reads it from there (internal/mcpclient/client.go:285,:320). - Value to mcpx. + low on its own; it is the other half of LV-22.
- Effort. S.
- Risk. A validator that treats the result as closed rejects it; an open one ignores it and finds no identity.
- Detail. Sending a field the peer’s revision does not define is a send-conservatively conflict.
- Sources.
schema/2026-07-28/schema.ts:678-697;internal/mcpserver/server.go:535;internal/mcpclient/client.go:285; wire W5.
LV-24 A 2026 request missing required _meta is malformed
- What. A 2026 request without
protocolVersionorclientCapabilitiesin_metais malformed; the server MUST reject it with-32602, and on HTTP with status 400. - Where. 2026-07-28.
- mcpx @ 05c78b2. Not rejected. No version: treated as legacy at 2025-03-26
(
internal/mcpserver/conn.go:134-146). Version present but no capabilities: capabilities nil, request served (internal/mcpserver/conn.go:150-166). Wire W9: a bareserver/discoverwith no_metaand no headers was answered like W5 and issued a session. - Value to mcpx. + med: conformance for modern peers. − rejecting every request without
_metawould break lenient legacy clients that skipinitialize, so reject only whenprotocolVersionis present andclientCapabilitiesis not. - Effort. S.
- Risk. As is, a modern client with a bug silently gets legacy-shaped results.
- Detail. 2026 defines behaviour for “a request carrying modern
_meta” and for “aninitializerequest”; a dual-era server receiving neither is not covered (see the process-model register). On HTTP, a server supporting pre-2025-06-18 clients MAY treat a missing version header as 2025-03-26, which matches mcpx’s default. The same malformed request can also draw-32020for the missing header; the order is unspecified (errors.mdERR-14). - Sources.
2026-07-28/basic/index.mdx:380“A request missing any required field is malformed; the server MUST reject it with”;2026-07-28/basic/index.mdx:381;internal/mcpserver/conn.go:144; wire W9.
LV-25 server/discover MUST be implemented
- What. Every 2026 server MUST answer
server/discover, which returns supported versions, capabilities, optional instructions and identity. Clients MAY call it; it is the recommended stdio probe (LV-27). The request carries nothing but_meta, yetparamsitself is required. - Where. 2026-07-28. opencode v2 calls it when a server’s
protocolisautoor pinned to2026-07-28. - mcpx @ 05c78b2. Answered in any era (
internal/mcpserver/server.go:529-538; wire W5, W9), with capabilities in the 2026 shape. The client probes it (internal/mcpclient/client.go:296). - Value to mcpx. + high: without a conformant answer no modern client can learn what mcpx speaks.
- Effort. S — the field fixes are LV-23 and LV-26.
- Risk. High while the field names are wrong: modern interop is broken in both directions.
- Detail.
DiscoverResult extends CacheableResult, sottlMsandcacheScopeare required; mcpx sends neither, and the 2026 changelog’s cacheable-result item forgets to listserver/discover(both covered in the caching register). The capability object mcpx puts in the answer, including a coretasksobject 2026 does not define, is covered in the capabilities register. Answering it on a legacy connection is accept-liberally. - Sources.
2026-07-28/server/discover.mdx:8“capabilities, and identity before sending any other requests. Servers MUST”;schema/2026-07-28/schema.ts:665-668;schema/2026-07-28/schema.ts:678“export interface DiscoverResult extends CacheableResult {”;2026-07-28/changelog.mdx:36;internal/mcpserver/server.go:529-538;v2:packages/core/src/mcp/client.ts:313.
LV-26 DiscoverResult.supportedVersions
- What. The version list in
DiscoverResultissupportedVersions: string[]; the client picks one for its subsequent requests. - Where. 2026-07-28.
- mcpx @ 05c78b2. Both halves use the wrong name. The server answers
"protocolVersions": Supported(internal/mcpserver/server.go:534); the client parsesprotocolVersions(internal/mcpclient/client.go:284), so against a conformant server its list is empty and it fails with “no shared protocol version; the server offers []” (internal/mcpclient/client.go:314-318). Tests pin the wrong name on both sides (internal/mcpserver/server_test.go:393,internal/mcpclient/era_test.go:87,internal/mcpclient/modern_test.go:88). Wire W5, W9. - Value to mcpx. + high: this is the whole “mcpx as a modern server” and “mcpx as a modern client” path.
- Effort. S, plus the three tests.
- Risk. High: mcpx↔mcpx works only because both halves are wrong the same way; every conformant modern peer fails.
- Detail. The client’s selection logic is right (newest mutually supported version, not the first listed); only
the key is wrong. The stdio probe text says to “Select a mutually supported version from
supportedVersions”. The nameprotocolVersionsmay come from an early SEP draft. Over HTTP a modern upstream fails even earlier, on the missingMcp-Methodheader (transports.mdTR-37). - Sources.
schema/2026-07-28/schema.ts:683“supportedVersions: string[];”;internal/mcpserver/server.go:534“"protocolVersions": Supported,”;internal/mcpclient/client.go:284;internal/mcpclient/client.go:314-318; wire W5.
LV-27 stdio probe order: server/discover before initialize
- What. A dual-era stdio client SHOULD send
server/discover, with its preferred modern version, before anything else: aDiscoverResultmeans modern; a recognised modern error means modern (retry with a version fromsupported, do not fall back); any other error or a timeout means legacy (fall back toinitialize). Modern-only clients are also recommended to probe. - Where. 2026-07-28. opencode v2 with
protocol: "auto"probesserver/discoverand falls back to legacy; its default is plain legacy. - mcpx @ 05c78b2. The default is the reverse,
initializefirst (internal/mcpclient/client.go:184,:231-232); modern-first is opt-in per server (LV-32). The comment atinternal/mcpclient/client.go:193-197defers the flip: “Legacy first is correct today and will stop being correct, which is why it is configurable”.docs/protocol.md:267gives the same reason. - Value to mcpx. + med: the spec’s SHOULD. − legacy first saves a round trip on today’s mostly legacy servers.
- Effort. S to flip the default, once LV-26 and LV-29 are fixed.
- Risk. A modern-only stdio server that treats a bad
initializeas fatal and exits makes legacy-first fail where probe-first works; with LV-29 unfixed, legacy-first is exactly what makes spec-following modern servers unreachable. - Detail. Legacy servers answer an unknown pre-
initializerequest with-32601,-32602or nothing, hence “or times out”. The HTTP equivalent is LV-30. Cache the result (LV-31). - Sources.
2026-07-28/basic/transports/stdio.mdx:124“legacy version that requires aninitializehandshake SHOULD probe with”;internal/mcpclient/client.go:193-197;internal/mcpclient/client.go:232;docs/protocol.md:267;v2:packages/schema/src/mcp.ts:20;v2:packages/core/src/mcp/client.ts:313.
LV-28 Which probe errors mean “modern server”
- What. A recognised modern error (for example
-32022) identifies a modern server; everything else is treated as legacy, and the legacy fallback MUST NOT be keyed to one specific error code. - Where. 2026-07-28.
- mcpx @ 05c78b2.
isVersionErrorisstrings.Contains(err.Error(), "-32022")(internal/mcpclient/client.go:327-331). A modern server’s-32020or-32021therefore triggers a legacy fallback, which a modern-only server also rejects. Whenserver/discoveritself draws-32022, the client aborts rather than retrying with a version fromdata.supported; that is moot today because mcpx knows one modern version (internal/mcpclient/client.go:26). - Value to mcpx. + med: correct era decisions against modern servers that reject for reasons other than the version.
- Effort. S — parse the error code and treat
-32020..-32022as modern. - Risk. Wrong-era fallback and a misleading reported error.
- Detail. A substring test also matches
-32022quoted anywhere in an error message. Pre-release SDKs that still send-32004are not recognised, which is the reading the 2026 allocation policy requires anyway (errors.mdERR-11). - Sources.
2026-07-28/basic/transports/stdio.mdx:139“The fallback MUST NOT be keyed to one specific error code: legacy servers”;internal/mcpclient/client.go:330“strings.Contains(err.Error(), "-32022")”;internal/mcpclient/client.go:26.
LV-29 Modern-only server refusing initialize; mcpx client aborts
- What. A modern-only server rejects
initializewith an implementation-defined JSON-RPC error and SHOULD name the versions it supports, since a legacy client has no way to fall forward. A dual-era client that tried legacy first should fall forward toserver/discover. - Where. 2026-07-28 versioning page and its compatibility matrix.
- mcpx @ 05c78b2.
newClientchecksisVersionErrorafter the first attempt regardless of which era it was (internal/mcpclient/client.go:244-249); the comment there assumes the first attempt was modern. So a modern server that answersinitializewith-32022, as mcpx’s own server does, makes the client give up. The era tests use a fake that answersinitializewith-32601, so the case is untested (internal/mcpclient/era_test.go:66-69). - Value to mcpx. + high: reachability of every modern-only server that follows the SHOULD.
- Effort. S — short-circuit only when the first attempt was modern.
- Risk. Not done: mcpx cannot connect to spec-following modern-only servers with its default settings.
- Detail. Over HTTP the mcpx client’s
initializecarriesMCP-Protocol-Versionbut noMcp-Method, so a modern-only HTTP server rejects it with 400 before method dispatch. If its header check runs first the body is-32020, which does not contain-32022, so the client does fall forward, and then fails again on the missingMcp-Method(transports.mdTR-37); if its version check runs first the body is-32022and the client aborts as above. The spec does not order the two checks (errors.mdERR-14). - Sources.
2026-07-28/basic/versioning.mdx:154“A server that supports only modern versions SHOULD name”;2026-07-28/basic/versioning.mdx:170;internal/mcpclient/client.go:246“if isVersionError(err) {”;internal/mcpclient/era_test.go:66-69.
LV-30 HTTP era detection from the 400 body
- What. On Streamable HTTP a dual-era client MAY send a modern request first. On a 400 it SHOULD inspect the body:
a recognised modern JSON-RPC error means modern (fix and retry); an empty or unrecognised body means fall back to
initialize, and possibly further to HTTP+SSE (transports.mdTR-11). - Where. 2026-07-28.
- mcpx @ 05c78b2. The client uses the stdio logic (legacy first) and never consults the status for era
(
internal/mcpclient/client.go:235-258). A non-2xx answer becomes an error carrying the status and up to 4 KiB of body (internal/mcpclient/http.go:136-140), whichisVersionErrorthen greps. - Value to mcpx. + med: as a server, mcpx must return 400 with a JSON-RPC body for modern errors for this to work
in its favour; it returns 200 (
errors.mdERR-13) and a plain JSON body for header mismatches (errors.mdERR-07). - Effort. S.
- Risk. Dual-era clients misclassify mcpx, and mcpx misclassifies servers that reject for header reasons.
- Detail. Modern servers use 400 for
-32602(missing_meta),-32020,-32021and-32022, and 404 with-32601for unknown methods. - Sources.
2026-07-28/basic/transports/streamable-http.mdx:654“era the server implements by attempting a modern request first. On”;2026-07-28/basic/transports/streamable-http.mdx:660;internal/mcpclient/http.go:136-140.
LV-31 Era is a property of the server; cache it
- What. Era belongs to the server, not the request. Clients SHOULD cache it for the life of a stdio process or per HTTP origin, MAY persist it across restarts, and re-probe if the assumption fails.
- Where. 2026-07-28.
- mcpx @ 05c78b2.
Client.Erais set once per connection (internal/mcpclient/client.go:237,:257); each new client, one per pool instance, probes again, and nothing persists across daemon restarts./v1/protocolreports each server’s era, negotiated version and preference (internal/daemon/routes_proto.go:515). - Value to mcpx. + low: mcpx spawns many instances of one server, and each repeats the probe.
- Effort. S.
- Risk. None.
- Detail. For HTTP the key is the origin, not the URL path.
- Sources.
2026-07-28/basic/versioning.mdx:148“The era determination is a property of the server, not of an individual”;internal/mcpclient/client.go:237;internal/daemon/routes_proto.go:515.
LV-32 Per-server era setting
- What. A per-upstream setting that chooses or forces the era.
- Where. mcpx:
protocol: legacy | modern | force-legacy | force-modern. opencode v2:protocol: "legacy" | "auto" | "2026-07-28", default legacy; a pinned version the server does not support fails with a message naming the versions it does, and a pin must be a modern revision. opencode v1 has no such option. - mcpx @ 05c78b2. Parsed at
internal/config/config.go:142-146and mapped atinternal/pool/pool.go:636-646, whosedefault:branch turns any unknown value, typos included, intolegacy; config loading validates only command and URL (internal/config/config.go:701-708). - Value to mcpx. + low: lets a user pin an era; typos should fail loudly.
- Effort. S — validate at load.
- Risk. Low: a misspelt
protocolsilently probes legacy first. - Detail.
docs/opencode-plugin.md:187says opencode v2 “negotiating2026-07-28per server” will exercise mcpx’s modern path; that happens only when the user setsprotocolfor mcpx, because v2 defaults to legacy. - Sources.
internal/pool/pool.go:645“return mcpclient.PreferLegacy”;internal/config/config.go:142-146;v2:packages/schema/src/mcp.ts:20“Schema.Literals(["legacy", "auto", "2026-07-28"])”;v2:packages/schema/src/mcp.ts:23;docs/opencode-plugin.md:187.
LV-33 Dual-era server: era chosen per opening message
- What. A request carrying modern
_metais served statelessly under 2026; aninitializeselects legacy semantics scoped to that stdio process or HTTP session; a dual-era server MAY serve both eras concurrently on one endpoint or process. - Where. 2026-07-28.
- mcpx @ 05c78b2. The peer is resolved per request, from
_metafirst and the connection’s handshake otherwise (internal/mcpserver/conn.go:133-147); legacy capabilities are stored per connection (internal/mcpserver/conn.go:101-105). - Value to mcpx. + high: this is the “speak every revision” goal.
- Effort. S — done.
- Risk. None.
- Detail. The rule is per request, so on stdio a request with modern
_metathat follows aninitializeon the same process is still served statelessly; mcpx does this because_metais checked first. The gap for a request with neither is covered in the process-model register. - Sources.
2026-07-28/basic/versioning.mdx:176“- A request carrying modern per-request_metais served statelessly”;2026-07-28/basic/versioning.mdx:182;2026-07-28/basic/versioning.mdx:130;internal/mcpserver/conn.go:133.
LV-34 ping received from a modern client
- What. Legacy
ping(either side; the receiver MUST answer promptly with{}) is removed in 2026 in both directions: servers cannot send requests, any request proves liveness client to server, and transport keep-alives do the rest. - Where. 2024-11-05..2025-11-25. Absent from the 2026 schema altogether: removed, not deprecated.
- mcpx @ 05c78b2. The server answers
pingin any era (internal/mcpserver/server.go:543-544); a modern one gets{resultType: "complete"}(wire W7). - Value to mcpx. + low: harmless accept-liberally.
- Effort. S — done.
- Risk. None;
docs/protocol.md:78is the statement of intent. - Detail. A modern-only server answers
pingwith-32601(404 on HTTP). 2026 recommends SSE comment lines as keep-alive on long streams (transports.mdTR-24). - Sources.
2024-11-05/basic/utilities/ping.mdx:28“1. The receiver MUST respond promptly with an empty response:”;2026-07-28/changelog.mdx:20“Removeping,logging/setLevel, andnotifications/roots/list_changed.”;seps/2575-stateless-mcp.md:569;internal/mcpserver/server.go:543; wire W7.
LV-35 ping sent by a client
- What. A client may ping a legacy server as a liveness check; against a 2026 server the method does not exist.
- Where. 2024-11-05..2025-11-25. lootbox’s health monitor pings every server periodically and reconnects with backoff (fork only; the deployed upstream build has no monitor).
- mcpx @ 05c78b2.
Client.Pingsendspingregardless of era (internal/mcpclient/client.go:726-728), but nothing in the pool or daemon calls it. The client does answer a server’spingin any era (internal/mcpclient/modern.go:110-115), which it once answered with method-not-found. - Value to mcpx. + low: a probe could catch a wedged stateful server. − pinging an idle, lazily started server
keeps it alive and defeats
idleTimeout; pinging a modern upstream gets-32601, a false “dead”. - Effort. S — gate
Ping()on the legacy era if it is ever used. - Risk. Low while unused.
- Detail. For a modern upstream the equivalent check is
server/discoveror nothing. - Sources.
internal/mcpclient/client.go:726-728;internal/mcpclient/modern.go:110-115;.lootbox/README.md:385-388“Lootbox automatically monitors MCP server health with periodicping()”.
LV-36 resultType required on every 2026 result
- What. Every 2026 result MUST carry
resultType:"complete","input_required", or an extension value advertised through capabilities. An unrecognised value MUST be treated as invalid; an absent one (legacy servers) MUST be treated as"complete". - Where. 2026-07-28.
- mcpx @ 05c78b2. Stamped
"complete"on modern results that lack one (internal/mcpserver/server.go:432-436,:444-456), deleted below 2026 (internal/mcpserver/revisions.go:163-168); wire W6, W7. The client reads it to spotinput_required(internal/mcpclient/client.go:663-665). - Value to mcpx. + high: done.
- Effort. S — done.
- Risk. None for ordinary results.
- Detail. The TypeScript type is open (
"complete" | "input_required" | string) while the prose makes unknown values invalid. Error responses carry noresultType, correctly. The task-creation result stamped"complete"where the tasks extension says"task"is in the tasks register. - Also recorded from the content types register. Clients treat an absent value as
"complete"and an unrecognised one as invalid. The server stampscompletefor modern peers (internal/mcpserver/server.go:432-436) anddowngrade()deletes the field for anyone below 2026-07-28 (internal/mcpserver/revisions.go:163-168). One of the two branches that fire in production (CT-12). The deletion happens on a copy (internal/mcpserver/revisions.go:139-145), so a stored task result handed out twice is not mutated. - Sources.
2026-07-28/basic/index.mdx:73“TheresultMUST include aresultTypefield”;2026-07-28/basic/index.mdx:84;schema/2026-07-28/schema.ts:234“resultType: ResultType;”;schema/2026-07-28/schema.ts:216;internal/mcpserver/server.go:452-454.;2026-07-28/basic/index.mdx:85“clients MUST treat an absentresultTypeas\"complete\".”;internal/mcpserver/server.go:439“resp.Result = downgrade(resp.Result, peer.Version)”
LV-37 Request-id uniqueness: per session → among in-flight
- What. Legacy: a request id MUST NOT have been used before by the requestor within the same session. 2026: an id MUST NOT match any other request the sender has issued and not yet had answered.
- Where. Session-scoped in 2024-11-05..2025-11-25; in-flight-scoped in 2026-07-28.
- mcpx @ 05c78b2. Server-initiated ids are negative so they cannot collide with a client’s
(
internal/mcpserver/conn.go:239-244). - Value to mcpx. + low: a pooled proxy may reuse ids after completion under 2026, never under legacy.
- Effort. S.
- Risk. None.
- Detail.
subscriptionIdis the listen request’s id, so reusing an id while a listen stream is open is forbidden anyway (it is still in flight). MRTR retries MUST use a new id. Ids MUST NOT benullin every revision. - Sources.
2024-11-05/basic/messages.mdx:28“- The request ID MUST NOT have been previously used by the requestor within the same”;2026-07-28/basic/index.mdx:48;seps/2567-sessionless-mcp.md:168;internal/mcpserver/conn.go:240.
LV-38 stdio server exits promptly on stdin EOF
- What. 2026 makes stdin EOF the primary portable shutdown signal: a server SHOULD exit promptly when its standard input is closed.
- Where. 2026-07-28 stdio page (legacy revisions describe only the client’s side, LV-39).
- mcpx @ 05c78b2.
mcpx serveexited at stdin EOF in every recorded stdio run; the read loop ends when the scanner does (internal/mcpserver/server.go:1000-1073). - Value to mcpx. + med: hosts that only close the pipe get a clean exit.
- Effort. S — done.
- Risk. None.
- Detail. 2026 also gives Windows equivalents for the signals.
- Sources.
2026-07-28/basic/transports/stdio.mdx:102“Servers SHOULD exit promptly when their standard input is closed or reads”;internal/mcpserver/server.go:1000-1073; wire S1–S5.
LV-39 Client stdio shutdown: close stdin, wait, SIGTERM, SIGKILL
- What. The client SHOULD close the server’s stdin, wait for it to exit, send SIGTERM if it does not exit in reasonable time, then SIGKILL.
- Where. Legacy lifecycle pages; the 2026 stdio page keeps the sequence. opencode v2 follows it (close stdin,
wait 2 s, SIGTERM, SIGKILL after 2 s, whole process group). opencode v1 instead SIGTERMs descendants found with
pgrep -Pand closes the client, and only on instance disposal. - mcpx @ 05c78b2. Closes stdin and immediately SIGTERMs the process group, then SIGKILLs after
plumbing.stdioDrainGrace(3 s) (internal/mcpclient/stdio.go:197-209;internal/defaults/defaults.json:40). - Value to mcpx. + low: servers that flush state on EOF get their chance.
- Effort. S — wait for exit (bounded by the grace setting) before SIGTERM.
- Risk. A server that saves on stdin EOF may be killed mid-write.
- Detail. Using the process group means grandchildren die too, which matters for servers that fork a browser
(
internal/mcpclient/stdio.go:69). - Sources.
2024-11-05/basic/lifecycle.mdx:178“1. First, closing the input stream to the child process (the server)”;2025-11-25/basic/lifecycle.mdx:233-236;internal/mcpclient/stdio.go:202;v2:packages/core/src/mcp/stdio.ts:10;v2:packages/core/src/mcp/stdio.ts:57;v1:packages/opencode/src/mcp/index.ts:541;v1:packages/opencode/src/mcp/index.ts:546.
LV-40 Restart a stdio server that exited unexpectedly
- What. 2026: if the server process exits unexpectedly the client SHOULD restart it; in-flight requests are lost
and listen streams are re-established. Statelessness makes the restart free; a legacy server needs a fresh
initialize. - Where. 2026-07-28. lootbox’s fork reconnects with exponential backoff behind a circuit breaker. opencode v1 and v2 never respawn: the server is marked failed (“Connection closed”), its tools disappear, and recovery is a manual reconnect (v2 also reconnects when that server’s integration credential changes).
- mcpx @ 05c78b2. Dead instances are reaped (
internal/pool/pool.go:344-356) and the next call starts a fresh one, which probes and initializes again; a failed start sets a cooldown that grows byplumbing.restartBackoffStep(2 s) up toplumbing.restartBackoffMax(30 s) (internal/pool/pool.go:205-208;internal/defaults/defaults.json:49-50). - Value to mcpx. + med: as a client this works. As a server it matters the other way round: if
mcpx servedies, opencode loses mcpx until the user reconnects, so the stdio side should outlive daemon restarts. - Effort. S.
- Risk. A daemon upgrade that kills
mcpx servesilently removes mcpx from every open opencode session. - Detail. The cap is triggered by an inline
30*time.Secondrather than by the setting (internal/pool/pool.go:206): the computed backoff is replaced byrestartBackoffMaxonly once it exceeds 30 s, so arestartBackoffMaxbelow 30 s is not a cap. - Sources.
2026-07-28/basic/transports/stdio.mdx:111“If the server process exits unexpectedly, the client SHOULD restart it.”;internal/pool/pool.go:344-356;internal/pool/pool.go:205-208;v1:packages/opencode/src/mcp/index.ts:448;v2:packages/core/src/mcp/index.ts:355;.lootbox/README.md:385-388.