Operations

July 30, 2026 · View on GitHub

Day-2 operational concerns for the MediaWiki MCP Server: structured logging, health and readiness probes, Prometheus metrics, log tailing, and graceful shutdown.

Observability

Every stderr line is a JSON object. Each line has ts (ISO-8601 UTC) and level (RFC 5424 severity). Prose lines add message; structured events add event instead.

Tool calls

Every tool invocation emits one line:

{"ts":"...","level":"info","event":"tool_call","tool":"get-page","wiki":"example.org","target":"Main Page","outcome":"success","duration_ms":142,"caller":"sha256:7f2a4c1d9e0b","upstream_status":200,"truncated":false}

Fields you'll filter on:

  • outcomesuccess, cancelled (the caller cancelled the call or disconnected before it finished), or one of seven error categories: not_found, permission_denied, invalid_input, conflict, authentication, rate_limited, upstream_failure.
  • levelinfo for success and cancelled, error for upstream_failure, warning for everything else. A level=error alert catches server-side failures without firing on client mistakes like a typo'd page title.
  • callersha256: plus the first 12 hex chars of SHA-256 of the bearer token, or the literal string anonymous. Stable per token within a process; never the raw token.
  • target — a single identifier extracted from the tool's input (typically a page title, search query, or URL). Omitted for tools without one: get-pages, compare-pages, parse-wikitext, get-recent-changes.

All log output goes to stderr only; nothing is forwarded to the connected MCP client.

Startup banner

One line on server boot — a snapshot of the effective configuration that's safe to paste into a support ticket:

{"ts":"...","level":"info","event":"startup","version":"0.8.0","transport":"http","host":"0.0.0.0","port":8080,"auth_shape":"anonymous","default_wiki":"example.org","wikis":["example.org"],"allow_wiki_management":false,"allowed_hosts":["wiki.example.org"],"allowed_origins":["https://wiki.example.org"],"max_request_body":"1mb","upload_dirs_configured":false}
  • auth_shapeanonymous, static-credential, oauth-proxy (hosted sign-in configured), or bearer-passthrough (only while the deprecated MCP_ALLOW_BEARER_PASSTHROUGH is set).
  • host, port, allowed_hosts, allowed_origins — HTTP transport only. allowed_hosts is omitted when not configured. allowed_origins is always present: an empty array means every browser request is refused, not that the check is off.
  • upload_dirs_configuredtrue when uploadDirs (config) or MCP_UPLOAD_DIRS (env) is set. The actual paths are not logged.
  • max_request_body — HTTP transport only. The resolved MCP_MAX_REQUEST_BODY value.

Tokens, usernames, and passwords never appear.

Stray output

Log output from the MediaWiki client library, relocated here so it cannot corrupt the protocol stream. Expect it on a bot-password login, an API warning, or a request retry:

{"ts":"...","level":"warning","event":"stray_stdout","text":"[2026-07-23 16:48:43] [W] Warning received from API: main: ..."}

text is one line of library output, passed through unaltered, so it is neither JSON nor redacted — a login line carries the configured username.

Health vs readiness

  • GET /health — liveness. Returns 200 { "status": "ok" } whenever the process is responsive. Wire this into your orchestrator's restart policy.
  • GET /ready — readiness. Probes the default wiki via action=query&meta=siteinfo with a 3-second timeout and 5-second result cache. Wire this into traffic-shedding policy.

/ready response shape — 200 OK:

{ "status": "ready", "wiki": "example.org", "checked_at": "..." }

503 Service Unavailable:

{ "status": "not_ready", "wiki": "example.org", "reason": "...", "checked_at": "..." }

Metrics

Set MCP_METRICS=true to expose GET /metrics on the HTTP transport in Prometheus text format. Off by default.

Sample scrape:

# HELP mcp_tool_calls_total Total number of MCP tool invocations, labelled by tool, wiki, and outcome.
# TYPE mcp_tool_calls_total counter
mcp_tool_calls_total{tool="get-page",wiki="example.org",outcome="success"} 142
mcp_tool_calls_total{tool="get-page",wiki="example.org",outcome="not_found"} 4

