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.
printf'FortiGate API token: '; read-rsFORTIGATE_TOKEN; echo
claudemcpaddfortigate\
--env FORTIGATE_HOST=fgt.example.com\
--env FORTIGATE_TOKEN="$FORTIGATE_TOKEN"\
-- uvxmcfortigate
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.