diff --git a/gns3server/api/routes/mcp/__init__.py b/gns3server/api/routes/mcp/__init__.py index d728a348c..75c67a77b 100644 --- a/gns3server/api/routes/mcp/__init__.py +++ b/gns3server/api/routes/mcp/__init__.py @@ -344,6 +344,71 @@ async def update_link(project_id: str, link_id: str, **kwargs: Any) -> list[dict return await asyncio.to_thread(_run_handler_sync, update_link_handler, params) +# ── Template tools ──────────────────────────────────────────────────── + +@mcp.tool() +async def list_templates() -> list[dict[str, Any]]: + """List all available templates on the server.""" + from .templates import list_templates_handler + return await asyncio.to_thread(_run_handler_sync, list_templates_handler, {}) + + +@mcp.tool() +async def get_template(template_id: str = None, name: str = None) -> list[dict[str, Any]]: + """Get detailed information about a specific template. + + Args: + template_id: Template UUID (optional if name is provided) + name: Template name (optional if template_id is provided) + """ + from .templates import get_template_handler + return await asyncio.to_thread(_run_handler_sync, get_template_handler, { + "template_id": template_id, "name": name, + }) + + +@mcp.tool() +async def create_template(name: str, template_type: str, compute_id: str = "local") -> list[dict[str, Any]]: + """Create a new template. + + Args: + name: Template name + template_type: Template type (e.g. qemu, docker, dynamips) + compute_id: Compute ID (optional, default: local) + """ + from .templates import create_template_handler + return await asyncio.to_thread(_run_handler_sync, create_template_handler, { + "name": name, "template_type": template_type, "compute_id": compute_id, + }) + + +@mcp.tool() +async def update_template(template_id: str = None, name: str = None, **kwargs: Any) -> list[dict[str, Any]]: + """Update an existing template's properties. + + Args: + template_id: Template UUID (optional if name is provided) + name: Template name (optional if template_id is provided) + """ + from .templates import update_template_handler + params = {"template_id": template_id, "name": name, **kwargs} + return await asyncio.to_thread(_run_handler_sync, update_template_handler, params) + + +@mcp.tool() +async def delete_template(template_id: str = None, name: str = None) -> list[dict[str, Any]]: + """Delete a template. + + Args: + template_id: Template UUID (optional if name is provided) + name: Template name (optional if template_id is provided) + """ + from .templates import delete_template_handler + return await asyncio.to_thread(_run_handler_sync, delete_template_handler, { + "template_id": template_id, "name": name, + }) + + # ── Auth‑wrapped SSE app ────────────────────────────────────────────── def _make_auth_wrapper(inner_app): diff --git a/gns3server/api/routes/mcp/templates.py b/gns3server/api/routes/mcp/templates.py new file mode 100644 index 000000000..b5df86faf --- /dev/null +++ b/gns3server/api/routes/mcp/templates.py @@ -0,0 +1,156 @@ +# +# Copyright (C) 2026 GNS3 Technologies Inc. +# Author: Yue Guobin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +MCP tool handlers for GNS3 template management. + +Handlers receive (params, gns3_ctx) and call GNS3's REST API +via Gns3Connector (from custom_gns3fy). +""" + +from typing import Any + +import logging + +log = logging.getLogger(__name__) + + +# ── Helper ───────────────────────────────────────────────────────────────── + +def _get_connector(gns3_ctx: dict[str, Any]): + from gns3server.agent.gns3_copilot.gns3_client.custom_gns3fy import Gns3Connector + return Gns3Connector( + url=gns3_ctx["server_url"], + jwt_token=gns3_ctx["jwt_token"], + api_version=3, + verify=False, + ) + + +# ── Tool handlers ────────────────────────────────────────────────────────── + +def list_templates_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: + conn = _get_connector(gns3_ctx) + templates = conn.get_templates() + return {"templates": templates, "count": len(templates)} + + +def get_template_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: + template_id = params.get("template_id") + name = params.get("name") + if not template_id and not name: + return {"error": "template_id or name is required"} + conn = _get_connector(gns3_ctx) + template = conn.get_template(name=name, template_id=template_id) + if template is None: + return {"error": "Template not found"} + return template + + +def create_template_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: + name = params.get("name") + template_type = params.get("template_type") + if not name or not template_type: + return {"error": "name and template_type are required"} + conn = _get_connector(gns3_ctx) + return conn.create_template(**params) + + +def update_template_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: + template_id = params.get("template_id") + name = params.get("name") + if not template_id and not name: + return {"error": "template_id or name is required"} + conn = _get_connector(gns3_ctx) + return conn.update_template(name=name, template_id=template_id, **{ + k: v for k, v in params.items() if k not in ("template_id", "name") + }) + + +def delete_template_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: + template_id = params.get("template_id") + name = params.get("name") + if not template_id and not name: + return {"error": "template_id or name is required"} + conn = _get_connector(gns3_ctx) + conn.delete_template(name=name, template_id=template_id) + return {"message": f"Template deleted"} + + +# ── Tool definitions ─────────────────────────────────────────────────────── + +TEMPLATE_TOOLS = [ + { + "name": "list_templates", + "description": "List all available templates on the server", + "parameters": { + "type": "object", + "properties": {}, + }, + "handler": list_templates_handler, + }, + { + "name": "get_template", + "description": "Get detailed information about a specific template", + "parameters": { + "type": "object", + "properties": { + "template_id": {"type": "string", "description": "Template UUID"}, + "name": {"type": "string", "description": "Template name"}, + }, + }, + "handler": get_template_handler, + }, + { + "name": "create_template", + "description": "Create a new template", + "parameters": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "Template name"}, + "template_type": {"type": "string", "description": "Template type (e.g. qemu, docker, dynamips)"}, + "compute_id": {"type": "string", "description": "Compute ID (optional, default: local)"}, + }, + "required": ["name", "template_type"], + }, + "handler": create_template_handler, + }, + { + "name": "update_template", + "description": "Update an existing template's properties", + "parameters": { + "type": "object", + "properties": { + "template_id": {"type": "string", "description": "Template UUID"}, + "name": {"type": "string", "description": "Template name"}, + }, + }, + "handler": update_template_handler, + }, + { + "name": "delete_template", + "description": "Delete a template", + "parameters": { + "type": "object", + "properties": { + "template_id": {"type": "string", "description": "Template UUID"}, + "name": {"type": "string", "description": "Template name"}, + }, + }, + "handler": delete_template_handler, + }, +]