Skip to content

Releases

v0.4.1

Choose a tag to compare

@github-actions github-actions released this 03 Sep 09:55

A conformance release. No behavioural change to any tool.

All 34 tools declare the four behaviour hints as explicit booleans

The 25 read-only tools declared readOnlyHint, idempotentHint and openWorldHint but omitted destructiveHint. That is valid under the MCP specification — the hint only carries meaning once readOnlyHint is false — but tool directories do not read it that way: OpenAI's rejects a tool where any of the four is missing or non-boolean, and an external audit marked 25 of these 34 tools down for it.

destructiveHint: false states what a client already inferred from readOnlyHint: true, so nothing a caller sees changes except the annotation itself. A new test in test/server.test.ts asserts the four hints across every tool set at once, so the omission cannot come back unnoticed.

Also in this release

CI moved to the node24 action majors. The runner had been forcing the node20 actions onto node24 and warning on every run; that forcing ends with the deprecation window, so the bump is the fix rather than a way to silence the warning. checkout v4→v7, setup-node v4→v7, pnpm/action-setup v4→v6, and the five docker actions to their current majors. Two breaking changes were checked and neither bites: setup-node v7 drops the dummy NODE_AUTH_TOKEN export, and the publish step sets the real value itself; setup-node v5 caches automatically when packageManager is present, which only duplicates the cache: pnpm already spelled out. pnpm/action-setup v6 is a correction rather than a bump — it is the first release that knows pnpm 11, which packageManager pins to 11.22.0.

The Dockerfile secret check now reads values instead of names. SecretsUsedInArgOrEnv judges a variable by its name, so it flagged five declarations on every build: ES_API_KEY, ES_PASSWORD, ES_OAUTH_CLIENT_SECRET, ES_OAUTH_TOKEN_URL and ES_OAUTH_AUTH_STYLE. Nothing is baked into the image — all five are declared empty, and the loader treats an empty value as unset throughout. They are declared rather than merely documented so docker inspect and --env-file show an operator the surface the image accepts, so the fix is to correct the check rather than delete the declarations. A repository-wide skip would also hide a real secret typed in later, so CI gains a stricter rule in its place: any credential-shaped ENV or ARG whose value is not exactly ="" fails the build. Verified by mutation — the current Dockerfile passes, and baking a key into ES_API_KEY is caught.

README: the fork's Elasticsearch 7.x targeting is now called out up front, plus header formatting and a live-monitoring badge.

Full Changelog: v0.4.0...v0.4.1

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 26 Aug 10:25

Two additions, both behind flags. The default surface is unchanged at 17 tools.

get_node_stats — diagnostic set (ES_ADMIN_TOOLS)

list_nodes reports heap, CPU and disk from _cat/nodes, which leaves three things invisible: whether the JVM is spending its time collecting garbage, whether work is queueing or being rejected, and whether a circuit breaker has fired. A non-zero rejected on the write pool is where an ingest silently loses documents, and nothing in this server could show it.

  • One tool, one call — _nodes/stats/jvm,thread_pool,breaker. The metric filter is what keeps it cheap: a bare _nodes/stats also returns indices, which is most of the payload and answers none of these questions.
  • Uptime is printed on every node's line, because every counter here is cumulative since the node started. Without it, 128 rejections reads as "the cluster is rejecting writes" to a calling model rather than "it did, at some point in the last six weeks" — a different answer, and the wrong one.
  • The filtering is asymmetric on purpose. Nodes are never dropped: they number in the tens, and a node missing from the answer reads as a node that is fine. Idle thread pools and breakers are dropped, since around twenty pools per node would swamp the result. Pools are selected by activity rather than by a whitelist of names, so a saturated force_merge or snapshot pool stays visible.
  • No indexing_pressure: it arrived in 7.9 and the target cluster is 7.8.
  • Cost: 746 bytes of tools/list, and only when ES_ADMIN_TOOLS is on. The diagnostic set now holds eight read-only tools.

trace_request and an environment filter — ECS set (ES_ECS_TOOLS)

