axis-camera-mcp
axis-camera-mcp
An MCP (Model Context Protocol) server that lets an LLM (Claude Code/Desktop) talk to an Axis network camera over VAPIX (Axis's HTTP/JSON camera API): view the live stream in VLC, control pan/tilt/zoom, adjust image sensor settings (brightness, exposure, gain, white balance), capture snapshots, add text/image overlays, get/set the clock, manage local camera user accounts, and control the light.
Built and live-verified against an AXIS M3057-PLVE Mk II on a local network.
See docs/live-verification-checklist.md for exactly what's been tested live vs.
not, and docs/open-questions.md for a confirmed limitation of this specific camera:
PTZ commands succeed and the reported position updates, but produce no real visual
pan/tilt/zoom effect on this fixed-camera model.
Architecture
axis_camera_mcp/
config.py Loads camera host/username/password from config.json
vapix/
session.py requests.Session + HTTP Digest auth
jsonrpc.py Client for VAPIX's modern JSON-RPC-style APIs
legacy.py Client for VAPIX's legacy query-param/plain-text CGIs
client.py VapixClient facade - the only thing tools/* imports
exceptions.py
vlc_launcher.py Starts/stops the local VLC install to view the RTSP stream
rag/ Local knowledge base: VAPIX docs + this project's own
live-verified findings, hybrid BM25+embedding search
(see "Knowledge base (RAG)" below)
app.py / context.py Shared FastMCP instance + lazy VapixClient/VlcLauncher/
HybridRetriever singletons
tools/ One module per capability, each registering @mcp.tool()s
server.py Entrypoint: import tools, preload_rag(), mcp.run()
tests/
test_*.py Unit tests, mocked HTTP, no live camera needed
manual/ Plain scripts (not pytest) exercised against the real camera
docs/
live-verification-checklist.md One row per tool - what's actually been tested live
open-questions.md Confirmed limitations and things not yet triedTwo VAPIX request styles exist side by side on this camera, and the client layer handles both without leaking the difference into tool code:
Modern JSON-RPC style (
api-discovery,basicdeviceinfo,streamprofile,lightcontrol,time.cgi,dynamicoverlay): POST JSON body{"apiVersion", "method", "params"}, response{"data"}or{"error"}.Legacy query-param/plain-text CGIs (
com/ptz.cgi,com/ptzconfig.cgi,pwdgrp.cgi,usergroup.cgi,jpg/image.cgi): plain GET with query params, responses are plain text (sometimeskey=valuelines) or, for the snapshot endpoint, raw JPEG bytes.
Setup
Credentials live in a config file, never in code or environment variables:
copy config.example.json config.json
notepad config.json # fill in host/username/passwordconfig.json is gitignored - it never gets committed.
py -m venv .venv
.venv\Scripts\python -m pip install -e ".[dev]"Running
.venv\Scripts\python -m axis_camera_mcp.serverKnowledge base (RAG)
A local, offline knowledge base lets the LLM search the real VAPIX API docs and this project's own hard-won, live-verified findings (undocumented required fields, real parameter values confirmed only through live trial-and-error, etc.) instead of guessing or re-deriving them each time.
All open-source, all local - no paid API, no vector-DB service:
Crawler (
rag/crawler.py): plainrequests+ BeautifulSoup againstdeveloper.axis.com(a server-rendered Docusaurus site - no headless browser needed), plus this project's own README/docs/vapix/*.pyas local sources.Chunking (
rag/chunker.py): paragraph-aware, ~700 chars with 100 overlap.Retrieval (
rag/retriever.py): hybrid - BM25 (rank-bm25, exact keyword matches likestreamProfileName) and dense embeddings (sentence-transformers,all-MiniLM-L6-v2, CPU-only, for paraphrased questions), combined via reciprocal rank fusion. A plain numpy.npzfile serves as the vector store (a few hundred chunks makes brute-force cosine search microseconds-scale - not worth a real vector-DB dependency).Tracking (
rag/tracking.py): a local SQLite file logs every ingestion run (per-source chunk/char counts, ok/error status) and every query (text, results, scores) - inspectable directly or via therag_statstool.
Setup: sentence-transformers pulls in torch, a genuinely heavy install. On a
CPU-only machine, install the CPU-only wheel explicitly first to keep it much
smaller than the default resolved build:
.venv\Scripts\python -m pip install torch --index-url https://download.pytorch.org/whl/cpu
.venv\Scripts\python -m pip install -e ".[dev]"Build/refresh the index (a separate, manually-run step - never runs automatically at server startup, so startup stays fast and network-call-free):
.venv\Scripts\python -m axis_camera_mcp.rag.ingestEdit axis_camera_mcp/rag/sources.json to add/remove doc pages or local files, then
re-run the command above. The embedding model (~80MB) downloads once from Hugging
Face Hub on first run and is cached afterward; everything else is fully offline.
Generated index files (rag/data/*.jsonl, *.npz, *.sqlite3) are gitignored.
Query it with the search_knowledge and rag_stats tools (see Tool reference below).
Register with Claude Code/Desktop
{
"mcpServers": {
"axis-camera": {
"command": "<path-to-repo>\\.venv\\Scripts\\python.exe",
"args": ["-m", "axis_camera_mcp.server"]
}
}
}Restart Claude Code/Desktop after adding this for the tools to appear.
Testing
.venv\Scripts\python -m pytest tests/ --ignore=tests/manual # unit tests, mocked HTTP
.venv\Scripts\python tests/manual/live_smoke_test.py # read-only checks against the real cameraThe tests/manual/ scripts talk to the real camera in config.json - review what
each one does before running it against a camera you care about. live_smoke_test.py
is read-only. The mcp_harness_*.py scripts exercise the full MCP tool-call path
(including mutations like PTZ moves, clock/timezone changes, and user add/remove) -
these were this project's own live-verification scripts; treat them as reference,
not something to re-run casually against a production camera.
Tool reference
Device/API info (read-only): get_device_info, get_supported_apis, get_camera_overview
Streaming (view the live stream in the local VLC install): start_stream,
stop_stream, get_stream_status, list_stream_profiles
Snapshot (single still frame, separate from the video stream): save_snapshot
(file only, the LLM never sees the image), get_snapshot (returned directly to the
LLM so it can describe what's in view)
PTZ (pan/tilt/zoom - see docs/open-questions.md re: no visual effect on
this specific camera): ptz_move_absolute, ptz_move_relative, ptz_get_position,
ptz_save_preset, ptz_goto_preset, ptz_list_presets
Image sensor (brightness/contrast/saturation/sharpness, exposure, gain,
shutter, white balance - the actual sensor/ISP settings, distinct from PTZ's
framing/crop and light's physical illuminator): get_image_settings,
set_image_appearance, set_exposure_mode, set_exposure_value,
set_manual_gain, set_manual_shutter, set_white_balance
Overlays (text/image burned into the video stream): add_text_overlay,
set_text_overlay, add_image_overlay, set_image_overlay, list_overlays,
remove_overlay, get_overlay_capabilities
Clock: get_clock_info, set_clock, set_timezone
Light (this specific camera has no light hardware - confirmed via
get_light_status, which reports that clearly): get_light_status,
activate_light/deactivate_light, enable_light/disable_light,
set_light_intensity, get_valid_intensity, set_light_auto_mode
Users - mutations require confirm: true, the one area with real security
consequence: list_users (returns every account with its privilege groups, for a
privileged caller), add_user, update_user, remove_user
Knowledge base (see "Knowledge base (RAG)" above): search_knowledge (hybrid
BM25+embedding search over VAPIX docs and this project's own live-verified findings,
with source citations and scores), rag_stats (what's indexed, from where, when,
and recent query history - states plainly if the index needs rag.ingest run)
Safety
User-account mutations (add_user, update_user, remove_user) require an explicit
confirm: true tool argument and raise a clear error without it - this is the one
area where a careless call has real security consequence (unauthorized access or an
accidental lockout). Everything else (PTZ, light, clock, streaming, overlays, snapshots)
is unconfirmed by design: it's freely reversible and this is a single camera the user
owns directly, not a shared production system.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/cdp/GSX2026-axis-camera-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server