Scope
01 / 06Five tool groups, one diagnostic ladder
Small primitives the model composes — plus two composite tools whose value is
interpretation, not data. Everything returns the same shape regardless
of OS; the model never sees raw netsh or ip output.
| Group | Covers |
|---|---|
| net | Connectivity. Ping with honest TCP fallback, traceroute, HTTP timing split (dns/connect/ttfb), port check & bounded scan, TLS certificate grading, public IP — and an optional, opt-in nmap deep scan. |
| dns | Name resolution. Lookups across record types, reverse PTR, which resolvers the OS actually uses, propagation diffs — and the killer check: system resolver vs trusted DoH on known-good domains. |
| lan | Devices & inventory. ARP cache, RFC1918-gated sweeps, device inventory enriched with IEEE-OUI vendor and PTR hostname, dated snapshots, new-device diffs against baselines, and a confirm-gated Wake-on-LAN. |
| wifi | Link health. Current link (band, channel, RSSI, PHY rate), AP survey — and wifi.health, which interprets: RSSI quality bands, 2.4 GHz channel overlap, band mismatch, each with a concrete next step. |
| sec | Host posture. Active sockets with owning PID, listening-port exposure flags, firewall state per platform, a bounded speed test — and sec.exposure, the headline composite: exposed listeners × firewall × risky-port knowledge. |
Safety
02 / 06Bounded probes, gated mutations
Three tool classes. Reads run free; network probes carry hard caps and token-bucket rate limits; the one mutating tool plans first.
Local state: interfaces, ARP cache, sockets, firewall, snapshots — 15 tools, no gate.
Network probes: ping, sweep, scan, http, ssl, DoH — 15 tools, each behind hard caps (≤256 ports, ≤1024 hosts, byte ceilings, per-probe timeouts) and a token-bucket rate limit. Breach ⇒ RATE_LIMITED with retryAfterMs, never a retry loop.
One mutating tool: lan.wol — a single UDP magic packet, planned before it is sent.
Call lan.wol with a MAC. Nothing is sent. You get a PlannedAction summary and a confirmToken.
Do nothing. The token expires in 10 minutes; the network was never touched.
Re-call with the same MAC and the confirmToken. The service validates: single-use, action-bound — parameters frozen at plan time. Then one broadcast.
No shell, ever. Every subprocess spawns via arg arrays (execFile-style, shell: false); every input passes zod validation before it reaches the OS. No raw sockets, no Npcap, no Python — ICMP is the OS binary, ARP sweeps are ping-then-read-cache.
LAN tools stay on the LAN. lan.* sweeps and the nmap adapter accept RFC1918 targets only; public IPs are rejected with a redirect to the bounded alternatives. The inverted-SSRF rule you'd expect from a web tool would be wrong here — public targets are the point of net.* and dns.*, so the boundary is drawn per namespace, not per host.
Caps are ceilings, not suggestions. Config values are upper bounds; tool inputs may go lower, never higher. config.json refuses any key containing password, token, secret, or credential — credentials never ride in a config file.
Structure
03 / 06Four layers, one envelope
Every tool declares its backend chain; the platform registry resolves what
actually runs on this OS. net.health reports the resolved
capability matrix so the model never calls a tool that would 404.
L1 node stdlib net / dns / tls / http / dgram — identical on all platforms
L2 systeminformation interfaces, WiFi, connections, stats — well-known npm
L3 platform commands argv arrays + pure parsers (arp, ping, netsh, iw, …)
L4 optional binaries nmap adapter — capability-detected, opt-in, never required
src/
├── server.ts entrypoint: stdio + optional stateless HTTP (/mcp, /health)
├── config.ts zod-validated config; NETEXPERT_* env overrides; no secrets
├── exec/
│ ├── executor.ts CommandRunner interface + LocalExecutor (execFile, arg arrays)
│ └── platform.ts per-platform command maps: win32 / linux / darwin
├── parse/ pure parsers (arp, ping, traceroute) — fixture-tested
├── safety/
│ ├── caps.ts hard bounds + token-bucket rate limiter
│ └── confirm.ts ConfirmationTokenService — the dry-run/confirm core
├── services/ one module per tool group, each takes ServiceContext
│ ├── coreService net.* (health ping trace httpcheck portcheck portscan ssl publicip)
│ ├── dnsService dns.* (lookup reverse resolvers propagation hijackcheck)
│ ├── lanService lan.* (interfaces gateway arp sweep devices hostname snapshot newdevices wol)
│ ├── wifiService wifi.* (status networks health)
│ ├── secService sec.* (connections listening firewall exposure speedtest)
│ ├── nmapService net.deepscan (opt-in adapter)
│ └── util.ts envelope builders, ServiceContext, exec helpers
├── data/oui.json 40,099 IEEE OUI entries — committed, never fetched at runtime
└── mcp/
├── createServer.ts McpServer wrapper
└── toolset.ts all 31 tool registrations
Closed error alphabet. Seven codes, full stop: VALIDATION · BACKEND_MISSING · EXEC_FAILED · TIMEOUT · CAP_EXCEEDED · RATE_LIMITED · CONFIRMATION_REQUIRED. Every failure carries a hint — an actionable next step, not a shrug.
Parsers are pure and fixture-driven. Real captured output per platform (tests/fixtures/win32/arp-a.txt, linux/ip-j-neigh.json, …) feeds unit tests through the same CommandRunner interface production uses — a fake runner, zero real network in the test suite.
Honest backends. When ICMP is unavailable, net.ping says backend: "tcp-connect" rather than pretending. sec.speedtest reports a lower bound with complete: false when the transfer hits its cap, instead of failing on slow links.
Index
04 / 06The complete ledger — 31 tools
read runs directly probe capped & rate-limited gated plans first
| net.health | Version, platform, capability matrix with install hints, config caps. | read |
| net.ping | ICMP loss/latency (min/avg/max/mdev); honest TCP-connect fallback. | probe |
| net.trace | Traceroute hops: ttl, ip, latency — normalized across platforms. | probe |
| net.httpcheck | GET/HEAD timing split: dns / connect / ttfb / total, status, headers. | probe |
| net.portcheck | Single TCP connect: open/closed + latency. | probe |
| net.portscan | Bounded connect scan, default top common ports, cap 256. | probe |
| net.ssl | TLS cert: subject, SANs, issuer, expiry, days-left, grade. | probe |
| net.publicip | Public IPv4 + ASN/org, via configurable endpoints. | probe |
| net.deepscan | Optional nmap adapter — opt-in via config, RFC1918 targets only. | probe |
| dns.lookup | A/AAAA/CNAME/MX/TXT/NS/SRV/PTR with optional nameserver override. | probe |
| dns.reverse | PTR names for an IP. | probe |
| dns.resolvers | Which DNS servers the OS actually uses, per platform. | read |
| dns.propagation | Same record at system + trusted resolvers + DoH — diff table. | probe |
| dns.hijackcheck | Known-good domains: system vs trusted DoH; mismatch ⇒ Finding. | probe |
| lan.interfaces | NICs: name, MAC, addresses, type, virtual flag. | read |
| lan.gateway | Default gateway + interface + local subnet CIDR. | read |
| lan.arp | Parsed neighbor cache, stale entries filtered. | read |
| lan.sweep | Ping-probe an RFC1918 subnet (cap 1024 hosts) → live set. | probe |
| lan.devices | Inventory: ARP ∩ RFC1918, OUI vendor + PTR hostname enrichment. | read |
| lan.hostname | All names for an IP: PTR + NetBIOS (win). | probe |
| lan.snapshot | Save device inventory to dated snapshots; or list them. | read |
| lan.newdevices | Diff live vs baseline ⇒ Findings; first run auto-saves baseline. | read |
| lan.wol | Wake-on-LAN magic packet — dry-run plan → confirmToken → broadcast. | gated |
| wifi.status | Current link: SSID, band, channel, RSSI, PHY rate, quality 0–100. | read |
| wifi.networks | Nearby AP survey with band inference. | read |
| wifi.health | Interprets: RSSI bands, channel overlap, band mismatch ⇒ Findings. | read |
| sec.connections | Active sockets with owning PID/process. | read |
| sec.listening | Listeners per process; flags non-loopback exposures. | read |
| sec.firewall | Firewall state: profiles (win) / ufw→nft (linux) / socketfilterfw (mac). | read |
| sec.exposure | Headline composite: exposed listeners × firewall × risky ports ⇒ Findings. | read |
| sec.speedtest | Bounded Cloudflare download; lower-bound reporting on slow links. | probe |
Setup
05 / 06Configuration & wiring
Zero-config first run — defaults are sane and everything below is optional.
The server reads ~/.netexpert-mcp/config.json (override with
NETEXPERT_MCP_CONFIG); NETEXPERT_* env variables win
over the file.
# ~/.netexpert-mcp/config.json — every key optional { "defaults": { "pingCount": 4, "traceMaxHops": 20 }, "resolvers": { "trusted": ["1.1.1.1", "8.8.8.8"], "dohUrl": "https://cloudflare-dns.com/dns-query" }, "caps": { "portscan": { "maxPorts": 256 }, "sweep": { "maxHosts": 1024 } }, "rate": { "sweep": 30, "portscan": 10, "speedtest": 60, "httpCheck": 2 }, "nmap": { "enabled": false } } // caps are ceilings — inputs may go lower, never higher. // keys containing password / token / secret / credential are rejected.
Client wiring (stdio)
# official package — no clone, no build npx netexpert-mcp # or: npm i -g netexpert-mcp && netexpert-mcp # client config { "mcpServers": { "netexpert": { "command": "npx", "args": ["-y", "netexpert-mcp"] } } }
Optional HTTP transport
NETEXPERT_HTTP_PORT=3100 netexpert-mcp POST 127.0.0.1:3100/mcp # JSON-RPC, stateless (fresh dispatch per request) GET 127.0.0.1:3100/health # liveness
Optional binaries
nmap. Only net.deepscan. Install (winget install Insecure.Nmap / sudo apt install nmap), then set "nmap": { "enabled": true } in config — deep scans are opt-in by design. Absence produces an install hint, never a crash.
OUI database. src/data/oui.json (40k IEEE MA-L entries) is committed — never fetched at runtime. Regenerate on demand: node scripts/gen-oui.mjs.
Monitoring without a daemon
netexpert stays request/response. Recurring checks are the client's job —
e.g. watch_create-style recipes (where your client supports them):
new-device tripwire lan.newdevices every 15 min fire on Finding[] gateway reachability net.ping <gw> every 60 s fire on lossPct > 0 dns integrity dns.hijackcheck hourly fire on sev=high wifi degradation wifi.health every 10 min fire on severity ≥ med
Judgment
06 / 06The persona — hands meet judgment
The server provides hands; docs/persona.md provides the judgment —
a rule pack for AI clients. The triage ladder (link → ARP → IP/DNS → port → app,
always naming the layer that exonerated or implicated), the baselines for
"suspicious", the Finding{severity, title, evidence, nextStep}
report format, and composition rules with the ugnas-mcp
server for the NAS half.
Every finding ends with the concrete next tool call. No bare data dumps, no guessing: if a backend is missing, the hint is reported verbatim; if the ping fell back to TCP, it says "TCP probe", not "ping".