ESET Protect MCP
Servidor MCP para la API de ESET Connect: 102 herramientas, modo RO/RW, stdio+HTTP, OAuth2
Documentación
ESET-MCP
Un servidor de Model Context Protocol para toda la superficie de gestión de ESET: ESET Connect (nube, todas las regiones), ESET PROTECT On-Prem, ESET Inspect y ESET Cloud Office. Dirige cualquiera de ellos desde cualquier host MCP (Claude Desktop, Claude Code, o un agente personalizado) mediante herramientas, recursos y prompts.
Construido como un único centro para cualquier número de despliegues de ESET. Un solo proceso hace frente a consolas en la nube y on-prem al mismo tiempo; los clientes eligen el destino por solicitud mediante cabeceras. Mientras MCP reciba credenciales válidas (autenticación básica, más una anulación de URL opcional y un token de servicio de Cloudflare Access opcional), enruta la llamada al backend correcto, genera sus propios tokens y mantiene a los inquilinos aislados en el pool.
⚠️ Para que quede claro, amigos
Este es un proyecto independiente de código abierto impulsado por la comunidad y no está afiliado, respaldado oficialmente ni avalado por ESET, spol. s r.o. ESET y sus nombres de productos son marcas registradas de sus respectivos propietarios.Aunque se ha hecho todo lo posible para garantizar que este software sea seguro y robusto (incluida la estricta puerta de modo de solo lectura), este código se proporciona "TAL CUAL", sin garantía de ningún tipo. Usted es el único responsable de cómo utiliza esta herramienta y de cualquier cambio realizado en su entorno ESET.
Tabla de contenidos
- Características
- Arquitectura de un vistazo
- Seguridad
- Inicio rápido
- Configuración
- Despliegue multiinquilino (modo autenticación básica)
- Soporte para ESET PROTECT On-Prem
- Despliegue en producción (HTTPS mediante Caddy)
- Herramientas, recursos y prompts
- Arquitectura
- Pruebas
- Actualización de las especificaciones OpenAPI
- Licencia
Características
Cobertura completa de la API
- 102 herramientas generadas automáticamente a partir de 16 especificaciones oficiales de ESET Connect OpenAPI 3.0.1, que cubren gestión de aplicaciones, gestión de activos, automatización, gestión de dispositivos, identidad, gestión de incidentes, gestión de instaladores, gestión de dispositivos móviles, protección de acceso a red, gestión de parches, gestión de políticas, gestión de cuarentena, gestión de usuarios, gestión de vulnerabilidades y protección de acceso web.
- 4 composites de alto nivel que combinan 3-6 llamadas brutas en una:
eset_search,device_full_profile,incident_full_context,latest_detections.
Modos de solo lectura / lectura-escritura
ESET_MODE=RO→ el catálogo expone solo herramientas de solo lectura (51 en total). Las herramientas de escritura están ocultas delist_toolspor completo.ESET_MODE=RW→ se anuncian las 106 herramientas; las herramientas de mutación llevandestructiveHint: trueen sus anotaciones MCP.- Independiente de los permisos subyacentes de la cuenta ESET.
- Una puerta en memoria de defensa en profundidad rechaza los nombres de herramientas RW en modo RO antes de que se envíe cualquier solicitud HTTP.
Autenticación
ESET_AUTH_MODE=env- inquilino único, credenciales de.env.ESET_AUTH_MODE=basic- multiinquilino, los clientes pasanAuthorization: Basic <base64(user:password)>por solicitud (además de opcionalmenteX-ESET-Regionpara una región de nube diferente, oX-ESET-Server-URLpara enrutar la solicitud a una consola PROTECT on-prem). Un servidor hace frente a muchas cuentas ESET y puede mezclar nube + on-prem en el mismo proceso.- Tokens OAuth por inquilino, agrupados y aislados por
(user, password_hash, deployment, region-or-server-url, cf_secret_hash). Rotar una contraseña o un token de servicio de Cloudflare Access genera un cliente nuevo; los clientes de nube y on-prem para el mismo usuario nunca comparten una entrada del pool.
Transportes
- stdio - JSON-RPC sobre stdin/stdout para hosts locales.
- HTTP Streamable - el transporte MCP actual (especificación de noviembre de 2025).
Multirregión
eu / de / us / ca / jpn. Fijado mediante ESET_REGION en modo env;
por solicitud mediante X-ESET-Region en modo basic.
Nube + on-prem en un solo proceso
Además de las regiones de nube, un solo servidor MCP puede hacer frente a
consolas ESET PROTECT On-Prem alojadas por el cliente. El formato de autenticación
on-prem (POST /GetTokens con una respuesta en camelCase) y la estructura de URL
por host se manejan de forma transparente; los clientes eligen el destino por solicitud
mediante la cabecera X-ESET-Server-URL. Consulte
Soporte para ESET PROTECT On-Prem.
Cloudflare Access (opcional)
Cuando la consola on-prem está detrás de un túnel de Cloudflare Access, MCP
se autentica como un token de servicio
(por defecto de entorno o por solicitud X-ESET-CF-Access-Client-Id /
X-ESET-CF-Access-Client-Secret) y atraviesa hasta el origen. El
token CF es una capa de ingreso adicional delante de - no un reemplazo de -
las credenciales de la cuenta ESET. Las solicitudes de nube nunca llevan estas cabeceras.
Resiliencia
- OAuth2 con renovación proactiva ~5 minutos antes de la expiración del token y una renovación forzada + reintento en 401.
- Reintentos 429 con retroceso exponencial (hasta 3 intentos, respeta
Retry-After). - Paginación (
nextPageToken) recorrida de forma transparente. - Sondeo largo 202 con la cabecera
response-id, hasta 10 minutos.
Forma de la respuesta (protección de la ventana de contexto)
Una sola llamada list_* sin límite puede devolver cientos de KB, suficiente para
desbordar el contexto de un modelo. Se aplican dos transformaciones a cada
respuesta de herramienta:
- Proyección
fields- cada herramienta GET expone un parámetro opcionalfields: [string]que filtra cada elemento de lista hasta las claves solicitadas (p. ej.["uuid", "displayName"]). Aplicado en el lado del servidor después de la obtención. - Límite de bytes (
ESET_MCP_RESPONSE_BYTES_MAX, por defecto 100 KB) - si una carga útil aún supera el presupuesto, la lista más larga se recorta mientras se conserva cada campo de nivel superior (nextPageToken,totalSize, …), y se adjunta un bloque de metadatos_cappedcon una pista accionable sobre cómo continuar. Los agentes conservan acceso completo a los datos mediante la paginación.
Errores amigables para agentes
Los errores HTTP se asignan a pistas legibles: 403 → comprobar los Permission Sets en ESET PROTECT Hub; 401 → el servidor renueva el token automáticamente; 429 → retroceder; 5xx → reintentar en breve.
Observabilidad (registros + Prometheus)
- Registros estructurados en stderr. Texto (por defecto) para desarrollo, JSON Lines para
los shippers de registros de producción mediante
ESET_MCP_LOG_FORMAT=json. Cada llamada de herramienta, renovación de token, reintento HTTP y expulsión del pool emite un registro tipadoeventcon campos de baja cardinalidad (tool, deployment, status, duration_ms, response_bytes, ...). - Métricas Prometheus en un endpoint
/metricsopcional (ESET_MCP_METRICS_ENABLED=true, requierepip install eset-mcp[metrics]). Contadores para llamadas de herramienta, renovaciones de token, reintentos HTTP, aciertos de límite; histogramas para duración de herramientas y tamaños de respuesta; gauge para el tamaño del pool de clientes. - Lo que nunca entra en registros o métricas: contraseñas, cabeceras
Authorization, secretos de CF Access, cuerpos de solicitud/respuesta, cadenas de consulta, parámetros de ruta sustituidos (que pueden filtrar UUIDs). Una lista de denegación defensiva en el registrador elimina claves conocidas como sensibles antes de que cualquier formateador las vea. - Aislamiento de fallos: la emisión de telemetría está envuelta en try/except para que
un registro de métricas o formateador roto nunca convierta una llamada de herramienta
exitosa en un error para el agente.
/metricsdevuelve 500 (no 503) si la exposición alguna vez lanza - el worker permanece activo. - Silenciamiento en producción: establezca
ESET_LOG_LEVEL=WARNINGpara silenciar los eventos INFO por llamada pero mantener visibles los reintentos/errores; establezcaESET_LOG_LEVEL=ERRORpara silenciar todo excepto fallos graves. Desactive las métricas por completo conESET_MCP_METRICS_ENABLED=false(por defecto). Los tres controles son independientes.
Arquitectura de un vistazo
La configuración más simple: credenciales en .env, un host MCP, una región de nube
ESET. Adecuado para uso personal, un solo equipo o un cliente de IA de escritorio como
Claude Desktop o Claude Code.
Para despliegues reales multiinquilino o empresariales, coloque un gestor de credenciales delante de ESET-MCP. El diagrama siguiente muestra un patrón de este tipo usando IBM mcp-context-forge como la capa de "gestión de credenciales" - cualquier puerta de enlace MCP equivalente (o su propio proxy de autenticación) funciona de la misma manera:
El forge mantiene secretos por inquilino e inyecta las cabeceras Authorization: Basic,
X-ESET-Region, X-ESET-Server-URL y X-ESET-CF-Access-* por
solicitud. ESET-MCP enruta cada solicitud al backend correcto (región de nube,
consola PROTECT on-prem u on-prem detrás de Cloudflare Access) y el
pool de clientes LRU por inquilino mantiene los tokens OAuth completamente aislados entre
inquilinos. Este es un patrón funcional, no aspiracional - las cabeceras, claves
del pool y reglas de enrutamiento descritas aquí están todas en la suite de pruebas bajo
tests/test_concurrency.py y tests/test_onprem.py.
Seguridad
Credenciales
- En modo
envla contraseña se lee una vez al inicio y se mantiene en memoria. - En modo
basicla contraseña está en el cable solo durante la duración de la solicitud, y en memoria solo mientras el cliente por inquilino está activo en el pool LRU. Nunca se registra. - La clave del pool usa un hash SHA-256 de la contraseña en lugar de la contraseña en sí.
- Los tokens de acceso/renovación OAuth se mantienen por inquilino; los tokens nunca cruzan límites de inquilino dentro de una sola sesión.
Modos de autenticación y transporte
| Modo | Transporte permitido | Fuente de credenciales |
|---|---|---|
env | stdio o http | .env (ESET_USER / ESET_PASSWORD) |
basic | solo http | Cabecera Authorization: Basic por solicitud |
El modo basic sobre HTTP simple filtraría contraseñas. El servidor aplica
transporte HTTP para el modo basic al inicio pero no aplica TLS -
eso es trabajo del despliegue. El perfil docker-compose prod pone
el servidor detrás de Caddy + Let's Encrypt.
Authorization faltante o malformado en modo basic → HTTP 401 con un
desafío WWW-Authenticate: Basic. Región desconocida en X-ESET-Region
→ HTTP 401.
Aislamiento RO / RW
Dos capas independientes:
- Ocultación del catálogo -
list_toolsfiltra cada herramienta no GET en modo RO. El agente nunca ve herramientas de escritura. - Puerta de defensa en profundidad -
call_toolvalida el modo declarado de la herramienta contraESET_MODEantes de que salga cualquier solicitud HTTP. Clientes codificados, intentos de inyección de prompts y capturas de agente obsoletas chocan todos con la puerta y reciben una respuesta de texto estructuradaModeForbiddenError(sin excepción, sin llamada de red).
En modo RW, las herramientas de mutación llevan destructiveHint: true para que los hosts MCP
que respetan anotaciones puedan requerir una confirmación por llamada.
Aislamiento por inquilino (modo autenticación básica)
- Las cabeceras de autenticación se analizan en middleware ASGI dedicado y se guardan en un
ContextVar; nunca entran en cuerpos de solicitud o registros. - Cada solicitud se resuelve a una instancia
Credentialscon clave por(user, password_hash, region). - Un límite LRU en el pool de clientes evita el crecimiento de memoria ilimitado por rociado de credenciales aleatorias.
Superficie de red
- En desarrollo (
docker compose up) el servidor MCP publica:8765. - En el perfil
prodel contenedor MCP no tiene puerto publicado - Caddy se une a la misma red puente docker y hace proxy de HTTPS. Los únicos puertos del host son 80 (ACME HTTP-01) y 443 (HTTPS). - Sin tráfico saliente excepto a
*.eset.systems(autenticación + APIs).
Auditoría de dependencias y código
- Snyk Code: 0 problemas en
eset_mcp/. - Ruff: limpio (
select = E F W I B UP RUF). - Dependencias de ejecución:
mcp,httpx,pydantic,python-dotenv. Másstarlette+uvicornal ejecutar HTTP.
Fuera de alcance (por diseño)
- Sin receptores de webhook.
- Sin almacenamiento persistente; los registros van a stdout.
- Sin caché en disco de tokens OAuth.
- Sin escritura de credenciales de modo
basicen disco.
Divulgación responsable
Por favor, abre un aviso de seguridad privado en lugar de un issue público: https://github.com/maciekaz/ESET-MCP/security/advisories/new.
Inicio rápido
La vía más rápida es la imagen Docker publicada. Sin instalar Python, sin venv, sin clonar el código fuente: solo .env + docker run. La imagen es multi-arquitectura (amd64 + arm64), firmada con cosign, incluye SBOM + procedencia de compilación, y se publica en GHCR en cada release.
cp .env.example .env # fill in ESET_USER / ESET_PASSWORD / ESET_REGION
docker run --rm -i --env-file .env ghcr.io/maciekaz/eset-mcp:1
Política de fijación de versiones:
:1- última 1.x.x (actualizaciones automáticas dentro de la major):1.0- última 1.0.x (actualizaciones automáticas dentro de la minor):1.0.1- versión exacta (producción):latest- release estable más reciente:main/:sha-<short>- builds edge desde main (no para producción)
Conectar a Claude Desktop / Claude Code (stdio)
// claude_desktop_config.json
{
"mcpServers": {
"eset": {
"command": "docker",
"args": ["run", "--rm", "-i", "--env-file", "/absolute/path/to/.env",
"ghcr.io/maciekaz/eset-mcp:1"]
}
}
}
Transporte HTTP (single-tenant o detrás de tu propio proxy)
docker run -d --name eset-mcp \
--env-file .env -p 8765:8765 \
-e ESET_MCP_TRANSPORT=http \
ghcr.io/maciekaz/eset-mcp:1
# MCP endpoint: http://localhost:8765/mcp
Verificar la imagen (cosign keyless)
cosign verify \
--certificate-identity-regexp '^https://github.com/maciekaz/ESET-MCP/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/maciekaz/eset-mcp:1
Desde el código fuente (contribuidores / desarrollo local)
git clone https://github.com/maciekaz/ESET-MCP.git
cd ESET-MCP
cp .env.example .env
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
eset-mcp
Docker Compose (usa la imagen publicada)
docker compose up -d eset-mcp-http
# MCP endpoint: http://localhost:8765/mcp
Por defecto, el archivo compose descarga ghcr.io/maciekaz/eset-mcp:1 - sin compilación local, primer arranque rápido. Fija una versión específica editando la línea image: en docker-compose.yml.
stdio puntual vía compose:
docker compose --profile stdio run --rm eset-mcp-stdio
¿Trabajando en el código fuente? Usa el perfil dev para compilar desde tu checkout local en lugar de descargar:
docker compose --profile dev up --build eset-mcp-http-dev
Configuración
Todos los ajustes viven en .env. Los campos obligatorios están marcados en .env.example.
| Variable | Por defecto | Propósito |
|---|---|---|
ESET_AUTH_MODE | env | env (single tenant) o basic (multi tenant) |
ESET_USER | - | Usuario de API (obligatorio en modo env) |
ESET_PASSWORD | - | Contraseña de API (obligatoria en modo env) |
ESET_MODE | RO | RO (catálogo de solo lectura) o RW |
ESET_REGION | eu | eu / de / us / ca / jpn |
ESET_MCP_TRANSPORT | stdio | stdio o http |
ESET_MCP_HTTP_HOST | 127.0.0.1 | Dirección de enlace HTTP |
ESET_MCP_HTTP_PORT | 8765 | Puerto HTTP |
ESET_MCP_RESPONSE_BYTES_MAX | 100000 | Límite de bytes de respuesta por llamada; 0 lo desactiva |
ESET_LOG_LEVEL | INFO | DEBUG / INFO / WARNING / ERROR |
ESET_MCP_LOG_FORMAT | text | text (dev, legible para humanos) o json (shippers de logs de producción) |
ESET_MCP_METRICS_ENABLED | false | Montar /metrics de Prometheus; requiere eset-mcp[metrics] |
ESET_MCP_METRICS_PATH | /metrics | Dónde montar el endpoint de métricas |
ESET_DEPLOYMENT | cloud | cloud (ESET Connect) o onprem (PROTECT alojado por el cliente) |
ESET_ONPREM_SERVER_URL | - | https://host[:port] de la consola on-prem (obligatorio en env+onprem) |
ESET_ONPREM_VERIFY_SSL | true | Establece false para consolas on-prem con certificados autofirmados |
ESET_ONPREM_CF_ACCESS_CLIENT_ID | - | Cloudflare Access Service Token client-id (on-prem detrás de CF) |
ESET_ONPREM_CF_ACCESS_CLIENT_SECRET | - | Cloudflare Access Service Token client-secret (emparejado con el anterior) |
ESET_PUBLIC_DOMAIN | - | Dominio para el que Caddy emite un certificado TLS (solo perfil prod) |
ESET_ACME_EMAIL | - | Email que Let's Encrypt usa para renovaciones (solo perfil prod) |
Usa un usuario de API dedicado - no tu inicio de sesión de consola. Créalo en ESET PROTECT Hub / ESET Business Account → API users.
Despliegue multi-tenant (modo basic-auth)
# .env
ESET_AUTH_MODE=basic
ESET_MCP_TRANSPORT=http
ESET_REGION=eu # default region; clients can override per request
Cada petición HTTP debe incluir:
| Cabecera | Obligatoria | Notas |
|---|---|---|
Authorization | sí | Basic <base64(user:password)> |
X-ESET-Region | no | Sobrescribe la región por defecto (eu/de/us/ca/jpn) |
X-ESET-Server-URL | no | Enruta esta petición a una consola PROTECT on-prem (p. ej. https://protect.example.com:9443) - ver Soporte on-prem |
X-ESET-CF-Access-Client-Id | no | Cloudflare Access Service Token client-id (on-prem detrás de CF Access) |
X-ESET-CF-Access-Client-Secret | no | Emparejado con el anterior - ambos deben enviarse juntos |
Ejemplo de cliente Python:
import base64
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
token = base64.b64encode(b"api-user@tenant.tld:secret").decode()
headers = {"Authorization": f"Basic {token}", "X-ESET-Region": "us"}
async with streamablehttp_client(
"https://eset-mcp.example.com/mcp/", headers=headers
) as (r, w, _):
async with ClientSession(r, w) as session:
await session.initialize()
tools = await session.list_tools()
⚠️ Basic auth sin TLS filtra credenciales. Ejecuta siempre el modo
basicdetrás de HTTPS.
Soporte de ESET PROTECT on-prem
ESET distribuye PROTECT tanto como servicio en la nube (la API de ESET Connect en
*.eset.systems) como consola on-prem que los clientes auto-alojan. La API REST on-prem
vive en un único host (puerto por defecto 9443) y usa un endpoint de autenticación
diferente - POST /GetTokens con cuerpo JSON y respuesta en camelCase - pero por lo demás
comparte la estructura de URLs de la API en la nube. ESET-MCP soporta ambos, en el mismo proceso.
Cómo decide el servidor entre nube y on-prem
ESET_AUTH_MODE | Qué controla el despliegue por petición |
|---|---|
env | Estático: ESET_DEPLOYMENT (nube) o ESET_DEPLOYMENT=onprem + ESET_ONPREM_SERVER_URL |
basic | Por petición: la presencia de X-ESET-Server-URL cambia esa única petición a on-prem; su ausencia vuelve al valor por defecto del entorno (nube u on-prem) |
Así, un único servidor MCP puede dar servicio a la nube para la mayoría de clientes y
enrutar peticiones específicas a una o más consolas on-prem - determinado enteramente por la
URL que cada cliente envía en X-ESET-Server-URL.
On-prem single-tenant (modo env)
# .env
ESET_AUTH_MODE=env
ESET_DEPLOYMENT=onprem
ESET_ONPREM_SERVER_URL=https://protect.company.local:9443
ESET_ONPREM_VERIFY_SSL=true # set to false only for self-signed certs you trust
ESET_USER=api-user@company.local
ESET_PASSWORD=...
On-prem multi-tenant (basic auth, URL por petición)
# .env
ESET_AUTH_MODE=basic
ESET_MCP_TRANSPORT=http
ESET_DEPLOYMENT=cloud # default; clients opt into on-prem per-request
# ESET_ONPREM_SERVER_URL is optional - if set it becomes the on-prem default
Cliente dirigido a on-prem:
headers = {
"Authorization": f"Basic {token}",
"X-ESET-Server-URL": "https://protect.client-a.local:9443",
}
El mismo servidor MCP, otra petición - mismas cabeceras menos X-ESET-Server-URL
- permanece en la nube.
Qué funciona en on-prem vs nube
El catálogo de herramientas es idéntico para ambos despliegues (las 102 herramientas derivadas de OpenAPI más las 4 compuestas). En el momento de la llamada, el servidor usa rutas de nube para credenciales de nube y rutas on-prem para credenciales on-prem.
- Compartidas y verificadas:
device_*,asset_groups_*,policy_*y la mayoría detask_*(Automation) funcionan igual en nube y on-prem. - Módulos solo nube:
incident_*,mobile_*,wap_*,nap_*,quarantine_*y la mayoría devuln_*corresponden a productos ESET separados (ESET Inspect, Cloud Office Security, MDM) que no forman parte de la instalación de PROTECT on-prem. Llamarlos contra una consola on-prem devuelve un 404 simple de ESET - presentado al agente como una respuesta de textoESET API error: 404sin tratamiento especial. - Sobrescrituras de ruta: algunos endpoints tienen una URL diferente on-prem - p. ej.
POST /v1/devices/{uuid}:renamees:renameDeviceon-prem. Están declarados eneset_mcp/openapi/onprem-path-overrides.jsony se aplican automáticamente cuando la petición apunta a on-prem.
Cloudflare Access delante de la consola on-prem
Cuando la consola PROTECT on-prem se expone mediante un túnel de Cloudflare y está protegida por Cloudflare Access, MCP puede autenticarse como service token. La cadena pasa a ser MCP → Cloudflare Access → ESET on-prem.
Dos valores por par de tokens, proporcionados ya sea vía .env:
ESET_ONPREM_CF_ACCESS_CLIENT_ID=abc1234567890.access
ESET_ONPREM_CF_ACCESS_CLIENT_SECRET=<long-secret>
…o por petición en modo basic-auth (sobrescribe los valores por defecto del entorno - útil cuando cada tenant tiene su propio túnel y su propio service token):
headers = {
"Authorization": f"Basic {token}",
"X-ESET-Server-URL": "https://protect.client-a.local:9443",
"X-ESET-CF-Access-Client-Id": "abc1234567890.access",
"X-ESET-CF-Access-Client-Secret": "<long-secret>",
}
MCP traduce las cabeceras de entrada X-ESET-CF-* a las cabeceras reales
CF-Access-Client-Id / CF-Access-Client-Secret que Cloudflare
Access espera, y las adjunta a cada llamada saliente - tanto el
handshake de autenticación POST /GetTokens como cada petición posterior a la API de ESET.
El secreto de CF se trata como la contraseña: nunca se registra, solo su hash SHA-256 entra en la clave del pool de clientes. Rotar el secreto genera un cliente nuevo
- token ESET nuevo. Las peticiones a la nube nunca llevan cabeceras de CF Access independientemente de los valores por defecto del entorno - ESET Connect es un SaaS público.
Notas de seguridad para on-prem
X-ESET-Server-URLacepta solo URLshttps://sin ruta, query ni fragmento. Las barras finales se eliminan. Cualquier otra cosa → HTTP 400.ESET_ONPREM_VERIFY_SSL=falsedesactiva la verificación de certificados TLS y expone la conexión a MITM. El servidor registra un único WARNING por construcción de cliente cuando está desactivado. Úsalo solo en intranets de confianza con certificados autofirmados que no puedas reemplazar.- Los tokens on-prem se mantienen en memoria por
(user, password_hash, server_url, cf_secret_hash)- mismas reglas de aislamiento que los tokens de nube. El pool los clave por separado para que los clientes de nube y on-prem nunca colisionen, y dos clientes que apunten a la misma URL on-prem con distintos service tokens de CF obtienen entradas de pool separadas. - Enviar solo una de las dos cabeceras
X-ESET-CF-*devuelve HTTP 400 en lugar de volver silenciosamente al valor por defecto del entorno (casi seguro un error de tecleo del operador).
Despliegue en producción (HTTPS vía Caddy)
El perfil docker-compose prod lanza Caddy delante del servidor MCP.
Caddy obtiene un certificado de Let's Encrypt en el primer arranque (challenge HTTP-01 -
los puertos 80 / 443 deben ser accesibles desde internet público)
y hace de proxy HTTPS hacia el contenedor MCP interno.
# .env
ESET_AUTH_MODE=basic
ESET_PUBLIC_DOMAIN=eset-mcp.example.com
ESET_ACME_EMAIL=ops@example.com
docker compose --profile prod up -d
# MCP endpoint: https://eset-mcp.example.com/mcp
Obtienes:
- HTTPS en 443 con certificado Let's Encrypt de renovación automática.
- Challenge HTTP-01 en el puerto 80.
- Contenedor MCP vinculado solo a la red bridge de docker - sin puerto publicado.
- Compresión gzip / zstd, logs de acceso JSON en stdout.
Herramientas, recursos y prompts
Herramientas compuestas de alto nivel
| Herramienta | Devuelve |
|---|---|
eset_search(query, kinds?, limit_per_kind?) | Coincidencias de subcadena sin distinguir mayúsculas/minúsculas en dispositivos / usuarios / políticas / grupos |
device_full_profile(deviceUuid) | Registro del dispositivo + detecciones recientes + vulnerabilidades + escaneos recientes |
incident_full_context(incidentUuid) | Incidente + comentarios + detecciones relacionadas + dispositivos afectados |
latest_detections(hours=24, limit=10, severity_min?) | Detecciones más recientes en una ventana de tiempo, ordenadas por occurTime desc; v2 → v1 fallback |
Cada compuesta degrada con elegancia cuando una sub-llamada devuelve 403/404
(p. ej. en tenants a los que les falta un módulo). La forma lleva flags skipped /
truncated donde corresponda.
Recursos
eset://config/mode-ROoRW.eset://config/region- región actual (por solicitud en modo autenticación básica).eset://config/deployment-cloudoonprem (<server-url>)para esta solicitud.eset://config/tools-catalog- catálogo JSON de las 106 herramientas (nombre, modo, método, ruta, servicio, descripción).eset://docs/rate-limits- recordatorio rápido sobre el límite de 10 solicitudes por segundo.
Indicaciones
audit_inactive_devices(days=30)- candidatos de baja.vulnerability_report- informe CVE por dispositivo.incident_triage- incidentes abiertos + detecciones relacionadas.
Arquitectura
eset_mcp/
├── __main__.py # entrypoint - stdio or HTTP, wires resolver + pool
├── server.py # MCP server (tools / resources / prompts) + telemetry
├── credentials.py # Credentials + EnvResolver / BasicAuthResolver + ContextVar
├── middleware.py # ASGI Basic-auth middleware (basic mode only)
├── client_pool.py # LRU pool of EsetHttpClient keyed by (user, region, ...)
├── http_client.py # async httpx + 202 polling + 429 retry + 401 refresh
├── auth.py # CloudTokenManager (OAuth2) + OnPremTokenManager (/GetTokens)
├── regions.py # cloud region → per-service domains + on-prem URL resolver
├── modes.py # RO/RW gate
├── errors.py # HTTP error → agent-friendly text
├── config.py # .env loading
├── response_shaping.py # fields projection + byte cap
├── composite_tools.py # hand-written high-level tools
├── tools_loader.py # generator: tools from OpenAPI specs + on-prem path overrides
├── observability/ # JSON/text structured logging + Prometheus metrics
└── openapi/ # 16 ESET Connect OpenAPI 3.0.1 specs + onprem path overrides
Pruebas
pytest # full suite (RO smoke + unit + integration)
pytest -m "not rw" # RO only (default in CI)
pytest -m rw # RW (requires an account with RW permissions)
Las pruebas de integración se ejecutan contra un tenant real de ESET: las credenciales se proporcionan mediante el mismo .env. El flujo de CI: .github/workflows/integration.yml se ejecuta en PR, al hacer push a main, y una vez al día a las 03:17 UTC. El cron detecta desviaciones entre el servidor y las especificaciones OpenAPI publicadas por ESET.
Actualización de las especificaciones OpenAPI
cd eset_mcp/openapi
for name in business-account application-management asset-management automation \
device-management iam incident-management installer-management \
mobile-device-management network-access-protection patch-management \
policy-management quarantine-management user-management \
vulnerability-management web-access-protection; do
curl -sO "https://eu.esetconnect.eset.systems/swagger/api/${name}.json"
done
tests/test_catalog_vs_openapi.py marca cualquier operación nueva o modificada después de una actualización.
Licencia
MIT