mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
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.
This commit is contained in:
21 files changed
+65
-62
No files matched your search
File renamed without changes.
@@ -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,
|
||||
@@ -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 = {
|
||||
Reference in new issue
Block a user