716 lines
28 KiB
Rust
716 lines
28 KiB
Rust
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<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[schemars(description = "Sort order: 'asc' (default) or 'desc'")]
|
|
pub sort_order: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
|
pub struct CreateSessionParam {
|
|
#[schemars(description = "Session name. If omitted, auto-generated.")]
|
|
pub name: Option<String>,
|
|
#[schemars(description = "Session description.")]
|
|
pub description: Option<String>,
|
|
#[schemars(description = "Name of the agent creating the session (identifies the creator).")]
|
|
pub created_by_name: Option<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[schemars(description = "New session description.")]
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[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<String>,
|
|
#[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<String>,
|
|
#[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<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[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<f32>,
|
|
#[schemars(
|
|
description = "Y position (Rectangle/Text top-left Y, or Line/Arrow start Y via y1)"
|
|
)]
|
|
pub y: Option<f32>,
|
|
#[schemars(description = "Width (Rectangle only)")]
|
|
pub width: Option<f32>,
|
|
#[schemars(description = "Height (Rectangle only)")]
|
|
pub height: Option<f32>,
|
|
#[schemars(description = "Center X (Ellipse only; falls back to x)")]
|
|
pub center_x: Option<f32>,
|
|
#[schemars(description = "Center Y (Ellipse only; falls back to y)")]
|
|
pub center_y: Option<f32>,
|
|
#[schemars(description = "X radius (Ellipse only, default 50)")]
|
|
pub radius_x: Option<f32>,
|
|
#[schemars(description = "Y radius (Ellipse only, default 50)")]
|
|
pub radius_y: Option<f32>,
|
|
#[schemars(description = "Start X (Line/Arrow; falls back to x)")]
|
|
pub x1: Option<f32>,
|
|
#[schemars(description = "Start Y (Line/Arrow; falls back to y)")]
|
|
pub y1: Option<f32>,
|
|
#[schemars(description = "End X (Line/Arrow)")]
|
|
pub x2: Option<f32>,
|
|
#[schemars(description = "End Y (Line/Arrow)")]
|
|
pub y2: Option<f32>,
|
|
#[schemars(description = "Text content (Text shape only)")]
|
|
pub text: Option<String>,
|
|
#[schemars(description = "Font size in pixels (Text shape, default 20)")]
|
|
pub font_size: Option<f32>,
|
|
#[schemars(description = "Fill color as hex e.g. '#ff0000', or null for no fill")]
|
|
pub fill: Option<String>,
|
|
#[schemars(description = "Stroke color as hex e.g. '#ffffff' (default white)")]
|
|
pub stroke_color: Option<String>,
|
|
#[schemars(description = "Stroke width in pixels (default 2.0)")]
|
|
pub stroke_width: Option<f32>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[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<String>,
|
|
#[schemars(description = "X position")]
|
|
pub x: Option<f32>,
|
|
#[schemars(description = "Y position")]
|
|
pub y: Option<f32>,
|
|
#[schemars(description = "Width (Rectangle)")]
|
|
pub width: Option<f32>,
|
|
#[schemars(description = "Height (Rectangle)")]
|
|
pub height: Option<f32>,
|
|
#[schemars(description = "Center X (Ellipse)")]
|
|
pub center_x: Option<f32>,
|
|
#[schemars(description = "Center Y (Ellipse)")]
|
|
pub center_y: Option<f32>,
|
|
#[schemars(description = "X radius (Ellipse)")]
|
|
pub radius_x: Option<f32>,
|
|
#[schemars(description = "Y radius (Ellipse)")]
|
|
pub radius_y: Option<f32>,
|
|
#[schemars(description = "Start X (Line/Arrow)")]
|
|
pub x1: Option<f32>,
|
|
#[schemars(description = "Start Y (Line/Arrow)")]
|
|
pub y1: Option<f32>,
|
|
#[schemars(description = "End X (Line/Arrow)")]
|
|
pub x2: Option<f32>,
|
|
#[schemars(description = "End Y (Line/Arrow)")]
|
|
pub y2: Option<f32>,
|
|
#[schemars(description = "Text content (Text shape)")]
|
|
pub text: Option<String>,
|
|
#[schemars(description = "Font size (Text shape)")]
|
|
pub font_size: Option<f32>,
|
|
#[schemars(description = "Fill color as hex e.g. '#ff0000'")]
|
|
pub fill: Option<String>,
|
|
#[schemars(description = "Stroke color as hex e.g. '#ffffff'")]
|
|
pub stroke_color: Option<String>,
|
|
#[schemars(description = "Stroke width in pixels")]
|
|
pub stroke_width: Option<f32>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[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<String>,
|
|
#[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<String>,
|
|
#[schemars(description = "If true, delete source elements after combining")]
|
|
pub consume: Option<bool>,
|
|
#[schemars(description = "Fill color for result as hex e.g. '#ff0000'")]
|
|
pub fill: Option<String>,
|
|
#[schemars(description = "Stroke color for result as hex e.g. '#ffffff'")]
|
|
pub stroke_color: Option<String>,
|
|
#[schemars(description = "Stroke width for result in pixels")]
|
|
pub stroke_width: Option<f32>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[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<f32>,
|
|
#[schemars(description = "Y position on canvas (default: 0)")]
|
|
pub y: Option<f32>,
|
|
#[schemars(description = "Override width (default: natural SVG width)")]
|
|
pub width: Option<f32>,
|
|
#[schemars(description = "Override height (default: natural SVG height)")]
|
|
pub height: Option<f32>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[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<f32>,
|
|
#[schemars(description = "Background color as hex e.g. '#1a1a2e' (default dark canvas color)")]
|
|
pub background: Option<String>,
|
|
}
|
|
|
|
#[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<Self>,
|
|
}
|
|
|
|
#[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<ListSessionsParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<CreateSessionParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<UpdateSessionParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<SessionIdParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<SessionIdParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<GetElementParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<GetElementsAtPointParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<SessionIdParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<GenerateCodeParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<CreateDrawingElementParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<UpdateDrawingElementParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<BooleanOpParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<RenderMermaidParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<ExportCanvasParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<BatchParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
let requests = match serde_json::from_str::<serde_json::Value>(¶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<DeleteDrawingElementParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<SessionIdParam>,
|
|
) -> Result<CallToolResult, McpError> {
|
|
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<CallToolResult, McpError> {
|
|
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()
|
|
}
|
|
}
|
|
}
|