refactor: sink shared REST handlers into gns3_client, drop gns3fy wrappers

- 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
This commit is contained in:
YueGuobin committed 2026-08-25 09:15:32 +08:00
1 parent 1649fb7b7a
commit 629bb9194f
35 files changed
+2180 -5298

No files matched your search

+1 -1
View File
@@ -440,7 +440,7 @@ sequenceDiagram
- **FastMCP** (Anthropic MCP SDK) is used for tool registration and SSE transport
- The SSE app is mounted as a Starlette sub-application under `/v3/mcp/transport`
- **Auth:** JWT validation via `auth_service`. API key (`gns3_<uuid>_<secret>`) extracts UUID for O(1) DB lookup, runs bcrypt in thread pool, returns a fresh JWT — subsequent calls use the JWT with zero extra bcrypt.
- Tool handlers use `Gns3Connector` (from `custom_gns3fy`) to call GNS3's own REST API, keeping the MCP layer decoupled
- Tool handlers use `Gns3Connector` (from `gns3_copilot.gns3_client.connector`) via the shared handler layer (`gns3_copilot.gns3_client.api_handlers`), keeping the MCP layer decoupled
- The JWT token is stored in a `contextvars.ContextVar` so it is available within tool handler threads (Python ≥ 3.9 propagates contextvars through `asyncio.to_thread`)
### Console WebSocket
+3 -3
View File
@@ -114,8 +114,8 @@ if await asyncio.to_thread(bcrypt.checkpw, secret.encode(), db_key.key_hash.enco
| Node creation Pool | 5 | 100 | `controller/project.py` |
| Link creation Pool | 5 | 100 | `controller/project.py` |
| MCP BATCH_MAX_WORKERS | 10 | 100 | `agent/mcp/nodes.py` |
| MCP HTTP timeout | 10s | 30s | `agent/gns3_copilot/gns3_client/custom_gns3fy.py` |
| HTTP connection pool | 10 (default) | 500/1000 | `agent/gns3_copilot/gns3_client/custom_gns3fy.py` |
| MCP HTTP timeout | 10s | 30s | `agent/gns3_copilot/gns3_client/connector.py` |
| HTTP connection pool | 10 (default) | 500/1000 | `agent/gns3_copilot/gns3_client/connector.py` |
| Start nodes Pool | 3 | 3 (unchanged) | `controller/project.py` |
### 8. MCP Auth Returns JWT
@@ -144,5 +144,5 @@ if user:
| `gns3server/agent/mcp/__init__.py` | Auth returns JWT, tool enhancements |
| `gns3server/agent/mcp/nodes.py` | fields filter, inherited template_id, name passthrough |
| `gns3server/agent/mcp/links.py` | fields filter, compact array format |
| `gns3server/agent/gns3_copilot/gns3_client/custom_gns3fy.py` | Timeout 30s, connection pool 500/1000 |
| `gns3server/agent/gns3_copilot/gns3_client/connector.py` | Timeout 30s, connection pool 500/1000 |
| `gns3server/utils/images.py` | md5sum cache error → warning |
@@ -516,29 +516,39 @@ gns3server/agent/gns3_copilot/tools_v2/
### API Integration
The tools use the `Node` and `Link` classes from `custom_gns3fy`:
The tools call the shared REST handler layer (`gns3_copilot.gns3_client.api_handlers`), the same functions the MCP service exposes as MCP tools:
```python
from gns3server.agent.gns3_copilot.gns3_client import Node, Link, get_gns3_connector
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import (
build_gns3_ctx, create_node_handler, create_link_handler,
get_nodes_handler, start_node_handler, stop_node_handler,
suspend_node_handler, update_node_handler,
)
# Get templates
templates = get_gns3_connector().get_templates()
gns3_ctx = build_gns3_ctx() # JWT + server URL from the request context
# Create node
node = Node(project_id=project_id, template_id=template_id, x=x, y=y, connector=gns3_server)
node.create()
# Create node (single POST, batch mode is parallel)
created = create_node_handler(
{"project_id": project_id, "template_id": template_id, "x": x, "y": y, "name": name},
gns3_ctx,
)
# Create link
link = Link(project_id=project_id, connector=gns3_server, nodes=[...])
link.create()
link = create_link_handler(
{"project_id": project_id, "nodes": [{"node_id": nid, "adapter_number": 0, "port_number": 0}, ...]},
gns3_ctx,
)
# Update node name
node = Node(project_id=project_id, node_id=node_id, connector=gns3_server)
node.update(name=new_name)
# Update node name — the PUT response is the updated node
updated = update_node_handler({"project_id": project_id, "node_id": node_id, "name": new_name}, gns3_ctx)
# Start/stop/suspend node
node = Node(project_id=project_id, node_id=node_id, connector=gns3_server)
node.start() # or node.stop() / node.suspend()
# Start/stop/suspend nodes (node_ids batch runs in parallel)
start_node_handler({"project_id": project_id, "node_ids": [nid1, nid2]}, gns3_ctx)
stop_node_handler({"project_id": project_id, "node_ids": [nid1, nid2]}, gns3_ctx)
suspend_node_handler({"project_id": project_id, "node_ids": [nid1, nid2]}, gns3_ctx)
# Node listing/status (single call for the whole project)
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)
```
### Progress Tracking