Skip to main content
Glama
mirastacklabs-ai

Mirador MCP

Official

MIRASTACK Mirador MCP Server

Mirador Core is a zero-vendor-knowledge observability analysis engine. Pointed at a Prometheus-compatible metrics backend and (optionally) a log backend belonging to a deployment it has never seen, it discovers the signal inventory, resolves the entity topology, accepts anchors that focus an analysis, correlates metrics and logs, runs root-cause analysis and capacity forecasting inside that scope, and reports every finding with a four-dimensional confidence vector plus an explicit statement of what input would improve it. It ships as an MCP server, an importable Python library, and a CLI, under Apache-2.0.

Zero vendor knowledge

Mirador contains no metric names, no label names, and no vendor vocabulary — the rule is mechanically enforced by scripts/check_no_vendor_vocabulary.sh, which rejects any metric-name-shaped string literal in src/mirador/**.

Everything vendor-specific is either discovered from the data or supplied at query time as an anchor:

  • Signals — metric families are grouped from the backend's own series index; type, unit, role, cadence, discriminator dimensions, and log fields/templates are inferred (src/mirador/catalog/), never read from a built-in table.

  • Entities — identifier keys are profiled from label cardinality and cross-family reuse, and edges come from FK/overlap/affix/conservation/component evidence with named guards for every rejection (src/mirador/entity/). A fixture designed to look joinable but that is not (tests/fixtures/negative) must yield zero edges.

  • Topology — user-supplied topology is a merge input (entity/usertopo.py) with conflicts surfaced, not a required configuration file.

  • Anchors — a user- or agent-supplied term is resolved across five lexical channels into the exact families, label keys, label values, and log fields an analysis may touch. See docs/anchors.md.

The practical consequence: a new backend needs no onboarding config. The practical cost: early findings are honestly labelled low-confidence rather than confidently wrong — see docs/confidence.md.

Related MCP server: Prometheus MCP Server

Getting started

This section assumes no prior knowledge of the project. Every command below is copy-pasteable.

1. Prerequisites

You need

Why

Check it

Python 3.11 or newer

requires-python = ">=3.11"

python3 --version

git

to clone the repo

git --version

~2 GB free disk

virtualenv + the committed test fixtures

(optional) Docker

only for the container deployment in §6

docker --version

You do not need Prometheus, Kubernetes, or any monitoring system to build, test, and try the engine — four synthetic estates are committed under tests/fixtures/.

2. Build it

git clone https://github.com/mirastacklabs-ai/mirastack-mirador-mcp
cd mirastack-mirador-mcp

python3 -m venv .venv          # create an isolated Python environment
source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install -e ".[dev]"        # editable install + dev tools (pytest, ruff, mypy)

-e means editable: your source edits take effect without reinstalling.

Confirm the console script is on your PATH:

mirastack-mirador-mcp --help

3. Prove the build is healthy

make check

That runs, in order: ruff (lint) → mypy (types) → the 9 gate scripts → the test suite → a determinism check that runs the engine twice and diffs the bytes. Expect every test to pass, with 8 skipped (the skips are live-backend tests that need env vars from §5).

Individual pieces, if you prefer:

make lint          # ruff
make typecheck     # mypy
make gates         # the 9 scripts/check_*.sh invariant guards
make test          # pytest
make determinism   # byte-identical output across two runs

4. First run, with no backend at all

Mirador stores everything it learns in one embedded DuckDB file. Two steps: build a catalog, then ask a question.

# (a) Learn the estate from a committed synthetic fixture.
mirastack-mirador-mcp \
  --fixture-dir tests/fixtures/vmware_pure \
  --duckdb /tmp/mirador.duckdb \
  --json catalog build

# (b) Confirm it is ready.
mirastack-mirador-mcp \
  --fixture-dir tests/fixtures/vmware_pure \
  --duckdb /tmp/mirador.duckdb \
  --json catalog state

catalog build is synchronous — it returns only when the sweep has finished, so it is safe to script. catalog state should report ready.

vmware_pure is a VMware + Pure Storage estate (a classic datacenter shape, no Kubernetes). From it Mirador discovers ~16 metric families, ~780 entities of types vm, host, volume, disk, backing_serial, and the cross-source joins that link compute to storage — without being told a single metric or label name.

Now ask it something:

BASE="--fixture-dir tests/fixtures/vmware_pure --duckdb /tmp/mirador.duckdb --json"

# --lookback-s sizes the analysis window. 900 = 15 minutes, the usual shape of an
# outage investigation. Without it the tools search a full day, which is far slower.
mirastack-mirador-mcp $BASE --lookback-s 900 detect vm      # ranked anomalies
mirastack-mirador-mcp $BASE --lookback-s 900 rca vm         # causal chains + evidence
mirastack-mirador-mcp $BASE --lookback-s 900 correlate vm   # signal pairs with q-values
mirastack-mirador-mcp $BASE topology get                    # discovered entity graph
mirastack-mirador-mcp $BASE capacity vm                     # exhaustion ETA distributions

capacity deliberately takes no --lookback-s: a forecast needs long history, so an outage-sized window would be meaningless for it.

Pick the window to match the question. 15 minutes for an active incident, an hour for a slow degradation, a day for a trend. The cost difference is large — per-series analysis over a day of fine-cadence data can take minutes, where 15 minutes of it takes seconds.

The word vm is an anchor — a plain-language hint about what you care about. You are not naming a metric; Mirador resolves the term against the catalog it discovered. Anchors also accept explicit globs when you want precision:

mirastack-mirador-mcp $BASE detect '{"term": "memory", "metrics": ["arr_volume_*"]}'

5. Point it at a real backend

Any Prometheus-compatible endpoint works (Prometheus, VictoriaMetrics, Thanos, Mimir). Logs are optional.

# VictoriaMetrics cluster (note the /select/0/prometheus path prefix)
METRICS="http://localhost:8481/select/0/prometheus"

mirastack-mirador-mcp \
  --metrics-url "$METRICS" \
  --logs-url http://localhost:9471 --logs-kind vlogs \
  --duckdb ~/mirador-prod.duckdb \
  --json catalog build

--logs-kind accepts vlogs, loki, file, or none.

Expect the first build to take minutes, not seconds — it profiles every metric family in the estate. On a ~700-family estate it takes about 10 minutes. Re-runs are incremental.

Global flags worth knowing:

Flag

Meaning

--metrics-url URL

Prometheus-compatible query endpoint (required unless --fixture-dir)

--fixture-dir DIR

use committed fixture data instead of a live backend

--logs-url / --logs-kind

optional log backend

--duckdb PATH

where the learned catalog lives (default ~/.mirador/mirador.duckdb)

--config FILE

YAML config; precedence is CLI > environment > file > default

--json

machine-readable output (use this when scripting)

--max-points-per-run N

raise the query budget for very large estates

--lookback-s N

analysis window in seconds (900 = 15 min). Default is a full day

--fail-on-insufficient

exit non-zero when confidence is insufficient (good for CI)

Environment variables use the MIRADOR_ prefix with __ for nesting, e.g. MIRADOR_BUDGET__MAX_POINTS_PER_RUN=50000000. Note the double underscore: it is the nesting delimiter, so MIRADOR_METRICS__KIND configures the backend and MIRADOR_METRICS_KIND does nothing. Flags are optional — the environment alone is a complete configuration, which is how MCP clients and orchestrators (which pass no arguments) run this server:

MIRADOR_METRICS__KIND=prometheus \
MIRADOR_METRICS__BASE_URL="$METRICS" \
mirastack-mirador-mcp serve

A config file version of the same thing:

# mirador.yaml
metrics:
  kind: prometheus
  base_url: http://localhost:8481/select/0/prometheus
logs:
  kind: vlogs
  base_url: http://localhost:9471
duckdb_path: /var/lib/mirador/mirador.duckdb
budget:
  max_points_per_run: 20000000
  max_concurrent: 8
mirastack-mirador-mcp --config mirador.yaml --json catalog build

6. Deploy it

As an MCP server (this is the primary mode — an AI agent calls the tools):

# stdio: the transport MCP clients such as Claude Desktop expect (the default)
mirastack-mirador-mcp --metrics-url "$METRICS" --duckdb ~/mirador.duckdb serve

# streamable HTTP, for remote clients and for a governing proxy
mirastack-mirador-mcp --metrics-url "$METRICS" serve \
  --transport streamable-http --listen 127.0.0.1:9001

serve flags: --transport {stdio,streamable-http,sse} (--http is shorthand for streamable-http), --listen HOST:PORT (or --host/--port), --path (default /mcp), and --json-response / --stateless (both on by default; --no-json-response restores SSE framing and --no-stateless restores server-side sessions). Each has an environment equivalent — MIRADOR_SERVER__TRANSPORT, MIRADOR_SERVER__LISTEN, and so on — plus the cross-server aliases MCP_TRANSPORT, MCP_LISTEN_ADDR / MCP_HTTP_LISTEN, MCP_HTTP_HOST, MCP_HTTP_PORT, MCP_HTTP_PATH, so one operator template configures this server and its siblings identically.

In serve mode a scheduler keeps the catalog fresh: one sweep immediately at startup, then incremental sweeps every 15 minutes and a full sweep daily.

Traces (optional). Point Mirador at a VictoriaTraces instance with --traces-url http://vt:10428 (or MIRADOR_TRACES__KIND=vtraces + MIRADOR_TRACES__BASE_URL=...; auth/TLS mirror the metrics/logs blocks as MIRADOR_TRACES__AUTH__* / MIRADOR_TRACES__TLS_VERIFY). Mirador queries the Jaeger-compatible API (/select/jaeger/api/...) and derives three per-service signals — span rate, error rate, and a span-duration percentile — that participate in correlate, rca, and detect_anomalies alongside metrics and log-template signals. Trace series join the entity graph through the service label. An unreachable trace backend degrades honestly to metrics+logs; trace metadata is never persisted to DuckDB and never feeds anchor lexical search (scope boundary, documented here on purpose).

MCP client configuration (e.g. Claude Desktop's claude_desktop_config.json):

{
  "mcpServers": {
    "mirador": {
      "command": "/absolute/path/to/.venv/bin/mirastack-mirador-mcp",
      "args": ["--metrics-url", "http://localhost:8481/select/0/prometheus",
               "--duckdb", "/absolute/path/to/mirador.duckdb", "serve"]
    }
  }
}

In Docker:

docker build -t mirador-mcp .
docker run --rm -it \
  -e MIRADOR_DUCKDB_PATH=/home/mirador/mirador.duckdb \
  mirador-mcp --metrics-url "$METRICS" serve

The image runs as a non-root user (uid 10001) and excludes the test fixtures.

7. Reading the output

Every response is an envelope with the same shape, so one reading habit covers all tools:

{
  "resolved": { "anchors": [ { "term": "vm", "families": ["cmp_vm_info"],
                              "confidence": 0.9, "via": ["family_name"] } ] },
  "result":   { "chains": [], "windows": [] },
  "confidence": { "scope": 0.9, "structure": 1.0, "statistical": 0.4,
                  "data": 0.8, "tier": "indicative" },
  "missing_inputs": [ { "what": "longer window, more samples" } ],
  "next_actions": [ ]
}

Read it in this order:

  1. resolved.anchors[].viahow your term was understood. family_name (0.9) is a confident match; lexical_unconfirmed (0.55) means "this is my best evidence-backed guess, please confirm"; structural_fallback means the term matched nothing and the shape of the data was used instead.

  2. confidence.tierinsufficientindicativeprobablestrong. insufficient is a feature, not an error. Mirador refuses to sound certain when the evidence is thin.

  3. missing_inputs — the concrete thing that would raise the tier (a longer window, a confirmed anchor, retention metadata).

  4. result — the findings themselves.

An empty result with an honest low tier is a valid, deliberate answer.

8. When something looks wrong

Symptom

Cause and fix

no metrics backend configured: pass --fixture-dir/--metrics-url, use --config, or set MIRADOR_METRICS__KIND (and MIRADOR_METRICS__BASE_URL)

Give one of them, or use --config.

catalog_state: absent

Run catalog build first, or wait for serve's startup sweep.

Every result is insufficient

Usually genuine. Check missing_inputs; try confirm_anchor, or a longer window.

A command seems to hang

You are probably on the default one-day window. Add --lookback-s 900.

BudgetExhausted: point budget exhausted

Very large estate: raise --max-points-per-run.

ConnectError: All connection attempts failed

The metrics URL is unreachable (dead port-forward, wrong path prefix).

Anchor resolves to unrelated families

Use the explicit form: '{"term": "x", "metrics": ["prefix_*"]}'.

Results differ between two identical runs

Should be impossible — make determinism guards it. Please file a bug.

9. Using it as a Python library

The CLI and the MCP server are thin wrappers; the engine is importable.

import asyncio
from pathlib import Path

from mirador.catalog.build import build_catalog
from mirador.datasource.base import BudgetLedger, QueryBudget
from mirador.datasource.file import FileDataSource
from mirador.store.repo import Repo

budget = QueryBudget()
ds = FileDataSource(Path("tests/fixtures/vmware_pure"), budget)
repo = Repo(Path("mirador.duckdb"))

result = asyncio.run(
    build_catalog(ds, repo, now=1_730_000_000.0, ledger=BudgetLedger(budget), logs=ds)
)
print(len(result.families), "families,", f"coverage {result.coverage:.2%}")
for signal in result.signals[:5]:
    print(signal.signal_id, signal.mtype, signal.unit, signal.role)

Note the injected now: Mirador never reads the wall clock inside the analysis path, which is what makes runs bit-for-bit reproducible (scripts/check_no_wallclock.sh, scripts/check_determinism.sh). Regenerate fixtures with make fixtures.

Onboarding into MIRASTACK

MIRASTACK does not talk to this server directly. It runs the MCP sidecar (mira-mcp-wrapper) as the pod's only listener; the sidecar spawns this server on loopback and governs every tool call (RBAC, audit, serialization). Two things must line up: the Helm entry (plumbing) and the target env template (configuration). They are edited in different places on purpose.

Why the container ships a runtime tree

The sidecar runs on gcr.io/distroless/base-debian12 and fork/execs the child inside its own container, after an initContainer copies the server out of this image. That container has no Python and no shell. The pip console script mirastack-mirador-mcp cannot work there: it is a text file whose #!/usr/local/bin/python shebang and mirador package both stay behind, so exec fails with

start http child: fork/exec /shared/mcp-server: no such file or directory

which is misleading — the file is there; the missing thing is the shebang interpreter.

The sibling Go and Bun servers answer this with a single self-contained binary, but Python has no equivalent: PyInstaller and Nuitka cannot cross-compile, so building one would force them to run under emulation for a foreign architecture, which is not dependable. Instead this image ships a relocatable CPython at /opt/mirador-runtime with Mirador pip installed into it. The initContainer copies that tree to /shared/runtime and the wrapper runs it directly, so the chart uses serverDir + childCmd rather than serverBinary.

Nothing is compiled to achieve this, which is what keeps the image cleanly multi-architecture: the interpreter and every wheel (duckdb, numpy, scipy, pandas, statsmodels, ruptures) are downloads published for both x86_64 and aarch64. The tree also vendors the three libraries distroless does not ship — libz.so.1, libstdc++.so.6, libgcc_s.so.1 — so the shared sidecar image needs no modification for this server.

1. Helm entry (plumbing only)

In deployments/reference/kubernetes/helm/mirastack/values.yaml under mcpServers.servers:

- name: mirastack-mirador-mcp
  enabled: true
  serverImage: docker.io/mirastack/mirastack-mirador-mcp
  serverTag: "0.1.0"                                  # must match the tag you built/pushed; the package version is 0.1.0 (pyproject.toml)
  serverDir: /opt/mirador-runtime                     # the relocatable tree, not a single binary
  childCmd: /shared/runtime/bin/python3 -m mirador serve
  backend: http
  childHTTP: http://127.0.0.1:9003/mcp                # loopback; must be unique per pod
  endpoint: /mcp
  integrationID: ""
  resources: {}
  registrationTokenSecretKey: ""

childCmd carries arguments because the wrapper splits the value, so no launcher script is needed — which matters, since distroless has no shell.

The chart carries no target configuration — no endpoints, no credentials, no target-facing env var names. That is deliberate and enforced by comments in values.yaml.

2. Target env template (configuration)

Set these in the MIRASTACK UI at Admin → MIRA → MCP Ecosystem → Runtimes → mirastack-mirador-mcp → target env template, then bind an Integration to the runtime.

Variable

Value

Why it is required

MCP_TRANSPORT

streamable-http

Transport defaults to stdio, which never opens a listener. Must be exactly streamable-http — the value is validated against Literal["stdio", "streamable-http", "sse"], so http is rejected.

MCP_LISTEN_ADDR

${{runtime.child_listen}}

Resolves to 127.0.0.1:9003. The sidecar refuses to spawn the child unless some env value contains exactly the host:port it probes.

MIRADOR_METRICS__KIND

prometheus

Required field with no default. Note the double underscore — the nesting delimiter is __, so MIRADOR_METRICS_KIND configures nothing and is silently ignored.

MIRADOR_METRICS__BASE_URL

${{url}}

Comes from the bound Integration. kind: prometheus is rejected without it.

MIRADOR_DUCKDB_PATH

/tmp/mirador/mirador.duckdb

The default is ~/.mirador/…, and $HOME does not exist in the distroless sidecar. /tmp is present and 1777. The parent directory is created automatically.

Optional, only if you also want log-based analysis:

Variable

Value

MIRADOR_LOGS__KIND

vlogs (or loki)

MIRADOR_LOGS__BASE_URL

the logs endpoint

You do not need to set MCP_HTTP_JSON_RESPONSE, MCP_HTTP_STATELESS, or MCP_HTTP_PATH. JSON responses and stateless mode are already the defaults because the sidecar issues its own Mcp-Session-Id and relabels responses application/json; the default path is already /mcp.

3. Two vocabularies that look alike

The commonest onboarding mistake is mixing them up:

Setting

Owner

Value

backend in values.yaml

the wrapper

http

MCP_TRANSPORT in the env template

Mirador

streamable-http

Equally, VM_INSTANCE_ENTRYPOINT / VL_INSTANCE_ENTRYPOINT belong to the VictoriaMetrics and VictoriaLogs MCP servers. Mirador does not read them; it uses MIRADOR_METRICS__BASE_URL.

4. Verifying it worked

kubectl -n mirastack logs deploy/mirastack-mirador-mcp -c sidecar --tail=20

Expected: runtime registered with engine, then no child start failed.

Sidecar error

Meaning

fork/exec /shared/mcp-server: no such file or directory

The chart is using serverBinary for this server. It needs serverDir: /opt/mirador-runtime plus childCmd.

error while loading shared libraries: libz.so.1

An old image built before the runtime tree vendored libz/libstdc++/libgcc_s. Pull a current tag.

nothing tells the MCP server to listen on 127.0.0.1:<port>

MCP_LISTEN_ADDR missing, or not set to ${{runtime.child_listen}}.

no target env template declared

The runtime has no env template yet.

no integration bound

Bind an Integration to the runtime.

child starts, then exits complaining about a missing backend

MIRADOR_METRICS__KIND was written with one underscore.

child never becomes ready

MCP_TRANSPORT missing or set to http instead of streamable-http.

The 15 MCP tools

Full parameter documentation: docs/mcp-tools.md.

Tool

Tier

Purpose

rca

composite

Chains, per-step evidence, confidence. Self-sufficient.

capacity_forecast

composite

ETA distributions, steps/month, cross-layer divergence.

correlate

composite

Signal pairs with q-values, effect sizes, change-point order.

detect_anomalies

composite

Ranked anomalies with a baseline-quality flag.

get_catalog_state

inspection

absent | building | partial | ready | stale.

discover_signals

inspection

Catalog summary, coverage percentage, maturity.

get_signal_catalog

inspection

Families with inferred type/unit/role/cadence.

resolve_scope

inspection

Dry run — inspect the scope manifest before analysing.

get_topology

inspection

Entity subgraph with per-edge provenance.

explain

inspection

Narrative for a finding — tier-gated.

confirm_anchor

mutation

Persist a learned anchor alias.

set_topology

mutation

Merge user topology; conflicts surfaced.

get_job

utility

Job state plus partial output.

cancel_job

utility

Cancel a running job.

raw_query

utility

Escape hatch, budget-capped.

The four composite tools are self-sufficient: each performs scope resolution, window localization, detection, and analysis internally, so no prerequisite call is required.

Confidence tiers

Confidence is reported as four independent dimensions (scope, structure, statistical, data). rank() is their product and exists only to sort and to tier. Thresholds come from src/mirador/constants.py (TIER_SUPPORTED, TIER_INDICATED, TIER_WEAK):

Tier

rank() threshold

Behaviour it triggers

supported

>= 0.80 (TIER_SUPPORTED)

Names a root cause; a single chain is led; LLM narration is permitted.

indicated

>= 0.50 (TIER_INDICATED) and < 0.80

Leading hypothesis plus alternatives, always; template narration only.

weak

>= 0.30 (TIER_WEAK) and < 0.50

Ranked candidates only — no causal language anywhere in the output.

insufficient

< 0.30

Reports what is missing instead of a finding, with remediation steps.

A day-one deployment with little history will legitimately report weak. That is the system being honest, not a defect — the reasoning is spelled out in docs/confidence.md.

Scale envelope

Mirador is designed for a single container on 2–4 vCPU / 8 GB with embedded DuckDB and one environment per instance. It is comfortable with backends up to roughly 5 M active series (the catalog tracks families, on the order of thousands, never every series), up to about 100 k entities in the networkx graph, 200–2 000 signals per analysis, and up to about 5 000 capacity signals at 90 days / 1 h resolution. A warm RCA takes seconds to tens of seconds and a full background catalog sweep takes minutes. The bottleneck is backend I/O against the customer's Prometheus, not Mirador's arithmetic — which is exactly why QueryBudget is a first-class object (tuning guide: docs/operations.md). For more than one environment, run more instances.

Documentation

Development

make install-dev
make check   # lint + typecheck + gates + tests + verify-done + determinism

See CONTRIBUTING.md for the non-negotiable engineering rules and SECURITY.md for the security posture.

License

Apache-2.0. See LICENSE.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    A
    quality
    D
    maintenance
    Enables AI agents to query Prometheus metrics and Loki logs for intelligent alert investigation and troubleshooting. Provides service discovery, metric querying, log searching, and correlation tools to help identify root causes of issues.
    9
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants to query Prometheus metrics, monitor alerts, and analyze system health through read-only access to your Prometheus server with built-in query safety and optional AI-powered metric analysis.
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables natural language querying and analysis of OpenTelemetry traces, metrics, and logs stored in Elasticsearch/OpenSearch, allowing AI assistants to investigate performance issues, find root causes, and explore system behavior.
    21
    14
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A Model Context Protocol server that enables AI agents to interact directly with Prometheus metrics data through natural language queries.
    MIT