Files
dbmole-mcp/packages/esmole-mcp
admin 9bbd179a40 fix(esmole): publish typed add_connection inputSchema
add_connection advertised an empty `{ properties: {} }` while the server
validated strictly, so clients could not see field types or the required
`type` literal and sent port/verifyTls as strings.

Cause: the connection schema was wrapped in `.superRefine`, producing a
ZodEffects with no `.shape`. The MCP SDK's normalizeObjectSchema returns
undefined for it and falls back to EMPTY_OBJECT_JSON_SCHEMA. update_connection
passed a raw shape, so it serialized fine - hence the asymmetry.

Split the schema: a strict z.object (connectionObjectSchema) is advertised as
the wire schema, and the refined connectionConfigSchema is passed as a new
optional parseSchema applied in the handler. Strict unknown-key rejection and
the apiKey-or-user+password check are both preserved. fullSchema is now typed
as a ZodObject so a ZodEffects can no longer be passed by mistake.
2026-06-16 23:15:08 +05:00
..
2026-06-16 21:38:41 +05:00
2026-06-16 21:38:41 +05:00

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, 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:

{
    "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:

{
    "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:

"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):

docker build -f packages/esmole-mcp/Dockerfile -t esmole-mcp .

Run it from your client, persisting the store in a named volume:

{
    "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

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 for the contributor style contract: code style, TypeScript discipline, testing rules, and commit conventions.