obsbot-mcp
MCP serverDev toolsLets your agent control an OBSBOT Tiny 2 camera: pan, tilt, zoom, tracking, focus, exposure, snapshots, and recording.
Unavailable. This server has no hosted endpoint yet, so Ahel can't serve it.
Add to setup to save this item as a reference. Ahel cannot run it, and signing in will not install it.
About this server
Control an OBSBOT Tiny 2 camera: pan/tilt/zoom, AI tracking, focus, exposure, snapshots, recording
Getting started
- Save this item in Your setup as a reference.
- Read the source or reference documentation for its setup requirements. Saving it here does not connect it to your AI.
- Check this page for availability before trying to install it through Ahel.
From the project's README
As published by lxman/obsbot-mcp in README.md.
A cross-platform Model Context Protocol server that controls an OBSBOT Tiny 2 camera over its standard UVC/USB interface — pan/tilt/roll the gimbal, zoom, AI subject tracking, focus/exposure/white-balance/image controls, HDR and field-of-view, plus snapshot, preview, and recording — without any vendor SDK. It also controls the OBSBOT Tail 2 network camera over its HTTP/WS API (see OBSBOT Tail 2 below).
Install
npm install obsbot-mcp
MCP client configuration
Add a stdio server entry pointing at the installed binary (or directly at dist/index.js):
{
"mcpServers": {
"obsbot": {
"command": "obsbot-mcp"
}
}
}
If you're running from a local checkout instead of an npm install, point command/args at
node and the built entry point instead:
{
"mcpServers": {
"obsbot": {
"command": "node",
"args": ["path/to/obsbot-mcp/dist/index.js"]
}
}
}
Debug / diagnostics tools
By default the server advertises only the normal control surface. Pass --debug to additionally
expose the diagnostics surface — the obsbot_debug_probe tool (raw XU byte get/set/query) and the
raw 60-byte status block on obsbot_status:
{
"mcpServers": {
"obsbot": {
"command": "node",
"args": ["path/to/obsbot-mcp/dist/index.js", "--debug"]
}
}
}
With the installed binary, use "command": "obsbot-mcp" and "args": ["--debug"].
Tools
35 tools on Windows and macOS, 34 on Linux (obsbot_gimbal_move_speed is unavailable there — see
limitations). --debug adds obsbot_debug_probe for
one more. Every Tail 2 camera on the network adds the 32 obsbot_tail2_* tools in
OBSBOT Tail 2 below (all platforms — they are pure TypeScript).
All names below are current as of v0.4.0 — every tool was renamed in this
release and there is no backward-compatible alias; see CHANGELOG.md for the
full old→new mapping if you're updating a caller.
The camera selector
Every camera-addressing tool accepts an optional camera parameter: the target camera's serial
number. Omit it with a single camera attached and nothing changes — this matches the server's
pre-v0.4.0, single-camera behaviour exactly. With more than one camera attached, a call that omits
camera fails with an error naming every attached serial, so you always know what to pass next.
Exempt (no camera parameter, ever): obsbot_devices (enumerates the whole fleet),
obsbot_capture_stop / obsbot_capture_list (address a sessionId, not a device), and
obsbot_debug_probe (operates on the current diagnostics transport). Two more tools honor it only
partially — see Capture below.
Multi-camera support is new in v0.4.0. It's exercised by the unit test suite against fakes; running two physical Tiny 2s at once has not yet been hardware-verified (see Known limitations).
obsbot_devices is the way to discover the serials you pass as camera: it reports each attached
camera's serial (where obtainable — reading it requires briefly opening the camera), name, and
status (available | bound | busy). A camera another process already holds comes back busy
with no serial, since it can't be opened to read one.
Device & power
| Tool | Parameters | Description |
|---|---|---|
obsbot_devices | — | List attached OBSBOT cameras with each one's serial (where obtainable), name, and status (available/bound/busy). A busy camera is held by another process. |
obsbot_wake | camera? | Wake the camera/gimbal (sends "run"). Moves the camera: un-stows the gimbal back to level (pitch ~0). Most control commands also wake it implicitly. |
obsbot_sleep | camera? | Sleep the camera/gimbal (sends "sleep"). Moves the camera: stows the gimbal face-down at roughly pitch 84, so obsbot_gimbal_position reads ~84 rather than the pose you left. |
obsbot_status | camera? | Read the live status block: { awake, hdr, faceAe, aiMode, trackSpeed, fovMode, zoomPercent, focusMode, focusPosition } (faceAe = auto-exposure metering for a detected face; fovMode = wide|medium|narrow|custom|unknown, where custom means a continuous zoom overrode the discrete modes; zoomPercent = zoom position, 0-100; focusMode = auto|manual|unknown). focusPosition appears only in manual mode, on the same 0-100 scale obsbot_focus_manual takes — under autofocus the camera echoes the last written value rather than exposing the motor, so a number there would read as a live focus distance while being stale. Focus is a standard UVC control, not part of the status block, so it costs one extra read; a device that can't answer reports focusMode: "unknown" rather than failing the whole call. Under --debug, also returns the raw 60-byte block as hex. |
Gimbal (PTZ)
| Tool | Parameters | Description |
|---|---|---|
obsbot_gimbal_move | yaw, pitch, roll (degrees, roll defaults 0), camera? | Move the gimbal to an absolute angle. Positive yaw pans to the camera's left, positive pitch tilts down. Yaw clamped to [-150, 150], pitch to [-90, 90]. Absolute 1:1 degrees, hardware-verified. |
obsbot_gimbal_move_speed | yaw, pitch, roll (deg/s, clamped to ±150, roll defaults 0), autoStopMs (default 800), camera? | Drive the gimbal at a speed, then auto-stop after autoStopMs so it can't run away. Same yaw/pitch sign convention as gimbal_move. Returns the speeds actually used. Past its limit the firmware ignores the command outright rather than saturating — 180 deg/s and above move the gimbal exactly 0° — so requests are clamped into the hardware-verified band. Not available on Linux — see limitations. |
obsbot_gimbal_recenter | camera? | Recenter the gimbal — drives it to yaw 0 / pitch 0. Returns as soon as the command is sent, so poll obsbot_gimbal_position if you need to know it arrived. |
obsbot_gimbal_position | camera? | Read the gimbal's current absolute { yaw, pitch } in degrees via standard UVC Pan/Tilt. Valid during a move as well as after one. On Linux this is the last-commanded value, not a live in-flight reading — see limitations. |
obsbot_aim_at_pixel | x, y, frameWidth, frameHeight, source (default "device"), camera? | Point the camera at a pixel from a frame you just captured. Reads the camera's magnification from its own reported state — a discrete FOV mode or a continuous zoom alike — so it needs no FOV or zoom argument and works at any zoom. Refuses while AI tracking is active, when the FOV mode can't be decoded, or when a corrupt zoom reading would resolve to an implausible magnification, and refuses if the camera had to be woken (waking moves the gimbal, invalidating the frame you measured) or if the zoom is still ramping (the frame was captured at a magnification the camera has already left, so wait for the zoom and take a fresh snapshot). source declares which feed the frame came from (default device) — it cannot be inferred from the pixels. A virtual/ndi frame is accepted, not refused, but only aims correctly if that feed is an unmodified pass-through of the camera; a compositor's rescale or letterbox is invisible in the picture and silently wrong here, so a non-device declaration returns a note stating that assumption. It reads the live pose to compute the aim, so on Linux it is affected the same way obsbot_gimbal_position is — see limitations. Returns clamped:true if the target was outside the gimbal's range, in which case the camera still moves — to the nearest reachable pose. Refuses (ok:false) instead of moving when the pixel lies past vertical from the current pose, since the only rotation that reaches it would swing the camera toward the opposite side of the room; tilt toward the pixel first, then re-aim. |
obsbot_zoom_to_fit | x, y, width, height, frameWidth, frameHeight, margin (default 0.1), source (default "device"), camera? | Frame a region of a frame you just captured: centre the gimbal on it and zoom so the region fills the frame. Same refusal conditions as obsbot_aim_at_pixel (AI tracking, undecodable FOV/zoom, a woken camera, a zoom still ramping, an over-the-top target, a non-16:9 frame), and the same source declaration, plus a refusal if the region isn't within the frame (edges included) or has non-positive size. margin backs the zoom off by that fraction so the region isn't framed edge-to-edge; the tighter of the region's two axes sets the zoom, so the whole region stays visible rather than being cropped on one side. Moves the gimbal before zooming — zoom is centre-preserving but not target-preserving, so zooming first can push the region out of frame. Zoom ramps rather than jumping, so the tool polls for up to 3s and returns settled:false (not an error) if the zoom hadn't arrived in time — check it before trusting a follow-up snapshot. |
Aiming at what you can see
obsbot_capture_snapshot returns the frame as an image plus its width/height, so a model can
locate something in the picture and then point the camera at it:
obsbot_capture_snapshot— look at the frameobsbot_aim_at_pixel— pass the target's pixel and that frame's dimensionsobsbot_capture_snapshotagain — confirm it landed, and repeat if needed
Pass the frameWidth/frameHeight from the same snapshot the pixel came from. Mixing a pixel from
one frame with dimensions from another aims at the wrong place, and nothing can detect it.
The snapshot must be source: "device". obsbot_capture_snapshot can also read from
source: "virtual" or "ndi", which come from OBSBOT Center's own output rather than the camera's
raw stream. Those are framed and cropped by OBSBOT Center, not by this camera's optics, so the
measured field-of-view constants this tool relies on don't describe them — aiming from a virtual or
NDI frame lands in the wrong place with no way to detect it. Only use a device-source snapshot's
pixel and dimensions here.
The tool reads the camera's magnification itself — a discrete FOV mode or a continuous zoom alike
(m = 3*ratio-2, measured on hardware to better than 0.05%) — so there is no FOV or zoom argument to
get wrong, and it works at any zoom. It refuses rather than guessing when AI tracking is on (tracking
drives the gimbal and would fight the aim), when the FOV mode can't be decoded, when a corrupt zoom
reading would resolve to an implausible magnification, or when the camera had to
be woken from sleep (waking moves the gimbal, so the frame you measured no longer matches where the
camera is pointing — take a fresh snapshot and retry).
Framing what you can see
obsbot_zoom_to_fit extends the same idea from a point to a region: instead of just centring on a
pixel, it also zooms so that region fills the frame.
obsbot_capture_snapshot— look at the frame- Pick a bounding box around whatever should fill the frame (a face, a whiteboard, ...)
obsbot_zoom_to_fit— pass the box (x,y,width,height) and that frame's dimensionsobsbot_capture_snapshotagain — confirm the framing, and repeat if needed
It shares obsbot_aim_at_pixel's refusals (AI tracking, undecodable FOV/zoom, a woken camera, a zoom still ramping, a
non-16:9 frame), and adds one of its own: the region must lie within the frame — edges included, so a
region that already IS the full frame is valid — with a positive width and height, or the call refuses
rather than guess what a negative width or an off-frame box was supposed to mean.
margin (default 0.1, i.e. 10%) backs the requested zoom off by that fraction so the region isn't
framed exactly edge-to-edge — some breathing room around it survives small aim/zoom error. The
region's two axes rarely need the same zoom to fill the frame; the tool always picks the smaller of
the two required magnifications, because zooming to the larger one would fill one axis by cropping the
other. The result is clamped to the camera's [1x, 4x] magnification range (reported via clamped) —
a region demanding more zoom than the camera has still gets the closest fit available, rather than
being refused outright.
The gimbal moves before the zoom is commanded. Zoom re-centres what's already in frame but does not keep a specific pixel under the crosshair as it changes — zooming first can push the region's centre out of frame entirely, which would make the subsequent move aim at a pixel that no longer means what it did when the caller measured it.
Zoom is not instantaneous: on this hardware it ramps toward the commanded value rather than jumping to
it, so a status read taken immediately after commanding it can catch it mid-transit (observed:
commanding ratio 1.5 read back partway there before settling). obsbot_zoom_to_fit polls for up to 3
seconds waiting for the zoom to arrive and returns settled:false — not an error — if it didn't. A
frame captured while the zoom is still moving is at an unknown magnification, so check settled
before trusting a follow-up snapshot; a false just means the camera was moving slower than expected,
not that anything failed.
Gimbal presets
Three on-device preset slots (1–3). Slots are create-once: obsbot_preset_save requires an
empty slot (delete first to reuse one); every other preset tool requires the slot to already be
occupied. Each tool re-reads the slot list after writing and returns a structured { ok:false }
failure if the device didn't land the change.
| Tool | Parameters | Description |
|---|---|---|
obsbot_preset_list | camera? | Read the three preset slots: occupied/empty, name, and pose in degrees. |
obsbot_preset_save | slot (1|2|3), camera? | Save the gimbal's current live pose into an empty slot. |
obsbot_preset_recall | slot (1|2|3), camera? | Recall an occupied slot, driving the gimbal to its saved pose. |
obsbot_preset_update | slot (1|2|3), camera? | Overwrite an occupied slot with the gimbal's current live pose. |
obsbot_preset_rename | slot (1|2|3), name, camera? | Rename an occupied slot (names over 40 bytes are truncated). |
obsbot_preset_delete | slot (1|2|3), camera? | Delete an occupied slot, freeing it for obsbot_preset_save. |
Zoom
Two tools, not one — they ride different transports (standard UVC vs. the vendor command frame)
and produce different physical zoom at the same commanded ratio, so merging them would silently
change what ratio means. Pick by which behaviour you need.
| Tool | Parameters | Description |
|---|---|---|
obsbot_zoom_uvc | ratio (1.0–2.0), camera? | Standard UVC zoom: set an absolute zoom ratio, clamped to [1.0, 2.0]. Snaps to the requested target exactly. Waits for the zoom to arrive and returns settled — the ramp is not instant (a full 1.0→2.0 sweep takes about 2.4s), and obsbot_aim_at_pixel / obsbot_zoom_to_fit refuse while it is in flight, so returning early would only move the failure downstream. settled:false means it hadn't arrived within the timeout; the command was still sent. |
obsbot_zoom_vendor | ratio (1.0–2.0), speed (default 0), camera? | Vendor zoom path with adjustable speed: zoom to a ratio at a chosen speed (0 = device default, 1–10 slow→fast, 255 = maximum). Its ratio scale differs from obsbot_zoom_uvc's and may not land exactly on the requested target — see Known limitations. |
AI tracking
| Tool | Parameters | Description |
|---|---|---|
obsbot_ai_track | enabled (bool), mode (default "normal"), camera? | Enable/disable AI tracking and choose the mode: a human framing (normal | upper-body | close-up | headless | lower-body) or a scene mode (group | whiteboard | desk | hand). Polls status and returns { verified, matched } (matched:false = no subject tracked yet). |
obsbot_ai_track_speed | speed: "standard" | "sport", camera? | Set the tracking-speed preset (Center's Standard/Sport): standard (slower follow) or sport (snappier). |
obsbot_focus_face | enabled (bool), camera? | Enable or disable face-priority autofocus. |
Image & lens
Focus, white balance, and exposure each split into a dedicated _auto and _manual tool in
v0.4.0 (previously one tool with a mode parameter) — auto and manual take different parameters, so
splitting them lets each schema say exactly what it needs.
| Tool | Parameters | Description |
|---|---|---|
obsbot_image_fov | fov: "wide" | "medium" | "narrow", camera? | Set the field of view: wide (86°), medium (78°), narrow (65°). |
obsbot_image_hdr | enabled (bool), camera? | Toggle HDR/WDR imaging on or off. |
obsbot_focus_auto | camera? | Enable continuous autofocus. |
obsbot_focus_manual | position (0–100, default 50), camera? | Set the focus motor to position (near→far). |
obsbot_image_exposure_auto | priority ("global" | "face", optional), camera? | Enable auto-exposure; optional priority selects global vs face metering. |
obsbot_image_exposure_manual | level (0–100, default 50), camera? | Set exposure level (0 darkest → 100 brightest). |
obsbot_image_wb_auto | camera? | Enable auto white balance. |
obsbot_image_wb_manual | temperature (Kelvin, default 5000), camera? | Set a colour temperature (clamped to device range). |
obsbot_image_adjust | control, level (0–100), camera? | Adjust brightness | contrast | hue | saturation | sharpness | gain | backlight-compensation; level maps onto the device range. gain and backlight-compensation are not implemented on the Tiny 2 (it reports them as zero-length controls) and are refused with an error; the other five work. |
Capture
obsbot_capture_record and obsbot_capture_preview do not take camera. They select a device
by source (device/virtual/ndi) through ffmpeg/ffplay, not by serial — there is no
serial-to-ffmpeg-device mapping yet. obsbot_capture_snapshot honors camera only for
source:"device"; for source:"virtual"/"ndi" the pixel source is still resolved by device
name, independent of camera.
| Tool | Parameters | Description |
|---|---|---|
obsbot_capture_snapshot | resolution (256–1920, default 640), quality (1–100, default 80), settleMs (0–15000, default 600), source (default "device"), camera? (source:"device" only) | Grab one still frame and return it as an image (for framing/lighting/exposure checks). resolution is the longest edge in pixels — larger costs proportionally more tokens. source: device | virtual | ndi. Also returns sourceFormat — the format the capture graph negotiated, e.g. MJPG 1920x1080@30.00 — because frame rate selects the field of view on this camera (see limitations); Windows only, absent means unknown. A source that connects lazily (NDI) may need a larger settleMs. |
obsbot_capture_record | durationSec (optional), audio (default true), outputPath (optional), source (default "device") | Start recording to MP4. Open-ended recordings auto-stop after 60 min; audio uses the OBSBOT mic; defaults to ~/Videos/OBSBOT on every platform, including macOS, where that is not the usual ~/Movies. Returns a sessionId. **Needs ffmpeg.**¹ No camera. |
obsbot_capture_preview | source (default "device") | Open a live preview window. Returns a sessionId. **Needs ffplay.**¹ No camera. device is pinned to 1080p60 mjpeg for smooth motion — which costs field of view (see limitations). virtual/ndi negotiate instead, since neither offers mjpeg and pinning it there prevents the device opening at all. |
obsbot_capture_stop | sessionId | Stop a recording or preview session (recordings are finalized gracefully). No camera. |
obsbot_capture_list | — | List active recording/preview sessions. No camera. |
Diagnostics (--debug only)
| Tool | Parameters | Description |
|---|---|---|
obsbot_debug_probe | mode: "get" | "set" | "query", plus selector, length, hex, opcode, payloadHex | RE/diagnostics only — raw XU byte get/set and framed table queries. Advertised only under --debug. No camera. |
¹ record/preview shell out to ffmpeg/ffplay (install: winget install Gyan.FFmpeg
on Windows, brew install ffmpeg on macOS, apt install ffmpeg on Linux). snapshot does not
need ffmpeg — it grabs the frame through the native helper.
OBSBOT Tail 2 (network cameras)
The Tail 2 is a different animal: a network PTZ camera (NDI/RTSP/SRT/RTMP output, ethernet + WiFi,
and a USB-C port that is either MTP for footage offload or a UVC webcam). The tools below use its
network control plane — an HTTP REST API plus a WebSocket status push, documented in
TAIL2-PROTOCOL.md from on-device reverse engineering. That makes the whole
module pure TypeScript with zero platform-specific code: no native helper, no helper build,
identical behavior on Windows/Linux/macOS.
In UVC mode the camera also answers the standard UVC controls over USB (pan, tilt, zoom, pan/tilt speed, focus, exposure, white balance), and its vendor Extension Unit gives tracking on/off, human tracking mode, tracking speed and Auto Zoom. This server has no Tail 2 USB transport yet, so none of that is exposed as a tool; presets, portrait rotation and roll trim are network only. See TAIL2-PROTOCOL.md §11 for what was measured.
Getting started:
- Put the Tail 2 on your network (it defaults to DHCP on ethernet).
- Call
obsbot_tail2_scanonce — it listens ~5 s for the camera's own mDNS announcements (it multicasts its MAC, name and IPs every few seconds, the same channel OBSBOT Center discovers by) and falls back to an HTTP subnet sweep only on multicast-filtered networks. Or setOBSBOT_TAIL2_HOSTS(comma-separated addresses) in the server's environment, or just pass any Tail 2 tool acameraparameter holding the camera's IP.
The camera selector for these tools is the camera's MAC (its stable identity), or its
device_name, or its host address — case-insensitive. Omitted with one Tail 2 registered it
resolves to that one.
Shortened here. Read the whole README on GitHub.
Signals
- GitHub stars
- 1
- Forks
- 1
- Last commit
- Oct 2026
- Weekly downloads
- 822
- Weekly_downloads
- 46 weekly_downloads
Advanced
- Delivery
- obsbot-mcp MCP server → your Ahel connector (mcp.ahel.ai) → your AI.
- Item type
- mcp-server
- Key
io-github-lxman-obsbot-mcp- Source
- github.com/lxman/obsbot-mcp