Querying several appliances
One server can reach any number of FortiGates. Set FORTIGATE_TARGETS to a
JSON object and the keys become the target argument on every tool.
FORTIGATE_TARGETS='{ "edge": {"host": "fgt-edge1.example.com", "token": "..."}, "branch": {"host": "fgt-br2.example.com", "token": "...", "verify_ssl": false}, "lab": {"host": "https://fgt.lab:8443", "username": "readonly", "password": "..."}}'That is three appliances, each with its own credential, one of them on a non-standard port with legacy auth and one with certificate verification off. Per-target settings are independent; there is no shared default to fight with.
Choosing aliases
Section titled “Choosing aliases”The aliases are not cosmetic. They are what the model types, what it reports back to you, and what it has to get right when you say check the branch firewall.
Use what people in the room already call the boxes. edge, branch, dc,
lab work. Serial numbers and inventory codes work badly, because a model
asked about the Boise firewall cannot map that to FGT60F-0042 without being
told, and it will guess.
Keep them short. They appear in every tool call and every response.
Keep them unambiguous by prefix. branch1 and branch2 are fine; branch and
branch-backup invite a model to pick the wrong one when you say branch.
The quoting
Section titled “The quoting”This is the part that trips everyone once. The value is a JSON object inside a shell environment variable, so the outer quoting has to survive the shell and the inner quoting has to survive JSON.
In a .env file or a shell, wrap the whole thing in single quotes and use
double quotes inside, as above. Do not swap them: JSON requires double quotes
around its keys and string values, and single quotes there are a parse error.
In a JSON client config the object has to be a string, which means escaping:
{ "mcpServers": { "fortigate": { "command": "uvx", "args": ["mcfortigate"], "env": { "FORTIGATE_TARGETS": "{\"edge\":{\"host\":\"fgt-edge1.example.com\",\"token\":\"...\"},\"branch\":{\"host\":\"fgt-br2.example.com\",\"token\":\"...\"}}" } } }}Unreadable, and easy to get wrong. Setting the variable in the environment and leaving it out of the client config entirely is the better move whenever the client inherits your shell.
Malformed JSON is a startup error naming the parse failure, so you find out immediately rather than through an empty target list.
Growing from one to two
Section titled “Growing from one to two”The two configuration styles coexist, and FORTIGATE_HOST is read after
FORTIGATE_TARGETS. A working single-appliance setup can therefore grow a
second appliance without being restructured — add FORTIGATE_TARGETS with just
the new box in it and leave the original variables alone.
The existing appliance keeps whatever alias FORTIGATE_NAME gave it, or its
hostname if you never set one. That is the moment to set FORTIGATE_NAME,
because a hostname makes a poor alias and the model now has to choose between
two.
How the model handles the target argument
Section titled “How the model handles the target argument”With several appliances configured, omitting target is an error — and the
error lists the valid aliases:
Several FortiGates are configured, so 'target' is required.Choose one of: branch, edge, labThat is deliberate. A silent pick of the first alphabetically would produce a
confident, well-formatted, entirely wrong answer about branch when you meant
edge. An error naming the options lets the next call be correct, and in
practice the model recovers without you noticing it happened.
list_targets is how the model orients itself, and it makes no network calls —
it reports what the server was configured with. It therefore answers instantly,
and it answers even when every appliance is unreachable, which makes it the
right first call when something is wrong.
Asking across all of them
Section titled “Asking across all of them”Nothing in the protocol stops a model from calling the same tool once per target and comparing. In practice this works well for questions with small answers:
Check every firewall for a policy allowing inbound SSH from anywhere.
The model calls list_targets, then list_policies per appliance, and
compares.
What is worth understanding is that the model is doing the fan-out, not the server. There is no tool that queries every appliance at once, and that is a design decision rather than a gap — see below.
This is multi-target, not fleet management
Section titled “This is multi-target, not fleet management”The distinction matters when you are deciding whether to point this at ten appliances or a hundred.
There is no fan-out. Every tool answers about exactly one appliance. Check every firewall for X is N separate tool calls that the model issues and correlates itself. It works, and it is visible in the transcript, but nothing is parallelised on your behalf.
Sessions are per call, not pooled. Each tool call opens a fresh session to
its appliance and closes it afterwards. Nothing is cached between calls. That
is the right trade for a server that may sit idle for hours between questions —
a pooled connection to a firewall is a stale connection waiting to happen — but
it means the cost is linear and paid every time. A find_references call
measured at roughly 1.1 s against the lab appliance is roughly 11 s across ten
of them, in sequence.
The context fills up faster than the clock runs out. Three appliances with large rulesets is three large rulesets in one conversation. This is usually the binding constraint long before latency is.
So the practical ceiling is a handful of appliances you ask questions about, not an estate you inventory nightly. Five is comfortable. Fifty is the wrong shape of problem for this tool.
Making the fan-out cheaper
Section titled “Making the fan-out cheaper”Three things help, in rough order of effect.
Filter before you fan out. list_policies with service: "SSH" across
three appliances is fast; three unfiltered rulesets is slow and floods the
context. The same applies to search_config with include_policies: false
when only object definitions matter.
Read total_available, not just what came back. Every listing tool bounds
its result at 200 rows by default. Across several appliances it is easy to
compare pages rather than tables without noticing —
the paging contract is what tells you the
difference.
Ask orientation questions first. list_targets costs nothing, and
get_system_status per appliance is small. Establishing which boxes are on
which firmware before asking an expensive question often narrows it to one.
When to reach for the sibling instead
Section titled “When to reach for the sibling instead”If the question is really what is the state of the whole estate, on a schedule, this is the wrong tool and nautobot-ssot-fortinet is the right one. It syncs FortiGate configuration into Nautobot as scheduled Jobs, so the repeated whole-estate work happens once, into a database built for it — and then you ask Nautobot, which is designed to answer across many devices, instead of asking N appliances one at a time.
The two compose well. Nautobot for the estate-wide and the historical, mcfortigate for what is this specific box doing right now, which is the question a sync cannot answer because its answer changes between syncs.