Skip to main content
Glama
kotyzap
by kotyzap

EdgeMCP for Axis (on-camera ACAP)

A standalone ACAP (.eap) that runs a Model Context Protocol server directly on an Axis camera (ARTPEC-8, AXIS OS 12.10–13 — e.g. Q1656). It exposes VAPIX-backed tools over Streamable HTTP so Claude Desktop / Claude Code can connect to the camera and inspect/control it.

It speaks both MCP protocol revisions from one endpoint: 2025-era clients over the stateless legacy path, and 2026-07-28 clients — which dropped the initialize handshake and protocol sessions entirely — natively, including server/discover and cacheable tools/list results.

Built with the ACAP Native SDK (Node.js 20 bundled into the package). No Docker runs on the camera — Docker is only used to build the .eap on your Mac.

2.0.0 removed the on-camera agent. 1.x bundled a 45M language model, speech-to-text and a neural voice alongside the MCP server — 89 % of the download and nine of the thirteen endpoints, inside a product called EdgeMCP. This is an MCP server and nothing else. The agent continues as its own project; its source was moved to Needle LLM on AXIS/src-extracted-from-edgemcp/.

The package id changed with it, needle_mcpedgemcp, so existing installs are not upgraded in place and client URLs move to /local/edgemcp/mcp.

Links: Project page · Releases (prebuilt .eap)

Layout

EdgeMCP-for-Axis/
├── Dockerfile              # ACAP Native SDK build (aarch64)
├── build.sh                # docker build + docker cp -> .eap
├── dev-loop.sh             # fast iteration: build, deploy, ask, tail
├── release.sh              # build + verify + notes -> releases/
└── app/
    ├── manifest.json        # schemaVersion 2.2.0; appName edgemcp
    ├── edgemcp              # launcher (filename == appName)
    ├── Makefile             # no-op (acap-build runs make)
    ├── LICENSE
    ├── package.json         # main: dist/bootstrap.js; `npm test` runs the suite
    ├── tsconfig.json
    ├── test/                 # node --test: guardrail/annotation invariants,
    │                         # manifest schema-v2 checks, and both protocol
    │                         # eras exercised over real HTTP
    ├── src/
    │   ├── bootstrap.ts     # HTTP servers: /mcp, /status.cgi, /settings.cgi;
    │   │                     # createMcpHandler per listener, origin guard
    │   ├── mcpServer.ts      # tool catalog (built once) + per-request server
    │   ├── guardrails.ts     # access levels, per-caller rate limits,
    │   │                     # allow-lists, and the tool annotations
    │   ├── version.ts        # app version, read from package.json
    │   ├── vapix.ts          # Digest auth client (MD5 + SHA-256)
    │   ├── settings.ts       # persisted config (PERSISTENT_DATA_PATH)
    │   └── tools/            # device, imaging, events, apps, system, ptz,
    │                         # io, storage, streaming, camstreamer,
    │                         # camswitcher, camoverlay, aoa, lpv, vmd4,
    │                         # queuemonitor, guardApps, legacyAnalytics,
    │                         # demographics, peoplecounter, overlay,
    │                         # zipstream, viewareas, imagetuning, ptzAdvanced,
    │                         # diagnostics, netops, streamingApis, audio,
    │                         # deviceConfig, recordingPipeline, pairing,
    │                         # dataHub, eventPull (shared ONVIF helper),
    │                         # controlCgiAcapFactory + legacyAcapFactory
    │                         # (shared ACAP factories) + deviceConfigApi
    │                         # (shared REST helper)
    └── html/
        └── index.html       # settings UI (light default + theme switcher)

Related MCP server: Device MCP Server

Requirements

Camera

ARTPEC-8 (or ARTPEC-7 for the armv7hf build)

AXIS OS

12.10 – 13 — the floor comes from ACAP manifest schema v2, which the package uses because AXIS OS 13 requires it

