Your coding agent wants to know if you left the stove on
You are three tabs deep at 2 AM, and you ask Claude Code a question that has nothing to do with the code you are shipping: “is the NAS actually online, or did it just drop off Wi-Fi again.” A month ago that meant tabbing over to a dashboard. Today, if you run NetAlertX, you can just ask.
NetAlertX v26.9.0, released September 2, 2026, shipped a built-in MCP Server Bridge on its API port. No sidecar, no wrapper script, no third-party bridge project. Twelve tools, one bearer token, and your agent can list devices, check who joined the network in the last day, and find out which box has port 22 open, all from the same chat window you use to review a pull request.
One trade comes with that: the same token that lets your agent check presence also lets it run an nmap scan against any host on your LAN and send a Wake-on-LAN packet. If you are running NetAlertX on a box you already trust with a shell, on a LAN-only agent, that is a fair trade and the right call. If your agent is a cloud client, or you just want a presence feed with zero scan and wake power, skip the built-in bridge and route through Home Assistant’s MQTT plugin instead. Both paths are real and documented. Pick based on where your agent runs.
Full example: Clone the working Compose file and setup steps at github.com/KingPin/sumguy-examples/tree/main/networking/netalertx-mcp-presence-agent.
What NetAlertX actually watches
NetAlertX is a self-hosted network presence and asset discovery tool, GPL-3.0, forked from the old Pi.Alert project. It watches your LAN with arp-scan, can import from a Pi-hole database, runs nmap for port data, and pulls in UniFi controller details, with more sources available through plugins. It keeps a history: a Presence View timeline per device (Week, Month, Year), a change log of what changed and when, and a workflow engine that can react to events like a device coming back online after a long absence.
The catch that shapes everything downstream: arp-scan needs Layer 2 access to the network it is scanning. That is why the official Docker setup runs with network_mode: host instead of a bridge network. It also means NetAlertX cannot see devices on a different subnet without extra plumbing (the project’s remote-networks docs cover workarounds), Wi-Fi-only environments sometimes need a different scanner entirely, and Windows hosts are not supported because Windows does not do host networking the same way Linux does. Keep that in your head. Every “why isn’t my phone showing up” question usually traces back to one of these three limits.
One more thing before you deploy: v26.9.0 moved the custom plugins directory from /front/plugins to /server/plugins. If you are upgrading an existing install with a custom plugin bind mount, update the path or your plugin silently stops loading.
Getting NetAlertX running
Here is a compose file adapted from the project’s official one, using the published image instead of a local build:
services: netalertx: image: ghcr.io/netalertx/netalertx:latest container_name: netalertx network_mode: host # ARP scanning needs Layer 2 access read_only: true cap_drop: - ALL cap_add: - NET_ADMIN # arp-scan, nmap, traceroute - NET_RAW # raw sockets for the same tools - NET_BIND_SERVICE # privileged ports for nbtscan - CHOWN # root-entrypoint chowns /data + /tmp - SETUID - SETGID sysctls: net.ipv4.conf.all.arp_ignore: 1 net.ipv4.conf.all.arp_announce: 2 volumes: - type: volume source: netalertx_data target: /data read_only: false - type: bind source: /etc/localtime target: /etc/localtime read_only: true # Custom plugins live under /server/plugins as of v26.9.0: # - /path/on/host:/app/server/plugins/custom tmpfs: - "/tmp:mode=1700,uid=0,gid=0,rw,noexec,nosuid,nodev" environment: PUID: 20211 PGID: 20211 LISTEN_ADDR: 0.0.0.0 PORT: 20211 # web UI GRAPHQL_PORT: 20212 # REST + GraphQL + MCP ALWAYS_FRESH_INSTALL: "false" NETALERTX_DEBUG: 0 mem_limit: 2048m mem_reservation: 1024m pids_limit: 512 restart: unless-stopped
volumes: netalertx_data:network_mode: host means the “ports” your browser and your agent hit come straight from the environment variables: 20211 for the web UI, 20212 for the REST, GraphQL, and MCP server. read_only: true plus the tight cap_add list is the container doing the least it can get away with while still running arp-scan and nmap, which both need raw socket access on Linux.
Before anything else, set an API token and, while you are there, turn on the web UI password. NetAlertX ships with no login by default. If you skip SETPWD_enable_password=true and SETPWD_password=... in the app config, your dashboard sits open to anyone who can reach port 20211. Do not leave the default password (123456) in place if you do enable it.
The direct path: NetAlertX’s built-in MCP bridge
The MCP Server Bridge lives on the same GRAPHQL_PORT as the REST and GraphQL APIs. The connection endpoint is GET/POST /mcp/sse, using Server-Sent Events as the transport, with /mcp/sse/openapi.json describing the tool schema (an OpenAPI 3.0.0 document titled “NetAlertX Tools”, version 1.1.0). The docs’ architecture diagram also shows a /mcp/messages message handler, but nothing specs it as a path you call yourself.
Auth is the same single bearer token used everywhere else in the API:
Authorization: Bearer <API_TOKEN>Get the token wrong and you get an HTTP 403 with a body of {"success": false, "message": "ERROR: Not authorized", "error": "Forbidden"}. There is exactly one API_TOKEN setting. No scopes, no read-only variant, no per-tool permission. Whoever holds that token can call any of the twelve tools.
The twelve tools
Eight are read-only:
| Tool | Endpoint | What it does |
|---|---|---|
list_devices | /devices/by-status | List devices by status (online, offline, down, archived, favorites, new, my) |
get_device_info | /device/{mac} | Full detail on one device |
search_devices | /devices/search | Search by MAC, name, or IP fragment |
get_latest_device | /devices/latest | Most recently connected device |
get_open_ports | /device/open_ports | Stored nmap results. Run run_nmap_scan first if it comes back empty |
get_network_topology | /devices/network/topology | The network map |
get_recent_alerts | /events/recent | Events from the last 24 hours |
get_last_events | /events/last | The 10 most recent events |
Four take action:
| Tool | Endpoint | What it does |
|---|---|---|
set_device_alias | /device/{mac}/set-alias | Rename a device |
trigger_scan | /nettools/trigger-scan | Kick off a scan (argument type, for example "ARPSCAN") |
run_nmap_scan | /nettools/nmap | Port-scan a target with nmap |
wol_wake_device | /nettools/wakeonlan | Send Wake-on-LAN (argument devMac) |
That second table is the part to sit with. run_nmap_scan means your coding agent can port-scan any host it can reach. wol_wake_device means it can power on a machine on your network. trigger_scan kicks off a network sweep on demand. None of that is hidden behind a separate credential. It rides the same token as “what devices are online right now.”
Wiring it into Claude Code
Claude Code’s own docs (fetched September 21, 2026) are direct about SSE: “The SSE (Server-Sent Events) transport is deprecated. Use HTTP servers instead, where available.” NetAlertX’s MCP bridge only speaks SSE, so you add it with the explicit SSE transport flag:
claude mcp add --transport sse netalertx \ http://<netalertx-host>:20212/mcp/sse \ --header "Authorization: Bearer <API_TOKEN>"Check the connection landed:
claude mcp listclaude mcp get netalertxOnce it is connected, you can ask things like: “is the NAS online,” “what joined the network in the last 24 hours,” “which device has port 22 open,” or “rename the device at 192.168.1.50 to something I’ll recognize.” Behind the scenes those map to search_devices or list_devices, get_recent_alerts, get_open_ports, and set_device_alias.
A quick note on the project’s own MCP docs: the “Claude Desktop Integration” example points mcp.json at "args": ["/path/to/mcp-client.js"], a file the project does not ship. There is also a generic Python example that wraps a raw curl -N call in a stdio client as a workaround. Neither is a working recipe you can copy and paste. The docs are thin here, so use the claude mcp add --transport sse command above instead; it is documented behavior straight from Claude Code, not a guess.
Calling a tool directly
If you want to see the wire format without going through an agent, here is a raw JSON-RPC tools/call:
{ "jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": { "name": "search_devices", "arguments": { "query": "192.168.1" } }}The response comes back wrapped in MCP’s content envelope:
{ "jsonrpc": "2.0", "id": "1", "result": { "content": [ { "type": "text", "text": "{\"success\": true, \"devices\": [{\"devName\": \"Router\", \"devMac\": \"AA:BB:CC:DD:EE:FF\", \"devLastIP\": \"192.168.1.1\"}]}" } ], "isError": false }}Failed tool calls set "isError": true with a text field like "Error calling tool: Device not found", on top of the usual HTTP-level 401, 403, 400, 404, or 500.
Checking without MCP at all
You do not need MCP wired up to poke at the same data. A plain REST call against the online-devices endpoint:
curl -H "Authorization: Bearer <API_TOKEN>" \ "http://<netalertx-host>:20212/devices/by-status?status=online"Or the GraphQL endpoint with the documented query shape:
curl 'http://<netalertx-host>:20212/graphql' \ -X POST \ -H 'Authorization: Bearer <API_TOKEN>' \ -H 'Content-Type: application/json' \ --data '{ "query": "query GetDevices($options: PageQueryOptionsInput) { devices(options: $options) { devices { rowid devMac devName devOwner devType devVendor devLastConnection devStatus } count } }", "variables": { "options": { "page": 1, "limit": 10, "sort": [{ "field": "devName", "order": "asc" }], "search": "", "status": "connected" } } }'The interactive API docs at http://<netalertx-host>:20212/docs list the full surface if you want to browse it before you wire up MCP.
The read-only path: MQTT to Home Assistant
If your agent is not sitting on the same trusted box, or you just do not want a chat client holding a token that can nmap-scan your network, skip the direct bridge and go through Home Assistant instead.
NetAlertX has an MQTT plugin that publishes to a broker (Mosquitto is what the docs walk through). It creates two kinds of MQTT devices: one overview device carrying online, down, and archived counts (toggle with the SEND_STATS setting), and one Home Assistant device per detected network device. Under NetAlertX Settings, MQTT, you point it at your broker and set MQTT_RUN to either schedule or on_notification.
The presence data you actually want lands as one binary sensor per device, is_present. The plugin announces it through Home Assistant MQTT discovery (config topic homeassistant/binary_sensor/mac_44_ef_44_ef_44_ef/is_present/) and then publishes state to:
system-sensors/binary_sensor/mac_44_ef_44_ef_44_ef/statewith a payload of:
{ "is_present": "ON"}That becomes a normal Home Assistant entity. No scan trigger, no nmap, no Wake-on-LAN. Just online or offline, per device, sitting in your entity list next to your lights and your thermostats.
From there, the rest is Home Assistant’s job, and this blog already covers it in detail in Home Assistant MCP: Server and Client. Short version: HA’s mcp_server integration exposes a Streamable HTTP endpoint, and the only access control that matters is the Assist exposure list under Settings, Voice Assistants. Expose the is_present sensors for the devices you care about and nothing else. Your agent gets a clean, strictly read-only presence feed, and if that MCP client gets compromised or misconfigured, the blast radius is “can read whether the NAS is online,” not “can nmap your LAN and power on your homelab hypervisor at 3 AM.”
The tradeoff is plumbing. You need a broker, the MQTT plugin configured, Home Assistant’s own MCP setup, and an exposure list you maintain. A few things to know going in: discovery takes about 10 seconds per device the first time, devices you delete in NetAlertX do not disappear from Home Assistant automatically (clean them up with MQTT Explorer), and device definitions only re-push when the icon, name, or MAC changes, though the online/offline state itself always updates the moment it changes. Slower to set up, but the agent that talks to it cannot do anything except read.
The gap nobody talks about
NetAlertX’s own security documentation tells you to “Use Read-Only API Keys” and “scope keys tightly” when integrating with other tools. That is good advice. It is also advice the software cannot follow, because there is exactly one API_TOKEN and it is all-or-nothing. Every one of the twelve MCP tools, from “list devices” to “run nmap against a target,” authenticates with the same string.
That is not a knock on the project. Most self-hosted tools ship a single API key and call it a day. But if you are deciding which of these two paths to use, factor in that the built-in bridge cannot give you a scoped, read-only credential no matter how carefully you configure it. Either your agent runs somewhere you already trust with that level of access, or it does not get the token at all and you route through the MQTT and Home Assistant path instead. There is no middle setting to dial down.
Common Questions
Does NetAlertX support scoped or read-only API tokens?
No. NetAlertX has exactly one API_TOKEN setting with no scopes and no read-only variant, even though its own SECURITY.md recommends using read-only, tightly scoped keys. Anyone holding the token can call all 12 MCP tools, including the four that trigger scans, run nmap, or send Wake-on-LAN. Use the Home Assistant MQTT path for a strictly read-only setup.
Can Claude Desktop connect to the NetAlertX MCP server?
Not cleanly with the example in NetAlertX’s own docs, which points at a mcp-client.js file the project does not ship. The MCP bridge only speaks SSE, and Claude Code’s docs mark SSE as deprecated in favor of HTTP transports. Use claude mcp add --transport sse with Claude Code instead; that command is documented and works.
Can NetAlertX detect devices on another subnet or VLAN?
Not without extra setup. NetAlertX relies on arp-scan, which needs Layer 2 access and does not cross subnet boundaries. That is why the official container runs with network_mode: host on the subnet you want scanned. The project’s remote-networks documentation covers workarounds for multi-subnet setups, but out of the box, one container watches one broadcast domain.
Does NetAlertX work on Windows or Wi-Fi-only networks?
Windows hosts are unsupported because NetAlertX depends on Docker host networking for ARP scanning, which Windows does not support the way Linux does. Wi-Fi-only networks can also be a problem for arp-scan depending on your access point, and may need a different discovery method, such as the Pi-hole or UniFi integrations, layered on top.
What breaks when upgrading to NetAlertX v26.9.0?
Custom plugin bind mounts break if you do not update them. Version v26.9.0, released September 2, 2026, moved the plugins directory from /front/plugins to /server/plugins. If your compose file still bind-mounts a custom plugin folder to the old path, the plugin stops loading silently after the upgrade. Update the mount target and restart the container.