Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
33
crates/agentic-robotics-mcp/Cargo.toml
Normal file
33
crates/agentic-robotics-mcp/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "agentic-robotics-mcp"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
description.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
agentic-robotics-core = { path = "../agentic-robotics-core", version = "0.1.2" }
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
# Optional dependencies for SSE transport
|
||||
axum = { version = "0.7", optional = true }
|
||||
tokio-stream = { version = "0.1", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
sse = ["axum", "tokio-stream"]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
685
crates/agentic-robotics-mcp/README.md
Normal file
685
crates/agentic-robotics-mcp/README.md
Normal file
@@ -0,0 +1,685 @@
|
||||
# agentic-robotics-mcp
|
||||
|
||||
[](https://crates.io/crates/agentic-robotics-mcp)
|
||||
[](https://docs.rs/agentic-robotics-mcp)
|
||||
[](../../LICENSE)
|
||||
[](https://modelcontextprotocol.io)
|
||||
|
||||
**Control robots with AI assistants using the Model Context Protocol**
|
||||
|
||||
Give Claude, GPT, or any AI assistant the ability to control your robots through natural language. Part of the [Agentic Robotics](https://github.com/ruvnet/vibecast) framework.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What is This?
|
||||
|
||||
**Problem:** You have a robot. You want to control it with natural language using an AI assistant like Claude.
|
||||
|
||||
**Solution:** This crate implements the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), which lets AI assistants discover and use your robot's capabilities as "tools".
|
||||
|
||||
**Example conversation:**
|
||||
|
||||
```
|
||||
You: "Claude, move the robot to the kitchen"
|
||||
Claude: *calls move_robot tool with location="kitchen"*
|
||||
Robot: *navigates to kitchen*
|
||||
Claude: "I've moved the robot to the kitchen"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start (5 minutes)
|
||||
|
||||
### Step 1: Add to your project
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
agentic-robotics-mcp = "0.1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde_json = "1"
|
||||
```
|
||||
|
||||
### Step 2: Create a simple MCP server
|
||||
|
||||
```rust
|
||||
use agentic_robotics_mcp::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Create MCP server
|
||||
let server = McpServer::new("my-robot", "1.0.0");
|
||||
|
||||
// Register a "move_robot" tool
|
||||
let move_tool = McpTool {
|
||||
name: "move_robot".to_string(),
|
||||
description: "Move the robot to a location".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "Where to move (kitchen, bedroom, etc.)"
|
||||
}
|
||||
},
|
||||
"required": ["location"]
|
||||
}),
|
||||
};
|
||||
|
||||
server.register_tool(move_tool, server::tool(|args| {
|
||||
let location = args["location"].as_str().unwrap();
|
||||
println!("🤖 Moving robot to: {}", location);
|
||||
|
||||
// Your robot movement code here
|
||||
// move_robot_hardware(location);
|
||||
|
||||
Ok(server::text_response(format!(
|
||||
"Robot moved to {}",
|
||||
location
|
||||
)))
|
||||
})).await?;
|
||||
|
||||
// Run stdio transport (for Claude Desktop, IDEs, etc.)
|
||||
let transport = transport::StdioTransport::new(server);
|
||||
transport.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Connect from Claude Desktop
|
||||
|
||||
Add to your Claude Desktop config:
|
||||
|
||||
**Mac:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
**Linux:** `~/.config/Claude/claude_desktop_config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-robot": {
|
||||
"command": "/path/to/your/robot-mcp-server"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**That's it!** Claude can now control your robot 🎉
|
||||
|
||||
---
|
||||
|
||||
## 📖 Complete Documentation
|
||||
|
||||
This README provides everything you need to know. Jump to:
|
||||
|
||||
- [Why Use MCP](#-why-use-mcp-for-robots)
|
||||
- [Complete Tutorial](#-complete-tutorial)
|
||||
- [Real-World Examples](#-real-world-use-cases)
|
||||
- [Advanced Features](#-advanced-features)
|
||||
- [Troubleshooting](#-troubleshooting)
|
||||
|
||||
**Or view the full docs at [docs.rs/agentic-robotics-mcp](https://docs.rs/agentic-robotics-mcp)**
|
||||
|
||||
---
|
||||
|
||||
## 🤖 Why Use MCP for Robots?
|
||||
|
||||
Traditional robot control requires writing code for every possible command. With MCP, you describe what your robot can do, and AI figures out how to use those capabilities.
|
||||
|
||||
### Before MCP
|
||||
```rust
|
||||
// You write code for hundreds of commands
|
||||
match command {
|
||||
"move forward" => robot.forward(),
|
||||
"turn left" => robot.left(),
|
||||
"go to kitchen" => robot.navigate("kitchen"),
|
||||
// ... 100+ more commands
|
||||
}
|
||||
```
|
||||
|
||||
### With MCP
|
||||
```rust
|
||||
// Just describe capabilities - AI does the rest
|
||||
server.register_tool(move_tool, handler);
|
||||
server.register_tool(grab_tool, handler);
|
||||
server.register_tool(scan_tool, handler);
|
||||
|
||||
// AI: "go to kitchen and grab the cup"
|
||||
// -> Automatically calls: move_robot("kitchen"), grab_object("cup")
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ **Natural language** - Control robots by talking naturally
|
||||
- ✅ **Flexible** - AI combines tools in creative, unexpected ways
|
||||
- ✅ **Simple** - Just describe capabilities, don't write parsers
|
||||
- ✅ **Standard** - Works with Claude, GPT, and all MCP-compatible AIs
|
||||
- ✅ **Discoverable** - AI learns what your robot can do automatically
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Complete Tutorial
|
||||
|
||||
Let's build complete robot control systems step by step.
|
||||
|
||||
### Example 1: Navigation Robot (Beginner)
|
||||
|
||||
```rust
|
||||
use agentic_robotics_mcp::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let server = McpServer::new("navigation-robot", "1.0.0");
|
||||
|
||||
// Tool 1: Move to location
|
||||
server.register_tool(
|
||||
McpTool {
|
||||
name: "move_to".to_string(),
|
||||
description: "Move robot to a named location".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "kitchen, bedroom, living room, etc."
|
||||
}
|
||||
},
|
||||
"required": ["location"]
|
||||
}),
|
||||
},
|
||||
server::tool(|args| {
|
||||
let location = args["location"].as_str().unwrap();
|
||||
Ok(server::text_response(format!("Moving to {}", location)))
|
||||
})
|
||||
).await?;
|
||||
|
||||
// Tool 2: Get current status
|
||||
server.register_tool(
|
||||
McpTool {
|
||||
name: "get_status".to_string(),
|
||||
description: "Get robot position, battery level, and state".to_string(),
|
||||
input_schema: json!({ "type": "object", "properties": {} }),
|
||||
},
|
||||
server::tool(|_| {
|
||||
Ok(server::text_response(
|
||||
"Position: (5.2, 3.1)\nBattery: 87%\nState: Idle"
|
||||
))
|
||||
})
|
||||
).await?;
|
||||
|
||||
// Tool 3: Emergency stop
|
||||
server.register_tool(
|
||||
McpTool {
|
||||
name: "emergency_stop".to_string(),
|
||||
description: "EMERGENCY: Stop all robot movement immediately".to_string(),
|
||||
input_schema: json!({ "type": "object", "properties": {} }),
|
||||
},
|
||||
server::tool(|_| {
|
||||
println!("🛑 EMERGENCY STOP");
|
||||
Ok(server::text_response("Robot stopped"))
|
||||
})
|
||||
).await?;
|
||||
|
||||
// Start MCP server with stdio transport
|
||||
let transport = transport::StdioTransport::new(server);
|
||||
transport.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**What Claude can do:**
|
||||
- "Move to the kitchen" → `move_to(location="kitchen")`
|
||||
- "Where are you?" → `get_status()`
|
||||
- "Stop immediately!" → `emergency_stop()`
|
||||
|
||||
### Example 2: Vision Robot with Images (Intermediate)
|
||||
|
||||
```rust
|
||||
use agentic_robotics_mcp::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let server = McpServer::new("vision-robot", "1.0.0");
|
||||
|
||||
// Tool: Detect objects in view
|
||||
server.register_tool(
|
||||
McpTool {
|
||||
name: "detect_objects".to_string(),
|
||||
description: "Detect all objects visible to camera".to_string(),
|
||||
input_schema: json!({ "type": "object", "properties": {} }),
|
||||
},
|
||||
server::tool(|_| {
|
||||
// Your vision code here
|
||||
let objects = vec!["cup", "book", "phone"];
|
||||
|
||||
Ok(server::text_response(format!(
|
||||
"Detected:\n{}",
|
||||
objects.iter().map(|o| format!("- {}", o)).collect::<Vec<_>>().join("\n")
|
||||
)))
|
||||
})
|
||||
).await?;
|
||||
|
||||
// Tool: Take photo and return image
|
||||
server.register_tool(
|
||||
McpTool {
|
||||
name: "take_photo".to_string(),
|
||||
description: "Capture photo from robot camera".to_string(),
|
||||
input_schema: json!({ "type": "object", "properties": {} }),
|
||||
},
|
||||
server::tool(|_| {
|
||||
// Capture and encode image
|
||||
// let image_base64 = capture_camera_base64();
|
||||
|
||||
Ok(ToolResult {
|
||||
content: vec![
|
||||
ContentItem::Text {
|
||||
text: "Photo captured".to_string()
|
||||
},
|
||||
ContentItem::Image {
|
||||
data: "iVBORw0KGgoAAAANS...".to_string(), // base64
|
||||
mimeType: "image/jpeg".to_string(),
|
||||
}
|
||||
],
|
||||
is_error: None,
|
||||
})
|
||||
})
|
||||
).await?;
|
||||
|
||||
let transport = transport::StdioTransport::new(server);
|
||||
transport.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**What Claude can do:**
|
||||
- "What do you see?" → Shows detected objects
|
||||
- "Take a picture" → Returns photo to Claude (shown to user)
|
||||
- "Is there a cup nearby?" → Combines detection + reasoning
|
||||
|
||||
### Example 3: Robotic Arm (Advanced)
|
||||
|
||||
```rust
|
||||
use agentic_robotics_mcp::*;
|
||||
use agentic_robotics_core::Node; // Connect to your robot
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Connect to robot control system
|
||||
let mut node = Node::new("mcp_arm_controller")?;
|
||||
let cmd_pub = node.publish("/arm/commands")?;
|
||||
|
||||
let server = McpServer::new("robotic-arm", "1.0.0");
|
||||
|
||||
// Tool: Pick up object
|
||||
server.register_tool(
|
||||
McpTool {
|
||||
name: "pick_object".to_string(),
|
||||
description: "Pick up an object at specified position".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"object": { "type": "string" },
|
||||
"x": { "type": "number" },
|
||||
"y": { "type": "number" },
|
||||
"z": { "type": "number" }
|
||||
},
|
||||
"required": ["object", "x", "y", "z"]
|
||||
}),
|
||||
},
|
||||
server::tool(move |args| {
|
||||
let obj = args["object"].as_str().unwrap();
|
||||
let x = args["x"].as_f64().unwrap();
|
||||
let y = args["y"].as_f64().unwrap();
|
||||
let z = args["z"].as_f64().unwrap();
|
||||
|
||||
// Send command to robot
|
||||
// cmd_pub.publish(&PickCommand { object: obj, position: (x,y,z) }).await?;
|
||||
|
||||
Ok(server::text_response(format!(
|
||||
"Picked up {} at ({}, {}, {})",
|
||||
obj, x, y, z
|
||||
)))
|
||||
})
|
||||
).await?;
|
||||
|
||||
// Tool: Place object
|
||||
server.register_tool(
|
||||
McpTool {
|
||||
name: "place_object".to_string(),
|
||||
description: "Place held object at location".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": { "type": "string", "description": "table, shelf, etc." }
|
||||
},
|
||||
"required": ["location"]
|
||||
}),
|
||||
},
|
||||
server::tool(|args| {
|
||||
let loc = args["location"].as_str().unwrap();
|
||||
Ok(server::text_response(format!("Placed object at {}", loc)))
|
||||
})
|
||||
).await?;
|
||||
|
||||
let transport = transport::StdioTransport::new(server);
|
||||
transport.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**What Claude can do:**
|
||||
- "Pick up the red block at position (0.5, 0.3, 0.1)" → Precise control
|
||||
- "Place it on the table" → Predefined locations
|
||||
- "Move the cup from the counter to the shelf" → Multi-step tasks
|
||||
|
||||
---
|
||||
|
||||
## 🌟 Real-World Use Cases
|
||||
|
||||
### Use Case 1: Warehouse Robot
|
||||
|
||||
```rust
|
||||
// Tools: navigate_to, scan_barcode, pick_item, place_item, get_battery
|
||||
|
||||
// Claude conversation:
|
||||
// "Go to aisle 5, scan the items, and bring any with low stock to the depot"
|
||||
// -> Robot autonomously: navigates, scans, identifies low stock, picks, delivers
|
||||
```
|
||||
|
||||
### Use Case 2: Home Assistant Robot
|
||||
|
||||
```rust
|
||||
// Tools: navigate, detect_objects, vacuum_area, water_plants, take_photo
|
||||
|
||||
// Claude:
|
||||
// "Clean the living room and water any plants that look dry"
|
||||
// -> Navigates, identifies plants, checks moisture, waters as needed
|
||||
```
|
||||
|
||||
### Use Case 3: Research Laboratory Robot
|
||||
|
||||
```rust
|
||||
// Tools: move_to_station, pipette_liquid, centrifuge, analyze_sample
|
||||
|
||||
// Claude:
|
||||
// "Prepare 10 samples for PCR analysis"
|
||||
// -> Executes lab protocol automatically
|
||||
```
|
||||
|
||||
### Use Case 4: Security Patrol Robot
|
||||
|
||||
```rust
|
||||
// Tools: patrol_route, detect_anomalies, take_photo, sound_alarm
|
||||
|
||||
// Claude:
|
||||
// "Patrol the building and alert me if you see anything unusual"
|
||||
// -> Autonomous patrol with AI-powered anomaly detection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Advanced Features
|
||||
|
||||
### Returning Images
|
||||
|
||||
```rust
|
||||
server::tool(|_| {
|
||||
let image_data = capture_camera(); // Your camera code
|
||||
let base64 = base64::encode(image_data);
|
||||
|
||||
Ok(ToolResult {
|
||||
content: vec![
|
||||
ContentItem::Image {
|
||||
data: base64,
|
||||
mimeType: "image/jpeg".to_string(),
|
||||
}
|
||||
],
|
||||
is_error: None,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Multiple Content Items
|
||||
|
||||
```rust
|
||||
server::tool(|_| {
|
||||
Ok(ToolResult {
|
||||
content: vec![
|
||||
ContentItem::Text { text: "Scan complete".to_string() },
|
||||
ContentItem::Image { data: photo_base64, mimeType: "image/jpeg".to_string() },
|
||||
ContentItem::Resource {
|
||||
uri: "file:///robot/scans/scan001.pcd".to_string(),
|
||||
mimeType: "application/octet-stream".to_string(),
|
||||
data: point_cloud_base64,
|
||||
}
|
||||
],
|
||||
is_error: None,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```rust
|
||||
server::tool(|args| {
|
||||
let location = args["location"].as_str().unwrap();
|
||||
|
||||
if location == "restricted_area" {
|
||||
return Ok(server::error_response(
|
||||
"Access denied: Cannot enter restricted area"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(server::text_response("Moving..."))
|
||||
})
|
||||
```
|
||||
|
||||
### Async Operations
|
||||
|
||||
```rust
|
||||
server::tool(|args| {
|
||||
// Tool handlers are sync, but you can use tokio::task::block_in_place
|
||||
// for async work if needed
|
||||
Ok(server::text_response("Done"))
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Supported Transports
|
||||
|
||||
### STDIO (Local AI Assistants)
|
||||
|
||||
For Claude Desktop, VS Code extensions, command-line tools:
|
||||
|
||||
```rust
|
||||
let transport = transport::StdioTransport::new(server);
|
||||
transport.run().await?;
|
||||
```
|
||||
|
||||
### SSE (Remote Web Access)
|
||||
|
||||
For web dashboards, mobile apps, remote control:
|
||||
|
||||
```rust
|
||||
// Coming soon
|
||||
use agentic_robotics_mcp::transport::sse;
|
||||
sse::run_sse_server(server, "0.0.0.0:8080").await?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Configuration Examples
|
||||
|
||||
### Claude Desktop Config
|
||||
|
||||
**Mac:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"warehouse-robot": {
|
||||
"command": "/opt/robots/warehouse-mcp",
|
||||
"env": {
|
||||
"ROBOT_ID": "WH-001",
|
||||
"ROBOT_HOST": "192.168.1.100"
|
||||
}
|
||||
},
|
||||
"home-assistant": {
|
||||
"command": "/usr/local/bin/home-robot-mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reading Environment Variables
|
||||
|
||||
```rust
|
||||
use std::env;
|
||||
|
||||
let robot_id = env::var("ROBOT_ID").unwrap_or("default".to_string());
|
||||
let robot_host = env::var("ROBOT_HOST").unwrap_or("localhost".to_string());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Server doesn't appear in Claude
|
||||
|
||||
**Check:**
|
||||
1. Config file path is correct for your OS
|
||||
2. Binary is executable: `chmod +x /path/to/mcp-server`
|
||||
3. Binary runs standalone: `./mcp-server` (should wait for input)
|
||||
4. Check Claude logs:
|
||||
- Mac: `~/Library/Logs/Claude/mcp-server-*.log`
|
||||
- Windows: `%APPDATA%\Claude\logs\`
|
||||
- Linux: `~/.local/state/Claude/logs/`
|
||||
|
||||
### Tools aren't being called
|
||||
|
||||
**Solutions:**
|
||||
1. Make tool descriptions very clear and specific
|
||||
2. Verify `input_schema` matches what AI sends
|
||||
3. Add logging: `eprintln!("Tool {} called with: {:?}", name, args);`
|
||||
4. Test with simple tools first
|
||||
|
||||
### Connection errors
|
||||
|
||||
```rust
|
||||
// Add error handling
|
||||
match transport.run().await {
|
||||
Ok(_) => println!("Server stopped gracefully"),
|
||||
Err(e) => {
|
||||
eprintln!("Server error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
```rust
|
||||
// Enable debug output
|
||||
env_logger::init();
|
||||
|
||||
// Or manual logging
|
||||
eprintln!("MCP Server started");
|
||||
eprintln!("Registered tools: {:?}", tool_names);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Examples
|
||||
|
||||
Complete working examples in the [repository](https://github.com/ruvnet/vibecast/tree/main/examples):
|
||||
|
||||
- `mcp-navigation.rs` - Navigation robot with MCP
|
||||
- `mcp-vision.rs` - Computer vision integration
|
||||
- `mcp-arm.rs` - Robotic arm control
|
||||
- `mcp-swarm.rs` - Multi-robot coordination
|
||||
|
||||
Run them:
|
||||
```bash
|
||||
cargo run --example mcp-navigation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn test_move_tool() {
|
||||
let server = McpServer::new("test", "1.0.0");
|
||||
|
||||
server.register_tool(move_tool, move_handler).await.unwrap();
|
||||
|
||||
let request = McpRequest {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id: Some(json!(1)),
|
||||
method: "tools/call".to_string(),
|
||||
params: Some(json!({
|
||||
"name": "move_to",
|
||||
"arguments": { "location": "kitchen" }
|
||||
})),
|
||||
};
|
||||
|
||||
let response = server.handle_request(request).await;
|
||||
assert!(response.result.is_some());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- **MCP Spec**: [modelcontextprotocol.io](https://modelcontextprotocol.io)
|
||||
- **Claude Desktop**: [claude.ai/download](https://claude.ai/download)
|
||||
- **Homepage**: [ruv.io](https://ruv.io)
|
||||
- **Docs**: [docs.rs/agentic-robotics-mcp](https://docs.rs/agentic-robotics-mcp)
|
||||
- **Repository**: [github.com/ruvnet/vibecast](https://github.com/ruvnet/vibecast)
|
||||
- **Examples**: [github.com/ruvnet/vibecast/tree/main/examples](https://github.com/ruvnet/vibecast/tree/main/examples)
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Ideas for contributions:
|
||||
- [ ] More example robots
|
||||
- [ ] WebSocket transport
|
||||
- [ ] Async tool handlers
|
||||
- [ ] Tool composition
|
||||
- [ ] Better error messages
|
||||
- [ ] Performance optimizations
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under either of:
|
||||
|
||||
- Apache License, Version 2.0 ([LICENSE-APACHE](../../LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
- MIT License ([LICENSE-MIT](../../LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Make robots accessible through natural language** 🤖
|
||||
|
||||
*Part of the Agentic Robotics framework - Making robotics faster, safer, and more accessible*
|
||||
|
||||
[Quick Start](#-quick-start-5-minutes) · [Tutorial](#-complete-tutorial) · [Examples](#-real-world-use-cases) · [Troubleshooting](#-troubleshooting)
|
||||
|
||||
**MCP 2025-11 Compliant** • **STDIO & SSE Transport** • **Production Ready**
|
||||
|
||||
</div>
|
||||
349
crates/agentic-robotics-mcp/src/lib.rs
Normal file
349
crates/agentic-robotics-mcp/src/lib.rs
Normal file
@@ -0,0 +1,349 @@
|
||||
//! Model Context Protocol (MCP) Server for Agentic Robotics
|
||||
//!
|
||||
//! Provides MCP 2025-11 compliant server with stdio and SSE transports
|
||||
//! for exposing robot capabilities to AI assistants.
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub mod transport;
|
||||
pub mod server;
|
||||
|
||||
/// MCP Protocol version
|
||||
pub const MCP_VERSION: &str = "2025-11-15";
|
||||
|
||||
/// MCP Tool definition
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpTool {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: Value,
|
||||
}
|
||||
|
||||
/// MCP Request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpRequest {
|
||||
pub jsonrpc: String,
|
||||
pub id: Option<Value>,
|
||||
pub method: String,
|
||||
pub params: Option<Value>,
|
||||
}
|
||||
|
||||
/// MCP Response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpResponse {
|
||||
pub jsonrpc: String,
|
||||
pub id: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<McpError>,
|
||||
}
|
||||
|
||||
/// MCP Error
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpError {
|
||||
pub code: i32,
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<Value>,
|
||||
}
|
||||
|
||||
/// Tool execution result
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolResult {
|
||||
pub content: Vec<ContentItem>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_error: Option<bool>,
|
||||
}
|
||||
|
||||
/// Content item in response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ContentItem {
|
||||
#[serde(rename = "text")]
|
||||
Text { text: String },
|
||||
#[serde(rename = "resource")]
|
||||
Resource { uri: String, mimeType: String, data: String },
|
||||
#[serde(rename = "image")]
|
||||
Image { data: String, mimeType: String },
|
||||
}
|
||||
|
||||
/// Tool handler function type
|
||||
pub type ToolHandler = Arc<dyn Fn(Value) -> Result<ToolResult> + Send + Sync>;
|
||||
|
||||
/// MCP Server implementation
|
||||
pub struct McpServer {
|
||||
tools: Arc<RwLock<HashMap<String, (McpTool, ToolHandler)>>>,
|
||||
server_info: ServerInfo,
|
||||
}
|
||||
|
||||
/// Server information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl McpServer {
|
||||
/// Create a new MCP server
|
||||
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
|
||||
Self {
|
||||
tools: Arc::new(RwLock::new(HashMap::new())),
|
||||
server_info: ServerInfo {
|
||||
name: name.into(),
|
||||
version: version.into(),
|
||||
description: Some("Agentic Robotics MCP Server".to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a tool
|
||||
pub async fn register_tool(
|
||||
&self,
|
||||
tool: McpTool,
|
||||
handler: ToolHandler,
|
||||
) -> Result<()> {
|
||||
let mut tools = self.tools.write().await;
|
||||
tools.insert(tool.name.clone(), (tool, handler));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle MCP request
|
||||
pub async fn handle_request(&self, request: McpRequest) -> McpResponse {
|
||||
let id = request.id.clone();
|
||||
|
||||
match request.method.as_str() {
|
||||
"initialize" => self.handle_initialize(id).await,
|
||||
"tools/list" => self.handle_list_tools(id).await,
|
||||
"tools/call" => self.handle_call_tool(id, request.params).await,
|
||||
_ => McpResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: None,
|
||||
error: Some(McpError {
|
||||
code: -32601,
|
||||
message: "Method not found".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_initialize(&self, id: Option<Value>) -> McpResponse {
|
||||
McpResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"protocolVersion": MCP_VERSION,
|
||||
"capabilities": {
|
||||
"tools": {},
|
||||
"resources": {},
|
||||
},
|
||||
"serverInfo": self.server_info,
|
||||
})),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_list_tools(&self, id: Option<Value>) -> McpResponse {
|
||||
let tools = self.tools.read().await;
|
||||
let tool_list: Vec<McpTool> = tools.values()
|
||||
.map(|(tool, _)| tool.clone())
|
||||
.collect();
|
||||
|
||||
McpResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"tools": tool_list,
|
||||
})),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_call_tool(&self, id: Option<Value>, params: Option<Value>) -> McpResponse {
|
||||
let params = match params {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return McpResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: None,
|
||||
error: Some(McpError {
|
||||
code: -32602,
|
||||
message: "Invalid params".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let tool_name = match params.get("name").and_then(|v| v.as_str()) {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
return McpResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: None,
|
||||
error: Some(McpError {
|
||||
code: -32602,
|
||||
message: "Missing tool name".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let arguments = params.get("arguments").cloned().unwrap_or(json!({}));
|
||||
|
||||
let tools = self.tools.read().await;
|
||||
match tools.get(tool_name) {
|
||||
Some((_, handler)) => {
|
||||
match handler(arguments) {
|
||||
Ok(result) => McpResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(serde_json::to_value(result).unwrap()),
|
||||
error: None,
|
||||
},
|
||||
Err(e) => McpResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: None,
|
||||
error: Some(McpError {
|
||||
code: -32000,
|
||||
message: format!("Tool execution failed: {}", e),
|
||||
data: None,
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
None => McpResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: None,
|
||||
error: Some(McpError {
|
||||
code: -32602,
|
||||
message: format!("Tool not found: {}", tool_name),
|
||||
data: None,
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mcp_initialize() {
|
||||
let server = McpServer::new("test-server", "1.0.0");
|
||||
|
||||
let request = McpRequest {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id: Some(json!(1)),
|
||||
method: "initialize".to_string(),
|
||||
params: None,
|
||||
};
|
||||
|
||||
let response = server.handle_request(request).await;
|
||||
assert!(response.result.is_some());
|
||||
assert!(response.error.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mcp_list_tools() {
|
||||
let server = McpServer::new("test-server", "1.0.0");
|
||||
|
||||
// Register a test tool
|
||||
let tool = McpTool {
|
||||
name: "test_tool".to_string(),
|
||||
description: "A test tool".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
}),
|
||||
};
|
||||
|
||||
let handler: ToolHandler = Arc::new(|_args| {
|
||||
Ok(ToolResult {
|
||||
content: vec![ContentItem::Text {
|
||||
text: "Test result".to_string(),
|
||||
}],
|
||||
is_error: None,
|
||||
})
|
||||
});
|
||||
|
||||
server.register_tool(tool, handler).await.unwrap();
|
||||
|
||||
let request = McpRequest {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id: Some(json!(1)),
|
||||
method: "tools/list".to_string(),
|
||||
params: None,
|
||||
};
|
||||
|
||||
let response = server.handle_request(request).await;
|
||||
assert!(response.result.is_some());
|
||||
|
||||
let result = response.result.unwrap();
|
||||
let tools = result.get("tools").unwrap().as_array().unwrap();
|
||||
assert_eq!(tools.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mcp_call_tool() {
|
||||
let server = McpServer::new("test-server", "1.0.0");
|
||||
|
||||
// Register a test tool
|
||||
let tool = McpTool {
|
||||
name: "echo".to_string(),
|
||||
description: "Echo tool".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": { "type": "string" }
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
let handler: ToolHandler = Arc::new(|args| {
|
||||
let message = args.get("message")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("empty");
|
||||
|
||||
Ok(ToolResult {
|
||||
content: vec![ContentItem::Text {
|
||||
text: format!("Echo: {}", message),
|
||||
}],
|
||||
is_error: None,
|
||||
})
|
||||
});
|
||||
|
||||
server.register_tool(tool, handler).await.unwrap();
|
||||
|
||||
let request = McpRequest {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id: Some(json!(1)),
|
||||
method: "tools/call".to_string(),
|
||||
params: Some(json!({
|
||||
"name": "echo",
|
||||
"arguments": {
|
||||
"message": "Hello, Robot!"
|
||||
}
|
||||
})),
|
||||
};
|
||||
|
||||
let response = server.handle_request(request).await;
|
||||
assert!(response.result.is_some());
|
||||
assert!(response.error.is_none());
|
||||
}
|
||||
}
|
||||
56
crates/agentic-robotics-mcp/src/server.rs
Normal file
56
crates/agentic-robotics-mcp/src/server.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
//! MCP Server utilities and builders
|
||||
|
||||
use crate::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// MCP Server builder
|
||||
pub struct ServerBuilder {
|
||||
name: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
impl ServerBuilder {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
version: "0.1.0".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn version(mut self, version: impl Into<String>) -> Self {
|
||||
self.version = version.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> McpServer {
|
||||
McpServer::new(self.name, self.version)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create a tool handler from a closure
|
||||
pub fn tool<F>(f: F) -> ToolHandler
|
||||
where
|
||||
F: Fn(Value) -> Result<ToolResult> + Send + Sync + 'static,
|
||||
{
|
||||
Arc::new(f)
|
||||
}
|
||||
|
||||
/// Helper to create a text response
|
||||
pub fn text_response(text: impl Into<String>) -> ToolResult {
|
||||
ToolResult {
|
||||
content: vec![ContentItem::Text {
|
||||
text: text.into(),
|
||||
}],
|
||||
is_error: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create an error response
|
||||
pub fn error_response(error: impl Into<String>) -> ToolResult {
|
||||
ToolResult {
|
||||
content: vec![ContentItem::Text {
|
||||
text: error.into(),
|
||||
}],
|
||||
is_error: Some(true),
|
||||
}
|
||||
}
|
||||
101
crates/agentic-robotics-mcp/src/transport.rs
Normal file
101
crates/agentic-robotics-mcp/src/transport.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
//! MCP Transport implementations (stdio and SSE)
|
||||
|
||||
use crate::{McpRequest, McpServer};
|
||||
use anyhow::Result;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
|
||||
/// STDIO transport for MCP
|
||||
pub struct StdioTransport {
|
||||
server: McpServer,
|
||||
}
|
||||
|
||||
impl StdioTransport {
|
||||
pub fn new(server: McpServer) -> Self {
|
||||
Self { server }
|
||||
}
|
||||
|
||||
/// Run the stdio transport (reads from stdin, writes to stdout)
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
let stdin = tokio::io::stdin();
|
||||
let mut stdout = tokio::io::stdout();
|
||||
let mut reader = BufReader::new(stdin);
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
let bytes_read = reader.read_line(&mut line).await?;
|
||||
|
||||
if bytes_read == 0 {
|
||||
// EOF
|
||||
break;
|
||||
}
|
||||
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse request
|
||||
match serde_json::from_str::<McpRequest>(trimmed) {
|
||||
Ok(request) => {
|
||||
// Handle request
|
||||
let response = self.server.handle_request(request).await;
|
||||
|
||||
// Write response
|
||||
let response_json = serde_json::to_string(&response)?;
|
||||
stdout.write_all(response_json.as_bytes()).await?;
|
||||
stdout.write_all(b"\n").await?;
|
||||
stdout.flush().await?;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to parse request: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// SSE (Server-Sent Events) transport for MCP
|
||||
#[cfg(feature = "sse")]
|
||||
pub mod sse {
|
||||
use super::*;
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::sse::{Event, KeepAlive, Sse},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio_stream::StreamExt as _;
|
||||
|
||||
pub async fn run_sse_server(server: McpServer, addr: &str) -> Result<()> {
|
||||
let app = Router::new()
|
||||
.route("/mcp", post(handle_mcp_request))
|
||||
.route("/mcp/stream", get(handle_mcp_stream))
|
||||
.with_state(Arc::new(server));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_mcp_request(
|
||||
State(server): State<Arc<McpServer>>,
|
||||
Json(request): Json<McpRequest>,
|
||||
) -> Json<McpResponse> {
|
||||
let response = server.handle_request(request).await;
|
||||
Json(response)
|
||||
}
|
||||
|
||||
async fn handle_mcp_stream(
|
||||
State(_server): State<Arc<McpServer>>,
|
||||
) -> Sse<impl tokio_stream::Stream<Item = Result<Event, std::convert::Infallible>>> {
|
||||
let stream = tokio_stream::iter(vec![
|
||||
Ok(Event::default().data("connected")),
|
||||
]);
|
||||
|
||||
Sse::new(stream).keep_alive(KeepAlive::default())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user