use crate::bridge::send_request; use rmcp::{ handler::server::{router::tool::ToolRouter, wrapper::Parameters}, model::*, schemars, tool, tool_handler, tool_router, ErrorData as McpError, ServerHandler, }; #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct SessionIdParam { #[schemars(description = "Session ID to target. If omitted, uses the active session.")] pub session_id: Option, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct ListSessionsParam { #[schemars( description = "Sort field: 'name', 'created_at' (default), 'created_by', or 'element_count'" )] pub sort_by: Option, #[schemars(description = "Sort order: 'asc' (default) or 'desc'")] pub sort_order: Option, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct CreateSessionParam { #[schemars(description = "Session name. If omitted, auto-generated.")] pub name: Option, #[schemars(description = "Session description.")] pub description: Option, #[schemars(description = "Name of the agent creating the session (identifies the creator).")] pub created_by_name: Option, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct UpdateSessionParam { #[schemars(description = "ID of the session to update.")] pub session_id: String, #[schemars(description = "New session name.")] pub name: Option, #[schemars(description = "New session description.")] pub description: Option, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct GetElementParam { #[schemars(description = "Session ID to target. If omitted, uses the active session.")] pub session_id: Option, #[schemars(description = "Element ID to look up")] pub id: String, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct GetElementsAtPointParam { #[schemars(description = "Session ID to target. If omitted, uses the active session.")] pub session_id: Option, #[schemars(description = "X coordinate in canvas space")] pub x: f32, #[schemars(description = "Y coordinate in canvas space")] pub y: f32, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct GenerateCodeParam { #[schemars(description = "Session ID to target. If omitted, uses the active session.")] pub session_id: Option, #[schemars(description = "Code generation target: html, react, tailwind, svelte, or vue")] pub target: String, #[schemars( description = "Element ID to generate code for. If omitted, generates for the entire tree." )] pub element_id: Option, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct CreateDrawingElementParam { #[schemars(description = "Session ID to target. If omitted, uses the active session.")] pub session_id: Option, #[schemars(description = "Shape type: Rectangle, Ellipse, Line, Arrow, or Text")] pub shape_type: String, #[schemars( description = "X position (Rectangle/Text top-left X, or Line/Arrow start X via x1)" )] pub x: Option, #[schemars( description = "Y position (Rectangle/Text top-left Y, or Line/Arrow start Y via y1)" )] pub y: Option, #[schemars(description = "Width (Rectangle only)")] pub width: Option, #[schemars(description = "Height (Rectangle only)")] pub height: Option, #[schemars(description = "Center X (Ellipse only; falls back to x)")] pub center_x: Option, #[schemars(description = "Center Y (Ellipse only; falls back to y)")] pub center_y: Option, #[schemars(description = "X radius (Ellipse only, default 50)")] pub radius_x: Option, #[schemars(description = "Y radius (Ellipse only, default 50)")] pub radius_y: Option, #[schemars(description = "Start X (Line/Arrow; falls back to x)")] pub x1: Option, #[schemars(description = "Start Y (Line/Arrow; falls back to y)")] pub y1: Option, #[schemars(description = "End X (Line/Arrow)")] pub x2: Option, #[schemars(description = "End Y (Line/Arrow)")] pub y2: Option, #[schemars(description = "Text content (Text shape only)")] pub text: Option, #[schemars(description = "Font size in pixels (Text shape, default 20)")] pub font_size: Option, #[schemars(description = "Fill color as hex e.g. '#ff0000', or null for no fill")] pub fill: Option, #[schemars(description = "Stroke color as hex e.g. '#ffffff' (default white)")] pub stroke_color: Option, #[schemars(description = "Stroke width in pixels (default 2.0)")] pub stroke_width: Option, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct UpdateDrawingElementParam { #[schemars(description = "Session ID to target. If omitted, uses the active session.")] pub session_id: Option, #[schemars(description = "ID of the drawing element to update")] pub id: String, #[schemars(description = "New shape type (provide with coordinates to replace shape)")] pub shape_type: Option, #[schemars(description = "X position")] pub x: Option, #[schemars(description = "Y position")] pub y: Option, #[schemars(description = "Width (Rectangle)")] pub width: Option, #[schemars(description = "Height (Rectangle)")] pub height: Option, #[schemars(description = "Center X (Ellipse)")] pub center_x: Option, #[schemars(description = "Center Y (Ellipse)")] pub center_y: Option, #[schemars(description = "X radius (Ellipse)")] pub radius_x: Option, #[schemars(description = "Y radius (Ellipse)")] pub radius_y: Option, #[schemars(description = "Start X (Line/Arrow)")] pub x1: Option, #[schemars(description = "Start Y (Line/Arrow)")] pub y1: Option, #[schemars(description = "End X (Line/Arrow)")] pub x2: Option, #[schemars(description = "End Y (Line/Arrow)")] pub y2: Option, #[schemars(description = "Text content (Text shape)")] pub text: Option, #[schemars(description = "Font size (Text shape)")] pub font_size: Option, #[schemars(description = "Fill color as hex e.g. '#ff0000'")] pub fill: Option, #[schemars(description = "Stroke color as hex e.g. '#ffffff'")] pub stroke_color: Option, #[schemars(description = "Stroke width in pixels")] pub stroke_width: Option, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct DeleteDrawingElementParam { #[schemars(description = "Session ID to target. If omitted, uses the active session.")] pub session_id: Option, #[schemars(description = "ID of the drawing element to delete")] pub id: String, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct BooleanOpParam { #[schemars(description = "Session ID to target. If omitted, uses the active session.")] pub session_id: Option, #[schemars(description = "Boolean operation: union, intersection, difference, or xor")] pub operation: String, #[schemars(description = "IDs of drawing elements to combine (minimum 2)")] pub element_ids: Vec, #[schemars(description = "If true, delete source elements after combining")] pub consume: Option, #[schemars(description = "Fill color for result as hex e.g. '#ff0000'")] pub fill: Option, #[schemars(description = "Stroke color for result as hex e.g. '#ffffff'")] pub stroke_color: Option, #[schemars(description = "Stroke width for result in pixels")] pub stroke_width: Option, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct RenderMermaidParam { #[schemars(description = "Session ID to target. If omitted, uses the active session.")] pub session_id: Option, #[schemars(description = "Mermaid diagram source code (e.g., 'flowchart LR\\n A-->B')")] pub mermaid_source: String, #[schemars(description = "X position on canvas (default: 0)")] pub x: Option, #[schemars(description = "Y position on canvas (default: 0)")] pub y: Option, #[schemars(description = "Override width (default: natural SVG width)")] pub width: Option, #[schemars(description = "Override height (default: natural SVG height)")] pub height: Option, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct ExportCanvasParam { #[schemars(description = "Session ID to target. If omitted, uses the active session.")] pub session_id: Option, #[schemars(description = "File path to save the PNG export to")] pub path: String, #[schemars(description = "Scale factor for the export (default 2.0 for high DPI)")] pub scale: Option, #[schemars(description = "Background color as hex e.g. '#1a1a2e' (default dark canvas color)")] pub background: Option, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct BatchParam { #[schemars( description = "JSON array of request objects. Each object must have a 'type' field and the same parameters as individual tool calls. Example: [{\"type\": \"CreateDrawingElement\", \"shape_type\": \"rectangle\", \"x\": 0, \"y\": 0, \"width\": 100, \"height\": 100}, {\"type\": \"CreateDrawingElement\", \"shape_type\": \"ellipse\", \"center_x\": 200, \"center_y\": 200, \"radius_x\": 50, \"radius_y\": 50}]" )] pub requests_json: String, } #[derive(Debug, Clone)] pub struct AgCanvasServer { ws_url: String, tool_router: ToolRouter, } #[tool_router] impl AgCanvasServer { pub fn new(ws_url: String) -> Self { Self { ws_url, tool_router: Self::tool_router(), } } #[tool( description = "List all open sessions/tabs in Augmented Canvas. Returns session IDs, names, creator info, description, timestamps, and whether they have SVG or drawing content loaded. Supports sorting." )] async fn list_sessions( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({"type": "ListSessions"}); let obj = request.as_object_mut().unwrap(); if let Some(v) = params.sort_by { obj.insert("sort_by".into(), v.into()); } if let Some(v) = params.sort_order { obj.insert("sort_order".into(), v.into()); } self.call_agcanvas(&request).await } #[tool( description = "Create a new session/tab in Augmented Canvas. The session is created by an agent. Returns the created session with its ID and metadata." )] async fn create_session( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({"type": "CreateSession"}); let obj = request.as_object_mut().unwrap(); if let Some(v) = params.name { obj.insert("name".into(), v.into()); } if let Some(v) = params.description { obj.insert("description".into(), v.into()); } if let Some(v) = params.created_by_name { obj.insert("created_by_name".into(), v.into()); } self.call_agcanvas(&request).await } #[tool( description = "Update an existing session's name or description. Only provided fields are changed." )] async fn update_session( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({ "type": "UpdateSession", "session_id": params.session_id, }); let obj = request.as_object_mut().unwrap(); if let Some(v) = params.name { obj.insert("name".into(), v.into()); } if let Some(v) = params.description { obj.insert("description".into(), v.into()); } self.call_agcanvas(&request).await } #[tool( description = "Get the full parsed SVG element tree from the canvas. Returns a hierarchical JSON structure with element types (Group, Rectangle, Circle, Path, Text, etc.), bounds, styles, and children. This is the primary way to understand what design is on the canvas." )] async fn get_element_tree( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({"type": "GetTree"}); if let Some(sid) = params.session_id { request["session_id"] = serde_json::Value::String(sid); } self.call_agcanvas(&request).await } #[tool( description = "Get a human-readable semantic description of the canvas content. Returns structured text describing the element hierarchy with types, dimensions, and colors. Useful for quickly understanding a design without parsing JSON." )] async fn describe_canvas( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({"type": "Describe"}); if let Some(sid) = params.session_id { request["session_id"] = serde_json::Value::String(sid); } self.call_agcanvas(&request).await } #[tool(description = "Get a specific element by its ID from the SVG element tree.")] async fn get_element_by_id( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({"type": "GetElementById", "id": params.id}); if let Some(sid) = params.session_id { request["session_id"] = serde_json::Value::String(sid); } self.call_agcanvas(&request).await } #[tool( description = "Query which elements exist at a specific (x, y) coordinate on the canvas." )] async fn get_elements_at_point( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({"type": "GetElementsAtPoint", "x": params.x, "y": params.y}); if let Some(sid) = params.session_id { request["session_id"] = serde_json::Value::String(sid); } self.call_agcanvas(&request).await } #[tool( description = "Get all user-drawn shapes (rectangles, ellipses, lines, arrows, text) from the drawing layer." )] async fn get_drawing_elements( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({"type": "GetDrawingElements"}); if let Some(sid) = params.session_id { request["session_id"] = serde_json::Value::String(sid); } self.call_agcanvas(&request).await } #[tool( description = "Generate a code stub from the SVG structure. Targets: html, react, tailwind, svelte, vue. Returns a starting template based on the element hierarchy." )] async fn generate_code( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({ "type": "GenerateCode", "target": params.target, "element_id": params.element_id, }); if let Some(sid) = params.session_id { request["session_id"] = serde_json::Value::String(sid); } self.call_agcanvas(&request).await } #[tool( description = "Create a drawing element on the canvas. Shape types: Rectangle (x,y,width,height), Ellipse (center_x,center_y,radius_x,radius_y), Line (x1,y1,x2,y2), Arrow (x1,y1,x2,y2), Text (x,y,text,font_size). Colors are hex strings like '#ff0000'. Returns the created element with its auto-generated ID." )] async fn create_drawing_element( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({ "type": "CreateDrawingElement", "shape_type": params.shape_type, }); let obj = request.as_object_mut().unwrap(); if let Some(v) = params.session_id { obj.insert("session_id".into(), v.into()); } if let Some(v) = params.x { obj.insert("x".into(), v.into()); } if let Some(v) = params.y { obj.insert("y".into(), v.into()); } if let Some(v) = params.width { obj.insert("width".into(), v.into()); } if let Some(v) = params.height { obj.insert("height".into(), v.into()); } if let Some(v) = params.center_x { obj.insert("center_x".into(), v.into()); } if let Some(v) = params.center_y { obj.insert("center_y".into(), v.into()); } if let Some(v) = params.radius_x { obj.insert("radius_x".into(), v.into()); } if let Some(v) = params.radius_y { obj.insert("radius_y".into(), v.into()); } if let Some(v) = params.x1 { obj.insert("x1".into(), v.into()); } if let Some(v) = params.y1 { obj.insert("y1".into(), v.into()); } if let Some(v) = params.x2 { obj.insert("x2".into(), v.into()); } if let Some(v) = params.y2 { obj.insert("y2".into(), v.into()); } if let Some(v) = params.text { obj.insert("text".into(), v.into()); } if let Some(v) = params.font_size { obj.insert("font_size".into(), v.into()); } if let Some(v) = params.fill { obj.insert("fill".into(), v.into()); } if let Some(v) = params.stroke_color { obj.insert("stroke_color".into(), v.into()); } if let Some(v) = params.stroke_width { obj.insert("stroke_width".into(), v.into()); } self.call_agcanvas(&request).await } #[tool( description = "Update an existing drawing element. Provide shape_type + coordinates to replace the shape. Provide fill/stroke_color/stroke_width to update the style. Only provided fields are changed." )] async fn update_drawing_element( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({ "type": "UpdateDrawingElement", "id": params.id, }); let obj = request.as_object_mut().unwrap(); if let Some(v) = params.session_id { obj.insert("session_id".into(), v.into()); } if let Some(v) = params.shape_type { obj.insert("shape_type".into(), v.into()); } if let Some(v) = params.x { obj.insert("x".into(), v.into()); } if let Some(v) = params.y { obj.insert("y".into(), v.into()); } if let Some(v) = params.width { obj.insert("width".into(), v.into()); } if let Some(v) = params.height { obj.insert("height".into(), v.into()); } if let Some(v) = params.center_x { obj.insert("center_x".into(), v.into()); } if let Some(v) = params.center_y { obj.insert("center_y".into(), v.into()); } if let Some(v) = params.radius_x { obj.insert("radius_x".into(), v.into()); } if let Some(v) = params.radius_y { obj.insert("radius_y".into(), v.into()); } if let Some(v) = params.x1 { obj.insert("x1".into(), v.into()); } if let Some(v) = params.y1 { obj.insert("y1".into(), v.into()); } if let Some(v) = params.x2 { obj.insert("x2".into(), v.into()); } if let Some(v) = params.y2 { obj.insert("y2".into(), v.into()); } if let Some(v) = params.text { obj.insert("text".into(), v.into()); } if let Some(v) = params.font_size { obj.insert("font_size".into(), v.into()); } if let Some(v) = params.fill { obj.insert("fill".into(), v.into()); } if let Some(v) = params.stroke_color { obj.insert("stroke_color".into(), v.into()); } if let Some(v) = params.stroke_width { obj.insert("stroke_width".into(), v.into()); } self.call_agcanvas(&request).await } #[tool( description = "Perform a boolean operation (union, intersection, difference, xor) on two or more drawing elements. Combines filled shapes into a new path element. Only works on Rectangle and Ellipse shapes." )] async fn boolean_op( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({ "type": "BooleanOp", "operation": params.operation, "element_ids": params.element_ids, }); let obj = request.as_object_mut().unwrap(); if let Some(v) = params.session_id { obj.insert("session_id".into(), v.into()); } if let Some(v) = params.consume { obj.insert("consume".into(), v.into()); } if let Some(v) = params.fill { obj.insert("fill".into(), v.into()); } if let Some(v) = params.stroke_color { obj.insert("stroke_color".into(), v.into()); } if let Some(v) = params.stroke_width { obj.insert("stroke_width".into(), v.into()); } self.call_agcanvas(&request).await } #[tool( name = "render_mermaid", description = "Render a Mermaid diagram (flowchart, sequence, etc.) as an SVG overlay at a specific position on the canvas. The diagram appears as a visual element that can coexist with other shapes and diagrams." )] async fn render_mermaid( &self, Parameters(params): Parameters, ) -> Result { let request = serde_json::json!({ "type": "RenderMermaid", "session_id": params.session_id, "mermaid_source": params.mermaid_source, "x": params.x, "y": params.y, "width": params.width, "height": params.height, }); self.call_agcanvas(&request).await } #[tool( description = "Export the canvas as a PNG image. Renders all layers (SVG, drawing elements) into a single image file." )] async fn export_canvas( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({ "type": "ExportCanvas", "path": params.path, }); if let Some(sid) = params.session_id { request["session_id"] = serde_json::Value::String(sid); } if let Some(s) = params.scale { request["scale"] = serde_json::json!(s); } if let Some(bg) = params.background { request["background"] = serde_json::Value::String(bg); } self.call_agcanvas(&request).await } #[tool( description = "Send multiple Augmented Canvas operations in one request. Accepts a JSON array of request objects and returns one response per operation in a BatchResult." )] async fn batch( &self, Parameters(params): Parameters, ) -> Result { let requests = match serde_json::from_str::(¶ms.requests_json) { Ok(serde_json::Value::Array(requests)) => requests, Ok(_) => { return Ok(CallToolResult::error(vec![Content::text( "Invalid requests_json: expected a JSON array of request objects", )])) } Err(e) => { return Ok(CallToolResult::error(vec![Content::text(format!( "Invalid requests_json: {}", e ))])) } }; let request = serde_json::json!({ "type": "Batch", "requests": requests, }); self.call_agcanvas(&request).await } #[tool(description = "Delete a drawing element by its ID.")] async fn delete_drawing_element( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({ "type": "DeleteDrawingElement", "id": params.id, }); if let Some(sid) = params.session_id { request["session_id"] = serde_json::Value::String(sid); } self.call_agcanvas(&request).await } #[tool(description = "Clear all drawing elements from the canvas.")] async fn clear_drawing_elements( &self, Parameters(params): Parameters, ) -> Result { let mut request = serde_json::json!({"type": "ClearDrawingElements"}); if let Some(sid) = params.session_id { request["session_id"] = serde_json::Value::String(sid); } self.call_agcanvas(&request).await } } impl AgCanvasServer { async fn call_agcanvas(&self, request: &serde_json::Value) -> Result { let request_str = serde_json::to_string(request) .map_err(|e| McpError::internal_error(e.to_string(), None))?; match send_request(&self.ws_url, &request_str).await { Ok(response) => { let parsed: serde_json::Value = serde_json::from_str(&response) .map_err(|e| McpError::internal_error(e.to_string(), None))?; let is_error = parsed.get("type").and_then(serde_json::Value::as_str) == Some("Error"); if is_error { let msg = parsed .get("message") .and_then(serde_json::Value::as_str) .unwrap_or("Unknown error from Augmented Canvas"); return Ok(CallToolResult::error(vec![Content::text(msg)])); } let pretty = serde_json::to_string_pretty(&parsed).unwrap_or_else(|_| response.clone()); Ok(CallToolResult::success(vec![Content::text(pretty)])) } Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( "Failed to communicate with Augmented Canvas: {}. Make sure Augmented Canvas is running.", e ))])), } } } #[tool_handler] impl ServerHandler for AgCanvasServer { fn get_info(&self) -> ServerInfo { ServerInfo { instructions: Some( "Augmented Canvas MCP server — connects to the Augmented Canvas desktop app to query SVG designs, \ element trees, and user-drawn shapes. Use describe_canvas to understand the \ current design, get_element_tree for structured data, and generate_code for \ code stubs. Requires Augmented Canvas to be running." .into(), ), capabilities: ServerCapabilities::builder().enable_tools().build(), server_info: Implementation { name: "agcanvas-mcp".into(), version: env!("CARGO_PKG_VERSION").into(), title: None, icons: None, website_url: None, }, ..Default::default() } } }