Skip to content

Latest commit

Β 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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_mcp β†’ edgemcp, 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)

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 annotations β€” readOnlyHint, 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 apps β†’ Add app β†’ upload the aarch64 .eap β†’ Start. 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.

About

πŸ“· The first on-camera Model Context Protocol server for AXIS cameras. One .eap opens the camera to Claude, Gemini, ChatGPT and Perplexity πŸ€– β€” no cloud ☁️, no middleware. Zero hops. Zero cloud. ⚑

Resources

Security policy

Stars

6 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages