diff --git a/gns3server/api/routes/mcp/__init__.py b/gns3server/api/routes/mcp/__init__.py index d544460a3..1deb2473b 100644 --- a/gns3server/api/routes/mcp/__init__.py +++ b/gns3server/api/routes/mcp/__init__.py @@ -612,10 +612,16 @@ async def link_create( @mcp.tool() async def link_delete( project_id: Annotated[str, Field(description="UUID of the project")], - link_id: Annotated[str, Field(description="UUID of the link to delete")], + link_id: Annotated[str | None, Field(description="Link UUID (single mode)")] = None, + link_ids: Annotated[list[str] | None, Field(description="Batch mode: [\"uuid1\",\"uuid2\"] — delete multiple links in parallel")] = None, ) -> list[dict[str, Any]]: - """Delete a link from a project.""" - return await asyncio.to_thread(_run_handler_sync, delete_link_handler, {"project_id": project_id, "link_id": link_id}) + """Delete one or more links from a project.""" + params = {"project_id": project_id} + if link_ids: + params["link_ids"] = link_ids + else: + params["link_id"] = link_id + return await asyncio.to_thread(_run_handler_sync, delete_link_handler, params) @mcp.tool() @@ -909,9 +915,10 @@ async def node_links( @mcp.tool() async def link_reset( project_id: Annotated[str, Field(description="UUID of the project")], - link_id: Annotated[str, Field(description="UUID of the link")], + link_id: Annotated[str | None, Field(description="Link UUID (single mode)")] = None, + link_ids: Annotated[list[str] | None, Field(description="Batch mode: [\"uuid1\",\"uuid2\"] — reset multiple links in parallel")] = None, ) -> list[dict[str, Any]]: - """Reset a link by tearing down the underlying UDP connection and recreating it. + """Reset one or more links by tearing down and recreating the UDP connection. Use cases: - Clear accumulated packet errors/drops from the link's UDP connection @@ -920,9 +927,12 @@ async def link_reset( Filters are preserved but their internal application state resets. """ - return await asyncio.to_thread(_run_handler_sync, reset_link_handler, { - "project_id": project_id, "link_id": link_id, - }) + params = {"project_id": project_id} + if link_ids: + params["link_ids"] = link_ids + else: + params["link_id"] = link_id + return await asyncio.to_thread(_run_handler_sync, reset_link_handler, params) @mcp.tool() diff --git a/gns3server/api/routes/mcp/links.py b/gns3server/api/routes/mcp/links.py index 83627d47c..cae2b07db 100644 --- a/gns3server/api/routes/mcp/links.py +++ b/gns3server/api/routes/mcp/links.py @@ -137,9 +137,24 @@ def create_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic def delete_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: project_id = params.get("project_id") + if not project_id: + return {"error": "project_id is required"} + link_ids = params.get("link_ids") + if link_ids: + if not isinstance(link_ids, list): + return {"error": "link_ids must be a list"} + conn = _get_connector(gns3_ctx) + def _del(lid): + try: + conn.http_call("delete", f"{conn.base_url}/projects/{project_id}/links/{lid}") + return {"link_id": lid, "status": "deleted"} + except Exception as e: + return {"link_id": lid, "status": "error", "error": str(e)} + with ThreadPoolExecutor(max_workers=min(len(link_ids), BATCH_MAX_WORKERS)) as pool: + return list(pool.map(_del, link_ids)) link_id = params.get("link_id") - if not project_id or not link_id: - return {"error": "project_id and link_id are required"} + if not link_id: + return {"error": "link_id or link_ids is required"} conn = _get_connector(gns3_ctx) conn.http_call("delete", f"{conn.base_url}/projects/{project_id}/links/{link_id}") return {"message": f"Link {link_id} deleted", "link_id": link_id} @@ -167,9 +182,25 @@ def update_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic def reset_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: project_id = params.get("project_id") + if not project_id: + return {"error": "project_id is required"} + link_ids = params.get("link_ids") + if link_ids: + if not isinstance(link_ids, list): + return {"error": "link_ids must be a list"} + conn = _get_connector(gns3_ctx) + def _rst(lid): + try: + url = f"{conn.base_url}/projects/{project_id}/links/{lid}/reset" + r = conn.http_call("post", url).json() + return {"link_id": lid, "status": "reset", "link": r} + except Exception as e: + return {"link_id": lid, "status": "error", "error": str(e)} + with ThreadPoolExecutor(max_workers=min(len(link_ids), BATCH_MAX_WORKERS)) as pool: + return list(pool.map(_rst, link_ids)) link_id = params.get("link_id") - if not project_id or not link_id: - return {"error": "project_id and link_id are required"} + if not link_id: + return {"error": "link_id or link_ids is required"} conn = _get_connector(gns3_ctx) url = f"{conn.base_url}/projects/{project_id}/links/{link_id}/reset" result = conn.http_call("post", url).json()