magi-code 0.62.0

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

pub(crate) const PROTOCOL_VERSION: &str = "2025-03-26";
#[allow(dead_code)]
pub(crate) const DRAFT_2025_03_26: &str = PROTOCOL_VERSION;
pub(crate) const METHOD_INITIALIZE: &str = "initialize";
pub(crate) const METHOD_INITIALIZED: &str = "notifications/initialized";
pub(crate) const METHOD_TOOLS_LIST: &str = "tools/list";
pub(crate) const METHOD_TOOLS_CALL: &str = "tools/call";
#[allow(dead_code)]
pub(crate) const METHOD_TOOLS_LIST_CHANGED: &str = "notifications/tools/list_changed";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct Implementation {
    pub(crate) name: String,
    pub(crate) version: String,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct ClientCapabilities {}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ServerCapabilities {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) tools: Option<ToolsCapability>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ToolsCapability {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) list_changed: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct InitializeRequestParams {
    pub(crate) protocol_version: String,
    pub(crate) capabilities: ClientCapabilities,
    pub(crate) client_info: Implementation,
}

impl Default for InitializeRequestParams {
    fn default() -> Self {
        Self {
            protocol_version: PROTOCOL_VERSION.to_string(),
            capabilities: ClientCapabilities::default(),
            client_info: Implementation {
                name: "magi-code".to_string(),
                version: env!("CARGO_PKG_VERSION").to_string(),
            },
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct InitializeResult {
    pub(crate) protocol_version: String,
    pub(crate) capabilities: ServerCapabilities,
    pub(crate) server_info: Implementation,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) instructions: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Tool {
    pub(crate) name: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) title: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) description: Option<String>,
    #[serde(default = "default_input_schema")]
    pub(crate) input_schema: Value,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) output_schema: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) annotations: Option<ToolAnnotations>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ToolAnnotations {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) title: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) read_only_hint: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) destructive_hint: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) idempotent_hint: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) open_world_hint: Option<bool>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ListToolsParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) cursor: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ListToolsResult {
    pub(crate) tools: Vec<Tool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) next_cursor: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CallToolParams {
    pub(crate) name: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) arguments: Option<Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CallToolResult {
    pub(crate) content: Vec<ContentBlock>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) structured_content: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) is_error: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(
    tag = "type",
    rename_all = "snake_case",
    rename_all_fields = "camelCase"
)]
pub(crate) enum ContentBlock {
    Text { text: String },
    Image { data: String, mime_type: String },
    Audio { data: String, mime_type: String },
    Resource { resource: ResourceContents },
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ResourceContents {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) uri: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) mime_type: Option<String>,
    #[serde(flatten)]
    pub(crate) extra: serde_json::Map<String, Value>,
}

fn default_input_schema() -> Value {
    serde_json::json!({"type":"object","properties":{}})
}

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

    #[test]
    fn initialize_params_serialize_camel_case_empty_capabilities() {
        let value = serde_json::to_value(InitializeRequestParams::default()).unwrap();
        assert_eq!(value["protocolVersion"], "2025-03-26");
        assert_eq!(value["capabilities"], json!({}));
        assert_eq!(value["clientInfo"]["name"], "magi-code");
    }

    #[test]
    fn tool_round_trips_camel_case_schema_fields() {
        let tool: Tool = serde_json::from_value(json!({
            "name":"echo",
            "title":"Echo",
            "description":"Echo input",
            "inputSchema":{"type":"object"},
            "outputSchema":{"type":"object"},
            "annotations":{"readOnlyHint":true,"openWorldHint":false}
        }))
        .unwrap();
        assert_eq!(tool.input_schema, json!({"type":"object"}));
        assert_eq!(
            tool.annotations.as_ref().unwrap().read_only_hint,
            Some(true)
        );
        assert!(
            serde_json::to_value(tool)
                .unwrap()
                .get("inputSchema")
                .is_some()
        );
    }

    #[test]
    fn call_tool_result_deserializes_content_blocks() {
        let result: CallToolResult = serde_json::from_value(json!({
            "content":[
                {"type":"text","text":"ok"},
                {"type":"image","data":"aW1n","mimeType":"image/png"},
                {"type":"audio","data":"YXVkaW8=","mimeType":"audio/wav"},
                {"type":"resource","resource":{"uri":"file:///tmp/a.txt","mimeType":"text/plain","text":"body"}}
            ],
            "structuredContent":{"ok":true},
            "isError":false
        }))
        .unwrap();
        assert!(matches!(result.content[0], ContentBlock::Text { .. }));
        assert!(matches!(
            &result.content[1],
            ContentBlock::Image { data, mime_type } if data == "aW1n" && mime_type == "image/png"
        ));
        assert!(matches!(
            &result.content[2],
            ContentBlock::Audio { data, mime_type } if data == "YXVkaW8=" && mime_type == "audio/wav"
        ));
        assert!(matches!(
            &result.content[3],
            ContentBlock::Resource { resource } if resource.uri.as_deref() == Some("file:///tmp/a.txt") && resource.mime_type.as_deref() == Some("text/plain")
        ));
        let value = serde_json::to_value(result).unwrap();
        assert_eq!(value["content"][1]["mimeType"], "image/png");
        assert_eq!(value["content"][2]["mimeType"], "audio/wav");
        assert_eq!(value["content"][3]["resource"]["mimeType"], "text/plain");
    }

    #[test]
    fn missing_input_schema_defaults_to_empty_object_schema() {
        let tool: Tool = serde_json::from_value(json!({"name":"no_schema"})).unwrap();
        assert_eq!(tool.input_schema, json!({"type":"object","properties":{}}));
    }
}