///|
fn client_capabilities_to_json(
  capabilities : @types.ClientCapabilities,
) -> Json {
  let caps_map : Map[String, Json] = Default::default()
  match capabilities.roots {
    Some(roots_cap) =>
      caps_map.set("roots", { "listChanged": roots_cap.list_changed })
    None => ()
  }
  match capabilities.sampling {
    Some(_) => caps_map.set("sampling", {})
    None => ()
  }
  match capabilities.elicitation {
    Some(_) => caps_map.set("elicitation", { "form": {} })
    None => ()
  }
  match capabilities.extensions {
    Some(extensions) => {
      let ext_map : Map[String, Json] = Default::default()
      extensions.each(fn(k, v) { ext_map.set(k, v) })
      caps_map.set("extensions", Json::object(ext_map))
    }
    None => ()
  }
  Json::object(caps_map)
}

///|
/// Build the `_meta` object every modern request carries in its `params`.
/// Contains the required `io.modelcontextprotocol/protocolVersion` and
/// `clientCapabilities`, plus the recommended `clientInfo`. The optional
/// `logLevel` maps to the per-request `_meta` field that replaced the removed
/// `logging/setLevel` RPC — note the Logging feature itself is Deprecated by
/// the spec (SEP-2577: new implementations should not add support), so this
/// exists for interop with servers that still honor it, and nothing in this
/// SDK reads or emits log-level-gated messages.
fn build_request_meta(
  client_name : String,
  client_version : String,
  capabilities : @types.ClientCapabilities,
  log_level? : String? = None,
) -> Json {
  let meta : Map[String, Json] = Default::default()
  meta.set(
    "io.modelcontextprotocol/protocolVersion",
    Json::string(@types.ProtocolVersion),
  )
  meta.set(
    "io.modelcontextprotocol/clientCapabilities",
    client_capabilities_to_json(capabilities),
  )
  meta.set("io.modelcontextprotocol/clientInfo", {
    "name": client_name,
    "version": client_version,
  })
  match log_level {
    Some(level) =>
      meta.set("io.modelcontextprotocol/logLevel", Json::string(level))
    None => ()
  }
  Json::object(meta)
}

///|
fn build_tools_list_request(
  id : Int,
  meta : Json,
  cursor? : String = "",
) -> String {
  let params : Map[String, Json] = Default::default()
  params.set("_meta", meta)
  if cursor != "" {
    params.set("cursor", Json::string(cursor))
  }
  Json::object({
    "jsonrpc": "2.0",
    "id": id,
    "method": "tools/list",
    "params": Json::object(params),
  }).stringify()
}

///|
fn build_tools_call_request(
  id : Int,
  name : String,
  meta : Json,
  arguments? : String = "{}",
  progress_token? : String = "",
) -> String {
  let arguments_json = @json.parse(arguments) catch { _ => Json::object({}) }
  let params_map : Map[String, Json] = Default::default()
  params_map.set("name", Json::string(name))
  params_map.set("arguments", arguments_json)
  // Merge progressToken into the request _meta alongside protocol-level fields.
  match meta {
    Object(obj) => {
      if progress_token != "" {
        obj.set("progressToken", Json::string(progress_token))
      }
      params_map.set("_meta", meta)
    }
    _ => params_map.set("_meta", meta)
  }
  Json::object({
    "jsonrpc": "2.0",
    "id": id,
    "method": "tools/call",
    "params": Json::object(params_map),
  }).stringify()
}

///|
fn build_resources_list_request(
  id : Int,
  meta : Json,
  cursor? : String = "",
) -> String {
  let params : Map[String, Json] = Default::default()
  params.set("_meta", meta)
  if cursor != "" {
    params.set("cursor", Json::string(cursor))
  }
  Json::object({
    "jsonrpc": "2.0",
    "id": id,
    "method": "resources/list",
    "params": Json::object(params),
  }).stringify()
}

///|
fn build_resources_read_request(id : Int, uri : String, meta : Json) -> String {
  Json::object({
    "jsonrpc": "2.0",
    "id": id,
    "method": "resources/read",
    "params": { "uri": uri, "_meta": meta },
  }).stringify()
}

///|
fn build_prompts_list_request(
  id : Int,
  meta : Json,
  cursor? : String = "",
) -> String {
  let params : Map[String, Json] = Default::default()
  params.set("_meta", meta)
  if cursor != "" {
    params.set("cursor", Json::string(cursor))
  }
  Json::object({
    "jsonrpc": "2.0",
    "id": id,
    "method": "prompts/list",
    "params": Json::object(params),
  }).stringify()
}

