agentic_robotics_mcp/
lib.rs

1//! Model Context Protocol (MCP) Server for Agentic Robotics
2//!
3//! Provides MCP 2025-11 compliant server with stdio and SSE transports
4//! for exposing robot capabilities to AI assistants.
5
6use 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
16/// MCP Protocol version
17pub const MCP_VERSION: &str = "2025-11-15";
18
19/// MCP Tool definition
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct McpTool {
22    pub name: String,
23    pub description: String,
24    pub input_schema: Value,
25}
26
27/// MCP Request
28#[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/// MCP Response
37#[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/// MCP Error
48#[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/// Tool execution result
57#[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/// Content item in response
65#[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
76/// Tool handler function type
77pub type ToolHandler = Arc<dyn Fn(Value) -> Result<ToolResult> + Send + Sync>;
78
79/// MCP Server implementation
80pub struct McpServer {
81    tools: Arc<RwLock<HashMap<String, (McpTool, ToolHandler)>>>,
82    server_info: ServerInfo,
83}
84
85/// Server information
86#[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    /// Create a new MCP server
96    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    /// Register a tool
108    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    /// Handle MCP request
119    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        // Register a test tool
267        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        // Register a test tool
307        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}