vigi-nvr-mcp
This MCP server gives an AI agent safe, typed access to a single TP-Link VIGI NVR over its local JSON API (plus RTSP for video), with reads free and every write double-gated to protect against lockout and accidental changes.
Auth & health —
nvr_status,nvr_auth_status,nvr_login: check reachability, offered auth scheme, lockout counters, session state, TLS fingerprint (never auto-retries a login).Catalog gateway —
nvr_list_modules,nvr_list_calls,nvr_describe_call,nvr_call,nvr_raw_call: reach all 586 documented calls across 61 modules through one validated, write-gated entry point.Device & network reads — device info, module/capability spec, system, network, video resolution tables, video/image/detection config, users (names only), firewall, cloud status, time.
Channel management — list/get channels, find duplicate and ghost channels by device UUID, generate a read-only resumable cleanup plan, back up config, and remove/move channels with per-write UUID verification.
Storage & recording — disks, storage capacity/health and overwrite policy, recording status, typed recording timelines for a channel/day, recent events.
RTSP / media export — check and enable ONVIF/RTSP, build redacted stream URLs, export replay windows to mp4 (
-c copyor re-encode to a size/width target), snapshots, contact sheets, frame sampling, list/get/delete/purge exports, andexports://<name>MCP resources.Footage investigation — merge timeline + event logs into de-duplicated motion windows, then view them cheaply via contact sheets and sample frames to pin the exact moment before exporting a mail-sized clip.
Safety guarantees — reads need no gates; writes need
VIGI_NVR_ALLOW_WRITES=trueplus a real JSON booleanconfirm_write=true; dry-run mode; credential redaction; and a persistent per-host lockout breaker (breaker --show/--clear) that fails closed.
Talks directly to a TP-Link VIGI NVR's local JSON API, exposing its device and camera data to an agent. Provides typed read tools for device/system/network info, users, storage and disks, recording status, video/image/detection config, firewall, cloud status and time; channel management (listing bound cameras, grouping duplicates by device UUID, flagging and cleaning up stale ghost channels); a write-gated catalog gateway reaching 586 documented API calls across 61 modules; and RTSP/ONVIF footage workflows including stream URL building, exporting replay windows to mp4, snapshots, contact sheets and frame sampling.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@vigi-nvr-mcp list my NVR cameras and check storage health"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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.
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 ( |
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 attemptThe 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_callreaches 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/ffprobeare optional and only needed for the media/export tools. If they are not found, those tools return aMEDIA_UNAVAILABLEenvelope instead of failing; everything else works without them. PointVIGI_NVR_FFMPEGat the binary if it is not onPATH.
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 |
| (required) | NVR hostname or IP (no scheme/port/path) |
|
| HTTPS API port |
|
| Login username |
| (required) | Login password (1–128 chars) |
|
| NVRs ship self-signed certs, so off by default |
| (unset) | Pin the cert by SHA-256 (64 hex, colons optional); fails closed on mismatch. Observe the value via |
|
| Per-request timeout, 1–120 |
Safety switches
Variable | Default | Meaning |
|
| First write gate (env). Writes also need |
|
| Writes return the exact request without sending it |
|
| Freeze authentication before any network I/O |
|
| Per-host failure budget before the breaker trips, 1–5 |
|
| Where |
|
| Persistent login-breaker state (per host, 0600) |
RTSP / media export
Variable | Default | Meaning |
|
| RTSP/ONVIF port probed by |
| (NVR username) | RTSP account; defaults to |
| (NVR password) | RTSP password; defaults to |
|
| Clip/snapshot output (dir 0700) |
|
| Longest export window, 1–1440 |
|
| Age after which |
| (PATH) | Path to |
MCP server
Variable | Default | Meaning |
|
|
|
|
| IP literal to bind for HTTP (keep it loopback) |
|
| HTTP port |
|
| 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/mcpThe 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
getcall runs without extra gates.Writes are double-gated. Every mutating call needs both
VIGI_NVR_ALLOW_WRITES=trueon the server andconfirm_write: true(the literal JSON boolean) on the call. Theconfirm_writeparameter is typed, so a truthy string or number ("true","1",1,"yes") is collapsed tofalseat the MCP boundary and refused with aWRITE_REFUSEDenvelope and no network call — it is never coerced through the gate. Channel writes add three more checks:expected_uuidmust match the live row (re-read just before writing); remove refuses a live (online=="1") row unlessforce=true; move refuses an occupied target. Guarded writes are serialised so two cannot interleave.Dry-run.
VIGI_NVR_DRY_RUN=truemakes 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_FAILURESfailures (default 1) the breaker trips: every failure/success is reserved-then-recorded under a cross-process lock in a per-host JSON file underVIGI_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 credentialsA corrupt or unwritable state file fails closed (refuses logins).
VIGI_NVR_LOGIN_DISABLED=truefreezes authentication before any network I/O. The credential-free challenge/reachability probe stays allowed either way.TLS pinning (TOFU). Set
VIGI_NVR_TLS_FINGERPRINT_SHA256to 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-authreport 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 containingpass/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 |
| read | Healthcheck: reachability, offered auth scheme, lockout counters, session state, safety policy, observed TLS fingerprint. Never logs in |
| local | Session state and last-failure counters. No I/O |
| auth | Explicit single login attempt (never retried) |
Catalog gateway
Tool | Kind | Description |
| read | Every API module with its call/mutating counts |
| read | Catalogued calls for one module |
| read | One call's spec: example params, whether it mutates, response-shape hint |
| read/write | Catalogued gateway |
| read/write | Off-catalog escape hatch |
Device & network
Tool | Kind | Description |
| read | Model, firmware, identity |
| read | Capability spec: max channels, codecs, feature flags |
| read | Device name, time zone, session timeout |
| read | IP/mask/gateway/DNS, service ports |
| read | Main/minor stream resolution tables |
| read | Per-channel resolution, codec, bitrate |
| read | Image, OSD, privacy-mask, ROI config for one channel |
| read | Detection config for a channel and detection kind |
| read | User account names and groups only (never credentials) |
| read | Allow/deny lists and service exposure |
| read | TP-Link ID binding/connection status |
| read | Device time, time zone, NTP, DST |
Channels
Tool | Kind | Description |
| read | All bound cameras ( |
| read | One channel by id |
| read | Group by device |
| read | Ordered, resumable cleanup plan (back up, remove ghosts, re-home real cameras) |
| read | Download the config backup ( |
| write | Unbind a channel ( |
| write | Move a binding to an empty slot ( |
| write | Re-authenticate a bound camera by pushing a username + RSA-encrypted password ( |
| write | Re-point a bound channel to a new camera IP (delete + re-add + rename, since |
Storage & recording
Tool | Kind | Description |
| read | Installed hard disks and state (raw reply) |
| read | Disks (status/capacity/free/health) plus overwrite policy |
| read | Recording/storage policy status |
| read | Typed recording timeline for a channel/day ( |
| read | Alias of |
| read | Best-effort recent events/alerts (disk, network, video-loss, smart) |
Media / export
Tool | Kind | Description |
| read | Whether ONVIF/RTSP is enabled, plus a TCP probe of the RTSP port |
| write | One-time ONVIF/RTSP enable ( |
| read | Redacted live RTSP URL + which env vars hold the credentials |
| write (local) | Export a replay window to mp4 ( |
| read | One JPEG frame from a live stream into the export dir |
| read | List exports with age and next-purge time |
| write (local) | Delete one export by name (path-confined) |
| read | Return a file's content as base64 (≤ |
| write (local) | Delete exports older than the retention window (double-gated, dry-run) |
Investigation
Tool | Kind | Description |
| read | Merged, de-duplicated activity windows across the timeline and event logs |
| read | One JPEG grid of time-stamped frames across a window, with a tile→time map |
| 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.
Back up first (not write-gated, so always safe):
nvr_backup_configSee the duplicates. Groups channels by device UUID and flags the ghosts:
nvr_find_duplicate_channelsGet 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_cleanupDry-run the plan. With
VIGI_NVR_DRY_RUN=true, run each step's tool to see the exact request body without sending it.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)Verify:
nvr_list_channelsandnvr_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.
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}].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)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)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)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 needffmpeg/ffprobe.RTSP credentials are visible to local
ps.ffmpegreceives 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 theffmpeginvocation — use a dedicated viewer account and a single-admin box.Some response shapes are
INFERREDuntil 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 aPROTOCOL_ERRORenvelope (never a crash) on an unexpected shape:nvr_list_recording_segments/nvr_search_recordings(theplaybacksearch timeline)nvr_list_events(event/alarm lists)the merge inputs for
nvr_list_motion_windowsThe channel and backup primitives, auth, and the catalog counts are verified.
Troubleshooting
-40401everywhere. 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 --loginreturns-40401withattempts_leftdropping, 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, thenvigi-nvr-mcp breaker --clear.-40408means 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 withbreaker --clearafter fixing credentials, and checkVIGI_NVR_LOGIN_DISABLED.RTSP tools fail or return nothing. Check
nvr_get_rtsp_status: if RTSP is off or port 554 is closed, runnvr_enable_rtsp(confirm_write=true)once. If tools returnMEDIA_UNAVAILABLE,ffmpeg/ffprobeare not found — install them or setVIGI_NVR_FFMPEG. An empty replay window surfaces asNO_FOOTAGE_IN_WINDOW.Login scheme mismatch. If
--check-authshows anencrypt_typethis 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/FAILAll 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; acore/VERSIONmanifest plustests/test_core_identity.pyfail the gate if a copy drifts. Editcore/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 toolsnvr_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
Scored across 45 tools
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.
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.
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.
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
Related MCP Connectors
214 tools for AI agents over one MCP endpoint: image, video, audio, 3D, data, storage, mail, GPUs.
OCR, transcription, file extraction, and image generation for AI agents via MCP.
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Related MCP Servers
- AlicenseBqualityBmaintenanceEnables 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.23PolyForm Noncommercial 1.0.0
- AlicenseAqualityBmaintenanceEnables interacting with a self-hosted UniFi Protect console through natural language, covering cameras, recorded events, smart detections, snapshots, footage export, and connected devices, with optional write access.2267 npm1MIT
- AlicenseNot gradedqualityBmaintenanceEnables 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
- AlicenseNot gradedqualityBmaintenanceEnables 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