///|
fn build_prompts_get_request(
  id : Int,
  name : String,
  meta : Json,
  arguments? : String = "{}",
) -> String {
  let arguments_json = @json.parse(arguments) catch { _ => Json::object({}) }
  Json::object({
    "jsonrpc": "2.0",
    "id": id,
    "method": "prompts/get",
    "params": { "name": name, "arguments": arguments_json, "_meta": meta },
  }).stringify()
}

///|
fn build_resources_templates_list_request(
  id : Int,
  meta : Json,
  cursor? : String = "",
) -> String {
  let params : Map[String, Json] = Default::default()
  params.set("_meta", meta)
  if cursor != "" {
    params.set("cursor", Json::string(cursor))
  }
  Json::object({
    "jsonrpc": "2.0",
    "id": id,
    "method": "resources/templates/list",
    "params": Json::object(params),
  }).stringify()
}

///|
fn build_completion_complete_request(
  id : Int,
  meta : Json,
  ref_type~ : String,
  ref_name~ : String,
  argument_name~ : String,
  argument_value~ : String,
) -> String {
  Json::object({
    "jsonrpc": "2.0",
    "id": id,
    "method": "completion/complete",
    "params": {
      "ref": { "type": ref_type, "name": ref_name },
      "argument": { "name": argument_name, "value": argument_value },
      "_meta": meta,
    },
  }).stringify()
}

///|
/// Build a `subscriptions/listen` request. The `notifications` array only
/// contains the channels the filter has enabled; `resourceSubscriptions` is
/// omitted when empty per the 2026-07-28 stateless spec.
fn build_subscriptions_listen_request(
  id : Int,
  meta : Json,
  filter : ListenFilter,
) -> String {
  let notifications : Array[Json] = []
  if filter.tools_list_changed {
    notifications.push(Json::string("notifications/tools/list_changed"))
  }
  if filter.prompts_list_changed {
    notifications.push(Json::string("notifications/prompts/list_changed"))
  }
  if filter.resources_list_changed {
    notifications.push(Json::string("notifications/resources/list_changed"))
  }
  let params : Map[String, Json] = Default::default()
  params.set("notifications", Json::array(notifications))
  if filter.resource_subscriptions.length() > 0 {
    params.set(
      "resourceSubscriptions",
      Json::array(filter.resource_subscriptions.map(Json::string)),
    )
  }
  params.set("_meta", meta)
  Json::object({
    "jsonrpc": "2.0",
    "id": id,
    "method": "subscriptions/listen",
    "params": Json::object(params),
  }).stringify()
}

///|
/// Build a `server/discover` request. Per the 2026-07-28 spec the request
/// carries no parameters beyond the standard `_meta`; it is the primary way
/// clients learn a server's supported versions, capabilities, and identity,
/// and serves as the era probe for legacy fallback.
fn build_discover_request(id : Int, meta : Json) -> String {
  Json::object({
    "jsonrpc": "2.0",
    "id": id,
    "method": "server/discover",
    "params": { "_meta": meta },
  }).stringify()
}

///|
/// Result of validating one tool's `x-mcp-header` mirror schema.
priv enum HeaderSchemaValidation {
  Valid(Array[(Array[String], String)])
  Invalid(String)
}

///|
/// Validate a tool's `inputSchema` for `x-mcp-header` mirror annotations.
/// Returns the list of `(property_path, header_name)` entries when valid, or
/// an error message describing the first violation. Per the 2026-07-28
/// streamable HTTP spec:
///
/// * header names must be non-empty HTTP `tchar`s (RFC 9110 §5.1);
/// * names are case-insensitively unique within the tool;
/// * only `integer`, `string`, and `boolean` typed properties may carry the
///   annotation (not `number`);
/// * the annotation may only appear on properties statically reachable through
///   `properties` chains; any occurrence under `items`/`oneOf`/`anyOf`/`allOf`/
///   `if`/`then`/`else`/`$ref` invalidates the whole tool.
fn validate_tool_header_schema(input_schema : Json) -> HeaderSchemaValidation {
  validate_header_schema_node(input_schema, path=[], in_properties=false)
}

