Pure protocol-negotiation decisions for the client: which era a server speaks, which version to use, and which capabilities to advertise.
Versions
The client supports "2026-07-28", "2025-11-25" and "2025-06-18",
newest first (client_versions/0). 2026-07-28 is the modern era
(stateless, server/discover, per-request _meta); the others are
legacy (initialize handshake).
Probing (classify_probe/3)
With protocol: :auto the client probes server/discover and reads the
outcome with classify_probe/3. Which outcomes count as evidence of a
legacy server depends on the transport:
| Transport | Outcome | Verdict |
|---|---|---|
| both | valid DiscoverResult with a mutual modern version | {:modern, version, result} |
| both | DiscoverResult-shaped but invalid, or no mutual modern version | {:error, %Error{kind: :protocol}} |
| both | a result that is not DiscoverResult-shaped (no resultType, no supportedVersions) | :legacy |
| both | -32022 with a mutual modern version in data.supported | {:modern_retry, version} |
| both | -32022 without one | {:error, %Error{reason: :no_common_version}} |
| both | -32021 / -32020 | {:error, %Error{kind: :protocol}}; never a fallback |
| stdio | any other JSON-RPC error | :legacy |
| stdio | timeout | :legacy |
| stdio | the server process exited during the probe | {:reconnect, :legacy} |
| HTTP | 401 / 403 | {:authorize, status} |
| HTTP | 408 / 429 / 5xx, network error, timeout | {:retry, reason}; never a downgrade |
| HTTP | 400 / 404 / 405 with a non-modern body (empty, not JSON, not JSON-RPC, or a non-modern error) | :legacy |
| HTTP | any status with JSON-RPC -32601 | :legacy |
| HTTP | any other status or error | {:error, %Error{}} |
A recognized modern error is never treated as legacy evidence, and the fallback is never keyed to one error code. Once a connection has reached a modern verdict, the caller pins the era and does not downgrade silently.
Capabilities (client_capabilities/2)
Built only from the ConduitMcp.Client.Handler callbacks a module
implements; with no handler the client advertises %{}.
Summary
Functions
Classifies the outcome of a server/discover probe; see the moduledoc
table.
The ClientCapabilities map to advertise for handler (a module
implementing ConduitMcp.Client.Handler, or nil).
Every protocol version the client speaks, newest first.
The era of a protocol version string: dates on or after 2026-07-28 are
:modern, earlier dates :legacy, anything else :unknown.
Whether code is one of the modern-era JSON-RPC errors (-32020,
-32021, -32022) that prove the server speaks 2026-07-28.
Picks the version to use: the first entry of client_supported (in the
client's preference order) that the server also lists.
Types
@type era() :: :modern | :legacy
@type verdict() :: {:modern, String.t(), ConduitMcp.Client.DiscoverResult.t()} | {:modern_retry, String.t()} | :legacy | {:reconnect, :legacy} | {:authorize, 401 | 403} | {:retry, term()} | {:error, ConduitMcp.Client.Error.t()}
Functions
@spec classify_probe(:stdio | :http, stdio_outcome() | http_outcome(), keyword()) :: verdict()
Classifies the outcome of a server/discover probe; see the moduledoc
table.
outcome is a stdio_outcome/0 for :stdio and a http_outcome/0
for :http. An HTTP body may be the raw bytes (decoded here as JSON for
application/json, or as an event stream for text/event-stream) or an
already decoded JSON-RPC message map.
Options
:versions- the client's versions in preference order (defaultclient_versions/0); only its modern entries are considered.
The ClientCapabilities map to advertise for handler (a module
implementing ConduitMcp.Client.Handler, or nil).
Only implemented callbacks contribute, so nil or a module without
sampling, elicitation or roots callbacks gives %{}.
Options
:era-:modern(default) or:legacy; legacy roots may carrylistChanged.:sampling_tools- also declaresampling.tools(defaultfalse).:sampling_context- also declaresampling.context, allowingincludeContextother than"none"(defaultfalse).:roots_list_changed- declareroots.listChangedin the legacy era, promisingnotifications/roots/list_changed(defaultfalse).
Example
iex> ConduitMcp.Client.Negotiation.client_capabilities(nil)
%{}
@spec client_versions() :: [String.t(), ...]
Every protocol version the client speaks, newest first.
The era of a protocol version string: dates on or after 2026-07-28 are
:modern, earlier dates :legacy, anything else :unknown.
iex> ConduitMcp.Client.Negotiation.era("2025-11-25")
:legacy
Whether code is one of the modern-era JSON-RPC errors (-32020,
-32021, -32022) that prove the server speaks 2026-07-28.
Picks the version to use: the first entry of client_supported (in the
client's preference order) that the server also lists.
iex> ConduitMcp.Client.Negotiation.pick_version(["2025-06-18", "2025-11-25"])
{:ok, "2025-11-25"}
iex> ConduitMcp.Client.Negotiation.pick_version(["2024-11-05"])
{:error, :no_common_version}