Add drawing tools, Mermaid support, and MCP server bridge
- Convert flat project to Cargo workspace (crates/agcanvas, crates/agcanvas-mcp) - Add drawing layer: rectangles, ellipses, lines, arrows, text with select/move/resize - Add Mermaid diagram rendering via mermaid-rs-renderer - Add agcanvas-mcp: MCP server bridge for Claude Code, OpenCode, and Codex - Add toolbar UI with keyboard shortcuts (V/R/E/L/A/T) and shape interaction - Add example MCP configs for Claude Code, OpenCode, and Codex - Update README with full feature docs, MCP setup, and updated architecture
This commit is contained in:
202
crates/agcanvas-mcp/src/tools.rs
Normal file
202
crates/agcanvas-mcp/src/tools.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
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,
|
||||
};
|
||||
|
||||
#[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 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, 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 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.")]
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 agcanvas");
|
||||
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 agcanvas: {}. Make sure agcanvas is running.",
|
||||
e
|
||||
))])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tool_handler]
|
||||
impl ServerHandler for AgCanvasServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
instructions: Some(
|
||||
"agcanvas MCP server — connects to agcanvas 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 agcanvas 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user