Mirador MCP
OfficialREADME.md
# 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](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](docs/confidence.md).
## 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
```bash
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:
```bash
mirastack-mirador-mcp --help
```
### 3. Prove the build is healthy
```bash
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:
```bash
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**.
```bash
# (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:
```bash
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:
```bash
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.
```bash
# 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:
```bash
MIRADOR_METRICS__KIND=prometheus \
MIRADOR_METRICS__BASE_URL="$METRICS" \
mirastack-mirador-mcp serve
```
A config file version of the same thing:
```yaml
# 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
```
```bash
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):
```bash
# 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`):
```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:**
```bash
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:
```json
{
"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[].via`** — *how* 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.tier`** — `insufficient` → `indicative` → `probable` → `strong`.
**`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.
```python
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/exec`s 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 install`ed 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`:
```yaml
- 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
```bash
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](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](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](docs/operations.md)).
For more than one environment, run more instances.
## Documentation
- [docs/architecture.md](docs/architecture.md) — the L0–L5b layer pipeline, module by module
- [docs/anchors.md](docs/anchors.md) — anchor resolution and the five lexical channels
- [docs/confidence.md](docs/confidence.md) — the four-dimensional confidence vector
- [docs/mcp-tools.md](docs/mcp-tools.md) — the 15 tools with parameters
- [docs/fixtures.md](docs/fixtures.md) — the four committed synthetic fixtures
- [docs/operations.md](docs/operations.md) — `QueryBudget` tuning and scheduler intervals
- `developer/MIRADOR_CORE_ENGINEERING_SPEC.md` — the authoritative design document
## Development
```bash
make install-dev
make check # lint + typecheck + gates + tests + verify-done + determinism
```
See [CONTRIBUTING.md](CONTRIBUTING.md) for the non-negotiable engineering rules and
[SECURITY.md](SECURITY.md) for the security posture.
## License
Apache-2.0. See [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues