Skip to main content
Glama
crunchtools

mcp-syslog

by crunchtools
README.md
# mcp-syslog-crunchtools

MCP server for the logs collected by [`crunchtools/syslog`](https://github.com/crunchtools/syslog).

Built for RT #1460, to close a specific gap: Hermes gets paged by Nagios and can
restart a service, but it cannot read the service's logs — so every remediation
is a blind restart. This turns that into an informed one.

## Capabilities

| Tool | What it answers |
|---|---|
| `syslog_sources_tool` | What can I query? |
| `syslog_search_tool` | Show me ERRs from this service in the last 15 minutes |
| `syslog_grep_tool` | Where does this string appear across the whole fleet? |
| `syslog_tail_tool` | What are this service's most recent lines? |
| `syslog_context_tool` | What was *everything* saying around 03:14? |
| `syslog_stats_tool` | Which service is loudest, and which is actually unhealthy? |

## The triage loop

```
nagios_current_problems_tool          → what is broken
syslog_search_tool(source=…,          → why it broke
                   severity="ERR",
                   since="15m")
syslog_context_tool(timestamp=…)      → what else was happening at that moment
nagios_schedule_check_tool            → confirm the fix
```

`syslog_context_tool` without a `source` is the one that earns its keep. It spans
every source at once, which is how "the app died" gets connected to "the database
container OOMed four seconds earlier".

## Design notes

**Every result is bounded, and says when it is.** Logs are unbounded and this
output lands in a model's context window. Each tool caps its results, caps how
many lines it will scan, and annotates the answer when either limit is hit:

```
[!] Stopped after the 2,000,000-line scan limit, so this result is INCOMPLETE
    and an empty or short result does not mean nothing happened.
```

That annotation is load-bearing. A caller that cannot distinguish "no errors
occurred" from "I stopped looking" will draw the wrong conclusion from an empty
result — and this server exists to inform remediation decisions.

**Time filtering is cheap.** The collector puts the date in the filename, so a
ten-minute query opens one file rather than reading ninety days of history.

**Source names are untrusted.** They come from a model and are used to build a
filesystem path. Each is resolved and then confirmed to still be inside the log
root, which catches traversal, absolute paths, and symlinks pointing out of the
tree — see `tests/test_security.py`.

**Severity is "at least this severe".** `severity="ERR"` returns ERR, CRIT, ALERT
and EMERG. An unrecognised severity is kept rather than dropped, on the grounds
that hiding a line you do not understand is worse than showing it — but it is
*not* counted as an error in `syslog_stats_tool`, or a healthy service would
report a 57% error rate.

**Severity is not badness.** Podman records anything a container writes to stderr
at priority `err`, and plenty of services log routine INFO there. One chatty
`httpx`-based service in this fleet sits around 65% "ERR" while being entirely
healthy:

```
PRIORITY=3 | 2026-08-23 15:55:24 INFO  httpx: HTTP Request: GET https://... "200 OK"
```

The collector is reporting the journal faithfully; the journal is reporting the
file descriptor. Read the message, and prefer a *change* in error rate to its
absolute value. This caveat is in the server's MCP instructions too, so an agent
querying it is told the same thing.

**Two line formats are parsed.** The collector emitted five fields before
2026-08-23 and six after, and the old lines stay in retention for 90 days. Which
layout a line uses is decided by where a real severity sits, not by counting
fields.

## Log format

The collector writes six space-delimited fields:

```
2026-08-23T15:41:52+00:00 crunchtools.com crunchtools.com httpd ERR AH00169: caught SIGTERM
└─ timestamp ───────────┘ └─ host ──────┘ └─ source ────┘ └prog┘ └sev┘ └─ message ────────┘
```

`source` is the log stream — normally a container name. `program` is the process
inside it, which matters for systemd containers where `httpd`, `php-fpm` and
`mariadb` all file under one service name.

## Configuration

| Variable | Default | Purpose |
|---|---|---|
| `SYSLOG_LOG_ROOT` | `/logs` | Collector log root, mounted read-only |
| `SYSLOG_MAX_RESULTS` | `200` | Cap on entries returned per call |
| `SYSLOG_SCAN_LIMIT` | `2000000` | Cap on lines examined per call |

No credentials — the server reads files off a read-only bind mount.

## Running

```bash
podman run -d --name mcp-syslog \
  --network crunchtools \
  -p 127.0.0.1:8027:8027 \
  -v /path/to/syslog/data/logs:/logs:ro \
  quay.io/crunchtools/mcp-syslog:latest \
  --transport streamable-http --host 0.0.0.0 --port 8027
```

Mount `:ro`. This server never needs to write, and a read-only mount means a bug
here cannot destroy the forensic record it exists to protect.

## Development

```bash
uv sync
uv run ruff check src tests
uv run mypy src
uv run pytest -v
```

TDQS

A3.5/5.0

Scored across 6 tools

Disambiguation4/5

Most tools target distinct operations: listing sources, tailing, stats, and context retrieval are clearly separated. However, syslog_search_tool (pattern search) and syslog_grep_tool (regex search across/within sources) overlap substantially and an agent could easily pick the wrong one for a pattern-matching query.

Naming Consistency5/5

All six tools use the identical syslog_<action>_tool convention, making the pattern predictable and easy to scan. The redundant '_tool' suffix is uniform, so it does not hinder consistency.

Tool Count5/5

Six tools is well-scoped for a log-query server, each covering a distinct access pattern (discovery, search, grep, tail, aggregate, context). No filler or redundancy beyond the search/grep overlap.

Completeness4/5

The surface covers the core log-investigation lifecycle: discovering sources, filtering, tailing, aggregating, and contextual surrounding entries. Minor gaps exist, such as no explicit pagination/offset control or config/retention management, but agents can work around these.

Maintenance

ActivityActive
ResponsivenessUnresponsive