From 641d10177af5cad6dbf821c19cd1e842034e7a85 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 22 Aug 2026 19:32:13 +0800 Subject: [PATCH] refactor: move MCP service from api/routes to agent package 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. --- docs/features/mcp-service.md | 12 +-- docs/features/project-open-performance.md | 10 +- gns3server/agent/__init__.py | 5 +- .../{api/routes => agent}/mcp/__init__.py | 0 .../{api/routes => agent}/mcp/appliances.py | 0 .../{api/routes => agent}/mcp/computes.py | 0 .../routes => agent}/mcp/device_config.py | 0 .../{api/routes => agent}/mcp/drawings.py | 0 .../{api/routes => agent}/mcp/images.py | 0 gns3server/{api/routes => agent}/mcp/links.py | 0 gns3server/{api/routes => agent}/mcp/nodes.py | 0 .../{api/routes => agent}/mcp/projects.py | 0 .../{api/routes => agent}/mcp/server.py | 0 .../{api/routes => agent}/mcp/snapshots.py | 0 .../{api/routes => agent}/mcp/symbols.py | 0 .../{api/routes => agent}/mcp/templates.py | 0 gns3server/api/server.py | 2 +- gns3server/core/tasks.py | 2 +- tests/{api/routes => agent}/mcp/conftest.py | 0 .../routes => agent}/mcp/test_handlers.py | 94 +++++++++---------- .../routes => agent}/mcp/test_tool_params.py | 2 +- 21 files changed, 65 insertions(+), 62 deletions(-) rename gns3server/{api/routes => agent}/mcp/__init__.py (100%) rename gns3server/{api/routes => agent}/mcp/appliances.py (100%) rename gns3server/{api/routes => agent}/mcp/computes.py (100%) rename gns3server/{api/routes => agent}/mcp/device_config.py (100%) rename gns3server/{api/routes => agent}/mcp/drawings.py (100%) rename gns3server/{api/routes => agent}/mcp/images.py (100%) rename gns3server/{api/routes => agent}/mcp/links.py (100%) rename gns3server/{api/routes => agent}/mcp/nodes.py (100%) rename gns3server/{api/routes => agent}/mcp/projects.py (100%) rename gns3server/{api/routes => agent}/mcp/server.py (100%) rename gns3server/{api/routes => agent}/mcp/snapshots.py (100%) rename gns3server/{api/routes => agent}/mcp/symbols.py (100%) rename gns3server/{api/routes => agent}/mcp/templates.py (100%) rename tests/{api/routes => agent}/mcp/conftest.py (100%) rename tests/{api/routes => agent}/mcp/test_handlers.py (85%) rename tests/{api/routes => agent}/mcp/test_tool_params.py (99%) diff --git a/docs/features/mcp-service.md b/docs/features/mcp-service.md index 422b9ca42..bfcb43271 100644 --- a/docs/features/mcp-service.md +++ b/docs/features/mcp-service.md @@ -471,10 +471,10 @@ websocat ws://192.168.1.3:3080/v3/projects/{project_id}/nodes/{node_id}/console/ | File | Purpose | |------|---------| -| `gns3server/api/routes/mcp/__init__.py` | FastMCP server, tool decorators, SSE transport, JWT auth wrapper | -| `gns3server/api/routes/mcp/projects.py` | Project tool handlers | -| `gns3server/api/routes/mcp/nodes.py` | Node tool handlers | -| `gns3server/api/routes/mcp/links.py` | Link tool handlers | -| `gns3server/api/routes/mcp/templates.py` | Template tool handlers | -| `gns3server/api/routes/mcp/computes.py` | Compute tool handlers | +| `gns3server/agent/mcp/__init__.py` | FastMCP server, tool decorators, SSE transport, JWT auth wrapper | +| `gns3server/agent/mcp/projects.py` | Project tool handlers | +| `gns3server/agent/mcp/nodes.py` | Node tool handlers | +| `gns3server/agent/mcp/links.py` | Link tool handlers | +| `gns3server/agent/mcp/templates.py` | Template tool handlers | +| `gns3server/agent/mcp/computes.py` | Compute tool handlers | | `gns3server/api/server.py` | Mounts MCP routes via `register_starlette_routes()` | diff --git a/docs/features/project-open-performance.md b/docs/features/project-open-performance.md index 869c90824..3abd49a60 100644 --- a/docs/features/project-open-performance.md +++ b/docs/features/project-open-performance.md @@ -113,14 +113,14 @@ 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 | `api/routes/mcp/nodes.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` | | Start nodes Pool | 3 | 3 (unchanged) | `controller/project.py` | ### 8. MCP Auth Returns JWT -**File:** `gns3server/api/routes/mcp/__init__.py` +**File:** `gns3server/agent/mcp/__init__.py` When an MCP client connects with an API key, the `_resolve_token` function validates the key then returns a fresh short-lived JWT instead of the raw API key. The JWT is stored in a `ContextVar` and reused for all subsequent tool calls within the same SSE session — zero extra bcrypt. @@ -141,8 +141,8 @@ if user: | `gns3server/controller/udp_link.py` | Pre-allocated port consumption | | `gns3server/api/routes/controller/api_keys.py` | O(1) key format | | `gns3server/api/routes/controller/dependencies/authentication.py` | O(1) auth + thread pool bcrypt | -| `gns3server/api/routes/mcp/__init__.py` | Auth returns JWT, tool enhancements | -| `gns3server/api/routes/mcp/nodes.py` | fields filter, inherited template_id, name passthrough | -| `gns3server/api/routes/mcp/links.py` | fields filter, compact array format | +| `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/utils/images.py` | md5sum cache error → warning | diff --git a/gns3server/agent/__init__.py b/gns3server/agent/__init__.py index e6a0f8491..d914a912c 100644 --- a/gns3server/agent/__init__.py +++ b/gns3server/agent/__init__.py @@ -85,7 +85,10 @@ MCP_AVAILABLE = False # Try to import MCP dependencies try: - import mcp.server.fastmcp # noqa: F401 — test import only + # Use importlib so the top-level SDK name "mcp" is not bound in this + # namespace — it would shadow the gns3server.agent.mcp subpackage. + import importlib + importlib.import_module("mcp.server.fastmcp") MCP_AVAILABLE = True except ImportError: # MCP dependencies not installed, disable MCP feature diff --git a/gns3server/api/routes/mcp/__init__.py b/gns3server/agent/mcp/__init__.py similarity index 100% rename from gns3server/api/routes/mcp/__init__.py rename to gns3server/agent/mcp/__init__.py diff --git a/gns3server/api/routes/mcp/appliances.py b/gns3server/agent/mcp/appliances.py similarity index 100% rename from gns3server/api/routes/mcp/appliances.py rename to gns3server/agent/mcp/appliances.py diff --git a/gns3server/api/routes/mcp/computes.py b/gns3server/agent/mcp/computes.py similarity index 100% rename from gns3server/api/routes/mcp/computes.py rename to gns3server/agent/mcp/computes.py diff --git a/gns3server/api/routes/mcp/device_config.py b/gns3server/agent/mcp/device_config.py similarity index 100% rename from gns3server/api/routes/mcp/device_config.py rename to gns3server/agent/mcp/device_config.py diff --git a/gns3server/api/routes/mcp/drawings.py b/gns3server/agent/mcp/drawings.py similarity index 100% rename from gns3server/api/routes/mcp/drawings.py rename to gns3server/agent/mcp/drawings.py diff --git a/gns3server/api/routes/mcp/images.py b/gns3server/agent/mcp/images.py similarity index 100% rename from gns3server/api/routes/mcp/images.py rename to gns3server/agent/mcp/images.py diff --git a/gns3server/api/routes/mcp/links.py b/gns3server/agent/mcp/links.py similarity index 100% rename from gns3server/api/routes/mcp/links.py rename to gns3server/agent/mcp/links.py diff --git a/gns3server/api/routes/mcp/nodes.py b/gns3server/agent/mcp/nodes.py similarity index 100% rename from gns3server/api/routes/mcp/nodes.py rename to gns3server/agent/mcp/nodes.py diff --git a/gns3server/api/routes/mcp/projects.py b/gns3server/agent/mcp/projects.py similarity index 100% rename from gns3server/api/routes/mcp/projects.py rename to gns3server/agent/mcp/projects.py diff --git a/gns3server/api/routes/mcp/server.py b/gns3server/agent/mcp/server.py similarity index 100% rename from gns3server/api/routes/mcp/server.py rename to gns3server/agent/mcp/server.py diff --git a/gns3server/api/routes/mcp/snapshots.py b/gns3server/agent/mcp/snapshots.py similarity index 100% rename from gns3server/api/routes/mcp/snapshots.py rename to gns3server/agent/mcp/snapshots.py diff --git a/gns3server/api/routes/mcp/symbols.py b/gns3server/agent/mcp/symbols.py similarity index 100% rename from gns3server/api/routes/mcp/symbols.py rename to gns3server/agent/mcp/symbols.py diff --git a/gns3server/api/routes/mcp/templates.py b/gns3server/agent/mcp/templates.py similarity index 100% rename from gns3server/api/routes/mcp/templates.py rename to gns3server/agent/mcp/templates.py diff --git a/gns3server/api/server.py b/gns3server/api/server.py index 064b3d810..3b3218380 100644 --- a/gns3server/api/server.py +++ b/gns3server/api/server.py @@ -51,7 +51,7 @@ from gns3server.core import tasks from gns3server.agent import MCP_AVAILABLE if MCP_AVAILABLE: - from gns3server.api.routes import mcp + from gns3server.agent import mcp _mcp_router = mcp.router else: from fastapi import APIRouter diff --git a/gns3server/core/tasks.py b/gns3server/core/tasks.py index 9fc911d55..f8e8dadc9 100644 --- a/gns3server/core/tasks.py +++ b/gns3server/core/tasks.py @@ -97,7 +97,7 @@ async def startup(app: FastAPI) -> None: from gns3server.agent import MCP_AVAILABLE if MCP_AVAILABLE: - from gns3server.api.routes.mcp import set_mcp_server_ready + from gns3server.agent.mcp import set_mcp_server_ready set_mcp_server_ready(True) log.info("GNS3 server startup completed") diff --git a/tests/api/routes/mcp/conftest.py b/tests/agent/mcp/conftest.py similarity index 100% rename from tests/api/routes/mcp/conftest.py rename to tests/agent/mcp/conftest.py diff --git a/tests/api/routes/mcp/test_handlers.py b/tests/agent/mcp/test_handlers.py similarity index 85% rename from tests/api/routes/mcp/test_handlers.py rename to tests/agent/mcp/test_handlers.py index fc8cd00d2..1a2870752 100644 --- a/tests/api/routes/mcp/test_handlers.py +++ b/tests/agent/mcp/test_handlers.py @@ -15,7 +15,7 @@ def _mock_conn(json_result=None): return conn -BASE = "gns3server.api.routes.mcp" +BASE = "gns3server.agent.mcp" @pytest.fixture @@ -31,60 +31,60 @@ class TestProject: mod = "projects" def test_list(self, ctx): - from gns3server.api.routes.mcp.projects import list_projects_handler + from gns3server.agent.mcp.projects import list_projects_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn([{"project_id": "p1", "name": "Test", "status": "opened"}]) result = list_projects_handler({}, ctx) assert result["count"] == 1 def test_get(self, ctx): - from gns3server.api.routes.mcp.projects import get_project_handler + from gns3server.agent.mcp.projects import get_project_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"project_id": "p1"}) result = get_project_handler({"project_id": "p1"}, ctx) assert result["project_id"] == "p1" def test_get_missing_id(self, ctx): - from gns3server.api.routes.mcp.projects import get_project_handler + from gns3server.agent.mcp.projects import get_project_handler assert "error" in get_project_handler({}, ctx) def test_create(self, ctx): - from gns3server.api.routes.mcp.projects import create_project_handler + from gns3server.agent.mcp.projects import create_project_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"project_id": "p1"}) result = create_project_handler({"name": "New"}, ctx) assert result["project_id"] == "p1" def test_delete(self, ctx): - from gns3server.api.routes.mcp.projects import delete_project_handler + from gns3server.agent.mcp.projects import delete_project_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({}) result = delete_project_handler({"project_id": "p1"}, ctx) assert "message" in result def test_open(self, ctx): - from gns3server.api.routes.mcp.projects import open_project_handler + from gns3server.agent.mcp.projects import open_project_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"status": "opened"}) result = open_project_handler({"project_id": "p1"}, ctx) assert result["status"] == "opened" def test_close(self, ctx): - from gns3server.api.routes.mcp.projects import close_project_handler + from gns3server.agent.mcp.projects import close_project_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"status": "closed"}) result = close_project_handler({"project_id": "p1"}, ctx) assert "error" not in result def test_update(self, ctx): - from gns3server.api.routes.mcp.projects import update_project_handler + from gns3server.agent.mcp.projects import update_project_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"name": "Updated"}) result = update_project_handler({"project_id": "p1", "name": "Updated"}, ctx) assert result["name"] == "Updated" def test_stats(self, ctx): - from gns3server.api.routes.mcp.projects import get_project_stats_handler + from gns3server.agent.mcp.projects import get_project_stats_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"nodes": 5, "links": 3}) result = get_project_stats_handler({"project_id": "p1"}, ctx) @@ -99,7 +99,7 @@ class TestNode: mod = "nodes" def test_list_fields(self, ctx): - from gns3server.api.routes.mcp.nodes import get_nodes_handler + from gns3server.agent.mcp.nodes import get_nodes_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn([ {"node_id": "n1", "name": "R1", "status": "started", "node_type": "qemu", "console": 5000}, @@ -108,21 +108,21 @@ class TestNode: assert result == {"nodes": [{"name": "R1", "status": "started"}], "count": 1} def test_list_invalid_fields(self, ctx): - from gns3server.api.routes.mcp.nodes import get_nodes_handler + from gns3server.agent.mcp.nodes import get_nodes_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn([]) result = get_nodes_handler({"project_id": "p1", "fields": "not-a-list"}, ctx) assert "error" in result def test_get(self, ctx): - from gns3server.api.routes.mcp.nodes import get_node_handler + from gns3server.agent.mcp.nodes import get_node_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"node_id": "n1", "name": "R1"}) result = get_node_handler({"project_id": "p1", "node_id": "n1"}, ctx) assert result["name"] == "R1" def test_create_single_passes_name(self, ctx): - from gns3server.api.routes.mcp.nodes import create_node_handler + from gns3server.agent.mcp.nodes import create_node_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"node_id": "n1", "name": "MyRouter"}) m.return_value = conn @@ -137,7 +137,7 @@ class TestNode: assert result == {"node_id": "n1", "name": "MyRouter"} def test_create_fields_filter(self, ctx): - from gns3server.api.routes.mcp.nodes import create_node_handler + from gns3server.agent.mcp.nodes import create_node_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"node_id": "n1", "name": "R1", "status": "started"}) result = create_node_handler({ @@ -147,7 +147,7 @@ class TestNode: assert result == {"node_id": "n1", "name": "R1"} def test_create_fields_validation(self, ctx): - from gns3server.api.routes.mcp.nodes import create_node_handler + from gns3server.agent.mcp.nodes import create_node_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn() m.return_value = conn @@ -159,7 +159,7 @@ class TestNode: conn.http_call.assert_not_called() def test_create_batch_inherits_template_id(self, ctx): - from gns3server.api.routes.mcp.nodes import create_node_handler + from gns3server.agent.mcp.nodes import create_node_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"node_id": "n1", "name": "R1"}) result = create_node_handler({ @@ -169,39 +169,39 @@ class TestNode: assert result[0]["status"] == "success" def test_create_missing_project_id(self, ctx): - from gns3server.api.routes.mcp.nodes import create_node_handler + from gns3server.agent.mcp.nodes import create_node_handler assert create_node_handler({}, ctx) == {"error": "project_id is required"} def test_delete_batch(self, ctx): - from gns3server.api.routes.mcp.nodes import delete_node_handler + from gns3server.agent.mcp.nodes import delete_node_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({}) result = delete_node_handler({"project_id": "p1", "node_ids": ["n1", "n2"]}, ctx) assert len(result) == 2 def test_start_batch(self, ctx): - from gns3server.api.routes.mcp.nodes import start_node_handler + from gns3server.agent.mcp.nodes import start_node_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"status": "started"}) result = start_node_handler({"project_id": "p1", "node_ids": ["n1"]}, ctx) assert result[0]["status"] == "success" def test_stop_batch(self, ctx): - from gns3server.api.routes.mcp.nodes import stop_node_handler + from gns3server.agent.mcp.nodes import stop_node_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"status": "stopped"}) result = stop_node_handler({"project_id": "p1", "node_ids": ["n1"]}, ctx) assert result[0]["status"] == "success" def test_suspend_batch(self, ctx): - from gns3server.api.routes.mcp.nodes import suspend_node_handler + from gns3server.agent.mcp.nodes import suspend_node_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"status": "suspended"}) result = suspend_node_handler({"project_id": "p1", "node_ids": ["n1"]}, ctx) assert result[0]["status"] == "success" def test_console(self, ctx): - from gns3server.api.routes.mcp.nodes import get_node_console_info_handler + from gns3server.agent.mcp.nodes import get_node_console_info_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"console_url": "ws://host/console"}) result = get_node_console_info_handler({"project_id": "p1", "node_id": "n1"}, ctx) @@ -216,21 +216,21 @@ class TestLink: mod = "links" def test_list(self, ctx): - from gns3server.api.routes.mcp.links import get_links_handler + from gns3server.agent.mcp.links import get_links_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn([{"link_id": "l1", "link_type": "ethernet"}]) result = get_links_handler({"project_id": "p1", "fields": ["link_id"]}, ctx) assert result["links"] == [{"link_id": "l1"}] def test_get(self, ctx): - from gns3server.api.routes.mcp.links import get_link_handler + from gns3server.agent.mcp.links import get_link_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"link_id": "l1", "link_type": "ethernet"}) result = get_link_handler({"project_id": "p1", "link_id": "l1"}, ctx) assert result["link_id"] == "l1" def test_create_compact_format(self, ctx): - from gns3server.api.routes.mcp.links import create_link_handler + from gns3server.agent.mcp.links import create_link_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"link_id": "l1", "link_type": "ethernet", "nodes": []}) m.return_value = conn @@ -247,7 +247,7 @@ class TestLink: ) def test_create_standard_format(self, ctx): - from gns3server.api.routes.mcp.links import create_link_handler + from gns3server.agent.mcp.links import create_link_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"link_id": "l1"}) result = create_link_handler({ @@ -260,7 +260,7 @@ class TestLink: assert result["link_id"] == "l1" def test_create_fields_validation(self, ctx): - from gns3server.api.routes.mcp.links import create_link_handler + from gns3server.agent.mcp.links import create_link_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn() m.return_value = conn @@ -273,14 +273,14 @@ class TestLink: conn.http_call.assert_not_called() def test_delete_batch(self, ctx): - from gns3server.api.routes.mcp.links import delete_link_handler + from gns3server.agent.mcp.links import delete_link_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({}) result = delete_link_handler({"project_id": "p1", "link_ids": ["l1", "l2"]}, ctx) assert len(result) == 2 def test_update(self, ctx): - from gns3server.api.routes.mcp.links import update_link_handler + from gns3server.agent.mcp.links import update_link_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"link_id": "l1", "suspend": True}) result = update_link_handler({ @@ -297,14 +297,14 @@ class TestAppliance: mod = "appliances" def test_get(self, ctx): - from gns3server.api.routes.mcp.appliances import get_appliance_handler + from gns3server.agent.mcp.appliances import get_appliance_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"appliance_id": "a1", "name": "Cisco ISE"}) result = get_appliance_handler({"appliance_id": "a1"}, ctx) assert result["name"] == "Cisco ISE" def test_install_with_version(self, ctx): - from gns3server.api.routes.mcp.appliances import install_appliance_handler + from gns3server.agent.mcp.appliances import install_appliance_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"status": "installed"}) m.return_value = conn @@ -317,7 +317,7 @@ class TestAppliance: ) def test_install_missing_id(self, ctx): - from gns3server.api.routes.mcp.appliances import install_appliance_handler + from gns3server.agent.mcp.appliances import install_appliance_handler result = install_appliance_handler({}, ctx) assert "error" in result @@ -330,7 +330,7 @@ class TestTemplate: mod = "templates" def test_list_fields(self, ctx): - from gns3server.api.routes.mcp.templates import list_templates_handler + from gns3server.agent.mcp.templates import list_templates_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn([ {"template_id": "t1", "name": "Cisco 7200", "template_type": "dynamips", @@ -340,21 +340,21 @@ class TestTemplate: assert result["templates"] == [{"template_id": "t1", "name": "Cisco 7200"}] def test_list_invalid_field(self, ctx): - from gns3server.api.routes.mcp.templates import list_templates_handler + from gns3server.agent.mcp.templates import list_templates_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn() result = list_templates_handler({"fields": ["does_not_exist"]}, ctx) assert "error" in result def test_get(self, ctx): - from gns3server.api.routes.mcp.templates import get_template_handler + from gns3server.agent.mcp.templates import get_template_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"template_id": "t1", "name": "Test"}) result = get_template_handler({"template_id": "t1"}, ctx) assert result["name"] == "Test" def test_delete(self, ctx): - from gns3server.api.routes.mcp.templates import delete_template_handler + from gns3server.agent.mcp.templates import delete_template_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({}) result = delete_template_handler({"template_id": "t1"}, ctx) @@ -370,7 +370,7 @@ class TestLinkMarker: mod = "links" def test_update_direction_both_clears(self, ctx): - from gns3server.api.routes.mcp.links import link_marker_handler + from gns3server.agent.mcp.links import link_marker_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"name": "icmp"}) m.return_value = conn @@ -384,7 +384,7 @@ class TestLinkMarker: ) def test_update_direction_tx_sets(self, ctx): - from gns3server.api.routes.mcp.links import link_marker_handler + from gns3server.agent.mcp.links import link_marker_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"name": "icmp"}) m.return_value = conn @@ -398,7 +398,7 @@ class TestLinkMarker: ) def test_update_direction_omitted_preserved(self, ctx): - from gns3server.api.routes.mcp.links import link_marker_handler + from gns3server.agent.mcp.links import link_marker_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"name": "icmp"}) m.return_value = conn @@ -412,7 +412,7 @@ class TestLinkMarker: ) def test_create_direction_both_omitted(self, ctx): - from gns3server.api.routes.mcp.links import link_marker_handler + from gns3server.agent.mcp.links import link_marker_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"name": "icmp"}) m.return_value = conn @@ -426,7 +426,7 @@ class TestLinkMarker: ) def test_create_direction_tx(self, ctx): - from gns3server.api.routes.mcp.links import link_marker_handler + from gns3server.agent.mcp.links import link_marker_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"name": "icmp"}) m.return_value = conn @@ -451,7 +451,7 @@ class TestMarkerDefinition: mod = "links" def test_create_builds_body(self, ctx): - from gns3server.api.routes.mcp.links import marker_definition_handler + from gns3server.agent.mcp.links import marker_definition_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"name": "arp"}) m.return_value = conn @@ -465,7 +465,7 @@ class TestMarkerDefinition: ) def test_create_ignores_direction(self, ctx): - from gns3server.api.routes.mcp.links import marker_definition_handler + from gns3server.agent.mcp.links import marker_definition_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"name": "arp"}) m.return_value = conn @@ -479,7 +479,7 @@ class TestMarkerDefinition: ) def test_update_builds_body(self, ctx): - from gns3server.api.routes.mcp.links import marker_definition_handler + from gns3server.agent.mcp.links import marker_definition_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"name": "arp"}) m.return_value = conn @@ -493,7 +493,7 @@ class TestMarkerDefinition: ) def test_update_ignores_direction(self, ctx): - from gns3server.api.routes.mcp.links import marker_definition_handler + from gns3server.agent.mcp.links import marker_definition_handler with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"name": "arp"}) m.return_value = conn @@ -507,7 +507,7 @@ class TestMarkerDefinition: ) def test_update_requires_a_field(self, ctx): - from gns3server.api.routes.mcp.links import marker_definition_handler + from gns3server.agent.mcp.links import marker_definition_handler with patch(f"{BASE}.{self.mod}._get_connector"): result = marker_definition_handler( {"project_id": "p", "action": "update", "def_name": "arp"}, ctx, diff --git a/tests/api/routes/mcp/test_tool_params.py b/tests/agent/mcp/test_tool_params.py similarity index 99% rename from tests/api/routes/mcp/test_tool_params.py rename to tests/agent/mcp/test_tool_params.py index 8eee59f72..806d809f3 100644 --- a/tests/api/routes/mcp/test_tool_params.py +++ b/tests/agent/mcp/test_tool_params.py @@ -15,7 +15,7 @@ from pathlib import Path import pytest -MCP_DIR = Path(__file__).resolve().parents[4] / "gns3server" / "api" / "routes" / "mcp" +MCP_DIR = Path(__file__).resolve().parents[3] / "gns3server" / "agent" / "mcp" TOOL_FILE = MCP_DIR / "__init__.py" HANDLER_FILES = {