elastic-mcp
# elastic-mcp
[](https://www.npmjs.com/package/elastic-mcp)
[](https://github.com/csimi/elastic-mcp/actions/workflows/build.yml)
> ⚠️ **Disclaimer: this MCP server was vibe coded.** It was built iteratively with an AI assistant and is
> typechecked and unit-tested, but review it yourself before relying on it — especially the write tools —
> and use it at your own risk.
A [Model Context Protocol](https://modelcontextprotocol.io) server for Elasticsearch, written in JavaScript with JSDoc types checked by TypeScript (`@ts-check` / `checkJs`). Read-only by default, with optional write tools.
It exposes search, index inspection, and cluster information tools over the **stdio** transport, and transparently supports:
- Elasticsearch deployments served behind a reverse proxy under a **base path** (for example `https://host/elasticsearch`) — something the official client does not handle out of the box.
- Both **Elasticsearch 8.x and 9.x** clusters — the v9 client speaks `compatible-with=9` and is rejected by 8.x clusters, so the matching client major is selected at startup (auto-detected or pinned).
> **Tested in corporate environments** where API keys are often issued to restricted users with limited
> privileges (e.g. index `read` only, without cluster `monitor` or `view_index_metadata`). The server is
> built to degrade gracefully in these setups rather than fail outright — it falls back to lower-privilege
> APIs where possible and surfaces clear authorization errors otherwise. See
> [Elasticsearch version compatibility](#elasticsearch-version-compatibility) and the
> [privilege fallback table](#creating-an-api-key) for details.
## Usage
Run it straight from npm with `npx` (no install needed — set the environment variables from
[Configuration](#configuration) first):
```sh
ELASTICSEARCH_URL=http://localhost:9200/elastic npx elastic-mcp
```
Or install it globally to get the `elastic-mcp` command on your `PATH`:
```sh
npm install -g elastic-mcp
ELASTICSEARCH_URL=http://localhost:9200/elastic elastic-mcp
```
### Claude Desktop / Claude Code
Add the server to your MCP client configuration:
```json
{
"mcpServers": {
"elastic": {
"command": "npx",
"args": ["-y", "elastic-mcp"],
"env": {
"ELASTICSEARCH_URL": "https://host/elasticsearch",
"ELASTICSEARCH_API_KEY": "<base64-api-key>"
}
}
}
}
```
## Tools
### Read tools
Always registered, and all marked read-only (`readOnlyHint`).
| Tool | Description |
| --- | --- |
| `search` | Run a Query DSL search (query, aggs, sort, size, from, _source). |
| `count` | Count documents matching an optional query. |
| `esql` | Run an ES\|QL query (complete query string); returns column metadata plus keyed row objects. Requires Elasticsearch 8.11+. |
| `get_document` | Fetch a single document by index and id. |
| `list_indices` | List indices with health, status, doc count, and size; falls back to names-only via `_resolve/index` without cluster `monitor`. |
| `get_mapping` | Get field mappings for one or more indices; falls back to read-level `_field_caps` (field names + types) without `view_index_metadata`. |
| `get_settings` | Get settings for one or more indices. |
| `get_aliases` | List index aliases. |
| `list_shards` | List shards with role, state, doc count, store size, and node (equivalent to `_cat/shards`); the doc count and store size come back null without cluster `monitor`. |
| `get_kibana_object` | Fetch a Kibana saved object by `<type>:<id>`; decodes inline JSON-string fields (visState, searchSourceJSON, ...) and resolves `references[]` to titles. Reads `.kibana_analytics` / `.kibana`, so the key needs `read` on those. |
| `cluster_health` | Cluster health, optionally scoped to indices. |
| `cluster_stats` | Cluster-wide statistics. |
| `cluster_info` | Cluster name, UUID, and version. |
| `list_nodes` | List nodes with role, heap, CPU, and load. |
### Write tools
**Off by default.** Registered only when `ELASTICSEARCH_ENABLE_WRITES=true`. Each is marked
`destructiveHint` (except `create_index`) so MCP clients can prompt for confirmation, and of course only
works if the API key carries the matching write privileges.
| Tool | Description |
| --- | --- |
| `index_document` | Create or replace a document (auto-generates an id if omitted; `createOnly` to avoid overwriting). |
| `update_document` | Partially update a document by id, with optional upsert. |
| `delete_document` | Delete a single document by id. |
| `create_index` | Create an index, optionally with mappings, settings, and aliases. |
| `delete_index` | Permanently delete one or more indices — cannot be undone. |
## Configuration
Configuration is read from environment variables (see [`.env.example`](./.env.example)):
| Variable | Required | Description |
| --- | --- | --- |
| `ELASTICSEARCH_URL` | yes | Endpoint URL, optionally including a base path. |
| `ELASTICSEARCH_API_KEY` | no | Base64 API key; takes precedence over basic auth. |
| `ELASTICSEARCH_USERNAME` | no | Basic-auth username. |
| `ELASTICSEARCH_PASSWORD` | no | Basic-auth password. |
| `ELASTICSEARCH_CA_FINGERPRINT` | no | SHA-256 fingerprint of the CA certificate. |
| `ELASTICSEARCH_TLS_VERIFY` | no | Defaults to `true`; set to `false` to accept self-signed/invalid certs (dev only). |
| `ELASTICSEARCH_API_VERSION` | no | `8`, `9`, or `auto` (default). Selects the client major; see below. |
| `ELASTICSEARCH_ENABLE_WRITES` | no | Set to `true` to register the write tools. Off by default (server is read-only). |
### Elasticsearch version compatibility
The official client always sends a `compatible-with=<major>` media type that matches its own major
version, and a cluster of a different major rejects it. To work against both 8.x and 9.x clusters, both
client majors are bundled and the right one is chosen at startup:
- `auto` (default) — probe the cluster's root endpoint (with a plain `application/json` request that
side-steps the compatibility header) and pick the matching client major. Anything up to 8.x uses the v8
client; 9.x and newer use the v9 client.
- `8` / `9` — pin the client major explicitly and skip the probe.
> **Note:** the root probe requires the `cluster:monitor/main` privilege. Index-scoped API keys often lack
> it (it can only be granted by an admin), in which case the probe fails with
> `action [cluster:monitor/main] is unauthorized`. When that happens the server logs a warning and **falls
> back to the v8 client**, which speaks `compatible-with=8` — accepted by both 8.x and 9.x clusters — so
> search and index inspection keep working. Set `ELASTICSEARCH_API_VERSION` explicitly to skip the probe
> (and the warning) entirely.
### Base path support
When `ELASTICSEARCH_URL` contains a path (e.g. `https://host/elasticsearch`), the URL is split into its
origin (`https://host`) and prefix (`/elasticsearch`). A custom connection class prepends the prefix to every
request path, because the official client routes requests against the origin only and silently drops the path.
### Creating an API key
By default the server is read-only, so it only needs cluster **`monitor`** rights (for `cluster_health`,
`cluster_stats`, `cluster_info`, `list_nodes`, and the `_cat` listings) plus **`read`** and
**`view_index_metadata`** on the indices you want to expose. Create a least-privilege key with the
[Create API key API](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html):
```http
POST /_security/api_key
{
"name": "elastic-mcp",
"role_descriptors": {
"elastic_mcp_read_only": {
"cluster": ["monitor"],
"indices": [
{
"names": ["*"],
"privileges": ["read", "view_index_metadata"]
}
]
}
}
}
```
Or with `curl`:
```sh
curl -u elastic -X POST "$ELASTICSEARCH_URL/_security/api_key" \
-H 'Content-Type: application/json' \
-d '{
"name": "elastic-mcp",
"role_descriptors": {
"elastic_mcp_read_only": {
"cluster": ["monitor"],
"indices": [
{ "names": ["*"], "privileges": ["read", "view_index_metadata"] }
]
}
}
}'
```
The response contains an `encoded` field — that base64 value is exactly what `ELASTICSEARCH_API_KEY` expects:
```json
{
"id": "VuaCfGcBCdbkQm-e5aOx",
"name": "elastic-mcp",
"api_key": "ui2lp2axTNmsyakw9tvNnw",
"encoded": "VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw=="
}
```
```sh
ELASTICSEARCH_API_KEY=VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw==
```
Restrict `indices[].names` to specific index names or patterns to narrow access further.
#### Kibana saved-object access (only for `get_kibana_object`)
`get_kibana_object` reads Kibana's saved objects straight from the `.kibana_analytics` and `.kibana`
indices. These are **restricted system indices**, so a plain `read` on `*` does not reach them — the role
needs a dedicated entry that opts in with `allow_restricted_indices`. Add it alongside the existing read
entry (keep the wildcard entry at `false` so the key cannot read other system indices such as `.security-*`):
```json
{
"name": "elastic-mcp",
"role_descriptors": {
"elastic_mcp_read_only": {
"cluster": ["monitor"],
"indices": [
{ "names": ["*"], "privileges": ["read", "view_index_metadata"], "allow_restricted_indices": false },
{ "names": [".kibana*"], "privileges": ["read"], "allow_restricted_indices": true }
]
}
}
}
```
To widen an **existing** key without rotating it, the [Update API key API](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-update-api-key.html)
(`PUT /_security/api_key/<id>`) rewrites its `role_descriptors` in place — the id and encoded value are
unchanged, so `ELASTICSEARCH_API_KEY` does not need updating. Omit this entry entirely if you do not use
`get_kibana_object`; the tool then returns an authorization error and the rest of the server is unaffected.
#### Write privileges (only if `ELASTICSEARCH_ENABLE_WRITES=true`)
The read-only key above cannot mutate data — the write tools would return authorization errors. To allow
them, add the relevant index privileges: **`write`** (covers `index_document`, `update_document`,
`delete_document`), **`create_index`**, and **`delete_index`**:
```json
{
"name": "elastic-mcp",
"role_descriptors": {
"elastic_mcp_read_write": {
"cluster": ["monitor"],
"indices": [
{
"names": ["*"],
"privileges": ["read", "view_index_metadata", "write", "create_index", "delete_index"]
}
]
}
}
}
```
Grant only the subset you need — for example drop `delete_index` if you never delete indices. Cluster
`monitor` remains optional (see the fallback table below).
#### Graceful privilege degradation
Some tools degrade gracefully when privileges are missing, falling back **once** to a lower-privilege API
and remembering that for the rest of the process so they don't retry the rejected endpoint:
| Tool | Needs | Fallback when missing | Granted by |
| --- | --- | --- | --- |
| `list_indices` | cluster `monitor` | `_resolve/index` (names only) | `view_index_metadata` |
| `get_mapping` | `view_index_metadata` | `_field_caps` (field names + types) | `read` |
So a key with only `read` + `view_index_metadata` (or even just `read`) still gets useful results from
`search`, `count`, `esql`, `get_document`, `get_mapping`, and `list_indices`. The tools that have no lower-privilege
equivalent — `cluster_health`, `cluster_stats`, `cluster_info`, `list_nodes` (cluster `monitor`),
`get_settings`, `get_aliases` (`view_index_metadata`), and `get_kibana_object` (restricted `.kibana*` read,
see above) — return an authorization error without the privilege.
## Contributing
See [CLAUDE.md](./CLAUDE.md) for the project layout, architecture notes, and coding conventions.
## License
ISC — see [LICENSE](./LICENSE).
TDQS
Scored across 14 tools
The tool set maps cleanly to distinct Elasticsearch endpoints: cluster, nodes, shards, indices, documents, mappings, settings, aliases, and Kibana objects. Search, count, and esql overlap somewhat as query paths, but their descriptions clearly differentiate Query DSL hits/aggregations, simple counts, and ES|QL pipelines.
Naming is mostly predictable: list_* enumerates cluster resources, get_* fetches specific objects or metadata, cluster_* covers cluster-level views, and search/count/esql are self-explanatory actions. The mix of list_/get_ and bare verbs is a minor inconsistency, but it is still readable and not chaotic.
14 tools is well within the ideal range and each tool covers a major Elasticsearch operation area: cluster, nodes, shards, indices, documents, queries, and Kibana. No tool feels redundant or excessive for the apparent read/query/monitoring scope.
As a read/query/monitoring surface, it covers the main workflows: cluster health and stats, node/shards inspection, index metadata, aliases, mappings, documents, multiple query languages, and Kibana saved objects. The obvious gap is the absence of write/management tools such as create, update, or delete, so the server is complete only for read-oriented use.