From a8c6546c449b92e1c09352d0a97e63b63780363a Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 26 Jul 2026 12:50:14 +0800 Subject: [PATCH 1/2] Improve MCP tool descriptions for link_update, device_show_run, and link_reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - link_update: add bidirectional filter effect note, packet loss formula (packet_loss [50] ≈ 75% observed), filter clearing syntax (filters: {}), and ARP-also-filtered warning with static ARP recommendation - device_show_run: add prerequisite section — device_type: tag required, Docker/Linux nodes unsupported (use node_console) - link_reset: clarify that filter state machines (e.g. frequency_drop counters) restart while filter configuration is preserved --- gns3server/api/routes/mcp/__init__.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/gns3server/api/routes/mcp/__init__.py b/gns3server/api/routes/mcp/__init__.py index 03e6ff67a..290e14ced 100644 --- a/gns3server/api/routes/mcp/__init__.py +++ b/gns3server/api/routes/mcp/__init__.py @@ -660,6 +660,14 @@ async def link_update( {"filters": {"delay": [100, 10]}} {"filters": {"packet_loss": [5]}} {"filters": {"delay": [50, 5], "packet_loss": [2]}} + + To clear all filters: {"filters": {}} + + Filters are applied **bidirectionally** — a packet crossing the link twice + (e.g. ping round-trip) is filtered in both directions independently. + For example, packet_loss: [50] gives ~75% observed loss (1 - 0.5²), not 50%. + ARP frames also pass through filters; at high loss/corrupt rates, pre-set + static ARP entries to avoid false "Destination Host Unreachable" errors. """ params = {"project_id": project_id, "link_id": link_id, **kwargs} return await asyncio.to_thread(_run_handler_sync, update_link_handler, params) @@ -933,7 +941,9 @@ async def link_reset( - Force filter state (delay, packet loss, etc.) to restart fresh - Recover a stuck or abnormal link state - Filters are preserved but their internal application state resets. + This restarts the filter state machines (e.g. frequency_drop counters) + while keeping the filter configuration intact. Filters are preserved but + their internal application state resets. """ params = {"project_id": project_id} if link_ids: @@ -1389,7 +1399,13 @@ async def device_show_run( Use this to inspect device status, view configurations, or verify changes. For configuration changes use device_config_send instead. - Devices must be started first. + + Prerequisites: + - Devices must be started first (use node_start or node_start_all). + - Each node must have a device_type: tag set in GNS3 + (e.g. device_type:cisco_ios_telnet, device_type:gns3_huawei_telnet_ce). + Nodes without this tag will fail with "device_type tag not found". + Docker/Linux nodes are not supported (use node_console instead). """ params = {"project_id": project_id, "device_configs": device_configs} if template is not None: From 309d388b0b2a71b9b5f1abefa5632c3c380adfce Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 26 Jul 2026 13:01:06 +0800 Subject: [PATCH 2/2] Add MCP tools for traffic-insight marker feature Add 2 new MCP tools to expose the marker (traffic-insight) REST API: - link_marker: per-link marker CRUD (create/update/delete) POST/PUT/DELETE /projects/{pid}/links/{lid}/markers - marker_definition: project-level marker definition CRUD (create/update/delete/list) POST/PUT/DELETE/GET /projects/{pid}/marker-definitions Create auto-fans out global-{name} to every link Read operations use existing link_get (returns markers dict). --- gns3server/api/routes/mcp/__init__.py | 71 +++++++++++++++++ gns3server/api/routes/mcp/links.py | 106 ++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) diff --git a/gns3server/api/routes/mcp/__init__.py b/gns3server/api/routes/mcp/__init__.py index 290e14ced..f7d57d6e2 100644 --- a/gns3server/api/routes/mcp/__init__.py +++ b/gns3server/api/routes/mcp/__init__.py @@ -101,6 +101,7 @@ from .links import ( delete_link_handler, update_link_handler, reset_link_handler, start_capture_handler, stop_capture_handler, download_capture_file_handler, + link_marker_handler, marker_definition_handler, ) from .templates import ( list_templates_handler, get_template_handler, create_template_handler, @@ -1001,6 +1002,76 @@ async def link_capture_download( return await asyncio.to_thread(_run_handler_sync, download_capture_file_handler, params) +# ── Marker (traffic-insight) tools ───────────────────────────────────── + + +@mcp.tool() +async def link_marker( + project_id: Annotated[str, Field(description="UUID of the project")], + link_id: Annotated[str, Field(description="UUID of the link")], + action: Annotated[str, Field(description="Action: create, update, or delete")], + bpf: Annotated[str | None, Field(description="BPF expression, e.g. 'arp', 'icmp', 'tcp port 80' (required for create)")] = None, + marker_name: Annotated[str | None, Field(description="Marker name (required for update/delete actions)")] = None, + name: Annotated[str | None, Field(description="Custom marker name for create action (auto-generated if omitted)")] = None, + tag: Annotated[int | None, Field(description="Numeric tag for packet correlation")] = None, + enabled: Annotated[bool | None, Field(description="Enable or disable the marker (for update action)")] = None, + color: Annotated[str | None, Field(description="Hex color for UI highlight, e.g. '#ff5722'")] = None, + highlight_duration: Annotated[int | None, Field(description="UI highlight duration in milliseconds")] = None, +) -> list[dict[str, Any]]: + """Manage traffic-insight markers on a link. + + A marker highlights packets matching a BPF expression as they cross the link. + Set action='create' to add a marker, 'update' to modify it, 'delete' to remove. + + Create requires: project_id, link_id, action='create', bpf + Update requires: project_id, link_id, action='update', marker_name, and at least one of (bpf, tag, enabled, color, highlight_duration) + Delete requires: project_id, link_id, action='delete', marker_name + + To read current markers, use link_get — the response includes a 'markers' dict. + + NOTE: Markers named 'global-*' are inherited from project-level marker definitions + and cannot be modified or deleted via this tool. + """ + params = {"project_id": project_id, "link_id": link_id, "action": action} + for opt in ("bpf", "marker_name", "name", "tag", "enabled", "color", "highlight_duration"): + val = locals().get(opt) + if val is not None: + params[opt] = val + return await asyncio.to_thread(_run_handler_sync, link_marker_handler, params) + + +@mcp.tool() +async def marker_definition( + project_id: Annotated[str, Field(description="UUID of the project")], + action: Annotated[str, Field(description="Action: create, update, delete, or list")], + bpf: Annotated[str | None, Field(description="BPF expression, e.g. 'arp', 'ospf', 'tcp port 22' (required for create)")] = None, + def_name: Annotated[str | None, Field(description="Definition name (required for update/delete actions)")] = None, + name: Annotated[str | None, Field(description="Custom definition name for create action (auto-generated if omitted)")] = None, + tag: Annotated[int | None, Field(description="Numeric tag for packet correlation")] = None, + color: Annotated[str | None, Field(description="Hex color for UI highlight, e.g. '#ff5722'")] = None, + highlight_duration: Annotated[int | None, Field(description="UI highlight duration in milliseconds")] = None, +) -> list[dict[str, Any]]: + """Manage project-level marker definitions — traffic-insight rules that apply to ALL links. + + A marker definition is a global BPF rule. On create, it auto-fans out to every + link in the project as 'global-{name}'. Updates sync to all inherited copies. + On delete, 'global-{name}' is removed from every link. + + Create requires: project_id, action='create', bpf + Update requires: project_id, action='update', def_name, and at least one of (bpf, tag, color, highlight_duration) + Delete requires: project_id, action='delete', def_name + List requires: project_id, action='list' + + Common BPF examples: 'arp', 'icmp', 'ospf', 'tcp port 22', 'udp port 53' + """ + params = {"project_id": project_id, "action": action} + for opt in ("bpf", "def_name", "name", "tag", "color", "highlight_duration"): + val = locals().get(opt) + if val is not None: + params[opt] = val + return await asyncio.to_thread(_run_handler_sync, marker_definition_handler, params) + + # ── Snapshot tools ───────────────────────────────────────────────────── diff --git a/gns3server/api/routes/mcp/links.py b/gns3server/api/routes/mcp/links.py index 31daa1f26..b2a0352c3 100644 --- a/gns3server/api/routes/mcp/links.py +++ b/gns3server/api/routes/mcp/links.py @@ -367,6 +367,112 @@ def download_capture_file_handler(params: dict[str, Any], gns3_ctx: dict[str, An return result +# ── Marker (traffic-insight) handlers ────────────────────────────────── + + +def link_marker_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: + """ + Manage traffic-insight markers on a specific link. + + Actions: + - create: POST /projects/{pid}/links/{lid}/markers + - update: PUT /projects/{pid}/links/{lid}/markers/{name} + - delete: DELETE /projects/{pid}/links/{lid}/markers/{name} + """ + project_id = params.get("project_id") + link_id = params.get("link_id") + action = params.get("action") + if not all([project_id, link_id, action]): + return {"error": "project_id, link_id and action are required"} + if action not in ("create", "update", "delete"): + return {"error": f"Unknown action: {action}. Supported: create, update, delete"} + + conn = _get_connector(gns3_ctx) + base = f"{conn.base_url}/projects/{project_id}/links/{link_id}/markers" + + if action == "create": + bpf = params.get("bpf") + if not bpf: + return {"error": "bpf is required for create action"} + body: dict[str, Any] = {"bpf": bpf} + for opt in ("name", "tag", "color", "highlight_duration"): + if params.get(opt) is not None: + body[opt] = params[opt] + return conn.http_call("post", base, json_data=body).json() + + marker_name = params.get("marker_name") + if not marker_name: + return {"error": "marker_name is required for update/delete actions"} + + url = f"{base}/{marker_name}" + + if action == "update": + body = {} + for opt in ("bpf", "tag", "enabled", "color", "highlight_duration"): + if params.get(opt) is not None: + body[opt] = params[opt] + if not body: + return {"error": "At least one update field is required (bpf, tag, enabled, color, highlight_duration)"} + return conn.http_call("put", url, json_data=body).json() + + # action == "delete" + conn.http_call("delete", url) + return {"message": f"Marker '{marker_name}' deleted from link {link_id}", "link_id": link_id, "marker_name": marker_name} + + +def marker_definition_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: + """ + Manage project-level marker definitions (auto-fanout to all links). + + Actions: + - create: POST /projects/{pid}/marker-definitions → fans out global-{name} to every link + - update: PUT /projects/{pid}/marker-definitions/{name} + - delete: DELETE /projects/{pid}/marker-definitions/{name} + - list: GET /projects/{pid}/marker-definitions + """ + project_id = params.get("project_id") + action = params.get("action") + if not all([project_id, action]): + return {"error": "project_id and action are required"} + if action not in ("create", "update", "delete", "list"): + return {"error": f"Unknown action: {action}. Supported: create, update, delete, list"} + + conn = _get_connector(gns3_ctx) + base = f"{conn.base_url}/projects/{project_id}/marker-definitions" + + if action == "list": + return conn.http_call("get", base).json() + + if action == "create": + bpf = params.get("bpf") + if not bpf: + return {"error": "bpf is required for create action"} + body: dict[str, Any] = {"bpf": bpf} + for opt in ("name", "tag", "color", "highlight_duration"): + if params.get(opt) is not None: + body[opt] = params[opt] + return conn.http_call("post", base, json_data=body).json() + + def_name = params.get("def_name") + if not def_name: + return {"error": "def_name is required for update/delete actions"} + + url = f"{base}/{def_name}" + + if action == "update": + body = {} + for opt in ("bpf", "tag", "color", "highlight_duration"): + if params.get(opt) is not None: + body[opt] = params[opt] + if not body: + return {"error": "At least one update field is required (bpf, tag, color, highlight_duration)"} + return conn.http_call("put", url, json_data=body).json() + + # action == "delete" + conn.http_call("delete", url) + return {"message": f"Marker definition '{def_name}' deleted", "project_id": project_id, "def_name": def_name} + + # ── Tool definitions ─────────────────────────────────────────────────────── LINK_TOOLS = [