Loki MCP Server: Enabling Natural Language Log Queries for AI Clients
Project Overview
Positioning and Purpose
This project exposes Grafana Loki's log query capabilities to AI assistants through MCP (Model Context Protocol), enabling operations personnel to query logs using natural language instead of writing LogQL queries manually. This represents a significant advancement in making log analysis accessible to team members regardless of their query language expertise.
The core innovation lies in bridging the gap between human intent and machine-executable queries, allowing developers and operators to focus on problem-solving rather than syntax memorization.
Technology Stack
The project employs a carefully selected technology stack optimized for performance and maintainability:
| Layer | Technology | Version |
|---|---|---|
| Language | Go | 1.24+ |
| MCP SDK | github.com/mark3labs/mcp-go | v0.32.0 |
| Log Storage | Grafana Loki | 2.9.0 |
| Log Collection | Promtail | 2.9.0 |
| Visualization | Grafana | Latest |
| Containerization | Docker + Compose | - |
This stack provides a solid foundation for building a robust, scalable log query service that integrates seamlessly with existing observability infrastructure.
Core Capabilities
The server implements three essential MCP Tools that cover the fundamental log query workflow:
| Tool | Purpose | Loki API Endpoint |
|---|---|---|
loki_query | Execute LogQL queries | /loki/api/v1/query_range |
loki_label_names | Retrieve all label names | /loki/api/v1/labels |
loki_label_values | Get label value lists | /loki/api/v1/label/{name}/values |
These three tools form a complete toolkit for log exploration, enabling users to discover available labels, understand their values, and execute targeted queries.
Architecture Design
Overall System Architecture
The system follows a clean architectural pattern with clear separation of concerns:
┌─────────────────┐ ┌──────────────────┐ ┌──────────────┐
│ AI Client │ │ Loki MCP Server │ │ Grafana │
│ (Claude Code/ │────→│ :8080 │────→│ Loki │
│ Desktop/ │ MCP │ │ HTTP│ :3100 │
│ Cursor) │ │ 3 Transport │ │ │
└─────────────────┘ │ Protocols: │ └──────────────┘
│ - stdio │
│ - SSE (/sse) │
│ - HTTP (/stream) │
│ │
│ /healthz (K8s) │
└──────────────────┘This architecture enables flexible deployment scenarios while maintaining a single codebase.
Three Transport Protocols (Coexisting on Same Port)
The server supports three transport protocols, all accessible through the same port for deployment simplicity:
| Protocol | Endpoint | Use Case |
|---|---|---|
| stdio | Standard Input/Output | Local binary / Docker integration, Claude Desktop direct process launch |
| SSE | /sse + /mcp | Server-Sent Events, remote connections (legacy protocol) |
| Streamable HTTP | /stream | Next-generation MCP remote protocol (recommended) |
Key Design Decision: By registering both SSE and Streamable HTTP to the same port through http.ServeMux, with stdio running as a background goroutine, the implementation achieves maximum flexibility with minimal deployment complexity.
Project Structure
The codebase follows Go best practices for project organization:
loki-mcp/
├── cmd/
│ ├── server/main.go # Entry point: Tool registration + 3 transport startup
│ └── client/main.go # JSON-RPC test client
├── internal/
│ └── handlers/
│ ├── loki.go # Core: 3 Tool implementations (993 lines)
│ └── loki_test.go # Unit tests (261 lines)
├── pkg/
│ └── utils/logger.go # Simple logging utility
├── grafana/
│ └── provisioning/datasources/loki.yaml # Grafana datasource pre-configuration
├── promtail/
│ └── config.yml # Log collection configuration
├── examples/
│ ├── claude-desktop/ # 4 Claude Desktop configuration examples
│ ├── claude-code-commands/ # Slash Command templates
│ ├── simple-sse-client.html # SSE test page
│ └── sse-client.html # Complete SSE client
├── docker-compose.yml # Local 5-service development environment
├── Dockerfile # Multi-stage build
├── Makefile # Build/Test/Run commands
├── go.mod / go.sum
├── run-mcp-server.sh # Startup script
├── test-loki-query.sh # Query test script
├── insert-loki-logs.sh # Test log insertion script
└── README.mdThis structure separates concerns clearly while providing comprehensive examples and testing infrastructure.
Implementation Guide
Phase 1: Basic Framework Setup
1.1 Go Project Initialization
Begin by creating the project directory and initializing Go modules:
mkdir loki-mcp && cd loki-mcp
go mod init github.com/yourname/loki-mcp
go get github.com/mark3labs/mcp-go@v0.32.0This establishes the project foundation and fetches the MCP SDK dependency.
1.2 Understanding the MCP Server Entry Point
The core pattern follows: Create Server → Register Tools → Start Transport Layer
The main entry point (cmd/server/main.go) demonstrates this pattern:
// 1. Create MCP Server instance
s := server.NewMCPServer(
"Loki MCP Server", "0.1.0",
server.WithResourceCapabilities(true, true),
server.WithLogging(),
)
// 2. Register tools (Tool definition + Handler function)
lokiQueryTool := handlers.NewLokiQueryTool()
s.AddTool(lokiQueryTool, handlers.HandleLokiQuery)
// 3. Create transport layers
sseServer := server.NewSSEServer(s,
server.WithSSEEndpoint("/sse"),
server.WithMessageEndpoint("/mcp"),
)
streamableServer := server.NewStreamableHTTPServer(s)
// 4. Unified routing
mux := http.NewServeMux()
mux.Handle("/sse", sseServer)
mux.Handle("/mcp", sseServer)
mux.Handle("/stream", streamableServer)
mux.HandleFunc("/healthz", healthHandler)
// 5. Parallel startup: HTTP + stdio
go http.ListenAndServe(":8080", mux)
go server.ServeStdio(s)
// 6. Graceful shutdown
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stopCritical Design Decisions:
- Three protocols on same port: Simplifies deployment—one port handles all clients
- stdio background execution: Compatible with Claude Desktop's process mode
/healthzendpoint: Adapts to Kubernetes readiness/liveness probes
1.3 Understanding Tool Definition Patterns
Each Tool consists of two components:
- Tool Definition Function (
NewXxxTool()) — Declares parameter schema - Handler Function (
HandleXxx()) — Processes request logic
// Tool Definition: Declare parameters, types, defaults, descriptions
func NewLokiQueryTool() mcp.Tool {
return mcp.NewTool("loki_query",
mcp.WithDescription("Run a query against Grafana Loki"),
mcp.WithString("query", mcp.Required(), mcp.Description("LogQL query string")),
mcp.WithString("url", mcp.Description("Loki server URL"), mcp.DefaultString(lokiURL)),
mcp.WithString("start", mcp.Description("Start time (default: 1h ago)")),
mcp.WithNumber("limit", mcp.Description("Max entries (default: 100)")),
mcp.WithString("format", mcp.DefaultString("raw")),
// ... authentication parameters
)
}
// Handler: Extract parameters → Build request → Call Loki API → Format output
func HandleLokiQuery(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := request.GetArguments()
// ... processing logic
return mcp.NewToolResultText(formattedResult), nil
}This separation enables clean code organization and facilitates testing.
Phase 2: Core Logic Implementation
2.1 Request Processing Flow
The request handling follows a clear pipeline:
Parameter Extraction → Environment Variable Fallback → Time Parsing → URL Building → HTTP Request → Response Parsing → Formatted OutputEach stage builds upon the previous, creating a robust processing chain.
2.2 Parameter Extraction Pattern (with Environment Variable Fallback)
Every parameter follows the priority chain: Request Parameter > Environment Variable > Default Value
// Unified pattern: Check request parameter first, then environment variable
var lokiURL string
if urlArg, ok := args["url"].(string); ok && urlArg != "" {
lokiURL = urlArg
} else {
lokiURL = os.Getenv("LOKI_URL")
if lokiURL == "" {
lokiURL = "http://localhost:3100"
}
}Environment variable inventory:
| Variable | Purpose | Default Value |
|---|---|---|
LOKI_URL | Loki address | http://localhost:3100 |
LOKI_ORG_ID | Tenant ID | Empty |
LOKI_USERNAME | Basic Auth username | Empty |
LOKI_PASSWORD | Basic Auth password | Empty |
LOKI_TOKEN | Bearer Token | Empty |
PORT | Service port | 8080 |
This fallback mechanism provides deployment flexibility across different environments.
2.3 Time Parsing Implementation
The parseTime function supports multiple input formats, attempting them sequentially:
func parseTime(timeStr string) (time.Time, error) {
// 1. "now" keyword
if timeStr == "now" { return time.Now(), nil }
// 2. Relative time: "-1h", "-30m"
if timeStr[0] == '-' {
duration, err := time.ParseDuration(timeStr)
if err == nil { return time.Now().Add(duration), nil }
}
// 3. RFC3339: "2024-01-15T10:30:45Z"
// 4. ISO variants: "2006-01-02T15:04:05", "2006-01-02 15:04:05"
// 5. Date only: "2006-01-02"
}This flexible parsing accommodates various user input styles while maintaining precision.
2.4 URL Building
The URL construction handles various base URL formats intelligently:
func buildLokiQueryURL(baseURL, query string, start, end int64, limit int) (string, error) {
u, _ := url.Parse(baseURL)
// Path normalization: avoid duplicate concatenation
if !strings.Contains(u.Path, "loki/api/v1") {
u.Path = "/loki/api/v1/query_range"
}
// Query parameters
q := u.Query()
q.Set("query", query) // LogQL
q.Set("start", fmt.Sprintf("%d", start)) // Unix seconds
q.Set("end", fmt.Sprintf("%d", end))
q.Set("limit", fmt.Sprintf("%d", limit))
u.RawQuery = q.Encode()
return u.String(), nil
}This approach prevents common URL construction errors and ensures consistent API endpoint formatting.
2.5 Authentication Mechanism
The implementation supports three-tier authentication with clear priority:
Bearer Token > Basic Auth > No Authentication
if token != "" {
req.Header.Add("Authorization", "Bearer "+token)
} else if username != "" || password != "" {
req.SetBasicAuth(username, password)
}
// Multi-tenant isolation (always add if present)
if orgID != "" {
req.Header.Add("X-Scope-OrgID", orgID)
}This hierarchy accommodates various deployment scenarios from simple local testing to production multi-tenant environments.
2.6 Response Data Structure
The Loki response follows a well-defined structure:
type LokiResult struct {
Status string `json:"status"` // "success" | "error"
Data LokiData `json:"data"`
Error string `json:"error,omitempty"`
}
type LokiData struct {
ResultType string `json:"resultType"` // "streams"
Result []LokiEntry `json:"result"`
}
type LokiEntry struct {
Stream map[string]string `json:"stream"` // Labels: {job: "xx", pod: "xx"}
Values [][]string `json:"values"` // [[nanosecond timestamp, log line], ...]
}Understanding this structure proves essential for proper response handling and formatting.
2.7 Three Output Formats
The server supports three output formats to accommodate different use cases:
| Format | Purpose | Example |
|---|---|---|
raw (default) | AI parsing friendly, most compact | 2024-01-15T10:30:45Z {job=api} Request received |
json | Programmatic processing | Complete JSON structure |
text | Human readable | Numbered Stream + timestamped log lines |
The default raw format optimizes for AI consumption while maintaining human readability.
2.8 Known Bug and Fix
Timestamp Year 2262 Bug: Early implementations used time.Unix(ts, 0) which treated nanosecond timestamps as seconds, resulting in dates around year 2262.
Fix: Use time.Unix(0, int64(ts)) — first parameter as 0 seconds, second as nanoseconds.
This common pitfall highlights the importance of careful timestamp handling when working with Loki's nanosecond-precision timestamps.
Phase 3: Dockerization and Local Environment
3.1 Dockerfile (Multi-Stage Build)
# Stage 1: Compilation
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download # Utilize cache layer
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o loki-mcp-server ./cmd/server
# Stage 2: Runtime (minimal image)
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/loki-mcp-server .
EXPOSE 8080
ENTRYPOINT ["./loki-mcp-server"]Key Points:
CGO_ENABLED=0: Static linking, no glibc dependency- Copy
go.mod/go.sumfirst →go mod download: Leverages Docker layer caching for faster builds - Final image based on
alpine:latest: Minimizes attack surface
3.2 Docker Compose (5-Service Complete Environment)
services:
loki-mcp-server: # MCP Server :8080
depends_on:
loki:
condition: service_healthy # Wait for Loki readiness
loki: # Log storage :3100
healthcheck: # /ready endpoint check
test: ["CMD-SHELL", "wget -q --spider http://localhost:3100/ready || exit 1"]
grafana: # Visualization :3000
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning # Pre-configured datasources
promtail: # Log collection
volumes:
- /var/log:/var/log # Collect host logs
- /var/run/docker.sock:/var/run/docker.sock # Collect container logs
log-generator: # Test log generator
command: | # Generate INFO/ERROR logs every 5 seconds
while true; do echo "INFO: ..."; sleep 5; doneService Dependency Chain: log-generator → promtail → loki → loki-mcp-server, with Grafana operating independently.
Phase 4: Testing Strategy
4.1 Unit Testing
Focus testing on timestamp parsing (high bug-risk area):
func TestFormatLokiResults_NoYear2262Bug(t *testing.T) {
testCases := []struct {
name string
timestampNs string
expectedYear string
}{
{"Current", "1705312245000000000", "2024"}, // 2024-01-15
{"Recent", "1700000000000000000", "2023"}, // 2023-11-14
{"Future", "1800000000000000000", "2027"}, // 2027-01-11
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Build LokiResult → formatLokiResults → Assert year
})
}
}Test Case Coverage:
- Normal timestamp parsing
- Multiple log entry timestamps
- Illegal timestamp fallback
- Empty result handling
- Current time regression
- Year 2262 Bug regression (table-driven, 3 time points)
4.2 Running Tests
make test # All tests
go test -coverprofile=coverage.out ./... # With coverage
go tool cover -func=coverage.out # View coverage
go test -race ./... # Race detection4.3 Integration Test Scripts
# Insert test logs
./insert-loki-logs.sh --num 20 --job "custom-job" --app "my-app"
# Query verification
./test-loki-query.sh '{job="varlogs"}'
./test-loki-query.sh '{job="varlogs"} |= "ERROR"' '-1h' 'now' 50These scripts enable rapid validation of the complete system.
Phase 5: Deployment and Client Integration
5.1 Deployment Method Selection
| Method | Scenario | Command |
|---|---|---|
| Local binary | Development debugging | make run |
| Docker single container | Existing Loki instance | docker run -p 8080:8080 -e LOKI_URL=... loki-mcp-server |
| Docker Compose | Complete local environment | docker-compose up --build |
| K8s Deployment | Production environment | See manifest below |
| Remote URL | Team sharing | https://loki-mcp.loki.com/stream |
5.2 Kubernetes Deployment Recommendations
apiVersion: apps/v1
kind: Deployment
metadata:
name: loki-mcp-server
spec:
replicas: 2 # Stateless, horizontally scalable
template:
spec:
containers:
- name: loki-mcp-server
image: loki-mcp-server:v0.1.0 # Fixed version
ports:
- containerPort: 8080
env:
- name: LOKI_URL
value: "http://loki-gateway.monitoring:3100"
- name: LOKI_TOKEN
valueFrom:
secretKeyRef:
name: loki-auth
key: token
readinessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 30
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128MiThis configuration ensures high availability while maintaining resource efficiency.
5.3 Client Integration
Claude Code (Recommended: Streamable HTTP):
claude mcp add --transport http --scope user loki https://loki-mcp.loki.com/streamClaude Desktop (Local Docker):
{
"mcpServers": {
"loki": {
"command": "docker",
"args": ["run", "--rm", "-i", "-e", "LOKI_URL=http://host.docker.internal:3100", "loki-mcp-server:latest"]
}
}
}Cursor:
{
"mcpServers": {
"loki": {
"command": "docker",
"args": ["run", "--rm", "-i", "-e", "LOKI_URL=http://host.docker.internal:3100", "loki-mcp-server:latest"]
}
}
}Development Command Reference
# Build
make build # Compile local binary
make build-linux # Cross-compile Linux (amd64)
docker build -t loki-mcp-server . # Build image
# Run
make run # Local execution
docker-compose up --build # Complete local environment
# Test
make test # Unit tests
go test -race ./... # Race detection
./test-loki-query.sh # Integration test
./insert-loki-logs.sh # Insert test data
# Dependencies
make deps # Download dependencies
make tidy # Organize go.mod
# Cleanup
make clean # Delete binary
docker-compose down -v # Clean containers and volumesCode Standards
Go Standards
- Format with
gofmt/goimports - Error handling:
fmt.Errorf("context: %w", err)wrapping - Define environment variable names with
const, avoid hardcoded strings - HTTP clients must set
Timeout(currently 30s) - Use
context.Contextfor request context propagation
MCP Tool Development Standards
- Tool names use
snake_case(e.g.,loki_query) - Mark required parameters with
mcp.Required() - Every parameter requires
mcp.Description()including default value explanation - Environment variable fallback must implement in Handler (not just Tool definition)
- Return
mcp.NewToolResultText()as standard response
Adding New Tools Template
For adding new tools (e.g., loki_series), follow these steps:
In
internal/handlers/loki.goadd:NewLokiSeriesTool()— Define parametersHandleLokiSeries()— Implement logicbuildLokiSeriesURL()— URL constructionexecuteLokiSeriesQuery()— HTTP requestformatLokiSeriesResults()— Output formatting
Register in
cmd/server/main.go:seriesTool := handlers.NewLokiSeriesTool() s.AddTool(seriesTool, handlers.HandleLokiSeries)- Add tests in
internal/handlers/loki_test.go
Operations Considerations
Observability
/healthzendpoint returnsok, compatible with Kubernetes probes- Service is stateless, no persistent storage required
- Startup logs output all endpoint addresses
- Recommendation: Add Prometheus metrics endpoint (
/metrics) for production
Security
- Authentication information injected via environment variables, not written to image layers
- Bearer Token takes priority over Basic Auth
- Multi-tenant isolation through
X-Scope-OrgIDheader - HTTP timeout of 30s prevents slow queries from blocking
Extension Directions
| Direction | Description |
|---|---|
Add /metrics | Prometheus metrics exposure |
Add loki_series Tool | Query series metadata |
Add loki_stats Tool | Query ingester statistics |
| Extract authentication parameters to common function | Eliminate duplicate code across 3 Handlers |
| Add request logging middleware | Log each Tool call's query and duration |
| Support TLS | HTTPS termination or certificate configuration |
| Add rate limiting | Prevent AI from excessive querying |
Learning Path Recommendation
Day 1: Get it running
├── docker-compose up --build
├── Access Grafana :3000 to understand Loki data structure
├── ./insert-loki-logs.sh to insert test data
└── ./test-loki-query.sh to verify queries
Day 2: Understand entry point
├── cmd/server/main.go (100 lines) — MCP registration + transport layer
├── Understand mcp-go SDK's AddTool pattern
└── Understand three-protocol same-port architecture
Day 3: Deep dive into core
├── internal/handlers/loki.go (993 lines)
├── Parameter extraction → environment variable fallback pattern
├── parseTime multi-format time parsing
├── buildLokiQueryURL path normalization
├── executeLokiQuery HTTP + authentication
└── formatLokiResults three output formats
Day 4: Testing and bugs
├── internal/handlers/loki_test.go
├── Nanosecond timestamp year 2262 bug causes and fixes
└── Go table-driven testing style
Day 5: Docker + Deployment
├── Dockerfile multi-stage build
├── docker-compose.yml service orchestration
├── K8s deployment manifest design
└── Client integration configuration
Day 6: Hands-on extension
├── Try adding loki_series Tool
├── Extract authentication parameters to common function (eliminate duplication)
└── Add /metrics endpointCommon Issues and Solutions
Connection Failures
- Check configuration with
claude mcp get loki - Verify network connectivity
- Check HTTPS certificates
Query Returns No Results
- Confirm Loki has data for the corresponding time range
- Check
org_idin multi-tenant scenarios - Use
loki_label_namesfirst to check available labels
Docker Environment Issues
- Loki requires startup time, wait for healthcheck to pass
- On Mac, use
host.docker.internalfor Docker accessing host docker-compose down -vcleans data for fresh start
Timestamp Display Anomalies
- Confirm using
time.Unix(0, int64(ns))nottime.Unix(ns, 0) - Loki returns nanosecond timestamps, not seconds
Conclusion
The Loki MCP Server project demonstrates how to bridge traditional observability tools with modern AI assistants. By exposing log query capabilities through natural language interfaces, it significantly lowers the barrier to effective log analysis.
The implementation showcases best practices in Go development, including clean architecture, comprehensive testing, flexible deployment options, and thoughtful error handling. The three-protocol support ensures compatibility with various client environments while maintaining a single codebase.
For teams already invested in the Grafana Loki ecosystem, this project provides a straightforward path to AI-enhanced operations without requiring infrastructure changes. The modular design also facilitates future extensions as requirements evolve.
Whether you're looking to improve your team's operational efficiency or explore MCP server development patterns, this project offers valuable insights and a solid foundation for building similar integrations.