esmole
Elasticsearch 7.x/8.x with connections managed at runtime by the agent, SSH tunnels, readonly.
Que pouvez-vous faire avec esmole ?
add_connectiontakes the connection fields plusscope:"global"(default) oradd_connectionfails when the name already exists in the target layer. The same name inupdate_connectionpatches the connection where it lives (the winning layer). Anullremove_connectiondeletes it from that layer and tells you if that uncovers a same-name- Credentials are never echoed back:
list_connectionsshows scheme, host, port, the auth
Installation
Ajoutez ceci à la configuration MCP de votre client (Claude Desktop : claude_desktop_config.json ; Cursor : .cursor/mcp.json ; VS Code et les autres utilisent la même forme « mcpServers »).
esmole-mcpclaude mcp add esmole -- npx -y esmole-mcp@0.2.0
{
"mcpServers": {
"esmole": {
"command": "npx",
"args": [
"-y",
"esmole-mcp@0.2.0"
]
}
}
}
Documentation
Tirée du README du projet sur GitHub, actualisée chaque semaine. Les liens ouvrent GitHub.
Sommaire
esmole-mcp
Elasticsearch 7.x and 8.x for AI agents that manage their own connections: add, edit and remove clusters through MCP tools at runtime, per project, hot-reloaded, through SSH.
esmole-mcp is a stdio MCP server for Elasticsearch. Tell
your agent "connect to the logs cluster through bastion.example.com, readonly" and it calls
add_connection. The connection lands in a per-project .esmole.json or in your global store,
and both are re-read on every tool call, so a change made by the agent, by you in an editor,
or by another session applies on the next call. No client config edits, no restarts.
- Elasticsearch 7.x and 8.x, any number of named clusters, API key or basic auth.
- SSH tunnels with password, key or agent auth; host keys verified against
known_hostsor a pinned fingerprint. readonlyconnections limited to an allowlist of read endpoints, absolute request timeouts, a 10 MB response cap, and results capped at 40 KB so a big aggregation cannot flood the context.
Need PostgreSQL or MySQL? dbmole-mcp is the same design for SQL databases.
Demo
Install
Requires Node.js 22 or newer. The server needs no configuration to start; connections are added later.
Claude Code
claude mcp add esmole -- npx -y esmole-mcp
Add --scope user to enable it in every project. Or install it as a plugin (Claude Code
2.1.275 or later):
/plugin install esmole --marketplace ed-smartass/esmole-mcp
Codex
codex mcp add esmole -- npx -y esmole-mcp
or in ~/.codex/config.toml:
[mcp_servers.esmole]
command = "npx"
args = ["-y", "esmole-mcp"]
Cursor
VS Code
Claude Desktop
Download esmole-mcp-<version>.mcpb from the
latest release and open it, or add
the JSON below to claude_desktop_config.json. Claude Desktop does not start servers inside a
project, so keep connections in the global store (the default scope).
Any other MCP client
{
"mcpServers": {
"esmole": { "command": "npx", "args": ["-y", "esmole-mcp"] }
}
}
Docker
The image is built from source:
git clone https://github.com/ed-smartass/esmole-mcp.git && cd esmole-mcp
docker build -t esmole-mcp .
{
"mcpServers": {
"esmole": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-v", "esmole-store:/home/node/.config/esmole",
"-v", "/abs/path/to/project:/work",
"-v", "/home/me/.ssh/known_hosts:/home/node/.ssh/known_hosts:ro",
"esmole-mcp"
]
}
}
}
The container runs as node with cwd /work: mount a project there to use its
.esmole.json, and the named volume keeps the global store. Mount known_hosts for SSH
connections (or pin ssh.hostKey), and any private key you reference by privateKeyPath, at
the path the connection names; for ssh.agent: true, forward the agent socket
(-v "$SSH_AUTH_SOCK:/ssh-agent" -e SSH_AUTH_SOCK=/ssh-agent). To reach hosts that only your machine can reach, add
--add-host=host.docker.internal:host-gateway (Linux) and use host.docker.internal. The
image has no git, so the "not ignored by git" check always warns.
A 0.1 setup that still sets ESMOLE_CONNECTIONS, ESMOLE_CONFIG or --config starts fine:
the server prints one line on stderr saying they are ignored. See the
changelog.
Quick start
Ask the agent, for example:
Add a readonly elasticsearch connection
logsto this project: https on 10.0.0.20, API key VnVhQ2ZH…, through ssh as deploy@bastion.example.com with my ssh agent. Then show the five biggest indices.
It calls add_connection with:
{
"name": "logs",
"host": "10.0.0.20",
"apiKey": "VnVhQ2ZH…",
"readonly": true,
"ssh": { "host": "bastion.example.com", "user": "deploy", "agent": true },
"scope": "project"
}
then test_connection and es_list_indices. Credentials you type in chat pass through the
model's context; for production secrets, write the entry into the file yourself.
Connections
Two layers
| Layer | File | Shape |
|---|---|---|
project | .esmole.json, found from the server's working directory | { "connections": [ ... ] } |
global | ~/.config/esmole/connections.json, or the path in ESMOLE_STORE | [ ... ] |
- Same name in both: the project entry wins.
list_connectionsreports each connection'ssource(projectorglobal) and, when it hides an entry of the same name in the other layer,shadows. - Hot reload. Both files are re-read on every tool call. Edit a file mid-session and the next call uses the new settings; a changed connection gets a fresh client, the old one is closed.
- Project file discovery. Starting from the server's working directory (Claude Code and
Codex start it in the project directory), esmole walks up to the filesystem root and uses
the first
.esmole.jsonit finds. In a linked git worktree with no file of its own, it falls back to the main checkout's.esmole.json, so every worktree of a repo shares one file. - Both files are plain JSON with mode
0600, written atomically. Writes keep entries they cannot parse verbatim, and refuse to overwrite a file that is not valid JSON.
Managing connections with tools
add_connectiontakes the connection fields plusscope:"global"(default) or"project". Withprojectand no file yet, the file is created at the main checkout root (or the git root, or the working directory outside git). If git does not ignore it, the response warns you; add.esmole.jsonto.gitignoreso credentials are not committed.add_connectionfails when the name already exists in the target layer. The same name in the other layer is allowed; the response says which entry now shadows which.update_connectionpatches the connection where it lives (the winning layer). Anullvalue removes an optional field. Ansshobject is merged into the stored one field by field, so{ "ssh": { "hostKey": "SHA256:..." } }keeps the stored SSH secrets.remove_connectiondeletes it from that layer and tells you if that uncovers a same-name entry in the other layer.- Credentials are never echoed back:
list_connectionsshows scheme, host, port, the auth kind,verifyTls,readonly,sourceand whether SSH is used.
Broken and invalid entries
- An entry that fails validation does not silently disappear: its name resolves to an error
(
connection 'x' in <file> is invalid: ...) instead of falling through to a same-name entry in the global store, andlist_connectionslists it as invalid. A name defined twice in one file is an error too.remove_connectioncan delete an invalid entry. - A file that exists but is unreadable, not JSON, or the wrong shape is reported in
list_connections(brokenLayers). A broken project file blocks every connection; a broken global store blocks only names the project file does not define. A missing file is just empty.
Connection fields
Unknown fields are rejected. Authentication is required: an apiKey, or both user and
password.
| Field | Type | Notes | |
|---|---|---|---|
name | string | [a-zA-Z0-9_-]+ | |
type | elasticsearch | default elasticsearch | |
host | string | as seen from the SSH bastion when ssh is set | |
port | number | default 9200 | |
scheme | http \ | https | default https |
verifyTls | boolean | default true; false skips certificate verification | |
apiKey | string | API key (the base64 id:key form) | |
user, password | string | basic auth | |
readonly | boolean | default false, see Readonly | |
queryTimeoutMs | integer | milliseconds, default 60000, at most 2147483647; see Limits and timeouts | |
ssh | object | see SSH tunnels |
Example .esmole.json:
{
"connections": [
{ "name": "local", "host": "localhost", "scheme": "http", "user": "elastic", "password": "changeme" },
{
"name": "logs",
"host": "10.0.0.20",
"apiKey": "VnVhQ2ZHY0JDZGJrUW0t...",
"readonly": true,
"ssh": { "host": "bastion.example.com", "user": "deploy", "agent": true }
}
]
}
SSH tunnels
ssh field | Notes |
|---|---|
host, port | bastion; port defaults to 22 |
user | SSH user |
password | password auth |
privateKey / privateKeyPath | key contents, or a path (~ is expanded) |
passphrase | for an encrypted key |
agent | true to use the agent at SSH_AUTH_SOCK |
hostKey | pinned host key fingerprint, SHA256:... (OpenSSH format) |
{ "host": "bastion.example.com", "user": "deploy", "privateKeyPath": "~/.ssh/id_ed25519" }
{ "host": "bastion.example.com", "user": "deploy", "password": "…" }
{ "host": "bastion.example.com", "port": 2222, "user": "deploy", "agent": true }
{ "host": "bastion.example.com", "user": "deploy", "agent": true, "hostKey": "SHA256:EXAMPLEexampleEXAMPLEexampleEXAMPLEexample1" }
Host keys are verified. With hostKey set, the bastion's key must match it (trailing =
padding is accepted) and known_hosts is not consulted. Otherwise the key must match an entry
for the host in ~/.ssh/known_hosts (host on port 22, [host]:port otherwise):
- Plain and hashed host names match. Wildcard patterns never match, and a line with a negated
wildcard (
!*.example.com) is ignored. - A key listed under
@revokedis rejected for every host, whatever the line's host pattern.@cert-authoritylines and unknown markers are skipped.
There is no trust-on-first-use: an unknown key fails the connection with an error that shows
the presented fingerprint. Check it against a trusted source, then either add the host to
known_hosts or pin it:
ssh-keyscan -p 22 bastion.example.com 2>/dev/null | ssh-keygen -lf -
# 256 SHA256:EXAMPLEexampleEXAMPLEexampleEXAMPLEexample1 bastion.example.com (ED25519)
and ask the agent to update_connection with ssh.hostKey set to that SHA256:... value.
Tunnels send keepalives every 15 s and are rebuilt after 3 missed replies. Not supported:
ProxyJump and ~/.ssh/config, SSH certificates. With scheme: "https" through a tunnel,
the certificate is checked against host, not the tunnel's local address.
Readonly
readonly: true protects against accidental writes. Every tool, es_request included,
checks method and path against an allowlist before anything is sent:
GETandHEAD: allowed.POST: only_search,_msearch,_count,_field_capsand_pit(optionally under one index or pattern, e.g./logs-*/_search), and/_search/scroll.DELETE: only/_search/scrolland/_pit(closing read sessions).- Everything else is refused, as is any path containing
..,%2e,%2f,%5c,\or#, whatever the method. So on a readonly connectiones_requestcannot use date-math index names with/rounding, such as<logs-{now/d}>; writable connections are not affected. - Index and pattern arguments of the helper tools are validated too (no
/,\,?,#,%, whitespace,..); comma lists, wildcards,-exclusionsandcluster:indexwork. Date-math names need%-encoding, so they only work throughes_request. - Because every
GETpasses, GET-only maintenance endpoints such asGET /_flushpass too. They change no documents, but they are not reads.
It is not a security boundary: the agent can switch it off with update_connection, by
design. For a hard guarantee, use an Elasticsearch role or API key with read privileges only.
Tools
| Tool | What it does |
|---|---|
list_connections | connections with their source, shadowed names, invalid entries and broken files |
add_connection | add a connection to the project file or the global store |
update_connection | patch a connection where it lives; null removes a field |
remove_connection | remove a connection; reports a same-name entry it uncovers |
test_connection | connect end to end (including the tunnel) and report cluster version and latency |
es_request | any method, path, body and query params; readonly-guarded; a string body for NDJSON (_bulk, _msearch) |
es_search | query, aggs, size, source (sent as _source), sort, from, search_after, track_total_hits, highlight, script_fields on an index or pattern; a whole body nested into query is lifted, with a note |
es_list_indices | indices matching a pattern (default *) with health, doc count and store size |
es_get_mapping | mapping flattened to field: type; raw: true for the original |
es_cluster_health | cluster health |
es_search size defaults to 10 and is capped at 1000, also when it comes from a nested
body; with aggs and no query it is 0. Tool arguments are checked strictly: a wrong key is
rejected with a hint, e.g. unknown key '_source' — did you mean 'source'?.
A 4xx or 5xx from the cluster, es_request included, comes back as a tool error:
HTTP <status> <root_cause[0].type>: <reason> on the first line, then the trimmed response
body. test_connection fails on any non-2xx answer (e.g. 401 from a secured cluster).
The ten tool definitions cost about 2.1k tokens of context (tools/list is about 8,200
characters; measured with npm run smoke, at roughly 4 characters per token).
Limits and timeouts
- Every result is at most 40 KB of JSON. Strings longer than 2 KB are cut and marked
…[+N bytes]; if the body still does not fit, its longest array (hits, buckets) is halved until it does. The result says so intruncatedand stays valid JSON. Ask for less withsizeandsourceones_search, orfilter_pathones_request. - Responses larger than 10 MB are aborted with an error ("narrow it with size/_source"), never returned partially.
queryTimeoutMs(default 60 s) is an absolute deadline per request, from connecting to reading the last byte of the body.
How it compares
| esmole-mcp | elastic/mcp-server-elasticsearch | cr7258/elasticsearch-mcp-server | |
|---|---|---|---|
| Status | maintained | deprecated; Elastic points to the Agent Builder MCP endpoint (Elasticsearch 9.2+) | maintained |
| Elasticsearch 7.x | ✓ | — | ✓ |
| Elasticsearch 8.x | ✓ | ✓ | ✓ |
| OpenSearch | — | ✗ | ✓ |
| SSH tunnel built in | ✓ | ✗ | ✗ |
| Agent adds and edits connections through MCP tools | ✓ | ✗ | ✗ |
✓ documented, ✗ not offered, — not mentioned or not tested. Checked October 2026; corrections welcome.
Security
Connection files hold plaintext credentials (mode 0600). Keep .esmole.json out of git.
Cluster errors are passed to the client verbatim.
Whoever can change a connection, through update_connection or by editing a connection file,
can redirect its credentials. Changing host keeps the stored password or API key, and
changing ssh.host keeps the stored SSH password or key, so the next call sends them to the
new host. Treat write access to the connection tools and files like access to the credentials.
A project's .esmole.json is trusted as is: in a cloned or untrusted repository it can add
connections or shadow a global one of the same name and send your queries to another host.
Discovery walks up to /, so a file in a parent directory (such as ~) applies to every
project below it. Check list_connections before working in an unfamiliar repository.
Each open SSH tunnel listens on 127.0.0.1 at a random port without authentication: while it
is open, any local process, including other users on a shared host, can connect through it
to the target host. The cluster still asks for its own credentials.
See SECURITY.md for the threat model and how to report a vulnerability.
Development
npm ci
npm run lint # Biome
npm run typecheck # tsc
npm run test:unit # unit tests, mocked IO
npm run test:int # integration tests against Elasticsearch 7.17 and 8.x (needs Docker)
npm run smoke # build, npm pack, install the tarball in a temp dir, boot, tools/list
The shared connection core is @dbmole/core,
developed in the dbmole-mcp repo and bundled into this package at build time. See
CONTRIBUTING.md.
License
Questions fréquentes
Qu'est-ce que esmole ?
Elasticsearch 7.x/8.x with connections managed at runtime by the agent, SSH tunnels, readonly.
Comment installer esmole ?
Il s'exécute sur votre machine. Dans Claude Code : claude mcp add esmole -- npx -y esmole-mcp@0.2.0. Pour les autres clients, collez la configuration JSON de cette page dans leurs réglages MCP.
Faut-il une clé API pour esmole ?
Son entrée au registre n'indique aucune clé ni secret obligatoire. Certaines fonctions peuvent tout de même demander un compte auprès du service auquel il se connecte.
esmole est-il open source ?
Oui. Son code est sur GitHub sous licence MIT.
Qui maintient esmole ?
Il est publié par ed-smartass. AI.tn le référence sans le maintenir ni le recommander.
Issu du registre MCP officiel. Être listé n'est pas une recommandation : vérifiez le code et les permissions d'un serveur avant de lui donner accès à vos données.