Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
349
vendor/ruvector/crates/agentic-robotics-mcp/src/lib.rs
vendored
Normal file
349
vendor/ruvector/crates/agentic-robotics-mcp/src/lib.rs
vendored
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
vendor/ruvector/crates/agentic-robotics-mcp/src/server.rs
vendored
Normal file
56
vendor/ruvector/crates/agentic-robotics-mcp/src/server.rs
vendored
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
vendor/ruvector/crates/agentic-robotics-mcp/src/transport.rs
vendored
Normal file
101
vendor/ruvector/crates/agentic-robotics-mcp/src/transport.rs
vendored
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