graylog-mcp
A read-only Model Context Protocol server for Graylog 4.x to 7.x. It lets an LLM investigate incidents on its own: search logs, follow one request across services, group errors with exact counts, and find when a problem started. Sensitive data is masked before any log line leaves the machine.
Everything site-specific (trace field names, redaction rules, timezone, application packages, error query) lives in configuration, not in code.
How it differs from other Graylog MCP servers
| Typical community server | graylog-mcp | |
|---|---|---|
| Graylog versions | one range only (4.x/5.0 or 5.2+) | detects the version, works on 4.x through 7.x |
| Statistics | raw messages, or counting over a sample | exact counts and groupings computed by Graylog |
| Safety | no masking | masks by field name, regex and Luhn; per-country packs |
| LLM context | dumps raw log lines | folds stack traces, groups repeated lines, hard size cap |
| Writes to Graylog | some create saved searches | never writes anything |
Quick start
One command (needs uv), run in the folder holding your .graylog-mcp.toml or
anywhere for a fresh start:
uvx --from git+https://github.com/ntbang0901/graylog-mcp graylog-mcp start
It installs graylog-mcp, runs it in the background as one process for every Claude session, starts it again when you log in, connects Claude Code in the repositories of your groups and opens the admin page. Its Get set up card shows what is left; Finish setup does every step that needs no input in one click and then opens the form for what does (usually: your Graylog URL and a token).
Afterwards:
graylog-mcp ui |
open the admin page again |
graylog-mcp status |
is it running, which version, is Claude Code connected |
graylog-mcp update |
install the latest version and restart (or "Check for updates" on the page) |
graylog-mcp stop |
stop it and no longer start it at login |
Prefer the terminal? graylog-mcp init is a guided setup that writes .graylog-mcp.toml and registers the
server per repository (one process per session). See Setup helpers.
Or by hand:
Two environment variables are enough:
export GRAYLOG_URL=https://graylog.example.com
export GRAYLOG_TOKEN=<access token of a read-only user>
uvx graylog-mcp --check # connects, prints the detected version and APIs, exits
Claude Code:
claude mcp add graylog --env GRAYLOG_URL=https://graylog.example.com --env GRAYLOG_TOKEN=... -- uvx graylog-mcp
Claude Desktop / any MCP client (mcpServers JSON):
{
"mcpServers": {
"graylog": {
"command": "uvx",
"args": ["graylog-mcp"],
"env": {
"GRAYLOG_URL": "https://graylog.example.com",
"GRAYLOG_TOKEN": "...",
"GRAYLOG_TIMEZONE": "Asia/Ho_Chi_Minh",
"GRAYLOG_REDACTION_PACKS": "vn"
}
}
}
}
The Graylog user
Create a dedicated user with the Reader role and read access to the streams the model may see,
then generate an access token for it. The server only sends GET requests, plus POST to the four
search endpoints that execute or check a query without saving it (/views/search/sync, /search/messages,
/search/aggregate, /search/validate). Any other method or path is refused inside the client before a request is built.
Tools
All tools are annotated readOnlyHint and accept an optional instance. Tools that do several related things
take a kind, view or what; an argument that does not apply to the one chosen is refused, not ignored.
Scan
scan— "is anything wrong?" in one call. Runs every scan rule concurrently (crashes, resource exhaustion, error spikes, new error types, HTTP 5xx, connectivity, database, auth failures, plus your own) with exact counts against a baseline, and returns only what fired, most severe first, with its query, top groups and a sample. Ad hoc rules (checks) cover a specific request in the same call. See Scan rules.
Root cause analysis
root_cause— "what broke first, and why?" in one call. Compares every service's errors, traffic and latency with a baseline window, pins each service's first error to the millisecond, detects deploys in the logs, infers the call graph from traces, and returns a ranked verdict with a timeline and evidence.view="changes"— only the deploys and restarts found in the logs themselves: a new value of a version field (app_version,build,commit, ...), a host rollout (new sources replacing old ones), start/stop lines. No CI/CD integration needed.view="service_map"— only which service calls which, inferred from sampled traces with no configuration: edges with traffic, error rate and p50/p95 latency.
Search
search_logs— Lucene query, relative (15m,2h) or absolute time (instance timezone or ISO 8601), streams by name or id, field selection, sort, paging; repeated lines grouped by default.get_message— one message with all fields, byindex/idref. Long values are cut and listed incut_fields; name them infieldsto read them in full. Withcontext_seconds: the messages within ±N seconds of it instead, limited to the same source, the same streams or everything (scope).
Investigate
aggregate— exact counts computed by Graylog:kind="count"— number of matching messages.kind="histogram"— counts over time with automatic interval, peak bucket and the onset of a spike.kind="top"— top N values of thegroup_byfield with exact counts.kind="errors"— groups errors by exception, logger, source or any field: exact count, first and last seen, one sample message per group.
trace_request— follows a correlation/request/trace id through the configuredtrace_fieldson all streams (falls back to full text); returns a cross-service timeline, per-service steps with durations and the first error.compare_periods— two periods (e.g. before/after a deploy): which error groups are new, grew, disappeared or shrank, normalised per hour.
Discover
describe—what="instances"(default: detected version and the API in use),"streams","fields"(so the model writes queries with real names),"presets"or"scan_rules"(query, condition, severity, tags).run_preset— runs a named query you define in the config. A preset'stoolkeeps the names of the underlying operations:scan,root_cause,detect_changes,service_map,search_logs,count_logs,error_summary,log_histogram,top_values,trace_requestorcompare_periods.
The server also sends the model instructions about Lucene syntax and a suggested investigation flow.
Scan rules
scan answers "is anything wrong?" or "scan X" with one call. Each rule is a Lucene query plus a condition, and
fires when:
| Condition | Fires when | Use it for |
|---|---|---|
threshold = N |
more than N matches in the window (0: any match) |
things that should never happen: crashes, OOM, data corruption |
growth = X, min_count = M |
the rate is X times the baseline's (or the baseline had none), with at least M matches, and the rise is significant | things that always happen a little: timeouts, 5xx, auth failures |
group_by + new_groups = true |
a value of the field (exception, logger, ...) appears that the baseline never saw, at least min_count times |
new kinds of errors after a deploy |
What keeps false alarms out:
- Share of traffic (
per_traffic = true): the rule compares matches / traffic instead of matches per hour, so errors that triple because a sale tripled the traffic stay quiet, and a rise from 1% to 10% of a busy hour fires. Traffic is every message in the scan's scope, ortraffic_query(e.g.path:/checkoutfor checkout errors). The built-in error, 5xx, connectivity and database rules use it. - Seasonal baseline (
baseline_shift = "1d"or"7d",baseline_periods = 3): the window is compared with the same window one day (week) earlier, three times; the period with the median rate is the reference, so the morning peak is compared with yesterday's morning peak, and one bad day among the three changes nothing. Periods without any data (older than the index retention) are dropped and the finding says so (baseline_note). By default the baseline is the period right before the window, as long as the window, orbaseline = "24h". - Significance: a growth fires only if it is unlikely to be chance, by an exact conditional binomial test of
the window's count against the reference period's (the standard test comparing two Poisson rates). 3 errors
then 7 (x2.3) stays quiet with a
notesaying so; 400 then 600 (x1.5, withgrowth = 1.4) fires. Each growth result carries itsconfidence; the rule'sconfidence(default 0.99) is the bar.
Every rule costs two exact counts, plus one per extra baseline period and for traffic; identical counts are made
once per scan (rules share the error query, the traffic) and run concurrently (limits.scan_concurrency). Groups
and one sample are fetched only for rules that fired or need them to decide, and a sample shown by one finding is
not repeated by the next. The result has findings (fired, most severe first), quiet (checked and normal, with
counts and trend) and skipped (could not be checked, e.g. a field these logs do not have).
Built-in rules: crash and resource_exhaustion (critical, any match); error_spike (share of errors x2),
new_error_types (exception unseen in 24 h), http_5xx (share x2, only where a status field exists),
connectivity (timeouts, refused/reset connections, DNS; share x3) and database (deadlocks, lock timeouts,
too many connections; share x3), all high; auth_failures (rate x3, at least 20), medium.
[scan]
disable = ["auth_failures"] # built-in rules to turn off
exclude = 'logger_name:HealthCheck OR "GET /health"' # noise dropped from every rule
[scan.rules.connectivity] # a built-in: only the keys you set change
min_count = 30
[scan.rules.card_declined]
description = "Bank declines"
query = 'service:payment AND message:"declined"'
severity = "high" # critical | high | medium | low
growth = 2.0
min_count = 20
per_traffic = true # compare declines / payment traffic, not declines per hour
traffic_query = "service:payment"
baseline_shift = "7d" # against the same hour of the last 3 weeks (payday, weekends)
group_by = "bank_code" # top groups shown with the finding
tags = ["payment"]
[scan.rules.ledger_mismatch]
query = '"ledger mismatch"' # no condition: any match fires
severity = "critical"
instances = ["payment/prod"] # instances, groups or environments; everywhere when omitted
[scan.rules.consumer_lag]
query = "consumer_lag:>10000"
requires = ["consumer_lag"] # skipped (and said so) where the field does not exist
Other keys: errors_only = true ANDs the instance's error_query, exclude drops noise for one rule, baseline
sets the rule's own baseline length, baseline_periods the number of shifted periods, confidence the
significance bar. A rule's own baseline wins over the call's. The config is validated at startup like the rest (unknown keys, a rule that
would fire on all traffic, new_groups without group_by).
Selecting what to run: scan(rules=["payment"]) takes rule names or tags, min_severity="high" leaves out
lower rules before any request, and checks=[{"name": "declined", "query": "message:declined", "growth": 2}]
adds rules written for the request at hand (same keys as above). A preset with tool = "scan" saves a scan
the team runs often, and the scan MCP prompt (/mcp__graylog__scan in Claude Code) runs scan, verifies each
finding and reports them as a table.
Writing rules that are fast and accurate:
- Match on fields when you have them (
http_status:[500 TO 599],exception_class:...); otherwise quoted phrases ("Connection refused"). Avoid leading wildcards and regex: they scan every term in the index. - Pick the condition from how often the event normally happens: never, then
threshold = 0; always a little, thengrowth(the significance test handles noise;min_countonly sets the smallest count worth a look). - Anything that grows with traffic gets
per_traffic = true; traffic with a daily or weekly curve getsbaseline_shift. - Drop known noise with
exclude(per rule or under[scan]) rather than raising thresholds. - Use a longer baseline (
baseline = "24h") for rules on spiky or low-volume traffic, andrequiresfor rules that depend on a field only some systems log. - Give each rule a severity that matches who should be woken up, and tags that match how people ask ("payment", "security", "dependencies").
Searches that find nothing
search_logs and aggregate with kind="count" do not stop at "no matches". Four decisions, each explained in the result:
-
Safe rewrite, before the search runs, only for a query that cannot match as written: a field this Graylog does not have but with one clear counterpart (
Service:payment->service:payment; the instance's configured name for it,exception:->exception_class:fromgroup_fields; the one field ending in that word,status:500->http_status:500; or an alias learned below), or a level word on a numeric level field (level:ERROR->level:3). The result carriesrewritten: {from, ran, why}. -
Counted suggestions, after a search found nothing: a misspelled field (close names and known synonyms such as
app->service), the level in the other form, another letter case (keyword fields are case-sensitive), a prefix wildcard (OrderNotFound->OrderNotFound*), lower-caseand/or(Lucene searches them as words), a bare id (req-7f3a9c, a UUID) also looked for in thetrace_fields, outside the repository's focus, a wider range (withlast_seen, the time of the newest match). Each is counted exactly by Graylog (in parallel, bounded); the ones that find something come back insuggestionswith their count and reason, best first, and the hint names the one to run. -
Learning from use. A suggestion counts as offered; it counts as used when the model then runs it and finds something. A fix's weight is the mean of Beta(1 + used, 1 + offered - used), so it needs evidence and loses weight when it is ignored. A fix used at least twice with weight >= 0.75 applies by itself on that instance: a field alias becomes a safe rewrite, and a case or prefix fix reruns the empty search. Focus and range are only ever suggested, since they change the question.
-
Diagnosis, after a search found nothing, in
diagnosis: the counts of each top-levelANDclause, the ones that match nothing on their own (or that each matches but not together), the values the field of such a clause really has in the range with the close ones (service:paymnt-> did you meanpayment; numbers are never guessed), and the dead clauses that match on their own further back.
Learned fixes live in ~/.config/graylog-mcp/learned.json, per instance; the admin page (Activity > What
graylog-mcp learned) shows each one with its counts, weight and whether it applies by itself, and forgets one on
request. GRAYLOG_MCP_LEARN=off stops learning and applying learned fixes; the deterministic fixes stay.
Ask in plain language
For people who do not write Lucene or know field names. ask takes a question, works out what it means, finds the
error it describes and follows that error's trace across services:
ask("Why did checkout fail with a bank connection error on prod this morning?")
ask("Thanh toán bị lỗi timeout trên prod trong 1 giờ qua, đi qua những service nào?")
ask("How many errors were there yesterday?")
It is off by default. It turns on when TYPESAFE_API_KEY is set in the environment or in the [secrets] table
of ~/.config/graylog-mcp/secrets.toml; without a key the tool is not even listed and nothing leaves the machine.
The model is TypeSafe's Jev, reached over HTTPS at https://api.typesafe.ai. It only chooses: the answer is built
from the existing tools and fixed sentences, and starts with a line such as "Understood: prod, last hour, errors
only" so the asker can correct it.
What is sent to TypeSafe: the question, the names and descriptions of your configured instances, and up to
max_candidates (30) error messages, each redacted like every other output and cut to send_chars (250) characters.
Never raw logs, field values the redactor masks, or anything from the Graylog token. Graylog itself is still only
read (GET and the read-only searches).
When the question is too vague ("Is the system ok?") ask returns a clarify text instead of guessing; when
nothing in the period matches it says so with the best score; when two errors match about equally it lists them.
An error without a trace id gets a hint instead of a flow. cost in the result counts the requests made.
[ask]
# api_key_env = "TYPESAFE_API_KEY" # name of the variable, never the key
# language = "en" # "en" or "vi": the language of the summary text
# min_confidence = 0.6 # below this, ask asks back
# relevance = 0.5 # an error group scoring lower is "not what you described"
# code_patterns = ['ORD-\d+'] # lets "what happened to order ORD-123?" find the code
# model, base_url, max_candidates, send_chars
Limits: the error groups come from the newest 500 error messages of the period, and the traces of one error are
looked up in one query of at most 500 messages. The error query and trace and service fields are the instance's
(error_query, trace_fields, service_fields). A repository's own .graylog-mcp.toml may set the other [ask] keys but not base_url or
api_key_env, which decide where the key and the error text go: set those in your user or a central config.
Root cause analysis
Example verdict, from the scenario in the integration suite (a bad deploy of payment), identical on
Graylog 4.3, 5.0, 5.2, 6.1 and 7.0:
Most likely origin: payment (confidence high). errors began at 08:42:49.756, 182.5s after payment deployed 1.3.9 -> 1.4.0 (hosts pay-1, pay-2 -> pay-3, pay-4). Also affected: gateway errors (+50ms), bank-adapter traffic drop.
How it gets there:
- Signals per service. Two pivots (errors, and traffic with average latency) split by service over the incident window plus a baseline window right before it.
- Onsets. For each service, the first interval that is clearly abnormal against its own baseline (median and MAD, so one noisy minute in the baseline does not hide anything) and stays abnormal: error rise, traffic drop or spike, latency rise. Error onsets are then refined to the exact first error message.
- Changes. Version fields, host rollouts and start/stop lines (see
view="changes") in the window and the 24 hours before it. A service that merely went silent is reported as a traffic drop, not a change. - Call graph. Traces sampled per service (and from failing requests) give caller -> callee edges.
- Ranking. Earliest onset (exact timestamps break ties inside the first interval), a change shortly
before the onset, callers failing after it, and a dependency failing before it (which points further
down) all move the score. The result lists the reasons for each candidate, a merged timeline, the first
error message with its
ref, and the next calls to verify the hypothesis.
It is a ranked hypothesis with its evidence, not a certainty. Field names come from config:
service_fields, trace_fields, version_fields, latency_fields, change_query.
Version detection
At startup the server reads GET /api/system (falling back to GET /api/ when the token may not read
system info), picks the APIs below and caches the result; describe shows the choice. An API that
answers 404/405/501 is dropped and the next one is used.
| Version | Messages | Aggregations |
|---|---|---|
| 4.x – 5.1 | GET /search/universal/absolute → views |
POST /views/search/sync (pivot) |
| 5.2 – 6.x | universal → views → Scripting API | POST /search/aggregate → views pivot |
| 7.x | same as 5.2+ | same as 5.2+ |
Pivot requests use "field": "x" on 4.x and "fields": [...] from 5.0. Histograms always use a views
pivot with a time grouping. Query syntax errors and zero-result queries are checked with
POST /search/validate so the model gets the actual problem ("incomplete query", "unknown field: sevrity")
instead of a bare "all shards failed". Graylog 7 ships its own MCP endpoint; this server is still useful
there for masking and compact output.
Every tool, each API path forced on its own (universal, views, Scripting API), the read-only guarantee and the no-leak check pass against real containers of:
| Graylog | Search backend | Result |
|---|---|---|
| 4.3.15 | OpenSearch 1.3 | pass |
| 5.0.13 | OpenSearch 2.15 | pass |
| 5.2.12 | OpenSearch 2.15 | pass |
| 6.1.16 | OpenSearch 2.15 | pass |
| 7.0.13 | OpenSearch 2.15 | pass |
Behaviour observed on real servers and handled: 5.x+ pivots return an (Empty Value) bucket for documents
without the grouped field (reported separately as without_field); the Scripting API returns "-" for
missing fields and no message index; invalid queries come back as HTTP 500 from universal search on
4.x-5.x; universal search is still present in 7.0.
Output shaping
-
Masking (before anything else):
- always on: sensitive field names (
password,token,secret,authorization,cookie, ...), emails,Bearer/Basiccredentials, JWTs,key=value/"key": "value"secrets, credentials in URLs, PEM private keys, card numbers that pass the Luhn check; - packs enabled in config:
vn(phone numbers, 12-digit CCCD, optional 9-digit CMND),us(SSN),eu(IBAN with checksum),uk(NINO),in(Aadhaar, PAN); - a ready-made
profile(below), then your ownpacks,sensitive_fieldsand regexes on top, plus an allow-list and excluded field names to avoid false positives.
profileAdds to the core rules basic(default)nothing piifield names for personal names, phone, birth date, home/billing/shipping address, street financefield names for account, card, CVV, IBAN, routing and tax ids; the eupack (IBAN)strictpii+finance+ every country pack + IPv4 addresses (noisy:1.2.3.4version numbers are masked)[redaction] profile = "pii" packs = ["vn"] # added to the profile's packs sensitive_fields = ["order_note"] # added to the profile's field names exclude_fields = ["phone"] # always wins over the profile
Grouping keys (
aggregatewithkind="top"or"errors") are masked too. - always on: sensitive field names (
-
Stack traces (Java/Kotlin, Python, .NET, Go, Node): exception lines,
Caused byblocks and frames fromapp_packagesare kept; the rest becomes… N frames. Withoutapp_packages, the first N frames are kept (the last N for Python). -
Repeated lines: numbers, UUIDs, hex, IPs and timestamps are normalised and identical templates grouped with
count,first,lastand one sample. -
Size: per-value and per-call character caps, with
truncatedandnext_offset. -
Time: shown in the configured timezone with its offset. Each line carries
ref: index/idfor follow-up calls. -
Errors: clear messages for 401, 403, query syntax errors (with position), timeouts, unknown streams (with suggestions) and unsupported versions.
-
Load on Graylog: each instance takes at most
max_concurrencyrequests at a time (default 16), shared by every tool call and, with the shared server, every session. Connection errors, 429 and 502/503/504 are retriedretriestimes (default 2) with exponential backoff, honouringRetry-After.
Configuration
Environment variables:
| Variable | Purpose |
|---|---|
GRAYLOG_URL, GRAYLOG_TOKEN |
single instance with token auth |
GRAYLOG_USERNAME, GRAYLOG_PASSWORD |
basic auth instead of a token |
GRAYLOG_VERIFY_TLS, GRAYLOG_CA_BUNDLE, GRAYLOG_PROXY |
TLS and proxy for the env-only instance |
GRAYLOG_TIMEZONE |
display timezone and zone for naive times (default UTC) |
GRAYLOG_REDACTION_PACKS |
e.g. vn,eu |
GRAYLOG_REDACTION_PROFILE |
basic, pii, finance or strict |
GRAYLOG_APP_PACKAGES |
e.g. com.acme,/srv/app/ |
GRAYLOG_MCP_CONFIG |
path of a TOML config file |
GRAYLOG_MCP_HTTP_TOKEN |
bearer token required by the HTTP transport |
A TOML file (--config or GRAYLOG_MCP_CONFIG, else .graylog-mcp.toml in the current directory or a
parent up to the repository root, else ~/.config/graylog-mcp/config.toml) covers the
rest: several instances, token or basic auth, TLS verification and CA bundle, proxy, timezone,
trace_fields, error_query (because level is a syslog number or a string depending on how logs are
shipped), app_packages, redaction packs and custom patterns, presets and limits. Secrets are never
written in the file; it names the environment variable that holds them (token_env, password_env).
The file is validated at startup and any mistake (unknown key, bad regex, unknown timezone, missing
secret) stops the server with a precise message.
See examples/config.toml for every option.
One repository, several environments
Commit a .graylog-mcp.toml at the root of your application repository with one instance per environment.
The server finds it from the current directory or any parent up to the repository root, so everyone working
in the repo gets the same setup. Tokens stay out of the file: each instance names its environment variable.
Only use this with repositories you trust. The file is applied as it is, with no approval step, and with the
shared server, opening a client in the repository is enough. Its url, proxy, verify_tls and *_env settings
decide where a saved secret is sent and which one, include can pull in other files, [redaction] can switch masking
off, repos decides which folders get a .mcp.json, and [http] can open the server to the network. Read a file you
did not write before you run start, init or install there, or open a client in that repository.
# .graylog-mcp.toml
default_instance = "staging" # what tools use when no environment is named
timezone = "Asia/Ho_Chi_Minh"
[redaction]
packs = ["vn"]
[investigation]
trace_fields = ["traceId", "X-Request-ID"]
[instances.dev]
url = "https://graylog-dev.example.com"
token_env = "GRAYLOG_DEV_TOKEN"
description = "Development cluster, noisy, debug logs on"
[instances.staging]
url = "https://graylog-staging.example.com"
token_env = "GRAYLOG_STAGING_TOKEN"
description = "Staging, deployed on every merge to main"
[instances.prod]
url = "https://graylog.example.com"
token_env = "GRAYLOG_PROD_TOKEN"
description = "Production"
error_query = "level:<=3 AND NOT logger_name:healthcheck" # any key can differ per environment
Register the server once for the project (Claude Code reads .mcp.json at the repo root and expands
${VAR} from each developer's environment):
{
"mcpServers": {
"graylog": {
"command": "uvx",
"args": ["--from", "git+https://github.com/ntbang0901/graylog-mcp", "graylog-mcp"],
"env": {
"GRAYLOG_DEV_TOKEN": "${GRAYLOG_DEV_TOKEN}",
"GRAYLOG_STAGING_TOKEN": "${GRAYLOG_STAGING_TOKEN}",
"GRAYLOG_PROD_TOKEN": "${GRAYLOG_PROD_TOKEN}"
}
}
}
}
Then ask in plain words: "why is checkout failing on staging?", "compare errors on prod before and
after 14:00". Every tool takes instance, describe shows each environment with its description
and detected Graylog version (environments may run different versions), and results always name the
instance they come from. A developer who has no token for an environment simply gets a clear error for that
one; the others keep working.
Clients that do not start the server inside the repository (Claude Desktop) need the path explicitly:
GRAYLOG_MCP_CONFIG=/path/to/repo/.graylog-mcp.toml.
Setup helpers and admin UI
| Command | What it does |
|---|---|
graylog-mcp init |
Guided setup: environments, connection test, field detection, config file, client registration. --yes --env staging=https://... --env prod=https://... for scripts. |
graylog-mcp login [INSTANCE...] |
Asks for each missing token/password, tests it, and saves it for this user in ~/.config/graylog-mcp/secrets.toml (owner-only permissions, outside any repository). The server reads environment variables first, then this file. graylog-mcp logout forgets them. |
graylog-mcp doctor |
Checks every environment (token set, reachable, TLS, version, readable streams, data, configured fields exist, error query matches, redaction) and prints a fix for each problem. Exit code 1 on failure, --json for CI. |
graylog-mcp detect |
Suggests service_fields, trace_fields, version_fields, latency_fields, error_query (syslog numbers or words), exception/logger fields and app_packages from the logs, with coverage and redacted sample values. --apply writes them. |
graylog-mcp install <client> |
Registers the server in claude-code (.mcp.json), cursor, vscode or claude-desktop, merging with existing entries and keeping a .bak. Tokens are referenced as ${VAR} / ${env:VAR}, never written (Claude Desktop gets placeholders unless --with-secrets). |
graylog-mcp ui |
Local admin web UI (below). |
graylog-mcp ui opens a page on 127.0.0.1 with:
- Dashboard: the setup steps (one Finish setup button), log volume and errors over the last 24 hours or 7 days (read from Graylog when the page opens, nothing stored), every environment's health with a sparkline or its problem and a button to fix it (Add token, Fix), the tools the model used, and the searches that found nothing;
- Activity: every tool call the model made, per hour: found something / found nothing / error, per-tool counts and latency (median, p95), and the recent calls with what was asked; Re-run opens one in the Playground;
- Environments: one card per group with its environments, default environment, repositories and their
focus; descriptions and defaults save as you change them. The environment form tests the connection and saves
in one click (Save anyway if Graylog cannot be reached), names the environment from the URL
(
graylog-uat.acme.comgivesuat) and saves the token or password to your per-user secrets file; - Log fields: settings per scope, with detection from your logs in the same page: suggestions next to the value in effect, applied to the scope you are editing;
- Redaction: toggle country packs, add custom patterns and allow-list entries, and see live which rules mask your sample text;
- Playground: run any tool against any environment and see the exact output, its size and an approximate token count;
- Other clients: snippets and one-click install for Claude Desktop, Cursor, VS Code and Claude Code per project;
- Config file: edit the TOML with validation and an automatic backup.
Tool calls are recorded by the MCP server (stdio or shared) in ~/.config/graylog-mcp/usage.jsonl: tool,
environment, duration, number of results, size, and the query or id asked for, redacted with your redaction
rules before it is written. It stays on your machine, is trimmed past 4 MB, and GRAYLOG_MCP_USAGE=off in the
server's environment turns it off.
Ctrl/⌘+K jumps to any page, environment, tool or action; unsaved changes show a save bar (Ctrl/⌘+S) and are not lost by leaving the page. Light or dark follows the OS until you pick one (the Follow OS theme checkbox goes back to it), with a phone layout; slate greys, teal for actions and data, amber for warnings, coral for failures.
The UI listens on loopback only, checks the Host header, and every API call needs the random token in the
link printed at startup. It writes the config file and client configs when you ask. A token or password
entered in the form is saved, when you tick "Save it on this machine", to the same per-user secrets file as
graylog-mcp login, never to the config file.
*_env settings hold the name of a variable (GRAYLOG_PROD_TOKEN), never the secret: a value that is not
a valid variable name is refused, and never echoed back, since it is most likely a secret typed into the wrong
field.
Groups and your own environments
Large organisations often run one Graylog per system and per environment: ERP, CXP, PAYMENT... each with its own dev/uat/prod (or sandbox, dr, prod-eu...). Environment names are yours; nothing assumes dev/staging/prod, and each group can have a different set.
# graylog-org.toml: one file for the whole company, e.g. in a shared platform repository
timezone = "Asia/Ho_Chi_Minh"
default_environment = "uat" # used when a question names only the system
[environments.prod] # declare environments once; every group inherits these settings
description = "Production"
error_query = "level:<=2"
ca_bundle = "/etc/ssl/corp-ca.pem"
[environments.uat]
description = "User acceptance"
[groups.erp]
description = "ERP"
trace_fields = ["correlationId"] # anything set on a group applies to all its environments
[groups.erp.environments.uat]
url = "https://graylog-erp-uat.corp"
token_env = "GRAYLOG_ERP_UAT_TOKEN"
[groups.erp.environments.prod]
url = "https://graylog-erp.corp"
token_env = "GRAYLOG_ERP_PROD_TOKEN"
[groups.payment]
description = "Payment platform"
default_environment = "sandbox"
[groups.payment.environments.sandbox]
url = "https://graylog-pay-sbx.corp"
token_env = "GRAYLOG_PAYMENT_SANDBOX_TOKEN"
[groups.payment.environments.prod]
url = "https://graylog-pay.corp"
token_env = "GRAYLOG_PAYMENT_PROD_TOKEN"
Settings are layered: [environments.<env>] < [groups.<group>] < [groups.<group>.environments.<env>].
Each pair becomes an instance named <group>/<environment>.
A service repository then only says which group it belongs to:
# payment-api/.graylog-mcp.toml
include = "../platform/graylog-org.toml" # relative to this file; a list is allowed
default_group = "payment"
only_groups = "payment" # load only this group here (a list, or "*" for every group)
How the model (and you) pick an instance, in every tool's instance argument:
| You say | Instance |
|---|---|
payment/prod, payment prod, PAYMENT:prod |
payment/prod |
payment |
the group's default_environment (else the global one) |
prod |
prod of the default_group; an error listing the options if several groups have it |
| nothing | default_instance, else default_group + default_environment |
describe returns the groups with their environments and descriptions, graylog-mcp init asks for
groups first, and the admin UI shows environments by group (instances from an included file are marked and
edited in that file).
Repositories of a group
List the repositories each group serves, as local folders (paths relative to the config file, ~ allowed) or
git remotes (git@gitlab.corp:acme/payment-api.git, acme/payment-api or just payment-api):
[groups.payment]
repos = ["~/code/payment-api", "../payment-worker", "acme/payment-gateway"]
When the server runs inside one of them (a folder or any subfolder, or a clone whose origin matches), it
loads only that group: "errors on prod" means that system's production, and the other groups' Graylog
servers are not reachable from that repository. Asking for one returns an error that says why and how to
enable it. describe shows the repository and the scope; graylog-mcp doctor shows which repository
was recognised.
To reach more groups from a repository, set only_groups = ["payment", "erp"] (or "*" for all) in its
.graylog-mcp.toml, or GRAYLOG_MCP_GROUPS=payment,erp in the MCP client's environment. only_groups also
works without repositories, e.g. in a personal config. The admin UI always shows every group.
Manage them in the admin UI (Environments, in each group's card: add a local folder, see its git remote and
whether it is set up, "Set up" writes .graylog-mcp.toml with an include of the shared file and
only_groups, so the scope holds on every machine whatever its folder layout, and registers
Claude Code there) or from the command line:
graylog-mcp repo --config ../platform/graylog-org.toml add payment # current folder; also sets it up
graylog-mcp repo list
graylog-mcp repo remove payment ~/code/payment-api
Focus: search this repository's service by default
Inside a repository, "errors in the last hour" means that service's errors. search_logs, aggregate,
compare_periods and root_cause with view="changes" add a filter on the
service field and say so in their result (focus). Other services and streams are searched only when asked for:
streams=["*"] (everything), explicit streams, or a query that names a service field
(application:"other-service"). trace_request and the other root_cause views always span every service.
Each repository sets its focus in its own .graylog-mcp.toml:
[focus]
service = "catalog-mdm-service" # value of the service field; a list for several; false = no service filter
streams = ["MDM"] # optional: streams searched by default
# field = "application" # optional: the service field (default: the first of service_fields present)
Without [focus], the service is guessed from the repository name (folder or git remote) by matching it against
the values of the service fields (service, application, ...) over the last 24 hours: catalog-mdm finds
catalog-mdm-service. No clear match, no filter; describe shows what was picked. Set it from the admin UI
(Environments > the repository's Focus), with graylog-mcp repo focus catalog-mdm-service --streams MDM inside the
repository, or for one MCP client with GRAYLOG_MCP_SERVICE=<name> (off disables it).
Settings by scope
The admin UI's Log fields page edits field names, queries and connection options for a chosen scope: global, one environment (for every group), one group, or a single instance. Each field shows the value currently in effect; leaving it empty inherits it.
One process for every session
With stdio, the client starts a server process for every session: ten Claude Code sessions are ten
Python processes, each with its own connections and version detection. graylog-mcp start runs one server
for all of them instead, and still answers each session with the configuration of the repository it works
in (its .graylog-mcp.toml, group, focus), as if it had been started there.
What start does, so you know what is on your machine:
-
A stable copy:
uv tool install(in~/.local/share/uv/tools), not uvx's temporary cache.updatereinstalls it from the same source; the version shown everywhere includes the git commit. -
The server:
graylog-mcp serve --shared --admin --port 18742 --config <file>on 127.0.0.1 (the port is fixed so no client configuration goes stale: if another program uses it,startstops with an error;--portpicks another one, later commands follow it), started at login and started again if it crashes:Started at login by Restarted after a crash by macOS a LaunchAgent ( ~/Library/LaunchAgents/io.github.ntbang0901.graylog-mcp.plist)launchd Linux with systemd a user unit ( ~/.config/systemd/user/graylog-mcp.service)systemd Linux desktop without systemd XDG autostart ( ~/.config/autostart/graylog-mcp.desktop)graylog-mcp keepaliveWindows a Task Scheduler task graylog-mcp, no console windowgraylog-mcp keepaliveElsewhere (WSL or a server without systemd, no desktop session) it runs until you log out; on WSL, turning on systemd (
[boot] systemd=truein/etc/wsl.conf) is enough. Log:~/Library/Logs/graylog-mcp.logor~/.config/graylog-mcp/server.log. -
The admin page at
http://127.0.0.1:18742/admin/, with the access token kept in~/.config/graylog-mcp/admin-token(owner only);graylog-mcp uiopens it. -
Claude Code, connected repository by repository:
startwrites the shared server into the.mcp.jsonof each repository of your groups, creating the file when it has none (the other servers in it stay, the previous file is kept as.mcp.json.bak):{"mcpServers": {"graylog": {"type": "http", "url": "http://127.0.0.1:18742/mcp", "headers": {"X-Graylog-MCP-Repo": "${PWD:-}"}}}}. Claude Code fills in the folder it runs in; the server walks up to the repository, and asks you once to approve a project's server. A repository whose.mcp.jsonstill starts graylog-mcp itself gets a private override (Claude Code's local scope) instead: the file stays as it is for your teammates.graylog-mcp start --globalregisters it once for every project (user scope) instead. Without it,startremoves a registration for every project that an earlier version made, so that Claude Code does not report the server as defined in several scopes with different endpoints. -
A different port: whenever the server starts (by
start, at login, afterupdate), what an earlier port registered follows it: Claude Code's registrations and overrides, the URL of a.mcp.jsonentry on another local port (rewritten in place,.bakkept) and the Cursor and VS Code entries of your groups' repositories.statuslists what still points elsewhere.
How the server picks the configuration of a session:
- The repository's own
.graylog-mcp.toml; without one, the--configfile (startuses the one found where you ran it, else~/.config/graylog-mcp/config.toml), with the group matched from itsrepos. - Edits (admin page,
login,repo focus) apply within seconds, without a restart. - One Graylog client per instance, shared by every repository using it: one connection pool, one version detection, one stream and field cache.
- Tokens come from the server's environment or
graylog-mcp login(the admin page saves them there too). - Other clients: Cursor and VS Code send
${workspaceFolder}(graylog-mcp install cursor --shared), any client can add?repo=<folder>to the URL. Claude Desktop already runs one server for all its chats.
Shared HTTP server and Docker
stdio is the default. For one server shared by a team, use streamable HTTP with its own bearer token:
GRAYLOG_MCP_HTTP_TOKEN=$(openssl rand -hex 32) graylog-mcp --transport streamable-http --host 0.0.0.0 --port 8000
The server refuses to listen on a non-loopback address without a token (unless --allow-no-auth is
given, e.g. behind an authenticating proxy). /healthz is open; the MCP endpoint is /mcp.
docker build -t graylog-mcp .
docker run -p 8000:8000 \
-e GRAYLOG_URL=https://graylog.example.com -e GRAYLOG_TOKEN=... \
-e GRAYLOG_MCP_HTTP_TOKEN=... graylog-mcp
A server for the team on a VM
One serve --shared process on a VM can serve every developer and every repository. The VM cannot read the
developers' folders, so a client names its repository by identity: its git remote
(github.com/acme/payment-api, acme/payment-api, any git URL) or, failing that, its folder name. Groups list
their repositories by remote or name, and [repos."<repository>"] holds each one's settings (what a
repository's own .graylog-mcp.toml holds on a developer's machine):
# /etc/graylog-mcp/org.toml on the VM, kept in a git repository and deployed by the platform team
[groups.payment]
repos = ["acme/payment-api", "acme/payment-worker"]
[groups.payment.environments.prod]
url = "https://graylog-pay.example.com"
token_env = "GRAYLOG_PAYMENT_PROD_TOKEN"
[repos."acme/payment-api"] # focus, default_group/environment/instance, only_groups, scan, presets
focus = { service = "payment" }
[http]
auth_token_env = "GRAYLOG_MCP_HTTP_TOKEN" # every developer sends it
# repo_paths = false # default off a loopback address: folders clients name are never read
graylog-mcp serve --shared --host 0.0.0.0 --port 8000 --config /etc/graylog-mcp/org.toml
The config is read again within seconds of a change: deploy the file, no restart. The admin page writes files
on the machine it runs on, so it only listens on the loopback address; reach it with an SSH tunnel
(ssh -L 8000:127.0.0.1:8000 vm, then http://127.0.0.1:8000/admin/ with serve --admin).
On each developer's machine, nothing runs but Claude Code:
GRAYLOG_MCP_HTTP_TOKEN=<token> |
the server's token, in the environment Claude Code starts in (shell profile; setx on Windows). Claude Code sends it as Authorization: Bearer ${GRAYLOG_MCP_HTTP_TOKEN}; no graylog-mcp command prints it |
graylog-mcp connect https://graylog-mcp.example.com |
shows how the server identifies the current repository and registers the server in Claude Code for every project (asks for the token when the variable is not set, to check the connection) |
graylog-mcp connect URL --project |
writes the repository's .mcp.json instead. It holds no secret and no path: commit it; each teammate only sets the variable |
graylog-mcp status |
is the server reachable, which version, and how it identifies this repository: group, focus, default environment, each Graylog instance |
How a connection names its repository: in a project's .mcp.json, Claude Code runs graylog-mcp headers
(its headersHelper) in the project folder when it connects; it prints the repository's git remote. Without
graylog-mcp installed, the remote written by connect applies. Registered for every project, there is no helper
(it would run outside the project): the server identifies the repository by the name of the folder Claude Code
runs in (${PWD}), so list repositories by name in repos, or use connect --project for the ones listed by
remote.
Testing
uv sync
uv run pytest # unit + contract tests (no network), coverage >= 85%
uv run ruff check && uv run ruff format --check && uv run mypy
-
Unit tests: masking (including values that must not be masked), stack traces in five languages, line grouping, time parsing, pivot building and parsing, config validation, HTTP auth.
-
Contract tests: every tool against an in-memory Graylog (
tests/fake_graylog.py, served throughhttpx.MockTransport) that reproduces the response shapes of 4.3, 5.0, 5.2, 6.1 and 7.0, with planted sensitive values that must never reach the output, an assertion that no request could change state, and a size budget per call. -
Integration tests:
tests/integration/docker-compose.ymlstarts Graylog 4.3, 5.0, 5.2, 6.1 and 7.0, each with MongoDB and OpenSearch;seed.pyships the same GELF dataset, then every tool runs against it:tests/integration/run.sh v50 # or v43 v52 v61 v70
CI runs the whole matrix weekly, on demand, and on pull requests that change the backends.
CI on every push: ruff, mypy, tests on Linux (Python 3.11-3.13), Windows and macOS, wheel build with metadata check and a smoke test in a clean environment, and a Docker build with an HTTP smoke test. CodeQL scans the code and workflows; Dependabot keeps dependencies and actions current. See CONTRIBUTING.md and SECURITY.md.
Project layout
src/graylog_mcp/
config.py environment + TOML, validated at startup (fail fast)
client.py async httpx client: auth, TLS, proxy, read-only guard, HTTP error mapping
backends/ version detection and API selection: universal.py, views.py, scripting.py
timerange.py time parsing, display, interval selection
redact.py core rules, country packs, Luhn / IBAN checks
shaping.py field selection, truncation, stack traces, line grouping, output budget
tools.py tool implementations
rca.py root cause analysis: onsets, change detection, service map, ranking
ask/ the optional `ask` tool: Jev client, question understanding, error groups and flow, fixed texts
operations.py every operation in one table: what presets, the admin page and usage know by name
toolspecs/ the tools a model sees, each declared once: its arguments and the operations it runs
server.py model instructions, and the MCP tools built from those declarations
License
MIT
Metadata
Release files for graylog-mcp 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| graylog_mcp-0.3.0.tar.gz | 451.6 kB | With provenance | Details for graylog_mcp-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| graylog_mcp-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for graylog_mcp-0.3.0-py3-none-any.whl |
Total release size: 704.0 kB
Release files / graylog_mcp-0.3.0.tar.gz
| Download URL | graylog_mcp-0.3.0.tar.gz |
|---|---|
| Size | 451.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2f3af44970f3010475303970a5865b5b8e03b2ac17c2970c2e71ddab58f53b0a
|
|
BLAKE2b-256 checksum How to use checksums |
7f4c527b6a6bb6f22eb8fd788f01adff61c9c4656769fd5b3df36b50d36ecefa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logRelease files / graylog_mcp-0.3.0-py3-none-any.whl
| Download URL | graylog_mcp-0.3.0-py3-none-any.whl |
|---|---|
| Size | 252.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
66f55f66bd3a9602eb9ba50c3e64800404d97967a9f7fc3de769e7150034c801
|
|
BLAKE2b-256 checksum How to use checksums |
f6918bfd1e256032cf892e164f61a1d2ae47e329b2c5e3981ea4851a62b90fc8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logSecurity for graylog-mcp 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for graylog_mcp-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.