Astrolabe / ajxpublic /korvus
Repository

Bundled MCP Servers Overview

reference scope mcp-servers Verification failed
  • mcp-servers/loki/cmd/mcp-loki/main.go
  • mcp-servers/prometheus/cmd/mcp-prometheus/main.go
  • mcp-servers/tempo/cmd/mcp-tempo/main.go
  • mcp-servers/opennebula/cmd/mcp-opennebula/main.go
Relevant source files · 21

Korvus does not talk to Loki, Prometheus or Tempo directly. It talks to three small MCP servers that live under mcp-servers/, one per telemetry backend, and each of those servers is a separate Go module with its own go.mod and its own process. The three entry points share the same shape and the same comment about why they exist: "It is a standalone deployment unit: Korvus never spawns it (CLAUDE.md constraint #1), the container runtime does, and Korvus reaches it over the URL in mcp.servers. It imports nothing from the root module." (mcp-loki, mcp-prometheus, mcp-tempo).

That last clause is a hard boundary and it is enforced by the module graph: the root module requires the MCP SDK and nothing from mcp-servers/ (go.mod), while each server module requires only the SDK, with the remaining entries marked indirect (loki/go.mod, prometheus/go.mod, tempo/go.mod). A server can therefore be deployed, upgraded or removed without touching the Korvus binary, and Korvus only ever learns about them through the URLs an operator lists in the mcp.servers block of the configuration file (korvus.example.yaml). The configuration side of that contract is described in Configuration Loading and Validation; the client that consumes those URLs is described in MCP Client, Registry and Server Connection.

The shared entry point shape

Each of the three working servers follows the same lifecycle, and reading one of them tells you what the others do. main does nothing but delegate and exit non-zero on failure, while run owns the process lifecycle "and returns an error instead of exiting, so every deferred cleanup runs" (mcp-loki main.go).