Two gaps found by running 0.3.0 against a 7.8 cluster hosting three environments and 202 daily indices.

The environment filter. service.environment was already named in fields.ts but nothing filtered on it, so a question about a service was answered with the sum of all environments, and nothing in the response said it was a sum. Measured on that cluster: a service reporting 390 errors over 24 h was 210 in integration, 179 in staging and 1 in QA. env joins the shared filter block, so all five tools take it.

trace_request. The other four tools aggregate across requests, so none can attribute a failure to a call two services down — which is exactly the question asked once the error is in hand. The new tool takes a correlation id and returns the chain of services that handled the request, its failed events and its timeline, oldest first.

  • It matches trace.id or http.request.id, and the pair is the point: a stack without APM emits no trace.id. On the cluster above, over 24 h, http.request.id was set on 3 210 868 documents, trace.id on 9 205 and span.id on none — so trusting trace.id alone answers "no trace" for 99.7 % of requests. requestId joins the shared filters too, so the other tools can restrict on it.
  • The chain and the failed events come from aggregations, not from the returned hits. Derived from hits they are bounded by limit, and on a real 385-event trace the single ERROR sat past line 200: the tool rendered a chain without the cause of the failure, while its own description promised otherwise.
  • Failure levels come from minLevelTerms("error") rather than a hardcoded list, so SEVERE and crit count as ERROR.
  • The id goes in should with minimum_should_match: 1. Without it the clause only weighed on the score, and the tool returned the whole window.
  • Defaults differ on purpose: limit is 200 against 20 elsewhere, and since is 24h against 15m. You arrive at this tool with an id taken from an error that may be hours old, and half a chain is not an answer.
  • Cost: the ECS tools/list goes from 21 553 to 25 416 bytes, +3 863 — 2 654 for the tool and 1 209 for the two filters joining a block that five tools each describe.

Proven end to end over MCP against the real 7.8 cluster: a 385-event trace across 5 services in 103 ms.

Full Changelog: v0.3.0...v0.4.0

v0.3.0 — ECS log search

Choose a tag to compare

@github-actions github-actions released this 25 Aug 00:24

Four tools for clusters whose application logs are in
ECS, plus two
that answer questions the existing tools could not.

Upgrading from 0.2.0 adds two tools to the default set. The ECS tools stay off
until you set ES_ECS_TOOLS.

The ECS log tools

The 26 existing tools talk to a cluster. These talk to a schema: because the
fields are known in advance, they take named parameters instead of a query DSL,
answer in log lines instead of JSON documents, and request only the fields they
print. That last part is not tidiness — on five hundred events, asking for the
whole _source is the difference between an answer and an exhausted context.

Tool The question
search_logs What happened — newest first, one line each
log_histogram When it started, peaked, and whether it stopped
error_summary What is broken, grouped and counted
top_values Who, where, and what this cluster actually indexes

Filters are named and exact: service, levels / minLevel, host, logger,
dataset, traceId, and free text over message and error.message. The
window is a bare duration — 15m, 2h, 7d — and date math still works where
you need it.

{
  "mcpServers": {
    "elasticsearch": {
      "command": "npx",
      "args": ["-y", "@agrica/elasticsearch7-mcp"],
      "env": {
        "ES_HOST": "https://es.internal:9200",
        "ES_ECS_TOOLS": "true",
        "ES_ECS_INDEX_PATTERN": "logs-app-*"
      }
    }
  }
}

ES_ECS_INDEX_PATTERN is required and has no default: with the flag on and
the pattern missing, the server refuses to start. A guess like logs-* is not a
default — it would sweep whichever indices happen to match on your cluster and
answer confidently from the wrong data.

Why 7.8 decides how errors are grouped

