Merge pull request #2834 from yueguobin/mcp-tool-descriptions-enhancement

Improve MCP tool descriptions and add traffic-insight marker tools
This commit is contained in:
Jeremy Grossmann 2026-07-26 19:09:05 +02:00 committed by GitHub
commit deba9f8409
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 195 additions and 2 deletions

View File

@ -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,
@ -660,6 +661,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 +942,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:
@ -991,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 ─────────────────────────────────────────────────────
@ -1389,7 +1470,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:<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:

View File

@ -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 = [