The most common question about an on-camera MCP server is: "aren't you opening my camera to hackers?" This document explains exactly what the server can and cannot do, and which guardrails are enforced in code.
- Create or modify users. There is no user-management tool. The
set_paramwrite allowlist (Image,ImageSource,Brand,Time,AudioSource,Event,Overlay) excludesUsers,Security,Network,Systemand everything else — an AI cannot change credentials, open ports, or alter network configuration, even indirectly. - Reach the internet. The server only talks to VAPIX on the camera's own loopback interface. Nothing leaves the LAN unless the operator deliberately tunnels it.
- Bypass camera authentication. The sanctioned endpoint
(
/local/axis_mcp/mcp) sits behind the camera's reverse proxy, which enforces admin digest auth — the camera itself, not this app, is the gatekeeper.
A three-position switch on the settings page decides what connected agents may do.
Every tool call is checked centrally (src/guardrails.ts) before the tool runs;
blocked calls return a clear "disabled by operator" error and are logged.
| Level | Adds |
|---|---|
| Read-only | Status, device info, params (read), snapshots, stream URLs, lists |
| Operate (default) | PTZ, focus, image settings, overlays, streams, I/O, recordings |
| Full | reboot_camera, factory_default, set_param, control_app |
Unknown/future tools default to Full (fail-closed).
- 60 tool calls per minute overall.
- 20 state-changing calls per minute.
- PTZ preset cap:
ptz_preset_saverefuses once the camera has 20 presets — an agent loop cannot create "400 PTZ presets".
reboot_camera and factory_default additionally require an explicit
confirm: true argument (and Full access level).
- The direct LAN port is disabled by default.
- Enabling it without a bearer token auto-generates one (shown once); the "open port, no auth" configuration can no longer be created from the UI.
- Legacy installs that had the port open without a token keep working but log a
prominent
SECURITY:warning on every start.
Every request, every MCP tool call, and every guardrail refusal appears in the Live Log on the settings page and is mirrored to the AXIS system log — the operator can always see exactly what an agent did.
The VAPIX account this app uses is stored as plain JSON in the package's own
writable data directory (PERSISTENT_DATA_PATH, i.e.
/usr/local/packages/edgemcp/localdata/settings.json), as is the direct
port's bearer token. They are not encrypted.
The exposure is bounded by the fact that reading that file already requires administrative access to the camera — SSH or an equivalent — and anyone who has that can obtain the same access directly. It is called out here because it is a real property of the design rather than an oversight: an ACAP has no device keystore to hold a secret an unattended service must be able to read back. Treat the account as scoped to this camera, and prefer a dedicated service account over a shared administrator login.
The settings endpoint never returns either secret — settings.cgi reports only
hasPassword and hasBearerToken booleans.
The optional direct MCP listener speaks plain HTTP, so its bearer token crosses
the LAN in the clear on every call. It is off by default, and enabling it
without a token generates one automatically. It is additionally protected
against DNS rebinding: a request carrying a browser Origin header that is not
loopback is refused with 403 before the token is compared. Requests with no
Origin at all are allowed, because the MCP clients this port exists for are
not browsers and never send one.
Where the network is not trusted, prefer the reverse-proxy path
(/local/edgemcp/mcp), which the camera itself protects with admin digest
authentication over the camera's own TLS.
Please open a private security advisory on GitHub (https://github.com/kotyzap/EdgeMCP-for-Axis/security) or contact the author directly rather than filing a public issue.