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:

LayerTechnologyVersion
LanguageGo1.24+
MCP SDKgithub.com/mark3labs/mcp-gov0.32.0
Log StorageGrafana Loki2.9.0
Log CollectionPromtail2.9.0
VisualizationGrafanaLatest
ContainerizationDocker + 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:

ToolPurposeLoki API Endpoint
loki_queryExecute LogQL queries/loki/api/v1/query_range
loki_label_namesRetrieve all label names/loki/api/v1/labels
loki_label_valuesGet 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:

ProtocolEndpointUse Case
stdioStandard Input/OutputLocal binary / Docker integration, Claude Desktop direct process launch
SSE/sse + /mcpServer-Sent Events, remote connections (legacy protocol)
Streamable HTTP/streamNext-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.md

This 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.0

This 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)
<-stop

Critical Design Decisions:

  • Three protocols on same port: Simplifies deployment—one port handles all clients
  • stdio background execution: Compatible with Claude Desktop's process mode
  • /healthz endpoint: Adapts to Kubernetes readiness/liveness probes

1.3 Understanding Tool Definition Patterns

Each Tool consists of two components:

  1. Tool Definition Function (NewXxxTool()) — Declares parameter schema
  2. 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 Output

Each 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:

VariablePurposeDefault Value
LOKI_URLLoki addresshttp://localhost:3100
LOKI_ORG_IDTenant IDEmpty
LOKI_USERNAMEBasic Auth usernameEmpty
LOKI_PASSWORDBasic Auth passwordEmpty
LOKI_TOKENBearer TokenEmpty
PORTService port8080

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:

FormatPurposeExample
raw (default)AI parsing friendly, most compact2024-01-15T10:30:45Z {job=api} Request received
jsonProgrammatic processingComplete JSON structure
textHuman readableNumbered 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.sum first → 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; done

Service 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 detection

4.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' 50

These scripts enable rapid validation of the complete system.

Phase 5: Deployment and Client Integration

5.1 Deployment Method Selection

MethodScenarioCommand
Local binaryDevelopment debuggingmake run
Docker single containerExisting Loki instancedocker run -p 8080:8080 -e LOKI_URL=... loki-mcp-server
Docker ComposeComplete local environmentdocker-compose up --build
K8s DeploymentProduction environmentSee manifest below
Remote URLTeam sharinghttps://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: 128Mi

This 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/stream

Claude 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 volumes

Code 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.Context for 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:

  1. In internal/handlers/loki.go add:

    • NewLokiSeriesTool() — Define parameters
    • HandleLokiSeries() — Implement logic
    • buildLokiSeriesURL() — URL construction
    • executeLokiSeriesQuery() — HTTP request
    • formatLokiSeriesResults() — Output formatting
  2. Register in cmd/server/main.go:

    seriesTool := handlers.NewLokiSeriesTool()
    s.AddTool(seriesTool, handlers.HandleLokiSeries)
  3. Add tests in internal/handlers/loki_test.go

Operations Considerations

Observability

  • /healthz endpoint returns ok, 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-OrgID header
  • HTTP timeout of 30s prevents slow queries from blocking

Extension Directions

DirectionDescription
Add /metricsPrometheus metrics exposure
Add loki_series ToolQuery series metadata
Add loki_stats ToolQuery ingester statistics
Extract authentication parameters to common functionEliminate duplicate code across 3 Handlers
Add request logging middlewareLog each Tool call's query and duration
Support TLSHTTPS termination or certificate configuration
Add rate limitingPrevent 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 endpoint

Common Issues and Solutions

Connection Failures

  1. Check configuration with claude mcp get loki
  2. Verify network connectivity
  3. Check HTTPS certificates

Query Returns No Results

  1. Confirm Loki has data for the corresponding time range
  2. Check org_id in multi-tenant scenarios
  3. Use loki_label_names first to check available labels

Docker Environment Issues

  1. Loki requires startup time, wait for healthcheck to pass
  2. On Mac, use host.docker.internal for Docker accessing host
  3. docker-compose down -v cleans data for fresh start

Timestamp Display Anomalies

  • Confirm using time.Unix(0, int64(ns)) not time.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.