As a library¶
The MCP server is one consumer of annotools; your own agent can be another. Everything the tools do is
a plain function, so a pipeline built on the Claude Agent SDK or the Codex SDK can give its execution
agent the same eyes without running an MCP server at all. Importing annotools loads neither
fastmcp nor the annotools.mcp package.
annotools.__all__ is the public API; the API reference documents every name in it.
The shape of a call¶
Preview first, overlay second, encode last. The preview carries the metadata everything else needs.
from annotools import BBoxObject, draw_bboxes, encode, load_image, preview
image = load_image("photo.jpg")
result = preview(image, max_width=768, max_height=768)
overlay = draw_bboxes(result, [BBoxObject(bbox=(0.31, 0.44, 0.62, 0.78), label="cat")])
jpeg = encode(overlay.image, "jpeg")
result.metadata holds original_size, the applied crop, output_size and scale; overlay
adds objects. Boxes are in source coordinates, so the same call draws them correctly on a full view
and on a zoomed crop.
Zooming without losing the frame¶
zoom = preview(image, crop=(0.5, 0.5, 1.0, 1.0), max_width=768, max_height=768)
zoom.metadata["crop"] # the crop actually applied, rounded outward to whole source pixels
Pass that crop to normalize_coordinates and a model's answer about the zoom lands in the full
image.
Converting a model's answer¶
from annotools import normalize_coordinates
# Claude answered in pixels of the 768-px preview it saw.
boxes = normalize_coordinates(
[[240, 130, 470, 505]],
zoom.metadata["output_width"],
zoom.metadata["output_height"],
crop=zoom.metadata["crop"],
)
# Gemini answered [ymin, xmin, ymax, xmax] in a 0-1000 space.
boxes = normalize_coordinates([[130, 240, 505, 470]], 1000, 1000, axis_order="yx")
denormalize_coordinates is the inverse: use it to show a model its own stored
annotations in the frame it reasons in.
Defaults¶
Every size, width and color parameter falls back to annotools.Settings when you
pass None, resolved at call time:
from annotools import Settings, configure
configure(Settings(max_width=1024, max_height=1024, color="red"))
preview(image) # now 1024 px, overlays default to red
Because resolution happens per call, configure() affects everything that runs after it. The MCP
layer is the exception: it snapshots the settings when the server starts so its tool schemas can
advertise concrete numbers.
Video and audio¶
Both need annotools[media] (PyAV). Frames come back as PIL images, ready for the same preview
pipeline:
from annotools import clip_audio, preview, sample_frames
frames, meta = sample_frames("clip.mp4", fps=1, end=10, max_frames=8)
previews = [preview(frame, max_width=768, max_height=768) for _, frame in frames]
wav, audio_meta = clip_audio("talk.wav", start=30, end=45, sample_rate=16000)
Building agent tools on top¶
The agent-vision-tools
skill packages this into the tools an execution agent actually needs: look_at_item and
look_at_annotations to see, writer tools to record, and the workspace confinement that keeps the
agent inside its dataset. The four
example projects are complete working
versions of it.