Skip to main content
Glama

vigi-nvr-mcp

An MCP server for a single TP-Link VIGI NVR on your own network. It speaks the NVR's local JSON API directly — no cloud, no browser, no vendor app — and exposes it to an AI agent or any MCP client as a set of safe, typed tools: read the device and its cameras, manage channels, and export recorded footage over RTSP. Reads are free; every write needs two separate opt-ins; and login is engineered around the NVR's account-lockout so an agent cannot lock you out of your own recordings.

WARNING

Account lockout is the main risk. VIGI NVRs lock the admin account after ~10 failed logins, and while locked you cannot reach the NVR at all, including its recordings. This server never auto-retries a login and, by default, stops all logins after a single failure until a human clears the breaker. Confirm your password with vigi-nvr-mcp --check-auth --login before pointing an agent at it. See Safety model.

Supported hardware

Tested model

VIGI NVR1016H(UN)

Tested firmware

1.1.3 Build 260727

Auth scheme (this firmware)

MD5 + RSA-PKCS1v15 (encrypt_type 2), falling back to bare MD5 (encrypt_type 1)

Other VIGI models and firmware very likely work, but the login scheme varies by firmware — this unit uses MD5+RSA, and newer firmware is known to use SHA-256-based schemes. The server auto-detects encrypt_type 1 vs 2, but if your firmware uses something else, login will fail. Verify first, without risking the lockout counter:

vigi-nvr-mcp --check-auth           # fetch the challenge only: offered schemes, counters; NO login
vigi-nvr-mcp --check-auth --login   # plus exactly ONE login attempt

The full wire protocol is documented in docs/protocol/vigi-nvr.md.

Related MCP server: @mgcrea/mcp-unifi-protect

Features

  • 45 tools over one NVR, all returning a uniform {success, data, error} envelope with credentials redacted.

  • Catalog gateway — nvr_call reaches every one of the 586 documented calls across 61 API modules through one validated, write-gated entry point, so coverage is the whole inventory rather than 586 hand-written tools.

  • Typed reads for the high-value calls: device/system/network info, users, storage and disks, recording status, video/image/detection config, firewall, cloud status, time.

  • Channel management — list bound cameras, group duplicates by device UUID, flag stale "ghost" channels, and plan/execute cleanup (remove, move) with per-write UUID checks.

  • Footage over RTSP — enable ONVIF/RTSP, build redacted stream URLs, export replay windows to mp4 (lossless -c copy, or re-encode to a size target), grab snapshots, build time-stamped contact sheets, sample frames, and run a full "find the footage and send it" investigation recipe.

  • MCP resources — the export directory is also exposed as exports://<name> blobs for clients that support resources.

Install

python -m venv .venv && . .venv/bin/activate    # Windows: .venv\Scripts\activate
pip install .                                    # or: pip install -e '.[dev]'
  • Python 3.11+. Runtime deps: mcp (1.x, FastMCP), httpx, cryptography, pydantic, python-dotenv, tzdata.

  • ffmpeg/ffprobe are optional and only needed for the media/export tools. If they are not found, those tools return a MEDIA_UNAVAILABLE envelope instead of failing; everything else works without them. Point VIGI_NVR_FFMPEG at the binary if it is not on PATH.

Configuration

Configuration comes only from environment variables (a .env in the working directory is loaded if present). Copy .env.example and never commit your .env. Everything is validated once at startup; a bad value stops the server with a message that names the variable but never prints its value.

NVR connection

Variable

Default

Meaning

VIGI_NVR_HOST

(required)

NVR hostname or IP (no scheme/port/path)

VIGI_NVR_PORT

443

HTTPS API port

VIGI_NVR_USERNAME

admin

Login username

VIGI_NVR_PASSWORD

(required)

Login password (1–128 chars)

VIGI_NVR_VERIFY_TLS

false

NVRs ship self-signed certs, so off by default

VIGI_NVR_TLS_FINGERPRINT_SHA256

(unset)

Pin the cert by SHA-256 (64 hex, colons optional); fails closed on mismatch. Observe the value via nvr_status / --check-auth (tls_fingerprint_observed)

VIGI_NVR_TIMEOUT_SECONDS

10

Per-request timeout, 1–120

Safety switches

Variable

Default

Meaning

VIGI_NVR_ALLOW_WRITES

false

First write gate (env). Writes also need confirm_write: true per call

VIGI_NVR_DRY_RUN

false

Writes return the exact request without sending it

