Add agent drawing commands, session management, and MCP write tools
- Add CreateDrawingElement, UpdateDrawingElement, DeleteDrawingElement, ClearDrawingElements to WebSocket protocol and MCP bridge - Add multi-session/tab support with SessionStore shared state - Add bidirectional agent-GUI sync via broadcast + mpsc channels - Update README with correct OpenCode MCP config format and new tools - Fix dead code warning, clean up gitignore
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
use crate::bridge::send_request;
|
||||
use rmcp::{
|
||||
ErrorData as McpError, ServerHandler,
|
||||
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
|
||||
model::*,
|
||||
schemars, tool, tool_handler, tool_router,
|
||||
schemars, tool, tool_handler, tool_router, ErrorData as McpError, ServerHandler,
|
||||
};
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
@@ -36,10 +35,110 @@ pub struct GenerateCodeParam {
|
||||
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.")]
|
||||
#[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, Clone)]
|
||||
pub struct AgCanvasServer {
|
||||
ws_url: String,
|
||||
@@ -55,13 +154,17 @@ impl AgCanvasServer {
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "List all open sessions/tabs in agcanvas. Returns session IDs, names, and whether they have SVG or drawing content loaded.")]
|
||||
#[tool(
|
||||
description = "List all open sessions/tabs in agcanvas. Returns session IDs, names, and whether they have SVG or drawing content loaded."
|
||||
)]
|
||||
async fn list_sessions(&self) -> Result<CallToolResult, McpError> {
|
||||
let request = serde_json::json!({"type": "ListSessions"});
|
||||
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.")]
|
||||
#[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>,
|
||||
@@ -73,7 +176,9 @@ impl AgCanvasServer {
|
||||
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.")]
|
||||
#[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>,
|
||||
@@ -97,7 +202,9 @@ impl AgCanvasServer {
|
||||
self.call_agcanvas(&request).await
|
||||
}
|
||||
|
||||
#[tool(description = "Query which elements exist at a specific (x, y) coordinate on the canvas.")]
|
||||
#[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>,
|
||||
@@ -110,7 +217,9 @@ impl AgCanvasServer {
|
||||
self.call_agcanvas(&request).await
|
||||
}
|
||||
|
||||
#[tool(description = "Get all user-drawn shapes (rectangles, ellipses, lines, arrows, text) from the drawing layer.")]
|
||||
#[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>,
|
||||
@@ -122,7 +231,9 @@ impl AgCanvasServer {
|
||||
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.")]
|
||||
#[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>,
|
||||
@@ -137,13 +248,178 @@ impl AgCanvasServer {
|
||||
}
|
||||
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 = "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> {
|
||||
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))?;
|
||||
|
||||
@@ -152,10 +428,8 @@ impl AgCanvasServer {
|
||||
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");
|
||||
let is_error =
|
||||
parsed.get("type").and_then(serde_json::Value::as_str) == Some("Error");
|
||||
|
||||
if is_error {
|
||||
let msg = parsed
|
||||
@@ -165,8 +439,8 @@ impl AgCanvasServer {
|
||||
return Ok(CallToolResult::error(vec![Content::text(msg)]));
|
||||
}
|
||||
|
||||
let pretty = serde_json::to_string_pretty(&parsed)
|
||||
.unwrap_or_else(|_| response.clone());
|
||||
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!(
|
||||
|
||||
Reference in New Issue
Block a user