A Ryze RoboMaster TT flown by AI agents — telemetry, flight, and camera spoken as plain English, routed through agentgateway as MCP, and kept live by a five-second heartbeat.
The bird is a Ryze RoboMaster TT (Tello) in station mode — it joins the lab Wi-Fi at a reserved address and speaks the Tello SDK over three UDP channels. Everything the agents do bottoms out in those datagrams.
Flight & query commands in, ACKs back.
takeoff · move · battery?Telemetry broadcast ~10 Hz while in SDK mode.
bat · height · attitudeH.264 stream for photos & recording.
take_photo · recordA request crosses four boundaries — model, gateway, MCP server, radio — and the telemetry comes back the same way. The gateway is the one control point: it prices, traces, and shapes every tool call.
Plain English in the kagent UI.
"do a flip"Reasons, picks a tool.
kagent · declarativeTool-mode proxy, tracing, budgets.
/drone · :31606Owns the UDP link · hostNetwork pod · 5s keepalive.
drone-system nsExecutes, streams telemetry back up the chain.
Tello SDKA real trip through the stack — "what's the battery?" — plus the heartbeat that runs the whole time in the background so the answer is never stale.
You type it (or tap an example). The agent, on gpt-5.5, decides get_battery is the right tool.
The call hits agentgateway at /drone, which routes it to the drone MCP server — and traces it to Langfuse.
The server sends battery? as a UDP datagram to the drone on port 8889 and waits for the reply.
The drone replies 76; it flows back up the same chain and the agent says it in plain English.
Every 5s the server re-sends command, keeping the drone in SDK mode so telemetry never stops.
get_state reports connected / stale / state_age_s — a dead link reads false, never a frozen value.
agentgateway can present the same 15 tools four different ways — from the full list to a single code sandbox — trading model context for directness. Each mode is its own agent and endpoint, all flying the same aircraft.
All 15 tools, exposed directly. The simplest path — the model sees everything.
:31606/droneget_state · takeoff · move · flip · take_photo …Two meta-tools. The model looks a tool up, then invokes it — keeps context small.
:31606/drone-searchget_tool · invoke_toolOne tool: the model writes JavaScript that calls the drone in a 15s sandbox.
:31606/drone-coderun_codeDiscover typed signatures, then write code against them. Smallest footprint of all.
:31606/drone-codesearchget_tool · run_codeEvery capability the drone MCP server exposes, grouped the way the aircraft is. The agent calls these by name (Standard mode) or discovers them at runtime (Search / Code). Plus two canned prompts — one-tap procedures that orchestrate several tools.
An agent isn't just a model with tools — it carries a skill (an operating runbook) and a brief that shape how it flies. Both live in Git, so every agent behaves the same across restarts.
A runbook the agent pulls from this repo (skills/drone) at runtime — not baked into the model. It carries the facts a pilot needs: the drone's IP and UDP ports, that takeoff is refused under 15% and <30% means "land soon", how to read stale telemetry, and to describe a photo rather than dump JSON. Edit the runbook, and every agent's behavior updates on the next pull.
Each of the four agents is the same recipe with one dial changed — the tool mode. The model, skill, and safety rules stay constant; only how the tools are presented differs. That's what makes them a clean side-by-side comparison.
The drone drops SDK mode after ~15s of silence and its Wi-Fi is unreliable. Four systems keep the picture honest and the aircraft safe.
A background thread re-sends command every 5 seconds, so telemetry streams non-stop — and a drone you just charged reconnects on its own within ~5s. No pod restart.
get_state stamps every packet. connected means "heard from it in the last 5s" — otherwise stale:true, so the agent never quotes a frozen number.
get_battery actively asks the drone (battery?) instead of trusting the cache — the reading you get is the reading right now.
Idempotent commands (battery, land, emergency, keepalive) retry through Wi-Fi blips. Movement never does — a timed-out move may have already run, and a retry would double it.
Everything is GitOps — edit, push, ArgoCD syncs. Here's the shape: the server on the drone LAN, a gateway endpoint per mode, and an agent that speaks to it.
# owns the drone's UDP link directly env: - name: TELLO_IP value: "172.16.10.168" # DHCP-reserved hostNetwork: true nodeSelector: kubernetes.io/hostname: talos-9kw-b68
spec: entMcp: toolMode: Code # Standard|Search|Code|CodeSearch codeMode: { timeout: "15s" } targets: - static: host: drone-mcp-server.drone-system… port: 8090 path: /mcp/
curl -s http://172.16.10.155:31606/drone \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize", "params":{"protocolVersion":"2025-06-18", "capabilities":{},"clientInfo":{"name":"cli","version":"0"}}}'
| Endpoint | URL | Exposes |
|---|---|---|
| Standard | 172.16.10.155:31606/drone | all 15 tools |
| Search | …/drone-search | get_tool · invoke_tool |
| Code | …/drone-code | run_code |
| CodeSearch | …/drone-codesearch | get_tool · run_code |