Skip to main content
Glama
csimi

elastic-mcp

by csimi
README.md
# elastic-mcp

[![npm version](https://img.shields.io/npm/v/elastic-mcp.svg)](https://www.npmjs.com/package/elastic-mcp)
[![build](https://github.com/csimi/elastic-mcp/actions/workflows/build.yml/badge.svg)](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

A3.9/5.0

Scored across 14 tools

Disambiguation4/5

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 Consistency4/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivitySlowing
ResponsivenessNo issues