1use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9use std::collections::HashMap;
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13pub mod transport;
14pub mod server;
15
16pub const MCP_VERSION: &str = "2025-11-15";
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct McpTool {
22 pub name: String,
23 pub description: String,
24 pub input_schema: Value,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct McpRequest {
30 pub jsonrpc: String,
31 pub id: Option<Value>,
32 pub method: String,
33 pub params: Option<Value>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct McpResponse {
39 pub jsonrpc: String,
40 pub id: Option<Value>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub result: Option<Value>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub error: Option<McpError>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct McpError {
50 pub code: i32,
51 pub message: String,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub data: Option<Value>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ToolResult {
59 pub content: Vec<ContentItem>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub is_error: Option<bool>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(tag = "type")]
67pub enum ContentItem {
68 #[serde(rename = "text")]
69 Text { text: String },
70 #[serde(rename = "resource")]
71 Resource { uri: String, mimeType: String, data: String },
72 #[serde(rename = "image")]
73 Image { data: String, mimeType: String },
74}
75
76pub type ToolHandler = Arc<dyn Fn(Value) -> Result<ToolResult> + Send + Sync>;
78
79pub struct McpServer {
81 tools: Arc<RwLock<HashMap<String, (McpTool, ToolHandler)>>>,
82 server_info: ServerInfo,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ServerInfo {
88 pub name: String,
89 pub version: String,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub description: Option<String>,
92}
93
94impl McpServer {
95 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
97 Self {
98 tools: Arc::new(RwLock::new(HashMap::new())),
99 server_info: ServerInfo {
100 name: name.into(),
101 version: version.into(),
102 description: Some("Agentic Robotics MCP Server".to_string()),
103 },
104 }
105 }
106
107 pub async fn register_tool(
109 &self,
110 tool: McpTool,
111 handler: ToolHandler,
112 ) -> Result<()> {
113 let mut tools = self.tools.write().await;
114 tools.insert(tool.name.clone(), (tool, handler));
115 Ok(())
116 }
117
118 pub async fn handle_request(&self, request: McpRequest) -> McpResponse {
120 let id = request.id.clone();
121
122 match request.method.as_str() {
123 "initialize" => self.handle_initialize(id).await,
124 "tools/list" => self.handle_list_tools(id).await,
125 "tools/call" => self.handle_call_tool(id, request.params).await,
126 _ => McpResponse {
127 jsonrpc: "2.0".to_string(),
128 id,
129 result: None,
130 error: Some(McpError {
131 code: -32601,
132 message: "Method not found".to_string(),
133 data: None,
134 }),
135 },
136 }
137 }
138
139 async fn handle_initialize(&self, id: Option<Value>) -> McpResponse {
140 McpResponse {
141 jsonrpc: "2.0".to_string(),
142 id,
143 result: Some(json!({
144 "protocolVersion": MCP_VERSION,
145 "capabilities": {
146 "tools": {},
147 "resources": {},
148 },
149 "serverInfo": self.server_info,
150 })),
151 error: None,
152 }
153 }
154
155 async fn handle_list_tools(&self, id: Option<Value>) -> McpResponse {
156 let tools = self.tools.read().await;
157 let tool_list: Vec<McpTool> = tools.values()
158 .map(|(tool, _)| tool.clone())
159 .collect();
160
161 McpResponse {
162 jsonrpc: "2.0".to_string(),
163 id,
164 result: Some(json!({
165 "tools": tool_list,
166 })),
167 error: None,
168 }
169 }
170
171 async fn handle_call_tool(&self, id: Option<Value>, params: Option<Value>) -> McpResponse {
172 let params = match params {
173 Some(p) => p,
174 None => {
175 return McpResponse {
176 jsonrpc: "2.0".to_string(),
177 id,
178 result: None,
179 error: Some(McpError {
180 code: -32602,
181 message: "Invalid params".to_string(),
182 data: None,
183 }),
184 };
185 }
186 };
187
188 let tool_name = match params.get("name").and_then(|v| v.as_str()) {
189 Some(name) => name,
190 None => {
191 return McpResponse {
192 jsonrpc: "2.0".to_string(),
193 id,
194 result: None,
195 error: Some(McpError {
196 code: -32602,
197 message: "Missing tool name".to_string(),
198 data: None,
199 }),
200 };
201 }
202 };
203
204 let arguments = params.get("arguments").cloned().unwrap_or(json!({}));
205
206 let tools = self.tools.read().await;
207 match tools.get(tool_name) {
208 Some((_, handler)) => {
209 match handler(arguments) {
210 Ok(result) => McpResponse {
211 jsonrpc: "2.0".to_string(),
212 id,
213 result: Some(serde_json::to_value(result).unwrap()),
214 error: None,
215 },
216 Err(e) => McpResponse {
217 jsonrpc: "2.0".to_string(),
218 id,
219 result: None,
220 error: Some(McpError {
221 code: -32000,
222 message: format!("Tool execution failed: {}", e),
223 data: None,
224 }),
225 },
226 }
227 }
228 None => McpResponse {
229 jsonrpc: "2.0".to_string(),
230 id,
231 result: None,
232 error: Some(McpError {
233 code: -32602,
234 message: format!("Tool not found: {}", tool_name),
235 data: None,
236 }),
237 },
238 }
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[tokio::test]
247 async fn test_mcp_initialize() {
248 let server = McpServer::new("test-server", "1.0.0");
249
250 let request = McpRequest {
251 jsonrpc: "2.0".to_string(),
252 id: Some(json!(1)),
253 method: "initialize".to_string(),
254 params: None,
255 };
256
257 let response = server.handle_request(request).await;
258 assert!(response.result.is_some());
259 assert!(response.error.is_none());
260 }
261
262 #[tokio::test]
263 async fn test_mcp_list_tools() {
264 let server = McpServer::new("test-server", "1.0.0");
265
266 let tool = McpTool {
268 name: "test_tool".to_string(),
269 description: "A test tool".to_string(),
270 input_schema: json!({
271 "type": "object",
272 "properties": {},
273 }),
274 };
275
276 let handler: ToolHandler = Arc::new(|_args| {
277 Ok(ToolResult {
278 content: vec![ContentItem::Text {
279 text: "Test result".to_string(),
280 }],
281 is_error: None,
282 })
283 });
284
285 server.register_tool(tool, handler).await.unwrap();
286
287 let request = McpRequest {
288 jsonrpc: "2.0".to_string(),
289 id: Some(json!(1)),
290 method: "tools/list".to_string(),
291 params: None,
292 };
293
294 let response = server.handle_request(request).await;
295 assert!(response.result.is_some());
296
297 let result = response.result.unwrap();
298 let tools = result.get("tools").unwrap().as_array().unwrap();
299 assert_eq!(tools.len(), 1);
300 }
301
302 #[tokio::test]
303 async fn test_mcp_call_tool() {
304 let server = McpServer::new("test-server", "1.0.0");
305
306 let tool = McpTool {
308 name: "echo".to_string(),
309 description: "Echo tool".to_string(),
310 input_schema: json!({
311 "type": "object",
312 "properties": {
313 "message": { "type": "string" }
314 },
315 }),
316 };
317
318 let handler: ToolHandler = Arc::new(|args| {
319 let message = args.get("message")
320 .and_then(|v| v.as_str())
321 .unwrap_or("empty");
322
323 Ok(ToolResult {
324 content: vec![ContentItem::Text {
325 text: format!("Echo: {}", message),
326 }],
327 is_error: None,
328 })
329 });
330
331 server.register_tool(tool, handler).await.unwrap();
332
333 let request = McpRequest {
334 jsonrpc: "2.0".to_string(),
335 id: Some(json!(1)),
336 method: "tools/call".to_string(),
337 params: Some(json!({
338 "name": "echo",
339 "arguments": {
340 "message": "Hello, Robot!"
341 }
342 })),
343 };
344
345 let response = server.handle_request(request).await;
346 assert!(response.result.is_some());
347 assert!(response.error.is_none());
348 }
349}