# esmole-mcp esmole-mcp is a stdio MCP server for Elasticsearch (7.x and 8.x). It exposes named connections, optional SSH tunnels, and runtime connection management: you add, edit, and remove connections through MCP tools without restarting the harness. It is built on the same core as [dbmole-mcp](../dbmole-mcp/README.md), so connection sources, the persistent store, and SSH tunnelling behave identically. It ships as both an npx package and a Docker image, so it runs anywhere your MCP client does. ## Quick start (npx) Add the server to your client's `.mcp.json`: ```jsonc { "mcpServers": { "dbmole": { "command": "npx", "args": ["-y", "dbmole-mcp"] }, "esmole": { "command": "npx", "args": ["-y", "esmole-mcp"] } } } ``` That is enough to start the server. With no connections configured it still runs; use `add_connection` to register a cluster, or supply connections up front through one of the sources below. ## Connection sources Connections are merged from three layers. Higher layers win on name collisions. | Priority | Source | How to set | Mutable at runtime | | --- | --- | --- | --- | | High | `ESMOLE_CONNECTIONS` env | JSON array of connection objects | No (read-only) | | Medium | Config file | `--config ` or `ESMOLE_CONFIG`, shape `{ "connections": [...] }` | No (read-only) | | Low | Persistent store | `~/.config/esmole/connections.json` (mode `0600`); override with `ESMOLE_STORE` | Yes (via tools) | Reads resolve name collisions by layer priority (env > config > store), and `list_connections` reports the source layer that won. `add_connection`, `update_connection`, and `remove_connection` only touch the store layer; editing a connection that came from the env or a config file returns an error naming its source. ## Connection config reference Each connection is an object with the following fields. Unknown fields are rejected (the schemas are strict). | Field | Type | Notes | | --- | --- | --- | | `name` | string | matches `[a-zA-Z0-9_-]+`; `add_connection` refuses a name already present in any layer | | `type` | `elasticsearch` | the cluster type | | `host` | string | cluster host | | `port` | number | defaults to `9200` | | `scheme` | `http` \| `https` | defaults to `https` | | `verifyTls` | boolean | defaults to `true`; set `false` to skip TLS certificate verification | | `user` | string | basic-auth user (pair with `password`) | | `password` | string | basic-auth password | | `apiKey` | string | API key, as an alternative to `user`+`password` | | `readonly` | boolean | defaults to `false`; see Readonly mode below | | `ssh` | object | optional SSH tunnel (see below) | Authentication is required: provide either an `apiKey` **or** both `user` and `password`. The `ssh` object accepts: | Field | Type | Notes | | --- | --- | --- | | `host` | string | bastion host | | `port` | number | defaults to `22` | | `user` | string | SSH user | | `password` | string | password auth | | `privateKey` | string | key contents | | `privateKeyPath` | string | path to a key file; a leading `~` is expanded | | `agent` | boolean | use the SSH agent at `SSH_AUTH_SOCK` | | `passphrase` | string | passphrase for an encrypted key | A full example — an HTTPS cluster reachable only through a bastion, opened read-only with an API key: ```json { "name": "prod-es", "type": "elasticsearch", "host": "10.0.0.20", "port": 9200, "scheme": "https", "verifyTls": true, "apiKey": "VnVhQ2ZHY0JDZGJrUW0t...", "readonly": true, "ssh": { "host": "bastion.example.com", "port": 22, "user": "deploy", "privateKeyPath": "~/.ssh/id_ed25519" } } ``` ## Tools Every ES tool returns `{ status, body }` — the HTTP status code and the parsed response body. A 4xx or 5xx from the cluster never throws: the error body is returned so the caller can inspect it. | Tool | What it does | | --- | --- | | `list_connections` | list configured connections and their source layer | | `add_connection` | register a new connection in the store | | `update_connection` | patch a stored connection; a `null` value removes that field | | `remove_connection` | delete a stored connection | | `test_connection` | open a connection and report success or the failure reason | | `es_request` | generic passthrough: any method/path/body, readonly-guarded, response truncated to a size budget | | `es_search` | run a `query`/`aggs` against an index; `_source` projection; pass aggs only with `size: 0` for aggregation-only requests | | `es_list_indices` | list indices on the cluster | | `es_get_mapping` | mapping for an index, flattened to `field: type`; `raw: true` returns the unflattened mapping | | `es_cluster_health` | cluster health | ## Readonly mode The `readonly` flag blocks all writes. It is enforced by an allowlist, not by trusting the caller: - `GET` and `HEAD` are always allowed. - `POST` is allowed only to read-intent endpoints — `_search`, `_msearch`, `_count`, `_field_caps`, `_mapping`, `_search/scroll`, `_pit`, and `_cat`. - `DELETE` is allowed only for read-session teardown (`_pit`, `_search/scroll`). - Everything else (index, update, bulk, delete-by-query, settings changes, …) is rejected before any request reaches the cluster. For a hard guarantee in untrusted contexts, still connect with a least-privilege ES role — readonly mode is defence in depth, not a substitute for cluster permissions. ## MCP client config Run dbmole and esmole side by side: ```jsonc "mcpServers": { "dbmole": { "command": "npx", "args": ["-y", "dbmole-mcp"] }, "esmole": { "command": "npx", "args": ["-y", "esmole-mcp"] } } ``` ## Docker The image is built from the repository root (it needs the workspace so `@dbmole/core` resolves): ```bash docker build -f packages/esmole-mcp/Dockerfile -t esmole-mcp . ``` Run it from your client, persisting the store in a named volume: ```jsonc { "mcpServers": { "esmole": { "command": "docker", "args": [ "run", "-i", "--rm", "-v", "esmole-store:/home/node/.config/esmole", "esmole-mcp" ] } } } ``` The image runs as the non-root `node` user, so the store lives at `/home/node/.config/esmole` — mount the volume there (as above). ## Development ```bash npm run test:unit # fast unit tests (mocked IO) npm run test:int # integration tests (needs Docker) npm run coverage # unit tests with coverage npm run lint # Biome check npm run build # bundle to dist/ ``` See [AGENTS.md](../../AGENTS.md) for the contributor style contract: code style, TypeScript discipline, testing rules, and commit conventions.