# HELP mcp_inflight_requests Number of /mcp requests currently being served (subscription streams excluded).
# TYPE mcp_inflight_requests gauge
mcp_inflight_requests 3

Exposed series:

  • mcp_tool_calls_total{tool,wiki,outcome} — counter of tool invocations.
  • mcp_tool_call_duration_seconds{tool,wiki} — histogram of tool-call durations.
  • mcp_upstream_status_total{tool,wiki,status} — counter of upstream MediaWiki HTTP status codes.
  • mcp_inflight_requests — gauge of /mcp requests currently being served. Subscription streams are excluded: they are held open by design.
  • mcp_subscription_streams — gauge of open change-notification streams (subscriptions/listen), the closest measure of connected clients.
  • mcp_ready_failures_total — counter of /ready probes that returned non-200.
  • mcp_rate_limited_total{caller} — counter of tools/call requests refused with 429; the caller label is caller for authenticated callers and anonymous for the shared bucket. A rising caller series means authenticated callers hit MCP_RATE_LIMIT; a rising anonymous series means the shared backstop is engaging.
  • mcp_proxy_store_upstream_tokens — gauge of upstream MediaWiki tokens held in the hosted OAuth proxy store. This set grows with cumulative sign-ins over the process lifetime; watch it to size memory and the flush cost below.
  • mcp_proxy_store_clients — gauge of registered clients held in the hosted OAuth proxy store. FIFO-capped at 10,000, so this plateaus rather than growing without bound.
  • mcp_proxy_store_flush_duration_seconds — histogram of hosted-proxy store durable-flush durations (serialize + encrypt + write). Every upstream-token write flushes the whole store synchronously, so this scales with the token count above. Records successful flushes only.
  • mcp_proxy_store_flush_failures_total — counter of durable flushes that failed to write (a disk error). A failure means the most recent sign-in change is held in memory only and would be lost on restart; alert on any increase.

The endpoint is unauthenticated. Restrict reverse-proxy access to your scrape network only — most Kubernetes-style deployments expose /metrics on a separate port or path that isn't routable from the public ingress.

Cardinality for mcp_tool_calls_total scales as tools × wikis × outcomes — low thousands of series in a typical deployment, comfortably within Prometheus ingest budgets. With allowWikiManagement enabled, treat the wiki label set as monotonically growing: remove-wiki does not retract values already exported in past samples.

Tailing logs

Pipe stderr through jq or humanlog for live reading:

docker logs -f mediawiki-mcp-server | jq -R 'fromjson? // empty'
docker logs -f mediawiki-mcp-server | humanlog

Graceful shutdown

The server registers SIGTERM and SIGINT handlers in both the HTTP and stdio transports. On signal:

  1. The HTTP listener stops accepting new connections (server.close()). /health and /ready keep responding until the listener finishes closing.
  2. In-flight /mcp requests are given up to MCP_SHUTDOWN_GRACE_MS (default 10000) to finish, after which open change-notification streams are closed gracefully. The value is capped at 600000 (10 min); invalid values fall back to the default with a warning.
  3. The server emits two structured stderr events:
    • event: "shutdown" with signal, transport, grace_ms, in_flight_at_signal.
    • event: "shutdown_complete" with in_flight_drained, grace_exceeded, duration_ms.
  4. The process exits with code 0 if the drain finished within grace, 1 if grace_exceeded is true.

A second SIGTERM or SIGINT during drain forces an immediate exit with code 1, so an operator can escape a hung shutdown with a second Ctrl-C or follow-up signal.

The stdio transport closes its single transport on the same signals; MCP_SHUTDOWN_GRACE_MS is logged as 0 since stdio has no per-call queue to drain.

This makes docker stop, Kubernetes pod termination, and systemctl stop behave correctly: the orchestrator's default SIGTERM triggers a drain rather than a hard kill, and the orchestrator's escalation to SIGKILL after its own timeout still works as the backstop. Keep MCP_SHUTDOWN_GRACE_MS ≤ the orchestrator's own grace (Docker's default is 10s, Kubernetes' terminationGracePeriodSeconds defaults to 30s) — otherwise the drain never finishes before the orchestrator escalates to SIGKILL.