ESET Protect MCP

Servidor MCP para la API de ESET Connect: 102 herramientas, modo RO/RW, stdio+HTTP, OAuth2

Documentación

ESET-MCP

Tests License: MIT MCP spec

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

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 de list_tools por completo.
  • ESET_MODE=RW → se anuncian las 106 herramientas; las herramientas de mutación llevan destructiveHint: true en 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 pasan Authorization: Basic <base64(user:password)> por solicitud (además de opcionalmente X-ESET-Region para una región de nube diferente, o X-ESET-Server-URL para 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 opcional fields: [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 _capped con 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 tipado event con campos de baja cardinalidad (tool, deployment, status, duration_ms, response_bytes, ...).
  • Métricas Prometheus en un endpoint /metrics opcional (ESET_MCP_METRICS_ENABLED=true, requiere pip 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. /metrics devuelve 500 (no 503) si la exposición alguna vez lanza - el worker permanece activo.
  • Silenciamiento en producción: establezca ESET_LOG_LEVEL=WARNING para silenciar los eventos INFO por llamada pero mantener visibles los reintentos/errores; establezca ESET_LOG_LEVEL=ERROR para silenciar todo excepto fallos graves. Desactive las métricas por completo con ESET_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.

Single-tenant: one ESET account, one deployment

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:

Multi-tenant: many clients, one MCP, many backends

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 env la contraseña se lee una vez al inicio y se mantiene en memoria.
  • En modo basic la 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

ModoTransporte permitidoFuente de credenciales
envstdio o http.env (ESET_USER / ESET_PASSWORD)
basicsolo httpCabecera 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:

  1. Ocultación del catálogo - list_tools filtra cada herramienta no GET en modo RO. El agente nunca ve herramientas de escritura.
  2. Puerta de defensa en profundidad - call_tool valida el modo declarado de la herramienta contra ESET_MODE antes 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 estructurada ModeForbiddenError (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 Credentials con 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 prod el 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ás starlette + uvicorn al 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 basic en 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.

VariablePor defectoPropósito
ESET_AUTH_MODEenvenv (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_MODERORO (catálogo de solo lectura) o RW
ESET_REGIONeueu / de / us / ca / jpn
ESET_MCP_TRANSPORTstdiostdio o http
ESET_MCP_HTTP_HOST127.0.0.1Dirección de enlace HTTP
ESET_MCP_HTTP_PORT8765Puerto HTTP
ESET_MCP_RESPONSE_BYTES_MAX100000Límite de bytes de respuesta por llamada; 0 lo desactiva
ESET_LOG_LEVELINFODEBUG / INFO / WARNING / ERROR
ESET_MCP_LOG_FORMATtexttext (dev, legible para humanos) o json (shippers de logs de producción)
ESET_MCP_METRICS_ENABLEDfalseMontar /metrics de Prometheus; requiere eset-mcp[metrics]
ESET_MCP_METRICS_PATH/metricsDónde montar el endpoint de métricas
ESET_DEPLOYMENTcloudcloud (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_SSLtrueEstablece 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:

CabeceraObligatoriaNotas
AuthorizationBasic <base64(user:password)>
X-ESET-RegionnoSobrescribe la región por defecto (eu/de/us/ca/jpn)
X-ESET-Server-URLnoEnruta 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-IdnoCloudflare Access Service Token client-id (on-prem detrás de CF Access)
X-ESET-CF-Access-Client-SecretnoEmparejado 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 basic detrá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_MODEQué controla el despliegue por petición
envEstático: ESET_DEPLOYMENT (nube) o ESET_DEPLOYMENT=onprem + ESET_ONPREM_SERVER_URL
basicPor 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 de task_* (Automation) funcionan igual en nube y on-prem.
  • Módulos solo nube: incident_*, mobile_*, wap_*, nap_*, quarantine_* y la mayoría de vuln_* 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 texto ESET API error: 404 sin tratamiento especial.
  • Sobrescrituras de ruta: algunos endpoints tienen una URL diferente on-prem - p. ej. POST /v1/devices/{uuid}:rename es :renameDevice on-prem. Están declarados en eset_mcp/openapi/onprem-path-overrides.json y 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-URL acepta solo URLs https:// sin ruta, query ni fragmento. Las barras finales se eliminan. Cualquier otra cosa → HTTP 400.
  • ESET_ONPREM_VERIFY_SSL=false desactiva 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

HerramientaDevuelve
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 - RO o RW.
  • eset://config/region - región actual (por solicitud en modo autenticación básica).
  • eset://config/deployment - cloud o onprem (<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