
20% de paquetes no existen: MCP defensivo en Claude Code
TL;DR
Un MCP defensivo es un servidor que se interpone entre Claude Code y comandos sensibles como pip install o npm i para validar que el paquete existe en el registry oficial antes de ejecutarlos. Sirve para cortar el slopsquatting: ataques que registran nombres de paquetes inventados por LLMs. En este artículo montamos uno en Python con mcp, lo conectamos a Claude Code y revisamos sus límites en producción.
El problema: cuando el agente se inventa un paquete
Has pedido a Claude Code que añada rate limiting a tu FastAPI y sugiere pip install fastapi-throttler. Suena bien, es coherente con la convención de nombres, pero ese paquete no existe en PyPI. Lo instalas sin mirar y, si alguien ha registrado ese nombre en el último minuto, acabas de ejecutar código arbitrario en tu máquina.
Este patrón tiene nombre: slopsquatting. Aprovecha que los LLMs alucinan nombres de paquetes verosímiles. Un estudio reciente sobre recomendaciones de modelos conversacionales cifra la tasa de paquetes inexistentes recomendados cerca del 20% en escenarios típicos. No es teórico: hay casos documentados de paquetes squatter registrados justo después de una alucinación popular.
El agente no tiene contexto del registry real. Si le das acceso a tu terminal, cada sugerencia no verificada es un vector de supply chain. Afecta también a Codex, Cursor y cualquier harness con shell.
¿Qué es un MCP defensivo?
Un servidor MCP defensivo expone herramientas que el agente debe llamar antes de ejecutar acciones sensibles, y devuelve un veredicto (permitir, bloquear, pedir confirmación). A diferencia de un MCP tradicional que añade capacidades, este añade guardrails operativos.
El patrón encaja con la tendencia de tratar el contexto y la seguridad como responsabilidades separadas dentro del workflow, en lugar de depender solo del prompt o de la memoria del modelo.
Comparativa: opciones de validación
| Enfoque | Cuándo sirve | Limitación |
|---|---|---|
| Hook de permisos en Claude Code | Bloquear comandos concretos | No inspecciona el contenido del paquete |
| MCP defensivo con consulta al registry | Validar existencia y metadatos | Añade latencia por cada dependencia |
| Lockfiles + CI | Entornos de equipo | No previene el primer install local |
Implementación paso a paso
1. Instalar el SDK de MCP para Python
El SDK oficial mcp expone un servidor con decoradores. Versión recomendada a fecha 22/04/2026: mcp>=1.6.
pip install "mcp[cli]>=1.6" httpx
2. Escribir el servidor que valida paquetes
Este snippet expone dos herramientas: check_pypi y check_npm. Cada una consulta el registry oficial y devuelve si el paquete existe, su última versión y metadatos básicos.
from mcp.server.fastmcp import FastMCP
import httpx
mcp = FastMCP("package-guard")
# Valida un paquete contra PyPI antes de que el agente proponga instalarlo
@mcp.tool()
async def check_pypi(name: str) -> dict:
url = f"https://pypi.org/pypi/{name}/json"
async with httpx.AsyncClient(timeout=5.0) as client:
r = await client.get(url)
if r.status_code == 404:
return {"exists": False, "verdict": "BLOCK", "reason": "no existe en PyPI"}
info = r.json().get("info", {})
return {"exists": True, "verdict": "ALLOW", "version": info.get("version")}
# Misma validación para el registry de npm
@mcp.tool()
async def check_npm(name: str) -> dict:
url = f"https://registry.npmjs.org/{name}"
async with httpx.AsyncClient(timeout=5.0) as client:
r = await client.get(url)
if r.status_code == 404:
return {"exists": False, "verdict": "BLOCK", "reason": "no existe en npm"}
latest = r.json().get("dist-tags", {}).get("latest")
return {"exists": True, "verdict": "ALLOW", "latest": latest}
if __name__ == "__main__":
mcp.run()
3. Registrarlo en Claude Code
Añade el servidor en ~/.claude.json o en el .mcp.json del proyecto.
{
"mcpServers": {
"package-guard": {
"command": "python",
"args": ["/ruta/absoluta/package_guard.py"]
}
}
}
4. Forzar al agente a usarlo
Un MCP disponible no es un MCP usado. Añade una regla en tu CLAUDE.md para que el agente consulte antes de instalar:
## Reglas de dependencias
- Antes de proponer `pip install X` o `npm i X`, llama a `check_pypi` o `check_npm`.
- Si el verdict es BLOCK, no ejecutes el install y pregunta al usuario.
Caso real: refactor con dependencias nuevas
Sesión típica en la que pides añadir caché con Redis a un servicio FastAPI. El agente propone tres paquetes: fastapi-cache2, redis-asyncio y aiocache-redis. Con el MCP activo, Claude Code llama a check_pypi para cada uno: los dos primeros existen, el tercero devuelve BLOCK. El agente para, te avisa y sugiere aiocache con extras. Te acaba de ahorrar un susto.
Si trabajas con pipelines Python en tiempo real o con proyectos Node con Prisma, el coste de una dependencia fantasma es mucho mayor que los 200 ms de latencia extra por consulta al registry. Y si tu arquitectura de microservicios tiene varios package.json, el ahorro se multiplica.
En Producción
Lo que funciona en tu máquina rompe de formas previsibles en un equipo.
- Latencia: cada consulta HTTP añade 100-300 ms. Cachea respuestas en memoria con TTL de 10 minutos si validas muchas dependencias seguidas.
- Falsos positivos: un paquete recién publicado puede no estar indexado. Devuelve
WARNen vez deBLOCKcuando no haya metadatos pero el nombre resuelva. - Paquetes privados: registries internos (Artifactory, GitHub Packages) requieren auth. Pasa el token por variable de entorno, nunca hardcodeado.
- Coste: consultas a PyPI y npm son gratuitas, pero respeta rate limits (PyPI recomienda menos de 10 req/s por IP).
- Alcance: esto valida existencia, no confianza. Un paquete real puede ser malicioso. Complementa con listas de reputación (Socket, Snyk) si el riesgo lo justifica.
Errores comunes
- Error: el agente ignora el MCP y ejecuta
pip installdirecto. Causa: falta regla explícita enCLAUDE.md. Solución: añade la regla y configura un hookPreToolUseque bloqueeBashconpip installsi no hubo llamada previa acheck_pypi. - Error:
httpx.ConnectErroren redes corporativas. Causa: proxy no configurado. Solución: respetaHTTPS_PROXYpasándolo al cliente httpx. - Error: nombres con guiones bajos vs guiones en Python. Causa: PyPI normaliza pero algunos endpoints no. Solución: normaliza el nombre a minúsculas con guiones antes de consultar.
Preguntas Frecuentes
¿Esto sustituye a un escáner como Socket o Snyk?
No. Un MCP defensivo valida que el paquete existe en el registry oficial, lo que ya elimina la clase de ataques por nombres inventados. Para detectar paquetes maliciosos reales (typosquatting, código ofuscado, post-install hooks sospechosos) necesitas un escáner especializado.
¿Funciona con Codex CLI o Cursor?
Sí, cualquier cliente compatible con MCP puede conectarse al mismo servidor. La regla que obliga a llamarlo se escribe en el archivo de instrucciones del harness (AGENTS.md en Codex, reglas de Cursor).
¿Añade mucho coste por sesión?
Las consultas a PyPI y npm son gratuitas y no consumen tokens del modelo (solo el JSON de respuesta). En una sesión típica con 3-5 dependencias nuevas el overhead es inferior a 2 segundos totales.
Cierre
Hemos visto cómo un servidor MCP de unas 40 líneas corta una clase entera de errores de supply chain en agentes de código. La idea general es simple: cuando el agente tiene shell, cada comando sensible merece una capa de validación que no dependa del prompt. Los guardrails viven mejor en código ejecutable que en instrucciones en lenguaje natural.
Si ya usas MCPs de productividad, este patrón es el siguiente paso lógico. En el próximo artículo voy a montar un hook PreToolUse que bloquea Bash cuando detecta curl | sh u otros patrones peligrosos. ¿Ya tienes guardrails en tu setup de Claude Code? Cuéntamelo en Twitter @sergiomarquezp_.


