A firewall management tool server is exactly the kind of tool where every person should act as themselves. Each administrator’s FortiOS REST API token carries their own admin profile, their own trusted hosts and their own entry in the firewall’s log. A single shared token would erase all three. This article shows how an administrator connects a FortiGate tool server once, and how each person then brings their own token and settings through My variables, without ever typing a token into a chat.
It is written from a real FortiGate tool server we examined on 27 September 2026: 240 tools, every one of them taking the firewall address, the VDOM and the API token as tool arguments. That design is the first thing to change, and the next section explains why.
Who does what
| Step | Who | Where |
|---|---|---|
| Make the tool server read the token from the request, not from tool arguments | The server’s developer | The tool server’s code |
| Register the server, declare its variables, choose who may use it | A console administrator | /mcp |
| Create a FortiOS API token and save it with their settings | Each person | FortiGate, then My variables (/vault) |
| Ask the chat about their firewalls | Each person | The chat workspace, the editor extension, their agents |
People cannot register servers or declare variables themselves. The section Why people cannot set this up themselves explains the reasons.
Before you start: the token must not be a tool argument
Open the server’s row on /mcp and look at its tools’ parameters. If a tool takes
something like api_token, password or secret as an argument, stop here.
A tool argument is written by the model. For the model to fill in api_token, the
token has to be in the conversation, so the person pastes it into the chat. From
there it is:
- in the model’s context, and so in front of the model provider;
- in the audit trail, because tool arguments are recorded in full;
- scanned and stored like any other prompt text.
On the server we examined, a token of 16 or more characters sat in the stored arguments of 4 of its 12 recorded calls. The vault cannot help here: it fills request headers or a sealed per-person bundle, and it never writes a tool argument. If your server looks like this, rotate any token that was ever typed into a chat.
The same applies, in a milder form, to a host argument. A model that can choose the address can be talked into pointing the tool at an address nobody intended.
How a server can receive a person’s own values
The vault has three delivery paths. Which one fits depends on how the server is built.
| The server | Path | Status |
|---|---|---|
| Is a remote HTTP tool server you can change, or a third-party service that reads a header | Header: the value lands in a header the administrator names, on that person’s calls only | Live |
| Is a server you run and can extend | Sealed bundle: all of the person’s values in one encrypted header that only this server can open | Live |
| Is a stdio package that reads its token from environment variables at start-up | Environment bridge: one process per person, started with that person’s values | Live, public HTTPS only (see below) |
For a FortiGate tool server that speaks HTTP, headers are the simplest path, and the rest of this article uses them.
Part A: the administrator
A1. Have the server read the request, not the arguments
Ask the server’s developer for one change: take the FortiOS token, and optionally the target device and VDOM, from the request headers of each call, and remove those parameters from every tool’s input schema. Here is a sketch of the idea using the Python MCP SDK; adapt it to whatever framework the server uses:
from mcp.server.fastmcp import FastMCP, Context
mcp = FastMCP("fortigate")
DEVICES = { # the firewalls this server may reach, chosen by its operator
"hq-fw": "https://203.0.113.10",
"branch-fw": "https://203.0.113.20",
}
def fortios_target(ctx: Context):
h = ctx.request_context.request.headers
token = h.get("authorization", "").removeprefix("Bearer ").strip()
if not token:
raise ValueError("No FortiOS token: set FORTIGATE_API_TOKEN on My variables.")
device = h.get("x-fortigate-device", "hq-fw")
if device not in DEVICES:
raise ValueError(f"Unknown device {device!r}. Known: {', '.join(DEVICES)}")
vdom = h.get("x-fortigate-vdom") or "root"
return DEVICES[device], token, vdom
@mcp.tool()
async def system_status(ctx: Context) -> dict:
"""FortiOS system status: firmware, hostname, serial, uptime, HA state."""
base, token, vdom = fortios_target(ctx)
... # GET {base}/api/v2/monitor/system/status?vdom={vdom}
# with the header Authorization: Bearer {token}
Two design choices in that sketch are deliberate:
- A device name, not an address. The person picks
hq-fwfrom a list the server’s operator controls. Nobody, person or model, can point the server at an arbitrary address. It also avoids a vault rule that refuses private addresses inurl-format values, since most firewall management addresses are private. - The server’s own access key on a different header. The tool server itself should
still require a key of its own, so that only the gateway can reach it. Put that
key on a header such as
X-Api-Key, not onAuthorization. A person’sAuthorizationvalue replaces a server-levelAuthorizationheader of the same name, so the two cannot share one header.
A2. Register the server
On /mcp, Register a server:
| Field | Value |
|---|---|
| Name | FortiGate |
| URL | the server’s streamable HTTP endpoint, for example https://fortigate-mcp.corp.example/mcp |
| Auth | custom header, header X-Api-Key, with the server’s own access key as Secret |
| Identity | require user (signed assertion) ticked |
Press Register & sync. Discovery uses the server’s own access key, so the tool list loads even before anyone has saved a FortiOS token.
A3. Declare the variables
On the Variables tab of /mcp, declare one variable per value a person brings.
The first is the one that matters; the other two are the optional settings this
design uses. The header names are illustrative: use whatever your server reads.
| Variable | Delivery | Header | Template | Required | Secret | Format |
|---|---|---|---|---|---|---|
FORTIGATE_API_TOKEN |
header | Authorization |
Bearer ${value} |
yes | yes | string |
FORTIGATE_DEVICE |
header | X-FortiGate-Device |
${value} |
no | no | string |
FORTIGATE_VDOM |
header | X-FortiGate-VDOM |
${value} |
no | no | string |
Write a description for each, because people read it on their own page. For example: “Your FortiOS REST API token (read-only is enough). Create it under System → Administrators → REST API Admin.”
What the platform enforces for you:
- Names and headers are the administrator’s. A person supplies values only. They cannot choose a header, a template or a server.
- Transport headers cannot be targeted.
Host,Content-*,Cookie,Mcp-Session-Idand everyX-GpuPool-*header are refused as delivery headers. - A value may not contain a line break and is at most 8 KiB.
Requiredmeans nothing leaves without it. A person who has not saved a token gets a tool error with a direct link to their page, and the request never reaches the server.
Other settings a FortiGate server could reasonably accept per person, all illustrative:
| Variable | What it would do | Advice |
|---|---|---|
FORTIGATE_TIMEOUT_SECONDS |
Allow slow operations, such as a large policy export | Harmless; format number |
FORTIGATE_READ_ONLY |
Ask the server to refuse write tools for this person | Useful as a second guard; the token’s admin profile is the real one |
FORTIGATE_VERIFY_TLS |
Turn certificate checks off | Do not offer. A person must not be able to switch off the check that proves they are talking to their firewall |
FORTIGATE_HOST |
A free address instead of a device name | Avoid it, for the reasons in A1 |
A4. Choose who may use it
In the server’s Access column, choose granted and add the people who administer firewalls. Everyone else gets the same answer as for a server that does not exist. A server in granted mode is also kept out of the chat workspace’s shared tool list, because one shared connection cannot tell people apart. The editor extension and agents list it per person.
Part B: each person
B1. Create your own FortiOS REST API token
On the FortiGate, as an administrator with the right to create accounts:
- Go to System → Administrators → Create New → REST API Admin.
- Give it a name that says whose it is, for example
api-ayse. - Pick an admin profile. Read-only is enough to ask about status, sessions and policies. Grant write access only if you mean to make changes from a chat.
- Under Trusted Hosts, enter the address the tool server calls from. The token then works from nowhere else, even if it leaks.
- Press OK and copy the key. FortiOS shows it once.
B2. Save it on My variables
Sign in to the console, open My variables (/vault) and find FortiGate:
| Variable | What to enter |
|---|---|
FORTIGATE_API_TOKEN |
the key you just copied |
FORTIGATE_DEVICE |
the device name your administrator gave you, for example hq-fw (optional) |
FORTIGATE_VDOM |
your VDOM, for example root (optional) |
Press Save on each. The token row then shows set and a short fingerprint; the value is never shown again, not even to you. The device and VDOM are not secret, so they are shown back.
B3. Ask
In a chat with the FortiGate tools enabled, ask for example:
- What firmware and uptime does my firewall report?
- List the firewall policies that allow traffic from the guest VLAN.
- Which IPsec tunnels are down right now?
Each call carries your token to your device and VDOM. The firewall’s own log
records api-ayse, not a shared account.
What travels where, on one call
you ──chat──▶ model ──tool call, no token──▶ gateway
gateway ──X-Api-Key: <server key>
Authorization: Bearer <your FortiOS token>
X-FortiGate-Device: hq-fw
X-FortiGate-VDOM: root ──────────────▶ FortiGate tool server ──▶ your firewall
audit trail: tool, arguments, and the variable NAMES used. Never the token.
- The model writes the tool call, and there is no token in it to write.
- The gateway adds your values to that one call. It decrypts them for this call only; they are never cached, never in a log line and never in the model’s context.
- If the server’s answer ever echoes your token, the stored audit copy shows
[uservar:FORTIGATE_API_TOKEN]instead.
If the server only reads environment variables
Some FortiGate tool servers are command-line packages that read FORTIGATE_HOST and
FORTIGATE_API_TOKEN from their environment once, at start-up. For those, the
environment bridge runs one process per person, started with that person’s
values. The administrator lists the server in an instance file, which is illustrative
here:
{
"slug": "fortigate",
"name": "FortiGate",
"command": "fortigate-mcp",
"args": [],
"egress": "public-https",
"vars": [
{ "name": "FORTIGATE_API_TOKEN", "env": "FORTIGATE_API_TOKEN", "isSecret": true, "isRequired": true },
{ "name": "FORTIGATE_VDOM", "env": "FORTIGATE_VDOM", "isSecret": false }
]
}
Know one limit before choosing this path: today the bridge offers no network at all or public HTTPS only. A firewall whose management interface is on a private network is unreachable from it until a private-network egress option exists. For a firewall on the LAN, use the header path above.
Why people cannot set this up themselves
It is tempting to let each person register their own tool server and declare their own variables. Each of those steps is a decision about where the platform sends traffic and credentials, so they stay with administrators:
- Registering a server chooses a destination. The gateway would call any address a person typed, from inside the network, with the signed identity of whoever uses the tool. That is a path to internal services nobody meant to expose, and a way to collect other people’s identities if the server is shared.
- Declaring a variable chooses a header and a template. Letting the person who supplies the value also choose its header is how a value ends up overwriting another credential. This is a known class of gateway vulnerability, and the reason people here supply values only.
- Access and grants decide who may use a server. Self-service would mean granting yourself access.
- The environment bridge runs a command. Choosing that command is choosing what code runs next to everyone else’s processes.
What people do control is everything that is theirs: their own token, device and VDOM, set and replaced on their own page, never visible to anyone else.
When something is not right
| Situation | What to do |
|---|---|
| The tools ask you for a token in the chat | The server still takes the token as an argument (see Before you start). Do not paste it; tell your administrator. |
| This tool needs your own FORTIGATE_API_TOKEN | Save your token on My variables; the message links there. |
401 from the firewall |
The token was revoked, expired, or your address is not in its Trusted Hosts. Create a new one and Replace the value. |
403 on a change |
Your admin profile is read-only. That is the intended guard. |
| Unknown device | Use a device name your administrator lists. The server refuses anything else. |
| FortiGate is not on My variables | You have not been granted it yet. Ask your administrator. |
| Someone leaves | They, or an administrator on the Variables tab, delete their values. Also delete their REST API admin on the FortiGate. |
The walkthrough at the top of this page shows the same flow with a different tool, and the GitHub article uses the header path with a third-party service.