From a8a83051ff70f61c104b06b39bdb0d469d2edf5a Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Mon, 15 Jun 2026 21:42:26 +0800 Subject: [PATCH] Add compact array format for link node entries to reduce token usage Supports both standard and compact formats: Standard: [{"node_id": "uuid", "adapter_number": 0, "port_number": 0}] Compact: ["uuid", 0, 0, "uuid", 0, 0] - 3x less tokens --- gns3server/api/routes/mcp/__init__.py | 4 ++-- gns3server/api/routes/mcp/links.py | 27 ++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/gns3server/api/routes/mcp/__init__.py b/gns3server/api/routes/mcp/__init__.py index bd522d473..23b14e145 100644 --- a/gns3server/api/routes/mcp/__init__.py +++ b/gns3server/api/routes/mcp/__init__.py @@ -596,10 +596,10 @@ async def link_get( @mcp.tool() async def link_create( project_id: Annotated[str, Field(description="UUID of the project")], - nodes: Annotated[list | None, Field(description="Single mode: [{node_id, adapter_number, port_number}]")] = None, + nodes: Annotated[list | None, Field(description="Single mode: [{node_id, adapter_number, port_number}] or compact [id, ad, pt, id, ad, pt]")] = None, link_type: Annotated[str, Field(description="Link type - ethernet or serial")] = "ethernet", filters: Annotated[dict | None, Field(description="Optional packet filters")] = None, - links: Annotated[list | None, Field(description="Batch mode: [{nodes, link_type?, filters?}] — creates multiple links in parallel")] = None, + links: Annotated[list | None, Field(description="Batch mode: [{nodes, link_type?, filters?}] — nodes supports compact [id, ad, pt, id, ad, pt] format")] = None, fields: Annotated[list[str] | None, Field(description="Response fields to include (default: [link_id, link_type, nodes]). " "Available: link_id, project_id, link_type, nodes, suspend, " "link_style, filters, show_filters_icon, capturing, " diff --git a/gns3server/api/routes/mcp/links.py b/gns3server/api/routes/mcp/links.py index 2e2f94b79..f4f2d4ca6 100644 --- a/gns3server/api/routes/mcp/links.py +++ b/gns3server/api/routes/mcp/links.py @@ -46,6 +46,26 @@ def _get_connector(gns3_ctx: dict[str, Any]): ) +def _normalize_link_nodes(nodes) -> list[dict[str, Any]]: + """ + Normalize link node entries, accepting both standard object format and + compact array format to reduce token usage. + + Standard: [{"node_id": "uuid", "adapter_number": 0, "port_number": 0}] + Compact: ["uuid", 0, 0, "uuid", 0, 0] + """ + if not nodes: + return nodes + if isinstance(nodes[0], dict): + return nodes + if isinstance(nodes, list) and len(nodes) == 6: + return [ + {"node_id": nodes[0], "adapter_number": nodes[1], "port_number": nodes[2]}, + {"node_id": nodes[3], "adapter_number": nodes[4], "port_number": nodes[5]}, + ] + return nodes + + # ── Tool handlers ────────────────────────────────────────────────────────── VALID_LINK_FIELDS = { @@ -100,10 +120,11 @@ def create_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic results = [] conn = _get_connector(gns3_ctx) def _create_one(link_data): - if not link_data.get("nodes"): + raw_nodes = link_data.get("nodes") + if not raw_nodes: return {"status": "error", "error": "nodes is required for each link"} try: - body = {"nodes": link_data["nodes"]} + body = {"nodes": _normalize_link_nodes(raw_nodes)} if link_data.get("link_type"): body["link_type"] = link_data["link_type"] if link_data.get("filters"): @@ -126,7 +147,7 @@ def create_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic if not nodes: return {"error": "nodes is required"} conn = _get_connector(gns3_ctx) - data = {"nodes": nodes} + data = {"nodes": _normalize_link_nodes(nodes)} if "link_type" in params: data["link_type"] = params["link_type"] if "filters" in params: