///|
/// Backend selected after era probing. Modern clients speak the stateless
/// 2026-07-28 protocol; Legacy clients wrap the 2025-11-25 session client.
pub enum ClientBackend {
Modern
Legacy(@legacy.LegacyClient)
}
///|
/// Outcome of classifying a probe failure.
priv enum ProbeVerdict {
/// The server returned a recognized modern JSON-RPC error
/// (-32020/-32021/-32022). The carried error should be surfaced to the user.
ModernError(@types.MCPError)
/// The failure is not modern; fall back to a legacy client if possible.
FallbackLegacy
/// A transport-level failure that should not trigger legacy fallback.
Fatal
}
///|
/// Parse a JSON-RPC error body and return a strongly-typed MCPError when the
/// error code is one of the modern era-probe codes:
/// - -32020 HeaderMismatch
/// - -32021 MissingRequiredClientCapability
/// - -32022 UnsupportedProtocolVersion
fn parse_jsonrpc_error(body : String) -> @types.MCPError? {
let json = @json.parse(body) catch { _ => return None }
if json is Object(obj) {
match obj.get("error") {
Some(Object(err_obj)) => {
let code = match err_obj.get("code") {
Some(Number(n, ..)) => n.to_int()
_ => return None
}
let message = match err_obj.get("message") {
Some(String(s)) => s
_ => "Unknown error"
}
let data = err_obj.get("data")
match code {
-32020 => Some(@types.HeaderMismatch(message))
-32021 => {
let required = match data {
Some(Object(d)) =>
match d.get("requiredCapabilities") {
Some(Array(arr)) =>
arr.filter_map(fn(j) {
match j {
String(s) => Some(s)
_ => None
}
})
_ => []
}
_ => []
}
Some(@types.MissingRequiredClientCapability(message, required~))
}
-32022 => {
let (supported, requested) = match data {
Some(Object(d)) => {
let supported = match d.get("supported") {
Some(Array(arr)) =>
arr.filter_map(fn(j) {
match j {
String(s) => Some(s)
_ => None
}
})
_ => []
}
let requested = match d.get("requested") {
Some(String(s)) => s
_ => @types.ProtocolVersion
}
(supported, requested)
}
_ => ([], @types.ProtocolVersion)
}
Some(
@types.UnsupportedProtocolVersion(message, supported~, requested~),
)
}
_ => None
}
}
_ => None
}
} else {
None
}
}
///|
/// Classify a probe failure into one of three verdicts.
///
/// * Modern JSON-RPC errors (directly or carried in an HTTP 400 body) mean the
/// server is modern but rejected the probe; surface the error.
/// * HTTP 400 bodies that do not contain a recognized modern error, and direct
/// modern-era errors that are not transport failures, trigger legacy fallback.
/// * Non-HTTP transport failures (connection refused, invalid state, etc.) are
/// fatal and should propagate without fallback.
fn classify_probe_error(err : @types.MCPError) -> ProbeVerdict {
match err {
@types.HeaderMismatch(_)
| @types.MissingRequiredClientCapability(_, ..)
| @types.UnsupportedProtocolVersion(_, ..) => ModernError(err)
@types.TransportError(te) =>
match te {
@types.HttpError(400, body) =>
match parse_jsonrpc_error(body) {
Some(modern_err) => ModernError(modern_err)
None => FallbackLegacy
}
@types.Timeout => FallbackLegacy
_ => Fatal
}
_ => FallbackLegacy
}
}
///|
/// Build the `_meta` object used for legacy requests. It omits all
/// `io.modelcontextprotocol/*` keys and only carries a progress token when one
/// is supplied.
fn build_legacy_meta(progress_token? : String = "") -> Json {
let meta : Map[String, Json] = Default::default()
if progress_token != "" {
meta.set("progressToken", Json::string(progress_token))
}
Json::object(meta)
}
///|
/// Rewrite a modern request so its `params._meta` is suitable for a legacy
/// server. If `meta` is empty, `_meta` is removed entirely; otherwise it is
/// replaced with the provided legacy meta.
fn apply_legacy_meta_to_request(request : String, meta : Json) -> String {
let json = @json.parse(request) catch { _ => return request }
if json is Object(obj) {
match obj.get("params") {
Some(Object(params)) => {
let meta_obj = match meta {
Object(m) => Some(m)
_ => None
}
match meta_obj {
Some(m) =>
if m.is_empty() {
params.remove("_meta")
} else {
params.set("_meta", meta)
}
None => params.remove("_meta")
}
obj.set("params", Json::object(params))
}
_ => ()
}
json.stringify()
} else {
request
}
}
///|
/// Send a raw `server/discover` probe over the given transport and parse the
/// response. Returns the server capabilities on success, or a failure that can
/// be classified by `classify_probe_error`.
async fn send_discover_probe(
transport : @transport.AnyTransport,
meta : Json,
) -> Result[@types.ServerCapabilities, @types.MCPError] {
let id = 1
let request = build_discover_request(id, meta)
transport.send(request) catch {
e => return Err(@types.TransportError(e))
}
let response = transport.receive() catch {
e => return Err(@types.TransportError(e))
}
let response_str = match response {
Some(s) => s
None => return Err(@types.TransportError(@types.ConnectionClosed))
}
// Recognize modern JSON-RPC errors before the generic parser loses the code.
match parse_jsonrpc_error(response_str) {
Some(modern_err) => return Err(modern_err)
None => ()
}
match parse_jsonrpc_response(response_str) {
Err(e) => Err(e)
Ok(result_json) => {
let caps = parse_discover_capabilities(result_json)
Ok(caps)
}
}
}