AI-Driven Network Management via MCP
@waybarrios
| 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
De predecir palabras a ejecutar acciones
Un LLM es una red neuronal entrenada con enormes cantidades de texto para predecir la siguiente palabra en una secuencia.
Arquitectura Transformer — Vaswani et al., 2017
Un LLM es autoregresivo: genera un token a la vez, condicionado a los anteriores.
Logits → probabilidades. La palabra con mayor probabilidad se elige.
0.1 = determinista • 0.7 = balanceado • 1.5 = creativo
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])
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.
Function calling — Martin Fowler
| OpenAI | GPT-4o, GPT-4.1 |
| Anthropic | Claude Sonnet, Opus |
| Gemini 2.5 Pro/Flash | |
| OpenRouter | 100+ modelos, un API |
| vLLM | Produccion, batching, PagedAttention |
| TGI | HuggingFace, optimizado |
| TensorRT-LLM | NVIDIA, maxima velocidad |
| llama.cpp | GGUF, CPU/GPU, cualquier OS |
| Ollama | CLI simple, pull & run |
| LM Studio | GUI, descarga modelos, local |
| MLX / mlx-lm | Apple, Metal nativo |
| vLLM-MLX | vLLM + MLX, 400+ tok/s |
Todos exponen OpenAI-compatible API → cualquiera puede ser el backend de un agente MCP
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)
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.
"Hazme un ping a google.com y muestrame las interfaces de red"
Tools: ping + ifconfig
Bloque 2
Un estandar abierto para conectar agentes con herramientas
Cada proveedor de IA tiene su propio formato para tools. Si cambias de modelo, reescribes las integraciones.
Un servidor, un protocolo estandar. Cualquier agente compatible puede usar tus tools. Escribes una vez.
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.
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).
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()
"Que puertos estan escuchando y que procesos los tienen abiertos?"
Tools: ports_listening + net_processes
Principio de minimo privilegio: el agente empieza con tools pasivas y solo escala a activas cuando necesita mas informacion.
Bloque 4
Raw sockets, paquetes artesanales, analisis en tiempo real
Scapy es una libreria Python que permite construir, enviar, capturar y diseccionar paquetes de red a nivel raw.
Cada capa se apila con el operador / — Ether() / IP() / TCP()
Modelo OSI — las capas que Scapy manipula
Protocolo ARP — Request / Reply
ARP (Address Resolution Protocol) resuelve IPs a direcciones MAC en una red local.
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("."))))
Un solo paquete broadcast descubre todos los hosts activos en la subred
"Escanea mi red local y dime cuantos dispositivos hay conectados"
Tools: default_interface + scan_network (ARP)
El SYN scan (half-open scan) envia un SYN y analiza la respuesta sin completar la conexion.
Clave: sr() envia todos los SYN en paralelo. 75 puertos en ~2 segundos.
TCP 3-way handshake — el SYN scan interrumpe en paso 2
# 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)
sr() envia todos los SYN simultaneamente.
Nunca completa el handshake. Envia RST para limpiar.
HTTP, SSH, MySQL, Docker, K8s, Redis, y mas.
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:
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
ICMP (clasico), TCP:80 (pasa firewalls), UDP (stateless)
10 timeouts consecutivos → para. Evita esperar 60s+ en vano.
Bloque 6
Sin programar workflows, el agente aprende a diagnosticar
El agente descubre este flujo solo, leyendo los docstrings de las tools:
Zero workflow programming. El docstring de cada tool es suficiente para que el agente razone.
26 herramientas. Un servidor MCP. Cualquier agente de IA.
Codigo abierto.
@waybarrios
Gracias. Preguntas?