match_only_text arrived in Elasticsearch 7.14 and wildcard in 7.9, so a
mapping pushed to a 7.8 cluster necessarily predates ECS 1.12 — the revision
where error.message and error.stack_trace moved to those types. The tools are
built for what 7.8 can actually express, and two consequences are visible in the
answers:

  • error.message is text with no keyword sub-field, so errors cannot be
    grouped by message. error_summary groups by error.type, and gives events
    carrying no error.type their own reported bucket — a vendor who logs
    failures with only log.level and message would otherwise make the tool
    answer "no errors" about a cluster full of them.
  • error.stack_trace is never aggregated, although ECS 1.x types it as
    keyword and it looks like the better fingerprint. ECS sets
    ignore_above: 1024 on keywords, so a longer trace is not indexed and would
    vanish from the count — a wrong number that looks right.

Two tools for questions that had none

  • field_caps — which fields exist across an index pattern and whether
    each is searchable and aggregatable. get_mappings needs a concrete index,
    which is the wrong shape over a year of daily indices. It is also the only way
    to see a field mapped as two different types across indices, which makes an
    aggregation over it silently partial rather than failing.
  • analyze — the terms a text is broken into, which is what a query must
    produce to match. The answer to "my search returns nothing and I cannot see
    why".

Both are in the default set, beside get_mappings: splitting the same question
across two gates would be incoherent. The default tool list goes from 15 to 17.

What it costs, measured

tools/list is 13 186 bytes for the default 17 tools and 21 553 with the
ECS set enabled. So the flag buys back 8.4 KB of context per session for any
deployment whose logs are not in ECS — which is the whole reason the set is
gated rather than always on. Most of that is structural, ten shared filters
described once per tool; trimming the repeated prose reclaimed 724 bytes of it.

Two things measurement changed

  • log_histogram groups its rows fifty to a fragment. One fragment per bucket
    followed the project's own rule about chunked detail, but the bucket count is
    unbounded — a 1-minute interval over 30 days is 43 200 — and every fragment
    carries its own envelope, so the result outgrew the byte budget by the count of
    its own fragments.
  • top_values detects the "fielddata is disabled" refusal on the error body,
    not its message: ResponseError.message carries only the error type unless
    root_cause is an array. It then names the fix — <field>.keyword, or the ECS
    keyword fields — rather than forwarding a message that says what Elasticsearch
    will not do without saying what to do instead.

Also in this release

320 tests, up from 225. The design record, including the ECS revisions the field
types were checked against:
docs/superpowers/specs/2026-08-25-ecs-log-search-design.md.
The last French strings in scripts/smoke.mjs are translated, and the script now
exercises all six new tools.

The image is published for linux/amd64 and linux/arm64 as
ghcr.io/agrica/elasticsearch7-mcp:0.3.0.

v0.2.0 — OAuth2 client_credentials

Choose a tag to compare

@github-actions github-actions released this 24 Aug 21:58

A third authentication factor beside ES_API_KEY and ES_USERNAME/ES_PASSWORD:
an OAuth2 client_credentials token from an external identity provider, renewed
before it expires and sent as Authorization: Bearer … on every request.

Upgrading from 0.1.0 changes nothing unless you set the new variables.

What it is for

An Elasticsearch 7.x cluster cannot validate a third-party OAuth2 token
itself: the JWT realm arrived in 8.2, and the 7.x OIDC realm needs a platinum
licence and a browser flow that a stdio server has no way to run. So this targets
the deployment where a gateway in front of the cluster validates the token and
forwards the request. Point ES_HOST at the gateway.

This is also, by its own terms, what the MCP specification asks of a server in
this position — the authorization spec says stdio implementations SHOULD NOT
follow it and should read credentials from the environment, and separately
provides for a server acting as an OAuth client to an upstream API with a token
of its own.

Configuration

Variable Role
ES_OAUTH_TOKEN_URL Token endpoint. Setting it turns the factor on. Must be https://, except on loopback
ES_OAUTH_CLIENT_ID Client id
ES_OAUTH_CLIENT_SECRET Client secret
ES_OAUTH_CLIENT_SECRET_FILE Or a file holding it, for a mounted Docker secret
ES_OAUTH_SCOPE Scope, if the provider needs one
ES_OAUTH_AUDIENCE Audience — Auth0 needs it to issue a JWT
ES_OAUTH_AUTH_STYLE post (default) or basic client authentication

