Skip to content
Conduit

mcp server

room-snapshot-api

A small service that turns RTSP surveillance cameras into a simple HTTP API and an MCP server

barval/room-snapshot-apicommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

This project does not publish an install method we can turn into a config. Follow the README instead.

README

synced Oct 29, 2026

Room Snapshot API v1.4

English | Русский

Python 3.15 License: MIT Docker MCP

A small service that turns RTSP surveillance cameras into a simple HTTP API and an MCP server. Other projects — scripts, sites, apps and AI agents — talk to it instead of dealing with RTSP URLs and camera credentials.

Ships with an agent skill, so Claude Code, Cursor and other agents can install and use it on their own.

Features

  • HTTP API for room lists, room details and JPEG snapshots.
  • MCP server (streamable HTTP and stdio) so AI agents get cameras as ready-made tools.
  • Agent skill that can deploy the service and use it from Claude Code, Cursor and other agents.
  • Per-camera credentials instead of one shared login.
  • API key authentication on every endpoint (REST and MCP).
  • Concurrent capture: requests to different cameras run in parallel.
  • In-memory snapshot cache with ETag/304 and Cache-Control.
  • Localized room names (ru/en) with a per-installation translation file.
  • Real health check: live TCP probes of every camera.
  • One-command installation: ./install.sh prepares the config and starts Docker.
  • Docker image, runs as a non-root user.

Requirements

  • Docker and Docker Compose (recommended), or
  • Python 3.15 and FFmpeg for local development.
  • Network access to the RTSP cameras.

Tested hardware

The service is tested with TP-Link Tapo C100, C200 and C500 IP cameras over RTSP. Other cameras work as long as ffmpeg can read their RTSP stream; the setup notes for Tapo (camera account, /stream1 and /stream2 paths) are in skills/room-snapshot/references/cameras.md.

Quick start

One command creates .env with a generated API key, copies the config templates, builds the image and waits until the API is healthy:

git clone git@gitea.prits.ru:barval-my/room-snapshot-api.git
cd room-snapshot-api
./install.sh

Then put your cameras into config/cameras.conf and restart:

docker compose restart api mcp

Alternatively, do it by hand:

cp .env.example .env               # set API_KEY and RTSP_AUTH
cp config/cameras.conf.example config/cameras.conf
cp config/cameras.auth.example config/cameras.auth     # optional per-camera credentials
cp config/rooms.i18n.example.json config/rooms.i18n.json  # optional translations

docker compose up -d --build

The REST API is available at http://localhost:5005 and the MCP server at http://localhost:5006/mcp.

Agent skill

The repository ships a self-contained skill in skills/room-snapshot/. It covers both cases:

  • deploy — the agent checks prerequisites, runs ./install.sh, adds cameras, verifies the endpoints and registers the MCP server;
  • use — the agent is given a base URL and an API key and only reads rooms, takes snapshots and reports health.
mkdir -p ~/.claude/skills
cp -r skills/room-snapshot ~/.claude/skills/

See skills/README.md for other agents and for a smoke test. The skill is MIT like the rest of the project and is published free of charge.

Installation-specific files

The set of cameras, their names and credentials differ per installation and are never committed. Only the *.example templates are tracked; the real files live in config/ and are excluded from the Docker image (mounted as a volume).

FilePurposeFormat
config/cameras.confCamera listcode=ip:port/path:name
config/cameras.authPer-camera credentials (optional)code=user:password
config/rooms.i18n.jsonLocalized room names (optional){"kuh": {"ru": "Кухня", "en": "Kitchen"}}
  • A camera without a record in cameras.auth falls back to the global RTSP_AUTH.
  • A camera without any credentials gets a URL without userinfo.
  • A missing rooms.i18n.json means names from cameras.conf are used as-is.

Configuration (.env)

VariableDescriptionDefault
API_KEYShared API key for REST and MCP. Required.—
RTSP_AUTHFallback camera credentials user:password.``
CORS_ORIGINSComma-separated allowed origins; empty blocks cross-origin.``
DEFAULT_LANGDefault room-name language (en or ru).en
SNAPSHOT_CACHE_TTLSnapshot cache lifetime, seconds.5
SNAPSHOT_TIMEOUTffmpeg capture timeout, seconds.15
MAX_CONCURRENT_CAPTURESMaximum ffmpeg processes at once.4
RATE_LIMIT_PER_MINUTEPer-client request limit; 0 disables it.0
MCP_PORTMCP server port.8000
FLASK_DEBUGDebug mode (never in production).false
HOST, PORTBind address/port (development server).0.0.0.0, 5000
CONFIG_DIRDirectory with the installation files./app/config
CAMERAS_CONFOverride path to cameras.conf.<CONFIG_DIR>/cameras.conf
CAMERAS_AUTHOverride path to cameras.auth.<CONFIG_DIR>/cameras.auth
ROOMS_I18NOverride path to rooms.i18n.json.<CONFIG_DIR>/rooms.i18n.json

