ipcam-mcp
README.md
# ipcam-mcp
An [MCP](https://modelcontextprotocol.io) server that lets Claude Code see through and control
the IP cameras on your local network (ONVIF + RTSP): snapshots, video clips, pan/tilt and
change detection, all handled locally.
Any ONVIF camera should work. See [known cameras](docs/cameras/) for what has been tested.
## Tools
| Tool | What it does |
|---|---|
| `list_cameras` | Configured cameras and their capabilities (`camera`, `ptz`) |
| `camera_snapshot(camera, quality="high", crop=None, region=None, from_path=None)` | Returns the picture (≤1280 px wide) and saves the full-size JPEG. `crop=[x, y, w, h]` (fractions 0–1) or `region` (`top-left`, `top`, `top-right`, `left`, `center`, `right`, `bottom-left`, `bottom`, `bottom-right`) returns a detail at full resolution. `from_path` crops an earlier snapshot without contacting the camera |
| `camera_save_baseline(camera, name, quality="high")` | Takes a snapshot and keeps it as a named reference (e.g. `door-closed`) |
| `camera_compare(camera, baseline=None, quality="high")` | Takes a snapshot and reports what changed against the previous snapshot, or against a named baseline |
| `camera_record(camera, seconds=10, quality="high")` | Records 1–60 s to an mp4, returns its path |
| `ptz_move(camera, direction, amount="small")` | Pan/tilt `left/right/up/down` by `small/medium/large`; waits for the move to finish and returns the final position if the camera reports one |
| `ptz_list_presets(camera)` | Saved positions |
| `ptz_goto_preset(camera, preset)` | Move to a preset by name or token |
| `ptz_save_preset(camera, name, overwrite=False)` | Saves the current position as a preset; refuses an existing name unless `overwrite=true` |
| `ptz_remove_preset(camera, preset)` | Deletes a preset by name or token (cannot be undone) |
`camera` is a camera's name: its section name in `cameras.toml`. `quality="high"` uses the
camera's main stream (1080p), `"low"` the sub stream (smaller, quicker). Media is saved to the
camera's own folder, `<media_dir>/<uid>/<camera>_<YYYYmmdd-HHMMSS>.jpg|.mp4` (`media_dir`
defaults to `~/.local/share/ipcam-mcp/media`).
**Compare and crop.** `camera_compare` lists the grid cells that changed (the same names as
`region`) and a box around them. Pass a changed cell as `region` to `camera_snapshot`, ideally
with `from_path` set to the snapshot you just looked at, to see it closer. Baselines live in
`<media_dir>/<uid>/baselines/` as a JPEG plus a small JSON file.
**Camera uids.** Every camera has a permanent `uid` (8 characters, `a-z0-9`) in its
`cameras.toml` section. The server fills it in when it is missing, so you never write one
yourself; don't change or copy it. The uid names the camera's media folder, which also holds a
`camera.json` with the camera's current name. Because of it, a renamed camera keeps its
snapshots and baselines, and a new camera that reuses an old name starts with an empty history.
When you upgrade from 0.2, the first start moves existing snapshots, recordings and baselines
from the old flat layout into these folders; files of cameras that are no longer configured get
a folder of their own and are kept.
## Prerequisites
- Linux or WSL2, on the same network as the camera
- [uv](https://docs.astral.sh/uv/) on your `PATH` (it installs Python 3.13+ and the dependencies
for you)
- ffmpeg: `sudo apt install ffmpeg`
- An ONVIF-capable IP camera with a local camera account (username and password), with ONVIF
and RTSP enabled
- [Claude Code](https://claude.com/claude-code) or another MCP client that supports stdio
servers
## Setup
1. **Clone and install**:
```
git clone https://github.com/aidev-iprokszin/ipcam-mcp.git && cd ipcam-mcp && uv sync
```
2. **Camera account**: create or find the camera's local ONVIF/RTSP account (username and
password). This is **not** your cloud login. See [known cameras](docs/cameras/) for where
your brand hides it.
3. **Secrets file** (outside the repo), e.g. `~/.config/ipcam-mcp/secrets.env`:
```
CAMERA_IP=192.168.x.y
CAMERA_USER=...
CAMERA_PASSWORD=...
```
Make it readable only by you: `chmod 600 <file>`. The server warns at startup if other
users can read it.
4. **Camera config**: copy `cameras.example.toml` to `~/.config/ipcam-mcp/cameras.toml` (or
point `IPCAM_MCP_CONFIG` at another path) and adjust the camera name / variable names. Only
variable *names* go in this file, never the secrets themselves. Use `type = "onvif"`.
Steps 3 and 4 are optional if you let Claude add cameras: without a config file the server
starts with no cameras, and `add_camera` (see [Managing cameras](#managing-cameras))
creates `cameras.toml` and the camera's secrets file for your first camera.
5. **Use it in Claude Code**: the repo ships a project-scoped `.mcp.json`, so running `claude`
anywhere inside this repo offers the `ipcam` server. Approve it when prompted. If you
rejected it by mistake, run `claude mcp reset-project-choices` and start `claude` again. Then
ask Claude e.g. "list my cameras" or "take a snapshot of livingroom".
To use it from **every** folder, also register it in your user scope (inside the repo the
project entry takes precedence, so there are no duplicates):
```
claude mcp add ipcam -s user -- uv --directory /absolute/path/to/ipcam-mcp run ipcam-mcp
```
### Camera settings
All optional, set in the camera's section of `cameras.toml`. A wrong type is reported when the
config loads.
| Setting | Type | Default |
|---|---|---|
| `onvif_port` | integer 1–65535 | 80 |
| `rtsp_port` | integer 1–65535 | the port in the camera's stream URI, else 554 (a value you set always wins) |
| `invert_pan` | boolean | false |
| `ptz` | boolean | false |
| `stream_high` | path starting with `/` | looked up over ONVIF |
| `stream_low` | path starting with `/` | looked up over ONVIF |
`ptz = false` means the camera has no pan/tilt tools. Set `stream_high`/`stream_low` if the
ONVIF lookup fails or picks the wrong stream. Set `invert_pan = true` if `ptz_move left` turns
the view to the right. Some brands need other values; see [known cameras](docs/cameras/).
### Other MCP clients
Any client that supports stdio servers (Cursor, VS Code, Codex CLI, …) can run the same
command: `uv --directory /absolute/path/to/ipcam-mcp run ipcam-mcp`.
**Claude Desktop on Windows with the repo in WSL** (untested): add to
`%APPDATA%\Claude\claude_desktop_config.json` and restart the app:
```json
{
"mcpServers": {
"ipcam": {
"command": "wsl.exe",
"args": ["-e", "/home/<you>/.local/bin/uv", "--directory",
"/home/<you>/path/to/ipcam-mcp", "run", "ipcam-mcp"]
}
}
}
```
### WSL2 networking
If the camera can't be reached from WSL (`nc -zv <ip> 554` fails), enable mirrored networking
in `%UserProfile%\.wslconfig`:
```
[wsl2]
networkingMode=mirrored
```
then run `wsl --shutdown` from Windows.
## Managing cameras
Instead of editing `cameras.toml` by hand you can let Claude add, rename and remove cameras.
This is on by default. To turn it off, set `allow_management = false` in `cameras.toml` (see
`cameras.example.toml`), then run `/mcp reconnect ipcam`; the flag is only read at startup.
Claude Code still asks before each tool call unless you allowlist it; if you allowlist the
camera tools, consider leaving `remove_camera` and `ptz_remove_preset` off the list.
`add_camera` logs into whatever host it is given. Before you type a password into the login form,
check that the IP it shows is your camera's (from the camera's app or your router), not one the
agent picked up elsewhere.
| Tool | What it does |
|---|---|
| `add_camera(name, host, credentials_via="form", onvif_port=None)` | Verifies the login over ONVIF (tries port 80, then 2020, unless you give `onvif_port`), detects pan/tilt, then adds the camera with a new uid. `name` is lowercase letters, digits, `-`, `_` (max 32) |
| `rename_camera(old, new)` | Changes a camera's name; its uid, baselines, snapshots and recordings stay with it (no files move), and its secrets file is renamed too if the tool created it |
| `remove_camera(id)` | Removes the camera from the config; never deletes snapshots or recordings |
| `reload_cameras()` | Always available. Re-reads `cameras.toml` after a manual edit and lists the cameras |
Changes apply immediately, without restarting the server. If loading the new config fails, the
previous `cameras.toml` is restored.
**IP address.** Network discovery is not supported (WSL2 NAT blocks it), so tell Claude the
camera's IP from the camera's app or your router.
**Credentials never pass through the model.** Two routes:
- **Form** (default): your client shows a login form. MCP forms have no masked field, so the
password is visible while you type it, but it is not stored in the conversation. Clients on
MCP protocol 2026-07-28 cannot show the form mid-call, and neither can some phone (Remote
Control) views; there `add_camera` always uses the secrets-file route and says so in its
result.
- **File** (`credentials_via="file"`): `add_camera` creates `<secrets_dir>/<name>.env` (mode
`0600`, default `secrets_dir` is `~/.config/ipcam-mcp/secrets`) with `HOST=`, empty `USER=` and
`PASSWORD=`. Fill in USER and PASSWORD yourself, then ask Claude to call `add_camera` again
with the same name and host, **keeping `credentials_via="file"`**. Calling it with the default
`form` would ask for the credentials again instead of reading the file.
Each camera added this way has its own `<name>.env`, and files at `<secrets_dir>/<name>.env` are
treated as managed: renamed and deleted along with the camera. Keep hand-made files elsewhere;
those, such as a shared `secrets.env`, are never renamed or deleted. Secrets files are read literally: `${VAR}` in a
value is not expanded.
## Transports
`ipcam-mcp` uses stdio by default. `ipcam-mcp --transport http [--host 127.0.0.1] [--port 8765]`
serves streamable HTTP, but **without authentication**, so only use it on a trusted machine
and keep it bound to localhost. On `127.0.0.1`, `localhost` or `::1` the server rejects
requests addressed to other hosts, such as from web pages (DNS-rebinding protection). Any other
`--host` is refused unless you also pass `--allow-remote`; then anyone who can reach the port
controls your cameras.
## Security
See [SECURITY.md](SECURITY.md) for the threat model and how to report a vulnerability.
- Credentials are read only from your secrets file, never from the repo, and are not put into
environment variables.
- They are kept in memory by the camera's config and adapter, and are excluded from `repr`.
- The password is scrubbed (raw and URL-encoded) from every error message the server returns
or logs.
- RTSP URLs are masked (`rtsp://***@host…`), which also hides the username. The username is
not scrubbed anywhere else.
- Logs go to stderr; stdout carries only the MCP protocol.
## Troubleshooting
- **Auth errors / `401` / ONVIF error on every PTZ call**: use the camera's local account, not a
cloud login. See [known cameras](docs/cameras/).
- **Snapshots fail but ONVIF works**: set `stream_high`/`stream_low` (see
[Camera settings](#camera-settings)).
- **`timed out … (camera unreachable?)`**: the camera is offline or not reachable from this
machine. See [WSL2 networking](#wsl2-networking).
- **`ffmpeg not installed`**: `sudo apt install ffmpeg`.
## Known limitations
- While the camera is offline, failed ONVIF connection attempts aren't cleaned up, and you may
see `Unclosed client session` warnings on stderr. They're harmless.
- Filenames have one-second resolution; a second capture in the same second gets a `-2`, `-3`, …
suffix.
- Media files are never deleted. Clean up `~/.local/share/ipcam-mcp/media/` yourself.
- HTTP transport has no authentication (see above).
## Development
```
uv run pytest # unit tests (no network; the preview test is skipped without ffmpeg)
uv run pytest -m live # against the real camera from your config (IPCAM_MCP_CAMERA, default livingroom)
IPCAM_MCP_CAMERA=office uv run pytest -m live # e.g. against another configured camera
uv run ruff check . && uv run ruff format .
```
> ⚠️ The live tests **physically move the camera** (a small pan left and back) and save
> snapshots and a 3 s clip to the media folder. They also create and delete a preset and a
> baseline, both named `cmcp_test`.
### Adding a camera type
1. Add an adapter in `src/ipcam_mcp/adapters/` taking `(DeviceConfig, media_dir)` and
implementing the `Camera` protocol (and `PTZ` if it can pan/tilt) from
`src/ipcam_mcp/capabilities.py`.
2. Map its config `type` to the adapter in `ADAPTERS` in `src/ipcam_mcp/registry.py`.
A new camera feature (e.g. privacy mode) gets a `Protocol` in `capabilities.py`, registered in
`CAPABILITIES`, plus its tools in `src/ipcam_mcp/server.py`.
## Contributing
Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) and the
[Code of Conduct](CODE_OF_CONDUCT.md).
This project was built with [Claude Code](https://claude.com/claude-code).
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessUnresponsive