Kali MCP Server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Kali MCP Serverscan network 192.168.1.0/24 for open ports"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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.
Related MCP server: Arsenal MCP
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)This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- 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