The service fails closed: if API_KEY is empty and FLASK_DEBUG is not true, it refuses to start.

Run the development server with python -m app.app (uses HOST/PORT); under Docker the port is fixed by the gunicorn command.

Authentication

Every request needs the shared key, either as an X-API-Key header or as Authorization: Bearer <key>:

curl -H "X-API-Key: $API_KEY" http://localhost:5005/rooms

A missing or wrong key returns 401 {"error": "Unauthorized"}.

REST endpoints

MethodPathDescription
GET/infoAPI metadata and endpoint list.
GET/rooms{code: name} for all rooms (language via ?lang= / Accept-Language).
GET/rooms/<code>Room details: name, names, ip, stream, masked url.
GET/snapshot/<code>JPEG snapshot. Params: width=<px>, fresh=1.
GET/healthLive camera reachability; always HTTP 200.

Snapshot responses carry ETag and Cache-Control; send If-None-Match to get 304 Not Modified. ?fresh=1 bypasses the cache and refreshes it.

curl -H "X-API-Key: $API_KEY" http://localhost:5005/snapshot/kuh?width=640 -o kuh.jpg

MCP server

AI agents connect over streamable HTTP at http://host:5006/mcp, using the same API_KEY (X-API-Key header or Authorization: Bearer).

Tools exposed:

ToolArgumentsReturns
list_roomslang?Room code → localized name.
get_snapshotroom, width?JPEG frame as image content.
room_inforoom, lang?Room details with a masked URL.
health—Camera reachability and ffmpeg availability.

Unknown rooms and capture failures are returned as tool errors with a readable message, so the agent can explain the problem instead of showing nothing.

Example MCP client configuration (URL transport):

{
  "mcpServers": {
    "room-snapshot": {
      "url": "http://localhost:5006/mcp",
      "headers": { "X-API-Key": "your-api-key" }
    }
  }
}

Claude Code:

claude mcp add --transport http room-snapshot http://localhost:5006/mcp \
  --header "X-API-Key: your-api-key"

Clients without remote HTTP support can run the server locally over stdio; the pipe is trusted, so no API key is required:

python -m app.mcp_server --stdio
{
  "mcpServers": {
    "room-snapshot": {
      "command": "python",
      "args": ["-m", "app.mcp_server", "--stdio"],
      "cwd": "/path/to/room-snapshot-api"
    }
  }
}

Localization

The room code (kuh) is a stable, language-neutral identifier used by URLs, ACLs and integrations. Only the label is translated. Language resolution order: ?lang= → Accept-Language header → DEFAULT_LANG. /rooms keeps returning {code: name}; /rooms/<code> additionally includes names with every translation.

Because the default is en, existing Russian-language clients (for example FLAI) must send Accept-Language: ru or ?lang=ru to keep receiving Russian labels.

Monitoring

curl -H "X-API-Key: $API_KEY" http://localhost:5005/health | jq

status is healthy, degraded (some cameras unreachable) or unhealthy (none). The HTTP code stays 200 so a switched-off camera does not restart the container. docker compose logs -f shows JSON logs.

Docker

./install.sh               # first run: config, key, build, start, health check
docker compose build
docker compose up -d      # api on :5005, mcp on :5006
docker compose down

Development

python -m venv .venv && . .venv/bin/activate
pip install -r requirements-dev.txt
ruff check app tests
pytest -q

CI runs ruff and pytest on every push (.gitea/workflows/ci.yml).

Security notes

  • Credentials live in config/cameras.auth and .env, never in git or the image.
  • Every endpoint requires the API key; the service will not start without one.
  • Captures are capped by MAX_CONCURRENT_CAPTURES so one client cannot spawn an unbounded number of ffmpeg processes; set RATE_LIMIT_PER_MINUTE for a hard per-client request limit.
  • Requests to different cameras run in parallel; one slow stream does not block the rest.
  • Use HTTPS and a reverse proxy in production; camera credentials are URL-encoded and masked in API responses and logs.
  • Behind a reverse proxy the rate limit counts the proxy, not the client. With RATE_LIMIT_PER_MINUTE enabled, every client behind the same nginx/Caddy/Traefik shares one bucket: one busy client can return 429 to all of them. Either keep the application limit at 0 and limit requests in the proxy (limit_req in nginx), or enable trusted proxy headers. Do not trust X-Forwarded-For while ports 5005 and 5006 are reachable directly — a client could spoof any address and bypass the limit. The same applies to the MCP server and uvicorn's --proxy-headers.

Roadmap

  • MJPEG / WebSocket live streams
  • ONVIF camera discovery
  • Web viewing UI
  • Archive recording
  • Prometheus metrics

License

MIT — see LICENSE. Russian translation: LICENSE-ru.

↑↓ navigate openesc close