1use serde::{Deserialize, Serialize};
2use serde_json::{Value, json};
3
4use crate::agent::{AgentResult, ToolCallOutcome};
5
6#[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#[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#[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}