Build SDK

ACAP Native SDK 12.11.0 (schema 2.2.0 needs 12.11; older SDKs reject the manifest)

Runtime

Node.js 20, bundled into the package

Cameras below AXIS OS 12.10 are not supported. That is a consequence of schema v2, not a choice — schema v2 is mandatory for AXIS OS 13, and an ACAP that does not declare compatibleOsVersions is rolled back when the device is upgraded.

Protocol support

2025-era client

2026-07-28 client

Handshake

initialize

none — server/discover

Sessions

stateless

removed from the spec

tools/list

plain result

resultType, and a 1-hour cache hint so the list is not re-sent every turn

Served by

the SDK's stateless legacy path

natively

Both arrive at the same /mcp endpoint; nothing needs configuring per client.

Tools (269 across 34 modules)

Tool

VAPIX

get_device_info / get_device_properties

basicdeviceinfo.cgi (getAllProperties / getProperties)

get_system_status

param.cgi (Network/Brand/Firmware) + temperaturecontrol.cgi

get_time

time.cgi

get_view_areas

viewarea/info.cgi (param.cgi fallback)

take_snapshot

jpg/image.cgi → MCP image content

get_image_settings / set_image_settings

param.cgi ImageSource/Image

get_optics

opticscontrol.cgi getOptics (ids + capabilities)

autofocus

cascade: opticscontrol.cgi performAutofocus → startFocusSearch → opticssetup.cgi → ptz.cgi?autofocus=on (caches the working method)

set_zoom_focus

opticscontrol.cgi setMagnification / autofocus cascade

list_event_declarations

SOAP GetEventInstances

get_analytics_status

applications/list.cgi (filtered)

aoa_get_scenarios

/local/objectanalytics/control.cgi getConfiguration

aoa_get_object_counts

/local/objectanalytics/control.cgi getOccupancy / getAccumulatedCounts

aoa_get_triggered_alerts

SOAP pull-point subscription (CreatePullPointSubscription / PullMessages) filtered to ObjectAnalytics topics

list_apps / control_app

applications/list.cgi, control.cgi

get_params / set_param

param.cgi (writes allowlisted)

get_api_list / get_capabilities

apidiscovery.cgi, param.cgi Properties.API/PTZ

get_firmware_properties

firmwaremanagement.cgi getProperties

reboot_camera / factory_default

firmwaremanagement.cgi (legacy CGI fallback); both require confirm: true

ptz_move / ptz_relative_move / ptz_absolute_move / ptz_continuous_move

com/ptz.cgi

ptz_query / ptz_capabilities

com/ptz.cgi?query= / ?info=1

ptz_preset_list

com/ptz.cgi?query=presetposcamdata (falls back to presetposcam) — presets grouped per view area, with pan/tilt/zoom when reported

ptz_preset_goto / ptz_preset_save / ptz_preset_remove

com/ptz.cgi presets

io_set_output / io_get_inputs

io/output.cgi (word + symbol action fallback), io/port.cgi

storage_list_recordings / storage_start_recording / storage_stop_recording

record/list.cgi, record/record.cgi

get_rtsp_url / get_mjpeg_url / get_audio_urls

URL builders (no request)

get_audio_config

param.cgi AudioSource/AudioDevice/AudioOutput

CamStreamer suite (require the respective ACAP installed)

Tool

Endpoint

camstreamer_list_streams

/local/camstreamer/stream_list.cgi (param-tree fallback)

camstreamer_stream_status

/local/camstreamer/get_streamstat.cgi

camstreamer_control_stream

/local/camstreamer/set_stream_enabled.cgi (start/stop)

camswitcher_list_playlists

/local/camswitcher/playlists.cgi?action=get

camswitcher_switch_playlist

/local/camswitcher/playlist_switch.cgi

camswitcher_queue_playlist

/local/camswitcher/playlist_queue_push.cgi