VIGI_NVR_LOGIN_DISABLED

false

Freeze authentication before any network I/O

VIGI_NVR_MAX_LOGIN_FAILURES

1

Per-host failure budget before the breaker trips, 1–5

VIGI_NVR_BACKUP_DIR

backups

Where nvr_backup_config writes (dir/files 0600)

VIGI_NVR_STATE_DIR

~/.local/state/vigi-nvr-mcp/

Persistent login-breaker state (per host, 0600)

RTSP / media export

Variable

Default

Meaning

VIGI_NVR_RTSP_PORT

554

RTSP/ONVIF port probed by nvr_get_rtsp_status

VIGI_NVR_RTSP_USERNAME

(NVR username)

RTSP account; defaults to VIGI_NVR_USERNAME

VIGI_NVR_RTSP_PASSWORD

(NVR password)

RTSP password; defaults to VIGI_NVR_PASSWORD

VIGI_NVR_EXPORT_DIR

~/.local/share/vigi-nvr-mcp/exports

Clip/snapshot output (dir 0700)

VIGI_NVR_EXPORT_MAX_MINUTES

60

Longest export window, 1–1440

VIGI_NVR_EXPORT_RETENTION_DAYS

7

Age after which nvr_purge_exports deletes a clip, 1–3650

VIGI_NVR_FFMPEG

(PATH)

Path to ffmpeg if not on PATH (ffprobe is found beside it)

MCP server

Variable

Default

Meaning

VIGI_MCP_TRANSPORT

stdio

stdio or streamable-http

VIGI_MCP_HOST

127.0.0.1

IP literal to bind for HTTP (keep it loopback)

VIGI_MCP_PORT

8765

HTTP port

VIGI_MCP_LOG_LEVEL

INFO

Log level; logs go to stderr

Any unknown VIGI_NVR_* / VIGI_MCP_* variable is rejected at startup (a typo must not silently leave a write gate or TLS verification in an unintended state).

Running

stdio (Claude Desktop / Claude Code)

{
  "mcpServers": {
    "vigi-nvr": {
      "command": "/path/to/.venv/bin/vigi-nvr-mcp",
      "env": { "VIGI_NVR_HOST": "192.0.2.1", "VIGI_NVR_PASSWORD": "<password>" }
    }
  }
}

streamable-HTTP (hosting)

VIGI_MCP_TRANSPORT=streamable-http vigi-nvr-mcp   # http://127.0.0.1:8765/mcp
IMPORTANT

The server hasno authentication of its own. Keep it bound to loopback and reach it over Tailscale or an SSH tunnel — never bind it to the LAN. A hardened systemd example and install guide are in deploy/.

vigi-nvr-mcp --list-tools prints the tool names without touching any device.

Safety model

The whole point of this server is to be safe to hand to an autonomous agent.

  • Reads are free. Any get call runs without extra gates.

  • Writes are double-gated. Every mutating call needs both VIGI_NVR_ALLOW_WRITES=true on the server and confirm_write: true (the literal JSON boolean) on the call. The confirm_write parameter is typed, so a truthy string or number ("true", "1", 1, "yes") is collapsed to false at the MCP boundary and refused with a WRITE_REFUSED envelope and no network call — it is never coerced through the gate. Channel writes add three more checks: expected_uuid must match the live row (re-read just before writing); remove refuses a live (online=="1") row unless force=true; move refuses an occupied target. Guarded writes are serialised so two cannot interleave.

  • Dry-run. VIGI_NVR_DRY_RUN=true makes every write return {"dry_run": true, "request": <exact body>} and send nothing. Dry-run lives in the one guarded-write executor every mutating tool shares, so no write path can forget it.

  • Lockout breaker. A failed login is never retried. After VIGI_NVR_MAX_LOGIN_FAILURES failures (default 1) the breaker trips: every failure/success is reserved-then-recorded under a cross-process lock in a per-host JSON file under VIGI_NVR_STATE_DIR (mode 0600), so even a crash-loop with bad credentials cannot keep spending the device's login budget. "Tripped" means a sticky flag is set; once tripped, no login is sent until a human clears it — raising the budget afterwards does not silently un-trip it. Inspect or clear it:

    vigi-nvr-mcp breaker --show     # state, failures, whether it is tripped
    vigi-nvr-mcp breaker --clear    # reset after you have fixed the credentials

    A corrupt or unwritable state file fails closed (refuses logins). VIGI_NVR_LOGIN_DISABLED=true freezes authentication before any network I/O. The credential-free challenge/reachability probe stays allowed either way.

  • TLS pinning (TOFU). Set VIGI_NVR_TLS_FINGERPRINT_SHA256 to pin the NVR's self-signed certificate; the pin is enforced on this server's own connection and fails closed on mismatch. On an unpinned connection, nvr_status / --check-auth report the observed fingerprint (tls_fingerprint_observed) so you can trust-on-first-use and paste it back into your config.

  • Redaction. Every tool result is recursively stripped of credential-bearing fields by a rule-based matcher (keys equal to ciphertext, stok, token, nonce, cookie, secret, authorization, pubkey, key, or containing pass/pwd/secret/token/cipher, or ending _key). No tool can return camera or session credentials. Request bodies are never logged; session tokens are masked in every log line, including httpx's own.

  • Exit codes (so a shell script can branch): 0 success · 1 auth failed · 2 config error · 3 lockout / breaker open / login disabled · 4 transport error.

