Skip to main content
Glama
snickery

loki-tail-mcp

by snickery
README.md
# loki-tail-mcp

An [MCP](https://modelcontextprotocol.io) server for
[Grafana Loki](https://grafana.com/oss/loki/), designed around how LLMs
actually query logs: compact output, hard row caps, and a
**fuzzy container-name rescue** that turns the classic
empty-result-because-wrong-name failure into an auto-corrected retry or
an actionable suggestion list. Built on the
[Python MCP SDK](https://github.com/modelcontextprotocol/python-sdk)
(FastMCP); runs as a local stdio server or a containerized Streamable
HTTP service with bearer auth.

## Tools

| Tool | Notes |
|---|---|
| `loki_tail_container` | "What is service X saying?" — the primary tool. Accepts approximate names: service vocabulary (`vpn`, `proxy`), bare names (`sonarr`), typos. On zero results it distinguishes *valid-but-quiet* from *unknown name*, auto-substitutes a unique fuzzy match (flagged in the output), or suggests candidates. |
| `loki_query_range` | Raw LogQL range query — multi-container correlation (`{container=~"a\|b"} \|= "..."`) and metric queries (`count_over_time(...)`). |
| `loki_query_instant` | Instant query at a point in time (metric queries). |
| `loki_list_containers` | Durable container names (ephemeral CI/batch names hidden). |
| `loki_list_labels` / `loki_list_label_values` | Raw label discovery. |
| `loki_patterns` | Log pattern mining — thousands of lines → ranked recurring templates with counts. Requires the server-side [pattern ingester](https://grafana.com/docs/loki/latest/operations/query-patterns/) (`pattern_ingester.enabled: true`). |
| `loki_log_volume` | Rank containers by log bytes over a window — "which service suddenly got noisy". |
| `loki_detected_fields` | Fields Loki can auto-extract from a stream (name/type/cardinality/parser) — discover `\| logfmt \| status>=500` opportunities before writing LogQL. |

### The name-resolution design

Matching always runs against **live label values**, never a hardcoded
list, so it survives renames. Resolution tries, in order: exact match →
alias vocabulary → substring both ways → typo distance (difflib). A
unique candidate is tailed automatically and flagged; multiple candidates
become a ranked suggestion list. Auto-generated container names
(docker/podman `adjective_noun`, hex-suffixed batch workers) are filtered
out of discovery and suggestions but stay queryable via raw LogQL.

The built-in alias vocabulary covers the common self-hosted stack
(`vpn`→gluetun, `proxy`→traefik, `movies`→radarr, …). Entries whose
targets don't exist in your fleet are inert; extend with your own via
`LOKI_ALIASES`.

## Quick start (stdio)

```jsonc
// e.g. Claude Desktop claude_desktop_config.json / Claude Code .mcp.json
{
  "mcpServers": {
    "loki": {
      "command": "uv",
      "args": ["run", "--project", "/path/to/loki-tail-mcp", "loki-tail-mcp", "--stdio"],
      "env": { "LOKI_URL": "http://your-loki-host:3100" }
    }
  }
}
```

stdio mode has no network surface and skips bearer auth — the client owns
the process.

## HTTP mode (container)

The bundled `Containerfile` builds a Streamable HTTP server at `/mcp`
(stateless — restarts never strand client sessions). HTTP mode **refuses
to start** without `MCP_BEARER_TOKEN`; clients authenticate with
`Authorization: Bearer <token>`.

```bash
podman build -t loki-tail-mcp .   # or: docker build -t loki-tail-mcp .
podman run -d --name loki-tail-mcp -p 8325:8325 \
  -e LOKI_URL=http://your-loki-host:3100 \
  -e MCP_BEARER_TOKEN=some-long-random-token \
  loki-tail-mcp
```

`loki_tail_mcp.healthcheck` does a full HTTP round-trip to `/mcp` (the 401
counts as alive); wire it to your container healthcheck. Terminate TLS at
a reverse proxy — the server itself speaks plain HTTP.

## Configuration

| Env var | Default | Purpose |
|---|---|---|
| `LOKI_URL` | `http://loki:3100` | Loki base URL. |
| `LOKI_TENANT_ID` | *(empty)* | Sent as `X-Scope-OrgID` for multi-tenant Loki. |
| `LOKI_BASIC_AUTH` | *(empty)* | `user:password` for a basic-auth-fronted Loki (reverse proxy, Grafana Cloud). |
| `LOKI_TIMEOUT` | `30` | Upstream request timeout (s). |
| `LOKI_DEFAULT_LIMIT` / `LOKI_MAX_LIMIT` | `100` / `1000` | Row caps — Loki will happily return millions of rows; an MCP client will happily feed them to an LLM. Neither is what you want. |
| `LOKI_ALIASES` | *(empty)* | Extra vocabulary merged over the built-ins: `term=fragment` or `term=frag\|frag2`, comma-separated (e.g. `cache=redis\|valkey,db=postgres`). |
| `LOKI_EPHEMERAL_PATTERNS` | *(built-ins)* | Comma-separated regexes marking names as ephemeral; replaces the defaults when set. |
| `PORT` | `8325` | HTTP listen port. |
| `MCP_BEARER_TOKEN` | *(empty)* | Required in HTTP mode; server refuses to start without it. Not used in `--stdio` mode. |

## Testing

```bash
# Full suite — mocked HTTP + pure resolution logic, no Loki needed
uv run --extra test pytest tests/ -v
```

## License

[MIT](LICENSE)

TDQS

A4.3/5.0

Scored across 9 tools

Disambiguation5/5

Each tool has a clearly distinct purpose. The two query tools (instant vs range) are explicitly differentiated in their descriptions, and the discovery/analysis tools (labels, patterns, volume, fields, containers) do not overlap. Even tail_container is clearly a convenience wrapper for a specific use case.

Naming Consistency4/5

Six of nine tools follow a consistent loki_verb_noun pattern (query_instant, query_range, tail_container, list_labels, list_label_values, list_containers). Three tools (patterns, log_volume, detected_fields) use noun phrases, breaking the verb-first convention. This is a minor inconsistency that doesn't hinder usability.

Tool Count5/5

Nine tools is a well-scoped number for a Loki exploration server. Each tool serves a distinct purpose without redundancy, covering querying, metadata discovery, log analysis, and container navigation. The count is neither too small nor overwhelming.

Completeness5/5

The tool set covers the full range of Loki interactions: instant queries, range queries, metadata discovery (labels, values), container listing and tailing, pattern mining, volume ranking, and field detection for parsing. There are no obvious gaps for a read-only log exploration workflow.

Maintenance

ActivitySlowing
ResponsivenessNo issues