186 lines
6.6 KiB
Markdown
186 lines
6.6 KiB
Markdown
# 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 <path>` 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.
|