diff --git a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py
index 148ccac63..822c54b20 100644
--- a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py
+++ b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py
@@ -77,7 +77,10 @@ from gns3server.agent.gns3_copilot.skills.registry import get_prompt
from gns3server.agent.gns3_copilot.tools_v2 import (
ExecuteMultipleDeviceCommands,
ExecuteMultipleDeviceConfigCommands,
+ GNS3CreateEllipseDrawingTool,
GNS3CreateNodeTool,
+ GNS3CreateRectangleDrawingTool,
+ GNS3CreateTextDrawingTool,
GNS3LinkTool,
GNS3PacketFilterTool,
GNS3StartNodeTool,
@@ -103,6 +106,9 @@ TEACHING_ASSISTANT_MODE_TOOLS = [
GNS3TemplateTool(), # Get GNS3 node templates
GNS3CreateNodeTool(), # Create new nodes in GNS3
GNS3LinkTool(), # Create links between nodes
+ GNS3CreateTextDrawingTool(), # Add text labels to the canvas
+ GNS3CreateRectangleDrawingTool(), # Add rectangle annotations to the canvas
+ GNS3CreateEllipseDrawingTool(), # Add ellipse annotations to the canvas
GNS3StartNodeTool(), # Start GNS3 nodes (returns immediately)
GNS3WaitTool(), # Wait for nodes to boot (pair with start_gns3_node)
GNS3UpdateNodeNameTool(), # Update node name
@@ -118,6 +124,9 @@ LAB_AUTOMATION_ASSISTANT_MODE_TOOLS = [
GNS3TemplateTool(), # Get GNS3 node templates
GNS3CreateNodeTool(), # Create new nodes in GNS3
GNS3LinkTool(), # Create links between nodes
+ GNS3CreateTextDrawingTool(), # Add text labels to the canvas
+ GNS3CreateRectangleDrawingTool(), # Add rectangle annotations to the canvas
+ GNS3CreateEllipseDrawingTool(), # Add ellipse annotations to the canvas
GNS3StartNodeTool(), # Start GNS3 nodes (returns immediately)
GNS3WaitTool(), # Wait for nodes to boot (pair with start_gns3_node)
GNS3StopNodeTool(), # Stop GNS3 nodes
diff --git a/gns3server/agent/gns3_copilot/gns3_client/api_handlers.py b/gns3server/agent/gns3_copilot/gns3_client/api_handlers.py
index ba3fca6c7..44e03688b 100644
--- a/gns3server/agent/gns3_copilot/gns3_client/api_handlers.py
+++ b/gns3server/agent/gns3_copilot/gns3_client/api_handlers.py
@@ -55,6 +55,7 @@ from typing import Any
from gns3server.agent.gns3_copilot.gns3_client.connector import Gns3Connector
from gns3server.services import access_ticket_service
from gns3server.services.access_tickets import DEFAULT_TICKET_TTL
+from gns3server.utils.drawings import build_ellipse_svg, build_rectangle_svg, build_text_svg
log = logging.getLogger(__name__)
@@ -1076,3 +1077,95 @@ def marker_definition_handler(params: dict[str, Any], gns3_ctx: dict[str, Any])
# action == "delete"
conn.http_call("delete", url)
return {"message": f"Marker definition '{def_name}' deleted", "project_id": project_id, "def_name": def_name}
+
+
+# ── Drawing handlers ──────────────────────────────────────────────────────
+# SVG assembly and validation live in gns3server.utils.drawings (Web UI
+# compatible format); these handlers only carry params and do the REST call.
+
+
+def create_text_drawing_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
+ """Create a canvas text label; the SVG is assembled from validated params."""
+
+ project_id = params.get("project_id")
+ if not project_id:
+ return {"error": "project_id is required"}
+ try:
+ svg, width, height, line_height = build_text_svg(
+ params.get("text"),
+ font_size=params.get("font_size", 13),
+ color=params.get("color", "#000000"),
+ bold=params.get("bold", False),
+ font_family=params.get("font_family", "monospace"),
+ )
+ except ValueError as e:
+ return {"error": str(e)}
+ conn = _get_connector(gns3_ctx)
+ data = {"svg": svg, "x": params.get("x", 0), "y": params.get("y", 0), "z": params.get("z", 1)}
+ result = conn.http_call("post", f"{conn.base_url}/projects/{project_id}/drawings", json_data=data).json()
+ return {
+ "message": "Text drawing created",
+ "drawing": result,
+ "width": width,
+ "height": height,
+ "line_height": line_height,
+ }
+
+
+def create_rectangle_drawing_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
+ """Create a canvas rectangle; the SVG is assembled from validated params."""
+
+ project_id = params.get("project_id")
+ if not project_id:
+ return {"error": "project_id is required"}
+ width = params.get("width")
+ height = params.get("height")
+ if width is None or height is None:
+ return {"error": "width and height are required"}
+ try:
+ svg, width, height = build_rectangle_svg(
+ width,
+ height,
+ fill=params.get("fill", "#FFFFFF"),
+ fill_opacity=params.get("fill_opacity", 1.0),
+ stroke=params.get("stroke"),
+ stroke_width=params.get("stroke_width", 1),
+ dashed=params.get("dashed", False),
+ dasharray=params.get("dasharray"),
+ rx=params.get("rx", 0),
+ )
+ except ValueError as e:
+ return {"error": str(e)}
+ conn = _get_connector(gns3_ctx)
+ data = {"svg": svg, "x": params.get("x", 0), "y": params.get("y", 0), "z": params.get("z", 0)}
+ result = conn.http_call("post", f"{conn.base_url}/projects/{project_id}/drawings", json_data=data).json()
+ return {"message": "Rectangle drawing created", "drawing": result, "width": width, "height": height}
+
+
+def create_ellipse_drawing_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
+ """Create a canvas ellipse from a bounding box; the SVG is assembled from validated params."""
+
+ project_id = params.get("project_id")
+ if not project_id:
+ return {"error": "project_id is required"}
+ width = params.get("width")
+ height = params.get("height")
+ if width is None or height is None:
+ return {"error": "width and height are required"}
+ try:
+ svg, width, height = build_ellipse_svg(
+ width,
+ height,
+ fill=params.get("fill", "#FFFFFF"),
+ fill_opacity=params.get("fill_opacity", 1.0),
+ stroke=params.get("stroke"),
+ stroke_width=params.get("stroke_width", 1),
+ dashed=params.get("dashed", False),
+ dasharray=params.get("dasharray"),
+ )
+ except ValueError as e:
+ return {"error": str(e)}
+ conn = _get_connector(gns3_ctx)
+ data = {"svg": svg, "x": params.get("x", 0), "y": params.get("y", 0), "z": params.get("z", 0)}
+ result = conn.http_call("post", f"{conn.base_url}/projects/{project_id}/drawings", json_data=data).json()
+ return {"message": "Ellipse drawing created", "drawing": result, "width": width, "height": height}
diff --git a/gns3server/agent/gns3_copilot/tools_v2/__init__.py b/gns3server/agent/gns3_copilot/tools_v2/__init__.py
index 0818bd81c..078105ada 100644
--- a/gns3server/agent/gns3_copilot/tools_v2/__init__.py
+++ b/gns3server/agent/gns3_copilot/tools_v2/__init__.py
@@ -40,6 +40,7 @@ Main modules:
- vpcs_tools_netmiko: VPCS device configuration tool using Netmiko
- gns3_create_node: GNS3 node creation tool
- gns3_create_link: GNS3 link creation tool
+- gns3_create_drawing: GNS3 canvas drawing tools (text, rectangle, ellipse)
- gns3_start_node: GNS3 node startup tool (immediate return, no boot wait)
- gns3_wait: Wait timer tool (pair with node start to let devices boot)
- gns3_get_node_temp: GNS3 template retrieval tool
@@ -54,6 +55,11 @@ Author: Yue Guobin (岳国宾)
# Import main tool classes
from .config_tools_nornir import ExecuteMultipleDeviceConfigCommands
from .display_tools_nornir import ExecuteMultipleDeviceCommands
+from .gns3_create_drawing import (
+ GNS3CreateEllipseDrawingTool,
+ GNS3CreateRectangleDrawingTool,
+ GNS3CreateTextDrawingTool,
+)
from .gns3_create_link import GNS3LinkTool
from .gns3_create_node import GNS3CreateNodeTool
from .gns3_get_node_temp import GNS3TemplateTool
@@ -81,7 +87,10 @@ __url__ = "https://github.com/yueguobin/gns3-copilot"
__all__ = [
"ExecuteMultipleDeviceCommands",
"ExecuteMultipleDeviceConfigCommands",
+ "GNS3CreateEllipseDrawingTool",
"GNS3CreateNodeTool",
+ "GNS3CreateRectangleDrawingTool",
+ "GNS3CreateTextDrawingTool",
"GNS3LinkTool",
"GNS3PacketFilterTool",
"GNS3StartNodeQuickTool",
diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_create_drawing.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_create_drawing.py
new file mode 100644
index 000000000..7f3f688fe
--- /dev/null
+++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_create_drawing.py
@@ -0,0 +1,162 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# GNS3-Copilot - AI-powered Network Lab Assistant for GNS3
+#
+# This file is part of GNS3-Copilot project.
+#
+# GNS3-Copilot is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# GNS3-Copilot is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNS3-Copilot. If not, see .
+#
+# Copyright (C) 2025 Yue Guobin (岳国宾)
+# Author: Yue Guobin (岳国宾)
+#
+# Project Home: https://github.com/yueguobin/gns3-copilot
+#
+
+"""
+GNS3 canvas drawing tools for text, rectangle and ellipse annotations.
+
+Thin LangChain wrappers over the shared REST handlers in
+``gns3_client.api_handlers``; the Web UI compatible SVG is assembled and
+validated by ``gns3server.utils.drawings`` (single implementation shared
+with the MCP drawing tools).
+"""
+
+import json
+import logging
+from typing import Any
+
+from langchain.tools import BaseTool
+from langchain_core.callbacks import CallbackManagerForToolRun
+
+from gns3server.agent.gns3_copilot.gns3_client.api_handlers import (
+ build_gns3_ctx,
+ create_ellipse_drawing_handler,
+ create_rectangle_drawing_handler,
+ create_text_drawing_handler,
+)
+
+# Configure logging
+logger = logging.getLogger(__name__)
+
+
+class _GNS3CreateDrawingToolBase(BaseTool):
+ """Shared plumbing: parse JSON input, build ctx, dispatch to a handler."""
+
+ def _dispatch(self, input_data: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
+ raise NotImplementedError
+
+ def _run(
+ self,
+ tool_input: str,
+ run_manager: CallbackManagerForToolRun | None = None,
+ ) -> dict[str, Any]:
+ try:
+ input_data = json.loads(tool_input)
+
+ gns3_ctx = build_gns3_ctx()
+ if gns3_ctx is None:
+ logger.error("Failed to create GNS3 connector")
+ return {"error": "Failed to connect to GNS3 server. Please check your configuration."}
+
+ return self._dispatch(input_data, gns3_ctx)
+ except json.JSONDecodeError as e:
+ logger.error("Invalid JSON input: %s", e)
+ return {"error": f"Invalid JSON input: {e}"}
+ except Exception as e:
+ logger.exception("Drawing creation failed")
+ return {"error": str(e)}
+
+
+class GNS3CreateTextDrawingTool(_GNS3CreateDrawingToolBase):
+ """
+ A LangChain tool to add a text label to the GNS3 project canvas.
+
+ **Input**:
+ A JSON object with project_id, x, y (top-left corner on the canvas)
+ and text; optional font_size (1-200, default 13), color (#RRGGBB,
+ default #000000), bold (default false), font_family (default
+ monospace) and z (default 1, keeps text above links).
+ Newlines in text start additional lines; XML characters are escaped
+ automatically; the computed width/height are returned.
+
+ **Output**:
+ A dictionary with the created drawing, its drawing_id and the
+ computed width/height/line_height, or an "error" key on failure.
+ """
+
+ name: str = "create_gns3_text_drawing"
+ description: str = """
+ Adds a text label to the GNS3 project canvas.
+ Input: JSON with project_id, x, y (top-left corner), text and optional
+ font_size, color, bold, font_family, z.
+ Returns: the created drawing with its computed width and height.
+ """
+
+ def _dispatch(self, input_data: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
+ return create_text_drawing_handler(input_data, gns3_ctx)
+
+
+class GNS3CreateRectangleDrawingTool(_GNS3CreateDrawingToolBase):
+ """
+ A LangChain tool to add a rectangle to the GNS3 project canvas.
+
+ **Input**:
+ A JSON object with project_id, x, y (top-left corner), width and
+ height (1-2000); optional fill (#RRGGBB or "none", default #FFFFFF),
+ fill_opacity (0-1, default 1.0), stroke (#RRGGBB, no border by
+ default), stroke_width (1-20), dashed (bool), dasharray (like "4,2",
+ requires stroke) and rx (corner radius, default 0). z defaults to 0
+ so shapes stay behind nodes and text.
+
+ **Output**:
+ A dictionary with the created drawing, or an "error" key on failure.
+ """
+
+ name: str = "create_gns3_rectangle_drawing"
+ description: str = """
+ Adds a rectangle to the GNS3 project canvas.
+ Input: JSON with project_id, x, y, width, height and optional fill,
+ fill_opacity, stroke, stroke_width, dashed, dasharray, rx, z.
+ Returns: the created drawing with its width and height.
+ """
+
+ def _dispatch(self, input_data: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
+ return create_rectangle_drawing_handler(input_data, gns3_ctx)
+
+
+class GNS3CreateEllipseDrawingTool(_GNS3CreateDrawingToolBase):
+ """
+ A LangChain tool to add an ellipse (or circle) to the GNS3 canvas.
+
+ **Input**:
+ A JSON object with project_id, x, y (bounding box top-left corner),
+ width and height (2-2000, equal values give a circle); the same
+ optional style fields as the rectangle tool. cx/cy are derived from
+ the bounding box; for odd width/height the drawing is 1 px smaller
+ than requested and the actual size is returned.
+
+ **Output**:
+ A dictionary with the created drawing, or an "error" key on failure.
+ """
+
+ name: str = "create_gns3_ellipse_drawing"
+ description: str = """
+ Adds an ellipse or circle to the GNS3 project canvas.
+ Input: JSON with project_id, x, y, width, height (bounding box) and
+ optional fill, fill_opacity, stroke, stroke_width, dashed, dasharray, z.
+ Returns: the created drawing with its actual width and height.
+ """
+
+ def _dispatch(self, input_data: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
+ return create_ellipse_drawing_handler(input_data, gns3_ctx)
diff --git a/gns3server/agent/gns3_copilot/utils/gns3_drawing_utils.py b/gns3server/agent/gns3_copilot/utils/gns3_drawing_utils.py
index 50456cbf5..540e401ea 100644
--- a/gns3server/agent/gns3_copilot/utils/gns3_drawing_utils.py
+++ b/gns3server/agent/gns3_copilot/utils/gns3_drawing_utils.py
@@ -33,6 +33,8 @@ Supports ellipse and rectangle shapes for two-node annotations.
import math
from typing import Any, Literal
+from gns3server.utils.drawings import build_ellipse_svg, build_rectangle_svg, build_text_svg
+
# Default parameters
DEFAULT_DEVICE_WIDTH = 50
DEFAULT_DEVICE_HEIGHT = 50
@@ -144,10 +146,16 @@ def calculate_two_node_shape(
center_y = (node1_center_y + node2_center_y) / 2
color_scheme = _get_color_scheme(area_name)
- text_svg = generate_text_svg(area_name, color_scheme)
- text_svg_width = len(area_name) * 8 + 20
- text_svg_height = DEFAULT_FONT_SIZE + 16
+ # Shared builder: Web UI compatible SVG plus the computed label size
+ # (the old len * 8 + 20 estimate ignored the Web UI's pt-based rendering).
+ text_svg, text_svg_width, text_svg_height, _ = build_text_svg(
+ area_name,
+ font_size=DEFAULT_FONT_SIZE,
+ color=color_scheme["stroke"],
+ bold=True,
+ font_family="TypeWriter",
+ )
if shape_type == "ellipse":
rx = distance / 2
@@ -236,8 +244,11 @@ def generate_ellipse_svg(
svg_width: int,
svg_height: int,
) -> str:
- """Generate SVG for ellipse."""
- return f""""""
+ """Generate SVG for ellipse (delegates to the shared Web UI compatible builder)."""
+ svg, _, _ = build_ellipse_svg(
+ rx * 2, ry * 2, fill=color_scheme["fill"], fill_opacity=color_scheme["fill_opacity"]
+ )
+ return svg
def generate_rectangle_svg(
@@ -245,16 +256,23 @@ def generate_rectangle_svg(
height: int,
color_scheme: dict[str, Any],
) -> str:
- """Generate SVG for rectangle."""
- return f""""""
+ """Generate SVG for rectangle (delegates to the shared Web UI compatible builder)."""
+ svg, _, _ = build_rectangle_svg(
+ width, height, fill=color_scheme["fill"], fill_opacity=color_scheme["fill_opacity"]
+ )
+ return svg
def generate_text_svg(text: str, color_scheme: dict[str, Any]) -> str:
- """Generate SVG for text label."""
- text_width = len(text) * 8 + 20
- text_height = DEFAULT_FONT_SIZE + 16
-
- return f""""""
+ """Generate SVG for text label (delegates to the shared Web UI compatible builder)."""
+ svg, _, _, _ = build_text_svg(
+ text,
+ font_size=DEFAULT_FONT_SIZE,
+ color=color_scheme["stroke"],
+ bold=True,
+ font_family="TypeWriter",
+ )
+ return svg
def _hsv_to_hex(h: int, s: int, v: int) -> str:
diff --git a/gns3server/agent/mcp/__init__.py b/gns3server/agent/mcp/__init__.py
index 05d8603c7..96de2b828 100644
--- a/gns3server/agent/mcp/__init__.py
+++ b/gns3server/agent/mcp/__init__.py
@@ -45,8 +45,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import (
available_filters_handler,
+ create_ellipse_drawing_handler,
create_link_handler,
create_node_handler,
+ create_rectangle_drawing_handler,
+ create_text_drawing_handler,
delete_link_handler,
delete_node_file_handler,
delete_node_handler,
@@ -110,7 +113,6 @@ from .device_config import (
)
from .drawings import (
create_drawing_handler,
- create_text_drawing_handler,
delete_drawing_handler,
get_drawing_handler,
get_drawings_handler,
@@ -1627,6 +1629,103 @@ async def drawing_create_text(
)
+@mcp.tool()
+async def drawing_create_rectangle(
+ project_id: Annotated[str, Field(description="Project name or UUID")],
+ x: Annotated[int, Field(description="X coordinate of the rectangle top-left corner on the canvas")],
+ y: Annotated[int, Field(description="Y coordinate of the rectangle top-left corner on the canvas")],
+ width: Annotated[int, Field(description="Rectangle width in px, 1-2000")],
+ height: Annotated[int, Field(description="Rectangle height in px, 1-2000")],
+ fill: Annotated[str, Field(description='Fill color, hex like #RRGGBB or "none" (default: #FFFFFF)')] = "#FFFFFF",
+ fill_opacity: Annotated[float, Field(description="Fill opacity, 0-1 (default: 1.0)")] = 1.0,
+ stroke: Annotated[str | None, Field(description="Border color, hex like #RRGGBB; omit for no border")] = None,
+ stroke_width: Annotated[int, Field(description="Border width, 1-20 (default: 1)")] = 1,
+ dashed: Annotated[bool, Field(description="Dashed border, uses the default 10,6 pattern (default: false)")] = False,
+ dasharray: Annotated[str | None, Field(description='Custom dash pattern like "4,2"; requires stroke')] = None,
+ rx: Annotated[int, Field(description="Corner radius, 0 to min(width, height)/2 (default: 0)")] = 0,
+ z: Annotated[int, Field(description="Z layer; 0 keeps shapes behind nodes and text (default: 0)")] = 0,
+) -> list[dict[str, Any]]:
+ """Add a rectangle to a project canvas.
+
+ The SVG is assembled server-side in the Web UI compatible format; prefer this
+ over hand-writing SVG for drawing_create.
+
+ - fill "none" renders an outline-only box (stroke recommended then).
+ - Border attributes are only emitted when stroke is given.
+ - dasharray must be plain "on,off" numbers; the Qt-remapped patterns
+ ("25, 25", "5, 25", ...) are rejected because the Web UI rewrites them.
+
+ Example: semi-transparent outline box at (200, 150):
+ drawing_create_rectangle(project_id="demo", x=200, y=150, width=400, height=200,
+ fill="none", stroke="#333333", dashed=True)
+ """
+ return await asyncio.to_thread(
+ _run_handler_sync,
+ create_rectangle_drawing_handler,
+ {
+ "project_id": project_id,
+ "x": x,
+ "y": y,
+ "z": z,
+ "width": width,
+ "height": height,
+ "fill": fill,
+ "fill_opacity": fill_opacity,
+ "stroke": stroke,
+ "stroke_width": stroke_width,
+ "dashed": dashed,
+ "dasharray": dasharray,
+ "rx": rx,
+ },
+ )
+
+
+@mcp.tool()
+async def drawing_create_ellipse(
+ project_id: Annotated[str, Field(description="Project name or UUID")],
+ x: Annotated[int, Field(description="X coordinate of the bounding box top-left corner on the canvas")],
+ y: Annotated[int, Field(description="Y coordinate of the bounding box top-left corner on the canvas")],
+ width: Annotated[int, Field(description="Bounding box width in px, 2-2000; equal to height gives a circle")],
+ height: Annotated[int, Field(description="Bounding box height in px, 2-2000")],
+ fill: Annotated[str, Field(description='Fill color, hex like #RRGGBB or "none" (default: #FFFFFF)')] = "#FFFFFF",
+ fill_opacity: Annotated[float, Field(description="Fill opacity, 0-1 (default: 1.0)")] = 1.0,
+ stroke: Annotated[str | None, Field(description="Border color, hex like #RRGGBB; omit for no border")] = None,
+ stroke_width: Annotated[int, Field(description="Border width, 1-20 (default: 1)")] = 1,
+ dashed: Annotated[bool, Field(description="Dashed border, uses the default 10,6 pattern (default: false)")] = False,
+ dasharray: Annotated[str | None, Field(description='Custom dash pattern like "4,2"; requires stroke')] = None,
+ z: Annotated[int, Field(description="Z layer; 0 keeps shapes behind nodes and text (default: 0)")] = 0,
+) -> list[dict[str, Any]]:
+ """Add an ellipse (or circle) to a project canvas.
+
+ The SVG is assembled server-side in the Web UI compatible format. cx/cy are
+ derived from the bounding box (the Web UI treats them as offsets inside the
+ drawing, not canvas coordinates); for odd width/height the drawing is 1 px
+ smaller than requested and the actual size is returned.
+
+ Example: circle of diameter 160 at (300, 300):
+ drawing_create_ellipse(project_id="demo", x=300, y=300, width=160, height=160,
+ fill="#5AA9DD", fill_opacity=0.8)
+ """
+ return await asyncio.to_thread(
+ _run_handler_sync,
+ create_ellipse_drawing_handler,
+ {
+ "project_id": project_id,
+ "x": x,
+ "y": y,
+ "z": z,
+ "width": width,
+ "height": height,
+ "fill": fill,
+ "fill_opacity": fill_opacity,
+ "stroke": stroke,
+ "stroke_width": stroke_width,
+ "dashed": dashed,
+ "dasharray": dasharray,
+ },
+ )
+
+
@mcp.tool()
async def drawing_get(
project_id: Annotated[str, Field(description="Project name or UUID")],
diff --git a/gns3server/agent/mcp/drawings.py b/gns3server/agent/mcp/drawings.py
index 4cd88c4fe..7ca6daeda 100644
--- a/gns3server/agent/mcp/drawings.py
+++ b/gns3server/agent/mcp/drawings.py
@@ -20,11 +20,7 @@ MCP tool handlers for GNS3 drawing management.
"""
import logging
-import re
-from math import ceil
from typing import Any
-from xml.etree import ElementTree as ET
-from xml.sax.saxutils import escape
log = logging.getLogger(__name__)
@@ -73,101 +69,6 @@ def create_drawing_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) ->
return {"message": "Drawing created", "drawing": result}
-# ── Text drawing ───────────────────────────────────────────────────────────
-
-# The Web UI renders font-size as pt (1pt = 4/3 px) and monospace glyphs advance
-# ~0.6em, so one character is ~0.8 x font_size in canvas pixels. Proportional
-# fonts get a wider worst-case factor so the selection box does not clip the text.
-_MONOSPACE_CHAR_FACTOR = 0.8
-_PROPORTIONAL_CHAR_FACTOR = 1.0
-# The Web UI stacks lines at dy=1.4em, i.e. 1.4 x 4/3 ~ 1.87 px per pt of font size.
-_LINE_HEIGHT_FACTOR = 1.9
-_MAX_TEXT_LENGTH = 500
-
-_COLOR_RE = re.compile(r"^#(?:[0-9A-Fa-f]{3,4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$")
-_FONT_FAMILY_RE = re.compile(r"^[A-Za-z0-9 ,.'-]+$")
-
-
-def build_text_svg(
- text: str, font_size: int = 13, color: str = "#000000", bold: bool = False, font_family: str = "monospace"
-) -> tuple[str, int, int, int]:
- """
- Build a Web UI compatible single SVG for a canvas label.
-
- The output mirrors what the Web UI itself serializes for text drawings:
- root