A production-ready Model Context Protocol (MCP) server for managing FortiGate firewalls
Features • Quick Start • Configuration • Tools • Architecture • Security • Testing
Fork Notice: This is an improved fork of alpadalar/fortigate-mcp-server with significant security, async, and code quality improvements. See the changelog below.
This fork includes a comprehensive overhaul of the original codebase. Every change is listed below so you know exactly what was modified and why.
- All API methods converted to
async defacross every tool and core module — the original used synchronoushttpx.Clientcalls insideasync defwrappers, which blocked the event loop - Persistent
httpx.AsyncClientwith connection pooling — replaced per-request client creation incore/fortigate.pywith a single persistent client per device, significantly reducing connection overhead - Async context manager support — added
__aenter__/__aexit__toFortiGateAPIfor proper resource lifecycle management - Async resource cleanup —
FortiGateManager.remove_device(),close_all(), andtest_all_connections()are now properly async, ensuring HTTP clients are closed cleanly
| What Changed | Before | After |
|---|---|---|
verify_ssl default |
False |
True |
allowed_origins (CORS) |
["*"] (allow all) |
[] (deny all) |
add_device() verify_ssl |
False in 4 locations |
True everywhere |
Bare except: clauses |
4 instances in firewall.py |
All replaced with except Exception: |
- Removed
get_schema_info()— dead method on all 5 tool classes and the HTTP server endpoint; never called, returned stale data - Removed duplicate
get_policy_detail_async()— redundant method infirewall.pythat duplicatedget_policy_detail() - Fixed
server.pysignature mismatches —create_address_object,create_service_object, andcreate_static_routeMCP registrations were passing dicts but tool methods expected individual parameters - Fixed
server_http.pysync init — removed_test_initial_connection()from__init__(can't call async from sync constructor) - Fixed
server.pystale reference —get_policy_detail_asyncrenamed toget_policy_detail
- Replaced wildcard import —
from .tools.definitions import *inserver.pyreplaced with 29 explicit named imports - Removed all unused imports — cleaned
Dict,Any,FortiGateAPIError,json,logging,MagicMock,patch,asyncioacross 7 files - Error output to stderr — entry point
print()calls redirected tosys.stderrfor proper error handling - Added missing tool definitions —
GET_INTERFACE_STATUS_DESCwas missing fromdefinitions.py - Added missing format handlers —
virtual_ips,virtual_ip_detail,interface_status, andstatic_route_detailhandlers added tobase.py
| What Changed | Before | After |
|---|---|---|
mcp dependency |
mcp @ git+https://... (git URL) |
mcp>=1.0.0 (PyPI) |
fastmcp dependency |
Duplicate entry | Removed duplicate |
- Complete test rewrite — all tests converted to async with
pytest-asyncioandAsyncMock - 117 tests passing across 6 test modules
- New
test_config.py— 17 tests validating security defaults (verify_ssl=True,allowed_origins=[]) - Strengthened weak assertions — 4 tests that only checked mock calls now also validate return values
- Cleaned test imports — removed unused
MagicMock,FortiGateManager,FortiGateAPI,AuthConfig,patch
| File | Changes |
|---|---|
core/fortigate.py |
Full async rewrite, connection pooling, context manager |
tools/device.py |
Async methods, removed dead code, fixed verify_ssl |
tools/firewall.py |
Async methods, fixed bare excepts, removed duplicates |
tools/network.py |
Async methods, removed dead code |
tools/routing.py |
Async methods, removed dead code |
tools/virtual_ip.py |
Async methods, removed dead code |
tools/base.py |
Added 4 missing format handlers |
tools/definitions.py |
Added missing GET_INTERFACE_STATUS_DESC |
server.py |
Explicit imports, fixed signatures, verify_ssl, stderr |
server_http.py |
Async wrappers, removed sync init test, verify_ssl, stderr |
config/models.py |
verify_ssl=True, allowed_origins=[] |
pyproject.toml |
PyPI mcp dependency, removed duplicate |
tests/conftest.py |
AsyncMock fixtures, cleaned imports |
tests/test_config.py |
New — 17 security default tests |
tests/test_device_manager.py |
Async rewrite |
tests/test_fortigate_api.py |
Async rewrite with connection pool tests |
tests/test_tools.py |
Async rewrite, strengthened assertions |
README.md |
Complete rewrite |
FortiGate MCP Server exposes FortiGate firewall management capabilities through the Model Context Protocol, enabling AI assistants and MCP-compatible tools to programmatically manage firewall policies, network objects, routing, and device configurations.
Built with fully async Python, persistent HTTP connection pooling, and security-first defaults.
Device Management
- Multi-device support with concurrent management
- API token and basic authentication
- Connection testing and health monitoring
- VDOM discovery and per-VDOM operations
Firewall Policy Management
- Full CRUD for firewall policies
- Policy detail with address/service object resolution
- VDOM-scoped operations
Network Object Management
- Address objects (subnet, IP range, FQDN)
- Service objects (TCP/UDP/SCTP with port ranges)
Virtual IP Management
- NAT/DNAT virtual IPs
- Port forwarding configuration
- Protocol-specific VIP rules
Routing
- Static route CRUD operations
- Routing table inspection
- Interface listing and status monitoring
Infrastructure
- Fully async API client with
httpx.AsyncClientconnection pooling - STDIO and HTTP transport modes
- Pydantic configuration models with validation
- Structured logging with API call tracing
- Rate limiting support
- Python 3.11+
- Access to a FortiGate device with API enabled
- API token (recommended) or admin credentials
git clone https://github.com/Aprazor/fortigate-mcp-server.git
cd fortigate-mcp-server
python -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
pip install -e .Create a configuration file (e.g., config/config.json):
{
"fortigate": {
"devices": {
"fw-primary": {
"host": "192.168.1.1",
"port": 443,
"api_token": "your-api-token-here",
"vdom": "root",
"verify_ssl": true,
"timeout": 30
}
}
},
"server": {
"name": "fortigate-mcp-server",
"host": "0.0.0.0",
"port": 8814
},
"auth": {
"require_auth": false,
"allowed_origins": []
},
"logging": {
"level": "INFO",
"console": true
}
}STDIO mode (for direct MCP client integration):
export FORTIGATE_MCP_CONFIG=config/config.json
python -m src.fortigate_mcp.serverHTTP mode (for web-based access):
python -m src.fortigate_mcp.server_http \
--host 0.0.0.0 \
--port 8814 \
--config config/config.jsonClaude Desktop / Claude Code (~/.claude/mcp_servers.json):
{
"mcpServers": {
"fortigate": {
"command": "python",
"args": ["-m", "src.fortigate_mcp.server"],
"env": {
"FORTIGATE_MCP_CONFIG": "/path/to/config.json"
}
}
}
}Cursor IDE (~/.cursor/mcp_servers.json):
{
"mcpServers": {
"FortiGateMCP": {
"url": "http://localhost:8814/fortigate-mcp/",
"transport": "http"
}
}
}| Tool | Description |
|---|---|
list_devices |
List all registered FortiGate devices |
get_device_status |
Get system status for a device |
test_device_connection |
Test connectivity to a device |
add_device |
Register a new FortiGate device |
remove_device |
Remove a registered device |
discover_vdoms |
Discover Virtual Domains on a device |
| Tool | Description |
|---|---|
list_firewall_policies |
List all firewall policies |
create_firewall_policy |
Create a new firewall policy |
update_firewall_policy |
Update an existing policy |
get_firewall_policy_detail |
Get policy with resolved objects |
delete_firewall_policy |
Delete a firewall policy |
| Tool | Description |
|---|---|
list_address_objects |
List firewall address objects |
create_address_object |
Create address object (subnet/range/FQDN) |
list_service_objects |
List firewall service objects |
create_service_object |
Create service object (TCP/UDP/SCTP) |
| Tool | Description |
|---|---|
list_virtual_ips |
List virtual IP configurations |
create_virtual_ip |
Create VIP with optional port forwarding |
update_virtual_ip |
Update virtual IP configuration |
get_virtual_ip_detail |
Get detailed VIP information |
delete_virtual_ip |
Delete a virtual IP |
| Tool | Description |
|---|---|
list_static_routes |
List configured static routes |
create_static_route |
Create a new static route |
update_static_route |
Update an existing static route |
delete_static_route |
Delete a static route |
get_static_route_detail |
Get detailed route information |
get_routing_table |
Get the active routing table |
list_interfaces |
List network interfaces |
get_interface_status |
Get interface operational status |
| Tool | Description |
|---|---|
health_check |
Server health and device connectivity status |
get_server_info |
Server version and configuration info |
fortigate-mcp-server/
├── src/fortigate_mcp/
│ ├── server.py # STDIO MCP server (FastMCP)
│ ├── server_http.py # HTTP MCP server (FastMCP)
│ ├── config/
│ │ ├── loader.py # Configuration file loading
│ │ └── models.py # Pydantic config models
│ ├── core/
│ │ ├── fortigate.py # Async API client + device manager
│ │ └── logging.py # Structured logging setup
│ ├── tools/
│ │ ├── base.py # Base tool class (error handling, formatting)
│ │ ├── definitions.py # Tool description constants
│ │ ├── device.py # Device management tools
│ │ ├── firewall.py # Firewall policy tools
│ │ ├── network.py # Address/service object tools
│ │ ├── routing.py # Routing and interface tools
│ │ └── virtual_ip.py # Virtual IP tools
│ └── formatting/
│ ├── formatters.py # MCP content formatters
│ └── templates.py # Response templates
└── tests/
├── conftest.py # Shared fixtures (AsyncMock)
├── test_config.py # Configuration model tests
├── test_device_manager.py # Device manager lifecycle tests
├── test_fortigate_api.py # Async API client tests
├── test_formatting.py # Response formatting tests
└── test_tools.py # Tool integration tests
- Fully async: All API calls use
httpx.AsyncClientwith persistent connection pooling per device - Security by default: SSL verification enabled, empty CORS origins, no wildcard defaults
- Clean separation: Config models, API client, tool logic, and formatting are independent layers
- Error categorization: FortiGate API errors are mapped to user-friendly messages with HTTP status awareness
This server is designed with security-first defaults:
| Setting | Default | Description |
|---|---|---|
verify_ssl |
true |
SSL certificate verification enabled |
allowed_origins |
[] |
No CORS origins allowed (explicit opt-in) |
require_auth |
false |
MCP server authentication (enable for production) |
Recommendations for production:
- Use API tokens instead of username/password authentication
- Keep
verify_ssl: trueunless testing with self-signed certificates - Set explicit
allowed_originswhen using HTTP transport - Enable
require_authwith configured API tokens for the MCP server itself - Run the server on a trusted network or behind a reverse proxy
- Use environment variables for sensitive configuration values
The project includes 117 tests covering the full async stack:
# Run all tests
python -m pytest
# Run with verbose output
python -m pytest -v
# Run specific test module
python -m pytest tests/test_tools.py
# Run with coverage report
python -m pytest --cov=src --cov-report=html| Module | Coverage |
|---|---|
| Config models | Security defaults, validation, Pydantic models |
| API client | Async HTTP, connection pooling, error handling |
| Device manager | Lifecycle (add/remove/list), async operations |
| Tool classes | All CRUD operations, VDOM support, error paths |
| Formatting | Templates, content rendering, edge cases |
Connection refused
- Verify the FortiGate device is reachable and the API is enabled
- Check that the port (default 443) is not blocked by network firewalls
Authentication failed (401)
- Verify your API token is valid and has appropriate permissions
- For basic auth, confirm the username/password are correct
SSL certificate error
- For self-signed certificates in lab environments, set
verify_ssl: false - For production, install a valid certificate on the FortiGate device
VDOM not found
- Use
discover_vdomsto list available VDOMs on the device - Ensure the VDOM name matches exactly (case-sensitive)
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Write tests for new functionality
- Ensure all tests pass (
python -m pytest) - Commit your changes (
git commit -m 'Add my feature') - Push to your branch (
git push origin feature/my-feature) - Open a Pull Request
This project is licensed under the MIT License. See the LICENSE file for details.
- Model Context Protocol - The protocol specification
- FastMCP - Python MCP server framework
- FortiGate REST API - FortiGate API documentation
- httpx - Async HTTP client