claude-code-sdk-rust 0.2.0

Async Rust SDK for the Claude Code CLI: streaming agent turns, tool use, and sessions.
Documentation
use std::collections::HashMap;

use crate::mcp::{MCPContent, SimpleMCPServer};
use serde_json::Value;

pub fn answer_mcp_message(
    servers: &HashMap<String, SimpleMCPServer>,
    server_name: &str,
    message: &Value,
) -> Value {
    let Some(server) = servers.get(server_name) else {
        return jsonrpc_error(
            message.get("id").cloned(),
            -32601,
            &format!("Server '{server_name}' not found"),
        );
    };

    let method = message.get("method").and_then(|v| v.as_str()).unwrap_or("");
    match method {
        "initialize" => serde_json::json!({
            "jsonrpc": "2.0",
            "id": message.get("id").cloned().unwrap_or(Value::Null),
            "result": {
                "protocolVersion": "2024-11-05",
                "capabilities": {"tools": {}},
                "serverInfo": {
                    "name": server.name(),
                    "version": server.version(),
                },
            },
        }),
        "tools/list" => serde_json::json!({
            "jsonrpc": "2.0",
            "id": message.get("id").cloned().unwrap_or(Value::Null),
            "result": {
                "tools": server
                    .list_tools()
                    .into_iter()
                    .map(tool_to_wire)
                    .collect::<Vec<_>>(),
            },
        }),
        "tools/call" => call_tool(server, message),
        "notifications/initialized" => serde_json::json!({
            "jsonrpc": "2.0",
            "result": {},
        }),
        _ => jsonrpc_error(
            message.get("id").cloned(),
            -32601,
            &format!("Method '{method}' not found"),
        ),
    }
}

fn call_tool(server: &SimpleMCPServer, message: &Value) -> Value {
    let id = message.get("id").cloned().unwrap_or(Value::Null);
    let params = message.get("params").and_then(|v| v.as_object());
    let name = params
        .and_then(|params| params.get("name"))
        .and_then(|v| v.as_str())
        .unwrap_or("");
    let arguments = params
        .and_then(|params| params.get("arguments"))
        .cloned()
        .unwrap_or_else(|| serde_json::json!({}));

    match server.call_tool(name, arguments) {
        Ok(content) => serde_json::json!({
            "jsonrpc": "2.0",
            "id": id,
            "result": {
                "content": content.into_iter().map(content_to_wire).collect::<Vec<_>>(),
            },
        }),
        Err(error) => serde_json::json!({
            "jsonrpc": "2.0",
            "id": id,
            "result": {
                "content": [{"type": "text", "text": error}],
                "isError": true,
            },
        }),
    }
}

fn tool_to_wire(tool: &crate::mcp::MCPTool) -> Value {
    let mut value = serde_json::json!({
        "name": tool.name,
        "description": tool.description,
        "inputSchema": tool.input_schema,
    });
    if let Some(annotations) = &tool.annotations {
        value["annotations"] = serde_json::json!({
            "title": annotations.title,
            "readOnlyHint": annotations.read_only_hint,
            "destructiveHint": annotations.destructive_hint,
            "idempotentHint": annotations.idempotent_hint,
            "openWorldHint": annotations.open_world_hint,
        });
        if let Some(max_size) = annotations.max_result_size_chars {
            value["_meta"] = serde_json::json!({
                "anthropic/maxResultSizeChars": max_size,
            });
        }
    }
    value
}

fn content_to_wire(content: MCPContent) -> Value {
    match content {
        MCPContent::Text { text } => serde_json::json!({"type": "text", "text": text}),
        MCPContent::Image { data, mime_type } => {
            serde_json::json!({"type": "image", "data": data, "mimeType": mime_type})
        }
        MCPContent::Audio { data, mime_type } => {
            serde_json::json!({"type": "audio", "data": data, "mimeType": mime_type})
        }
        MCPContent::ResourceLink {
            uri,
            name,
            description,
            mime_type,
        } => serde_json::json!({
            "type": "resource_link",
            "uri": uri,
            "name": name,
            "description": description,
            "mimeType": mime_type,
        }),
        MCPContent::Resource {
            uri,
            mime_type,
            text,
        } => serde_json::json!({
            "type": "resource",
            "resource": {
                "uri": uri,
                "mimeType": mime_type,
                "text": text,
            },
        }),
    }
}

fn jsonrpc_error(id: Option<Value>, code: i32, message: &str) -> Value {
    serde_json::json!({
        "jsonrpc": "2.0",
        "id": id.unwrap_or(Value::Null),
        "error": {
            "code": code,
            "message": message,
        },
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::mcp::{MCPContent, MCPTool};

    fn server() -> SimpleMCPServer {
        let mut server = SimpleMCPServer::new("greeter");
        server.register_tool(
            MCPTool {
                name: "greet".to_string(),
                description: "Greet someone".to_string(),
                input_schema: serde_json::json!({"type": "object"}),
                annotations: None,
            },
            |input| {
                let name = input
                    .get("name")
                    .and_then(|v| v.as_str())
                    .unwrap_or("there");
                Ok(vec![MCPContent::Text {
                    text: format!("Hi {name}"),
                }])
            },
        );
        server
    }

    #[test]
    fn answers_tools_list() {
        let servers = HashMap::from([("greeter".to_string(), server())]);
        let response = answer_mcp_message(
            &servers,
            "greeter",
            &serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list"}),
        );

        assert_eq!(response["result"]["tools"][0]["name"], "greet");
        assert_eq!(
            response["result"]["tools"][0]["inputSchema"]["type"],
            "object"
        );
    }

    #[test]
    fn answers_tools_call() {
        let servers = HashMap::from([("greeter".to_string(), server())]);
        let response = answer_mcp_message(
            &servers,
            "greeter",
            &serde_json::json!({
                "jsonrpc": "2.0",
                "id": 2,
                "method": "tools/call",
                "params": {"name": "greet", "arguments": {"name": "Ada"}}
            }),
        );

        assert_eq!(response["result"]["content"][0]["text"], "Hi Ada");
    }
}