Sniff, Scan, Speak

AI-Driven Network Management via MCP

Ether
IP
TCP
FastMCP

@waybarrios

Agenda

LLMs: de texto a acciones + como correrlos + demo 1
MCP: protocolo, JSON-RPC, FastMCP + demo 2
El proyecto: arquitectura, Scapy, ARP + demo 3
SYN scan, sniffing, traceroute
Comportamiento emergente
Cierre + Q&A

Bloque 1

Que es un LLM

De predecir palabras a ejecutar acciones

Large Language Model

Un LLM es una red neuronal entrenada con enormes cantidades de texto para predecir la siguiente palabra en una secuencia.

  • Entrenado con billones de tokens (libros, web, codigo)
  • Aprende patrones estadisticos del lenguaje
  • No "entiende" — predice probabilidades
  • GPT, Claude, Llama, Gemini — todos funcionan asi
Transformer Architecture

Arquitectura Transformer — Vaswani et al., 2017

Prediccion del siguiente token

Un LLM es autoregresivo: genera un token a la vez, condicionado a los anteriores.

$$P(x_1, x_2, \ldots, x_T) = \prod_{t=1}^{T} P(x_t \mid x_1, x_2, \ldots, x_{t-1})$$

Softmax

Logits → probabilidades. La palabra con mayor probabilidad se elige.

Temperatura

0.1 = determinista • 0.7 = balanceado • 1.5 = creativo

Generacion autoregresiva en Python

Asi funciona internamente la generacion token por token:

import torch, torch.nn.functional as F

def generate(model, tokenizer, prompt, max_tokens=50, temperature=0.7):
    """Generacion autoregresiva — el loop central de todo LLM."""
    input_ids = tokenizer.encode(prompt, return_tensors="pt")

    for _ in range(max_tokens):
        with torch.no_grad():
            logits = model(input_ids).logits[:, -1, :]   # prediccion del siguiente token

        probs = F.softmax(logits / temperature, dim=-1)   # logits -> probabilidades
        next_token = torch.multinomial(probs, num_samples=1)  # samplear de la distribucion
        input_ids = torch.cat([input_ids, next_token], dim=-1)  # agregar al contexto

        if next_token.item() == tokenizer.eos_token_id:
            break  # fin de secuencia

    return tokenizer.decode(input_ids[0])

El salto: de texto a acciones

Antes, los LLMs solo generaban texto. Ahora pueden invocar funciones externas — esto se llama tool use o function calling.

Antes: "La IP del servidor es probablemente 192.168.1.1"
Ahora: El modelo llama ping("192.168.1.1") y te da la respuesta real.

El modelo no ejecuta codigo directamente — genera una llamada estructurada que un runtime ejecuta y le devuelve el resultado.

LLM Function Calling

Function calling — Martin Fowler

Como correr LLMs

APIs comerciales

OpenAIGPT-4o, GPT-4.1
AnthropicClaude Sonnet, Opus
GoogleGemini 2.5 Pro/Flash
OpenRouter100+ modelos, un API

GPU servers (NVIDIA)

vLLMProduccion, batching, PagedAttention
TGIHuggingFace, optimizado
TensorRT-LLMNVIDIA, maxima velocidad

CPU / local

llama.cppGGUF, CPU/GPU, cualquier OS
OllamaCLI simple, pull & run
LM StudioGUI, descarga modelos, local

Apple Silicon (ARM)

MLX / mlx-lmApple, Metal nativo
vLLM-MLXvLLM + MLX, 400+ tok/s

github.com/waybarrios/vllm-mlx

Todos exponen OpenAI-compatible API → cualquiera puede ser el backend de un agente MCP

Un LLM server en la practica

vLLM-MLX — servidor OpenAI-compatible en Apple Silicon

# Levantar el servidor (un comando)
vllm-mlx serve \
  mlx-community/Llama-3.2-3B-Instruct-4bit \
  --port 8000
# Usar con OpenAI SDK
from openai import OpenAI
client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed")

