Skip to content

Troubleshooting

Work from the outside in. Most failures are the client not passing the environment, the appliance not trusting the source address, or the profile not granting a read — in roughly that order of frequency.

The server writes one line to stderr when it starts, and it answers the first question worth asking:

mcfortigate v2026.9.11 | targets: fgt.example.com

If that names your appliance, the environment reached the process. Everything still broken is between the server and the firewall.

If it says targets: none configured, the environment did not reach the process, and nothing about the FortiGate is relevant yet.

Claude Code surfaces this in its MCP logs. Other clients vary; if yours hides server output, run the command by hand to see it:

Terminal window
FORTIGATE_HOST=fgt.example.com FORTIGATE_TOKEN=your-token uvx mcfortigate

It will sit there waiting for stdio, which is correct. Ctrl-C out.

uvx is not on the client’s PATH. MCP clients often launch with a minimal environment that is not your login shell’s. which uvx in your terminal proving it exists proves nothing about what the client sees. Use the absolute path in the client configuration:

Terminal window
claude mcp add fortigate \
--env FORTIGATE_HOST=fgt.example.com \
--env FORTIGATE_TOKEN=your-token \
-- /home/you/.local/bin/uvx mcfortigate

The package could not be fetched. uvx downloads on first run, so a machine with no route to PyPI fails at startup. uvx mcfortigate --help in a terminal tells you which it is.

The environment variables did not arrive.

For claude mcp add, each variable needs its own --env flag and they must come before the -- separator. Anything after -- is the command, not configuration.

For a JSON client configuration, the variables go in an env object inside the server’s entry, not at the top level of the file.

For FORTIGATE_TARGETS, malformed JSON is a startup error naming the parse failure rather than an empty list — so if you are seeing none configured with FORTIGATE_TARGETS set, the variable itself did not arrive. The quoting rules are where this usually goes wrong.

Two causes, and they are indistinguishable from the response.

The token is wrong. API tokens are shown once at creation and cannot be retrieved afterwards. If you are not certain of it, generate a new one: execute api-user generate-key <name>.

The source address is not in the trusted hosts list. This is the more common one, and it moves: a laptop on a different network, a container with a different address than the host, a machine that got a new DHCP lease.

Check what the appliance actually sees:

diagnose debug enable
diagnose debug application httpsd -1

Then make a request and watch for the source address in the output. Turn it off afterwards with diagnose debug disable.

Confirm the credential independently of everything else:

Terminal window
curl -sk "https://fgt.example.com/api/v2/cmdb/system/global?access_token=YOUR_TOKEN" | head -c 200

Success there and failure through the server means the server is not sending what you think it is. Failure in both means the credential or the trusted host.

HTTP 403, or an empty result where there should be data

Section titled “HTTP 403, or an empty result where there should be data”

The API admin’s profile does not grant read on that part of the tree. A profile granting System but not Firewall answers get_system_status and returns nothing useful for policies, which looks like an empty firewall rather than a permissions problem.

Grant read on System, Network, Firewall, and — if the box has radios — WiFi & Switch Controller. The profile in full.

A FortiGate with the factory self-signed certificate fails verification, which is correct behaviour and not a bug.

For a lab appliance, turn verification off for that target:

Terminal window
FORTIGATE_VERIFY_SSL=false

or per-target in the JSON: {"host": "...", "token": "...", "verify_ssl": false}.

For anything carrying production traffic, install a real certificate on the management interface instead. The variable accepts 1, true, yes, and on as true and treats everything else as false, so a typo turns verification off rather than on — check the value if you expected verification and are not getting certificate errors.

If the appliance has no radios, monitor/wifi/client does not exist, and an absent endpoint yields no rows rather than an error. Empty is the correct answer on a wired-only box.

If it does have radios and the list is still empty, check that the profile grants read on WiFi & Switch Controller, and that you are querying the VDOM the radios live in.

With several targets configured, omitting target is an error rather than a guess — so a wrong-appliance answer means the wrong alias was passed.

list_targets is the fastest way to see what the server actually has. It makes no network calls, so it answers instantly and answers even when everything is unreachable.

If two aliases share a prefix, that is usually the cause. Choosing aliases.

The default is 30 seconds per request. A list_policies call against a very large ruleset, or any call across a slow WAN link to a branch appliance, can exceed it.

Terminal window
FORTIGATE_TIMEOUT=60

or per-target in the JSON. Before raising it much further, check whether the question can be narrowed — a filtered list_policies transfers a fraction of what an unfiltered one does, and search_config with include_policies: false drops most of its payload.

Check the VDOM. Every call is scoped to one, root by default, and an appliance with multiple VDOMs will cheerfully answer about the wrong one. The VDOM in use is reported by list_targets and by get_system_status.

Check the firmware. The version-dependent field shapes are handled for the versions they were observed on, and a version nobody has tested may have its own. get_system_status reports it; an unexpected shape is worth an issue with the raw response attached.