magi-code 0.61.0

Repository-aware CLI coding agent for terminal work
Documentation
use serde::{Deserialize, Deserializer, Serialize, de};
use serde_json::Value;

#[allow(dead_code)]
pub(crate) const PARSE_ERROR: i64 = -32700;
#[allow(dead_code)]
pub(crate) const INVALID_REQUEST: i64 = -32600;
#[allow(dead_code)]
pub(crate) const METHOD_NOT_FOUND: i64 = -32601;
#[allow(dead_code)]
pub(crate) const INVALID_PARAMS: i64 = -32602;
#[allow(dead_code)]
pub(crate) const INTERNAL_ERROR: i64 = -32603;

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum RequestId {
    Number(i64),
    String(String),
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct JsonRpcRequest {
    pub(crate) jsonrpc: String,
    pub(crate) id: RequestId,
    pub(crate) method: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) params: Option<Value>,
}

impl JsonRpcRequest {
    pub(crate) fn new(id: RequestId, method: impl Into<String>, params: Option<Value>) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            id,
            method: method.into(),
            params,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct JsonRpcResponse {
    pub(crate) jsonrpc: String,
    pub(crate) id: RequestId,
    pub(crate) result: Value,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct JsonRpcError {
    pub(crate) jsonrpc: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) id: Option<RequestId>,
    pub(crate) error: ErrorData,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct JsonRpcNotification {
    pub(crate) jsonrpc: String,
    pub(crate) method: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) params: Option<Value>,
}

impl JsonRpcNotification {
    pub(crate) fn new(method: impl Into<String>, params: Option<Value>) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            method: method.into(),
            params,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct ErrorData {
    pub(crate) code: i64,
    pub(crate) message: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) data: Option<Value>,
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) enum JsonRpcMessage {
    Response(JsonRpcResponse),
    Error(JsonRpcError),
    Notification(JsonRpcNotification),
    Request(JsonRpcRequest),
}

impl<'de> Deserialize<'de> for JsonRpcMessage {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = Value::deserialize(deserializer)?;
        if value.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
            return Err(de::Error::custom(
                "JSON-RPC message missing jsonrpc \"2.0\"",
            ));
        }
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum UntaggedMessage {
            Response(JsonRpcResponse),
            Error(JsonRpcError),
            Notification(JsonRpcNotification),
            Request(JsonRpcRequest),
        }
        match UntaggedMessage::deserialize(value).map_err(de::Error::custom)? {
            UntaggedMessage::Response(message) => Ok(JsonRpcMessage::Response(message)),
            UntaggedMessage::Error(message) => Ok(JsonRpcMessage::Error(message)),
            UntaggedMessage::Notification(message) => Ok(JsonRpcMessage::Notification(message)),
            UntaggedMessage::Request(message) => Ok(JsonRpcMessage::Request(message)),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn request_id_round_trips_number_and_string() {
        assert_eq!(
            serde_json::from_value::<RequestId>(json!(7)).unwrap(),
            RequestId::Number(7)
        );
        assert_eq!(
            serde_json::from_value::<RequestId>(json!("abc")).unwrap(),
            RequestId::String("abc".to_string())
        );
    }

    #[test]
    fn jsonrpc_request_serializes_2_0() {
        let value = serde_json::to_value(JsonRpcRequest::new(
            RequestId::Number(1),
            "tools/list",
            Some(json!({"cursor":"a"})),
        ))
        .unwrap();
        assert_eq!(value["jsonrpc"], "2.0");
        assert_eq!(value["id"], 1);
        assert_eq!(value["method"], "tools/list");
    }

    #[test]
    fn mcp_jsonrpc_serialization_fixture_remains_newline_framing_agnostic() {
        let request = JsonRpcRequest::new(
            RequestId::Number(42),
            "tools/call",
            Some(json!({"name":"echo","arguments":{"text":"hi"}})),
        );
        let encoded = serde_json::to_string(&request).unwrap();

        assert_eq!(
            encoded,
            r#"{"jsonrpc":"2.0","id":42,"method":"tools/call","params":{"arguments":{"text":"hi"},"name":"echo"}}"#
        );
        assert!(!encoded.contains("Content-Length"));
        assert!(!encoded.contains('\n'));
    }

    #[test]
    fn jsonrpc_error_deserializes() {
        let error: JsonRpcMessage = serde_json::from_value(json!({
            "jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"missing"}
        }))
        .unwrap();
        assert!(matches!(error, JsonRpcMessage::Error(_)));
    }

    #[test]
    fn jsonrpc_message_rejects_wrong_version() {
        let error = serde_json::from_value::<JsonRpcMessage>(json!({
            "jsonrpc":"1.0","id":1,"result":{}
        }))
        .unwrap_err()
        .to_string();
        assert!(error.contains("jsonrpc"), "{error}");
    }

    #[test]
    fn jsonrpc_message_rejects_missing_version() {
        let error = serde_json::from_value::<JsonRpcMessage>(json!({
            "id":1,"result":{}
        }))
        .unwrap_err()
        .to_string();
        assert!(error.contains("jsonrpc"), "{error}");
    }
}