Configuration arrives as flags with an environment-variable fallback: the listen address comes from MCP_LOKI_ADDR (default :9101) and the upstream Loki URL from LOKI_URL (default http://localhost:3100) (mcp-loki main.go); Prometheus reads MCP_PROMETHEUS_ADDR and PROMETHEUS_URL (mcp-prometheus main.go); Tempo reads MCP_TEMPO_ADDR and TEMPO_URL (mcp-tempo main.go). The envOr helper at the bottom of each file returns the environment value when it is non-empty and the fallback otherwise, and the reported server version is a version variable overridden at build time with -ldflags="-X main.version=..." (mcp-loki main.go).

From there the server is assembled in four steps: build the upstream HTTP client, create an SDK server carrying the implementation name and version, register the tool surface on it, and wrap the whole thing in a streamable-HTTP handler. The handler factory returns the same server instance for every session, because "the tools are stateless and the upstream client is safe for concurrent use, so there is nothing per-session to build" (mcp-loki main.go, mcp-tempo main.go).

The HTTP surface is two routes on one mux: the MCP endpoint at /mcp and a liveness probe at /healthz that answers with status 200 and the body ok (mcp-loki main.go). The example configuration points every server URL at the /mcp path for exactly this reason, and warns that "a bare host:port fails the initialize handshake with 'session not found' and the server is dropped from the catalog at startup" (korvus.example.yaml).

Timeouts

Every entry point defines the same five constants, and the reasoning is written into the source. queryTimeout bounds one upstream request at 30 seconds, because "The agent's own session timeout is 10 minutes across all turns, so a single tool call that hangs for minutes spends the whole investigation's budget" (mcp-loki main.go, mcp-prometheus main.go, mcp-tempo main.go).

The HTTP server's own timeouts are deliberately lopsided. ReadHeaderTimeout is 15 seconds, but WriteTimeout is two minutes, because "A range query over a long window legitimately takes tens of seconds upstream; a write timeout below queryTimeout would cut the response while the query is still running" (mcp-loki main.go). Tempo's copy of that comment says "A TraceQL search over a long window legitimately takes tens of seconds upstream; a write timeout below queryTimeout would cut the response while the search is still running" (mcp-tempo main.go). IdleTimeout is two minutes in each server, and shutdownTimeout is ten seconds.

Shutdown

Signal handling is identical across the three servers. A signal.NotifyContext watches SIGINT and SIGTERM, the listen loop runs in a goroutine that pushes its error into a channel, and run selects between that channel and the cancelled context (mcp-loki main.go). On cancellation the server drains on a fresh context, because "ctx is already cancelled by the signal, and shutting down on a cancelled context drops in-flight responses immediately instead of draining them" (mcp-loki main.go).

The upstream clients

Each server talks to its backend through a small hand-written client in its own internal/ package, and each package states why it does not use the vendor's official client. The Loki client "deliberately does not use github.com/grafana/loki's own client: that module pulls the whole Loki server tree in for three GETs, and this module's dependency budget is one SDK" (loki/client.go). Prometheus gives the same reason for skipping client_golang/api (prom/client.go), and Tempo for skipping Grafana's own Tempo packages (tempo/client.go).

The client itself is just a base URL and an http.Client whose timeout bounds each request end to end (loki/client.go, prom/client.go, tempo/client.go).

Tool surfaces

Each server's internal/tools package holds the contract the model programs against. The package doc for the Loki tools describes "three read-only LogQL tools over the Loki HTTP API" (loki/tools.go); Register adds them to the SDK server — loki_query_range, loki_labels and loki_label_values (loki/tools.go). The Prometheus module registers prometheus_query, prometheus_query_range, prometheus_series, prometheus_search_metrics and prometheus_label_values (prometheus/tools.go). Tempo registers tempo_search and tempo_get_trace (tempo/tools.go).

Two details are shared across all three servers. First, every tool is marked with a read-only annotation. Second, every timestamp argument accepts exactly one representation — RFC 3339 UTC — and the Tempo package says so explicitly: "It is the same sentence mcp-prometheus and mcp-loki use, so a model that learned one server's time arguments has learned all three" (tempo/tools.go).

The Loki MCP Server, Prometheus MCP Server and Tempo MCP Server pages describe each tool surface, its arguments and its formatting rules in detail.

The OpenNebula placeholder

The fourth directory holds no server yet. Its entire entry point is a log line and an exit:

go
func main() {
	slog.Info("mcp-opennebula: not implemented")
}

(mcp-opennebula/main.go)

The example configuration still lists an opennebula entry at http://localhost:9103, and the comment above it says the server "is still a stub that serves nothing; its path follows when it lands" (korvus.example.yaml). Because nothing answers there, that endpoint simply fails to connect and is reported as a failed server rather than crashing startup.

How Korvus reaches these servers

The client side lives in internal/mcp. ConnectAll walks the configured name-to-URL map, dials each server with a per-server ten-second timeout that "bounds one server's initialize handshake so a hanging endpoint cannot stall startup discovery for the others", and registers the reachable ones (connect.go). Failure handling is per server: "Failures are per-server: one bad endpoint never blocks the rest" (connect.go). A server that fails is recorded through RecordFailure, which keeps it out of Servers() so that skills gated on it are excluded, while the error stays visible to system status (registry.go).

Registration itself refuses ambiguity: if a tool name is already claimed by another server, RegisterServer returns an error and the colliding server is dropped rather than shadowing the first (registry.go).

The transport is the SDK's streamable-HTTP client with the standalone SSE stream disabled — "Korvus is a pure request/response consumer; the standalone SSE stream only carries server-initiated messages we never use" — and with a tracing transport attached so that "an MCP server's own spans join the incident's trace instead of starting their own" (client_streamable.go). That transport injects the trace context into the outgoing request headers so the server's spans land in the same trace (tracing.go).

System status reports the configured set rather than the connected set: "A server that failed to dial is absent from the registry, and reporting only the registry's view would silently drop exactly the server an operator is trying to diagnose" (system.go). Each reported server carries its name, URL, connected flag, tool list and last error. The full picture of that path is in Connect API Server and Service Handlers and MCP Client, Registry and Server Connection.

Deployment

The servers are not part of the Korvus process and are not started by it. In the development stack the upstream systems they query — Postgres, Tempo, Prometheus, Loki, Grafana and the demo checkout service that generates telemetry — run as compose services (docker-compose.yml), while the MCP servers themselves are separate containers or local processes pointed at those services by their LOKI_URL, PROMETHEUS_URL and TEMPO_URL variables. The Deployment Stack and Example Configuration page covers how the whole stack comes up, and the Checkout Demo Service and Telemetry page covers the service that produces the logs, metrics and traces these servers expose.

Relationships

flowchart LR
  Config["MCPServer"] --> ConnectAll
  ConnectAll --> Registry
  ConnectAll --> StreamableHTTPClient
  Registry --> ToolsReg["tools.Register"]
  StreamableHTTPClient --> LokiEntry["mcp-servers/loki/cmd/mcp-loki/main.go"]
  StreamableHTTPClient --> PromEntry["mcp-servers/prometheus/cmd/mcp-prometheus/main.go"]
  StreamableHTTPClient --> TempoEntry["mcp-servers/tempo/cmd/mcp-tempo/main.go"]
  LokiEntry --> LokiTools["mcp-servers/loki/internal/tools/tools.go"]
  PromEntry --> PromTools["mcp-servers/prometheus/internal/tools/tools.go"]
  TempoEntry --> TempoTools["mcp-servers/tempo/internal/tempo/tools.go"]
  Registry --> mcpServers
  LokiEntry --> LokiUpstream["loki.Client"]
  PromEntry --> PromUpstream["prom.Client"]
  TempoEntry --> TempoUpstream["tempo.Client"]

Configuration reference

Server Listen flag Listen env (default) Upstream flag Upstream env (default) MCP path
mcp-loki -addr MCP_LOKI_ADDR (:9101) -loki-url LOKI_URL (http://localhost:3100) /mcp
mcp-prometheus -addr MCP_PROMETHEUS_ADDR (:9100) -prometheus-url PROMETHEUS_URL (http://localhost:9090) /mcp
mcp-tempo -addr MCP_TEMPO_ADDR (:9102) -tempo-url TEMPO_URL (http://localhost:3200) /mcp
mcp-opennebula — — — — none (stub)

Sources: mcp-loki main.go, mcp-prometheus main.go, mcp-tempo main.go, mcp-opennebula main.go.

Reference

Symbol Kind What it does Location
mcp-loki entry point file Loki MCP server process: flags, SDK server, HTTP mux, graceful shutdown mcp-servers/loki/cmd/mcp-loki/main.go:1-127
mcp-prometheus entry point file Prometheus MCP server process, same shape mcp-servers/prometheus/cmd/mcp-prometheus/main.go:1-128
mcp-tempo entry point file Tempo MCP server process, same shape mcp-servers/tempo/cmd/mcp-tempo/main.go:1-127
mcp-opennebula entry point file Placeholder that logs "not implemented" mcp-servers/opennebula/cmd/mcp-opennebula/main.go:1-8
tools.Register (Loki) function Adds loki_query_range, loki_labels, loki_label_values to the SDK server mcp-servers/loki/internal/tools/tools.go:61-143
tools.Register (Prometheus) function Adds the Prometheus query, series, metric-search and label-value tools mcp-servers/prometheus/internal/tools/tools.go:74-230
tools.Register (Tempo) function Adds tempo_search and tempo_get_trace mcp-servers/tempo/internal/tools/tools.go:45-120
loki.NewClient function Loki HTTP client with a per-request timeout mcp-servers/loki/internal/loki/client.go:28-36
prom.NewClient function Prometheus HTTP API v1 client mcp-servers/prometheus/internal/prom/client.go:28-36
tempo.NewClient function Tempo HTTP API client mcp-servers/tempo/internal/tempo/client.go:26-36
MCPServer struct One configured MCP endpoint: name and URL internal/config/config.go:160-180
ConnectAll function Dials every configured server with a 10s handshake timeout, records failures internal/mcp/connect.go:20-47
Registry.RegisterServer function Discovers tools and rejects colliding tool names internal/mcp/registry.go:60-78
Registry.RecordFailure function Marks a configured server unavailable for status reporting internal/mcp/registry.go:37-42
StreamableHTTPClient.Connect method Initializes the MCP session over streamable HTTP with tracing internal/mcp/client_streamable.go:60-90
systemService.mcpServers method Reports every configured server with its connected flag and tools internal/transport/system.go:146-193

Generated by astrolabe from commit 0c2dca73 on branch main.