Keep the secret out of a project .mcp.json, which is version-controlled:

{
  "mcpServers": {
    "elasticsearch": {
      "command": "npx",
      "args": ["-y", "@agrica/elasticsearch7-mcp"],
      "env": {
        "ES_HOST": "https://es-gateway.internal",
        "ES_OAUTH_TOKEN_URL": "https://idp.internal/realms/data/protocol/openid-connect/token",
        "ES_OAUTH_CLIENT_ID": "mcp-elasticsearch",
        "ES_OAUTH_CLIENT_SECRET": "${ES_OAUTH_CLIENT_SECRET}",
        "ES_OAUTH_SCOPE": "es:read"
      }
    }
  }
}

Five things it refuses to do

Each is a decision with a test that fails if the decision is removed.

  • When OAuth2 wins, the base client carries no credential at all. If the
    bearer path breaks, the request fails with a 401 rather than quietly
    succeeding as whatever identity ES_API_KEY names. A silently substituted
    identity is worse than an outage.
  • A half-configured block refuses to start. The block is built as soon as
    any ES_OAUTH_* variable is set, so a forgotten one is a startup error, not
    a silent fallback to another factor.
  • ES_OAUTH_TOKEN_URL must be HTTPS, loopback excepted, with no override
    flag. What is at stake is the client secret crossing the network in the clear.
  • The secret is trimmed from both sources. A secret pasted into a config
    arrives with a newline, and a file written with echo always has one —
    untrimmed it yields invalid_client, an error that says nothing about its
    cause.
  • Neither the token nor the secret reaches a log line or a tool result. A
    token-endpoint failure reports only the fields RFC 6749 defines: an identity
    provider that echoes the request back in its error would otherwise carry the
    secret into the calling model's context.

Diagnostics instead of a retry

A rejected token is not retried, deliberately: with renewal happening a minute
before expiry, a 401 means a revoked token, clock skew, or the wrong scope — and
a retry fixes none of those. What happens instead is that the WWW-Authenticate
header is read, and its error and scope are named in the tool's error. So an
insufficient scope reads as

Error: … The target answered 403 with error="insufficient_scope",
and asks for scope "es:write" — ES_OAUTH_SCOPE has to include it.

Startup also says on stderr which factor is in force and which were ignored,
because a silent precedence is how the wrong identity ends up in production.

Under the hood

Token rotation costs no network round trip. The bearer is attached with the 7.17
client's child(), which shares the parent's connection pool and copies the
product-check symbol — so a new token opens no socket and repeats no GET /.
Verified before being relied on, and asserted by a test.

Ask for the scope you can actually use: if the deployment runs without
ES_ALLOW_DESTRUCTIVE, a read-only scope is the right request — a write scope
buys nothing when no write tool is registered, and widens what the secret is
worth if it leaks.

Also in this release

The design record, audited against MCP 2025-06-18 and 2025-11-25, OAuth 2.1 and
RFC 9700, with the sources listed so the audit can be re-run:
docs/superpowers/specs/2026-08-24-oauth2-client-credentials-design.md.

225 tests. The image is published for linux/amd64 and linux/arm64 as
ghcr.io/agrica/elasticsearch7-mcp:0.2.0.

v0.1.0 — Elasticsearch 7.x MCP server

Choose a tag to compare

@github-actions github-actions released this 24 Aug 21:07

First release of this fork: an MCP server that exposes an Elasticsearch 7.x
cluster to an MCP client (Claude Code, Claude Desktop, Codex, Cursor) over stdio.

Renamed fork of @awesome-ai/elasticsearch-mcp, retargeted at 7.x. The client is
pinned to @elastic/elasticsearch@^7.17 on purpose: the 8.x client hardcodes a
product check requiring the x-elastic-product header and compatible-with=8
media types, neither of which a 7.x cluster sends or accepts, and neither of
which is reachable through ClientOptions. Against a 7.8 cluster the 8.x client
fails in a way that looks like an unrelated connection error.