camswitcher_get_queue / camswitcher_play_next / camswitcher_clear_queue

playlist_queue_*.cgi

camswitcher_output_info / camswitcher_list_clips

output_info.cgi, clips.cgi

camoverlay_list_services

/local/camoverlay/api/services.cgi

camoverlay_set_service_enabled

services.cgi?action=set

camoverlay_update_graphic_text

customGraphics.cgi?action=update_text

camoverlay_infoticker

infoticker.cgi

These call the CamStreamer/CamSwitcher/CamOverlay CGIs on the same camera using the same digest auth. If the corresponding ACAP isn't installed, the tool returns a clear "not installed" error.

Analytics ACAPs (require the respective ACAP installed)

Tool

Endpoint

lpv_get_push_config

/local/fflprapp/cloud[N].cgi (flat XML, base64 url/password decoded)

lpv_get_heartbeat_config

/local/fflprapp/config_hb.cgi

lpv_get_recent_plates

SOAP pull-point subscription filtered to ALPV.* topics (listens a few seconds — most ALPV events are one-shot, not stateful)

lpv_get_plate_image

/local/fflprapp/tools.cgi?action=getImage → MCP image content

vmd4_get_profiles

/local/vmd/control.cgi getConfiguration

vmd4_get_triggered_profiles

SOAP pull-point subscription filtered to CameraApplicationPlatform/VMD/ topics

queue_get_live_count

/local/queue/.api?live-sum-people.json

queue_get_params

/local/queue/.api?params.json

queue_get_history

/local/queue/.api?export-csv-minutes / export-csv-people (CSV parsed to rows)