r = client.chat.completions.create(
    model="default",
    messages=[{"role": "user",
      "content": "Que es TCP?"}])
print(r.choices[0].message.content)

Servidor arrancando en M4 Max

vLLM-MLX running

Streaming response — token por token

vLLM-MLX streaming response

Function calling en codigo

Request — define tool y envia prompt:

from huggingface_hub import InferenceClient
client = InferenceClient(model_url)

tools = [{"type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get forecast",
    "parameters": {"type": "object",
      "properties": {
        "location": {"type": "string"},
        "days": {"type": "integer"}
}}}}]

response = client.chat_completion(
  messages=[{"role": "user",
    "content": "Clima en Bogota 3 dias"}],
  tools=tools, tool_choice="auto")

Response — el modelo elige la funcion:

>>> response.choices[0].message.tool_calls
[{
  "function": {
    "name": "get_weather",
    "arguments": {
      "location": "Bogota",
      "days": 3
    }
  }
}]

>>> # No respondio texto!
>>> # Genero una llamada estructurada.
>>> # El runtime ejecuta get_weather()
>>> # y devuelve el resultado al modelo.

Demo 1

"Hazme un ping a google.com y muestrame las interfaces de red"

Tools: ping + ifconfig

Demo 1 - ping + interfaces

Bloque 2

Model Context Protocol

Un estandar abierto para conectar agentes con herramientas

El problema que resuelve MCP

Sin MCP

Cada proveedor de IA tiene su propio formato para tools. Si cambias de modelo, reescribes las integraciones.

vs

Con MCP

Un servidor, un protocolo estandar. Cualquier agente compatible puede usar tus tools. Escribes una vez.

MCP es como un USB para la IA — un conector universal entre modelos y herramientas.

Arquitectura MCP

MCP Architecture

MCP High-Level Architecture — icodealot.com

Host (Claude Code, IDE) contiene el Client que habla JSON-RPC con el Server. El server expone tools, resources y prompts. El agente lee la lista y decide que usar.

JSON-RPC: el protocolo de transporte

RPC (Remote Procedure Call) permite ejecutar una funcion en otro proceso como si fuera local. JSON-RPC 2.0 usa JSON como formato — ligero, sin estado, sobre stdio o HTTP.

// Request: el agente llama una tool
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "scan_network",
    "arguments": {"network": "192.168.1.0/24"}
  }
}
// Response: resultado
{
  "jsonrpc": "2.0",
  "result": {
    "content": [{
      "type": "text",
      "text": "Hosts descubiertos: ..."
    }]
  }
}

Capa OSI 7 (Aplicacion). MCP transporta JSON-RPC sobre stdio (local) o HTTP+SSE (remoto).

FastMCP: asi de simple

282 lineas. 26 tools. El docstring ES la interfaz que el agente lee.

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("networking")

@mcp.tool()
def scan_network(network: str, timeout: int = 3) -> str:
    """Descubre hosts activos en la red usando ARP scan.
    Args:
        network: Rango CIDR (ej: "192.168.1.0/24")
        timeout: Tiempo de espera en segundos
    """
    return arp_scan(network, timeout)

# ... 25 tools mas con el mismo patron ...

if __name__ == "__main__":
    mcp.run()

Demo 2

"Que puertos estan escuchando y que procesos los tienen abiertos?"

Tools: ports_listening + net_processes

Demo 2 - ports + processes

Lo que construimos

26
tools de analisis de red
13
pasivas (sin root)
13
activas (Scapy + raw sockets)
75+
puertos en SYN scan
3
OS soportados
~1500
lineas de codigo

Capas de la arquitectura

AI Agent — Claude, GPT, Llama, cualquier cliente MCP
| JSON-RPC via stdio |
FastMCP Server — mcp = FastMCP("networking")
| @mcp.tool() |
13 Pasivas
subprocess → comandos del OS
13 Activas
Scapy → raw sockets
|
macOS: scutil, networksetup, airport, lsof  •  Linux: ip, ss, nmcli, resolvectl  •  Windows: netsh, ipconfig, netstat

