Elastic MCP Server
# Elastic MCP Server
A [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that gives AI assistants read-only access to your Elasticsearch/Kibana logs. Ask questions in natural language and get answers backed by real log data.
Works with [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Claude Desktop](https://claude.ai/download), and any MCP-compatible client. Your project can be in any language — Python, Java, Go, Node.js, etc. This server runs independently.
<p align="center">
<img src="https://img.shields.io/badge/MCP-compatible-blue" alt="MCP Compatible" />
<img src="https://img.shields.io/badge/node-%3E%3D18-green" alt="Node >= 18" />
<img src="https://img.shields.io/badge/license-MIT-brightgreen" alt="MIT License" />
</p>
## How It Works
```
Your project (any language) elastic-mcp-server Kibana / Elasticsearch
┌───────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Claude Code or │ stdio │ │ HTTPS │ │
│ Claude Desktop │────MCP──>│ Translates to │────────->│ Executes query │
│ │<─────────│ Kibana queries │<─────────│ Returns results │
└───────────────────┘ └──────────────────┘ └──────────────────┘
```
The server runs as a standalone Node.js process alongside your project. Claude spawns it, communicates via stdio, and uses it to search your logs. It doesn't depend on your project's language, framework, or build system. All requests are **read-only**.
## Requirements
- **Node.js** >= 18 **or Docker** (only for running this MCP server — your project can use any language)
## Quick Start
### Option A: Node.js
#### 1. Clone and build
```bash
git clone https://github.com/vanovarderesyan/elastic-mcp-server.git
cd elastic-mcp-server
npm install
npm run build
```
This creates the compiled server at `dist/main.js`.
#### 2. Configure
Add the MCP server to your AI assistant. You need:
- The **absolute path** to `dist/main.js` where you cloned this repo
- Your **Kibana URL** and **credentials**
**Claude Code** — create or edit `.mcp.json` in any project directory:
```json
{
"mcpServers": {
"elasticsearch": {
"command": "node",
"args": ["/home/john/elastic-mcp-server/dist/main.js"],
"env": {
"ELASTIC_NODES": "https://kibana.example.com",
"ELASTIC_USERNAME": "your-username",
"ELASTIC_PASSWORD": "your-password"
}
}
}
}
```
**Claude Desktop** — edit the config file:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"elasticsearch": {
"command": "node",
"args": ["/home/john/elastic-mcp-server/dist/main.js"],
"env": {
"ELASTIC_NODES": "https://kibana.example.com",
"ELASTIC_USERNAME": "your-username",
"ELASTIC_PASSWORD": "your-password"
}
}
}
}
```
> **Note:** Replace `/home/john/elastic-mcp-server` with the actual path where you cloned this repo.
### Option B: Docker
No Node.js required — just Docker.
#### 1. Build the image
```bash
git clone https://github.com/vanovarderesyan/elastic-mcp-server.git
cd elastic-mcp-server
docker build -t elastic-mcp-server .
```
#### 2. Configure
**Claude Code** — create or edit `.mcp.json`:
```json
{
"mcpServers": {
"elasticsearch": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "ELASTIC_NODES=https://kibana.example.com",
"-e", "ELASTIC_USERNAME=your-username",
"-e", "ELASTIC_PASSWORD=your-password",
"elastic-mcp-server"
]
}
}
}
```
**Claude Desktop** — same structure in your `claude_desktop_config.json`.
> **Why `-i` and no `-t`?** MCP communicates over stdin/stdout using structured JSON. The `-i` flag keeps stdin open for the MCP protocol. The `-t` flag (TTY) is not needed and would interfere with the JSON stream.
#### Cloudflare Zero Trust with Docker
If your Kibana is behind Cloudflare Access, the container needs access to your host's Cloudflare tokens. Mount the `~/.cloudflared` directory as a read-only volume:
```json
{
"mcpServers": {
"elasticsearch": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-v", "/home/john/.cloudflared:/root/.cloudflared:ro",
"-e", "ELASTIC_NODES=https://kibana.example.com",
"-e", "ELASTIC_USERNAME=your-username",
"-e", "ELASTIC_PASSWORD=your-password",
"elastic-mcp-server"
]
}
}
}
```
Replace `/home/john/.cloudflared` with your actual home directory path (e.g., `/Users/jane/.cloudflared` on macOS).
You must authenticate on your host machine first:
```bash
cloudflared access login https://kibana.example.com
```
The container reads the cached token from the mounted volume. When the token expires, re-run `cloudflared access login` on your host — the container picks up the new token automatically.
Alternatively, pass a token directly via environment variable:
```json
"-e", "CF_ACCESS_TOKEN=eyJhbGciOi..."
```
### 3. Discover your indices
Every Elasticsearch setup is different. Your indices might be named `filebeat-*`, `logstash-*`, `production-api-*`, `staging-*`, or something else entirely. The first thing to do after setup is ask Claude to list what's available:
```
List all data views
```
This shows you the actual index patterns in your Kibana. Use those names when searching.
> **Important:** By default, the `index` parameter is `*`, which searches **all environments** (production, staging, dev, etc.). Always specify the index when searching to avoid mixing logs from different environments:
```
Search for errors in production-api-* from the last hour
Show me logs from staging-auth-service-* today
Find "timeout" errors in filebeat-production-*
```
### 4. Start asking questions
```
Show me errors in production-* from the last hour
What are the top 10 services by log volume today in production-*?
Find logs containing "connection refused" in staging-*
Show error trends over the past 24 hours in filebeat-*
What fields are available in the logs-nginx-* index?
```
> **Tip:** If you're not sure which index to use, just ask Claude — it will use the `list_data_views` tool to find the right one.
## Tools
The server exposes 7 read-only tools:
### `search_logs`
Search logs with filters, time ranges, and free-text queries.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `index` | string | `*` | Index pattern (e.g., `logs-*`, `filebeat-*`) |
| `query` | string | | Free-text query (Lucene syntax) |
| `service` | string | | Filter by `kubernetes.container.name` |
| `from` | string | | Start time (`now-1h`, `2024-01-01T00:00:00Z`) |
| `to` | string | | End time (`now`) |
| `filters` | object | | Key-value field filters (e.g., `{"stream": "stderr"}`) |
| `messageLevel` | string | | Log level inside JSON message (`error`, `warn`, `info`) |
| `messageQuery` | string | | Phrase search within message field |
| `excludeHealthChecks` | boolean | `false` | Exclude health/readiness probe logs |
| `size` | number | `50` | Max results to return |
| `sort` | `asc`/`desc` | `desc` | Sort order by timestamp |
### `aggregate_logs`
Run aggregations for analytics and trend analysis.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `index` | string | `*` | Index pattern |
| `aggType` | enum | | `terms`, `date_histogram`, `stats`, or `count` |
| `field` | string | | Field to aggregate on |
| `service` | string | | Filter by service name |
| `from` / `to` | string | | Time range |
| `interval` | string | `1h` | Interval for `date_histogram` |
| `query` | string | | Free-text filter |
| `messageQuery` | string | | Phrase search in message |
| `filters` | object | | Key-value field filters |
### `list_indices`
List available data views filtered by pattern.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `pattern` | string | `*` | Glob-style filter (e.g., `logs-*`) |
### `list_data_views`
List all Kibana data views (index patterns). No parameters.
### `get_mapping`
Get field names, types, and whether they are aggregatable for an index.
| Parameter | Type | Description |
|-----------|------|-------------|
| `index` | string | Index pattern (e.g., `filebeat-*`) |
### `get_document`
Fetch a single document by its Elasticsearch ID.
| Parameter | Type | Description |
|-----------|------|-------------|
| `index` | string | Index name or pattern |
| `id` | string | Document ID |
### `cluster_health`
Check Kibana connectivity and list sample data views. No parameters.
## Configuration
All configuration is via environment variables in the `env` block of your MCP config.
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `ELASTIC_NODES` | **Yes** | | Kibana base URL (e.g., `https://kibana.example.com`) |
| `ELASTIC_USERNAME` | No | | Basic auth username |
| `ELASTIC_PASSWORD` | No | | Basic auth password |
| `CF_ACCESS_TOKEN` | No | *auto* | Cloudflare Access JWT (see below) |
| `KIBANA_SPACE` | No | `default` | Kibana space name |
| `ELASTIC_TLS_REJECT_UNAUTHORIZED` | No | `true` | Set `false` for self-signed certificates |
| `ELASTIC_REQUEST_TIMEOUT` | No | `60000` | Request timeout in milliseconds |
| `ELASTIC_MAX_RESULTS` | No | `500` | Maximum number of search results |
| `ALLOWED_SERVICES` | No | *(all)* | Comma-separated allowlist of `kubernetes.container.name` values |
### Index Patterns
The `index` parameter in search and aggregation tools accepts Elasticsearch index patterns. These depend on your setup. Common examples:
| Setup | Typical Pattern | Example |
|-------|----------------|---------|
| Filebeat | `filebeat-*` | `filebeat-2024.01.*` |
| Logstash | `logstash-*` | `logstash-production-*` |
| Fluentd/Fluent Bit | `fluentd-*` or `fluent-bit-*` | `fluent-bit-staging-*` |
| Data streams | `logs-*` | `logs-nginx-*` |
| Custom per-env | `production-*`, `staging-*` | `production-api-gateway*` |
Use the `list_data_views` tool to see what's available in your Kibana, or ask Claude: "What indices do we have?"
### Service Allowlist
Restrict which Kubernetes services can be queried by setting `ALLOWED_SERVICES`:
```json
"ALLOWED_SERVICES": "api-gateway,auth-service,payment-service"
```
When empty or unset, all services are accessible.
## Authentication
### Basic Auth
Set `ELASTIC_USERNAME` and `ELASTIC_PASSWORD` in the `env` block. These are sent as a standard HTTP Basic auth header on every request.
### Cloudflare Zero Trust (optional)
If your Kibana is behind [Cloudflare Zero Trust](https://developers.cloudflare.com/cloudflare-one/) (formerly Cloudflare Access), the server handles token management automatically. No extra configuration is needed beyond the initial login.
#### How it works
When the server starts, it looks for a Cloudflare Access JWT to include as a `cf-access-token` header on every request to Kibana. The token resolution order is:
1. **Read from `~/.cloudflared/` cache** — `cloudflared` stores tokens as files named `<hostname>-<audience>-token` after a successful login
2. **Auto-refresh via `cloudflared access login`** — if the cached token is expired or missing, the server runs `cloudflared access login <url>` which opens a browser for authentication (Node.js only, not in Docker)
3. **Fall back to `CF_ACCESS_TOKEN` env var** — if the above steps fail, the server uses the token from the environment variable
4. **Retry on 401/403** — if a request fails with an auth error, the server re-reads the token from disk and retries once (handles cases where the token was refreshed externally)
#### Prerequisites
Install [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/):
```bash
# macOS
brew install cloudflared
# Linux (Debian/Ubuntu)
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflare-main $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflare-main.list
sudo apt update && sudo apt install cloudflared
```
#### First-time setup
```bash
cloudflared access login https://your-kibana-url.com
```
A browser window opens for authentication. After completing the login, a JWT is cached in `~/.cloudflared/`. The server reads this automatically — no need to copy tokens manually.
#### Token lifecycle
| Scenario | What happens |
|----------|-------------|
| Token valid | Used automatically from `~/.cloudflared/` |
| Token expired | Server runs `cloudflared access login` → browser opens → new token cached |
| Token expired (Docker) | Browser can't open — re-run `cloudflared access login` on your **host** machine |
| Manual token | Set `CF_ACCESS_TOKEN` env var to skip auto-detection entirely |
| Request gets 401/403 | Server re-reads token from disk and retries once |
#### Docker considerations
Docker containers can't open a browser for interactive Cloudflare login. Two options:
**Option 1: Mount the host's token cache (recommended)**
```json
"-v", "/home/john/.cloudflared:/root/.cloudflared:ro"
```
The container reads your host's cached tokens. When they expire, re-run `cloudflared access login` on your host machine — the container sees the updated token immediately via the mounted volume.
**Option 2: Pass token via environment variable**
```json
"-e", "CF_ACCESS_TOKEN=eyJhbGciOi..."
```
Obtain a token manually (`cloudflared access login`, then read the file from `~/.cloudflared/`) and pass it directly. Note: this token will eventually expire and you'll need to update it.
#### Not using Cloudflare?
If your Kibana is not behind Cloudflare Zero Trust, you can ignore all of the above. The server will skip Cloudflare authentication entirely when no token is found and `cloudflared` is not installed.
## Project Structure
```
src/
main.ts # Entry point — registers tools and starts stdio transport
kibana-client.ts # Kibana HTTP client with auth and retry logic
cf-token.ts # Cloudflare Access token auto-management
allowed-services.ts # Service allowlist filtering
utils/
format-response.ts # Response truncation and formatting
tools/
search-logs.tool.ts # search_logs
aggregate-logs.tool.ts # aggregate_logs
list-indices.tool.ts # list_indices
list-data-views.tool.ts # list_data_views
get-mapping.tool.ts # get_mapping
get-document.tool.ts # get_document
cluster-health.tool.ts # cluster_health
```
## License
MIT
TDQS
Scored across 7 tools
Most tools have clear, distinct roles, but list_indices, list_data_views, and cluster_health all surface data-view/index information and could be confused during discovery. The remaining search, aggregate, mapping, and document tools are well separated.
The tool names mostly follow a predictable verb_noun pattern: list_*, get_*, search_*, and aggregate_*. cluster_health is the main outlier, since it reads as a noun phrase rather than an imperative action like the others.
Seven tools is a reasonable size for a focused Kibana/Elasticsearch log server. The count is slightly higher than necessary because listing functionality is split across multiple overlapping tools.
The toolset covers the full log-exploration workflow: discover data sources, inspect mappings, search logs, aggregate logs, and fetch individual documents. Health checking is also present, so agents can verify connectivity without hitting a dead end.