aoa_get_triggered_alerts, vmd4_get_triggered_profiles and lpv_get_recent_plates all share one ONVIF pull-point helper (tools/eventPull.ts) — a stateful event topic (AOA/VMD4's "active" flag) reports its current value immediately on subscribe, so a short-lived subscription doubles as a one-shot state read. ALPV plate events are mostly stateless pulses, so that tool is a genuine "listen for N seconds" call instead.

Analytics ACAP siblings (Fence/Loitering/Motion Guard, legacy VMD/tracking apps, people counting)

Tool

Endpoint

fenceguard_get_profiles / fenceguard_get_triggered_profiles

/local/fenceguard/control.cgi (control.cgi factory)

loiteringguard_get_profiles / loiteringguard_get_triggered_profiles

/local/loiteringguard/control.cgi

motionguard_get_profiles / motionguard_get_triggered_profiles

/local/motionguard/control.cgi

crosslinedetection_get_config / crosslinedetection_get_triggered

/axis-cgi/vaconfig.cgi (legacy XML; stateless event, listens a few seconds)

digitalautotracking_get_config / digitalautotracking_get_triggered

/axis-cgi/vaconfig.cgi (legacy XML; stateful)

vmd21_get_config / vmd21_get_triggered

/axis-cgi/vaconfig.cgi (legacy XML; stateful) — deprecated, superseded by VMD4

vmd3_get_config / vmd3_get_triggered

/axis-cgi/vaconfig.cgi (legacy XML; stateful) — deprecated, superseded by VMD4

demographics_get_live_tracks / demographics_get_ended_tracks / demographics_get_live_and_ended_tracks / demographics_get_stats

/local/demographics/.api

peoplecounter_get_live_count / peoplecounter_get_line_position / peoplecounter_get_available_days / peoplecounter_get_history / peoplecounter_get_occupancy_history

/local/tvpc/.api

p8815_get_live_occupancy / p8815_get_foot_traffic_history / p8815_get_occupancy_history

/a3dpc/api/... (AXIS P8815-2 3D People Counter)

The Fence/Loitering/Motion Guard trio shares tools/controlCgiAcapFactory.ts (same control.cgi JSON-RPC shape as VMD4/AOA). The four legacy apps (Cross line detection 1.1, Digital autotracking, VMD 2.1, VMD 3) share tools/legacyAcapFactory.ts — their config is arbitrarily-nested XML, so the config tools return raw XML rather than guessing at a generic nested mapping.

Overlay / Zipstream / View Area (native VAPIX, no extra ACAP required)

Tool

Endpoint

overlay_list / overlay_get_capabilities

/axis-cgi/dynamicoverlay/dynamicoverlay.cgi (list, getOverlayCapabilities)

overlay_add_text / overlay_add_image / overlay_set_text / overlay_set_image / overlay_remove

dynamicoverlay.cgi (addText/addImage/setText/setImage/remove)

zipstream_get_status

/axis-cgi/zipstream/getstatus.cgi

zipstream_list_strengths / zipstream_set_strength

liststrengths.cgi / setstrength.cgi

zipstream_list_gop_modes / zipstream_set_gop

listgopmodes.cgi / setgop.cgi

zipstream_list_fps_modes / zipstream_set_fps_mode / zipstream_set_min_fps

listfpsmodes.cgi / setfpsmode.cgi / setminfps.cgi

zipstream_list_profiles / zipstream_set_profile

listprofiles.cgi / setprofile.cgi

viewarea_list / viewarea_set_geometry / viewarea_reset_geometry

/axis-cgi/viewarea/info.cgi, configure.cgi

Camera / image tuning

Tool

Endpoint

daynight_get_capabilities / daynight_get_configuration / daynight_set_configuration

/axis-cgi/daynight.cgi

light_get_information / light_get_status / light_set_active / light_set_intensity / light_get_current_intensity

/axis-cgi/lightcontrol.cgi

imagestab_get_capabilities / imagestab_get_configuration / imagestab_set_configuration

/axis-cgi/imagestabilization.cgi (EIS/OIS)

imagerotation_get / imagerotation_set

param.cgi ImageSource.I#.{Rotation,AutoRotationEnabled}

orientation_get

/axis-cgi/orientation/getlongitudinalvalue.cgi + getlateralvalue.cgi (gyro/accelerometer)

ratecontrol_get / ratecontrol_set

param.cgi Image.I#.RateControl (VBR/MBR/ABR)

capturemode_get_modes / capturemode_set_mode

/axis-cgi/capturemode.cgi

dewarp_get_view_modes / dewarp_set_camera_orientation

param.cgi Image.*/ImageSource.I0.CameraTiltOrientation (360°/180° cameras)

PTZ / patrol

Tool

Endpoint

guardtour_list / guardtour_create / guardtour_remove

param.cgi GuardTour.G# (preset tours)

guardtour_add_preset / guardtour_remove_preset / guardtour_set_running

param.cgi GuardTour.G#.Tour.T#

recordedtour_list / recordedtour_record / recordedtour_stop_recording / recordedtour_play / recordedtour_stop_playback

/axis-cgi/recordedtour/*.cgi (manually-steered tour recordings)

autotracker_get_settings / autotracker_get_target / autotracker_set_target / autotracker_set_state

/axis-cgi/ptz-autotracking/admin.cgi

orientationaid_set_north / orientationaid_get_compass_state / orientationaid_set_compass_state / orientationaid_list_tags / orientationaid_set_tag_state

/axis-cgi/ptz-orientationaid.cgi (compass overlay)

ptzws_get_supported_versions / ptzws_get_capabilities / ptzws_continuous_move

PTZ Control WS API (ws://.../vapix/ws/v1, new in AXIS OS 12.11) — websocket continuous move, complements ptz_continuous_move. ARTPEC-7 (excl. V5925/V5938) / ARTPEC-9 mechanical PTZ only — not supported on ARTPEC-8/Q1656; use ptz_* (ptz.cgi) elsewhere.

Ops / diagnostics

Tool

Endpoint

auditlog_get / auditlog_get_version

/axis-cgi/auditlog.cgi

log_get_persistent_enabled / log_set_persistent_enabled / log_clear_persistent / log_write_message

/config/rest/log/v1/... (Device Configuration API)

network_get_tcp_retransmissions / network_get_tcp_retransmission_spikes

/config/rest/network-diagnostics/v1/netstats/...

get_systemready

/axis-cgi/systemready.cgi (no auth required)

get_server_report / get_system_log / get_access_log

/axis-cgi/serverreport.cgi, systemlog.cgi, accesslog.cgi

find_my_device / stop_find_my_device

/axis-cgi/findmydevice.cgi

featureflag_list_all / featureflag_get / featureflag_set

/axis-cgi/featureflag.cgi

regionalsettings_get / regionalsettings_set

/axis-cgi/regionalsettings.cgi

mdnssd_get_info / mdnssd_set_configuration / mdnssd_discover

/axis-cgi/mdnssd.cgi

geolocation_get_position / geolocation_set_position

/axis-cgi/geolocation/get.cgi, set.cgi

ntp_get_info / ntp_set_client_configuration

/axis-cgi/ntp.cgi

network_get_info

/axis-cgi/network_settings.cgi (getNetworkInfo — read-only by design)

The Log API and Network diagnostics API use a newer REST-ish "Device Configuration API" framework (/config/rest/<api>/v1/..., PATCH/POST bodies wrapped as {"data": ...}) instead of the {apiVersion, context, method, params} JSON-RPC shape used everywhere else — tools/deviceConfigApi.ts is the shared helper for it. User account management (pwdgrp.cgi) and network write methods (static IP / 802.1X / WLAN config) were deliberately left out — see ROADMAP.md's Tier 2 notes and the network_get_info description for why.

Streaming

Tool

Endpoint

streamprofile_list / streamprofile_create / streamprofile_update / streamprofile_remove

/axis-cgi/streamprofile.cgi

streamstatus_list

/axis-cgi/streamstatus.cgi (running RTSP streams)

media_get_stream_url

/axis-cgi/media.cgi (URL builder — Matroska/MP4 container stream)

mqtt_configure_client / mqtt_activate_client / mqtt_deactivate_client / mqtt_get_client_status

/axis-cgi/mqtt/client.cgi

mqtt_configure_event_publication / mqtt_get_event_publication_config / mqtt_configure_subscription / mqtt_get_subscription_config

/axis-cgi/mqtt/event.cgi (Event Service ⇄ MQTT bridge)

signedvideo_get / signedvideo_set

param.cgi Image.I#.MPEG.SignedVideo.Enabled

metadataproducer_list / metadataproducer_set_enabled / metadataproducer_get_sample

/axis-cgi/analyticsmetadataconfig.cgi

Audio

Tool

Endpoint

audio_get_settings / audio_set_enabled / audio_configure_source / audio_configure_stream / audio_set_global_settings

param.cgi Audio.*, AudioSource.*

audiodevice_get_capabilities / audiodevice_get_settings / audiodevice_set_settings / audiodevice_get_hazardous_settings

/axis-cgi/audiodevicecontrol.cgi

audioanalytics_get_plugin_schemas / audioanalytics_get_plugins_settings / audioanalytics_set_plugins_settings

/axis-cgi/audioanalytics.cgi

audiomixer_get_plugin_schema / audiomixer_get_plugins_settings / audiomixer_set_plugins_settings

/axis-cgi/audiomixer.cgi

mediaclip_list / mediaclip_play / mediaclip_stop / mediaclip_remove / mediaclip_rename

/axis-cgi/mediaclip.cgi, playclip.cgi, stopclip.cgi (upload/download binary transfer not covered)

Audio control service API, Audio relay service API (both deprecated since AXIS OS 10.12), Audio Multicast Controller (BETA, multi-speaker paging networks), Auto speaker test service API (speaker calibration hardware), and the AXIS Audio Manager Edge/Pro APIs (separate site-management software products, not a camera-resident CGI) were surveyed and deliberately excluded — see ROADMAP.md.

Device configuration

All of these use the newer /config/rest/<api>/<version>/... "Device Configuration API" REST framework (tools/deviceConfigApi.ts), not the {apiVersion, context, method, params} JSON-RPC shape.

Tool

Endpoint

dcparam_get / dcparam_export / dcparam_import

/config/rest/param/v2beta/... (BETA — generic param.cgi wrapper)

devicemode_get / devicemode_set_mode

/config/rest/device-mode/v1beta (BETA)

objectsnapshot_get / objectsnapshot_set

/config/rest/object-snapshot/v1

coordconv_list_spaces / coordconv_convert

/config/rest/coordinate-conversion-api/v1

eventschedule_list / eventschedule_create / eventschedule_update / eventschedule_remove

/config/rest/event-schedules/v2

recordinggroup_list / recordinggroup_create / recordinggroup_delete

/config/rest/recording-group/v2 (cloud recording pipeline, separate from storage.ts's local SD recording)

remotestorage_list / remotestorage_add / remotestorage_update / remotestorage_remove / remotestorage_get_failover / remotestorage_set_failover

/config/rest/remote-object-storage/v1 (S3/Azure destinations for recording groups)

netpairing_list / netpairing_add / netpairing_remove / netpairing_set_nice_name / netpairing_get_features / netpairing_set_capability_enabled / netpairing_sirenlight_*

/config/rest/networkpairing/v1beta (BETA — pair with e.g. an AXIS D4100-E siren/light)

camerapairing_list / camerapairing_add / camerapairing_get / camerapairing_get_status / camerapairing_update / camerapairing_remove

/config/rest/camera-pairing/v1beta (BETA — pull video from one paired external camera/intercom)

analyticsmqtt_list_data_sources / analyticsmqtt_list_publishers / analyticsmqtt_add_publisher / analyticsmqtt_remove_publisher

/config/rest/analytics-mqtt/v1

datatransform_list_topics / datatransform_list / datatransform_create / datatransform_update / datatransform_remove / datatransform_get_statistics

/config/rest/data-transformation/v1beta (BETA — JQ transforms between Device Data Hub topics)

Autopilot API, Directional audio detection API, Cellular routing API (all require hardware this box camera doesn't have — a paired remote PTZ, an audio direction-sensing array, or a cellular modem), and Speaker display settings/notification (a physical display screen on speaker/intercom products) were surveyed and excluded. Network time synchronization configuration API was skipped as a newer duplicate of the already-covered ntp_get_info/ntp_set_client_configuration. See ROADMAP.md.

See ROADMAP.md for what's next.

Security & guardrails

See SECURITY.md for the full model. In short:

  • No user management — the MCP cannot create users or touch credentials/network config (set_param writes are allowlisted to Image/ImageSource/Brand/Time/AudioSource/Event/Overlay).

  • Access levels — operator-set switch: Read-only / Operate (default) / Full. Applies immediately (no restart). See the map below.

  • Rate limits — 60 tool calls/min, 20 writes/min; PTZ presets capped at 20 via MCP.

  • Direct port off by default — enabling it auto-generates a bearer token.

  • Full audit trail — every call and every guardrail refusal in the Live Log + AXIS system log.

What the client actually sees

Three things follow from the access level, and they are visible from the client side:

  • tools/list is filtered. The server does not advertise what it would refuse. At Operate that is 232 of 269 tools; at Read-only, 154. This is why reboot_camera simply is not in the list on a default install — the tool exists, the camera declines to offer it.

    • Side effect worth knowing: a client holding a list from before you lowered the level gets -32602 Tool not found rather than the guardrail's "disabled by the camera operator" explanation. Still refused, less informative.

  • Every tool carries annotationsreadOnlyHint, destructiveHint, idempotentHint, openWorldHint — derived from the same classification the guardrails enforce, so a tool cannot advertise itself as read-only while being treated as a write. Clients use these to decide when to ask you before calling. 154 are read-only; 19 are destructive.

  • Rate limits are per caller, keyed by listener and credential — an agent looping on the direct LAN port cannot exhaust the budget for your own client on the authenticated proxy.

Access-level map

Every tool is classified in src/guardrails.ts (explicit operate/full sets + a read-only naming heuristic; unknown tools fail closed to Full). Each level includes everything below it.

Level

Tools

What's included

Read-only

154

Pure inspection — nothing changes on the camera. Device info & health, snapshots, view areas, param reads, PTZ position/status/limits, event topics, analytics reads (AOA, people counter, queue, demographics, LPV), audio/imaging/zipstream status, stream URLs & profile lists, recordings list, audit/system/access logs, MQTT/pairing/storage status, CamStreamer/CamSwitcher/CamOverlay listings

Operate (default)

+78

Day-to-day control. PTZ moves & presets, guard/recorded tours, autotracking, orientation aid, focus/zoom, image tuning (day-night, stabilizer, rotation, rate control, dewarp), view-area geometry, native & CamOverlay overlays, audio operation & media clips, relay outputs, start/stop recordings, stream & profile control, zipstream tuning, event schedules, find_my_device, CamStreamer/CamSwitcher control

Full

+37

Persistent configuration & maintenance. reboot_camera, factory_default (both also need confirm: true), set_param, control_app, config import (dcparam_import), device/capture mode, feature flags, regional/NTP/mDNS/geolocation settings, MQTT client & wiring, signed video, analytics MQTT publishers & data transforms, network/camera pairing, recording groups & remote storage topology, persistent-log administration

Full list of Full-level tools (37): analyticsmqtt_add_publisher, analyticsmqtt_remove_publisher, camerapairing_add, camerapairing_remove, camerapairing_update, capturemode_set_mode, control_app, datatransform_create, datatransform_remove, datatransform_update, dcparam_import, devicemode_set_mode, factory_default, featureflag_set, geolocation_set_position, log_clear_persistent, log_set_persistent_enabled, mdnssd_set_configuration, mqtt_activate_client, mqtt_configure_client, mqtt_configure_event_publication, mqtt_configure_subscription, mqtt_deactivate_client, netpairing_add, netpairing_remove, netpairing_set_capability_enabled, ntp_set_client_configuration, reboot_camera, recordinggroup_create, recordinggroup_delete, regionalsettings_set, remotestorage_add, remotestorage_remove, remotestorage_set_failover, remotestorage_update, set_param, signedvideo_set.

These counts and this list come from the code, not from editing this file: npm test prints 269 tools across 34 modules (readonly 154, operate 78, full 37) on every run. They were wrong here for a while — the table said 138/+76/+38 and listed audio_set_global_settings as Full, which it is not.

Access level selector on the settings page

Tests

cd app && npm test        # tsc && node --test

44 tests, no framework. They exist because the tool table is 269 entries wide and every one of them is classified by guardrails.ts, so the failure modes are silent rather than loud — a tool nobody classified is not a compile error, it just fails closed and gets refused at the default access level with no clue why. That is exactly how ptzws_continuous_move was broken.

File

Covers

test/tools.test.js

every tool is deliberately classified (no silent fail-closed); no duplicates; annotations agree with the enforced level; the destructive set; deterministic ordering; the catalog is built once; access levels nest; rate-limit budgets are per caller

test/manifest.test.js

schema is v2 with every required field; removed fields absent; vendorId is 10-char hex; vendor is the My Axis account email (the ACAP000045 trap); compatibleOsVersions has min and max; manifest and package.json versions agree; the /mcp proxy entry survives; appName still matches the launcher

test/protocol.test.js

spawns the real app and speaks both protocol revisions to it over HTTP — 2025 initialize, 2026-07-28 server/discover with no handshake, the cache hint, and a malformed envelope being rejected

test/security.test.js

the direct listener's gates: a no-Origin client passes, a browser cross-site POST gets 403, the origin guard runs before the bearer compare, and a wrong token gets 401 with WWW-Authenticate

Every check was validated by breaking the thing it guards — reverting a classification, dropping vendorId, renaming appName, pointing the rate limiter back at one global bucket. A test that does not fail when the bug returns is not protecting anything.

The suite also prints the tool counts on every run, which is where the numbers in this file come from.

Build

Prefer not to build? Grab a prebuilt .eap from Releases and skip to Install.

Requires Docker Desktop on your Mac.

sh dev-loop.sh build     # dev package -> build-arm64/
sh release.sh            # release package + notes -> releases/

release.sh refuses to build if the manifest's vendor is not your My Axis account email — the signing portal rejects anything else with ACAP000045, and since the manifest is baked in at build time, a rejected upload means a full rebuild. It also verifies the built package: the launcher must be named after appName, dist/ and the node runtime must be present, and anything Needle-shaped must not be.

Install

Camera UI → System → Apps → enable Allow unsigned appsAdd app → upload the aarch64 .eapStart. Open the app's settings page, enter VAPIX admin credentials, click Run self-test.

Connect an MCP client

Two endpoints:

  • Reverse-proxied (camera enforces admin digest auth): http://<camera-ip>/local/edgemcp/mcp

  • Direct LAN port (for clients without digest; optional bearer token): http://<camera-ip>:8000/mcp

claude mcp add --transport http axis-q1656 http://<camera-ip>:8000/mcp

The voice console lives at http://<camera-ip>/local/edgemcp/console.html.

Inspect with: npx @modelcontextprotocol/inspector

Verification checklist

  • npm test passes (44) before building anything

  • .eap installs and starts on AXIS OS 12.10+ without manifest errors

  • Settings page loads at http://<ip>/local/edgemcp/index.html — proves the schema-v2 reverseProxy survived

  • status.cgi reports "vapix":{"ok":true} after credentials are entered

  • tools/list returns 232 at the default Operate level, and reboot_camera is NOT among them

  • curl -X POST http://<ip>:8000/mcp with an initialize body returns server info (2025 path)

  • The same endpoint answers server/discover with a 2026-07-28 _meta envelope (modern path)

  • MCP Inspector lists all tools and each returns live data

  • take_snapshot returns a viewable JPEG

  • set_param refuses non-allowlisted groups

  • App survives respawn (save settings → app restarts → MCP still answers)

  • Clean SIGINT exit

  • The settings page loads no off-origin resource: DevTools → Network shows nothing outside the camera

  • The package contains nothing Needle-shaped: tar tzf <eap> | grep -i needle is empty

Notes

  • Digest auth honours the algorithm directive (MD5 and SHA-256 + -sess) — an MD5-only client gets a silent 401 on modern AXIS OS.

  • The in-app server binds process.env.HTTP_PORT (AXIS OS assigns it; 32554 only for local dev).

  • Direct-port reachability from the LAN depends on the camera firewall; the reverse-proxied path is the sanctioned route. Keep a bearer token set if the direct port is enabled.

  • The app version is single-sourced from package.json (src/version.ts); bump it there and in manifest.json when releasing.

  • reboot_camera and factory_default are exposed over MCP but always require confirm: true.

  • settings.cgi rejects a directPort outside 1–65535 or colliding with the primary port.

Two hard-won notes about the speech model and the neural voice moved out with the agent — see Needle LLM on AXIS/src-extracted-from-edgemcp/README-EXTRACTION.md and this repo's history at tag v2.0.0. They are about ONNX Runtime builds colliding and kokoro-js hard-coding a Hugging Face URL; neither applies to an MCP server.

Related MCP Connectors

Related MCP Servers