Write actions & the approval gate
The single largest objection to giving an AI agent access to operations is "you gave a language model write access to my vehicles". Fleeta MCP answers it structurally, not with a policy: a write tool call does not write.
The two-step flow
Every mutating action goes through the same gate:
- Propose — the agent calls the write tool (for example
reboot_device). Nothing happens to your fleet yet. The server returns a human-readable summary of the action, a danger note where one applies, and a confirmation token (mcp_cf_…). - Confirm — the agent shows you the summary and asks for your explicit
approval. Only after you approve does it call
confirm_actionwith the token, which performs the actual API call and returns the receipt.
you › Reboot the dashcam on Truck 114.
agent › reboot_device(psn="7XBPK0BE00000001")
server ← { status: "confirmation_required",
action: "reboot_device",
summary: "Remotely reboot dashcam 7XBPK0BE00000001",
danger: "Recording stops for 1–2 minutes during the reboot. Works
only while the device is online (409 otherwise).",
confirmationToken: "mcp_cf_…", expiresInSec: 300,
instruction: "Nothing has been executed yet. Show the summary and
danger above to the user verbatim, …" }
agent › "This will reboot Truck 114's dashcam; it stops recording for one to
two minutes. Go ahead?"
you › Yes.
agent › confirm_action(confirmationToken="mcp_cf_…")
server ← { status: "executed",
action: "reboot_device",
summary: "Remotely reboot dashcam 7XBPK0BE00000001",
result: { … the API response … } }
summary comes back a second time on purpose: it is the same sentence you
approved, so the agent reports what actually ran rather than what it meant to
run. result is the API response for that action — the created geofence, the
recall job with its jobId, or null where the endpoint returns no body.
A refusal has the same shape with status: "rejected" and a reason
(not a token, expired, tampered with, issued to a different key, unknown
action, or a scope this connection was not granted). Nothing ran in that case.
Cancel at step 2 and the request dies there — the token is simply never used and expires on its own.
Why the gate cannot be bypassed
Confirmation tokens are designed so the second step is real, not decorative:
- Self-contained and tamper-proof — the proposed action is sealed inside the token with AES-256-GCM. The server is stateless; there is no pending queue to edit. Tampered or forged tokens are rejected.
- Short-lived — tokens expire after 5 minutes. An expired token is rejected and the action must be proposed again.
- Key-bound — a token can only be confirmed by the same API key it was issued to. A token presented with a different key is rejected.
- Scope-checked twice — the scope is checked when the action is proposed
and again inside
confirm_actionat execution time. - One approval, one call — the token is sealed, not spent: a second
confirm_actionwith the same token performs the action a second time. The agent is instructed to call it once per approval, and to treat a409 device_busyraised mid-execution as "retry the same token in a few seconds", not as a fresh decision to put to you.
The gate is enforced server-side. It applies identically over OAuth and direct Bearer connections, and no client configuration can turn it off.
The six write tools
| Tool | What it changes | Scope | Danger note |
|---|---|---|---|
reboot_device | Restarts a dashcam | devices:write | Recording stops for 1–2 minutes; the device must be online |
recall_sd_recording | Uploads a recording off the SD card as a recall job | media:recall | Uses the vehicle's mobile data and the monthly recall allowance; 409 device_busy while another transfer runs |
cancel_recall_job | Stops an in-flight recall | media:recall | Partial upload is discarded and cannot be resumed; data already spent is not refunded |
delete_sd_recording | Deletes one recording from the SD card | media:write | Permanent — if it was never recalled, no copy survives anywhere |
create_geofence | Adds a circular geofence | geofences:write | Saved empty — no vehicles assigned, all recording options off, so no alerts start yet |
delete_geofence | Removes a geofence and its alerting | geofences:write | Gone for the whole organization and unrecoverable; recorded alerts stay, future ones stop |
Every one of them stops at the gate. There is no per-tool setting that exempts a write, and reads never need approval — there is nothing to approve.
The danger note on create_geofence is the honest one: the API stores a new
zone with devices: [] and every recordingEvents flag false when the
request omits them, which is all create_geofence can send. So the zone
exists, appears in list_geofences — and raises no alerts and records nothing
until vehicles are assigned and the enter/exit events are switched on. That
step is not available over MCP (web viewer, or PUT /v1/geofences/{geofenceId}).
Expect the agent to say so when it asks for your approval.
play_sd_recording returns a playable link for a recording still on the SD
card in a single step — it calls GET /v1/devices/{psn}/sd-files/{filename}/video,
which makes the dashcam upload the clip over its own mobile data and
counts toward the recall allowance. It is not gated because it only ever
requests the low-resolution sub-stream (a few MB, seconds to upload — the
tool has no quality input, so it cannot start a full-resolution upload) and a
second approval would make "let me watch that clip" a three-step exchange.
It is not a read either: it carries readOnlyHint: false so hosts do not
auto-approve it as a lookup, its description leads with the cost, and the
agent is told to use recall_sd_recording — behind the gate — when the user
wants full resolution. The recall job it creates (jobId) can be watched
and cancelled like any other.
Tool annotations
Every tool declares standard MCP annotations so clients can reason about it before calling:
readOnlyHint: trueon the 20 read tools — the tool never modifies anything, so clients can run it without ceremony.play_sd_recordingis deliberatelyreadOnlyHint: false(see above).destructiveHint: trueon destructive writes (delete_sd_recording,delete_geofence) — MCP hosts that honor annotations may show their own confirmation prompt before even sending the call.
Annotations are hints for the client UI; they complement — never replace — the
server-side gate. A host that ignores annotations still cannot skip
confirm_action.
Narrowing what an agent may propose
Two ways to keep an agent from proposing writes at all:
- OAuth consent (ChatGPT, Claude OAuth) — the consent screen lists the
key's scopes as checkboxes. The four write scopes (
devices:write,geofences:write,media:recall,media:write) are unchecked by default; leave them unchecked and every write tool fails before it can propose anything. - Scoped keys (Bearer header connections) — issue the key with read scopes only in Management › Open API. A header connection carries the key's full permissions, so scope the key itself down.
See Authentication & security for the details.