Tools

Every tool returns {"success": bool, "data": ..., "error": {code, message, details} | null}. "Kind" marks whether a tool mutates; a write needs both write gates.

Status & auth

Tool

Kind

Description

nvr_status

read

Healthcheck: reachability, offered auth scheme, lockout counters, session state, safety policy, observed TLS fingerprint. Never logs in

nvr_auth_status

local

Session state and last-failure counters. No I/O

nvr_login

auth

Explicit single login attempt (never retried)

Catalog gateway

Tool

Kind

Description

nvr_list_modules

read

Every API module with its call/mutating counts

nvr_list_calls

read

Catalogued calls for one module

nvr_describe_call

read

One call's spec: example params, whether it mutates, response-shape hint

nvr_call

read/write

Catalogued gateway (module, method, key, params); login/user_management always refused

nvr_raw_call

read/write

Off-catalog escape hatch (method, module, params); write-gated unless get

Device & network

Tool

Kind

Description

nvr_get_device_info

read

Model, firmware, identity

nvr_get_module_spec

read

Capability spec: max channels, codecs, feature flags

nvr_get_system_info

read

Device name, time zone, session timeout

nvr_get_network_info

read

IP/mask/gateway/DNS, service ports

nvr_get_video_resolutions

read

Main/minor stream resolution tables

nvr_get_video_config

read

Per-channel resolution, codec, bitrate

nvr_get_image_config

read

Image, OSD, privacy-mask, ROI config for one channel

nvr_get_detection_config

read

Detection config for a channel and detection kind

nvr_get_users

read

User account names and groups only (never credentials)

nvr_get_firewall

read

Allow/deny lists and service exposure

nvr_get_cloud_status

read

TP-Link ID binding/connection status

nvr_get_time

read

Device time, time zone, NTP, DST

Channels

Tool

Kind

Description

nvr_list_channels

read

All bound cameras (chm added_dev); credentials redacted

nvr_get_channel

read

One channel by id

nvr_find_duplicate_channels

read

Group by device uuid; flag offline/disconnected rows as stale ghosts

nvr_plan_channel_cleanup

read

Ordered, resumable cleanup plan (back up, remove ghosts, re-home real cameras)

nvr_backup_config

read

Download the config backup (download_conf); not write-gated, so run it before any cleanup

nvr_remove_channel

write

Unbind a channel (chm_del_dev); refuses a live row unless force=true

nvr_move_channel

write

Move a binding to an empty slot (chm_mod_dev_chn), keeping its settings

nvr_set_channel_credentials

write

Re-authenticate a bound camera by pushing a username + RSA-encrypted password (chm_edit_dev); polls until it reconnects

nvr_renumber_channel

write

Re-point a bound channel to a new camera IP (delete + re-add + rename, since chm_edit_dev ignores ip); DESTRUCTIVE and the channel id changes. Camera must already be reachable at the new IP

Storage & recording

Tool

Kind

Description

nvr_list_disks

read

Installed hard disks and state (raw reply)

nvr_get_storage

read

Disks (status/capacity/free/health) plus overwrite policy

nvr_get_recording_status

read

Recording/storage policy status

nvr_list_recording_segments

read

Typed recording timeline for a channel/day (nvr_search_recordings is an alias)

nvr_search_recordings

read

Alias of nvr_list_recording_segments

nvr_list_events

read

Best-effort recent events/alerts (disk, network, video-loss, smart)

Media / export

Tool

Kind

Description

nvr_get_rtsp_status

read

