FortiGate Read-Only
A production-grade, read-only Model Context Protocol server for the Fortinet FortiGate FortiOS REST API v2. Launches directly from your MCP client over stdio, in a Docker or Podman container, pinned to a single VDOM at startup.
What This Server Does
FortiGate Read-Only exposes a curated set of GET-only
tools that wrap the FortiOS REST API v2. It's designed to be safe in production:
no tool will ever issue a POST, PUT, PATCH,
or DELETE against the firewall.
The server is pinned to a single VDOM at startup via the FORTIGATE_VDOM
environment variable. Tools take no vdom parameter at all, and the
shared request helper strips any caller-supplied vdom, there is no
surface for the model to widen scope to a virtual domain the token can also reach.
Ships as both a Docker and a Podman container
from the same server.py, pick whichever runtime you already run.
What to Ask, in Plain English
A handful of real questions you can put to your AI client once this server is wired in. The model figures out which tools to call, you describe the outcome.
- "Which firewall policies have NAT enabled but logging disabled?"
- "Are any IPsec tunnels currently down, for how long?"
- "How many active sessions are there from source 10.0.0.5 right now?"
- "Show me policies 1 through 10 with their UTM profiles and hit counts."
- "What admin accounts exist, and what access profile does each have?"
- "Pull the last 25 system event logs and summarize what failed."
37 Read-Only Tools Across the FortiOS Surface
cmdb table or monitor resource for endpoints not covered by a named tool.Build & Configure
Two supported runtimes, pick the one you already run. The docker/ and
podman/ backends ship an identical server.py; only the
container file and the way the client is wired differ. Generate the REST API token
first (it's identical for either path), then expand your runtime below.
Step 0, Generate a REST API token (out of band). On the FortiGate, go to System → Administrators → Create New → REST API Admin. Assign a read-only access profile, set a Trusthost that matches where the container will run, and pin the admin to a single VDOM. Copy the token shown once at creation, FortiOS does not display it again.
Docker Docker Desktop · MCP Gateway ›
Clone the repository. The Docker backend lives in docker/ and ships a custom-catalog.yaml alongside the Dockerfile.
$ git clone https://github.com/rosarion97/Fortigate-mcp-server-public.git $ cd Fortigate-mcp-server-public/docker
The image tag must match the image: field in custom-catalog.yaml (fortigate-readonly-mcp:latest).
$ docker build -t fortigate-readonly-mcp:latest .
Three values are required: the API token, the FortiGate host, and the VDOM you want this instance to serve. Option A keeps your token in Docker's encrypted store and is recommended; Option B writes a plaintext .env for quick local testing.
$ docker mcp secret set FORTIGATE_API_TOKEN="..." $ docker mcp secret set FORTIGATE_HOST="fw01.example.com" $ docker mcp secret set FORTIGATE_VDOM="root" $ docker mcp secret set FORTIGATE_VERIFY_SSL="yes" # optional $ docker mcp secret ls # values are masked
With Option A, continue through Steps 4–7 below.
chmod 600 .env, never commit it. This path
skips the gateway: skip Steps 4–6, point your client straight at
docker run --env-file (below), then jump to Step 7.
$ cp .env.example .env $ chmod 600 .env # then set FORTIGATE_API_TOKEN, FORTIGATE_HOST, FORTIGATE_VDOM
{
"mcpServers": {
"fortigate-readonly": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"--env-file", "/absolute/path/to/.env",
"fortigate-readonly-mcp:latest"
]
}
}
}
Steps 4–6 are the Option A (secret store) path. Used Option B? Skip to Step 7.
$ mkdir -p ~/.docker/mcp/catalogs $ cp custom-catalog.yaml ~/.docker/mcp/catalogs/custom.yaml
Add the entry under the single top-level registry: key in ~/.docker/mcp/registry.yaml. Don't overwrite the file if it already exists.
registry:
fortigate-readonly:
catalog: custom
enabled: true
Add the gateway block to claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json). The gateway spawns the FortiGate image on demand, reads your catalog and registry, and resolves your FORTIGATE_* secrets at request time. Replace <your-username> with your macOS username (run whoami to check):
{
"mcpServers": {
"mcp-toolkit-gateway": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-v", "/var/run/docker.sock:/var/run/docker.sock",
"-v", "/Users/<your-username>/.docker/mcp:/mcp",
"-v", "/Users/<your-username>/Library/Caches/docker-secrets-engine/engine.sock:/root/.cache/docker-secrets-engine/engine.sock",
"docker/mcp-gateway:latest",
"--catalog=/mcp/catalogs/custom.yaml",
"--registry=/mcp/registry.yaml",
"--transport=stdio"
]
}
}
}
All three bind-mounts are required: the Docker socket lets the gateway spawn the server container; ~/.docker/mcp is where it reads your catalog and registry; the docker-secrets-engine socket is the resolver Docker Desktop exposes for the secret store, without it your FORTIGATE_* URLs resolve to empty strings and the server never starts. On Linux Docker Desktop the path is ~/.docker/desktop/secrets-engine/engine.sock; find it with find ~ -name engine.sock.
claude mcp add -s user mcp-toolkit-gateway -- docker run …; Codex reads the same shape from ~/.codex/config.toml as [mcp_servers.mcp-toolkit-gateway]. Any MCP-capable client, Google's Gemini included, reads an mcpServers block from its own settings file; only the location changes, the entry is identical.
Then quit and reopen the client. claude_desktop_config.json never contains FORTIGATE_API_TOKEN, the gateway resolves it from Docker's secret store at request time. As a shortcut, docker mcp client connect claude-desktop (or MCP Toolkit → Clients in Docker Desktop) writes a similar block automatically; the explicit JSON above survives Docker Desktop updates that may rewrite the auto-managed entry.
$ docker mcp server list # fortigate-readonly → enabled $ docker mcp tools list
Or run the image directly, it starts and waits silently on stdin (correct for an MCP stdio server). Press Ctrl+C to exit; a missing required value fails fast on stderr.
$ docker run --rm -i --env-file .env fortigate-readonly-mcp:latest
Podman Rootless · stdio ›
Clone the repository. The Podman backend lives in podman/.
$ git clone https://github.com/rosarion97/Fortigate-mcp-server-public.git $ cd Fortigate-mcp-server-public
On macOS / Windows, start a Podman machine first (podman machine init && podman machine start); Linux runs natively.
$ podman build -t fortigate-readonly-mcp:latest podman/
Three required values: FORTIGATE_API_TOKEN, FORTIGATE_HOST, and FORTIGATE_VDOM (one container serves one VDOM, use root if VDOMs are disabled). Set them in podman/.env; the file is gitignored and chmod 600'd.
$ cp podman/.env.example podman/.env $ chmod 600 podman/.env # then edit: FORTIGATE_API_TOKEN, FORTIGATE_HOST, FORTIGATE_VDOM
If the FortiGate uses a self-signed cert, also set FORTIGATE_VERIFY_SSL=no. Optional: FORTIGATE_PORT (default 443) and FORTIGATE_MAX_RESPONSE_BYTES (default 120000).
Point Claude Desktop at the container with the absolute path to podman/.env:
{
"mcpServers": {
"fortigate": {
"command": "podman",
"args": [
"run", "--rm", "-i",
"--env-file", "/absolute/path/to/podman/.env",
"fortigate-readonly-mcp:latest"
]
}
}
}
mcpServers block from its own settings.json,
Google's Gemini included. Drop this same entry into that client's
settings.json and the FortiGate tools appear there too. Only the
file's name and location change; the server entry is identical.
Claude Code takes the same invocation:
$ claude mcp add -s user fortigate -- \
podman run --rm -i \
--env-file /absolute/path/to/podman/.env \
fortigate-readonly-mcp:latest
-i is required, MCP stdio needs stdin attached, and every --env-file path must be absolute.
Run the image manually, it starts and waits silently on stdin. Press Ctrl+C to exit; a missing required value fails fast on stderr.
$ podman run --rm -i --env-file podman/.env fortigate-readonly-mcp:latest
One container serves one VDOM, run a second container with its own credentials to serve another VDOM. --rm cleans up the container after each session.
Three Required, Three Optional
- FORTIGATE_API_TOKEN, required. Bearer token used for every API call. Generated under System → Administrators → REST API Admin, shown once at creation.
- FORTIGATE_HOST, required. Management host/IP of the firewall, the same address you'd reach the admin GUI on.
- FORTIGATE_VDOM, required. Pins this instance to a single virtual domain. Use
rootif VDOMs are disabled. - FORTIGATE_PORT, optional. HTTPS admin port. Default
443; set if the FortiGate uses a non-standard port. - FORTIGATE_VERIFY_SSL, optional. TLS verification. Default
yes; setnoonly for an un-validatable self-signed cert. - FORTIGATE_MAX_RESPONSE_BYTES, optional. Caps the JSON size of any one tool response. Default
120000(~30k tokens). Over the cap, the server returns a truncation envelope with a_hintfield.
Before You Start
- Docker Desktop 4.27+ with the MCP Toolkit feature, or Podman 4.x+ on
$PATH. Pick one; the step-by-step for each is above. - A FortiGate running FortiOS with the REST API enabled and reachable from the host the container runs on.
- A REST API admin scoped to read-only, with a Trusthost matching where this container runs and pinned to the VDOM you want to serve. Defense-in-depth on top of the protocol-layer read-only guarantee.
- An MCP-capable client, Claude Desktop, Claude Code, Codex, Gemini CLI, or any client speaking MCP over stdio.
Read-Only by Construction
GET
against the FortiOS REST API v2. The server contains no code path that issues a
write verb, so it is incapable of modifying any FortiGate configuration regardless
of what an LLM or user asks for. Pull requests that add additional read-only
endpoints are welcome; any change that introduces a write-capable verb against the
firewall will be rejected. Not affiliated with or endorsed by Fortinet.