Principio de minimo privilegio: el agente empieza con tools pasivas y solo escala a activas cuando necesita mas informacion.

Bloque 4

Scapy Deep Dive

Raw sockets, paquetes artesanales, analisis en tiempo real

Que es Scapy

Scapy es una libreria Python que permite construir, enviar, capturar y diseccionar paquetes de red a nivel raw.

  • Acceso directo a raw sockets (/dev/bpf en macOS)
  • Construye paquetes capa por capa
  • Envia y recibe con sr(), srp(), sniff()
  • Disecciona respuestas automaticamente
Ether
IP
TCP
Payload

Cada capa se apila con el operador /Ether() / IP() / TCP()

Scapy Logo OSI Model

Modelo OSI — las capas que Scapy manipula

ARP Discovery L2

ARP Protocol

Protocolo ARP — Request / Reply

ARP (Address Resolution Protocol) resuelve IPs a direcciones MAC en una red local.

  • Broadcast: "Quien tiene 192.168.1.X?"
  • Cada host activo responde con su MAC
  • Enviamos a todo el CIDR de una vez
  • Solo funciona en la LAN (Layer 2)

ARP Discovery: el codigo

from scapy.all import ARP, Ether, srp

def arp_scan(network: str, timeout: int = 3) -> str:
    # Construir paquete: Ethernet broadcast + ARP request
    broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
    packet = broadcast / ARP(pdst=network)  # ej: "192.168.1.0/24"

    # Enviar y recibir respuestas
    answered, _ = srp(packet, timeout=timeout, verbose=0)

    hosts = []
    for sent, received in answered:
        ip = received.psrc       # IP del host que respondio
        mac = received.hwsrc     # MAC address
        vendor = _mac_vendor_hint(mac)  # OUI lookup
        hosts.append((ip, mac, vendor))

    # Ordenar por IP y formatear salida
    hosts.sort(key=lambda x: list(map(int, x[0].split("."))))
Ether
dst=ff:ff:ff:ff:ff:ff
ARP
pdst=192.168.1.0/24

Un solo paquete broadcast descubre todos los hosts activos en la subred

Demo 3

"Escanea mi red local y dime cuantos dispositivos hay conectados"

Tools: default_interface + scan_network (ARP)

Demo 3 - ARP Discovery

TCP SYN Scan 75+ puertos

El SYN scan (half-open scan) envia un SYN y analiza la respuesta sin completar la conexion.

  • SYN-ACK (0x12) = puerto abierto
  • RST-ACK (0x14) = puerto cerrado
  • Sin respuesta = filtrado
  • Enviamos RST para limpiar (nunca connect())

Clave: sr() envia todos los SYN en paralelo. 75 puertos en ~2 segundos.

TCP Handshake

TCP 3-way handshake — el SYN scan interrumpe en paso 2

SYN Scan: el codigo

# Enviar TODOS los SYN de golpe (paralelo, no secuencial)
pkts = IP(dst=target) / TCP(dport=port_list, flags="S")
answered, unanswered = sr(pkts, timeout=2, verbose=0)

for sent, received in answered:
    if received[TCP].flags == 0x12:    # SYN-ACK = ABIERTO
        open_ports.append(sent[TCP].dport)
    elif received[TCP].flags == 0x14:  # RST-ACK = cerrado
        closed_ports += 1

# Limpieza: cerrar half-open connections
rst = IP(dst=target) / TCP(dport=[p for p, _ in open_ports], flags="R")
sr(rst, timeout=1, verbose=0)

Paralelo

sr() envia todos los SYN simultaneamente.

Half-open

Nunca completa el handshake. Envia RST para limpiar.

75+ puertos

HTTP, SSH, MySQL, Docker, K8s, Redis, y mas.

Packet Sniffing

Captura y diseccion de paquetes en tiempo real con filtros BPF

