///| Pure-logic JSON-RPC / SSE framing helpers shared by all targets.
/// Split from http_server.mbt so js (and wasm-gc) keep these APIs even
/// though the transport I/O itself is gated.
///|
/// Classification of JSON-RPC messages arriving on the server-side reply
/// queue. Used to decide whether a POST response is a single JSON object or
/// an SSE stream.
pub(all) enum JsonRpcKind {
Notification
FinalResponse
Other
} derive(Eq, Debug)
///|
/// Returns `true` when the JSON body is a JSON-RPC notification
/// (`method` present, `id` absent).
pub fn is_notification(body : Json) -> Bool {
if body is Object(obj) {
obj.get("method") is Some(_) && obj.get("id") is None
} else {
false
}
}
///|
/// Classify a serialized JSON-RPC message for response-mode selection.
pub fn classify_jsonrpc_message(message : String) -> JsonRpcKind {
let json = @json.parse(message) catch { _ => return Other }
if json is Object(obj) {
let has_method = obj.get("method") is Some(_)
let has_id = obj.get("id") is Some(_)
let has_result = obj.get("result") is Some(_)
let has_error = obj.get("error") is Some(_)
if has_method && !has_id {
Notification
} else if has_id && (has_result || has_error) {
FinalResponse
} else {
Other
}
} else {
Other
}
}
///|
/// Extract the JSON-RPC error code from a serialized response, if any.
pub fn jsonrpc_error_code(message : String) -> Int? {
let json = @json.parse(message) catch { _ => return None }
if json is Object(obj) {
match obj.get("error") {
Some(Object(err)) =>
match err.get("code") {
Some(Number(n, ..)) => Some(n.to_int())
_ => None
}
_ => None
}
} else {
None
}
}
///|
/// Format a JSON-RPC payload as one SSE event line.
pub fn sse_event_line(json : String) -> String {
"data: " + json + "\n\n"
}
///|
/// Extract the JSON-RPC `id` member from a serialized request body, for
/// echoing in transport-level error responses. Returns `None` when the body
/// is not a JSON object or carries no `id` — JSON-RPC allows omitting `id`
/// when it cannot be determined from the (possibly malformed) request.
pub fn request_id_from_body(body : String) -> Json? {
let json = @json.parse(body) catch { _ => return None }
if json is Object(obj) {
obj.get("id")
} else {
None
}
}
///|
/// Serialize a JSON-RPC 2.0 error response body for a transport-level
/// rejection, echoing the request `id` extracted from `body` when readable.
fn jsonrpc_error_body(error : @types.MCPError, body~ : String) -> String {
let error_obj : Map[String, Json] = Default::default()
error_obj.set("code", Json::number(error.to_error_code().to_double()))
error_obj.set("message", Json::string(error.message()))
match error.to_error_data() {
Some(data) => error_obj.set("data", data)
None => ()
}
let response : Map[String, Json] = Default::default()
response.set("jsonrpc", Json::string("2.0"))
response.set("error", Json::object(error_obj))
match request_id_from_body(body) {
Some(id) => response.set("id", id)
None => ()
}
Json::object(response).stringify()
}
///|
/// Build a `HeaderMismatch` (-32020) JSON-RPC error response body. The
/// request `id` is echoed when readable from `body`.
pub fn header_mismatch_error(detail : String, body~ : String) -> String {
jsonrpc_error_body(@types.HeaderMismatch(detail), body~)
}
///|
/// Build an `Invalid params` (-32602) JSON-RPC error response body for a
/// malformed request rejected at transport entry (e.g. a request whose
/// `params._meta` is missing required fields). The request `id` is echoed
/// when readable from `body`.
pub fn invalid_params_error(detail : String, body~ : String) -> String {
jsonrpc_error_body(@types.InvalidParams(detail), body~)
}
///|
/// Build an `UnsupportedProtocolVersion` (-32022) JSON-RPC error response
/// body. The request `id` is echoed when readable from `body`; the error is
/// transport-level, so unreadable ids are simply omitted.
pub fn unsupported_protocol_version_error(
requested : String,
body~ : String,
) -> String {
jsonrpc_error_body(
@types.UnsupportedProtocolVersion(
"Unsupported protocol version",
supported=[@types.ProtocolVersion],
requested~,
),
body~,
)
}