Kali MCP Server
Kali MCP Server
A Docker-based Model Context Protocol (MCP) server that
exposes a curated set of Kali Linux security tools to an MCP client (Claude Desktop / Claude
Code) over stdio. It is built on kalilinux/kali-rolling, written in Python with
FastMCP, and runs as a non-root user with only the
specific Linux capabilities the offensive tools need. It exists for one purpose: authorized,
hands-on security testing of the operator's own home network and lab devices. Targets are
restricted in code to private ranges (192.168.x.x, 10.x.x.x, 172.16–31.x.x) — that permits the
full toolset against every device you own, and blocks pointing a tool at anything that isn't
yours. Nothing else.
The contract: no BS, no hallucination
This server's whole identity is trustworthy output. Mark makes real security decisions based on what these tools report, and a fabricated result is worse than no result — it can send you chasing a vulnerability that doesn't exist, or quietly reassure you about one that does. So every wrapper is built to the same rules:
Real output or a real failure — never a fabrication. A tool reports its actual stdout/stderr and the exact command that ran, or it says plainly that it didn't run.
"Errored" is not "found nothing." A tool that times out, isn't installed, or hits a permission error is a different fact from a tool that ran cleanly and found nothing. The two are never conflated.
Verdicts are earned, not defaulted. The rogue-host watcher's "all clear" only happens when devices were actually seen and every one matched — an empty or failed scan is reported as exactly that, not as a clean bill of health.
No invented specifics. IPs, MACs, ports, versions, CVEs — if there's no real result, the honest answer is "couldn't determine."
The authoritative statement of these rules — and of the locked architecture/scope decisions — is
CLAUDE.md. That file is the source of truth for this project; this README is
the front door.
Tools implemented today
These are the tools the server actually wraps and registers right now (verified in
kali_mcp/registry.py). Each one validates its input, runs the real
binary via an argument list (never a shell), captures real stdout/stderr, and surfaces a non-zero
exit as a clear failure:
Tool | What it does | Safety model |
| Reports every tool in the roster with its real install status (a live | Read-only; no target. |
| Port / service / version discovery. | Scope-gated (private-range only); |
| High-speed asynchronous port sweep. | Scope-gated + rate-limited ( |
| Bounded packet capture & protocol/talker summary; can also read an offline | Bounded (mandatory |
| Layer-2 host discovery on the local segment → | No scope gate needed (ARP is non-routable, self-limiting to the segment); interface shape-validated, explicit ranges must be private CIDRs. |
| Runs | Whitelist-diffed; refuses to give a verdict if the whitelist can't load. |
| Composes the whole platform's state into one honest snapshot: each roster tool's real install status, audit-log tallies, the whitelist's load state, and the last | Read-only; no target. Never fabricates a verdict — "no scan data", "all clear", and "whitelist broken" stay distinct (no data ≠ all clear). |
| Renders | Read-only + writes one HTML file (default |
Roster vs. reality.
CLAUDE.md §5lists a much larger offensive + defensive roster (nikto, nuclei, sqlmap, hydra, metasploit, suricata, zeek, …). That roster is the build target, not a claim of completion. Of the eight tools above, six wrap a roster binary and two (network_status,generate_dashboard) are platform tools that compose the others.list_toolsreports each roster binary's real install status — and everything outside the six wrapped binaries currently reportsinstalled: false, honestly, because it hasn't been wrapped or installed yet.
Security architecture
Non-root by design. The container runs as the unprivileged
pentesteruser — never root, never--privileged.Capabilities, not root. Raw-socket tools (nmap
-sS, masscan, arp-scan, tshark's dumpcap) get exactlyCAP_NET_RAW+CAP_NET_ADMINvia the composecap_addbounding set plus a reusablesetcap …+eipstep in theDockerfile. This is the Phase-2 caps pattern: a tool that ships without file-caps gets them added to one explicit list — never a blanket grant. (nmapalready ships its own caps and rides the bounding set for free.)Scope gate.
kali_mcp/scope.pyvalidates every routable target against private ranges (loopback + RFC1918 + ULA allowed; CGNAT, global, and link-local denied). It is enforced in both network modes — host networking gives more reach, never a weaker gate (8.8.8.8 stays refused under--network host).Append-only audit log. Every command run is logged as JSONL (tool, argv, target, exit code, duration, timestamp) to
logs/audit.jsonl(override withKALI_MCP_AUDIT_LOG). No code path bypasses it. A failed audit write (disk full, read-only mount, permission) never discards the real result or masquerades as a tool crash — the command's output is returned intact, the failure is surfaced on the result (audit_error) and shouted to stderr, so a run that couldn't be logged is loud rather than silent.DNS resolution off the event loop. Scope validation of a hostname target does a blocking
getaddrinfo; like the tool subprocess itself, it runs in a worker thread so a slow/hung resolver can't stall the MCP server.Input validation, no shell injection. Inputs are Pydantic-validated and commands are built as argument lists — no
shell=Truewith interpolated input.
Honest caveat — VPN killswitch vs. layer. On a host with an active Mullvad VPN killswitch (policy-routing
fwmarkrule), kernel-socket tools (nmap-sT/-sS, curl) read LAN TCP ports as filtered/down, while layer-2 / raw tools (masscan via AF_PACKET, arp-scan via ARP) still see the true LAN state — because they bypass the IP-socket policy routing. So on such a host the L2 tools reflect reality and the socket-based modes can disagree. That's the VPN, not a bug; keep it in mind when nmap and masscan/arp-scan diverge.
Network modes
Same image, same caps, same stdio — only the container's network stack differs (full reasoning in
NETWORKING.md):
Compose service | Network | Use it for |
| bridge (default, isolated) | loopback / self-tests. Start here. On bridge, host-discovery to LAN hosts is filtered, so your own router reads "down". |
|
| real LAN scans — your actual devices become reachable. A deliberate opt-in because it drops the bridge isolation layer. |
Quick start
1. Build the image
cd ~/building-mcps
docker compose build # tags kali-mcp:phase1
docker images | grep kali-mcp # confirm it exists2. Connect an MCP client
The server speaks MCP over the container's stdio (no network port is ever opened). The client
launches the container per session. Full instructions — Claude Desktop and Claude Code, both
network modes — are in CONNECTING.md; copy from
mcp-client-config.example.json. The short version:
# Claude Code, bridge (default):
claude mcp add-json kali-mcp '{"command":"docker","args":["run","-i","--rm","--cap-add","NET_RAW","--cap-add","NET_ADMIN","kali-mcp:phase1","python","server.py"]}'
# Claude Code, real-LAN (opt-in):
claude mcp add-json kali-mcp-lan '{"command":"docker","args":["run","-i","--rm","--network","host","--cap-add","NET_RAW","--cap-add","NET_ADMIN","kali-mcp:phase1","python","server.py"]}'3. Set up your device whitelist (for arp_watch)
cp whitelist.example.yaml whitelist.yaml # whitelist.yaml is gitignored — your real inventory
$EDITOR whitelist.yaml # add your devices: mac (required) + name; ip/note optional4. Run a first scan
Ask the client (in real-LAN mode) to run list_tools, then e.g. an arp_scan on your interface,
then arp_watch to diff it against your whitelist.
The whitelist & rogue-host watcher
arp_watch is the platform's headline feature: it answers "is there anything on my network I
don't recognize?"
Copy
whitelist.example.yaml→whitelist.yaml(gitignored, so your real MACs are never committed) and list your known devices. Amacis accepted in any common form (colon, hyphen, Cisco-dot, any case) and canonicalized internally, so format differences never cause a false alarm.nameis required;ipandnoteare optional.Run
arp_watchon your interface. It loads the whitelist, runs a realarp_scan, and classifies every discovered host into exactly one bucket, plus computes what's missing:Verdict
Meaning
KNOWN
MAC is in the whitelist (and, if an IP was specified, it matches).
ROGUE
MAC is not in the whitelist — the headline alert; reported with ip/mac/vendor so you can hunt it.
IP_MISMATCH
Known MAC, but on a different IP than expected — reported neutrally (could be DHCP, could be spoofing; stated as a fact, not an accusation).
ABSENT
A whitelisted device that didn't answer this scan — neutral (off/asleep/away).
A broken whitelist produces no verdict. If the whitelist is missing or malformed,
arp_watchrefuses and surfaces the load error instead of scanning and calling everything a rogue — a false alarm on a security tool is itself a failure. Likewise, if the underlyingarp_scanerrors or finds nothing, that real status is propagated — never repackaged as a fake "all clear."
The whitelist loader lives in kali_mcp/whitelist.py; the pure diff in
kali_mcp/watch.py.
The dashboard (Phase 4)
generate_dashboard turns the platform's real state into a self-contained static HTML file —
no web server, no open port, no external fonts or CDN. You open it straight from disk (file://).
It is a point-in-time snapshot, stamped with generated_at, not a live view — and it says so.
The full loop
The dashboard never scans on its own. It reads state that arp_watch produced, so the data flow
is explicit and each stage is independently honest:
arp_watch network_status generate_dashboard
│ runs a REAL │ reads the persisted │ inlines that snapshot into a
│ arp-scan + diff │ last_watch + probes │ self-contained HTML file
▼ ▼ tools/audit/whitelist ▼
state/last_watch.json ──► one honest snapshot ──────► state/dashboard.html ──► open in a browserarp_watchruns a realarp_scanand diffs it against your whitelist, then persists the result tostate/last_watch.json(gitignored — it holds real device MAC/IP).network_statusreads that persisted result (it does not run a scan), and probes the live install status, audit log, and whitelist, returning one structured snapshot.generate_dashboardembeds that snapshot into the dashboard template and writesstate/dashboard.html— self-contained, open it anywhere.
Honest states (this is the project's identity, in the UI)
The dashboard's whole reason to exist is that the three things a lazy dashboard collapses into one green light stay visually distinct — a green "all clear" is shown only when it's genuinely earned, never because data failed to load or is old:
State | What the dashboard shows |
Fresh + all clear | Green "ALL CLEAR — every device known" — only when devices were seen and all matched. |
Rogues present | A loud red alert with the rogue count and each rogue's ip/mac/vendor — the loudest thing on the page. |
Stale | An amber "⏱ DATA IS N OLD — re-run arp_watch" banner above the panel. A days-old all-clear is not a current all-clear. |
No data | A neutral grey/blue "ⓘ NO SCAN DATA — run arp_watch" panel — deliberately not green, never a pass. |
Whitelist broken | An amber "⚠ WHITELIST ERROR" with the load error — not green, not a rogue count. |
Generation failed |
|
Two timestamps are shown and never blurred: generated_at (when the snapshot was built) in the
header, and the network panel's own as_of (when arp_watch actually scanned). A fresh
snapshot can still carry an old scan — staleness is measured on as_of (default threshold 1 hour).
Accessibility (a requirement, not a nicety)
The dark-terminal theme is built for legibility: near-black background with bright (~17:1, past
WCAG AAA) foreground, large monospace text (≥19px), and status is never signalled by colour
alone — every state carries a symbol + text label (✓ KNOWN, ⚠ ROGUE, ≠ IP MISMATCH,
○ ABSENT), so it reads without colour perception.
Generate one
# 1. whitelist in place (see above), then run arp_watch in the real-LAN container so it persists state:
docker compose --profile lan run --rm -v "$PWD":/app kali-mcp-lan \
python -c "import asyncio; from kali_mcp.tools.arpwatch import watch; \
print(asyncio.run(watch(interface='wlan0'))['verdict'])"
# 2. build the dashboard from that real state:
docker compose --profile lan run --rm -v "$PWD":/app kali-mcp-lan \
python -c "from kali_mcp.dashboard import generate_dashboard as g; print(g()['path'])"
# 3. open it (it's self-contained — no server):
xdg-open state/dashboard.htmlIn normal use you'd drive steps 1–2 by asking your MCP client to run the arp_watch and
generate_dashboard tools; the commands above are the equivalent direct invocations. The
dashboard template + mock-state viewer are documented in
dashboard/README.md.
Desktop control panel
A native PySide6 GUI (desktop/) is the operator-facing companion to
the stdio MCP tools. It displays the honest platform state (the same rogue / all-clear /
no-data / stale / whitelist-broken distinctions the dashboard enforces) and can trigger
arp_watch on a chosen interface — through the same audited, scope-gated wrapper the MCP layer
uses, run in the container, so scope validation + the audit log apply to a GUI-triggered scan
exactly as to an MCP one. It's a native window — no port, no server (that's why a desktop app
was chosen over a web UI: it keeps the §4 no-open-port rule intact). The logic lives in a Qt-free,
fully-tested backend.py; the Qt layer is a thin view.
sudo apt-get install -y python3-pyside6.qtcore python3-pyside6.qtgui python3-pyside6.qtwidgets
python3 -m desktop.appProject status
Built one scoped task at a time; each commit on the branch is one task. Where things stand:
Phase 1 — core + first wrappers (done): the faithful executor (
run_tool) + audit logging, the private-range scope gate, honestlist_tools, and the first two tool archetypes —nmap_scan(active) andtshark_capture(passive/bounded).Phase 2 — capabilities & networking (done): the reusable non-root
setcapraw-socket pattern, the bridge-vs-host network modes, and two more wrappers —masscan_scanandarp_scan.Phase 3 — drivable + rogue-host watcher (done): real MCP-client connection config (
CONNECTING.md), the validated device whitelist store, and thearp_watchrogue-host watcher.Phase 4 — the dashboard (done): the
network_statushonest snapshot contract, the high-contrast accessible dashboard template, andgenerate_dashboard— a self-contained static HTML view of real platform state, with the fresh / stale / no-data / whitelist-broken / generation-failure states all kept visually distinct.
Deliberately not done yet (so this README doesn't imply more than exists):
Most of the
CLAUDE.md §5roster is unwrapped and uninstalled (nikto, nuclei, gobuster, sqlmap, hydra, john/hashcat, metasploit, enum4linux, the NIDS suite, etc.).nmap_scanexposes a fixed scan-type allow-list only — no-Pn(host-discovery skip) and no UDP (-sU) scanning yet.tshark_capture's BPF filter is a conservative allow-list (letters/digits/spaces and. : / -); brackets, arithmetic, and byte-offset filters are rejected, trading expressiveness for safety. Widening it is a future decision.GPU cracking, monitor-mode wireless, and session tools (responder/bettercap) need hardware passthrough and are not wrapped.
Tests
python -m pytest -q # 133 tests, all greenThe suite is fully offline: run_tool is monkeypatched with canned ToolResults built from
real sample tool output, so no live scanning or capture happens during tests. Every wrapper
has both happy-path and failure-path coverage (tool missing, bad input, timeout, permission
error) — failure handling is treated as a feature, not an afterthought.
Layout
.
├── CLAUDE.md # source of truth: rules, scope, locked decisions
├── README.md # this file
├── CONNECTING.md # connect an MCP client (Desktop / Code)
├── NETWORKING.md # bridge vs --network host, and why
├── Dockerfile # kali-rolling image; non-root pentester + setcap caps
├── docker-compose.yml # kali-mcp (bridge) + kali-mcp-lan (host, profile lan)
├── mcp-client-config.example.json
├── requirements.txt # fastmcp, pyyaml, pytest
├── pyproject.toml # pytest config (pythonpath, testpaths)
├── server.py # FastMCP entry point (stdio); wires the roster
├── whitelist.example.yaml # placeholder whitelist (committed)
├── whitelist.yaml # your real device inventory (gitignored)
├── dashboard/
│ ├── template.html # self-contained dark-terminal dashboard + render()
│ ├── mock_snapshots.js # fixture states for the offline mock viewer
│ └── README.md # dashboard shell + live-generation notes
├── desktop/ # native PySide6 control panel (display + trigger arp_watch)
│ ├── backend.py # Qt-free, tested core: snapshot + view-model + scan runner
│ ├── app.py # thin PySide6 view
│ └── README.md # run + architecture + accessibility notes
├── kali_mcp/
│ ├── executor.py # run_tool — the single faithful executor
│ ├── audit.py # append-only JSONL audit log
│ ├── scope.py # private-range target validator
│ ├── registry.py # the tool ROSTER + register_all wiring
│ ├── whitelist.py # device whitelist store + normalize_mac
│ ├── watch.py # pure rogue-host diff (KNOWN/ROGUE/IP_MISMATCH/ABSENT)
│ ├── state.py # persist/read the last arp_watch result (state/last_watch.json)
│ ├── status.py # build_status — the honest whole-platform snapshot
│ ├── dashboard.py # generate_dashboard core (snapshot -> self-contained HTML)
│ └── tools/
│ ├── meta.py # list_tools
│ ├── nmap.py # nmap_scan
│ ├── masscan.py # masscan_scan
│ ├── tshark.py # tshark_capture
│ ├── arpscan.py # arp_scan
│ ├── arpwatch.py # arp_watch
│ ├── status_tool.py # network_status
│ └── dashboard_tool.py # generate_dashboard
├── tests/ # offline unit tests (run_tool monkeypatched)
├── state/ # last_watch.json + generated dashboard.html (gitignored)
└── logs/audit.jsonl # runtime audit log (gitignored)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/t1964n/building-mcps'
If you have feedback or need assistance with the MCP directory API, please join our Discord server