Using the MCP Server
How an AI agent drives AprilCam over the Model Context Protocol — the golden path, the coordinate system, and the full tool reference.
Using the AprilCam MCP Server
The MCP server (aprilcam mcp) exposes AprilCam to AI agents over the
Model Context Protocol. You open cameras, build playfields, read tags and
objects, capture frames, draw paths and overlays, and look up playfield
features — all through tools, using opaque handles. You never need, and do
not have, filesystem access: camera_id and playfield_id are handles, not
paths.
The server is a thin client. All vision (AprilTag/ArUco detection, homography, deskew, object detection) runs in the daemon; the MCP server forwards perception results and raw frames. The live-camera flow needs no OpenCV in the MCP server’s environment.
Running it
The MCP server speaks stdio. Register it with your MCP client (e.g. an
.mcp.json):
{
"mcpServers": {
"aprilcam": { "command": "aprilcam", "args": ["mcp"] }
}
}
aprilcam init writes that block into .mcp.json and .vscode/mcp.json for
you. (A second console entry point, aprilcam-mcp, runs the same server.)
The MCP server is a thin client: it connects to a running daemon — it does
not start one. By default it finds the daemon the way every client does (a
local Unix socket, or mDNS on the LAN). To pin it to a specific daemon, register
it with a --daemon-host (or set APRILCAM_DAEMON_HOST):
{
"mcpServers": {
"aprilcam": {
"command": "aprilcam",
"args": ["mcp", "--daemon-host", "vidar.local"]
}
}
}
You can also switch daemons at runtime without restarting — call the
connect_daemon tool (below). If no daemon is reachable, tools return a clear
No aprilcam daemon found error; start one on the camera host
(aprilcam daemon start, or systemd) — see
Operating the Daemon.
Base install is enough — pipx install aprilcam ships the MCP SDK; OpenCV is not
required (see install tiers).
The server keeps no state across restarts and auto-opens nothing. Every session must call
open_camerafirst — it is the gate that registers the handle every downstream tool needs.
The golden path
1. open_camera(pattern="<name>") -> camera_id (+ playfield_id if configured)
2. (only if no playfield_id came back) create_playfield(camera_id)
3. stream_tags(playfield_id) -> start the detection loop
4. get_tags / get_objects / where / create_path using the playfield_id
5. stop_stream(playfield_id) -> when done
open_camerareturns aplayfield_idandplayfield_nameimmediately when the camera is already configured and calibrated — skip step 2. A"calibration_stale": trueflag means recalibration is needed.- Pass the
playfield_idas thesource_idtostream_tags/get_tags/get_objects/whereso thatworld_xypopulates. Passing thecamera_idalso works — the server resolves the camera’s playfield. - Discover the field with
get_playfield()/list_playfields(); search it withwhere(). Callget_robot_api_guide()for the high-rate Python API.
Camera numbers are the persistent enumeration handle
list_cameras returns each camera’s persistent enumeration number — a
stable handle that does not change when cameras are plugged/unplugged — as
index. open_camera(index=N) accepts that same number (it is resolved to the
live OS device index via the daemon). Prefer pattern= (a name substring) when
you can; it is unambiguous.
Coordinate system
When the playfield is calibrated, every tag/object carries world_xy in
centimetres, A1-centred: the origin is AprilTag 1 (playfield centre),
+x is east, +y is north. Angles (orientation_yaw, heading_rad) are
radians, 0 = +x, counter-clockwise positive — so a tag’s forward direction is
(cos yaw, sin yaw). Parallax correction and the A1 origin shift are applied by
the daemon. get_objects reports object world_xy in the same A1-centred
frame as get_tags.
Image return format
Every tool that returns an image takes a format: "base64" (inline in the
response, default) or "file" (a JPEG is written to a temp file and its path
returned). Choose per request.
Tool reference
Version & connection
| Tool | Purpose |
|---|---|
get_version() |
Package version and the active daemon target. |
connect_daemon(host?, port?, local?) |
Switch the daemon connection (mDNS by default, or a host/port, or local=True for the Unix socket). Tears down all session state and reconnects. |
Cameras
| Tool | Purpose |
|---|---|
list_cameras() |
Available cameras, each with its persistent index (enum), name, slug. |
open_camera(index?, pattern?, source?, backend?) |
Open a camera (or source="screen"); returns camera_id, and playfield_id/playfield_name if configured. The required first call. |
close_camera(camera_id) |
Release a camera. |
set_camera_playfield(camera_id, playfield) |
Link a camera to a named playfield definition (then calibrate). |
Playfields
| Tool | Purpose |
|---|---|
list_playfields() |
Named playfield definitions known to the server. |
get_playfield(name?) |
The whole playfield map: dimensions, origin, every AprilTag/ArUco/rectangle/dot with world positions. |
get_playfield_info(playfield_id) |
Live state of a registered playfield: corners, calibrated, homography. |
create_playfield(camera_id, max_frames?) |
Build a playfield by detecting ArUco corners from the live feed.¹ |
calibrate_playfield(playfield_id, camera_height_cm?, …) |
Compute and store the homography from the linked playfield definition.¹ |
Detection, tags & objects
| Tool | Purpose |
|---|---|
stream_tags(source_id, …) |
Preferred — start a detection loop (records an ops pipeline). |
start_detection(source_id, …) |
Legacy equivalent of stream_tags. |
stop_stream(source_id) / stop_detection(source_id) |
Stop the loop (match the one you started with). |
get_tags(source_id) |
Latest detections: id, center_px, world_xy, orientation_yaw, velocity, in_playfield. |
get_tag_history(source_id, num_frames?) |
Recent frames from the ring buffer (up to 300). |
get_objects(source_id) |
Detected non-tag colored objects with world_xy, color, bbox. |
where(query, source_id?) |
Natural-language playfield lookup (e.g. “where is the red dot”); merges live detection when source_id is given. |
pixel_to_world(source_id, pixels) |
Convert arbitrary pixel coordinates to A1-centred cm. |
Frame capture
| Tool | Purpose |
|---|---|
capture_frame(camera_id, format?, quality?) |
One frame; a playfield source is deskewed. |
get_frame(source_id, format?, quality?) |
Raw frame, no processing. |
Live view, paths & overlays
| Tool | Purpose |
|---|---|
start_live_view(camera_id, …) / stop_live_view(view_id) |
Open/close the live visualization window (tag overlays, paths). |
create_path(playfield_id, waypoints_json, name?) |
Add a persistent waypoint path (world cm); rendered in the live view. |
list_paths(playfield_id) / delete_path(path_id) / clear_paths(playfield_id) |
Manage paths. |
set_live_overlay(camera_id, elements_json, ttl?) |
Push transient overlay drawables (arc/arrow/point/polyline/text/rect/polygon, world cm). |
clear_live_overlay(camera_id) |
Remove the overlay immediately. |
Frame registry & static images ¹
| Tool | Purpose |
|---|---|
create_frame · create_frame_from_image · process_frame · get_frame_image · save_frame · release_frame · list_frames |
Capture/load a frame into a registry and run deskew/detect_tags/detect_aruco on it. |
deskew_image(playfield_id, image_path, …) |
Warp a static image to top-down via a playfield’s homography. |
¹ OpenCV note. Most tools are cv2-free, but a few do pixel work inside the MCP server and therefore need OpenCV in the MCP server’s environment:
create_playfield(live corner detection),calibrate_playfield, and the frame-registry / static-image tools. For an already-calibrated camera the golden path never hits these —open_camerareturns theplayfield_iddirectly. Calibration is normally done once on the daemon host withaprilcam calibrate. Moving this residual pixel work into the daemon is tracked work; until then these tools raise a clear install-hint error on a base (no-OpenCV) install.
Robot hand-off
For a robot control loop that needs tags at 5–50 Hz or live overlay drawing,
don’t drive it through MCP at runtime — call get_robot_api_guide() and hand
the program the DaemonControl Python API, which talks
to the daemon directly over gRPC.