Install

npm install -g @agrica/elasticsearch7-mcp --registry=https://npm.pkg.github.com

The package is public, but GitHub Packages requires authentication anyway:
without a ~/.npmrc carrying a read:packages token, npx answers 401. The
README spells out the two lines that need.

A multi-arch image is published alongside it, for linux/amd64 and
linux/arm64:

ghcr.io/agrica/elasticsearch7-mcp:0.1.0

Three tool sets, gated at registration

Set Flag Tools
Data always on 15 read/write: search, count, get_document, bulk, list_indices, get_mappings, create_index, create_mapping, reindex, the template tools, health and cluster info
Diagnostics ES_ADMIN_TOOLS 7 read-only: explain_allocation, list_shards, list_nodes, get_index_stats, get_index_settings, get_cluster_settings, list_tasks
Destructive ES_ALLOW_DESTRUCTIVE 4 irreversible: delete_index, delete_document, delete_by_query, delete_index_template

The gate is registration, not a runtime check: an unregistered tool is absent
from tools/list, so the model cannot call it and its schema costs nothing in
the caller's context. The diagnostic set is read-only by design, which is what
makes it safe to enable in production — an agent can explain an unhealthy index
without anyone opening a cluster console.

delete_by_query and the other destructive tools additionally refuse a
wildcard, a comma-separated list, * and _all even with the flag on: a
model that mistakes logs-* for one index gets a refusal and no request is sent.

What one call costs the caller's context

The design constraint this release is built around. Measured on a cluster shaped
like a year of daily indices — 365 indices, 2190 shards:

Call Before Now
list_shards 385 469 B (~96 000 tokens) 92 B, or 32 826 B with verbose
search, size: 500 127 716 B 32 886 B (size capped at 100, with the from to page with)
list_indices, 365 indices 54 405 B 32 606 B
get_mappings, 1000 fields ~80 000 B 32 803 B

Every result is capped at ES_MAX_RESULT_BYTES (32 KiB default). A trimmed
answer always says it was trimmed, says how much went, and says how to ask a
smaller question — silence is the real hazard, because a model handed a
shortened list with no notice concludes the entry it wanted does not exist.

Protocol conformance

Audited against the 2025-06-18 specification.

  • isError: true on every failure, guardrail refusals included — a refusal
    reported as a success would let a model conclude a delete had happened.
  • Every tool carries annotations and a title, via registerTool.
    idempotentHint is claimed only where repeating the identical call really
    leaves the same state.
  • notifications/cancelled aborts the in-flight Elasticsearch request, bound to
    the client rather than threaded through 26 tool signatures.
  • Four genuinely tabular tools — list_indices, list_shards,
    get_index_settings, get_mappings — return typed structuredContent
    alongside the text, assembled from whatever room the readable answer left.
  • Knowing deviation: no rate limiting, which the specification states as a
    MUST. This server is spawned by, and serves, one local client over stdio, so
    the client is the trust boundary and a limiter would throttle the operator
    rather than an attacker. Revisit if a non-stdio transport is ever added.

Configuration

ES_HOST (comma-separated for several nodes), ES_API_KEY or
ES_USERNAME/ES_PASSWORD, ES_CA_CERT, ES_REQUEST_TIMEOUT,
ES_MAX_RETRIES, ES_MAX_RESULT_BYTES, ES_INSTANCE_LABEL, ES_ADMIN_TOOLS,
ES_ALLOW_DESTRUCTIVE. A malformed numeric value is reported to stderr and
ignored rather than fatal — refusing to start over ES_MAX_RETRIES=three would
take the session down for a setting with a sane fallback.

ES_INSTANCE_LABEL is cosmetic and exists for one real problem: an
mcpServers block usually declares this server once per cluster, and without a
label a client shows two identically named servers. It becomes the server title
(Elasticsearch 7.x — production). Nothing depends on it — in particular not
the destructive gate, which is ES_ALLOW_DESTRUCTIVE and nothing else.

See README.md for the
per-client setup blocks.