Skip to main content

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:

  1. 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_…).
  2. Confirm — the agent shows you the summary and asks for your explicit approval. Only after you approve does it call confirm_action with 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_action at execution time.
  • One approval, one call — the token is sealed, not spent: a second confirm_action with the same token performs the action a second time. The agent is instructed to call it once per approval, and to treat a 409 device_busy raised 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

ToolWhat it changesScopeDanger note
reboot_deviceRestarts a dashcamdevices:writeRecording stops for 1–2 minutes; the device must be online
recall_sd_recordingUploads a recording off the SD card as a recall jobmedia:recallUses the vehicle's mobile data and the monthly recall allowance; 409 device_busy while another transfer runs
cancel_recall_jobStops an in-flight recallmedia:recallPartial upload is discarded and cannot be resumed; data already spent is not refunded
delete_sd_recordingDeletes one recording from the SD cardmedia:writePermanent — if it was never recalled, no copy survives anywhere
create_geofenceAdds a circular geofencegeofences:writeSaved empty — no vehicles assigned, all recording options off, so no alerts start yet
delete_geofenceRemoves a geofence and its alertinggeofences:writeGone 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.

"Create a geofence" does not switch anything on

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.

One tool with side effects runs outside the gate

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: true on the 20 read tools — the tool never modifies anything, so clients can run it without ceremony. play_sd_recording is deliberately readOnlyHint: false (see above).
  • destructiveHint: true on 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.