Are you the author? Sign in to claim
porthunter-mcp
PortHunter es un servidor MCP local (transport STDIO) que:
Está pensado para ser consumido por cualquier host/chatbot MCP.
python -m venv .venv
# Windows PowerShell: .\.venv\Scripts\Activate.ps1
# Linux/macOS: source .venv/bin/activate
pip install -U pip
pip install -e .
El
-e .instala el paqueteporthunteren editable desde este repo.
$env:PORT_HUNTER_TOKEN = "TEST_TOKEN"
$env:PORT_HUNTER_ALLOWED_DIR = (Get-Location).Path
python -m porthunter.server
set PORT_HUNTER_TOKEN=TEST_TOKEN
set PORT_HUNTER_ALLOWED_DIR=%CD%
python -m porthunter.server
export PORT_HUNTER_TOKEN=TEST_TOKEN
export PORT_HUNTER_ALLOWED_DIR="$PWD"
python -m porthunter.server
El servidor queda escuchando por STDIO a la espera de llamadas MCP
call_tool.
| Variable | Default | Descripción |
|---|---|---|
PORT_HUNTER_TOKEN | TEST_TOKEN | Token requerido si PORT_HUNTER_REQUIRE_TOKEN=true. |
PORT_HUNTER_REQUIRE_TOKEN | true | Exige auth_token en cada llamada de tool. |
PORT_HUNTER_ALLOWED_DIR | . | Directorio raíz permitido para leer PCAP/PCAPNG. |
PORT_HUNTER_MAX_PCAP_MB | 50 | Tamaño máximo del archivo a procesar. |
PORT_HUNTER_ALLOW_PRIVATE | false | Si true, permite enriquecer IPs privadas (por defecto se omite). |
mcp.json (ejemplo listo para usar){
"name": "porthunter",
"version": "0.1.0",
"transport": {
"stdio": { "command": "python", "args": ["-m", "porthunter.server"] }
},
"env": {
"PORT_HUNTER_TOKEN": "TEST_TOKEN",
"PORT_HUNTER_ALLOWED_DIR": ".",
"PORT_HUNTER_REQUIRE_TOKEN": "true",
"PORT_HUNTER_MAX_PCAP_MB": "50"
},
"tools": [
"scan_overview",
"list_suspects",
"first_scan_event",
"enrich_ip",
"correlate"
]
}
Todas las herramientas devuelven UTC ISO-8601 en
generated_at.
scan_overview(path, time_window_s=60, top_k=20)Input
{ "path": "captures/scan-demo.pcapng", "time_window_s": 60, "top_k": 20, "auth_token": "TEST_TOKEN" }
Return
{ "ok": true, "overview": { /* ver ejemplo */ }, "generated_at": "..." }
list_suspects(path, min_ports=10, min_rate_pps=5.0)Input
{ "path": "captures/scan-demo.pcapng", "min_ports": 10, "min_rate_pps": 5.0, "auth_token": "TEST_TOKEN" }
Return
{ "ok": true, "suspects": [ /* items */ ], "generated_at": "..." }
first_scan_event(path)Input
{ "path": "captures/scan-demo.pcapng", "auth_token": "TEST_TOKEN" }
Return
{ "ok": true, "first_event": { /* o null */ }, "generated_at": "..." }
enrich_ip(ip)Input
{ "ip": "8.8.8.8", "auth_token": "TEST_TOKEN" }
Return (ok)
{ "ok": true, "enrichment": { "asn": "...", "org": "...", "geo": { "country": "US" }, "threat": { "otx": {...}, "greynoise": {...} } }, "generated_at": "..." }
Return (error)
{ "ok": false, "error": "invalid_ip", "generated_at": "..." }
correlate(ips[])Input
{ "ips": ["abc", "192.168.0.10", "8.8.8.8"], "auth_token": "TEST_TOKEN" }
Return
{
"ok": true,
"results": [
{ "ip": "abc", "ok": false, "error": "invalid_ip" },
{ "ip": "192.168.0.10", "skipped": true, "reason": "private_ip" },
{ "ip": "8.8.8.8", "ok": true, "kind": "public", "enrichment": {/*...*/} }
],
"generated_at": "..."
}
scan_overview (ejemplo){
"ok": true,
"overview": {
"file": "captures/scan.pcapng",
"total_pkts": 12345,
"interval_s": 600,
"scanners": [
{
"ip": "1.2.3.4",
"pkts": 500,
"distinct_ports": 120,
"distinct_hosts": 30,
"flag_stats": { "SYN": 480, "FIN": 15, "XMAS": 5 }
}
],
"targets": [
{ "ip": "10.0.0.5", "pkts": 320, "ports_hit": [22, 80, 443] }
],
"port_distribution": [
{ "port": 80, "hits": 450 }, { "port": 22, "hits": 120 }
],
"suspected_patterns": ["syn_scan", "xmas_scan"]
},
"generated_at": "2025-09-20T23:00:02Z"
}
list_suspects (ejemplo){
"ok": true,
"suspects": [
{
"ip": "5.6.7.8",
"kind": "horizontal",
"distinct_ports": 50,
"rate_pps": 7.2,
"flags_seen": ["SYN"]
},
{
"ip": "9.9.9.9",
"kind": "vertical",
"distinct_ports": 1,
"rate_pps": 12.0,
"flags_seen": ["SYN","FIN"]
}
],
"generated_at": "2025-09-20T23:01:12Z"
}
first_scan_event (ejemplo){
"ok": true,
"first_event": {
"ts": "2025-09-20T22:59:58Z",
"src": "1.2.3.4",
"dst": "10.0.0.5",
"port": 80,
"flags": "S"
},
"generated_at": "2025-09-20T23:01:45Z"
}
enrich_ip (error por IP inválida){ "ok": false, "error": "invalid_ip", "generated_at": "2025-09-20T23:02:10Z" }
correlate (mixto){
"ok": true,
"results": [
{ "ip": "abc", "ok": false, "error": "invalid_ip" },
{ "ip": "192.168.0.10", "skipped": true, "reason": "private_ip" },
{ "ip": "8.8.8.8", "ok": true, "kind": "public" }
],
"generated_at": "2025-09-20T23:02:30Z"
}
{ "ok": false, "error": "path_outside_allowed_dir", "generated_at": "..." }
{ "ok": false, "error": "unsupported_file_type", "generated_at": "..." }
{ "ok": false, "error": "file_too_large", "generated_at": "..." }
{ "ok": false, "error": "unauthorized", "generated_at": "..." }
import asyncio, json
from mcp import StdioServerParameters, types
from mcp.client.stdio import stdio_client
from mcp.client.session import ClientSession
async def main():
params = StdioServerParameters(
command="python",
args=["-m", "porthunter.server"],
env={
"PORT_HUNTER_TOKEN": "TEST_TOKEN",
"PORT_HUNTER_ALLOWED_DIR": ".",
}
)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
resp = await session.call_tool(
name="scan_overview",
arguments={"path": "captures/scan-demo-20250906-1.pcapng", "auth_token": "TEST_TOKEN"}
)
# structuredContent preferente
sc = getattr(resp, "structuredContent", None)
if isinstance(sc, dict):
print(json.dumps(sc.get("result", sc), indent=2))
else:
text = "".join(b.text for b in resp.content if isinstance(b, types.TextContent))
print(text)
asyncio.run(main())
docker build -t porthunter-mcp .
docker run --rm -it \
-e PORT_HUNTER_TOKEN=TEST_TOKEN \
-e PORT_HUNTER_ALLOWED_DIR=/data \
-v "$PWD:/data" \
porthunter-mcp
python scripts/benchmark_porthunter.py captures/scan-demo-20250906-1.pcapng
Salida sugerida:
Incluye una tablita de resultados en el README si vas a reportar métricas.
porthunter/porthunter/utils/**pytest -qMIT (sugerida). Añade un archivo LICENSE si lo deseas.
Arranca con:
$env:PORT_HUNTER_TOKEN = "TEST_TOKEN"
$env:PORT_HUNTER_ALLOWED_DIR = (Get-Location).Path
python -m porthunter.server
Llama scan_overview / list_suspects / first_scan_event / enrich_ip / correlate y consume el JSON como en los ejemplos de arriba.
Run Claude Code as an MCP server so any agent can delegate coding tasks to it
Browser automation using accessibility snapshots instead of screenshots
Google's universal MCP server supporting PostgreSQL, MySQL, MongoDB, Redis, and 10+ databases
Official GitHub integration for repos, issues, PRs, and CI/CD workflows