//! 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(¶ms).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(())
}