eSentire Atlas MCP Server
# eSentire Atlas MCP Server
An MCP server exposing the eSentire **Atlas API** (Findings, Ticketing, MVS) and
**Threat Intelligence** feeds (IP Watch, Advanced/STIX) as tools.
Built for HTTP transport so it can run as a container behind a reverse proxy and
be consumed by remote MCP clients.
## Credentials
Atlas issues **four distinct credential types** and they are **not
interchangeable** — presenting the wrong type returns `402`/`403`:
| Atlas credential type | Env var | Covers |
|---|---|---|
| `Atlas` | `ATLAS_API_TOKEN` | `/finding`, `/tickets`, `/mvs` — read **and** write |
| `Atlas Readonly` | `ATLAS_API_TOKEN` | same paths, reads only |
| `Threat Intelligence` | `ESENTIRE_TI_TOKEN` | `/ti/ipwatch`, `/ti/indicators` |
| `GenAI` | — | not implemented (undocumented endpoints) |
Create them in **Atlas → Settings → New API Credentials**. Choose
**Authentication: Token** — the static-token flow is the one the Atlas API
Reference Guide documents. Optionally add an IP restriction for your Docker host's
egress address.
## Configuration
| Variable | Default | Purpose |
|---|---|---|
| `ATLAS_API_TOKEN` | — | Atlas credential (Findings / Ticketing / MVS) |
| `ESENTIRE_TI_TOKEN` | — | Threat Intelligence credential |
| `ESENTIRE_API_BASE` | `https://api.esentire.com` | API root |
| `ESENTIRE_CUSTOMER_CODE` | — | Default tenant code, so callers can omit it |
| `ESENTIRE_ENABLED_SURFACES` | `findings,tickets,ti,mvs` | Comma list; drop `mvs` if not licensed |
| `ESENTIRE_READ_ONLY` | `false` | `1` refuses every mutating call at the door |
| `MCP_TRANSPORT` | `http` | `http` or `stdio` |
| `MCP_HOST` / `MCP_PORT` | `0.0.0.0` / `3000` | Bind address |
| `MCP_PUBLIC_URL` | — | External URL; required for OAuth |
| `MCP_AUTH` | `none` | `none` \| `bearer` \| `oauth`/`azure` \| `github` |
| `MCP_BEARER_TOKEN` | — | Shared secret when `MCP_AUTH=bearer` |
| `AZURE_TENANT_ID` / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` | — | Entra app for `MCP_AUTH=oauth` |
| `TI_CACHE_TTL` | `3600` | Seconds. Matches the feed's hourly refresh |
| `MAX_RESPONSE_CHARS` | `40000` | Per-tool response ceiling |
| `LOG_LEVEL` | `info` | |
`MCP_AUTH` is the **front door** — how MCP clients authenticate to this server.
It is unrelated to the eSentire tokens, which authenticate this server upstream.
## Tools
**Findings** — `findings_search`, `findings_search_advanced`, `finding_get`,
`finding_update`
`findings_search` returns every state unless you pass `states`. Atlas on its own
returns only Open findings when the query has no STATE predicate, which makes a
week of resolved detections look like an empty week. `finding_get` and
`finding_update` take either the finding UUID or the CS case number.
**Ticketing** — `tickets_case_type_configs`, `tickets_list_cases`,
`tickets_get_case`, `tickets_create_case`, `tickets_update_case`,
`tickets_list_comments`, `tickets_list_emails`, `tickets_list_attachments`,
`tickets_get_attachment_link`, `tickets_upload_attachment`,
`tickets_delete_attachment`, `tickets_list_contacts`, `tickets_list_locations`
**Threat Intelligence** — `ti_ipwatch`, `ti_check_ip`, `ti_indicators`,
`ti_indicators_paged`, `ti_indicators_misp`
**MVS** — `mvs_list_assets`, `mvs_get_asset`,
`mvs_asset_details`, `mvs_get_asset_vulnerability`, `mvs_list_vulnerabilities`,
`mvs_get_vulnerability`, `mvs_list_missing_patches`, `mvs_assets_affected_by`
### Design notes
- **Filters are native objects.** Atlas wants URL-encoded JSON for `query`,
`sorts`, and `filters`. Pass real lists/dicts; encoding is handled internally.
- **`assignee_email` implies `use_v2`.** Atlas silently ignores the assignee
filter unless the V2 query engine is active, so `findings_search` enables it
for you rather than returning quietly-wrong results.
- **STIX is flattened by default.** `ti_indicators` reduces bundles to a compact
indicator/observable list. Pass `summarize=False` for raw STIX 2.1.
- **MVS auto-switches GET → POST** when the encoded filter payload gets long
enough to risk a query-string limit.
- **MVS is separately licensed.** Its tools are registered by default, but they
return a "wrong token type / not entitled" message if the service is not on
your account. Drop `mvs` from `ESENTIRE_ENABLED_SURFACES` to hide them.
- **`page` and `per_page` are mandatory on `/finding/findings`.** Undocumented in
the Atlas guide, but Atlas returns `400 Missing required query parameter`
without them, so `findings_search` always sends both (defaults 1 / 50).
- **Findings and MVS use different envelopes.** Findings returns
`{count, items, total_count}`; MVS returns `{data, paging}`.
- **`401` bodies are surfaced verbatim.** `"<CUSTOMERCODE> is not authorized"`
means the token is valid but the service is not subscribed; a bare
`"Unauthorized"` means the token itself was rejected. Very different fixes.
- **Trailing slashes are stripped.** Atlas returns `404` for a URL ending in `/`.
- **Tokens are sent raw** in `Authorization` — no `Bearer` prefix.
- **`/health` never calls upstream.** An expired token or an eSentire outage must
not make Docker restart-loop an otherwise-healthy container.
## Local development
```bash
uv venv --python 3.12
uv pip install -e .
MCP_TRANSPORT=stdio ATLAS_API_TOKEN=... uv run python -m esentire_mcp
```
## Deployment
Deployed as a Portainer stack that builds this repo directly on the Docker host —
see `docker-compose.yml`. No local Docker or image registry required.
The service joins two networks: its own `esentire_net`, and an **external**
`mcp-edge` that the reverse proxy also sits on, so the proxy can resolve
`esentire-atlas-mcp:3000` by name. Create it once with
`docker network create mcp-edge` if it does not exist.
Behind a reverse proxy, streamable HTTP needs buffering disabled and long
timeouts, or long-lived responses get cut:
```nginx
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
chunked_transfer_encoding on;
```
Set `MCP_PUBLIC_URL` to the external HTTPS URL. It is required for OAuth and
otherwise only used for advertising the server's own address.
## Safety
`tickets_create_case` opens a **real support case with eSentire's SOC**. Confirm
with a human before calling it. Use `case_type: "Atlas API Test"` for smoke
tests. Closed tickets can never be reopened; resolved tickets can — prefer
resolving. Set `ESENTIRE_READ_ONLY=1` to disable all mutations.
Resolve a detection with `finding_update` (`state: "Resolved"` plus a
`resolution_strategy`); that resolves the linked case as well. Resolving a case
directly with `tickets_update_case` needs `state: "resolved"`, a `comments`
text and a `username`; Atlas rejects the request otherwise. Both tools re-read
the record afterwards and report whether the change actually took.
TDQS
Scored across 29 tools
The major domains are cleanly separated by tickets_, ti_, findings/finding_, and mvs_ prefixes, and most tools target a distinct resource+action. The only mild ambiguity comes from near-variant tools such as findings_search vs findings_search_advanced and ti_indicators vs ti_indicators_paged vs ti_indicators_misp, but their descriptions explain the differences.
Most names follow a verb_noun pattern under a clear domain prefix, e.g. tickets_list_cases, mvs_get_asset, and ti_check_ip. A few deviations such as tickets_case_type_configs, mvs_asset_details, mvs_assets_affected_by, and finding_update alongside findings_search prevent a perfect score, but the style is otherwise consistent and readable.
29 tools feels heavy for an MCP surface and will consume meaningful agent context, placing the server above the ideal 3-15 range. The count is partly justified by the broad eSentire platform scope spanning cases, threat intel, findings, and MVS, but it is still borderline and could reasonably be split into separate servers.
The surface covers the core case lifecycle, findings search/update, threat-intel feeds, and MVS asset/vulnerability/patch workflows well. Minor gaps include no direct single-finding fetch and no support for attachments over 5MB, but agents can usually work around these using the existing search and metadata tools.