///|
fn validate_header_schema_node(
  schema : Json,
  path~ : Array[String],
  in_properties~ : Bool,
) -> HeaderSchemaValidation {
  match schema {
    Object(obj) => {
      // If we are in a property context and the property itself carries the
      // annotation, validate it before descending. The validated annotation
      // becomes this node's own entry in the collected result.
      let own_entries : Array[(Array[String], String)] = []
      if in_properties {
        match obj.get("x-mcp-header") {
          Some(String(header_name)) => {
            if header_name.length() == 0 {
              return Invalid("x-mcp-header value must be non-empty")
            }
            if !is_http_token(header_name) {
              return Invalid(
                "x-mcp-header contains invalid token characters: " + header_name,
              )
            }
            match property_type_is_primitive(obj) {
              false =>
                return Invalid(
                  "x-mcp-header can only annotate integer/string/boolean properties: " +
                  path.join("."),
                )
              true => ()
            }
            own_entries.push((path, header_name))
          }
          Some(_) =>
            return Invalid(
              "x-mcp-header value must be a string: " + path.join("."),
            )
          None => ()
        }
      } else if obj.contains("x-mcp-header") {
        // Annotation outside a property (root, items, combiners, etc.).
        return Invalid(
          "x-mcp-header must appear on a property: " + path.join("."),
        )
      }

      // Collect validated annotations and detect disallowed nesting.
      let collected : Array[(Array[String], String)] = []
      let disallowed = [
        "items", "oneOf", "anyOf", "allOf", "if", "then", "else", "$ref",
      ]
      for key, value in obj {
        if key == "properties" {
          match value {
            Object(props) =>
              for prop_name, prop_schema in props {
                let child_path = path.copy()
                child_path.push(prop_name)
                match
                  validate_header_schema_node(
                    prop_schema,
                    path=child_path,
                    in_properties=true,
                  ) {
                  Valid(entries) => collected.append(entries)
                  Invalid(msg) => return Invalid(msg)
                }
              }
            _ => ()
          }
        } else if disallowed.contains(key) {
          match validate_header_schema_node(value, path~, in_properties=false) {
            Valid(entries) =>
              if entries.length() > 0 {
                return Invalid(
                  "x-mcp-header cannot appear under '" +
                  key +
                  "' (must be via properties): " +
                  entries[0].0.join("."),
                )
              }
            Invalid(msg) => return Invalid(msg)
          }
        } else if key != "x-mcp-header" {
          // Other keys are allowed, but any annotation nested inside them is
          // not reachable through a pure `properties` chain.
          match validate_header_schema_node(value, path~, in_properties=false) {
            Valid(entries) =>
              if entries.length() > 0 {
                return Invalid(
                  "x-mcp-header must be reachable through properties: " +
                  entries[0].0.join("."),
                )
              }
            Invalid(msg) => return Invalid(msg)
          }
        }
      }

      // Enforce case-insensitive uniqueness among annotations found in this
      // tool's schema. Own annotation (if any) participates too.
      own_entries.append(collected)
      let seen : Map[String, Unit] = Default::default()
      for entry in own_entries {
        let lower = entry.1.to_lower()
        if seen.contains(lower) {
          return Invalid(
            "duplicate x-mcp-header (case-insensitive): " + entry.1,
          )
        }
        seen.set(lower, ())
      }
      Valid(own_entries)
    }
    Array(arr) => {
      let collected : Array[(Array[String], String)] = []
      for item in arr {
        match validate_header_schema_node(item, path~, in_properties=false) {
          Valid(entries) =>
            if entries.length() > 0 {
              return Invalid(
                "x-mcp-header cannot appear inside an array schema at path: " +
                path.join("."),
              )
            }
          Invalid(msg) => return Invalid(msg)
        }
      }
      Valid(collected)
    }
    _ => Valid([])
  }
}

///|
fn property_type_is_primitive(obj : Map[String, Json]) -> Bool {
  match obj.get("type") {
    Some(String("integer")) => true
    Some(String("string")) => true
    Some(String("boolean")) => true
    _ => false
  }
}

///|
/// Check whether every character is an HTTP `tchar` per RFC 9110 §5.1.
fn is_http_token(s : String) -> Bool {
  let mut ok = true
  for c in s {
    if !is_http_tchar(c) {
      ok = false
      break
    }
  }
  ok
}

///|
fn is_http_tchar(c : Char) -> Bool {
  if c >= 'A' && c <= 'Z' {
    return true
  }
  if c >= 'a' && c <= 'z' {
    return true
  }
  if c >= '0' && c <= '9' {
    return true
  }
  let token_specials = "!#$%&'*+-.^_`|~"
  for s in token_specials {
    if c == s {
      return true
    }
  }
  false
}