hefa_core/mcp/
mod.rs

1use serde::{Deserialize, Serialize};
2use serde_json::{Value, json};
3
4use crate::agent::{AgentResult, ToolCallOutcome};
5
6/// Basic MCP request wrapper.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct McpRequest {
9    pub id: String,
10    pub method: String,
11    #[serde(default)]
12    pub params: Value,
13}
14
15/// Successful or error response to an MCP request.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct McpResponse {
18    pub id: String,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub result: Option<Value>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub error: Option<McpError>,
23}
24
25impl McpResponse {
26    pub fn ok(id: impl Into<String>, result: Value) -> Self {
27        Self {
28            id: id.into(),
29            result: Some(result),
30            error: None,
31        }
32    }
33
34    pub fn error(id: impl Into<String>, error: McpError) -> Self {
35        Self {
36            id: id.into(),
37            result: None,
38            error: Some(error),
39        }
40    }
41
42    pub fn from_agent_result(id: impl Into<String>, result: &AgentResult) -> Self {
43        let payload = json!({
44            "content": result.response.content,
45            "tool_outputs": result.tool_outputs.iter().map(McpToolOutput::from).collect::<Vec<_>>(),
46        });
47        Self::ok(id, payload)
48    }
49}
50
51/// MCP error payload (compatible with JSON-RPC style).
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct McpError {
54    pub code: i32,
55    pub message: String,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub data: Option<Value>,
58}
59
60impl McpError {
61    pub fn new(code: i32, message: impl Into<String>) -> Self {
62        Self {
63            code,
64            message: message.into(),
65            data: None,
66        }
67    }
68
69    pub fn with_data(code: i32, message: impl Into<String>, data: Value) -> Self {
70        Self {
71            code,
72            message: message.into(),
73            data: Some(data),
74        }
75    }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct McpToolOutput {
80    pub call_id: String,
81    pub name: String,
82    pub output: Value,
83}
84
85impl From<&ToolCallOutcome> for McpToolOutput {
86    fn from(outcome: &ToolCallOutcome) -> Self {
87        Self {
88            call_id: outcome.call.id.clone(),
89            name: outcome.call.name.clone(),
90            output: outcome.result.content.clone(),
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::{
99        agent::{AgentResult, ToolCallOutcome},
100        llm::{LlmResponse, ToolCall},
101        tools::ToolResult,
102    };
103
104    #[test]
105    fn serializes_request() {
106        let req = McpRequest {
107            id: "1".into(),
108            method: "agent.invoke".into(),
109            params: json!({"prompt":"hello"}),
110        };
111        let text = serde_json::to_string(&req).unwrap();
112        assert!(text.contains("\"agent.invoke\""));
113    }
114
115    #[test]
116    fn converts_agent_result_to_response() {
117        let result = AgentResult {
118            response: LlmResponse {
119                content: "hi".into(),
120                tool_calls: vec![],
121            },
122            tool_outputs: vec![ToolCallOutcome {
123                call: ToolCall {
124                    id: "tool-1".into(),
125                    name: "echo".into(),
126                    arguments: json!({"msg":"hi"}),
127                },
128                result: ToolResult {
129                    content: json!({"echo":"hi"}),
130                },
131            }],
132        };
133        let resp = McpResponse::from_agent_result("1", &result);
134        assert!(resp.error.is_none());
135        let payload = resp.result.unwrap();
136        assert_eq!(payload["content"], "hi");
137        assert_eq!(payload["tool_outputs"][0]["name"], "echo");
138    }
139}