fix(mcp): remove unreliable node_reload / node_reload_all tools

For Docker nodes, reload bottoms out as a raw POST /containers/{id}/restart
to the Docker daemon, bypassing GNS3's start/stop lifecycle (uBridge
re-attach, console servers, NIC setup). The container restarts at the
Docker level but GNS3's plumbing goes out of sync, and the daemon call can
block up to the controller's 240s timeout — manifesting as "reload hangs /
no response". stop+start runs the full lifecycle and is reliable.

Remove the node_reload and node_reload_all MCP tools (tool functions,
handlers, NODE_TOOLS entry, tests, docs). The underlying REST endpoints
(POST /nodes/{id}/reload, POST /nodes/reload) are kept for native API
users. MCP callers should use node_stop + node_start (partial) or
close/open project (full restart) instead.
This commit is contained in:
YueGuobin committed 2026-07-16 13:27:21 +08:00
1 parent 013ca442f4
commit 37867a1e44
5 files changed
+4 -78

No files matched your search

+2 -26
View File
@@ -86,13 +86,13 @@ from .device_config import (
)
from .nodes import (
get_nodes_handler, get_node_handler, start_node_handler,
stop_node_handler, reload_node_handler, suspend_node_handler,
stop_node_handler, suspend_node_handler,
create_node_handler, delete_node_handler, update_node_handler,
get_node_console_info_handler,
list_node_files_handler, get_node_file_handler,
write_node_file_handler, delete_node_file_handler,
start_all_nodes_handler, stop_all_nodes_handler,
suspend_all_nodes_handler, reload_all_nodes_handler,
suspend_all_nodes_handler,
duplicate_node_handler, isolate_node_handler,
unisolate_node_handler, get_node_links_handler,
)
@@ -470,20 +470,6 @@ async def node_stop(
params["node_id"] = node_id
return await asyncio.to_thread(_run_handler_sync, stop_node_handler, params)
@mcp.tool()
async def node_reload(
project_id: Annotated[str, Field(description="UUID of the project")],
node_id: Annotated[str | None, Field(description="Node UUID (single mode)")] = None,
node_ids: Annotated[list[str] | None, Field(description="Batch mode: [\"uuid1\",\"uuid2\"] — reload multiple nodes in parallel")] = None,
) -> list[dict[str, Any]]:
"""Reload (restart) one or more nodes. Provide node_id for single, or node_ids for batch."""
params = {"project_id": project_id}
if node_ids:
params["node_ids"] = node_ids
else:
params["node_id"] = node_id
return await asyncio.to_thread(_run_handler_sync, reload_node_handler, params)
@mcp.tool()
async def node_suspend(
project_id: Annotated[str, Field(description="UUID of the project")],
@@ -884,16 +870,6 @@ async def node_suspend_all(
})
@mcp.tool()
async def node_reload_all(
project_id: Annotated[str, Field(description="UUID of the project")],
) -> list[dict[str, Any]]:
"""Reload (restart) all nodes in a project."""
return await asyncio.to_thread(_run_handler_sync, reload_all_nodes_handler, {
"project_id": project_id,
})
@mcp.tool()
async def node_duplicate(
project_id: Annotated[str, Field(description="UUID of the project")],
-40
View File
@@ -159,24 +159,6 @@ def stop_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[
return {"message": f"Node {node_id} stopped", "node_id": node_id}
def reload_node_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"}
node_ids = params.get("node_ids")
if node_ids:
if not isinstance(node_ids, list):
return {"error": "node_ids must be a list"}
conn = _get_connector(gns3_ctx)
return _batch_lifecycle(project_id, node_ids, "reload", conn, "reloaded")
node_id = params.get("node_id")
if not node_id:
return {"error": "node_id or node_ids is required"}
conn = _get_connector(gns3_ctx)
conn.http_call("post", f"{conn.base_url}/projects/{project_id}/nodes/{node_id}/reload")
return {"message": f"Node {node_id} reloaded", "node_id": node_id}
def suspend_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
project_id = params.get("project_id")
if not project_id:
@@ -452,15 +434,6 @@ def suspend_all_nodes_handler(params: dict[str, Any], gns3_ctx: dict[str, Any])
return {"message": "All nodes suspended", "project_id": project_id}
def reload_all_nodes_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"}
conn = _get_connector(gns3_ctx)
conn.http_call("post", f"{conn.base_url}/projects/{project_id}/nodes/reload")
return {"message": "All nodes reloaded", "project_id": project_id}
def duplicate_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
project_id = params.get("project_id")
node_id = params.get("node_id")
@@ -556,19 +529,6 @@ NODE_TOOLS = [
},
"handler": stop_node_handler,
},
{
"name": "reload_node",
"description": "Reload (restart) a node in a project",
"parameters": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project UUID"},
"node_id": {"type": "string", "description": "Node UUID"},
},
"required": ["project_id", "node_id"],
},
"handler": reload_node_handler,
},
{
"name": "suspend_node",
"description": "Suspend a node in a project",