easy_onvif_mcp 0.1.0
easy_onvif_mcp: ^0.1.0 copied to clipboard
MCP server exposing ONVIF IP camera and NVR control (device management, media, PTZ, imaging, recordings) over stdio.
easy_onvif_mcp #
An MCP (Model Context Protocol) server that
exposes ONVIF IP cameras and NVRs as tools, built on
easy_onvif.
Point a model at a camera: list profiles, resolve RTSP and snapshot URIs, drive PTZ, read device information, and discover devices over WS-Discovery — without writing a single SOAP request by hand.
Why is there a second package? #
The server exists exactly once, in
lib/src/onvif_tools.dart + lib/src/onvif_mcp_server.dart, and neither file
touches dart:io. Two hosts then plug themselves in through the small
McpRuntime seam (stdio, stderr logging,
environment, HTTP client, discovery probe, credentials file):
| Host | Entry point | How you run it |
|---|---|---|
| Dart VM | bin/easy_onvif_mcp_server.dart |
dart pub global activate easy_onvif_mcp |
| Node.js | easy_onvif_mcp_js (this package compiled with dart compile js) |
npx easy-onvif-mcp |
Adding a tool means adding one entry to buildOnvifTools() — both hosts pick it
up. See test/tool_defs_test.dart, which checks the
hand-written schemas stay self-consistent.
Credentials #
Resolved lazily on the first tool call, environment winning over the config file:
ONVIF_HOST,ONVIF_USERNAME,ONVIF_PASSWORD$HOME/.onvif/credentials.json— the same fileeasy_onvif_clireads, soonvif authorizesets up this server too. Override the path withONVIF_CONFIG_FILE.- Or call the
onvif_connecttool explicitly.
ONVIF_HOST may carry a port (192.168.1.10:8080) or a scheme. Nothing is read
until a tool needs it, so the server always starts.
Client configuration #
{
"mcpServers": {
"onvif": {
"command": "npx",
"args": ["-y", "easy-onvif-mcp"],
"env": {
"ONVIF_HOST": "192.168.1.10",
"ONVIF_USERNAME": "admin",
"ONVIF_PASSWORD": "secret"
}
}
}
}
With the Dart VM binary, use "command": "dart", "args": ["run", "easy_onvif_mcp:easy_onvif_mcp_server"].
Tools #
52 tools, all prefixed onvif_. Media and PTZ tools take a profile_token;
get one from onvif_get_profiles. Run dart run example/serve_stdio.dart --list-tools to print the live list with each tool's required arguments.
Connection — onvif_connect, onvif_disconnect, onvif_is_connected,
onvif_status, onvif_probe
Device management (read-only) — onvif_get_device_information,
onvif_get_services, onvif_get_capabilities,
onvif_get_system_date_and_time, onvif_get_hostname, onvif_get_dns,
onvif_get_ntp, onvif_get_dynamic_dns, onvif_get_network_protocols,
onvif_get_users, onvif_get_relay_outputs,
onvif_get_storage_configurations, onvif_get_discovery_mode,
onvif_get_endpoint_reference
Media — onvif_get_profiles, onvif_get_profile, onvif_get_stream_uri,
onvif_get_snapshot_uri, onvif_get_video_sources,
onvif_get_audio_sources, onvif_get_media_service_capabilities
PTZ — onvif_ptz_get_status, onvif_ptz_get_presets,
onvif_ptz_get_configurations, onvif_ptz_goto_preset,
onvif_ptz_set_preset, onvif_ptz_remove_preset,
onvif_ptz_absolute_move, onvif_ptz_relative_move,
onvif_ptz_continuous_move, onvif_ptz_stop, onvif_ptz_move,
onvif_ptz_move_up, onvif_ptz_move_down, onvif_ptz_move_left,
onvif_ptz_move_right, onvif_ptz_zoom,
onvif_ptz_get_service_capabilities
Imaging (read-only) — onvif_imaging_get_status,
onvif_imaging_get_presets, onvif_imaging_get_current_preset
Recordings / replay / search — onvif_get_recordings,
onvif_get_recording_jobs, onvif_get_replay_uri,
onvif_get_recording_summary, onvif_find_recordings,
onvif_get_recording_search_results
What is deliberately not exposed #
Device-mutating operations — system_reboot, user creation/deletion, hostname,
DNS, NTP, network protocol and IP-filter writes — are not tools here. A
stdio server holding live camera credentials should not hand a model a reboot
button by default. Everything exposed is either a read or a camera movement.
Two reads the ONVIF spec defines have no counterpart in the easy_onvif API and
so are absent: GetScopes, and imaging options/settings beyond
ImagingStatus20 (onvif_imaging_get_status is the closest available).
Add them once the core exposes those requests — one ToolDef entry each.
Credentials are never echoed back #
onvif_connect deliberately skips dart_mcp's automatic argument validation
and validates in-handler. The library's validation failure message quotes the
rejected value — Value `12345` is not of type `String` at path #root["password"] — and numeric default passwords are common on ONVIF
cameras, so a mistyped credential would otherwise be written into the model's
transcript. Errors name the offending field only.
One device at a time #
Tool calls are serialized. easy_onvif's SOAP layer keeps its clock offset and
XML builder in static state (Transport.timeDelta, Transport.builder), so two
in-flight requests would corrupt each other's WS-Security timestamps and XML
fragments. Serializing keeps correctness at the cost of throughput, and means
this server talks to one device per process — connect to a second camera by
running a second server.
Diagnostics #
stdout is the JSON-RPC channel, so every log line, including SOAP failures and
stack traces, goes to stderr. Tool failures come back as a normal MCP result
with isError: true and a one-line message.
Development #
dart test
{ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'; sleep 2; } | dart run bin/easy_onvif_mcp_server.dart
The sleep matters: closing stdin ends the session, and a piped heredoc
closes before the async reply is written.