What the specification asks of a client
created: 2026-09-30T08:00:00-05:00
last-updated: 2026-09-30T16:00:00-05:00
increment: 2
status: standard
tags: area:protocol, area:spec
description: per revision, what an MCP client MUST, SHOULD and MAY do,
including the obligations the prose implies but never states.
This is the counterpart to server-obligations.md
and follows the same conventions. It is about the specification. mcpx is a
client of every upstream server it runs, and its client behaviour is in
../protocol.md §4. Comparisons with other clients belong
in ../compare/.
“Legacy” means 2024-11-05 through 2025-11-25, and “modern” means 2026-07-28. Several obligations below attach to the host, the application that shows things to a user. The spec addresses these to clients. A proxy is a client with no user of its own, so for each of them it has to decide whether it meets the obligation itself or passes it downstream.
1. Lifecycle
Sources: 2025-11-25 lifecycle, 2026-07-28 basic, 2026-07-28 versioning.
Legacy
initializecomes first. A client MUST sendinitializebefore anything else, offering the latest version it supports.- Check the answer. If the server answers with a version the client cannot speak, the client SHOULD disconnect. The schema’s doc comment says MUST. A client that accepts whatever comes back is running a protocol it never agreed to.
- Then
notifications/initialized. After a successful result the client MUST sendnotifications/initialized. Before the server responds it SHOULD NOT send requests other than pings. - Declare what it can do. The client declares
roots,samplingand, from 2025-06-18,elicitation. What it declares is what the server may ask of it. - Never cancel
initialize.notifications/cancelledforinitializeis forbidden in every legacy revision, including after the request times out. The client closes the connection instead. - stdio shutdown. Close stdin, wait, then SIGTERM, then SIGKILL. In 2026 this rule moved to the stdio transport page.
Modern (2026-07-28)
- Every request carries
_meta. Every request MUST carry_meta["io.modelcontextprotocol/protocolVersion"]andclientCapabilities. The client capabilities apply to that request only. An empty object means none. clientInfo. A client SHOULD sendclientInfo.resultType. An absentresultTypemeans"complete". An unknown value MUST be treated as invalid, never as complete.-32022(unsupported version). The client SHOULD pick a version fromdata.supportedand retry.
Declaring capabilities per request has an effect that shows up in
multiplexing clients. The client can widen or narrow what it offers one call
at a time. For example, it can declare elicitation only on calls made while
a user is present. The server cannot hold it to an earlier declaration.
2. Finding the era
Source: 2026-07-28 versioning, backward compatibility.
The 2026 versioning page describes the probe. The result is “a property of the server”. A client SHOULD cache it for the life of the process (stdio) or the origin (HTTP), and MAY persist it across restarts of the same configuration.
stdio. Send server/discover first (SHOULD). If the answer is
anything other than a recognised modern result or modern error, fall back to
initialize, and that includes a timeout. The fallback MUST NOT be keyed
to one error code, because legacy servers answer an unknown method in
different ways.
HTTP. A dual-era client tries the steps in order:
- POST a modern request with every header. Success means the server is modern.
- A 400, 404 or 405 whose body is a JSON-RPC error with
-32020,-32021,-32022, or-32601together with 404, also means modern. Fix the request or pick a version, and retry. - Anything else: POST
initialize, which gives legacy Streamable HTTP. - If that fails with 400, 404 or 405 (the 2025-11-25 rule), GET the URL and
wait for an
endpointevent, which gives HTTP+SSE (2024-11-05).
A server that returns a bare-text 400 is therefore classified as legacy. That is the server’s fault, but the client can only act on what it received.
A pool of processes of one configuration shares one era answer. The cache
key is the configuration, not the process. A freshly spawned legacy process
still needs its own initialize, because the session is per process. A
modern one needs nothing. If a pooled process answers a different era from its
siblings, the binary changed, and the cached answer has to be dropped and the
probe run again.
3. Transports
Sources: 2024-11-05 transports, 2025-11-25 transports, 2026-07-28 stdio, 2026-07-28 Streamable HTTP, 2025-11-25 authorization.
stdio
- The client launches the server as a subprocess.
- It MUST NOT write anything to stdin that is not an MCP message.
- It SHOULD NOT treat stderr output as an error (2025-11-25 and later).
- In 2026 it SHOULD restart a server that exited and retry.
HTTP+SSE (2024-11-05)
- The client opens the SSE stream first and POSTs to the URL from the
endpointevent. - A backwards-compatible client uses this transport only as the last fallback (§2).
Streamable HTTP, legacy (2025-03-26 .. 2025-11-25)
- Accept header. POST with
Accept: application/json, text/event-stream, and handle both kinds of response. - Session id. Echo
Mcp-Session-Idon every request once it has been assigned. On a 404 for a request that carried a session id, MUST start a new session withinitialize. The client SHOULD DELETE the session when it is done. - Version header (2025-06-18 and later). Send
MCP-Protocol-Version: <negotiated version>on every request afterinitialize. This is the version the server answered, not the one the client prefers. A constant header is wrong whenever negotiation settled lower. - Disconnects. A disconnect is not a cancellation. To cancel, send
notifications/cancelledon a new POST. - Server-initiated messages. The client MAY open a GET stream for them,
and MAY resume with
Last-Event-ID.
Streamable HTTP, modern (2026-07-28)
- Headers. Send
MCP-Protocol-Versionfrom the request’s own_meta, plusMcp-Method,Mcp-Name, andMcp-Param-*for every tool parameter the server annotated withx-mcp-header.- A tool whose
x-mcp-headerannotations are invalid MUST be excluded from the client’s tool list. - A proxy that re-exports such a tool takes on both sides: it sends these headers upstream, and it validates them from its own clients.
- A tool whose
- No sessions or background streams. There is no session, no GET stream, and no resumption. A broken request is re-issued under a new id. That MUST appears only in the 2026 changelog.
- Cancellation. Closing the stream is cancellation.
Authorization (HTTP, 2025-03-26 and later)
A client that authenticates:
- follows the OAuth flow the server’s metadata describes (PKCE is REQUIRED);
- sends
Authorization: Beareron every request, never as a URL query parameter; - uses a token only for the resource it was issued for (the
resourceparameter from 2025-06-18 on).
On stdio the client passes credentials through the environment. A client
that sends static configured headers meets “never in the query” only if none
of its auth modes puts a token in the URL. mcpx’s query auth type does,
and since #240 an auth block is applied, so query sends one when declared.
4. Messages and utilities
Sources: 2025-11-25 utilities (cancellation, progress, ping), 2026-07-28 cancellation, subscriptions, caching, tasks extension.
- Ids.
- Request ids are unique within a session (legacy).
- A client answering server-to-client requests keeps its own id space for them.
- A proxy that re-issues upstream questions downstream must never let the two id spaces meet.
- Timeouts.
- A client SHOULD set a timeout on every request, and SHOULD cancel upstream when one fires.
- It MAY reset the timer on progress, but SHOULD still enforce a maximum.
- A proxy sits between two timeouts. Its upstream timeout has to be shorter than its downstream client’s, or the client gives up first and the upstream work runs on with no observer.
- A
subscriptions/listenrequest is an implied exception, because it normally never gets a response.
- Cancellation. A client MUST NOT cancel
initialize. After it cancels a request it ignores any response that still arrives. - Progress. A
progressTokenMUST be unique among active requests. In 2025-11-25, a token given to a task stays valid until the task is terminal. That can be longer than the connection that carried the original request. - Pagination.
- Cursors are opaque. The client MUST NOT parse or persist them across sessions.
- In 2026 an empty-string
nextCursoris not the end of the list. Only an absentnextCursorends it.
- Ping (legacy). A client MUST answer the server’s pings. 2026 has no
ping, so the health probe for a modern upstream is a cheap real request, andserver/discoveris cacheable and made for it. - Logging.
- Legacy:
logging/setLevelis per session. - 2026: the level is per request, in
_meta. A client that wants logs asks on each call.
- Legacy:
- Caching (2026).
- A client honours
ttlMsandcacheScope. - It MUST NOT share a
privateresult across authorization contexts. - Every page of one list carries the same
cacheScope. - An intermediary cache is bound by the same rules.
- A client honours
- Subscriptions (2026).
list_changedandresources/updatedarrive only on asubscriptions/listenstream whose filter asked for them. A client that never listens never hears about changes.- A client SHOULD check the filter in the acknowledgement against the one it requested.
- A client may open several listens, and tells them apart by
_meta.subscriptionId, which equals the listen request’s id. - Teardown arrives as a result or as a stdio
notifications/cancelled. The spec disagrees with itself on which (conflicts C-21), so a client accepts both.
- Tasks.
- A client that creates a task SHOULD store the task id durably. The handle outlives the request that created it and the connection it was created on.
- The client polls with
tasks/get, at no faster thanpollInterval. - With the 2026 extension, a client receives a task only if it declared the
extension. It has to handle
resultType: "task"on any request where it declared the extension.
5. Features the client provides
Sources: 2025-11-25 roots, sampling, elicitation; 2026-07-28 MRTR.
Roots
- Declaring. Declare
roots, withlistChangedonly if the client will sendnotifications/roots/list_changed. - Roots themselves. Roots MUST be
file://URIs. The client SHOULD validate them against path traversal and ask the user before exposing them. - 2026. Roots are requested through MRTR only. There is no change signal
and no session, so a server caches roots per request, or per
requestState.
Sampling
- Human in the loop. A client SHOULD let a human review and edit the request and the completion, and deny either. A host that auto-approves makes that choice for the user and should say so.
- Model preferences.
modelPreferencesare advisory, and hints are matched in order. - Tools and context (2025-11-25 and later).
- Declaring
sampling.toolspromises to run tool loops. A client must not declare it unless every path that can answer supports them. - A server MUST NOT send
toolsortoolChoiceto a client that did not declaresampling.tools. includeContextother than"none"is soft-deprecated, and needssampling.context.
- Declaring
- 2026.
- Sampling is deprecated, but it is fully functional during the deprecation window.
- The request travels through MRTR.
- The client SHOULD NOT retain sampling messages between requests.
Elicitation (2025-06-18 and later)
- Form mode.
- An empty
elicitation: {}means form mode only. - The requested schema is flat, made of primitives only.
- The client MUST let the user review and change a response before it is sent, and can decline or cancel.
- The client MUST show which server is asking.
- It MUST NOT ask for sensitive information through a form (2025-11-25 and later).
- An empty
- URL mode (2025-11-25 and later) requires
elicitation.url.- A client that did not declare it and receives a URL request MUST
answer
-32602. - A client that did declare it MUST show the full URL with the domain highlighted, MUST NOT open it without explicit consent, and MUST NOT prefetch it.
- URL mode exists precisely so the client never sees the secret.
-32042andnotifications/elicitation/completeexist only in 2025-11-25. The notification goes only to the client that received the elicitation.
- A client that did not declare it and receives a URL request MUST
answer
- Binding the answer to a user. Elicitation state MUST NOT be bound to session ids alone (2025-11-25), so the elicited authorization has to reach the user who started the call.
- 2026.
- Elicitation travels through MRTR only.
elicitationId, the completion notification,-32042, the session-id rule and client rate limiting are all gone.
What a proxy owes, in addition
A proxy answers these requests for upstream servers by asking its own clients. The spec does not address proxies here, but the rules above force the following:
- Check the capability per hop. Before forwarding a question downstream,
the proxy checks that that downstream declared the capability, on the
request being answered (2026), and down to the mode:
elicitation.url,sampling.toolsandsampling.contexteach need their own check. - Scope
includeContext. Through a proxy,"thisServer"means every aggregated server. The proxy forwards"none"unless it can scope the context to the originating upstream. - Translate between eras.
- Legacy upstream to modern downstream: keep the upstream’s wire request
open while the downstream receives
input_required. - Modern upstream to legacy downstream: ask each input request on the wire,
then retry upstream with the upstream’s own
requestState, byte for byte. - Never relay
-32042to a 2026 client. Convert it into a URL-mode input request. - Strip
elicitationIdfor 2026 clients, and synthesise one for 2025-11-25 clients.
- Legacy upstream to modern downstream: keep the upstream’s wire request
open while the downstream receives
- Always answer upstream. If a downstream never retries, a legacy
upstream request stays pending forever. The proxy has to answer it with an
error or
cancel. - Attribute the question and leave it intact. Name the originating
upstream. Leave
url,requestedSchema, hint order andmaxTokensuntouched. - Pooling breaks user binding. An elicited third-party authorization is bound to the proxy’s client identity. With a shared pooled upstream, one user’s URL completion becomes visible to another user’s calls. Per-user scoping is the only remedy.
6. Where mcpx stands
What mcpx does as a client is client.md and
era-probe.md, summarised in ../protocol.md
§4; the per-requirement status, each row with the test that verifies it, is
conformance-matrix.md. Of the gaps the first draft
of this page listed, the discover field names, the unchecked initialize
answer, the -32022 retry, the unknown-resultType fallback, the constant
version header, the missing Mcp-Method/Mcp-Name/Mcp-Param-* headers, the
HTTP+SSE fallback, the cancelled initialize, paging that stopped at an empty
cursor, undeclared url-mode elicitation and elicitationId reaching 2026
clients were fixed by #224, #232 and #239. What the rules above still expose:
- No OAuth (#253). The per-server
authblock is applied (#240);type: oauthis refused before connecting. samplingandelicitation.urlare declared whenever a handler is installed, and the daemon always installs one;rootsis declared and always answered with an empty list (#210).sampling.toolsandsampling.contextare not checked per hop when a question is forwarded (#210, #256).- No
progressTokenis sent upstream, so progress cannot extend a timeout (#212). - A legacy HTTP session answered
404is an error, not a reason to sendinitializeagain (#203). (stdio shutdown now closes stdin, waits, then SIGTERM, then SIGKILL, as the lifecycle page asks — #284.)