mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-05 23:01:15 +02:00
feat(mcp): add drawing_create_text tool with Web UI compatible SVG assembly
The Web UI re-parses drawing SVG into its own element model instead of rendering it raw, which makes hand-assembled SVG error-prone for LLM callers. The new tool assembles the SVG server-side, mirroring what the Web UI itself serializes for text drawings: - XML-escaped text content, validated color (#RGB/#RGBA/#RRGGBB/ #RRGGBBAA) and font family (attribute-safe charset) - multi-line support: literal newlines render as stacked lines in the Web UI (tspan split), width/height computed from the longest line - pt-aware sizing: the Web UI renders font-size in pt (1pt = 4/3 px), so width uses a 0.8/char factor for monospace (1.0 proportional) and line height 1.9 x font_size, instead of the px-based 0.62 estimate - ET.fromstring pre-check so bad input fails loudly instead of hitting the controller's silent drop of unparseable 500+ char SVG - computed width/height/line_height returned for label layout Also correct the raw drawing_create docstring: root width/height are required (the parser reads them for the selection box), x/y on rect are ignored, <g> is unsupported, <path> is re-interpreted as a freehand curve, and the old examples lacked the mandatory root dimensions.
This commit is contained in:
5 files changed
+312
-11
No files matched your search
@@ -110,6 +110,7 @@ from .device_config import (
|
||||
)
|
||||
from .drawings import (
|
||||
create_drawing_handler,
|
||||
create_text_drawing_handler,
|
||||
delete_drawing_handler,
|
||||
get_drawing_handler,
|
||||
get_drawings_handler,
|
||||
@@ -1548,20 +1549,24 @@ async def drawing_create(
|
||||
locked: Annotated[bool, Field(description="Lock the drawing (default: false)")] = False,
|
||||
rotation: Annotated[int, Field(description="Rotation angle in degrees, -359 to 359 (default: 0)")] = 0,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Create a new drawing (label, shape, or image) on a project canvas.
|
||||
"""Create a new drawing (label, shape, or image) on a project canvas from raw SVG.
|
||||
|
||||
GNS3 SVG rendering notes:
|
||||
- <rect> MUST have a solid fill color (e.g. fill=\"#FF0000\") to render.
|
||||
fill=\"none\" or fill=\"transparent\" will be invisible in the GUI.
|
||||
- <ellipse> works correctly with or without fill.
|
||||
- <line> and <text> work normally.
|
||||
Web UI compatibility rules (it re-parses the SVG instead of rendering it raw):
|
||||
- Root must be <svg width="W" height="H">; W/H set the selection box size and are required.
|
||||
- Exactly ONE child element, from: text, image, rect, line, ellipse, path.
|
||||
<g> is not supported; extra children are dropped (first recognized child wins).
|
||||
- <path> is re-drawn as a freehand curve from the points in d — not a faithful render.
|
||||
- <rect>: omit x/y (ignored, the rect sits at the drawing origin) and give a solid fill.
|
||||
- <ellipse>: cx/cy are offsets inside the drawing box, not canvas coordinates.
|
||||
- font-size is displayed in pt units by the Web UI (1pt = 4/3 px).
|
||||
|
||||
For text labels use drawing_create_text instead — it handles escaping and sizing.
|
||||
|
||||
SVG examples:
|
||||
Text label: <svg><text x=\"10\" y=\"20\" font-size=\"14\">R1</text></svg>
|
||||
Rectangle: <svg><rect x=\"10\" y=\"10\" width=\"80\" height=\"50\" fill=\"#4A90D9\" stroke=\"black\"/></svg>
|
||||
Ellipse: <svg><ellipse cx=\"50\" cy=\"50\" rx=\"40\" ry=\"20\" fill=\"red\" stroke=\"black\"/></svg>
|
||||
Line: <svg><line x1=\"0\" y1=\"0\" x2=\"100\" y2=\"100\" stroke=\"black\" stroke-width=\"2\"/></svg>
|
||||
Dashed line: <svg><line x1=\"0\" y1=\"0\" x2=\"100\" y2=\"100\" stroke=\"black\" stroke-dasharray=\"5,5\"/></svg>
|
||||
Rectangle: <svg width="80" height="50"><rect width="80" height="50" fill="#4A90D9"/></svg>
|
||||
Ellipse: <svg width="80" height="40"><ellipse cx="40" cy="20" rx="40" ry="20" fill="red"/></svg>
|
||||
Line: <svg width="100" height="100"><line x2="100" y2="100" stroke="black" stroke-width="2"/></svg>
|
||||
Dashed line: <svg width="100" height="100"><line x2="100" y2="100" stroke="black" stroke-dasharray="5,5"/></svg>
|
||||
"""
|
||||
return await asyncio.to_thread(
|
||||
_run_handler_sync,
|
||||
@@ -1578,6 +1583,50 @@ async def drawing_create(
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def drawing_create_text(
|
||||
project_id: Annotated[str, Field(description="Project name or UUID")],
|
||||
x: Annotated[int, Field(description="X coordinate of the text top-left corner on the canvas")],
|
||||
y: Annotated[int, Field(description="Y coordinate of the text top-left corner on the canvas")],
|
||||
text: Annotated[str, Field(description="Label text; newlines (\\n) start additional lines")],
|
||||
font_size: Annotated[int, Field(description="Font size, 1-200 (default: 13)")] = 13,
|
||||
color: Annotated[str, Field(description="Text color, hex like #RRGGBB (default: #000000)")] = "#000000",
|
||||
bold: Annotated[bool, Field(description="Bold text (default: false)")] = False,
|
||||
font_family: Annotated[str, Field(description="Font family name (default: monospace)")] = "monospace",
|
||||
z: Annotated[int, Field(description="Z layer; 1 keeps text above links (default: 1)")] = 1,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Add a text label 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.
|
||||
|
||||
- XML characters (& < >) are escaped automatically.
|
||||
- Multi-line: embed newlines (\\n) in text; each line renders below the previous one.
|
||||
- The drawing width/height are computed from the text and returned, so labels
|
||||
can be laid out without guessing the selection box size.
|
||||
- Constraints: color must be hex like #RRGGBB; font_family accepts plain names
|
||||
(letters, digits, spaces, . ' -); text capped at 500 characters.
|
||||
|
||||
Example: bold red label at (100, 80):
|
||||
drawing_create_text(project_id="demo", x=100, y=80, text="R1 management", color="#CC0000", bold=True)
|
||||
"""
|
||||
return await asyncio.to_thread(
|
||||
_run_handler_sync,
|
||||
create_text_drawing_handler,
|
||||
{
|
||||
"project_id": project_id,
|
||||
"text": text,
|
||||
"x": x,
|
||||
"y": y,
|
||||
"z": z,
|
||||
"font_size": font_size,
|
||||
"color": color,
|
||||
"bold": bold,
|
||||
"font_family": font_family,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def drawing_get(
|
||||
project_id: Annotated[str, Field(description="Project name or UUID")],
|
||||
|
||||
Reference in new issue
Block a user