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.