Skip to content

Commit 0c5b423

Browse files
committed
README: rework intro to cover the full ESET surface; drop em-dashes
The opening paragraph now names ESET PROTECT On-Prem alongside Connect / Inspect / Cloud Office, and re-frames the project as a single hub for any number of ESET deployments (cloud + on-prem in one process, per-request routing via headers). Replaces every em-dash and en-dash with a plain hyphen for consistent ASCII punctuation. No content changes outside the intro paragraph.
1 parent 11f3fce commit 0c5b423

1 file changed

Lines changed: 66 additions & 49 deletions

File tree

README.md

Lines changed: 66 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@
55
[![MCP spec](https://img.shields.io/badge/MCP%20spec-2025--11--25-informational)](https://modelcontextprotocol.io/specification/2025-11-25)
66

77
A [Model Context Protocol](https://modelcontextprotocol.io) server for the
8-
[ESET Connect API](https://help.eset.com/eset_connect/en-US/). Drive ESET
9-
PROTECT / ESET Inspect / ESET Cloud Office from any MCP host — Claude Desktop,
10-
Claude Code, or a custom agent — through tools, resources, and prompts.
8+
entire ESET management surface: [ESET Connect](https://help.eset.com/eset_connect/en-US/)
9+
(cloud, all regions), ESET PROTECT On-Prem, ESET Inspect, and ESET Cloud
10+
Office. Drive any of them from any MCP host (Claude Desktop, Claude Code,
11+
or a custom agent) through tools, resources, and prompts.
12+
13+
Built as a **single hub for any number of ESET deployments**. One process
14+
fronts cloud and on-prem consoles at the same time; clients pick the target
15+
per request via headers. As long as MCP receives valid credentials (Basic
16+
auth, plus an optional URL override and optional Cloudflare Access service
17+
token) it routes the call to the right backend, mints its own tokens, and
18+
keeps tenants isolated in the pool.
1119

1220
---
1321

@@ -38,7 +46,7 @@ Claude Code, or a custom agent — through tools, resources, and prompts.
3846
mobile-device-management, network-access-protection, patch-management,
3947
policy-management, quarantine-management, user-management,
4048
vulnerability-management, and web-access-protection.
41-
- **4 high-level composites** that fold 36 raw calls into one:
49+
- **4 high-level composites** that fold 3-6 raw calls into one:
4250
`eset_search`, `device_full_profile`, `incident_full_context`,
4351
`latest_detections`.
4452

@@ -54,8 +62,8 @@ Claude Code, or a custom agent — through tools, resources, and prompts.
5462

5563
### Authentication
5664

57-
- `ESET_AUTH_MODE=env` single tenant, credentials from `.env`.
58-
- `ESET_AUTH_MODE=basic` multi tenant, clients pass
65+
- `ESET_AUTH_MODE=env` - single tenant, credentials from `.env`.
66+
- `ESET_AUTH_MODE=basic` - multi tenant, clients pass
5967
`Authorization: Basic <base64(user:password)>` per request (plus optional
6068
`X-ESET-Region` for a different cloud region, or `X-ESET-Server-URL` to
6169
route the request to an on-prem PROTECT console). One server fronts many
@@ -67,8 +75,8 @@ Claude Code, or a custom agent — through tools, resources, and prompts.
6775

6876
### Transports
6977

70-
- **stdio** JSON-RPC over stdin/stdout for local hosts.
71-
- **Streamable HTTP** the current MCP transport (Nov 2025 spec).
78+
- **stdio** - JSON-RPC over stdin/stdout for local hosts.
79+
- **Streamable HTTP** - the current MCP transport (Nov 2025 spec).
7280

7381
### Multi-region
7482

@@ -84,6 +92,15 @@ structure are handled transparently; clients pick the target per request
8492
via the `X-ESET-Server-URL` header. See
8593
[On-prem ESET PROTECT support](#on-prem-eset-protect-support).
8694

95+
### Cloudflare Access (optional)
96+
97+
When the on-prem console sits behind a Cloudflare Access tunnel, MCP
98+
authenticates as a [service token](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/)
99+
(env-default or per-request `X-ESET-CF-Access-Client-Id` /
100+
`X-ESET-CF-Access-Client-Secret`) and rides through to the origin. The
101+
CF token is an extra ingress layer in front of - not a replacement for -
102+
the ESET account credentials. Cloud requests never carry these headers.
103+
87104
### Resilience
88105

89106
- OAuth2 with proactive refresh ~5 min before token expiry and a forced
@@ -95,15 +112,15 @@ via the `X-ESET-Server-URL` header. See
95112

96113
### Response shaping (context-window protection)
97114

98-
A single uncapped `list_*` call can return hundreds of KB enough to
115+
A single uncapped `list_*` call can return hundreds of KB - enough to
99116
overflow a model's context. Two transformations are applied to every
100117
tool response:
101118

102-
- **`fields` projection** every GET tool exposes an optional
119+
- **`fields` projection** - every GET tool exposes an optional
103120
`fields: [string]` parameter that filters each list-item down to the
104121
requested keys (e.g. `["uuid", "displayName"]`). Applied server-side
105122
after fetch.
106-
- **Byte cap** (`ESET_MCP_RESPONSE_BYTES_MAX`, default 100 KB) if a
123+
- **Byte cap** (`ESET_MCP_RESPONSE_BYTES_MAX`, default 100 KB) - if a
107124
payload still exceeds the budget, the longest list is trimmed while
108125
every top-level field (`nextPageToken`, `totalSize`, …) is preserved,
109126
and a `_capped` metadata block is attached with an actionable hint
@@ -139,7 +156,7 @@ back off; 5xx → retry shortly.
139156
| `basic` | http only | `Authorization: Basic` header per request |
140157

141158
`basic` mode over plain HTTP would leak passwords. The server enforces
142-
HTTP transport for `basic` mode at startup but does **not** enforce TLS
159+
HTTP transport for `basic` mode at startup but does **not** enforce TLS -
143160
that is the deployment's job. The `prod` docker-compose profile fronts
144161
the server with Caddy + Let's Encrypt.
145162

@@ -151,9 +168,9 @@ Missing / malformed `Authorization` in `basic` mode → HTTP 401 with a
151168

152169
Two independent layers:
153170

154-
1. **Catalog hiding** `list_tools` filters out every non-GET tool in
171+
1. **Catalog hiding** - `list_tools` filters out every non-GET tool in
155172
RO mode. The agent never sees write tools.
156-
2. **Defence-in-depth gate** `call_tool` validates the tool's declared
173+
2. **Defence-in-depth gate** - `call_tool` validates the tool's declared
157174
mode against `ESET_MODE` before any HTTP request goes out. Hard-coded
158175
clients, prompt-injection attempts, and stale agent snapshots all hit
159176
the gate and receive a structured `ModeForbiddenError` text response
@@ -174,7 +191,7 @@ that respect annotations can require a per-call confirmation.
174191
### Network surface
175192

176193
- In dev (`docker compose up`) the MCP server publishes `:8765`.
177-
- In the `prod` profile the MCP container has **no published port**
194+
- In the `prod` profile the MCP container has **no published port** -
178195
Caddy joins the same docker bridge network and proxies HTTPS in. The
179196
only host ports are 80 (HTTP-01 ACME) and 443 (HTTPS).
180197
- No outbound traffic except to `*.eset.systems` (auth + APIs).
@@ -247,8 +264,8 @@ All settings live in `.env`. Required fields are marked in
247264
| Variable | Default | Purpose |
248265
|--------------------------------|---------------|---------------------------------------------------------------|
249266
| `ESET_AUTH_MODE` | `env` | `env` (single tenant) or `basic` (multi tenant) |
250-
| `ESET_USER` | | API user (required in `env` mode) |
251-
| `ESET_PASSWORD` | | API password (required in `env` mode) |
267+
| `ESET_USER` | - | API user (required in `env` mode) |
268+
| `ESET_PASSWORD` | - | API password (required in `env` mode) |
252269
| `ESET_MODE` | `RO` | `RO` (read-only catalog) or `RW` |
253270
| `ESET_REGION` | `eu` | `eu` / `de` / `us` / `ca` / `jpn` |
254271
| `ESET_MCP_TRANSPORT` | `stdio` | `stdio` or `http` |
@@ -257,14 +274,14 @@ All settings live in `.env`. Required fields are marked in
257274
| `ESET_MCP_RESPONSE_BYTES_MAX` | `100000` | Per-call response byte cap; `0` disables |
258275
| `ESET_LOG_LEVEL` | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` |
259276
| `ESET_DEPLOYMENT` | `cloud` | `cloud` (ESET Connect) or `onprem` (customer-hosted PROTECT) |
260-
| `ESET_ONPREM_SERVER_URL` | | `https://host[:port]` of the on-prem console (req. in env+onprem) |
277+
| `ESET_ONPREM_SERVER_URL` | - | `https://host[:port]` of the on-prem console (req. in env+onprem) |
261278
| `ESET_ONPREM_VERIFY_SSL` | `true` | Set `false` for on-prem consoles with self-signed certs |
262-
| `ESET_ONPREM_CF_ACCESS_CLIENT_ID` | | Cloudflare Access Service Token client-id (on-prem behind CF) |
263-
| `ESET_ONPREM_CF_ACCESS_CLIENT_SECRET` | | Cloudflare Access Service Token client-secret (paired with the above) |
264-
| `ESET_PUBLIC_DOMAIN` | | Domain Caddy issues a TLS cert for (`prod` profile only) |
265-
| `ESET_ACME_EMAIL` | | Email Let's Encrypt uses for renewals (`prod` profile only) |
279+
| `ESET_ONPREM_CF_ACCESS_CLIENT_ID` | - | Cloudflare Access Service Token client-id (on-prem behind CF) |
280+
| `ESET_ONPREM_CF_ACCESS_CLIENT_SECRET` | - | Cloudflare Access Service Token client-secret (paired with the above) |
281+
| `ESET_PUBLIC_DOMAIN` | - | Domain Caddy issues a TLS cert for (`prod` profile only) |
282+
| `ESET_ACME_EMAIL` | - | Email Let's Encrypt uses for renewals (`prod` profile only) |
266283

267-
> Use a **dedicated API user** not your console login. Create one in
284+
> Use a **dedicated API user** - not your console login. Create one in
268285
> ESET PROTECT Hub / ESET Business Account → API users.
269286
270287
---
@@ -284,9 +301,9 @@ Every HTTP request must carry:
284301
|---------------------|----------|------------------------------------------------------------------------|
285302
| `Authorization` | yes | `Basic <base64(user:password)>` |
286303
| `X-ESET-Region` | no | Override default region (`eu`/`de`/`us`/`ca`/`jpn`) |
287-
| `X-ESET-Server-URL` | no | Route this request to an on-prem PROTECT console (e.g. `https://protect.example.com:9443`) see [On-prem support](#on-prem-eset-protect-support) |
304+
| `X-ESET-Server-URL` | no | Route this request to an on-prem PROTECT console (e.g. `https://protect.example.com:9443`) - see [On-prem support](#on-prem-eset-protect-support) |
288305
| `X-ESET-CF-Access-Client-Id` | no | Cloudflare Access Service Token client-id (on-prem behind CF Access) |
289-
| `X-ESET-CF-Access-Client-Secret` | no | Paired with the above both must be sent together |
306+
| `X-ESET-CF-Access-Client-Secret` | no | Paired with the above - both must be sent together |
290307

291308
Example Python client:
292309

@@ -316,8 +333,8 @@ async with streamablehttp_client(
316333
ESET ships PROTECT as both a cloud service (the ESET Connect API at
317334
`*.eset.systems`) and an on-prem console customers self-host. The on-prem
318335
REST API lives on a single host (default port `9443`) and uses a different
319-
authentication endpoint `POST /GetTokens` with a JSON body and a
320-
camelCase response but otherwise shares the URL structure of the cloud
336+
authentication endpoint - `POST /GetTokens` with a JSON body and a
337+
camelCase response - but otherwise shares the URL structure of the cloud
321338
API. ESET-MCP supports both, **in the same process**.
322339

323340
### How the server decides cloud vs on-prem
@@ -328,7 +345,7 @@ API. ESET-MCP supports both, **in the same process**.
328345
| `basic` | Per request: presence of `X-ESET-Server-URL` switches that single request to on-prem; absence falls back to the env default (cloud or on-prem) |
329346

330347
So a single MCP server can front the cloud for most clients **and** route
331-
specific requests to one or more on-prem consoles keyed entirely by which
348+
specific requests to one or more on-prem consoles - keyed entirely by which
332349
URL each client sends in `X-ESET-Server-URL`.
333350

334351
### Single-tenant on-prem (env mode)
@@ -350,7 +367,7 @@ ESET_PASSWORD=...
350367
ESET_AUTH_MODE=basic
351368
ESET_MCP_TRANSPORT=http
352369
ESET_DEPLOYMENT=cloud # default; clients opt into on-prem per-request
353-
# ESET_ONPREM_SERVER_URL is optional if set it becomes the on-prem default
370+
# ESET_ONPREM_SERVER_URL is optional - if set it becomes the on-prem default
354371
```
355372

356373
Client targeting on-prem:
@@ -362,8 +379,8 @@ headers = {
362379
}
363380
```
364381

365-
Same MCP server, different request same headers minus `X-ESET-Server-URL`
366-
stays on cloud.
382+
Same MCP server, different request - same headers minus `X-ESET-Server-URL`
383+
- stays on cloud.
367384

368385
### What works on on-prem vs cloud
369386

@@ -377,9 +394,9 @@ cloud credentials and on-prem paths for on-prem credentials.
377394
`quarantine_*` and most of `vuln_*` correspond to separate ESET products
378395
(ESET Inspect, Cloud Office Security, MDM) that are not part of the
379396
on-prem PROTECT installation. Calling them against an on-prem console
380-
returns a plain 404 from ESET surfaced to the agent as an
397+
returns a plain 404 from ESET - surfaced to the agent as an
381398
`ESET API error: 404` text response with no special handling.
382-
- **Path overrides**: a few endpoints have a different URL on-prem e.g.
399+
- **Path overrides**: a few endpoints have a different URL on-prem - e.g.
383400
`POST /v1/devices/{uuid}:rename` is `:renameDevice` on-prem. These are
384401
declared in [`eset_mcp/openapi/onprem-path-overrides.json`](eset_mcp/openapi/onprem-path-overrides.json)
385402
and applied automatically when the request targets on-prem.
@@ -398,7 +415,7 @@ ESET_ONPREM_CF_ACCESS_CLIENT_ID=abc1234567890.access
398415
ESET_ONPREM_CF_ACCESS_CLIENT_SECRET=<long-secret>
399416
```
400417

401-
…or per-request in basic-auth mode (overrides the env defaults handy
418+
…or per-request in basic-auth mode (overrides the env defaults - handy
402419
when each tenant has its own tunnel and its own service token):
403420

404421
```python
@@ -412,13 +429,13 @@ headers = {
412429

413430
MCP translates the `X-ESET-CF-*` input headers into the actual
414431
`CF-Access-Client-Id` / `CF-Access-Client-Secret` headers that Cloudflare
415-
Access expects, and attaches them to **every** outbound call both the
432+
Access expects, and attaches them to **every** outbound call - both the
416433
`POST /GetTokens` auth handshake and every subsequent ESET API request.
417434

418435
The CF secret is treated like the password: never logged, only its SHA-256
419436
hash enters the client pool key. Rotating the secret mints a fresh client
420437
+ fresh ESET token. Cloud requests **never** carry CF Access headers
421-
regardless of env defaults ESET Connect is a public SaaS.
438+
regardless of env defaults - ESET Connect is a public SaaS.
422439

423440
### Security notes for on-prem
424441

@@ -429,7 +446,7 @@ regardless of env defaults — ESET Connect is a public SaaS.
429446
client construction when it's disabled. Use only on trusted intranets
430447
with self-signed certs you cannot replace.
431448
- On-prem tokens are held in memory per
432-
`(user, password_hash, server_url, cf_secret_hash)` same isolation
449+
`(user, password_hash, server_url, cf_secret_hash)` - same isolation
433450
rules as cloud tokens. The pool keys them separately so cloud and
434451
on-prem clients never collide, and two clients hitting the same on-prem
435452
URL with different CF service tokens get separate pool entries.
@@ -443,7 +460,7 @@ regardless of env defaults — ESET Connect is a public SaaS.
443460

444461
The `prod` docker-compose profile launches Caddy in front of the MCP
445462
server. Caddy fetches a Let's Encrypt cert on first start (HTTP-01
446-
challenge ports 80 / 443 must be reachable from the public internet)
463+
challenge - ports 80 / 443 must be reachable from the public internet)
447464
and proxies HTTPS to the internal MCP container.
448465

449466
```bash
@@ -460,7 +477,7 @@ You get:
460477

461478
- HTTPS on 443 with auto-renewing Let's Encrypt cert.
462479
- HTTP-01 challenge on 80.
463-
- MCP container bound only to the docker bridge network no published port.
480+
- MCP container bound only to the docker bridge network - no published port.
464481
- gzip / zstd compression, JSON access logs on stdout.
465482

466483
---
@@ -482,26 +499,26 @@ Each composite degrades gracefully when a sub-call returns 403/404
482499

483500
### Resources
484501

485-
- `eset://config/mode` `RO` or `RW`.
486-
- `eset://config/region` current region (per-request in basic-auth mode).
487-
- `eset://config/deployment` `cloud` or `onprem (<server-url>)` for this request.
488-
- `eset://config/tools-catalog` JSON catalog of all 106 tools (name,
502+
- `eset://config/mode` - `RO` or `RW`.
503+
- `eset://config/region` - current region (per-request in basic-auth mode).
504+
- `eset://config/deployment` - `cloud` or `onprem (<server-url>)` for this request.
505+
- `eset://config/tools-catalog` - JSON catalog of all 106 tools (name,
489506
mode, method, path, service, description).
490-
- `eset://docs/rate-limits` quick reminder about the 10 req/s ceiling.
507+
- `eset://docs/rate-limits` - quick reminder about the 10 req/s ceiling.
491508

492509
### Prompts
493510

494-
- `audit_inactive_devices(days=30)` offboarding candidates.
495-
- `vulnerability_report` per-device CVE report.
496-
- `incident_triage` open incidents + related detections.
511+
- `audit_inactive_devices(days=30)` - offboarding candidates.
512+
- `vulnerability_report` - per-device CVE report.
513+
- `incident_triage` - open incidents + related detections.
497514

498515
---
499516

500517
## Architecture
501518

502519
```
503520
eset_mcp/
504-
├── __main__.py # entrypoint stdio or HTTP, wires resolver + pool
521+
├── __main__.py # entrypoint - stdio or HTTP, wires resolver + pool
505522
├── server.py # MCP server (tools / resources / prompts)
506523
├── credentials.py # Credentials + EnvResolver / BasicAuthResolver + ContextVar
507524
├── middleware.py # ASGI Basic-auth middleware (basic mode only)
@@ -528,7 +545,7 @@ pytest -m "not rw" # RO only (default in CI)
528545
pytest -m rw # RW (requires an account with RW permissions)
529546
```
530547

531-
Integration tests hit a real ESET tenant credentials supplied via the
548+
Integration tests hit a real ESET tenant - credentials supplied via the
532549
same `.env`. CI workflow:
533550
[`.github/workflows/integration.yml`](.github/workflows/integration.yml)
534551
runs on PR, on push to `main`, and once a day at 03:17 UTC. The cron

0 commit comments

Comments
 (0)