///|
/// JSON-RPC 2.0 wire types used by MCP.
///
/// This package deliberately knows nothing about MCP methods, transports, or
/// HTTP headers. It only validates the JSON-RPC envelope and preserves the
/// payload for the protocol layer.
pub const JSON_RPC_VERSION : String = "2.0"
///|
pub const PARSE_ERROR : Int = -32700
///|
pub const INVALID_REQUEST : Int = -32600
///|
pub const METHOD_NOT_FOUND : Int = -32601
///|
pub const INVALID_PARAMS : Int = -32602
///|
pub const INTERNAL_ERROR : Int = -32603
///|
pub enum RequestId {
String(String)
Number(value~ : Double, repr~ : String)
} derive(Eq, Debug)
///|
pub fn RequestId::string(value : String) -> RequestId {
String(value)
}
///|
pub fn RequestId::number(
value : Double,
repr? : String,
) -> RequestId raise JsonRpcValidationError {
guard !(value.is_nan() || value.is_inf()) else {
raise JsonRpcValidationError::InvalidEnvelope("JSON-RPC id must be finite")
}
Number(value~, repr=repr.unwrap_or(value.to_string()))
}
///|
pub fn RequestId::as_json(self : RequestId) -> Json {
match self {
String(value) => Json::string(value)
Number(value~, repr~) => Json::number(value, repr~)
}
}
///|
pub fn RequestId::to_routing_key(self : RequestId) -> String {
match self {
String(value) => "string:\{value}"
Number(repr~, ..) => "number:\{repr}"
}
}
///|
pub impl ToJson for RequestId with fn to_json(self) -> Json {
self.as_json()
}
///|
pub impl FromJson for RequestId with fn from_json(
value : Json,
path : @json.JsonPath,
) -> RequestId {
match value {
Json::String(value) => String(value)
Json::Number(value, repr~) =>
Number(value~, repr=repr.unwrap_or(value.to_string()))
_ =>
raise @json.JsonDecodeError(
(path, "JSON-RPC id must be a string or number"),
)
}
}
///|
/// A JSON-RPC request envelope. `Params` is intentionally unconstrained at
/// this layer; the MCP protocol package supplies the method-specific type.
pub struct Request[Params] {
id : RequestId
method_name : String
params : Params
} derive(Debug, Eq)
///|
pub fn[Params] Request::Request(
id~ : RequestId,
method_name~ : String,
params~ : Params,
) -> Request[Params] raise JsonRpcValidationError {
guard !method_name.is_empty() else {
raise JsonRpcValidationError::InvalidEnvelope(
"JSON-RPC request method must not be empty",
)
}
{ id, method_name, params }
}
///|
pub impl[Params : ToJson] ToJson for Request[Params] with fn to_json(self) -> Json {
Json::object({
"jsonrpc": Json::string(JSON_RPC_VERSION),
"id": ToJson::to_json(self.id),
"method": Json::string(self.method_name),
"params": ToJson::to_json(self.params),
})
}
///|
/// A JSON-RPC notification envelope. Notifications never carry an id.
pub struct Notification[Params] {
method_name : String
params : Params
} derive(Debug, Eq)
///|
pub fn[Params] Notification::Notification(
method_name~ : String,
params~ : Params,
) -> Notification[Params] raise JsonRpcValidationError {
guard !method_name.is_empty() else {
raise JsonRpcValidationError::InvalidEnvelope(
"JSON-RPC notification method must not be empty",
)
}
{ method_name, params }
}
///|
pub impl[Params : ToJson] ToJson for Notification[Params] with fn to_json(self) -> Json {
Json::object({
"jsonrpc": Json::string(JSON_RPC_VERSION),
"method": Json::string(self.method_name),
"params": ToJson::to_json(self.params),
})
}
///|
pub struct ErrorObject {
code : Int
message : String
data : Json?
} derive(Debug, Eq)
///|
pub fn ErrorObject::ErrorObject(
code~ : Int,
message~ : String,
data? : Json,
) -> ErrorObject {
{ code, message, data }
}
///|
pub impl ToJson for ErrorObject with fn to_json(self) -> Json {
let fields : Map[String, Json] = {
"code": Json::number(self.code.to_double(), repr="\{self.code}"),
"message": Json::string(self.message),
}
if self.data is Some(data) {
fields["data"] = data
}
Json::object(fields)
}
///|
pub impl FromJson for ErrorObject with fn from_json(value, path) -> ErrorObject {
guard value is Json::Object(fields) else {
raise @json.JsonDecodeError((path, "JSON-RPC error must be an object"))
}
guard fields.get("code") is Some(Json::Number(code_num, ..)) &&
code_num.to_int().to_double() == code_num else {
raise @json.JsonDecodeError(
(path.add_key("code"), "error code must be an integer"),
)
}
guard fields.get("message") is Some(Json::String(message)) else {
raise @json.JsonDecodeError(
(path.add_key("message"), "error message must be a string"),
)
}
let code = code_num.to_int()
let data = fields.get("data")
ErrorObject(code~, message~, data?)
}
///|
pub(all) enum Response[Result] {
Success(id~ : RequestId, result~ : Result)
Failure(id~ : RequestId?, error~ : ErrorObject)
} derive(Debug, Eq)
///|
pub impl[Result : ToJson] ToJson for Response[Result] with fn to_json(self) -> Json {
match self {
Success(id~, result~) =>
Json::object({
"jsonrpc": Json::string(JSON_RPC_VERSION),
"id": ToJson::to_json(id),
"result": ToJson::to_json(result),
})
Failure(id~, error~) =>
Json::object({
"jsonrpc": Json::string(JSON_RPC_VERSION),
"id": id.map(fn(i) { ToJson::to_json(i) }).unwrap_or(Json::null()),
"error": ToJson::to_json(error),
})
}
}
///|
pub enum Message {
Request(Request[Json])
Notification(Notification[Json])
Response(Response[Json])
}
///|
/// Decode the JSON-RPC envelope only. MCP semantic errors are intentionally
/// handled by the caller after this syntax boundary.
pub(all) suberror JsonRpcDecodeError {
InvalidMessage(String)
} derive(Debug, Eq)
///|
pub fn decode(value : Json) -> Message raise JsonRpcDecodeError {
guard value is Json::Object(fields) else {
raise JsonRpcDecodeError::InvalidMessage(
"JSON-RPC message must be an object",
)
}
guard fields.get("jsonrpc") is Some(Json::String(v)) && v == JSON_RPC_VERSION else {
raise JsonRpcDecodeError::InvalidMessage("jsonrpc must be \"2.0\"")
}
match fields.get("method") {
Some(Json::String(method_name)) => {
let params : Json = match fields.get("params") {
Some(Json::Object(params)) => Json::object(params)
Some(Json::Array(params)) => Json::array(params)
Some(_) =>
raise JsonRpcDecodeError::InvalidMessage(
"params must be structured JSON",
)
None => Json::object({})
}
match fields.get("id") {
Some(id_val) => {
let id : RequestId = @json.from_json(id_val) catch {
_ =>
raise JsonRpcDecodeError::InvalidMessage(
"id must be a string or number",
)
}
let request = Request::Request(id~, method_name~, params~) catch {
InvalidEnvelope(msg) =>
raise JsonRpcDecodeError::InvalidMessage(msg)
}
Message::Request(request)
}
None => {
let notification = Notification::Notification(method_name~, params~) catch {
InvalidEnvelope(msg) =>
raise JsonRpcDecodeError::InvalidMessage(msg)
}
Message::Notification(notification)
}
}
}
Some(_) =>
raise JsonRpcDecodeError::InvalidMessage("method must be a string")
None => {
let id : RequestId? = match fields.get("id") {
Some(Json::Null) | None => None
Some(id_val) =>
Some(
@json.from_json(id_val) catch {
_ =>
raise JsonRpcDecodeError::InvalidMessage(
"id must be a string or number",
)
},
)
}
match (fields.get("result"), fields.get("error")) {
(Some(_), Some(_)) | (None, None) =>
raise JsonRpcDecodeError::InvalidMessage(
"response must contain result or error",
)
(Some(result), None) => {
guard id is Some(id) else {
raise JsonRpcDecodeError::InvalidMessage(
"success response id is required",
)
}
Message::Response(Success(id~, result~))
}
(None, Some(error_val)) => {
let error : ErrorObject = @json.from_json(error_val) catch {
@json.JsonDecodeError((_, msg)) =>
raise JsonRpcDecodeError::InvalidMessage(
if msg.contains("code") {
"error code must be an integer"
} else if msg.contains("message") {
"error message must be a string"
} else {
"error must be an object"
},
)
}
Message::Response(Failure(id~, error~))
}
}
}
}
}
///|
pub(all) suberror JsonRpcValidationError {
InvalidEnvelope(String)
} derive(Debug, Eq)