Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ CLUSTERS_JSON=[{"name":"primary","elasticsearchUrl":"https://your-cluster.es.clo

# Alternative: load the same JSON from a file.
# CLUSTERS_FILE=/absolute/path/to/clusters.json

# Telemetry endpoint override. Defaults to production (telemetry.elastic.co).
# Set to `staging` to point at telemetry-staging.elastic.co — useful when
# verifying dashboards or working on the MCP App's analytics locally.
# See docs/telemetry.md for the full event catalog and opt-out story.
# MCP_APP_TELEMETRY_ENV=staging
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ When a user asks Claude to triage alerts or run a threat hunt, Claude calls a mo

See [docs/architecture.md](docs/architecture.md) for details on how views are built, how the UI communicates with the server, and key design decisions.

### Telemetry

The MCP App emits anonymised usage events via `@elastic/ebt`. Shipping is mirrored to the user's Kibana telemetry opt-in — nothing leaves the process unless Kibana reports `optIn === true`. See [docs/telemetry.md](docs/telemetry.md) for the event catalog, what's collected, and how to opt out.

### Skills

The `skills/` directory contains [Claude Skills](https://claude.com/docs/skills/overview) — `SKILL.md` files that teach Claude *when* and *how* to use the tools. See [docs/setup-skills.md](docs/setup-skills.md) for installation instructions.
Expand Down
3 changes: 3 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ The `generate-attack-discovery` tool triggers Kibana's Attack Discovery API with

### Kibana 9.x Compatibility
All Kibana API calls include `elastic-api-version: 2023-10-31` headers, `x-elastic-internal-origin: Kibana` for internal APIs, and camelCase field names.

### Telemetry
The MCP App emits a small set of anonymised usage events via `@elastic/ebt`, mirrored to the user's Kibana telemetry opt-in. The shipper is fail-closed: events never leave the process unless Kibana reports `optIn === true`. See [`docs/telemetry.md`](./telemetry.md) for the event catalog and opt-out story.
215 changes: 215 additions & 0 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# Telemetry

The Elastic Security MCP App emits a small set of anonymised usage events
via [`@elastic/ebt`](https://www.npmjs.com/package/@elastic/ebt) so the
team can see which views and tools are being used. Events ship to
`telemetry.elastic.co` and are subject to the same opt-in the user
controls in Kibana — the app never reports anything when Kibana
telemetry is disabled.

## Opt-in source of truth

On MCP server start the app fetches the user's telemetry config from the
default-cluster Kibana:

```
GET /api/telemetry/v2/config
```

The response's `optIn` field maps to the analytics client's shipping
state:

| Kibana `optIn` | App behaviour |
|------------------|-------------------------------------|
| `true` | Events ship to `telemetry.elastic.co` |
| `false` | Events queued in-memory, then dropped |
| `null` | Treated as opted-out |
| Fetch error | Treated as opted-out (fail-closed) |

The `optIn` is read once at startup; there is no polling. Restart the
MCP server (or your MCP host) after flipping the Kibana setting for the
change to take effect.

## What gets collected

### `mcp_tool_called` (server-side)

Emitted every time an MCP tool handler returns or throws. Wrapped
around every model-facing tool and every app-only tool _except_
`report-analytics-event` (which would otherwise duplicate the
`view_rendered` traffic).

| Field | Type | Notes |
|---------------|---------|--------------------------------------------------------|
| `tool_id` | keyword | One of the values listed under "Allowed tool ids" below |
| `duration_ms` | long | Wall-clock duration of the handler, in milliseconds |
| `success` | boolean | `true` if the handler resolved, `false` if it threw |

### `view_rendered` (client-side)

Emitted once per mount of each top-level React view, via the app-only
`report-analytics-event` MCP tool.

| Field | Type | Allowed values (closed enum) |
|-----------|---------|-------------------------------------------------------------------------------------------------------------|
| `view_id` | keyword | `alert-triage`, `attack-discovery`, `case-management`, `detection-rules`, `sample-data`, `threat-hunt` |

## Allowed `tool_id` values

`tool_id` is bound to the registered MCP tool name, so renames here are
schema-impacting events for downstream dashboards. The current set:

```
acknowledge-alert
acknowledge-alerts-bulk
acknowledge-discoveries
add-case-comment
approve-discoveries
assess-discovery-confidence
attach-alert-to-case
check-existing-sample-data
cleanup-sample-data
create-case
create-rule
create-rules-for-scenario
enrich-discovery
execute-esql
find-rules
generate-attack-discovery
generate-sample-data
generate-scenario
get-alert-context
get-case
get-case-alerts
get-case-comments
get-entity-detail
get-generation-status
get-mapping
get-rule
get-user-profile
investigate-entity
list-ai-connectors
list-cases
list-indices
manage-cases
manage-exceptions
manage-rules
noisy-rules
patch-rule
poll-alerts
poll-discoveries
threat-hunt
toggle-rule
triage-alerts
triage-attack-discoveries
unacknowledge-alert
update-case
validate-query
```

Add new tools by registering them with `registerTrackedAppTool` — they
will start appearing automatically once shipped.

## Context attached to every event

The Elastic V3 shipper enriches each event with a small context block
derived from the **default** cluster:

| Field | Source | Notes |
|-------------------|-------------------------------------|------------------------------------------------|
| `cluster_uuid` | Elasticsearch `GET /` | Required by the V3 shipper; events do not ship without it |
| `cluster_version` | Elasticsearch `GET /` `version.number` | Stack version |
| `license_id` | Elasticsearch `GET /_license` | Optional |
| `license_status` | Elasticsearch `GET /_license` | Optional |
| `license_type` | Elasticsearch `GET /_license` | Optional |
| `mcp_app_version` | `package.json` `version` | Version of the MCP App that emitted the event |

`cluster_name` is deliberately **not** collected. It's user-controlled
and frequently contains company / environment identifiers, so shipping
it would undermine the "anonymised" framing of this feed.

### Segmentation granularity

There is no `install_id` — two MCP App installs against the same Elastic
cluster will share `cluster_uuid` and are not distinguishable in the
telemetry stream. This is fine for v1; if per-install segmentation
becomes important later we can stamp a random UUID into the credential
file on first run and add it as a context field.

## What does **not** get collected

The schemas above are closed — no free-form text, no PII, no Kibana
user identifiers, no alert / case / rule bodies, no ES|QL queries.
Adding a new field requires:

1. Extending `McpToolCalledEvent` / `ViewRenderedEvent` in
`src/elastic/analytics/events.ts` (and its EBT schema sibling).
2. Adding the field to the registered context provider or event
definition.
3. Updating this document.

The `report-analytics-event` MCP tool that the frontend uses to forward
client-side events accepts a strict Zod schema (`eventType: z.literal`,
`viewId: z.enum`) so a malicious or buggy view cannot smuggle free-form
text into the pipeline.

## Opting out

End-users have one knob: the **Kibana** telemetry setting. The MCP
App mirrors it; flipping it off in Kibana stops the MCP App from
shipping any events. Restart the MCP host after flipping the setting.

### Developer escape hatches

| Environment variable | Effect |
|-------------------------------|--------------------------------------------------------------------------------------------------------------|
| `MCP_APP_TELEMETRY_ENV=staging` | Ships events to `telemetry-staging.elastic.co` instead of production. Useful for local dev / dashboard work. |
| `NODE_ENV=production` | Currently only affects EBT's internal `isDev` flag (logging verbosity). Does **not** disable shipping. |

The MCP App does not provide an "always off regardless of Kibana"
override beyond unsetting the user's Kibana opt-in. If you need that,
turn off Kibana telemetry on your dev cluster — the fail-closed
behaviour does the rest.

## Bundle-size impact

Adding `@elastic/ebt@^1.4.1` (which transitively pulls `rxjs`,
`fp-ts`, `io-ts`, `moment`, `js-sha256`, `lodash.get`, `lodash.has`,
`node-fetch@2`, and `@babel/runtime`) increases the bundled
`dist/main.bundle.mjs` by roughly **+560 KB raw / +113 KB gzipped**
(~15% raw, ~17% gzipped) on a fresh measurement. This is a one-time
download for `.mcpb` installs; the bundle is loaded into the MCP host
once and reused across sessions.

`node-fetch@2` is redundant on Node 22 (native `fetch`) but EBT pulls
it unconditionally; tree-shaking it out would require a fork of
`@elastic/ebt`. Acceptable for now.

## Where things live in the codebase

```
src/elastic/analytics/
analytics-client.ts - AnalyticsClient interface (what the rest of the app sees)
create-analytics-client.ts - Factory: registers ElasticV3ServerShipper + EBT event types + context providers
context-loader.ts - One-shot GET / + GET /_license on startup
events.ts - Event type IDs and Zod-typed payload schemas
index.ts - Public module surface

src/elastic/client/telemetryConfigClient.ts
Wraps GET /api/telemetry/v2/config on the default-cluster Kibana

src/elastic/service/telemetryService.ts
Fetches the telemetry config and mirrors optIn → analytics.setOptIn

src/tools/tracked-app-tool.ts
Drop-in replacement for `registerAppTool` that emits `mcp_tool_called`

src/tools/analytics.ts
Registers the app-only `report-analytics-event` MCP tool

src/shared/hooks/McpAppProvider.tsx, useMcpApp.ts, useAnalytics.ts
React context + hooks: provider owns the McpApp; useAnalytics reads it

src/shared/analytics-events.ts
Single source of truth for the view-id enum, imported by both server and views
```
Loading