# MCP (Model Context Protocol) Service ## Overview GNS3 Server provides a standard [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) interface, allowing AI assistants like Claude to interact with GNS3 network simulations through SSE (Server-Sent Events) transport. The MCP service exposes GNS3 project management operations as MCP tools that can be discovered and called by MCP clients. ## Endpoints | Path | Method | Description | |------|--------|-------------| | `/v3/mcp/` | GET | MCP service metadata | | `/v3/mcp/transport/sse` | GET | SSE stream (MCP connection) | | `/v3/mcp/transport/messages/` | POST | JSON-RPC messages | ## Authentication The SSE endpoint supports two types of credentials, passed the same way. 1. **Authorization header** (recommended): ``` Authorization: Bearer ``` 2. **Query parameter** (for clients that don't support custom headers): ``` GET /v3/mcp/transport/sse?token= ``` ### Option 1: JWT Token (24h expiry) ```bash curl -X POST http://localhost:3080/v3/access/users/authenticate \ -H "Content-Type: application/json" \ -d '{"username": "admin", "password": "admin"}' ``` Default lifetime is **1440 minutes (24 hours)**. Configurable in `gns3_server.conf`: ```ini jwt_access_token_expire_minutes = 1440 ; 24 hours ``` ### Option 2: API Key (permanent, revocable) — Recommended for MCP API keys never expire and can be revoked individually. Create one via the REST API: ```bash # Create an API key (requires a JWT to authenticate) curl -X POST http://localhost:3080/v3/access/api-keys \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"name": "MCP Production"}' # Response: {"api_key": "gns3_a1b2c3d4...", "api_key_id": "...", ...} # ⚠️ The key is only shown once — save it immediately. ``` API key management endpoints: | Endpoint | Description | |----------|-------------| | `POST /v3/access/api-keys` | Create a new key (returns plaintext once) | | `GET /v3/access/api-keys` | List all your keys | | `POST /v3/access/api-keys/{id}/revoke` | Revoke a key (can be restored) | | `POST /v3/access/api-keys/{id}/restore` | Restore a revoked key | | `DELETE /v3/access/api-keys/{id}` | Permanently delete a key | Both JWT tokens and API keys work for MCP and REST API endpoints interchangeably. ## Available Tools **82 tools** across 12 categories: ### Project (15) | Tool | Description | |------|-------------| | `project_list` | List all projects | | `project_get` | Get project details | | `project_create` | Create a project | | `project_delete` | Delete a project | | `project_open` | Open a closed project | | `project_close` | Close an open project | | `project_stats` | Get project statistics | | `project_update` | Update project properties | | `project_duplicate` | Duplicate a project | | `project_readme_get` | Get project README content | | `project_readme_update` | Update project README | | `project_lock` | Lock project (prevent edits) | | `project_unlock` | Unlock project | | `project_load` | Load project from path | | `project_locked` | Check if project is locked | ### Node (22) | Tool | Description | |------|-------------| | `node_list` | List all nodes in a project | | `node_get` | Get node details | | `node_create` | Create a node from template | | `node_delete` | Delete a node | | `node_update` | Update node properties | | `node_start` | Start a node | | `node_stop` | Stop a node | | `node_reload` | Reload a node | | `node_suspend` | Suspend a node | | `node_console` | Get WebSocket console URL | | `node_file_list` | List files in node directory | | `node_file_get` | Read a file (with offset/limit) | | `node_file_write` | Write a file | | `node_file_delete` | Delete a file | | `node_start_all` | Start all nodes | | `node_stop_all` | Stop all nodes | | `node_suspend_all` | Suspend all nodes | | `node_reload_all` | Reload all nodes | | `node_duplicate` | Duplicate a node | | `node_isolate` | Isolate a node (suspend links) | | `node_unisolate` | Un-isolate a node (resume links) | | `node_links` | List links connected to a node | ### Link (9) | Tool | Description | |------|-------------| | `link_list` | List all links in a project | | `link_get` | Get link details | | `link_create` | Create a link between nodes | | `link_delete` | Delete a link | | `link_update` | Update link (suspend, filters) | | `link_reset` | Reset link (delete + recreate) | | `link_capture_start` | Start packet capture | | `link_capture_stop` | Stop packet capture | | `link_capture_download` | Get PCAP download URL | ### Template (5) | Tool | Description | |------|-------------| | `template_list` | List all templates | | `template_get` | Get template details | | `template_create` | Create a template (Docker needs `image`) | | `template_update` | Update a template | | `template_delete` | Delete a template | ### Compute (3) | Tool | Description | |------|-------------| | `compute_list` | List registered remote computes | | `compute_get` | Get compute details (requires UUID) | | `compute_images` | List emulator images on a compute | ### Snapshot (4) | Tool | Description | |------|-------------| | `snapshot_list` | List snapshots | | `snapshot_create` | Create a snapshot | | `snapshot_delete` | Delete a snapshot | | `snapshot_restore` | Restore a snapshot | ### Drawing (5) | Tool | Description | |------|-------------| | `drawing_list` | List drawings on canvas | | `drawing_get` | Get drawing details | | `drawing_create` | Create drawing (SVG label/shape/image) | | `drawing_update` | Update drawing (position, rotation, SVG) | | `drawing_delete` | Delete a drawing | ### Symbol (6) | Tool | Description | |------|-------------| | `symbol_list` | List all symbols | | `symbol_get` | Get symbol download URL | | `symbol_dimensions` | Get symbol dimensions | | `symbol_defaults` | Get default symbol mapping | | `symbol_upload` | Upload a custom symbol (SVG content) | | `symbol_delete` | Delete a custom symbol (built-in: 403) | ### Appliance (3) | Tool | Description | |------|-------------| | `appliance_list` | List appliances from template library | | `appliance_get` | Get appliance details | | `appliance_install` | Create template from appliance (images must exist locally) | ### Image (5) | Tool | Description | |------|-------------| | `image_list` | List all images | | `image_get` | Get image details | | `image_delete` | Delete an image | | `image_prune` | Remove images not referenced by any template | | `image_install` | Auto-create templates from uploaded images by checksum | ### Server (2) | Tool | Description | |------|-------------| | `server_version` | Get GNS3 server version | | `server_statistics` | Get server statistics (computes, projects, nodes) | ### Device Config (3) | Tool | Description | |------|-------------| | `device_config_send` | Push config commands to devices via console (Nornir + Netmiko) | | `device_command_run` | Run read-only show commands on devices | | `vpcs_config_set` | Configure VPCS devices (IP, gateway, etc.) | Requires nodes to be started first. Device type is auto-detected from the node's `device_type:` tag. ## Configuration ### Claude Code (CLI) ```bash # Option A: Using API key (recommended — never expires) claude mcp add --transport sse My_GNS3_Server \ http://localhost:3080/v3/mcp/transport/sse \ -H "Authorization: Bearer gns3_a1b2c3d4..." # Option B: Using JWT token (expires after 24h) TOKEN=$(curl -s -X POST http://localhost:3080/v3/access/users/authenticate \ -H "Content-Type: application/json" \ -d '{"username": "admin", "password": "admin"}' | python3 -c \ "import sys,json; print(json.load(sys.stdin)['access_token'])") claude mcp add --transport sse My_GNS3_Server \ http://localhost:3080/v3/mcp/transport/sse \ -H "Authorization: Bearer $TOKEN" ``` ### Claude Desktop Add to `claude_desktop_config.json`: ```json { "mcpServers": { "My_GNS3_Server": { "url": "http://localhost:3080/v3/mcp/transport/sse?token=your_jwt_or_api_key" } } } ``` ## Transport Security MCP server uses FastMCP's DNS rebinding protection to prevent attackers from exploiting DNS resolution to access the MCP endpoint through unauthorized domains. ### Default Behaviour DNS rebinding protection is **disabled by default**, allowing connections from any host. This aligns with GNS3 server's default `host = 0.0.0.0` binding policy, which is designed for VM distribution scenarios where users access the server from various network locations. ### Enabling Protection Add to `gns3_server.conf` under the `[Server]` section: ```ini ; Enable DNS rebinding protection for MCP server mcp_enable_dns_rebinding_protection = True ; Allowed hosts (comma-separated, "host:*" port wildcard patterns only) mcp_allowed_hosts = 127.0.0.1:*,localhost:*,192.168.1.3:* ; Allowed origins (comma-separated) mcp_allowed_origins = http://127.0.0.1:*,http://localhost:*,http://192.168.1.3:* ``` > **Note**: The MCP library only supports `"host:*"` port wildcard patterns > (e.g., `"192.168.1.3:*"`). Standalone `"*"` wildcards are not supported. ### Protection Mechanism When protection is enabled, the MCP server validates the `Host` header of incoming SSE connection requests: ```python # Verify the request's Host header matches allowed patterns validate_request → check Host header → 421 Misdirected Request if invalid ``` This prevents DNS rebinding attacks: 1. Attacker registers `evil.com` pointing to your server's IP 2. User's browser makes requests to `evil.com:3080` 3. MCP server checks Host header = `"evil.com:3080"` 4. `"evil.com:3080"` is not in `allowed_hosts` → connection rejected ### Behaviour Summary | `mcp_enable_dns_rebinding_protection` | Result | |:---|:---| | `False` (default) | All hosts allowed | | `True` + correct hosts configured | Only configured hosts allowed | | `True` + missing/wrong hosts | Connections rejected with 421 | For public-facing MCP servers, set `allowed_hosts` to your server's domain name. ## Architecture ```mermaid sequenceDiagram participant Client as Claude Code / Claude Desktop participant MCP as MCP Service participant Auth as JWT Auth participant GNS3 as GNS3 REST API Note over Client: 1. Connect with JWT Client->>MCP: GET /sse (token in header or query) MCP->>Auth: Validate Token Auth-->>MCP: Token Valid MCP-->>Client: event: endpoint /messages/?session_id=xxx Note over Client: 2. Initialize Client->>MCP: POST /messages/ (initialize) MCP-->>Client: event: message (protocolVersion, capabilities) Note over Client: 3. List & Call Tools Client->>MCP: POST /messages/ (tools/list) MCP-->>Client: event: message (tools list) Client->>MCP: POST /messages/ (tools/call list_projects) MCP->>GNS3: Gns3Connector HTTP request GNS3-->>MCP: Projects data MCP-->>Client: event: message (tool result) ``` ## Internal Implementation - **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` - JWT tokens are validated using GNS3's existing `auth_service` - Tool handlers use `Gns3Connector` (from `custom_gns3fy`) to call GNS3's own REST API, 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 The `get_node_console_info` tool returns a WebSocket URL for connecting to a node's console. This endpoint is protocol-agnostic — it works for **telnet**, **ssh**, and **vnc** console types alike. The WebSocket simply proxies raw byte streams between the client and the compute node; protocol negotiation (e.g. SSH key exchange) happens on the compute side. The WebSocket URL is constructed using the server's `_server_url()`, which resolves the host as follows: | `Server.host` value | Resolved host in URL | |:---|:---| | Specific IP or hostname (e.g. `192.168.1.3`) | Used as-is | | `0.0.0.0` (IPv4 any, default) | Detected via **default route interface IP** | | `::` (IPv6 any) | Detected via default route interface IP | | Detection failure | Fallback to `127.0.0.1` | When `Server.host` is `0.0.0.0` (listen on all interfaces), the MCP server discovers the default route interface IP using a UDP socket connect to `8.8.8.8:80` — no network data is sent, the operating system simply selects the interface that would be used for the default route. This ensures the returned WebSocket URL uses a reachable address (e.g. `192.168.1.3` instead of `127.0.0.1`). If the configured host is already a specific IP or hostname (not `0.0.0.0`), it is used directly in the URL without modification. Use `websocat` to connect from the command line: ```bash # The host in the URL is automatically resolved to a reachable address websocat ws://192.168.1.3:3080/v3/projects/{project_id}/nodes/{node_id}/console/ws?token= ``` ### Source Files | 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/api/server.py` | Mounts MCP routes via `register_starlette_routes()` |