NetMap MCP server
Commit Oct 21, 2026An AI-powered network inventory system that reconciles a self-hosted asset list against live infrastructure like Docker, Proxmox, and UniFi.
Also known as netmap-app/netmap
- Stars
- 0
- Tools
- 18
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- Oct 21, 2026
- License
- MIT
What is NetMap MCP?
NetMap is an AI-powered network inventory system that reconciles a self-hosted asset list against live infrastructure like Docker, Proxmox, and UniFi. It exposes 18 tools, such as list_entries, get_entry, create_entry.
Summary generated from the project's README
About NetMap MCP server
Network administrators and home lab enthusiasts can use NetMap to maintain a self-hosted, living inventory of their digital infrastructure. The system works by reconciling a manually managed asset list against live, read-only data from sources like Docker, Proxmox, UniFi, OPNsense, and Pi-hole. It identifies discrepancies such as undocumented devices, moved services, or unauthorized open ports, ensuring your documentation matches reality.
By deploying as a single Docker container using FastAPI and SQLite, it provides a centralized view of hosts, VMs, containers, and services. Users can interact with the inventory through a web interface or via an AI assistant using the MCP protocol. To enable AI integration, you must set a specific bearer token. The server remains non-intrusive, as it never modifies your router, DNS, or container configurations, only reading data to report findings.
What you can do with it
- 1Identify network security gapsRequest a report on which hosts have public hostnames without an associated access policy.
Written from the project's README. Every use case is checked against a quote from it.
NetMap MCP tools 18
Detected from the READMElist_entriesget_entrycreate_entryupdate_entrydelete_entrylist_categoriescheck_statusget_service_contextderive_linksmap_hostget_topologyscan_docker
| list_entries | — | |
| get_entry | — | |
| create_entry | — | |
| update_entry | — | |
| delete_entry | — | |
| list_categories | — | |
| check_status | — | |
| get_service_context | — | |
| derive_links | — | |
| map_host | — | |
| get_topology | — | |
| scan_docker | — |
Show all 18 toolsShow fewer tools
scan_opnsensescan_all_sourceslist_sourcesscan_sourcefind_conflictsrecent_changes
| scan_opnsense | — | |
| scan_all_sources | — | |
| list_sources | — | |
| scan_source | — | |
| find_conflicts | — | |
| recent_changes | — |
README
View on GitHubNetMap
A self-hosted inventory of your network that checks itself. Write down what runs where - hosts, VMs, containers, services, ports, addresses - and NetMap compares it with what your infrastructure actually reports: Docker, Proxmox, OPNsense, Pi-hole, AdGuard Home, UniFi, Home Assistant, Nginx Proxy Manager, Traefik, Cloudflare, NetBox, DHCP leases, ARP over SNMP and an open-port sweep. Where the two disagree, it says so; a person decides.
- Inventory you edit from a desktop or a phone, with live reachability, 30-day uptime, relationships and a topology graph.
- Reconciliation against read-only sources: what is new, what is gone, what moved, which ports nobody declared, which hostnames are public without an access policy.
- New on the network: devices nobody has named yet - add them, ignore them, or watch them until you know.
- Notifications (ntfy, Gotify, Telegram, webhook), at once or as a daily or weekly summary.
- MCP server, so an AI assistant (Claude and others) can read and edit the same inventory.
- One container: FastAPI + SQLite, no external database, amd64 and arm64.
Every source is read-only: NetMap never changes your router, DNS, proxy or containers. Credentials you add are encrypted at rest.
A demo inventory with made-up names and addresses.
The screenshots show a demo inventory with made-up names and addresses.
| Web UI | http://<server>:8087/ |
| REST API | /api/… |
| MCP endpoint | off until NETMAP_MCP_TOKEN is set; /mcp by default (streamable HTTP, section 10) |
| Health | /healthz |
| Storage | SQLite at /data/netmap.db |
1. Install
You need Docker with the Compose plugin. Put docker-compose.yml
in a folder of its own, set NETMAP_ALLOWED_HOSTS to the address or name you
will open NetMap by, then:
mkdir data && sudo chown 1000:1000 data # the app runs as UID 1000
docker compose up -d
docker compose logs netmap | grep password # the one-time admin password
Open http://<server>:8087/, sign in as admin with that password, and set
your own under Settings › Profile. A new install starts with an empty
inventory and no sources: add entries with + Add (or import a JSON
export), and sources under Settings › Sources (section 12 says which
read-only credential each one needs).
To start from an inventory kept elsewhere, set NETMAP_SEED_FILE to a JSON
export: it is loaded once, into an empty database only.
Building it yourself instead: docker compose up -d --build in a clone of
this repository (uncomment build: .).
Update
docker compose pull && docker compose up -d. Everything lives in ./data,
so an update never loses entries; the database migrates itself at start-up.
Pin a version tag (e.g. :2.0.0) to upgrade on your own
schedule. Settings › About shows what changed.
2. Remote access
NetMap listens on port 8087. Reach it from elsewhere however you already do for other self-hosted apps:
- On the LAN only - the default; the local login protects it.
- A reverse proxy (Nginx Proxy Manager, Traefik, Caddy) - forward to
netmap:8087(same Docker network) or<server>:8087, with WebSockets on. Bind the port to loopback (127.0.0.1:8087:8087) or drop it once the proxy is the only way in, and add the proxy's host name toNETMAP_ALLOWED_HOSTS. - Cloudflare Tunnel + Access - route a hostname to
http://netmap:8087, protect it with an Access application, and setNETMAP_CF_ACCESS_TEAM/NETMAP_CF_ACCESS_AUDso NetMap verifies the Access login itself (below).
Authentication (1.69.0+)
Every request except /healthz, /login and /static/… must prove who it is -
being on the LAN is not an identity. Three ways, any one is enough:
-
Cloudflare Access JWT. Access adds a signed
Cf-Access-Jwt-Assertionheader (and aCF_Authorizationcookie) to every request it lets through. NetMap verifies the signature against your team's published keys, plus the audience, issuer and expiry. Set:NETMAP_CF_ACCESS_TEAM- thexxxinxxx.cloudflareaccess.com(Zero Trust → Settings → Custom Pages shows the team domain)NETMAP_CF_ACCESS_AUD- the netmap application's Application Audience (AUD) Tag (Access → Applications → netmap → Overview)
The verified e-mail is what the change history records. The unsigned
Cf-Access-Authenticated-User-Emailheader is no longer read - a header anyone can type is not proof that Access saw the request. -
NETMAP_API_TOKEN-Authorization: Bearer <token>for machines that cannot log in: a dashboard widget (section 9), backup scripts. Recorded in the change history asapi-token. -
Local password login at
/login- the way in without Cloudflare Access, and the spare key when Access is misconfigured. One account, usernameadminto start with, changeable along with the password under Settings › Profile. There is no default password in the code: the first start usesNETMAP_ADMIN_PASSWORDif set, otherwise generates one and prints it once to the container log (docker logs netmap | grep "local login"). Until it is changed, every page shows a banner saying so. Sessions are signed, HttpOnly, SameSite=Strict cookies valid for 14 days; changing the password signs out every other session. Five wrong attempts from one client (or twenty overall) in 15 minutes lock the password login for 15 minutes - Access and the API token keep working. Lost the password:docker exec -it netmap python -m app.accounts resetprints a new one.
A signed-out browser is sent to /login; API calls get 401. A state-changing
request authenticated by a cookie (Access or password) is refused if its
Origin names another site. NETMAP_AUTH=off turns all of this off for a local development copy; the
log and Settings › About both say so in capitals. The MCP endpoint is separate
and unaffected - it is off unless NETMAP_MCP_TOKEN is set, and then requires
that bearer token (section 10).
Requests whose Host is not in NETMAP_ALLOWED_HOSTS (port ignored) are
refused with 400 before any of this runs - that is what stops DNS rebinding,
where a page on another site re-points its own name at this address. The
refusal names the host and the setting to add it to.
3. Connect an AI assistant (MCP)
NetMap is an MCP server at NETMAP_MCP_PATH (default /mcp, streamable
HTTP). It is off until you set NETMAP_MCP_TOKEN, and then every request
needs that bearer token - section 10. Any MCP client that can send a bearer
token works: Claude (Desktop, Code, or claude.ai through a connector or an MCP
portal), and others.
Tools Claude gets
| Tool | Purpose |
|---|---|
list_entries |
search / list the inventory |
get_entry |
one entry by id |
create_entry |
add a device or service |
update_entry |
change any field |
delete_entry |
remove an entry (kept in history) |
list_categories |
categories, tags, totals |
check_status |
live TCP reachability check |
get_service_context |
one entry in full: status, links, history, edits |
link_entries / unlink_entries |
record or remove a relationship |
derive_links |
infer relationships from the entries |
map_host |
teach it which entry a host string means |
get_topology |
the runs-on tree |
scan_docker |
compare running containers with the inventory |
scan_opnsense |
compare leases, reservations and NAT rules with the inventory |
scan_all_sources |
every configured source at once |
list_sources |
every added source - id, type, roles, answering or not - without scanning |
scan_source |
one source by id (docker-2, adguard, traefik…) |
ignore_finding / list_ignored |
suppress a discovery finding, and review what is suppressed |
find_conflicts |
contradictions in the inventory |
recent_changes |
audit log |
4. Data model
| Field | Meaning |
|---|---|
category |
Core Network, Docker, Smart Home, … |
name |
device or service name |
host |
VM / CT / physical host |
ip |
IPv4 or hostname |
mac |
one or more MAC addresses, normalised on write |
ports |
free text (8443, 80, 443, 81 (admin), 2211 → 22) |
protocol |
HTTPS, SSH, MQTT, … |
url |
optional explicit link; otherwise built from ip:port |
tags |
free-form, clickable filters |
notes |
anything |
monitor |
include in reachability sweeps |
verified |
false shows an orange verify badge (the old yellow rows) |
criticality |
critical / important / normal / experimental |
zone |
network zone - LAN, IoT, DMZ, Management … |
secret_ref |
a pointer to credentials elsewhere, never a credential |
5. Status checks
A background sweep every NETMAP_CHECK_INTERVAL seconds (default 120) checks
each monitored entry. Green = answered, red = no answer, hollow = not monitored
or not probed. The dot's tooltip and the service card say what was checked and
what answered ("HTTPS 10.0.0.5:8443/health → 200 in 42 ms").
An entry counts as down only after 2 failed checks in a row (Settings › Sources, "Count an entry as down after", 1-10; 1 acts on the first failure), so a single dropped ping or refused connect puts nothing on the Overview and sends no notification. Until then it keeps the state it was last known in and its tooltip says "failed 1 of 2 checks before it counts as down". One good answer brings it back up at once.
By default that is a TCP connect to the entry's first port. An open port is not a working service, so an entry's Health check field can ask for more:
| Check | What it does |
|---|---|
| (empty) | TCP connect to the first port, as always |
tcp, tcp:22 |
TCP connect, to the first port or the one given |
http, https [:<port>][/<path>][=<code>] |
a GET. With =<code> only that status is up; without it anything below 500 is. The port is the one given, else the URL's, else 80/443 |
ping |
one ICMP echo, for devices with no open port |
none |
monitored, but not probed |
- HTTP(S) connects to the entry's IP and names the URL's host (TLS SNI, the
certificate check, the
Hostheader), so a service behind a name-based proxy answers as itself. It follows no redirect: a 3xx is below 500, so up. httpsreads the certificate. One that does not verify (self-signed, wrong name) is shown as "TLS invalid" on the card, not as down. One expiring within 21 days (Settings › Sources, 1–365) is an Overview warning and acertnotification; an expired one is critical.pinguses an unprivileged ICMP socket - the image has nopingbinary and runs as a normal user. Docker allows these by default (net.ipv4.ping_group_range); where it is not allowed the check reports "not permitted", as unknown rather than down.- A transition stores what was checked in
observations.detail.
Stale entries. NetMap records when something last confirmed each entry exists: a source's sighting (not a port found closed, not NetBox's plan), an address a source sees at the entry's MAC - or, for hardware and VMs, at its address - or its health check answering. The service card shows it ("Last seen"). An entry that was confirmed and then nothing has confirmed for 14 days (Settings › Sources, 1–365) is listed in one Overview note, each name a chip that opens its card. Entries never confirmed are never listed - nothing says a source could see them - and neither are those a source already reports gone (Docker's "no container", a stale NAT rule, a missing tunnel route or Home Assistant entity). On the first start, an entry whose health check went down and never came back counts as last seen when it went down.
Uptime. The service card shows the share of the last 30 days an entry's
check answered ("99.58% up over 30 days · down 3h in all") with a strip of
up, down and unknown periods; the status dot's tooltip carries the figure.
It is computed from the stored transitions, so it costs nothing to collect.
Unknown is never counted as up: time an entry was not monitored, time before
its first record (the card then says "since …"), and time NetMap itself was
not running - every sweep leaves a heartbeat, and a silence of more than two
sweep intervals found at start-up is kept as a gap. Where it is shown:
Settings › Sources, by criticality (default critical and important), kind
or category; a tag uptime:on / uptime:off on an entry overrides the rule.
This is a lightweight complement to Uptime Kuma, not a replacement: a
critical or important entry going down can be notified (5b), but there are no
thresholds, retries or escalation.
5b. Notifications
Settings › Notifications sends what the Overview would show to somewhere you will see it when NetMap is not open. It detects nothing new; it delivers.
| Channel | Needs | Notes |
|---|---|---|
| ntfy | server (default https://ntfy.sh), topic, optional access token |
JSON publish to the server root; urgent items get priority 4 |
| Gotify | server, application token | POST /message; urgent items get priority 8 |
| Telegram | bot token (from @BotFather), chat id, optional forum topic id | plain text, so _ and * in names arrive intact. Message the bot once, then read the chat id from https://api.telegram.org/bot<token>/getUpdates. "Bot API server" is for a self-hosted Bot API |
| Webhook | URL, optional bearer token | POST JSON {kind, key, title, detail, url, ts, level, version} - point it at an Apprise API container for anything else |
Each channel chooses its events:
- finding - a finding a source did not report on its previous scan (ignored ones never)
- entry - a monitored entry with criticality
criticalorimportantstops answering, or answers again - source - a source stops answering, or answers again
- cert - a certificate an
httpshealth check reads expires soon, or has expired (5) - critical - any other critical Overview item (e.g. stored secrets no key can open)
Rules:
- Once per change, never once per scan. What is wrong now is compared with
what was wrong last time (kept in the
kvrownotify_state). A finding that clears and comes back later is new again. - Restarts are silent. That state survives a restart, so only what changed meanwhile is sent. The first scan of a newly added source, and the first run after upgrading, record what is already true without sending it.
- Rate limit. At most N messages per channel per 10 minutes (a field, default 10). The rest collapse into one "and N more"; anything over the limit waits for the window, it is not dropped - editing the channel keeps it too. Only a restart in that window loses it (it is held in memory).
- Failures stay local. A delivery that fails is logged and shown in red on the channel's row; it never interrupts the scan or sweep that caused it. Test sends one message at once, outside the rate limit.
- Secrets as for sources. Tokens, and the webhook URL itself (Slack and Discord put the token in it), are encrypted with the source secrets' key and shown only as a last-four preview (nothing at all for one under 16 characters). A blank field on save keeps the stored value. Changing a server address, or the webhook URL, needs the token typed again.
- "Link back to NetMap" (optional) adds a link to your NetMap in each message.
Summaries (1.92.0). A channel's Delivery is Immediately by default.
Set it to Daily summary or Weekly summary and everything it would have
sent waits (kv digest_held:<channel>, so a restart loses nothing) and goes
as one message at the chosen hour - and day, for weekly - grouped by event.
Nothing waiting, no message. Urgent events still go at once: at critical
level, of the kinds ticked under "Send at once even in a summary" (default
all four: a critical entry down, a source not answering, an expired
certificate, any other critical item) - and so does the all-clear for one. The hour is local to the time zone
under the channel list (default the container's TZ, else UTC). Switching
back to Immediately sends what was waiting straight away.
6. The three views
Overview is the landing view. It opens with the status strip: when
nothing needs you, one row - "All clear - nothing needs you" - with the
numbers (monitored entries up, sources fresh, entries and how many of them
could have a health check and have none, sightings); otherwise "N things
need you" and the queue, worst first in the order the server ranks it (a dead
source above what it would have reported). Each row has its rank, a type
(source, down, certificate, exposure, mismatch, conflict, coverage, verify),
when it started, an action (Retry scan, Open entry, Accept, Review, Mark all
verified) and Snooze; snoozed rows sit behind "N snoozed" and come back
when the situation changes. Below it come Hardware & blast radius (one
tile per physical host that something runs on: how many entries depend on it
through runs_on, VMs and containers included, how many of those a health
check watches, and amber notes when two systems the network leans on -
firewall, DNS, proxy, edge, read from where each source's URL points - share
the host, when three or more critical entries depend on it, or when it is
tagged backup:none; plus the network gear, up out of monitored), an
inventory-by-category breakdown with each category's monitored share (solid)
inside its total (faint), Quick launch (pinned services by name, each a
link with its URL's host name - ip:port when it has no URL - and its status,
plus up to two critical entries with a URL worth pinning, one click each), and
Changes: the change log and status changes grouped by day, a run of
changes by one actor to one entry (or to entries on one address) within two
hours folded into one row that opens, with All / Discovered / Edits / State
filters and the six newest groups. Address space draws every /24 in the
inventory as a grid of its usable addresses (.1 to .254), 32 to a row, coloured used, shared (two or
more entries), seen (a source sees something the inventory does not have),
reserved (a DHCP reservation, outlined) or free; each cell names its address
and state on hover, and one with entries opens Inventory filtered to exactly
that address (/api/entries?ip=, shown as a removable "IP" chip). The numbers and
the breakdown always describe the whole inventory, never the current filter.
Clicking a category jumps to Inventory filtered to it. Customize overview
(the icon next to Overview in the sidebar while the Overview is open; with
the sidebar collapsed or on a phone, the button under the cards) shows, hides and reorders the cards below the status;
the layout is stored on the server, one for the instance. The status strip is
always shown, first.
Inventory is the editable table (cards on a phone), grouped by category with a per-group health summary in each heading.
Network is the map: topology, address usage and port usage.
The chosen view is remembered per browser; typing in the search box switches to Inventory automatically.
Refreshing is quiet. The page re-reads every 60 seconds, but it compares a
signature of what it is showing - each entry's id, updated_at and up/down
state, plus the conflict count - and repaints nothing when that has not moved.
Latency figures are deliberately excluded, so normal jitter does not cause a
repaint. A hidden tab does not poll at all, and returning to the tab refreshes
immediately rather than waiting out the interval.
Kind - what a thing is
Categories say what an entry does, host says where it runs, and kind
says what it is: hardware, vm, container, service, rule. Three
independent axes, so a Pi-hole in an LXC that serves DNS is
category Core Network / host LXC / kind container with no conflict.
Kinds appear as filter chips and as a badge on each row.
Settings
The gear button opens Settings: a theme switch (Auto / Light / Dark - Auto follows the OS, and an explicit choice is remembered per browser) and an About panel showing version, counts, conflicts, last status sweep, check interval, uptime, database size and path, and whether the MCP endpoint is on or off. About never reveals the MCP path. A Data panel exports and imports the inventory (section 11).
Explanations of how something works sit behind an ⓘ next to what they explain - hover or focus on a desktop, tap on a phone, Esc or a tap elsewhere closes. What the page reports (counts, states, errors) stays visible. Every source and notification channel has an Enabled switch at the top of its form; off pauses it and keeps its settings and secrets.
A tab that has not refreshed for more than two minutes - a laptop that slept, a server that stopped answering - greys every status colour and says "Not refreshed since …" until fresh data arrives; it asks for it at once. On the Network page each source is its product's mark with a status bar under it (the name and last answer are in the tooltip); a source with no mark gets a drawn glyph. Source colours are re-checked against the clock every 15 seconds: amber means the source's last good answer is older than 5 hours - a setting under Settings › Sources (1–720), next to the scan interval.
Quick links
Tick Pin to Quick links on any entry to put it on the Overview. Over MCP,
pass pinned: true to create_entry / update_entry. Nothing is pinned by
default - the section explains itself until you pin something.
Conflicts
The Overview grows a Conflicts section whenever the inventory contradicts itself. Five rules, all deliberately narrow - a detector that cries wolf gets ignored:
| Rule | Severity | What it means |
|---|---|---|
port-clash |
high | one ip:port claimed by two entries |
address-clash |
high | two entries of kind hardware or vm on one IP |
duplicate-name |
review | the same name used twice |
duplicate-url |
review | two entries pointing at one URL |
unprobeable |
review | monitoring is on but there is nothing to probe |
Things that are normal on a home network are not conflicts: twenty containers
sharing their host's IP, or a port-forward naming a service that already
exists (rules are excluded by kind, by the port-forward tag, or by a name
beginning "Port forward"). To silence a deliberate pair, tag either entry
dup-ok. Claude sees the same list through the find_conflicts tool.
NetMap MCP questions
What tools does NetMap MCP provide?
It exposes 18 tools, including list_entries, get_entry, create_entry, update_entry, delete_entry.
How do I enable the MCP server?
The MCP server is disabled by default and requires you to set the NETMAP_MCP_TOKEN environment variable.
What happens if a single network check fails?
An entry is only marked as down after two consecutive failed checks to prevent false positives from single dropped pings.
Does NetMap require an external database?
No, it uses a single container with FastAPI and SQLite, requiring no external database.