Skip to content

mcfortigate

Ask a FortiGate a question in plain language and get an answer back. Nothing here can change a configuration.

The obvious way to build this is one tool per FortiOS endpoint returning raw JSON, and that turns out to work badly. A single FortiOS policy object carries more than eighty fields, most of them empty strings, unused IPv6 arrays, and internal UUIDs. Ask a model which policies allow traffic into the DMZ against a raw wrapper and it spends most of its attention on "srcaddr6": [] before it finds anything useful.

So the tools here are shaped around the questions rather than around the endpoints. find_references answers whether an object is safe to delete by asking the appliance’s own reference lookup — the one the web UI uses, which knows all 234 tables that can reference an interface rather than the five a hand-rolled scan would check. find_device merges the wireless client list, the DHCP lease table, and the ARP table into a single record per device, which requires canonicalizing the MAC first, on both case and punctuation, or one device indexes as two and reports as unknown. That work happens once, here, instead of being rediscovered by every model that asks.

The long version of the argument is worth reading before you decide how much of your own tooling to shape this way.

Terminal
printf 'FortiGate API token: '; read -rs FORTIGATE_TOKEN; echo
claude mcp add fortigate \
--env FORTIGATE_HOST=fgt.example.com \
--env FORTIGATE_TOKEN="$FORTIGATE_TOKEN" \
-- uvx mcfortigate

That is the whole setup, once you have a read-only REST API admin on the appliance. Getting started walks through creating one.

The token goes through a variable rather than onto the command line, so shell history records "$FORTIGATE_TOKEN" instead of the credential itself.

Read-only, structurally

Every tool issues GET and nothing else, and says so through the MCP readOnlyHint annotation. There is no create, update, or delete path in the codebase to disable, misconfigure, or talk a model into reaching. How that is enforced.

Configuration and live state are different

A DHCP-assigned default route is in the live routing table and will never be in the static route configuration. The tools keep the two apart on purpose. Why it matters.

FortiOS quirks, already handled

Booleans are the strings "enable" and "disable", so bool() reads every disabled thing as enabled. That one mistake once flipped every route to blackhole. The full catalogue.

Several appliances, short aliases

Set FORTIGATE_TARGETS and the keys become the target argument on every tool. Omit it when only one is configured. Multi-target rather than fleet management — a handful you ask about, not an estate you inventory. Where that line falls.

This server cannot, by construction. Its sibling can.

nautobot-ssot-fortinet syncs FortiGate configuration in both directions against Nautobot — firewall objects, policies, NAT, wireless networks and radio profiles, plus a live-status job that joins wifi clients, DHCP leases, and the ARP table the same way find_device does here. Runs are idempotent, and destructive deletes are off unless you opt into them per job.

The two are worth understanding as a pair. Nautobot holds the state you intend; the sync reconciles the appliance to it. mcfortigate answers questions about what is actually there right now, and cannot alter the answer by asking — which is exactly what you want from the thing you check a change against.

They also share a lineage. A good half of the FortiOS behaviours handled here were learned in that project, against the same appliance, by writing to it and finding out afterwards what a misread field had done.

And if the rack has more than firewalls in it, mcidrac is this same idea pointed at Dell iDRAC over Redfish — question-shaped tools, validated against real hardware, for PowerEdge fleets.