Whether ONVIF/RTSP is enabled, plus a TCP probe of the RTSP port

nvr_enable_rtsp

write

One-time ONVIF/RTSP enable (onvif_server set)

nvr_get_stream_url

read

Redacted live RTSP URL + which env vars hold the credentials

nvr_export_clip

write (local)

Export a replay window to mp4 (-c copy; target_max_mb/max_width re-encode to fit)

nvr_snapshot

read

One JPEG frame from a live stream into the export dir

nvr_list_exports

read

List exports with age and next-purge time

nvr_delete_export

write (local)

Delete one export by name (path-confined)

nvr_get_export

read

Return a file's content as base64 (≤ max_mb, else TOO_LARGE)

nvr_purge_exports

write (local)

Delete exports older than the retention window (double-gated, dry-run)

Investigation

Tool

Kind

Description

nvr_list_motion_windows

read

Merged, de-duplicated activity windows across the timeline and event logs

nvr_contact_sheet

read

One JPEG grid of time-stamped frames across a window, with a tile→time map

nvr_sample_frames

read

Individual JPEG frames at an interval across a window

The export directory is also exposed as MCP resources (exports://<name>, blob content, path-confined) for clients that support them.

Recipes

Placeholder names (Front Door, dates, channel numbers) stand in for yours.

Channel cleanup (remove ghosts, re-home cameras)

Over time a VIGI NVR accumulates ghost channels — stale, offline duplicates of a camera that was re-added or moved. This cleans them up safely.

  1. Back up first (not write-gated, so always safe):

    nvr_backup_config
  2. See the duplicates. Groups channels by device UUID and flags the ghosts:

    nvr_find_duplicate_channels
  3. Get an ordered plan. Read-only; produces a resumable step list (ghosts first, then moves, one at a time, re-read after each):

    nvr_plan_channel_cleanup
  4. Dry-run the plan. With VIGI_NVR_DRY_RUN=true, run each step's tool to see the exact request body without sending it.

  5. Execute one step at a time, re-listing after each and stopping on any surprise (needs VIGI_NVR_ALLOW_WRITES=true):

    nvr_remove_channel(channel_id=13, expected_uuid="<uuid>", confirm_write=true)
    nvr_move_channel(from_id=9, to_id=1, expected_uuid="<uuid>", confirm_write=true)
  6. Verify: nvr_list_channels and nvr_get_recording_status.

Footage investigation ("find when a car came to the front door yesterday")

The MCP is the data plane; your agent (with vision and a mail tool) does the looking, deciding and sending.

  1. Find activity windows from the NVR's own timeline, not raw video:

    nvr_list_motion_windows(channel="Front Door", date="2026-10-02",
                            kinds=["motion","smart","alarm"])

    Returns merged [{channel, name, start, end, duration_s, kinds, sources}].

  2. Look at each window cheaply. One contact sheet per window; each tile has its exact time burned in, so a hit can be cited to the second:

    nvr_contact_sheet(channel=5, start="2026-10-02T14:03:00Z",
                      end="2026-10-02T14:05:00Z", cols=4, rows=3)
  3. Pin the moment. On a hit, pull individual frames:

    nvr_sample_frames(channel=5, start="2026-10-02T14:03:40Z",
                      end="2026-10-02T14:04:10Z", every_s=1)
  4. Export a mail-sized clip (re-encode to fit, e.g. 20 MB under Gmail's ~25 MB cap; needs VIGI_NVR_ALLOW_WRITES=true):

    nvr_export_clip(channel=5, start="2026-10-02T14:03:50Z",
                    end="2026-10-02T14:04:05Z", target_max_mb=20, confirm_write=true)
  5. Retrieve and send. Fetch the bytes (or read the exports:// resource) and hand them to the agent's mail tool:

    nvr_get_export(name="ch5_...mp4", max_mb=20)

Exports live on the server host (VIGI_NVR_EXPORT_DIR), not on the agent's machine; nvr_get_export and the exports:// resources are how a remote agent pulls them. Housekeeping runs through nvr_purge_exports.

Limitations

  • Local-monitor layout is not exposed. The NVR's own HDMI/VGA display grid and view layout are not reachable through this server.

  • Video is delivered over RTSP, not JSON. Clips and frames come from the RTSP endpoints via ffmpeg; there is no JSON "give me the video" call. The media tools therefore need ffmpeg/ffprobe.

  • RTSP credentials are visible to local ps. ffmpeg receives the RTSP URL with credentials in its userinfo, so they appear in that process's command line on the host running this server. Every URL the server returns or logs is redacted, but the plaintext is unavoidable in the ffmpeg invocation — use a dedicated viewer account and a single-admin box.

  • Some response shapes are INFERRED until a first live run. These were derived statically from the 1.1.3 web client and have not yet been confirmed against a live device; the parsers return a PROTOCOL_ERROR envelope (never a crash) on an unexpected shape:

    • nvr_list_recording_segments / nvr_search_recordings (the playback search timeline)

    • nvr_list_events (event/alarm lists)

    • the merge inputs for nvr_list_motion_windows The channel and backup primitives, auth, and the catalog counts are verified.

Troubleshooting

  • -40401 everywhere. This one code means three things depending on the path: the credential-free challenge reply (normal), a failed login (with a rising attempt counter), or an expired session token on /stok=.../ds (the client re-logs in once automatically). If --check-auth --login returns -40401 with attempts_left dropping, the password is wrong — stop before you hit the lockout.

  • Account locked (-40404 / -40408). Too many failed logins. Wait out the lock (or power-cycle per your firmware), fix the password, then vigi-nvr-mcp breaker --clear. -40408 means locked until manual intervention.

  • Nonce invalid (-40410). The challenge nonce went stale between fetch and login; fetch a fresh challenge and try once more.

  • Logins refused with no network call. The breaker is tripped or login is disabled. Run vigi-nvr-mcp breaker --show; clear it with breaker --clear after fixing credentials, and check VIGI_NVR_LOGIN_DISABLED.

  • RTSP tools fail or return nothing. Check nvr_get_rtsp_status: if RTSP is off or port 554 is closed, run nvr_enable_rtsp(confirm_write=true) once. If tools return MEDIA_UNAVAILABLE, ffmpeg/ffprobe are not found — install them or set VIGI_NVR_FFMPEG. An empty replay window surfaces as NO_FOOTAGE_IN_WINDOW.

  • Login scheme mismatch. If --check-auth shows an encrypt_type this server does not implement, your firmware uses a different login scheme (e.g. SHA-256). See docs/protocol/vigi-nvr.md.

Prior art

tools/capture-login/ is an optional, isolated, one-time dev utility (Node + puppeteer-core) that records the login envelopes the vendor web UI itself sends, on a device you own. It is not part of the server — the server imports nothing from it and runs no browser. See its README.

Development

pip install -e '.[dev]'
python scripts/gate.py   # ruff, pytest + coverage (core >= 90%, package >= 80%),
                         # secret scan, stub scan, --list-tools; prints PASS/FAIL
  • All tests run against mocks; nothing touches a device. The gate must print PASS before every commit.

  • vigi_nvr_mcp/core/ is a self-contained, device-agnostic template that sibling projects copy verbatim; a core/VERSION manifest plus tests/test_core_identity.py fail the gate if a copy drifts. Edit core/ only in this repo (see CONTRIBUTING.md).

  • Design docs: the specs (master plan, NVR spec, breaker protocol, breaker root-cause) and the wire protocol.

  • Security reporting and the threat model: SECURITY.md. Release history: CHANGELOG.md.

License

MIT — see LICENSE. Not affiliated with or endorsed by TP-Link. "TP-Link" and "VIGI" are trademarks of their owner.

Available Tools

45 tools
nvr_auth_statusA

NVR session state: authenticated, failed logins this process, the last failure's lockout counters and whether login is frozen. No network I/O.

Inputs: no named parameters. Returns: object

nvr_backup_configA

Download the NVR configuration backup to the server's backup directory (mode 0600). Read-only on the NVR; run this before any channel cleanup.

Inputs: no named parameters. Returns: object

nvr_callA

Catalogued gateway to any NVR call. Identify the call with (module, method, key) from nvr_list_calls/nvr_describe_call; pass the module body as params. Unknown calls are refused with the nearest matches. Anything the catalog flags as mutating, or whose method is not "get", needs VIGI_NVR_ALLOW_WRITES=true AND confirm_write=true. Set allow_extra=true to send keys outside the call's example. With VIGI_NVR_DRY_RUN the exact body is returned as data.request (and data.dry_run=true) and not sent. Credential fields in the reply are redacted.

Inputs: key (required), method (required), module (required), params, +2 more. Returns: object

nvr_contact_sheetA

One JPEG contact sheet of cols x rows frames evenly sampled across a replay window (ISO-8601 start/end, UTC offset required), each tile's exact time burned in. Read-only (reads frames into the export dir). Returns the file info plus a tile->timestamp map so an agent can cite the moment. Needs RTSP enabled and ffmpeg available.

Inputs: end (required), start (required), channel (required), cols, +3 more. Returns: object

nvr_delete_exportA

Delete one file from the export directory by name (path-confined; the name must be a generated export filename). WRITE: needs VIGI_NVR_ALLOW_WRITES=true and confirm_write=true.

Inputs: name (required), confirm_write. Returns: object

nvr_describe_callB

Describe one call by (module, method, key): its example parameters, whether it mutates and its response shape. Read-only.

Inputs: key (required), method (required), module (required). Returns: object

nvr_enable_rtspA

Enable ONVIF/RTSP on the NVR (onvif_server set). One-time setup. WRITE: needs VIGI_NVR_ALLOW_WRITES=true and confirm_write=true. Honours VIGI_NVR_DRY_RUN. Re-reads and returns enabled_before/after.

Inputs: confirm_write. Returns: object

nvr_export_clipA

Export a replay window (ISO-8601 start/end, UTC offset required) to an mp4 in the export dir. By default uses ffmpeg -c copy (lossless). Writes to local disk, so it is gated like a write: VIGI_NVR_ALLOW_WRITES=true AND confirm_write=true. VIGI_NVR_DRY_RUN returns the argv with the URL redacted. Window <= VIGI_NVR_EXPORT_MAX_MINUTES. Set target_max_mb (e.g. 20 for Gmail) and/or max_width to re-encode with libx264 (CRF/scale chosen from duration and target) so the file fits; then also returns original_bytes, encoded_bytes and fits_target. Returns path, bytes, duration_s, sha256 and a redacted ffmpeg stderr tail. An empty window surfaces as NO_FOOTAGE_IN_WINDOW.

Inputs: end (required), start (required), channel (required), stream, +3 more. Returns: object

nvr_find_duplicate_channelsB

Group channels by device uuid and flag duplicates. Within a group, rows with online="0" or conn_status!="0" are marked stale (removal candidates).

Inputs: no named parameters. Returns: object

nvr_get_channelB

One channel by id (credential fields redacted).

Inputs: channel_id (required). Returns: object

nvr_get_cloud_statusA

TP-Link cloud (TP-Link ID) binding and connection status (read-only). include_raw=true adds the raw reply at data.raw.

Inputs: include_raw. Returns: object

nvr_get_detection_configA

Detection configuration for one channel (1-16) and one detection kind (read-only). kind is one of: motion, people, vehicle, linecross, intrusion, region_entrance, region_exiting, loitering, abandon_and_taken, scene_change, audio_exception, tamper. include_raw=true adds the raw device reply under data.raw. Refuses an unknown kind or an out-of-range channel.

Inputs: kind (required), channel (required), include_raw. Returns: object

nvr_get_device_infoB

NVR model, firmware and identity (read-only).

Inputs: no named parameters. Returns: object

nvr_get_exportA

Return one export file's content as base64 for files up to max_mb (default 20; Gmail caps attachments near 25 MB). Over the limit returns TOO_LARGE with the size and a hint to re-export with target_max_mb or fetch the exports:// resource. Path-confined to the export dir.

Inputs: name (required), max_mb. Returns: object

nvr_get_firewallA

Firewall configuration: allow/deny lists and protocol/service exposure (read-only). include_raw=true adds the raw reply at data.raw.

Inputs: include_raw. Returns: object

nvr_get_image_configA

Image, OSD, privacy-mask (cover) and ROI configuration for one channel (1-16), read-only. include_raw=true adds the raw device reply at data.raw.

Inputs: channel (required), include_raw. Returns: object

nvr_get_module_specC

Capability spec: max channels, codecs, feature flags (read-only).

Inputs: no named parameters. Returns: object

nvr_get_network_infoB

NVR network configuration (read-only).

Inputs: no named parameters. Returns: object

nvr_get_recording_statusB

Recording / storage policy status (read-only, raw reply).

Inputs: no named parameters. Returns: object

nvr_get_rtsp_statusA

Report whether ONVIF/RTSP is enabled (onvif_server.onvif.enabled) and whether the RTSP port (default 554, VIGI_NVR_RTSP_PORT) accepts TCP connections. Read-only.

Inputs: no named parameters. Returns: object

nvr_get_storageB

Disks (status, capacity, free space, health) plus the overwrite and recording-plan policy (read-only). include_raw=true adds the raw reply at data.raw.

Inputs: include_raw. Returns: object

nvr_get_stream_urlA

Redacted live RTSP URL for a channel (1-16), stream 1=main/2=sub, plus a hint naming the env vars that hold the credentials. The password is never returned. Read-only.

Inputs: channel (required), stream. Returns: object

nvr_get_system_infoB

System basics such as device name, time zone and session timeout (read-only).

Inputs: no named parameters. Returns: object

nvr_get_timeA

Device time, time zone, NTP and DST configuration (read-only). include_raw=true adds the raw reply at data.raw.

Inputs: include_raw. Returns: object

nvr_get_usersA

NVR user accounts: names and groups only (credentials are never returned), read-only. include_raw=true adds the redacted raw reply at data.raw.

Inputs: include_raw. Returns: object

nvr_get_video_configA

Per-channel video stream configuration: main/minor resolution, codec and bitrate tables plus advanced encoder settings (read-only). Pass include_raw=true to also get the unprocessed device reply under data.raw.

Inputs: include_raw. Returns: object

nvr_get_video_resolutionsB

Main/minor stream resolution tables (read-only).

Inputs: no named parameters. Returns: object

nvr_list_callsB

List the catalogued calls for one module (method, key, whether it mutates, an example and a response-shape hint). Read-only.

Inputs: module (required). Returns: object

nvr_list_channelsB

All bound NVR channels (chm added_dev). Credential fields always redacted.

Inputs: no named parameters. Returns: object

nvr_list_disksB

Installed hard disks and their state (read-only, raw reply).

Inputs: no named parameters. Returns: object

nvr_list_eventsB

Best-effort list of recent NVR events/alerts (disk, network, video-loss and similar), read-only. Pass since as an ISO-8601 timestamp to drop older events that carry a parseable time. include_raw=true adds the raw reply at data.raw.

Inputs: since, include_raw. Returns: object

nvr_list_exportsB

List mp4/jpg files in the export directory (name, bytes, mtime). Read-only.

Inputs: no named parameters. Returns: object

nvr_list_modulesA

List every API module the NVR exposes, with per-module call and mutating-call counts (read-only; no device I/O).

Inputs: no named parameters. Returns: object

nvr_list_motion_windowsA

Merged, de-duplicated activity windows for one channel (1-16) or "all", read-only. Combines the typed recording timeline with event logs, merging spans that overlap or sit within min_gap_s and dropping windows shorter than min_len_s. Provide date (YYYY-MM-DD) or since (ISO-8601); optionally narrow with since/until. kinds defaults to motion/smart/alarm. Returns sorted windows [{channel, name, start, end, duration_s, kinds, sources}] and a per-channel summary.

Inputs: tz, date, kinds, since, +4 more. Returns: object

nvr_list_recording_segmentsA

Typed recording timeline for one channel (1-16) on one day (YYYY-MM-DD), read-only: a list of {start, end, type, raw_type} where type is normal / motion / smart / manual / alarm / unknown. tz is 'local', 'utc' or an IANA zone. include_raw=true adds the raw device reply at data.raw. On an unrecognised reply shape returns PROTOCOL_ERROR with the payload under data.raw (the live run corrects the shape).

Inputs: date (required), channel (required), tz, include_raw. Returns: object

nvr_loginA

Explicitly log in to the NVR (exactly one attempt, never retried).

Normally unnecessary: the first NVR tool call logs in. After any failed login, automatic logins stop; fix the cause before calling this. Once the server's failure budget is spent it refuses until restarted.

Inputs: no named parameters. Returns: object

nvr_move_channelA

Move a binding to another channel slot (chm_mod_dev_chn), keeping its credentials and settings. Refuses if new_id is occupied (the firmware would replace it). Same gates as nvr_remove_channel. Returns before/after rows.

Inputs: new_id (required), old_id (required), expected_uuid (required), confirm_write. Returns: object

nvr_plan_channel_cleanupA

Read-only. Produce an ordered, resumable cleanup plan: back up config, remove every ghost (one call each), re-read, then move each real camera stranded above slot 8 into the lowest confirmed-empty low slot. Each step lists the exact tool, arguments and the precondition to verify from the previous step; also returns a summary (counts, final layout) and an unsafe_if list of conditions that block moves (two real cameras share a uuid, more than 8 real cameras, an online ghost). Nothing is written; hand each step to the matching write tool yourself.

Inputs: no named parameters. Returns: object

nvr_purge_exportsA

Delete exports older than VIGI_NVR_EXPORT_RETENTION_DAYS (default 7). Double-gated like any delete: needs VIGI_NVR_ALLOW_WRITES=true and confirm_write=true. VIGI_NVR_DRY_RUN=true lists what would be deleted without deleting.

Inputs: confirm_write. Returns: object

nvr_raw_callA

Off-catalog escape hatch: send {"method": method, module: params} directly. Prefer nvr_call. Everything but method="get" needs both write gates; the login and user_management modules are always refused. Logged at WARNING. Honours VIGI_NVR_DRY_RUN. Reply credentials are redacted.

Inputs: method (required), module (required), params, confirm_write. Returns: object

nvr_remove_channelA

Unbind one channel (chm_del_dev). DESTRUCTIVE. Requires VIGI_NVR_ALLOW_WRITES=true, confirm_write=true and expected_uuid equal to the live row's uuid. Refuses a row whose live online=="1" (a connected camera) unless force=true. Honours VIGI_NVR_DRY_RUN. Run nvr_backup_config first. Returns before/after rows.

Inputs: channel_id (required), expected_uuid (required), force, confirm_write. Returns: object

nvr_sample_framesA

Individual JPEG frames every every_s seconds across a replay window (ISO-8601 start/end, UTC offset required), up to max_frames. Read-only. Returns each frame's name, path and timestamp. Use after a contact-sheet hit to pin the exact moment.

Inputs: end (required), start (required), channel (required), stream, +2 more. Returns: object

nvr_search_recordingsA

Alias of nvr_list_recording_segments (kept for compatibility). Lists recorded segments for one channel (1-16) on one day (YYYY-MM-DD), read-only.

Inputs: date (required), channel (required), include_raw. Returns: object

nvr_snapshotA

Capture one JPEG frame from a channel's live stream into the export dir (read-only; stream defaults to 2=sub). Needs RTSP enabled and ffmpeg available. Returns path, bytes and sha256.

Inputs: channel (required), stream. Returns: object

nvr_statusA

Healthcheck: reachability, offered auth scheme, the device's lockout counters, local session state and safety policy. Never logs in.

Inputs: no named parameters. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.4/5.0

    Scored across 45 tools

    Disambiguation3/5

    Descriptions are detailed, but several clusters overlap: nvr_search_recordings is an explicit alias of nvr_list_recording_segments; nvr_get_storage subsumes nvr_list_disks and nvr_get_recording_status; nvr_get_device_info, nvr_get_system_info, and nvr_get_module_spec are adjacent info endpoints. The docs help, but misselection risk remains across the 45-tool surface.

    Naming Consistency4/5

    All names use the nvr_ prefix and snake_case, with a mostly predictable verb_noun pattern (nvr_get_*, nvr_list_*, nvr_remove_channel, nvr_move_channel). Minor deviations like nvr_status, nvr_snapshot, nvr_contact_sheet, nvr_login, and nvr_call are still readable and consistent in style.

    Tool Count2/5

    45 tools far exceeds the 25+ threshold for 'too many' and the surface feels over-expanded for the NVR domain. Many narrow read tools could be subsumed by the generic nvr_call gateway, and the explicit alias further inflates the count.

    Completeness4/5

    Read coverage is extensive and write paths exist for channel cleanup, RTSP enabling, export deletion/purging, and clip export. The nvr_call/nvr_raw_call gateways provide access to catalogued mutations for missing direct tools, though explicit add_channel, reboot, or firmware update are not surfaced as first-class tools.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      B
      maintenance
      Enables AI agents to manage Keenetic routers through the same RCI API used by the router's web interface, working directly over the local network without cloud involvement. It supports reading device statuses and executing configuration changes, with confirm, dry-run, and destructive-action safeguards.
      23
      PolyForm Noncommercial 1.0.0
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables AI agents to query and interact with Extra Low Voltage security hardware such as Hikvision and Dahua cameras and NVRs through typed, read-only-by-default MCP tools for device info, channel enumeration, and JPEG snapshot capture. Translates proprietary ISAPI and CGI protocols into safe, auditable calls that any MCP client can use over stdio.
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables LLM/MCP clients to read and, when explicitly allowed, modify TP-Link Archer AX53 router settings directly over the LAN using the router's own web protocol, without a browser or cloud account. It is read-first, write-gated, and lockout-aware, with tools for clients, DHCP, Wi-Fi, access control, presence history, and other router operations.
      MIT