blink-camera-mcp
An MCP server for Amazon Blink cameras, including the pan/tilt mount that Blink's own API gives you no way to move.
Give any MCP host — Claude Desktop, an IDE agent, your own client — the ability to see where a camera is pointing, aim it, and look through it.
camera_status where it points, travel limits, session state, stream URL
pan_tilt aim at an absolute angle, wait until the hardware confirms
pan_tilt_nudge turn relative to where it points now
pan_tilt_stop stop the motors
pan_tilt_home go to the saved home position
pan_tilt_set_home save the current angle as home
pan_tilt_overview 360° sweep
snapshot one still frame, returned as image content
Why this exists
Blink cameras are cloud devices. There is no local API, no ONVIF, no UVC — video exists only as a short-lived session brokered by Blink's cloud, capped at 300 seconds. Two consequences:
- Anything that wants to use the camera has to hold that session open and refresh it.
- The pan/tilt mount is not a device you can talk to. It has no endpoint of its own: it is driven inside the camera's media session, and its position comes back on the same socket. A client that reads only the video and skips every other message never sees any of that traffic, which is why the mount has looked uncontrollable for years.
This server owns the session (or reuses one), frames it correctly, routes the accessory messages, and exposes the whole thing as MCP tools with closed-loop semantics.
Install
pip install blink-camera-mcp # or without installing: uvx blink-camera-mcp
The distribution is blink-camera-mcp; the command it installs is blink-mcp.
You need Python 3.10+, a Blink account, and ffmpeg on PATH for snapshot
(set BLINK_FFMPEG if it lives somewhere unusual).
Configure
Credentials come from the environment, and only the first run needs them:
export BLINK_USERNAME="you@example.com"
export BLINK_PASSWORD="..."
export BLINK_CAMERA_NAME="Front Door" # optional; defaults to the only camera
The token is cached (token material only — a password is never written to disk), so later
runs need no credentials. Cache location defaults to ~/.blink-mcp/state.json; override with
BLINK_STATE_FILE.
If Blink asks for a 2FA code, the server will not prompt you and will not take the code from anywhere but a file:
export BLINK_2FA_FILE=/path/to/code.txt
Write the newest code into that file; it is read once and deleted. This keeps a live credential out of chat logs, shell history and argument lists.
Add it to your MCP host
blink-mcp --print-config
{
"mcpServers": {
"blink": {
"command": "uvx",
"args": ["blink-camera-mcp", "--stream-port", "9000"]
}
}
}
Hosts that manage their own Python (Claude Desktop, VS Code, LM Studio, …) run uvx as above.
If you installed it into a virtualenv instead, set command to that interpreter and args to
["-m", "blink_mcp", …].
Two ways to run it
Standalone (default) — the server owns the camera's live session and publishes the video
to a local TCP port (tcp://127.0.0.1:<port>, or a fixed one with --stream-port 9000). Point
OBS at it as a Media Source if you want the camera as a webcam as well.
Against a bridge — if something else already holds the session:
blink-mcp --control-url http://127.0.0.1:9100 --stream-url tcp://127.0.0.1:9000
This matters because Blink allows one live session per camera: a viewer and a controller have to share it or take turns. Running two things that each insist on their own session means one of them loses.
What "closed-loop" means here
The mount reports its angle while it is moving. So pan_tilt does not sleep and hope: it
sends the command and returns only when the hardware reports that its motors stopped at the
requested angle — typically well under a second.
pan_tilt_nudge(+4) -> settled=True moved=True elapsed=0.67s -> pan=117 tilt=-75
pan_tilt_nudge(-4) -> settled=True moved=True -> pan=113 tilt=-75
Two deliberate behaviours, because a confident wrong answer is worse than a failure:
- if the mount never confirms, the tool fails rather than returning the last angle it happened to know (a stale position dressed up as success would have an agent announce a move that never happened);
- a command to the angle the camera already holds is a no-op reported as
moved: false— success, not failure.
Privacy and safety
snapshotdecodes a frame of whatever the camera is pointed at. The tool description says so, and tells the agent to use it only when the user asks. Nothing here captures anything on its own.- The server talks to Blink directly, and the token cache holds no password.
- Any bridge control plane is loopback-only by design: it moves a physical camera.
How it was built
The accessory channel is undocumented. It was recovered from the official Android app — dex bytecode and the native library's symbol table — and then verified against hardware:
frame [flag:1][id:4 big-endian][length:4 big-endian][payload]
send flag 0x14 INLINE_COMMAND, id = commandId
move=3 [0,0,0,0,<pan>,<tilt>,0] stop=4 home=5 set_home=6 overview=7
receive flag 0x15 ACCESSORY_MESSAGE, id = message id
POSITION=2 / HOME_POSITION=3 [<counter>,<pan>,<tilt>,<status>]
ROSIE_LIMITS=4 PAN_OVERVIEW_COMPLETE=5 lights/siren = 0/1/6/7
status is 0x00 idle and 0x10 moving. Angles are single signed bytes.
The same protocol knowledge is being contributed upstream to
blinkpy so every Blink integration benefits, not just
this server. blink_mcp/immi.py is byte-identical to the module submitted there, and switches
to the upstream copy automatically once it ships.
Standing on the shoulders of blinkpy, which implements the Blink cloud API and the IMMI transport.
Limitations
- Verified on a Blink Mini with the pan/tilt mount. Other camera families use a different live transport (WebRTC with JSON-RPC commands rather than this binary channel) and are not supported yet.
- One live session per camera, as above.
- Blink caps a session at 300 s; the server rotates it around 270 s and consumers never notice.
snapshotneedsffmpeg; it decodes one frame from the stream rather than opening a second session.
Tests
pytest # 55 offline tests: protocol bytes, closed-loop logic, tool layer,
# a real stdio handshake, and a stub control plane
The suite is offline and needs no camera. For real hardware:
python tests/live_smoke.py --state-file ~/.blink-mcp/state.json
which logs in, moves the camera out and back, and captures a frame.
License
MIT
Release files for blink-camera-mcp 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| blink_camera_mcp-0.1.0.tar.gz | 32.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| blink_camera_mcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 60.9 kB
Release files / blink_camera_mcp-0.1.0.tar.gz
| Download URL | blink_camera_mcp-0.1.0.tar.gz |
|---|---|
| Size | 32.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
29ab48310307534460b19a2fece485ad6eff86e4412e01d82245e1d0f98669cc
|
|
BLAKE2b-256 checksum How to use checksums |
aca94d7862d4cb1ef018eebbed7f0f50467d1ea02d52b73b7a1dfcee1b4cf310
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / blink_camera_mcp-0.1.0-py3-none-any.whl
| Download URL | blink_camera_mcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 28.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b7b86d5d32ba173746cd2afd6c7d806e464235b27a1b3b7b45138a7dce830dc3
|
|
BLAKE2b-256 checksum How to use checksums |
d0cf8108b537ecb76d48cccaee1c725c71a1dfd7bd7e303f00ef2c2143b8c2c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|