# FortiGate MCP Server [![Test](https://github.com/alpadalar/fortigate-mcp-server/actions/workflows/test.yml/badge.svg)](https://github.com/alpadalar/fortigate-mcp-server/actions/workflows/test.yml) [![Lint](https://github.com/alpadalar/fortigate-mcp-server/actions/workflows/lint.yml/badge.svg)](https://github.com/alpadalar/fortigate-mcp-server/actions/workflows/lint.yml) [![Security](https://github.com/alpadalar/fortigate-mcp-server/actions/workflows/security.yml/badge.svg)](https://github.com/alpadalar/fortigate-mcp-server/actions/workflows/security.yml) [![Release](https://github.com/alpadalar/fortigate-mcp-server/actions/workflows/release.yml/badge.svg)](https://github.com/alpadalar/fortigate-mcp-server/actions/workflows/release.yml) FortiGate MCP Server - A comprehensive Model Context Protocol (MCP) server for managing FortiGate devices. This project provides programmatic access to FortiGate devices and enables integration with MCP-compatible clients such as Claude Desktop, Claude Code, and Cursor. ## ๐Ÿš€ Features FortiGate MCP Server exposes 52 unique tools, 99 total tool-surface registrations across stdio (49) and HTTP (50) transports; 3 create-tools have transport-specific parameter shapes. Covered areas: - **Device Management**: Add, remove, and test connections to FortiGate devices - **Firewall Management**: List, create, update, and delete firewall rules - **Network Management**: Manage address and service objects - **Routing Management**: Manage static routes and interfaces - **Virtual IP Management**: Manage virtual IPs (VIP/DNAT) - **Security Visibility**: Read-only insight into security profiles, admin accounts (secret fields redacted), SSL-VPN settings, and IPv4 local-in policies - **Config-Object Visibility**: Read-only address groups, service groups, schedules (recurring, one-time, groups), IP pools, central SNAT maps, firewall zones (with an interface-to-zone index), and DHCP server scopes - **Monitor Visibility**: Read-only system resources, license status, HA status (standalone reported as such), ARP table, and DHCP leases - **IPsec, Sessions & Policy Hygiene**: Read-only IPsec tunnel status (no pre-shared keys), capped firewall session listing with validated filters, and offline policy hygiene analysis (shadowed, duplicate, any-any, disabled) - **HTTP Transport**: MCP protocol over HTTP using FastMCP 4 (`fastmcp>=4.1.2,<5`) - **Docker Support**: Easy installation and deployment - **MCP Client Integration**: Works with Claude Desktop, Claude Code, Cursor, and other MCP-compatible clients ## ๐Ÿ“‹ Requirements - Python 3.11+ (3.11, 3.12, 3.13 and 3.14 are tested in CI) - `uv` package manager (recommended) or `pip` - Access to FortiGate device - API token (recommended) or username/password - FortiOS 7.4 and 7.6 are the supported release lines. FortiOS 8.0 has not been verified. On 7.6 and later, use an API token: password/session login is reported to be deprecated there (unofficial community sources; see SECURITY.md) ## ๐Ÿ› ๏ธ Installation ### 1. Clone the Project ```bash git clone https://github.com/alpadalar/fortigate-mcp-server.git cd fortigate-mcp-server ``` ### 2. Install Dependencies ```bash # Using uv (recommended) - installs the locked, reproducible dependency set uv sync --locked # Or using pip pip install -e . ``` ### 3. Configuration Create your local config from the committed example (`config/config.json` is gitignored and does not exist on a fresh clone): ```bash cp config/config.example.json config/config.json ``` Then edit `config/config.json`: ```json { "server": { "allow_writes": false }, "fortigate": { "devices": { "default": { "host": "192.168.1.1", "port": 443, "username": "admin", "password": "your_password", "api_token": "your-api-token", "vdom": "root", "verify_ssl": true, "timeout": 30 } } }, "logging": { "level": "INFO", "file": "./logs/fortigate_mcp.log" } } ``` ## ๐Ÿš€ Usage ### Start HTTP Server ```bash # Start with script ./start_http_server.sh # Or manually uv run python -m src.fortigate_mcp.server_http \ --host 127.0.0.1 \ --port 8814 \ --path /fortigate-mcp \ --config config/config.json ``` Use `--host 0.0.0.0` only if you need access from other machines on the network AND have `auth.require_auth=true` configured in `config/config.json`; otherwise keep `127.0.0.1` โ€” unauthenticated HTTP should never bind wider than loopback. ### Run with Docker ```bash # Build and start docker-compose up -d # View logs docker-compose logs -f fortigate-mcp-server ``` The compose file publishes port 8814 on loopback only (`127.0.0.1:8814:8814`), so the server is reachable solely from the Docker host itself. To expose it beyond `127.0.0.1`, first set `auth.require_auth=true` in `config/config.json`, then widen the `ports:` mapping deliberately โ€” see SECURITY.md. ## ๐Ÿ”ง MCP Client Integration FortiGate MCP Server works with any MCP-compatible client. Verified, ready-to-use config examples are provided for Claude Desktop, Claude Code, and Cursor: - [`examples/claude_desktop_config.stdio.json`](examples/claude_desktop_config.stdio.json) โ€” Claude Desktop, stdio transport (recommended) - [`examples/claude_desktop_config.http.json`](examples/claude_desktop_config.http.json) โ€” Claude Desktop, HTTP transport via the `mcp-remote` bridge - [`examples/claude_code_mcp.json`](examples/claude_code_mcp.json) โ€” Claude Code project-scope `.mcp.json` (stdio and HTTP entries) - [`examples/cursor_mcp_config.json`](examples/cursor_mcp_config.json) โ€” Cursor MCP configuration Every stdio example in this project launches the server via `uv run --directory python -m src.fortigate_mcp.server` instead of a bare `python -m ...` command. This matters because GUI-launched MCP clients (like Claude Desktop) commonly start commands from a directory other than the repo root โ€” the explicit `--directory` flag makes the invocation working-directory independent, so it keeps working regardless of where the client process happens to start from. ### Claude Code (`.mcp.json`) Add a project-scope `.mcp.json` at your repository root (see `examples/claude_code_mcp.json` for the full file, including the HTTP entry): ```json { "mcpServers": { "fortigate-mcp-stdio": { "type": "stdio", "command": "uv", "args": ["run", "--directory", "${CLAUDE_PROJECT_DIR}", "python", "-m", "src.fortigate_mcp.server"], "env": { "FORTIGATE_MCP_CONFIG": "${CLAUDE_PROJECT_DIR}/config/config.json" } } } } ``` Or register the HTTP transport via the Claude Code CLI: ```bash claude mcp add --transport http fortigate-mcp http://127.0.0.1:8814/fortigate-mcp --header "Authorization: Bearer " ``` `--header` is only needed when `auth.require_auth=true` is set in `config/config.json`. ### Claude Desktop Claude Desktop's native config schema validates stdio servers only. Use `examples/claude_desktop_config.stdio.json` for the stdio transport (recommended). For the HTTP transport, `examples/claude_desktop_config.http.json` bridges to this server through the community `mcp-remote` npm package (`npx -y mcp-remote ...`) โ€” this package is not vetted or installed by this project; inspect it yourself before running it, since `npx` fetches and executes third-party code on your behalf. ### Cursor See `examples/cursor_mcp_config.json` for a working stdio configuration. ## ๐Ÿ“š API Commands ### Device Management - `list_devices` - List registered devices - `get_device_status` - Get device status - `test_device_connection` - Test connection - `add_device` - Add new device - `remove_device` - Remove device - `discover_vdoms` - Discover VDOMs ### Firewall Management - `list_firewall_policies` - List firewall rules - `create_firewall_policy` - Create new rule - `update_firewall_policy` - Update rule - `delete_firewall_policy` - Delete rule ### Network Management - `list_address_objects` - List address objects - `create_address_object` - Create address object - `list_service_objects` - List service objects - `create_service_object` - Create service object ### Virtual IP Management - `list_virtual_ips` - List virtual IPs - `create_virtual_ip` - Create virtual IP - `update_virtual_ip` - Update virtual IP - `get_virtual_ip_detail` - Get virtual IP detail - `delete_virtual_ip` - Delete virtual IP ### Routing Management - `list_static_routes` - List static routes - `create_static_route` - Create static route - `update_static_route` - Update static route - `delete_static_route` - Delete static route - `get_static_route_detail` - Get static route detail - `get_routing_table` - Get routing table - `list_interfaces` - List interfaces - `get_interface_status` - Get interface status ### Security & Admin Visibility (read-only) - `list_security_profiles` - List security profiles (antivirus, IPS, web filter, application control) - `list_admins` - List admin accounts (secret fields redacted; `vdom` is expected to be a no-op on this global-scope endpoint) - `get_sslvpn_settings` - Get SSL-VPN settings and portal bookmarks (bookmark credentials redacted) - `list_local_in_policies` - List IPv4 local-in policies (IPv6 `local-in-policy6` not covered) ### Config-Object & Monitor Visibility (read-only) All take `device_id` and an optional `vdom`. A FortiOS 404 is reported as "not supported on this FortiOS version". - `list_address_groups` - List address groups - `list_service_groups` - List service groups - `list_schedules` - List recurring, one-time and group schedules - `list_ip_pools` - List IP pools - `list_central_snat_maps` - List central SNAT maps - `get_system_resources` - Latest CPU/memory/session usage values - `get_license_status` - Get license and subscription status - `get_ha_status` - Get HA peer status (empty result means standalone) - `get_arp_table` - Get ARP table (`interface` filter, `limit` default 100, max 500) - `list_dhcp_leases` - List DHCP leases (`interface` filter, `limit` default 100, max 500) - `get_ipsec_tunnel_status` - IPsec tunnel and phase 2 selector status (never returns pre-shared keys) - `list_firewall_sessions` - List active sessions (`count` default 20, max 200; `srcaddr`, `dstaddr`, `srcport`, `dstport`, `proto` filters) - `analyze_policy_hygiene` - Offline analysis of shadowed, duplicate, any-any and disabled policies ### System Commands - `health` - Health check - `test_connection` - Connection test - `get_schema_info` - Schema information ## ๐Ÿงช Testing ### Run Tests ```bash # One-time setup: test dependencies (pytest, pytest-cov, respx, pyyaml) live in the # dev/test extras โ€” the plain `uv sync --locked` from the install step does not install them uv sync --locked --all-extras # Quick run with coverage disabled (a bare -q only reduces verbosity; # without --no-cov the coverage gate from pyproject.toml addopts still runs) uv run pytest -q --no-cov # Full suite with coverage (--cov-fail-under=67 enforced per pyproject.toml) uv run pytest # Run specific test files uv run pytest tests/test_device_manager.py uv run pytest tests/test_fortigate_api.py uv run pytest tests/test_tools.py # Verbose output uv run pytest -v # Detailed error information uv run pytest --tb=long ``` ### Test Categories - **Unit Tests**: Test individual components and functions - **Coverage**: Code coverage reporting with HTML output ### Manual Testing The `/health` route lives at the app root, is exempt from Bearer-token auth, and works with plain `curl`: ```bash # Liveness probe curl http://127.0.0.1:8814/health ``` The MCP protocol itself cannot be exercised with a bare `curl` POST: the streamable-HTTP transport requires an `initialize` handshake, session management, and `tools/call` framing. Use a real MCP client for protocol-level testing โ€” for example `fastmcp.Client`: ```bash uv run python - <<'PY' import asyncio from fastmcp import Client async def main(): # Trailing slash matches the mount convention; the non-slash form 307-redirects async with Client("http://127.0.0.1:8814/fortigate-mcp/") as client: tools = await client.list_tools() print(f"{len(tools)} tools registered") result = await client.call_tool("health", {}) print(result.content[0].text) asyncio.run(main()) PY ``` Alternatively, bridge with `npx -y mcp-remote http://127.0.0.1:8814/fortigate-mcp` or use the MCP Inspector. ## ๐Ÿ“ Project Structure ``` fortigate-mcp-server/ โ”œโ”€โ”€ src/ โ”‚ โ””โ”€โ”€ fortigate_mcp/ โ”‚ โ”œโ”€โ”€ __init__.py โ”‚ โ”œโ”€โ”€ server.py # STDIO MCP server โ”‚ โ”œโ”€โ”€ server_http.py # HTTP MCP server โ”‚ โ”œโ”€โ”€ config/ # Configuration management โ”‚ โ”œโ”€โ”€ core/ # Core components โ”‚ โ”œโ”€โ”€ tools/ # MCP tools โ”‚ โ””โ”€โ”€ formatting/ # Response formatting โ”œโ”€โ”€ config/ โ”‚ โ”œโ”€โ”€ config.json # Main configuration (gitignored, created from example) โ”‚ โ””โ”€โ”€ config.example.json # Example configuration โ”œโ”€โ”€ examples/ โ”‚ โ”œโ”€โ”€ claude_desktop_config.stdio.json # Claude Desktop, stdio transport โ”‚ โ”œโ”€โ”€ claude_desktop_config.http.json # Claude Desktop, HTTP via mcp-remote โ”‚ โ”œโ”€โ”€ claude_code_mcp.json # Claude Code .mcp.json โ”‚ โ””โ”€โ”€ cursor_mcp_config.json # Cursor MCP config โ”œโ”€โ”€ logs/ # Log files โ”œโ”€โ”€ tests/ # Test files โ”œโ”€โ”€ docker-compose.yml # Docker compose โ”œโ”€โ”€ Dockerfile # Docker image โ”œโ”€โ”€ start_server.sh # STDIO startup script โ”œโ”€โ”€ start_http_server.sh # HTTP startup script โ””โ”€โ”€ README.md # This file ``` ## ๐Ÿ” Troubleshooting ### Common Issues 1. **Connection Error** - Ensure FortiGate device is accessible - Verify API token or username/password - If you see SSL certificate errors, install the FortiGate device's certificate as trusted (or replace it with a CA-signed certificate) โ€” do not disable certificate verification. `verify_ssl` defaults to `true` and must stay `true`; see SECURITY.md for the rationale. 2. **Port Conflict** - Ensure port 8814 is available - Change port using `--port` parameter 3. **Configuration Error** - Ensure `config.json` is properly formatted - Check JSON syntax 4. **MCP Client Connection Issue** - Ensure the server is running - Verify the config file path and URL are correct - Restart the MCP client (Claude Desktop / Claude Code / Cursor) ### Logs Check logs using: ```bash # HTTP server logs tail -f logs/fortigate_mcp.log # Docker logs docker-compose logs -f fortigate-mcp-server ``` ## ๐Ÿ”’ Security ### Implemented Controls 1. **Write protection (default: read-only)** - Write and destructive tools are rejected unless explicitly enabled via `server.allow_writes: true` in config, or the `FORTIGATE_MCP_ALLOW_WRITES=1` environment variable 2. **TLS certificate verification (default: on)** - Config-loaded devices verify TLS certificates by default (`verify_ssl: true`) 3. **Bearer-token authentication (optional, default: off)** - Available via `auth.require_auth` / `auth.api_tokens`; unauthenticated by default โ€” run only on trusted networks when disabled 4. **Secret redaction** - API tokens and passwords are stored as `SecretStr` and are never written to logs ### Known Limitations - Rate limiting is parsed from config but **not enforced** โ€” do not rely on it as a working control. - Unauthenticated HTTP (the default) should bind to `127.0.0.1` (loopback) only; binding to `0.0.0.0` for wider network access requires enabling Bearer auth (`auth.require_auth=true`) plus network-level controls (firewall rules, VPN, reverse-proxy allowlists). See [SECURITY.md](SECURITY.md) for the full threat model and known limitations. ## ๐Ÿค Contributing 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## ๐Ÿ“„ License This project is licensed under the MIT License. See the `LICENSE` file for details. ## ๐Ÿ™ Acknowledgments - [FastMCP](https://gofastmcp.com/) - For MCP HTTP transport - [FortiGate API](https://docs.fortinet.com/document/fortigate/7.4.0/administration-guide/109229/rest-api) - For FortiGate integration ## ๐Ÿ“ž Support For issues: - Use the [Issues](https://github.com/alpadalar/fortigate-mcp-server/issues) page - Check the documentation - Review the logs --- **Note**: This project has been tested with FortiGate devices. Please perform comprehensive testing before using in production.