Content
# 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. |
| `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`, `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).
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available