Skip to main content
Glama
server.rs22.6 kB
//! MCP Server implementation //! //! Runs in separate thread, communicates with Python via channels use crate::error::internal_err; use crate::protocol::{Command, Response}; use crossbeam_channel::{Receiver, Sender}; use rmcp::{ handler::server::router::tool::ToolRouter, handler::server::wrapper::Parameters, model::{CallToolResult, Content, ServerCapabilities, ServerInfo}, tool, tool_handler, tool_router, transport::streamable_http_server::{ session::local::LocalSessionManager, StreamableHttpService, }, ErrorData as McpError, ServerHandler, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; /// Pending requests waiting for Python response type PendingRequests = Arc<Mutex<HashMap<u64, tokio::sync::oneshot::Sender<Response>>>>; /// MCP Server state #[derive(Clone)] pub struct BlenderServer { /// Channel to send commands to Python cmd_tx: Sender<Command>, /// Pending requests pending: PendingRequests, /// Request ID counter next_id: Arc<AtomicU64>, /// Tool router tool_router: ToolRouter<Self>, /// Instance tag tag: String, } impl BlenderServer { pub fn new(cmd_tx: Sender<Command>, tag: String) -> Self { Self { cmd_tx, pending: Arc::new(Mutex::new(HashMap::new())), next_id: Arc::new(AtomicU64::new(1)), tool_router: Self::tool_router(), tag, } } /// Send command to Python and wait for response async fn call_blender(&self, method: &str, params: impl Serialize) -> Result<String, McpError> { let id = self.next_id.fetch_add(1, Ordering::SeqCst); let params_json = serde_json::to_string(&params).map_err(internal_err("Failed to serialize params"))?; let cmd = Command { id, method: method.to_string(), params: params_json, }; // Create oneshot channel for response let (tx, rx) = tokio::sync::oneshot::channel(); // Register pending request { let mut pending = self.pending.lock().await; pending.insert(id, tx); } // Send command to Python self.cmd_tx .send(cmd) .map_err(internal_err("Failed to send command to Blender"))?; // Wait for response with timeout let response = tokio::time::timeout(Duration::from_secs(30), rx) .await .map_err(|_| McpError::internal_error("Timeout waiting for Blender response", None))? .map_err(internal_err("Response channel closed"))?; // Check for error if let Some(error) = response.error { return Err(McpError::internal_error(error, None)); } response .result .ok_or_else(|| McpError::internal_error("Empty response from Blender", None)) } } // ============================================================================ // Tool Arguments // ============================================================================ #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct EmptyArgs {} #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ListObjectsArgs { /// Filter by object type (MESH, LIGHT, CAMERA, EMPTY, etc.) #[serde(rename = "type")] pub object_type: Option<String>, /// Filter by collection name pub collection: Option<String>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct GetObjectArgs { /// Object name pub name: String, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct CreatePrimitiveArgs { /// Primitive type: cube, sphere, cylinder, plane, cone, torus #[serde(rename = "type")] pub primitive_type: String, /// Object name (optional, auto-generated if not provided) pub name: Option<String>, /// Location [x, y, z] pub location: Option<[f64; 3]>, /// Scale [x, y, z] pub scale: Option<[f64; 3]>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct UpdateObjectArgs { /// Object name pub name: String, /// New location [x, y, z] pub location: Option<[f64; 3]>, /// New rotation (euler) [x, y, z] in radians pub rotation: Option<[f64; 3]>, /// New scale [x, y, z] pub scale: Option<[f64; 3]>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct DeleteObjectArgs { /// Object name pub name: String, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ExecutePythonArgs { /// Python code to execute (has access to bpy) pub code: String, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct CreateMaterialArgs { /// Material name pub name: String, /// Base color [r, g, b, a] (0-1 range) pub color: Option<[f64; 4]>, /// Metallic value (0-1) pub metallic: Option<f64>, /// Roughness value (0-1) pub roughness: Option<f64>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct AssignMaterialArgs { /// Object name pub object: String, /// Material name pub material: String, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct RenderImageArgs { /// Output file path pub output: String, /// Render engine: CYCLES, EEVEE (default: current) pub engine: Option<String>, /// Resolution X pub width: Option<u32>, /// Resolution Y pub height: Option<u32>, /// Samples (for Cycles) pub samples: Option<u32>, } // New tool args #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct SetSelectionArgs { /// Object names to select pub names: Vec<String>, /// Object to make active pub active: Option<String>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct DuplicateObjectArgs { /// Object name to duplicate pub name: String, /// New object name pub new_name: Option<String>, /// Offset from original [x, y, z] pub offset: Option<[f64; 3]>, /// Link data instead of copy pub linked: Option<bool>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ParentObjectsArgs { /// Parent object name pub parent: String, /// Children object names pub children: Vec<String>, /// Keep world transform pub keep_transform: Option<bool>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct SetModeArgs { /// Mode: OBJECT, EDIT, SCULPT, VERTEX_PAINT, WEIGHT_PAINT, TEXTURE_PAINT pub mode: String, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct AddModifierArgs { /// Object name pub name: String, /// Modifier type: SUBSURF, MIRROR, ARRAY, BEVEL, BOOLEAN, etc. #[serde(rename = "type")] pub mod_type: String, /// Optional modifier name pub mod_name: Option<String>, /// Modifier properties as JSON object pub properties: Option<serde_json::Value>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ApplyModifierArgs { /// Object name pub name: String, /// Modifier name to apply pub modifier: String, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ExportSceneArgs { /// Output file path pub path: String, /// Format: fbx, obj, gltf, usd pub format: Option<String>, /// Export selected objects only pub selected_only: Option<bool>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ImportFileArgs { /// Input file path pub path: String, /// Format (auto-detected from extension if not provided) pub format: Option<String>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct SaveFileArgs { /// Save path (optional, uses current if not provided) pub path: Option<String>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct SetKeyframeArgs { /// Object name pub name: String, /// Frame number (default: current) pub frame: Option<i32>, /// Property to keyframe: location, rotation_euler, scale pub property: Option<String>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct MoveToCollectionArgs { /// Object name pub object: String, /// Collection name (created if not exists) pub collection: String, } // ============================================================================ // Tool Implementations // ============================================================================ #[tool_router] impl BlenderServer { #[tool( name = "scene_info", description = "Get current scene information: name, fps, frame range, resolution" )] async fn scene_info( &self, Parameters(_args): Parameters<EmptyArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("scene_info", serde_json::json!({})).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool( name = "list_objects", description = "List all objects in the scene. Can filter by type (MESH, LIGHT, CAMERA, EMPTY) or collection." )] async fn list_objects( &self, Parameters(args): Parameters<ListObjectsArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("list_objects", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool( name = "get_object", description = "Get detailed information about a specific object: transform, type, mesh data" )] async fn get_object( &self, Parameters(args): Parameters<GetObjectArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("get_object", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool( name = "create_primitive", description = "Create a primitive mesh object. Types: cube, sphere, cylinder, plane, cone, torus" )] async fn create_primitive( &self, Parameters(args): Parameters<CreatePrimitiveArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("create_primitive", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool( name = "update_object", description = "Update object transform: location, rotation (euler radians), scale" )] async fn update_object( &self, Parameters(args): Parameters<UpdateObjectArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("update_object", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "delete_object", description = "Delete an object from the scene")] async fn delete_object( &self, Parameters(args): Parameters<DeleteObjectArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("delete_object", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool( name = "list_materials", description = "List all materials in the scene" )] async fn list_materials( &self, Parameters(_args): Parameters<EmptyArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("list_materials", serde_json::json!({})).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool( name = "create_material", description = "Create a new Principled BSDF material with color, metallic, roughness" )] async fn create_material( &self, Parameters(args): Parameters<CreateMaterialArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("create_material", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool( name = "assign_material", description = "Assign a material to an object" )] async fn assign_material( &self, Parameters(args): Parameters<AssignMaterialArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("assign_material", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool( name = "render_image", description = "Render current view to an image file. Supports CYCLES and EEVEE engines." )] async fn render_image( &self, Parameters(args): Parameters<RenderImageArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("render_image", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool( name = "execute_python", description = "Execute arbitrary Python code in Blender. Has access to bpy module. Returns the value of the last expression or print output." )] async fn execute_python( &self, Parameters(args): Parameters<ExecutePythonArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("execute_python", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } // === New tools === #[tool(name = "get_selection", description = "Get currently selected objects and active object")] async fn get_selection( &self, Parameters(_args): Parameters<EmptyArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("get_selection", serde_json::json!({})).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "set_selection", description = "Select objects by name and optionally set active object")] async fn set_selection( &self, Parameters(args): Parameters<SetSelectionArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("set_selection", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "duplicate_object", description = "Duplicate an object with optional offset and naming")] async fn duplicate_object( &self, Parameters(args): Parameters<DuplicateObjectArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("duplicate_object", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "parent_objects", description = "Set parent-child relationship between objects")] async fn parent_objects( &self, Parameters(args): Parameters<ParentObjectsArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("parent_objects", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "set_mode", description = "Set edit mode: OBJECT, EDIT, SCULPT, VERTEX_PAINT, WEIGHT_PAINT, TEXTURE_PAINT")] async fn set_mode( &self, Parameters(args): Parameters<SetModeArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("set_mode", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "get_modifiers", description = "Get list of modifiers on an object")] async fn get_modifiers( &self, Parameters(args): Parameters<GetObjectArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("get_modifiers", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "add_modifier", description = "Add modifier to object. Types: SUBSURF, MIRROR, ARRAY, BEVEL, BOOLEAN, SOLIDIFY, etc.")] async fn add_modifier( &self, Parameters(args): Parameters<AddModifierArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("add_modifier", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "apply_modifier", description = "Apply a modifier to an object (makes it permanent)")] async fn apply_modifier( &self, Parameters(args): Parameters<ApplyModifierArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("apply_modifier", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "export_scene", description = "Export scene to file. Formats: fbx, obj, gltf, usd")] async fn export_scene( &self, Parameters(args): Parameters<ExportSceneArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("export_scene", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "import_file", description = "Import 3D file. Formats: fbx, obj, gltf/glb, usd")] async fn import_file( &self, Parameters(args): Parameters<ImportFileArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("import_file", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "undo", description = "Undo last action")] async fn undo( &self, Parameters(_args): Parameters<EmptyArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("undo", serde_json::json!({})).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "redo", description = "Redo last undone action")] async fn redo( &self, Parameters(_args): Parameters<EmptyArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("redo", serde_json::json!({})).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "save_file", description = "Save current file. Optionally save to new path.")] async fn save_file( &self, Parameters(args): Parameters<SaveFileArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("save_file", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "set_keyframe", description = "Insert keyframe on object property at frame")] async fn set_keyframe( &self, Parameters(args): Parameters<SetKeyframeArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("set_keyframe", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "get_collections", description = "Get scene collection hierarchy with objects")] async fn get_collections( &self, Parameters(_args): Parameters<EmptyArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("get_collections", serde_json::json!({})).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } #[tool(name = "move_to_collection", description = "Move object to collection (creates if not exists)")] async fn move_to_collection( &self, Parameters(args): Parameters<MoveToCollectionArgs>, ) -> Result<CallToolResult, McpError> { let result = self.call_blender("move_to_collection", &args).await?; Ok(CallToolResult::success(vec![Content::text(result)])) } } #[tool_handler] impl ServerHandler for BlenderServer { fn get_info(&self) -> ServerInfo { ServerInfo { protocol_version: Default::default(), capabilities: ServerCapabilities::builder().enable_tools().build(), server_info: rmcp::model::Implementation { name: format!("blender-mcp-rs:{}", self.tag), version: env!("CARGO_PKG_VERSION").to_string(), title: None, icons: None, website_url: None, }, instructions: Some( "Blender MCP Server. Control Blender 3D through MCP tools.".to_string(), ), } } } /// Response handler task - routes responses from Python to pending requests async fn response_handler(result_rx: Receiver<Response>, pending: PendingRequests) { loop { match result_rx.recv() { Ok(response) => { let mut pending = pending.lock().await; if let Some(tx) = pending.remove(&response.id) { let _ = tx.send(response); } } Err(_) => break, // Channel closed } } } /// Run MCP server pub async fn run_mcp_server( port: u16, cmd_tx: Sender<Command>, result_rx: Receiver<Response>, running: Arc<AtomicBool>, port_tx: std::sync::mpsc::Sender<u16>, tag: &str, ) -> anyhow::Result<()> { eprintln!("[MCP] run_mcp_server starting for '{}' on port {}", tag, port); let server = BlenderServer::new(cmd_tx, tag.to_string()); let pending = server.pending.clone(); // Spawn response handler tokio::spawn(response_handler(result_rx, pending)); // Bind to port (0 = auto-assign) let addr = format!("127.0.0.1:{}", port); eprintln!("[MCP] Binding to {}", addr); let listener = tokio::net::TcpListener::bind(&addr).await?; let actual_port = listener.local_addr()?.port(); eprintln!("[MCP] Bound to port {}", actual_port); // Send actual port back eprintln!("[MCP] Sending port {} back via channel", actual_port); port_tx.send(actual_port)?; eprintln!("[MCP] Port sent successfully"); tracing::info!("MCP server '{}' listening on http://127.0.0.1:{}/mcp", tag, actual_port); eprintln!("[MCP] Server '{}' listening on http://127.0.0.1:{}/mcp", tag, actual_port); // Create MCP HTTP service let service = StreamableHttpService::new( move || Ok(server.clone()), LocalSessionManager::default().into(), Default::default(), ); let router = axum::Router::new() .nest_service("/mcp", service) .route("/health", axum::routing::get(|| async { "OK" })); // Run with graceful shutdown axum::serve(listener, router) .with_graceful_shutdown(async move { while running.load(Ordering::SeqCst) { tokio::time::sleep(Duration::from_millis(100)).await; } }) .await?; Ok(()) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ssoj13/blender-mcp-rs'

If you have feedback or need assistance with the MCP directory API, please join our Discord server