Symbol tools (symbol_list/get/dimensions/defaults/upload/delete) require
a vision-capable model to be genuinely useful — they shuttle SVG
content, which a text-only LLM cannot inspect or produce. The tool
registrations and imports are commented out (handlers stay in
symbols.py); revisit when multimodal support is worked out.
Loading a .gns3 placed directly in the projects root registered the
shared projects directory as the project path (load_project derives
the path from the file's parent directory). Deleting such an entry ran
rmtree on the projects directory itself, wiping every project until a
root-owned file stopped it, and left a zombie entry in the controller.
Three layers of protection:
- Controller.load_project() refuses a .gns3 whose parent directory is
the projects directory; the normal subdirectory layout is unaffected
- the Project.path setter rejects the projects directory itself and
its ancestors, closing the same hole for POST/PUT with an explicit
path
- Project.delete() uses realpath + commonpath instead of commonprefix:
entries whose path is the projects root are refused, and sibling
directories sharing a string prefix (/srv/projects-evil vs
/srv/projects) are no longer treated as inside the projects dir
Also removes the project_load MCP tool: loading by raw server
filesystem path is a footgun for automated clients; projects can still
be opened by project_id via the remaining tools.
- Slim custom_gns3fy.py to connector-only Gns3Connector and rename to
connector.py; delete the unused Node/Link/Project dataclasses and
endpoint wrapper methods (~3000 lines)
- Move the MCP node/link handler implementations into
gns3_client/api_handlers.py as the shared REST client layer consumed
by both the MCP service and copilot tools; add available_filters
handler (exposed as link_available_filters MCP tool) and
build_gns3_ctx() for copilot callers
- Rewrite the tools_v2 node/link tools on top of the handlers: batch
lifecycle actions now run in parallel, node creation is a single
POST, project-wide status reads replace per-node GETs
- Port Project.nodes_inventory/links_summary aggregation into
project_inventory.py (output shape preserved) and rewrite the
topology reader / project info tools on it, dropping the unused
stats/snapshots/drawings calls
- Delete the dead mcp/nodes.py and mcp/links.py (NODE_TOOLS/LINK_TOOLS
had no consumers; __init__ imports handlers from api_handlers)
- Retarget mcp handler tests to patch api_handlers._get_connector and
replace test_custom_gns3fy.py with inventory contract tests
MCP is an optional AI feature that already depends on
agent.gns3_copilot (Gns3Connector, nornir/netmiko tools) and whose
MCP_AVAILABLE feature flag lives in gns3server/agent. Moving it there
collocates all AI features under one tree and removes AI code from the
core REST routes.
- git mv gns3server/api/routes/mcp -> gns3server/agent/mcp (no content changes)
- api/server.py, core/tasks.py: update import paths
- tests: tests/api/routes/mcp -> tests/agent/mcp, rewrite patch BASE and
handler imports; fix MCP_DIR depth in test_tool_params.py
- agent/__init__.py: probe the SDK via importlib.import_module so the
top-level name "mcp" is not bound in the agent namespace (it would
shadow the new gns3server.agent.mcp subpackage and break
'from gns3server.agent import mcp')
- docs: update source file paths
Verified: full suite 1567 passed; 82 MCP tools registered, SSE mounted
at /v3/mcp/transport.
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.
The name device_command_run was ambiguous and easily confused with
device_config_send. device_show_run makes the read-only intent explicit,
forming a clear pair: config_send (write) vs show_run (read).
Add documentation explaining how _server_url() resolves the host
when Server.host is 0.0.0.0 or :: — using the default route
interface IP instead of hardcoding 127.0.0.1.
Added Transport Security section to MCP service documentation covering:
- Default behaviour (disabled, allow all hosts)
- How to enable protection via gns3_server.conf
- Protection mechanism (Host header validation)
- DNS rebinding attack prevention explanation
- Behaviour summary table