packets = sniff(count=20, filter="tcp port 80", timeout=30)

for pkt in packets:
    if pkt.haslayer(TCP):
        flags = str(pkt[TCP].flags)       # SYN, ACK, FIN...
        info = f":{pkt[TCP].sport} -> :{pkt[TCP].dport} [{flags}]"

    if pkt.haslayer(DNS) and pkt[DNS].qr == 0:  # Solo queries
        info = f"Query: {pkt[DNS].qd.qname.decode()}"

Diseccion por capas — cada paquete se descompone automaticamente:

Ether
src/dst MAC
IP
src/dst addr
TCP/UDP
ports + flags
DNS/HTTP
payload

Traceroute inteligente

MAX_CONSECUTIVE_TIMEOUTS = 10  # Abort inteligente

for ttl in range(1, max_hops + 1):
    if method == "tcp":
        pkt = IP(dst=target, ttl=ttl) / TCP(dport=80, flags="S")
    elif method == "udp":
        pkt = IP(dst=target, ttl=ttl) / UDP(dport=33434 + ttl)
    else:  # icmp
        pkt = IP(dst=target, ttl=ttl) / ICMP()

    resp = sr1(pkt, timeout=1, verbose=0)

    if resp is None:
        consecutive_timeouts += 1
        if consecutive_timeouts >= MAX_CONSECUTIVE_TIMEOUTS:
            break   # No esperar 60s en vano
        continue
    consecutive_timeouts = 0  # Reset al recibir respuesta

3 metodos

ICMP (clasico), TCP:80 (pasa firewalls), UDP (stateless)

Abort inteligente

10 timeouts consecutivos → para. Evita esperar 60s+ en vano.

Bloque 6

Comportamiento emergente

Sin programar workflows, el agente aprende a diagnosticar

10 pasos que nadie programo

El agente descubre este flujo solo, leyendo los docstrings de las tools:

1 Info basica — ping, ifconfig, wifi "hay red?"
2 Reconocimiento — default_interface IP, MAC, CIDR
3 Conexiones — connections, ports, lsof "que esta abierto?"
4 Descubrimiento — scan_network (ARP) hosts en la red
5 Escaneo — tcp_port_scan, udp_port_scan servicios expuestos
6 Trafico — traffic_stats, sniff "que pasa en la red?"
7 DNS — resolve, reverse, ns_lookup anomalias DNS
8 Ruta — trace_route camino de red
9 Calidad — net_quality latencia, jitter, BW
10 Investigacion — whois contexto externo

Cada resultado informa la siguiente decision

Paso 1: default_interface() → IP: 192.168.1.42, CIDR: 192.168.1.0/24
Paso 2: scan_network("192.168.1.0/24") → 7 hosts: .1 (router), .42 (yo), .100, .101, .105, .110, .200
Paso 3: tcp_port_scan("192.168.1.200") → Puertos abiertos: 22 (SSH), 80 (HTTP), 3306 (MySQL)
Paso 4: net_processes() + traffic_stats() → "MySQL 3306 abierto sin conexiones activas"
Conclusion del agente: "El host .200 tiene MySQL expuesto en la LAN sin uso activo — posible misconfiguracion de seguridad."

Zero workflow programming. El docstring de cada tool es suficiente para que el agente razone.

Recapitulando

26
tools de analisis de red
1
servidor MCP
agentes compatibles
  • FastMCP + Scapy + raw sockets = analisis completo de red
  • 13 pasivas (sin root) + 13 activas (Scapy) = minimo privilegio
  • Multiplataforma: macOS, Linux, Windows
  • Comportamiento emergente: el agente aprende el workflow solo
  • Cada resultado informa la siguiente decision
No programamos un workflow de diagnostico. Construimos herramientas atomicas y dejamos que la inteligencia emergiera.

Sniff. Scan. Speak.

26 herramientas. Un servidor MCP. Cualquier agente de IA.
Codigo abierto.

@waybarrios

Gracias. Preguntas?