blink-camera-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blink_camera_mcp-0.1.0/LICENSE +21 -0
- blink_camera_mcp-0.1.0/PKG-INFO +214 -0
- blink_camera_mcp-0.1.0/README.md +188 -0
- blink_camera_mcp-0.1.0/blink_camera_mcp.egg-info/PKG-INFO +214 -0
- blink_camera_mcp-0.1.0/blink_camera_mcp.egg-info/SOURCES.txt +20 -0
- blink_camera_mcp-0.1.0/blink_camera_mcp.egg-info/dependency_links.txt +1 -0
- blink_camera_mcp-0.1.0/blink_camera_mcp.egg-info/entry_points.txt +2 -0
- blink_camera_mcp-0.1.0/blink_camera_mcp.egg-info/requires.txt +3 -0
- blink_camera_mcp-0.1.0/blink_camera_mcp.egg-info/top_level.txt +1 -0
- blink_camera_mcp-0.1.0/blink_mcp/__init__.py +13 -0
- blink_camera_mcp-0.1.0/blink_mcp/__main__.py +176 -0
- blink_camera_mcp-0.1.0/blink_mcp/backend.py +244 -0
- blink_camera_mcp-0.1.0/blink_mcp/control.py +339 -0
- blink_camera_mcp-0.1.0/blink_mcp/immi.py +179 -0
- blink_camera_mcp-0.1.0/blink_mcp/server.py +224 -0
- blink_camera_mcp-0.1.0/blink_mcp/session.py +361 -0
- blink_camera_mcp-0.1.0/blink_mcp/stream.py +128 -0
- blink_camera_mcp-0.1.0/pyproject.toml +69 -0
- blink_camera_mcp-0.1.0/setup.cfg +4 -0
- blink_camera_mcp-0.1.0/tests/test_control.py +182 -0
- blink_camera_mcp-0.1.0/tests/test_immi.py +168 -0
- blink_camera_mcp-0.1.0/tests/test_server.py +372 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abhijat Saxena
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blink-camera-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Amazon Blink cameras: see, aim (pan/tilt) and capture, including the undocumented pan/tilt accessory protocol.
|
|
5
|
+
Author: Abhijat Saxena
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/AbhijatSaxena/blink-camera-mcp
|
|
8
|
+
Project-URL: Issues, https://github.com/AbhijatSaxena/blink-camera-mcp/issues
|
|
9
|
+
Keywords: mcp,model-context-protocol,blink,amazon-blink,blink-camera,camera,pan-tilt,ptz,security-camera,home-automation
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Home Automation
|
|
18
|
+
Classifier: Topic :: Multimedia :: Video :: Capture
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: mcp<3,>=2.2.0
|
|
23
|
+
Requires-Dist: blinkpy>=0.25.0
|
|
24
|
+
Requires-Dist: aiohttp>=3.9
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# blink-camera-mcp
|
|
28
|
+
|
|
29
|
+
[](https://github.com/AbhijatSaxena/blink-camera-mcp/actions/workflows/ci.yml)
|
|
30
|
+
|
|
31
|
+
<!-- mcp-name: io.github.AbhijatSaxena/blink-camera-mcp -->
|
|
32
|
+
|
|
33
|
+
An [MCP](https://modelcontextprotocol.io) server for **Amazon Blink cameras**, including the
|
|
34
|
+
**pan/tilt mount** that Blink's own API gives you no way to move.
|
|
35
|
+
|
|
36
|
+
Give any MCP host — Claude Desktop, an IDE agent, your own client — the ability to see where
|
|
37
|
+
a camera is pointing, aim it, and look through it.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
camera_status where it points, travel limits, session state, stream URL
|
|
41
|
+
pan_tilt aim at an absolute angle, wait until the hardware confirms
|
|
42
|
+
pan_tilt_nudge turn relative to where it points now
|
|
43
|
+
pan_tilt_stop stop the motors
|
|
44
|
+
pan_tilt_home go to the saved home position
|
|
45
|
+
pan_tilt_set_home save the current angle as home
|
|
46
|
+
pan_tilt_overview 360° sweep
|
|
47
|
+
snapshot one still frame, returned as image content
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Why this exists
|
|
51
|
+
|
|
52
|
+
Blink cameras are cloud devices. There is no local API, no ONVIF, no UVC — video exists only
|
|
53
|
+
as a short-lived session brokered by Blink's cloud, capped at 300 seconds. Two consequences:
|
|
54
|
+
|
|
55
|
+
1. Anything that wants to use the camera has to hold that session open and refresh it.
|
|
56
|
+
2. The pan/tilt mount is not a device you can talk to. It has no endpoint of its own: it is
|
|
57
|
+
driven *inside* the camera's media session, and its position comes back on the same socket.
|
|
58
|
+
A client that reads only the video and skips every other message never sees any of that
|
|
59
|
+
traffic, which is why the mount has looked uncontrollable for years.
|
|
60
|
+
|
|
61
|
+
This server owns the session (or reuses one), frames it correctly, routes the accessory
|
|
62
|
+
messages, and exposes the whole thing as MCP tools with closed-loop semantics.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install blink-camera-mcp # or without installing: uvx blink-camera-mcp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The distribution is `blink-camera-mcp`; the command it installs is `blink-mcp`.
|
|
71
|
+
|
|
72
|
+
You need Python 3.10+, a Blink account, and `ffmpeg` on PATH for `snapshot`
|
|
73
|
+
(set `BLINK_FFMPEG` if it lives somewhere unusual).
|
|
74
|
+
|
|
75
|
+
## Configure
|
|
76
|
+
|
|
77
|
+
Credentials come from the environment, and only the first run needs them:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export BLINK_USERNAME="you@example.com"
|
|
81
|
+
export BLINK_PASSWORD="..."
|
|
82
|
+
export BLINK_CAMERA_NAME="Front Door" # optional; defaults to the only camera
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The token is cached (token material only — a password is never written to disk), so later
|
|
86
|
+
runs need no credentials. Cache location defaults to `~/.blink-mcp/state.json`; override with
|
|
87
|
+
`BLINK_STATE_FILE`.
|
|
88
|
+
|
|
89
|
+
**If Blink asks for a 2FA code**, the server will not prompt you and will not take the code
|
|
90
|
+
from anywhere but a file:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
export BLINK_2FA_FILE=/path/to/code.txt
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Write the newest code into that file; it is read once and deleted. This keeps a live
|
|
97
|
+
credential out of chat logs, shell history and argument lists.
|
|
98
|
+
|
|
99
|
+
## Add it to your MCP host
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
blink-mcp --print-config
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"mcpServers": {
|
|
108
|
+
"blink": {
|
|
109
|
+
"command": "uvx",
|
|
110
|
+
"args": ["blink-camera-mcp", "--stream-port", "9000"]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Hosts that manage their own Python (Claude Desktop, VS Code, LM Studio, …) run `uvx` as above.
|
|
117
|
+
If you installed it into a virtualenv instead, set `command` to that interpreter and `args` to
|
|
118
|
+
`["-m", "blink_mcp", …]`.
|
|
119
|
+
|
|
120
|
+
## Two ways to run it
|
|
121
|
+
|
|
122
|
+
**Standalone (default)** — the server owns the camera's live session and publishes the video
|
|
123
|
+
to a local TCP port (`tcp://127.0.0.1:<port>`, or a fixed one with `--stream-port 9000`). Point
|
|
124
|
+
OBS at it as a Media Source if you want the camera as a webcam as well.
|
|
125
|
+
|
|
126
|
+
**Against a bridge** — if something else already holds the session:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
blink-mcp --control-url http://127.0.0.1:9100 --stream-url tcp://127.0.0.1:9000
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This matters because **Blink allows one live session per camera**: a viewer and a controller
|
|
133
|
+
have to share it or take turns. Running two things that each insist on their own session means
|
|
134
|
+
one of them loses.
|
|
135
|
+
|
|
136
|
+
## What "closed-loop" means here
|
|
137
|
+
|
|
138
|
+
The mount reports its angle *while it is moving*. So `pan_tilt` does not sleep and hope: it
|
|
139
|
+
sends the command and returns only when the hardware reports that its motors stopped at the
|
|
140
|
+
requested angle — typically well under a second.
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
pan_tilt_nudge(+4) -> settled=True moved=True elapsed=0.67s -> pan=117 tilt=-75
|
|
144
|
+
pan_tilt_nudge(-4) -> settled=True moved=True -> pan=113 tilt=-75
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Two deliberate behaviours, because a confident wrong answer is worse than a failure:
|
|
148
|
+
|
|
149
|
+
* if the mount never confirms, the tool **fails** rather than returning the last angle it
|
|
150
|
+
happened to know (a stale position dressed up as success would have an agent announce a move
|
|
151
|
+
that never happened);
|
|
152
|
+
* a command to the angle the camera already holds is a no-op reported as `moved: false` —
|
|
153
|
+
success, not failure.
|
|
154
|
+
|
|
155
|
+
## Privacy and safety
|
|
156
|
+
|
|
157
|
+
* `snapshot` decodes a frame of whatever the camera is pointed at. The tool description says
|
|
158
|
+
so, and tells the agent to use it only when the user asks. Nothing here captures anything on
|
|
159
|
+
its own.
|
|
160
|
+
* The server talks to Blink directly, and the token cache holds no password.
|
|
161
|
+
* Any bridge control plane is loopback-only by design: it moves a physical camera.
|
|
162
|
+
|
|
163
|
+
## How it was built
|
|
164
|
+
|
|
165
|
+
The accessory channel is undocumented. It was recovered from the official Android app — dex
|
|
166
|
+
bytecode and the native library's symbol table — and then verified against hardware:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
frame [flag:1][id:4 big-endian][length:4 big-endian][payload]
|
|
170
|
+
send flag 0x14 INLINE_COMMAND, id = commandId
|
|
171
|
+
move=3 [0,0,0,0,<pan>,<tilt>,0] stop=4 home=5 set_home=6 overview=7
|
|
172
|
+
receive flag 0x15 ACCESSORY_MESSAGE, id = message id
|
|
173
|
+
POSITION=2 / HOME_POSITION=3 [<counter>,<pan>,<tilt>,<status>]
|
|
174
|
+
ROSIE_LIMITS=4 PAN_OVERVIEW_COMPLETE=5 lights/siren = 0/1/6/7
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`status` is `0x00` idle and `0x10` moving. Angles are single signed bytes.
|
|
178
|
+
|
|
179
|
+
The same protocol knowledge is being contributed upstream to
|
|
180
|
+
[blinkpy](https://github.com/fronzbot/blinkpy) so every Blink integration benefits, not just
|
|
181
|
+
this server. `blink_mcp/immi.py` is byte-identical to the module submitted there, and switches
|
|
182
|
+
to the upstream copy automatically once it ships.
|
|
183
|
+
|
|
184
|
+
Standing on the shoulders of [blinkpy](https://github.com/fronzbot/blinkpy), which implements
|
|
185
|
+
the Blink cloud API and the IMMI transport.
|
|
186
|
+
|
|
187
|
+
## Limitations
|
|
188
|
+
|
|
189
|
+
* Verified on a **Blink Mini with the pan/tilt mount**. Other camera families use a different
|
|
190
|
+
live transport (WebRTC with JSON-RPC commands rather than this binary channel) and are not
|
|
191
|
+
supported yet.
|
|
192
|
+
* One live session per camera, as above.
|
|
193
|
+
* Blink caps a session at 300 s; the server rotates it around 270 s and consumers never notice.
|
|
194
|
+
* `snapshot` needs `ffmpeg`; it decodes one frame from the stream rather than opening a second
|
|
195
|
+
session.
|
|
196
|
+
|
|
197
|
+
## Tests
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pytest # 55 offline tests: protocol bytes, closed-loop logic, tool layer,
|
|
201
|
+
# a real stdio handshake, and a stub control plane
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The suite is offline and needs no camera. For real hardware:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
python tests/live_smoke.py --state-file ~/.blink-mcp/state.json
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
which logs in, moves the camera out and back, and captures a frame.
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# blink-camera-mcp
|
|
2
|
+
|
|
3
|
+
[](https://github.com/AbhijatSaxena/blink-camera-mcp/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
<!-- mcp-name: io.github.AbhijatSaxena/blink-camera-mcp -->
|
|
6
|
+
|
|
7
|
+
An [MCP](https://modelcontextprotocol.io) server for **Amazon Blink cameras**, including the
|
|
8
|
+
**pan/tilt mount** that Blink's own API gives you no way to move.
|
|
9
|
+
|
|
10
|
+
Give any MCP host — Claude Desktop, an IDE agent, your own client — the ability to see where
|
|
11
|
+
a camera is pointing, aim it, and look through it.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
camera_status where it points, travel limits, session state, stream URL
|
|
15
|
+
pan_tilt aim at an absolute angle, wait until the hardware confirms
|
|
16
|
+
pan_tilt_nudge turn relative to where it points now
|
|
17
|
+
pan_tilt_stop stop the motors
|
|
18
|
+
pan_tilt_home go to the saved home position
|
|
19
|
+
pan_tilt_set_home save the current angle as home
|
|
20
|
+
pan_tilt_overview 360° sweep
|
|
21
|
+
snapshot one still frame, returned as image content
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Why this exists
|
|
25
|
+
|
|
26
|
+
Blink cameras are cloud devices. There is no local API, no ONVIF, no UVC — video exists only
|
|
27
|
+
as a short-lived session brokered by Blink's cloud, capped at 300 seconds. Two consequences:
|
|
28
|
+
|
|
29
|
+
1. Anything that wants to use the camera has to hold that session open and refresh it.
|
|
30
|
+
2. The pan/tilt mount is not a device you can talk to. It has no endpoint of its own: it is
|
|
31
|
+
driven *inside* the camera's media session, and its position comes back on the same socket.
|
|
32
|
+
A client that reads only the video and skips every other message never sees any of that
|
|
33
|
+
traffic, which is why the mount has looked uncontrollable for years.
|
|
34
|
+
|
|
35
|
+
This server owns the session (or reuses one), frames it correctly, routes the accessory
|
|
36
|
+
messages, and exposes the whole thing as MCP tools with closed-loop semantics.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install blink-camera-mcp # or without installing: uvx blink-camera-mcp
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The distribution is `blink-camera-mcp`; the command it installs is `blink-mcp`.
|
|
45
|
+
|
|
46
|
+
You need Python 3.10+, a Blink account, and `ffmpeg` on PATH for `snapshot`
|
|
47
|
+
(set `BLINK_FFMPEG` if it lives somewhere unusual).
|
|
48
|
+
|
|
49
|
+
## Configure
|
|
50
|
+
|
|
51
|
+
Credentials come from the environment, and only the first run needs them:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
export BLINK_USERNAME="you@example.com"
|
|
55
|
+
export BLINK_PASSWORD="..."
|
|
56
|
+
export BLINK_CAMERA_NAME="Front Door" # optional; defaults to the only camera
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The token is cached (token material only — a password is never written to disk), so later
|
|
60
|
+
runs need no credentials. Cache location defaults to `~/.blink-mcp/state.json`; override with
|
|
61
|
+
`BLINK_STATE_FILE`.
|
|
62
|
+
|
|
63
|
+
**If Blink asks for a 2FA code**, the server will not prompt you and will not take the code
|
|
64
|
+
from anywhere but a file:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export BLINK_2FA_FILE=/path/to/code.txt
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Write the newest code into that file; it is read once and deleted. This keeps a live
|
|
71
|
+
credential out of chat logs, shell history and argument lists.
|
|
72
|
+
|
|
73
|
+
## Add it to your MCP host
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
blink-mcp --print-config
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"blink": {
|
|
83
|
+
"command": "uvx",
|
|
84
|
+
"args": ["blink-camera-mcp", "--stream-port", "9000"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Hosts that manage their own Python (Claude Desktop, VS Code, LM Studio, …) run `uvx` as above.
|
|
91
|
+
If you installed it into a virtualenv instead, set `command` to that interpreter and `args` to
|
|
92
|
+
`["-m", "blink_mcp", …]`.
|
|
93
|
+
|
|
94
|
+
## Two ways to run it
|
|
95
|
+
|
|
96
|
+
**Standalone (default)** — the server owns the camera's live session and publishes the video
|
|
97
|
+
to a local TCP port (`tcp://127.0.0.1:<port>`, or a fixed one with `--stream-port 9000`). Point
|
|
98
|
+
OBS at it as a Media Source if you want the camera as a webcam as well.
|
|
99
|
+
|
|
100
|
+
**Against a bridge** — if something else already holds the session:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
blink-mcp --control-url http://127.0.0.1:9100 --stream-url tcp://127.0.0.1:9000
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
This matters because **Blink allows one live session per camera**: a viewer and a controller
|
|
107
|
+
have to share it or take turns. Running two things that each insist on their own session means
|
|
108
|
+
one of them loses.
|
|
109
|
+
|
|
110
|
+
## What "closed-loop" means here
|
|
111
|
+
|
|
112
|
+
The mount reports its angle *while it is moving*. So `pan_tilt` does not sleep and hope: it
|
|
113
|
+
sends the command and returns only when the hardware reports that its motors stopped at the
|
|
114
|
+
requested angle — typically well under a second.
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
pan_tilt_nudge(+4) -> settled=True moved=True elapsed=0.67s -> pan=117 tilt=-75
|
|
118
|
+
pan_tilt_nudge(-4) -> settled=True moved=True -> pan=113 tilt=-75
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Two deliberate behaviours, because a confident wrong answer is worse than a failure:
|
|
122
|
+
|
|
123
|
+
* if the mount never confirms, the tool **fails** rather than returning the last angle it
|
|
124
|
+
happened to know (a stale position dressed up as success would have an agent announce a move
|
|
125
|
+
that never happened);
|
|
126
|
+
* a command to the angle the camera already holds is a no-op reported as `moved: false` —
|
|
127
|
+
success, not failure.
|
|
128
|
+
|
|
129
|
+
## Privacy and safety
|
|
130
|
+
|
|
131
|
+
* `snapshot` decodes a frame of whatever the camera is pointed at. The tool description says
|
|
132
|
+
so, and tells the agent to use it only when the user asks. Nothing here captures anything on
|
|
133
|
+
its own.
|
|
134
|
+
* The server talks to Blink directly, and the token cache holds no password.
|
|
135
|
+
* Any bridge control plane is loopback-only by design: it moves a physical camera.
|
|
136
|
+
|
|
137
|
+
## How it was built
|
|
138
|
+
|
|
139
|
+
The accessory channel is undocumented. It was recovered from the official Android app — dex
|
|
140
|
+
bytecode and the native library's symbol table — and then verified against hardware:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
frame [flag:1][id:4 big-endian][length:4 big-endian][payload]
|
|
144
|
+
send flag 0x14 INLINE_COMMAND, id = commandId
|
|
145
|
+
move=3 [0,0,0,0,<pan>,<tilt>,0] stop=4 home=5 set_home=6 overview=7
|
|
146
|
+
receive flag 0x15 ACCESSORY_MESSAGE, id = message id
|
|
147
|
+
POSITION=2 / HOME_POSITION=3 [<counter>,<pan>,<tilt>,<status>]
|
|
148
|
+
ROSIE_LIMITS=4 PAN_OVERVIEW_COMPLETE=5 lights/siren = 0/1/6/7
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`status` is `0x00` idle and `0x10` moving. Angles are single signed bytes.
|
|
152
|
+
|
|
153
|
+
The same protocol knowledge is being contributed upstream to
|
|
154
|
+
[blinkpy](https://github.com/fronzbot/blinkpy) so every Blink integration benefits, not just
|
|
155
|
+
this server. `blink_mcp/immi.py` is byte-identical to the module submitted there, and switches
|
|
156
|
+
to the upstream copy automatically once it ships.
|
|
157
|
+
|
|
158
|
+
Standing on the shoulders of [blinkpy](https://github.com/fronzbot/blinkpy), which implements
|
|
159
|
+
the Blink cloud API and the IMMI transport.
|
|
160
|
+
|
|
161
|
+
## Limitations
|
|
162
|
+
|
|
163
|
+
* Verified on a **Blink Mini with the pan/tilt mount**. Other camera families use a different
|
|
164
|
+
live transport (WebRTC with JSON-RPC commands rather than this binary channel) and are not
|
|
165
|
+
supported yet.
|
|
166
|
+
* One live session per camera, as above.
|
|
167
|
+
* Blink caps a session at 300 s; the server rotates it around 270 s and consumers never notice.
|
|
168
|
+
* `snapshot` needs `ffmpeg`; it decodes one frame from the stream rather than opening a second
|
|
169
|
+
session.
|
|
170
|
+
|
|
171
|
+
## Tests
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
pytest # 55 offline tests: protocol bytes, closed-loop logic, tool layer,
|
|
175
|
+
# a real stdio handshake, and a stub control plane
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The suite is offline and needs no camera. For real hardware:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
python tests/live_smoke.py --state-file ~/.blink-mcp/state.json
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
which logs in, moves the camera out and back, and captures a frame.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blink-camera-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Amazon Blink cameras: see, aim (pan/tilt) and capture, including the undocumented pan/tilt accessory protocol.
|
|
5
|
+
Author: Abhijat Saxena
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/AbhijatSaxena/blink-camera-mcp
|
|
8
|
+
Project-URL: Issues, https://github.com/AbhijatSaxena/blink-camera-mcp/issues
|
|
9
|
+
Keywords: mcp,model-context-protocol,blink,amazon-blink,blink-camera,camera,pan-tilt,ptz,security-camera,home-automation
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Home Automation
|
|
18
|
+
Classifier: Topic :: Multimedia :: Video :: Capture
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: mcp<3,>=2.2.0
|
|
23
|
+
Requires-Dist: blinkpy>=0.25.0
|
|
24
|
+
Requires-Dist: aiohttp>=3.9
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# blink-camera-mcp
|
|
28
|
+
|
|
29
|
+
[](https://github.com/AbhijatSaxena/blink-camera-mcp/actions/workflows/ci.yml)
|
|
30
|
+
|
|
31
|
+
<!-- mcp-name: io.github.AbhijatSaxena/blink-camera-mcp -->
|
|
32
|
+
|
|
33
|
+
An [MCP](https://modelcontextprotocol.io) server for **Amazon Blink cameras**, including the
|
|
34
|
+
**pan/tilt mount** that Blink's own API gives you no way to move.
|
|
35
|
+
|
|
36
|
+
Give any MCP host — Claude Desktop, an IDE agent, your own client — the ability to see where
|
|
37
|
+
a camera is pointing, aim it, and look through it.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
camera_status where it points, travel limits, session state, stream URL
|
|
41
|
+
pan_tilt aim at an absolute angle, wait until the hardware confirms
|
|
42
|
+
pan_tilt_nudge turn relative to where it points now
|
|
43
|
+
pan_tilt_stop stop the motors
|
|
44
|
+
pan_tilt_home go to the saved home position
|
|
45
|
+
pan_tilt_set_home save the current angle as home
|
|
46
|
+
pan_tilt_overview 360° sweep
|
|
47
|
+
snapshot one still frame, returned as image content
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Why this exists
|
|
51
|
+
|
|
52
|
+
Blink cameras are cloud devices. There is no local API, no ONVIF, no UVC — video exists only
|
|
53
|
+
as a short-lived session brokered by Blink's cloud, capped at 300 seconds. Two consequences:
|
|
54
|
+
|
|
55
|
+
1. Anything that wants to use the camera has to hold that session open and refresh it.
|
|
56
|
+
2. The pan/tilt mount is not a device you can talk to. It has no endpoint of its own: it is
|
|
57
|
+
driven *inside* the camera's media session, and its position comes back on the same socket.
|
|
58
|
+
A client that reads only the video and skips every other message never sees any of that
|
|
59
|
+
traffic, which is why the mount has looked uncontrollable for years.
|
|
60
|
+
|
|
61
|
+
This server owns the session (or reuses one), frames it correctly, routes the accessory
|
|
62
|
+
messages, and exposes the whole thing as MCP tools with closed-loop semantics.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install blink-camera-mcp # or without installing: uvx blink-camera-mcp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The distribution is `blink-camera-mcp`; the command it installs is `blink-mcp`.
|
|
71
|
+
|
|
72
|
+
You need Python 3.10+, a Blink account, and `ffmpeg` on PATH for `snapshot`
|
|
73
|
+
(set `BLINK_FFMPEG` if it lives somewhere unusual).
|
|
74
|
+
|
|
75
|
+
## Configure
|
|
76
|
+
|
|
77
|
+
Credentials come from the environment, and only the first run needs them:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export BLINK_USERNAME="you@example.com"
|
|
81
|
+
export BLINK_PASSWORD="..."
|
|
82
|
+
export BLINK_CAMERA_NAME="Front Door" # optional; defaults to the only camera
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The token is cached (token material only — a password is never written to disk), so later
|
|
86
|
+
runs need no credentials. Cache location defaults to `~/.blink-mcp/state.json`; override with
|
|
87
|
+
`BLINK_STATE_FILE`.
|
|
88
|
+
|
|
89
|
+
**If Blink asks for a 2FA code**, the server will not prompt you and will not take the code
|
|
90
|
+
from anywhere but a file:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
export BLINK_2FA_FILE=/path/to/code.txt
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Write the newest code into that file; it is read once and deleted. This keeps a live
|
|
97
|
+
credential out of chat logs, shell history and argument lists.
|
|
98
|
+
|
|
99
|
+
## Add it to your MCP host
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
blink-mcp --print-config
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"mcpServers": {
|
|
108
|
+
"blink": {
|
|
109
|
+
"command": "uvx",
|
|
110
|
+
"args": ["blink-camera-mcp", "--stream-port", "9000"]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Hosts that manage their own Python (Claude Desktop, VS Code, LM Studio, …) run `uvx` as above.
|
|
117
|
+
If you installed it into a virtualenv instead, set `command` to that interpreter and `args` to
|
|
118
|
+
`["-m", "blink_mcp", …]`.
|
|
119
|
+
|
|
120
|
+
## Two ways to run it
|
|
121
|
+
|
|
122
|
+
**Standalone (default)** — the server owns the camera's live session and publishes the video
|
|
123
|
+
to a local TCP port (`tcp://127.0.0.1:<port>`, or a fixed one with `--stream-port 9000`). Point
|
|
124
|
+
OBS at it as a Media Source if you want the camera as a webcam as well.
|
|
125
|
+
|
|
126
|
+
**Against a bridge** — if something else already holds the session:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
blink-mcp --control-url http://127.0.0.1:9100 --stream-url tcp://127.0.0.1:9000
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This matters because **Blink allows one live session per camera**: a viewer and a controller
|
|
133
|
+
have to share it or take turns. Running two things that each insist on their own session means
|
|
134
|
+
one of them loses.
|
|
135
|
+
|
|
136
|
+
## What "closed-loop" means here
|
|
137
|
+
|
|
138
|
+
The mount reports its angle *while it is moving*. So `pan_tilt` does not sleep and hope: it
|
|
139
|
+
sends the command and returns only when the hardware reports that its motors stopped at the
|
|
140
|
+
requested angle — typically well under a second.
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
pan_tilt_nudge(+4) -> settled=True moved=True elapsed=0.67s -> pan=117 tilt=-75
|
|
144
|
+
pan_tilt_nudge(-4) -> settled=True moved=True -> pan=113 tilt=-75
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Two deliberate behaviours, because a confident wrong answer is worse than a failure:
|
|
148
|
+
|
|
149
|
+
* if the mount never confirms, the tool **fails** rather than returning the last angle it
|
|
150
|
+
happened to know (a stale position dressed up as success would have an agent announce a move
|
|
151
|
+
that never happened);
|
|
152
|
+
* a command to the angle the camera already holds is a no-op reported as `moved: false` —
|
|
153
|
+
success, not failure.
|
|
154
|
+
|
|
155
|
+
## Privacy and safety
|
|
156
|
+
|
|
157
|
+
* `snapshot` decodes a frame of whatever the camera is pointed at. The tool description says
|
|
158
|
+
so, and tells the agent to use it only when the user asks. Nothing here captures anything on
|
|
159
|
+
its own.
|
|
160
|
+
* The server talks to Blink directly, and the token cache holds no password.
|
|
161
|
+
* Any bridge control plane is loopback-only by design: it moves a physical camera.
|
|
162
|
+
|
|
163
|
+
## How it was built
|
|
164
|
+
|
|
165
|
+
The accessory channel is undocumented. It was recovered from the official Android app — dex
|
|
166
|
+
bytecode and the native library's symbol table — and then verified against hardware:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
frame [flag:1][id:4 big-endian][length:4 big-endian][payload]
|
|
170
|
+
send flag 0x14 INLINE_COMMAND, id = commandId
|
|
171
|
+
move=3 [0,0,0,0,<pan>,<tilt>,0] stop=4 home=5 set_home=6 overview=7
|
|
172
|
+
receive flag 0x15 ACCESSORY_MESSAGE, id = message id
|
|
173
|
+
POSITION=2 / HOME_POSITION=3 [<counter>,<pan>,<tilt>,<status>]
|
|
174
|
+
ROSIE_LIMITS=4 PAN_OVERVIEW_COMPLETE=5 lights/siren = 0/1/6/7
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`status` is `0x00` idle and `0x10` moving. Angles are single signed bytes.
|
|
178
|
+
|
|
179
|
+
The same protocol knowledge is being contributed upstream to
|
|
180
|
+
[blinkpy](https://github.com/fronzbot/blinkpy) so every Blink integration benefits, not just
|
|
181
|
+
this server. `blink_mcp/immi.py` is byte-identical to the module submitted there, and switches
|
|
182
|
+
to the upstream copy automatically once it ships.
|
|
183
|
+
|
|
184
|
+
Standing on the shoulders of [blinkpy](https://github.com/fronzbot/blinkpy), which implements
|
|
185
|
+
the Blink cloud API and the IMMI transport.
|
|
186
|
+
|
|
187
|
+
## Limitations
|
|
188
|
+
|
|
189
|
+
* Verified on a **Blink Mini with the pan/tilt mount**. Other camera families use a different
|
|
190
|
+
live transport (WebRTC with JSON-RPC commands rather than this binary channel) and are not
|
|
191
|
+
supported yet.
|
|
192
|
+
* One live session per camera, as above.
|
|
193
|
+
* Blink caps a session at 300 s; the server rotates it around 270 s and consumers never notice.
|
|
194
|
+
* `snapshot` needs `ffmpeg`; it decodes one frame from the stream rather than opening a second
|
|
195
|
+
session.
|
|
196
|
+
|
|
197
|
+
## Tests
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pytest # 55 offline tests: protocol bytes, closed-loop logic, tool layer,
|
|
201
|
+
# a real stdio handshake, and a stub control plane
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The suite is offline and needs no camera. For real hardware:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
python tests/live_smoke.py --state-file ~/.blink-mcp/state.json
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
which logs in, moves the camera out and back, and captures a frame.
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
blink_camera_mcp.egg-info/PKG-INFO
|
|
5
|
+
blink_camera_mcp.egg-info/SOURCES.txt
|
|
6
|
+
blink_camera_mcp.egg-info/dependency_links.txt
|
|
7
|
+
blink_camera_mcp.egg-info/entry_points.txt
|
|
8
|
+
blink_camera_mcp.egg-info/requires.txt
|
|
9
|
+
blink_camera_mcp.egg-info/top_level.txt
|
|
10
|
+
blink_mcp/__init__.py
|
|
11
|
+
blink_mcp/__main__.py
|
|
12
|
+
blink_mcp/backend.py
|
|
13
|
+
blink_mcp/control.py
|
|
14
|
+
blink_mcp/immi.py
|
|
15
|
+
blink_mcp/server.py
|
|
16
|
+
blink_mcp/session.py
|
|
17
|
+
blink_mcp/stream.py
|
|
18
|
+
tests/test_control.py
|
|
19
|
+
tests/test_immi.py
|
|
20
|
+
tests/test_server.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blink_mcp
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""MCP server for Amazon Blink cameras.
|
|
2
|
+
|
|
3
|
+
Exposes a Blink camera and its pan/tilt mount to any Model Context Protocol host: see
|
|
4
|
+
where the camera points, aim it, and capture a frame. Works either standalone (it owns
|
|
5
|
+
the camera's live session) or against a running bridge, which is useful when the same
|
|
6
|
+
camera is already streaming video somewhere else.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
__all__ = ["__version__"]
|