Bundled MCP Servers Overview
- 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
- mcp-servers/loki/cmd/mcp-loki/main.go 1–7, 25–35, 37–51, 53–63 +5 more
- mcp-servers/prometheus/cmd/mcp-prometheus/main.go 1–7, 26–36, 47–53
- mcp-servers/tempo/cmd/mcp-tempo/main.go 1–7, 25–35, 47–63, 72–81
- go.mod 1–30
- mcp-servers/loki/go.mod 1–16
- mcp-servers/prometheus/go.mod 1–10
- mcp-servers/tempo/go.mod 1–10
- korvus.example.yaml 97–121
- mcp-servers/loki/internal/loki/client.go 1–8, 28–36
- mcp-servers/prometheus/internal/prom/client.go 1–8, 28–36
- mcp-servers/tempo/internal/tempo/client.go 1–9, 26–36
- mcp-servers/loki/internal/tools/tools.go 1–8, 61–143
- mcp-servers/prometheus/internal/prom/tools/tools.go 74–230
- mcp-servers/tempo/internal/tempo/tools.go 22–28, 45–120
- mcp-servers/opennebula/cmd/mcp-opennebula/main.go 1–8
- internal/mcp/connect.go 9–47
- internal/mcp/registry.go 32–55, 60–78
- internal/mcp/client_streamable.go 60–90
- internal/mcp/tracing.go 45–62
- internal/transport/system.go 146–193
- deploy/compose/docker-compose.yml 1–91
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:
func main() {
slog.Info("mcp-opennebula: not implemented")
}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.