Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/docker-mcp/internal/interceptors/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ func TelemetryMiddleware() mcp.Middleware {
var tracked bool

switch method {
case "initialize":
params := req.GetParams().(*mcp.InitializeParams)
ctx, span = telemetry.StartInitializeSpan(ctx)
telemetry.RecordInitialize(ctx, params)
tracked = true
case "tools/list":
ctx, span = telemetry.StartListSpan(ctx, "tools")
telemetry.RecordListTools(ctx)
Expand Down
40 changes: 40 additions & 0 deletions cmd/docker-mcp/internal/telemetry/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"

"github.com/modelcontextprotocol/go-sdk/mcp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
Expand Down Expand Up @@ -41,6 +42,9 @@ var (
// GatewayStartCounter tracks gateway starts
GatewayStartCounter metric.Int64Counter

// InitializeCounter tracks initialize calls
InitializeCounter metric.Int64Counter

// ListToolsCounter tracks list tools calls
ListToolsCounter metric.Int64Counter

Expand Down Expand Up @@ -134,6 +138,16 @@ func Init() {
}
}

InitializeCounter, err = meter.Int64Counter("mcp.initialize",
metric.WithDescription("Number of initialize calls"),
metric.WithUnit("1"))
if err != nil {
// Log error but don't fail
if os.Getenv("DOCKER_MCP_TELEMETRY_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "[MCP-TELEMETRY] Error creating initialize counter: %v\n", err)
}
}

ListToolsCounter, err = meter.Int64Counter("mcp.list.tools",
metric.WithDescription("Number of list tools calls"),
metric.WithUnit("1"))
Expand Down Expand Up @@ -401,6 +415,13 @@ func StartPromptSpan(ctx context.Context, promptName string, attrs ...attribute.
trace.WithSpanKind(trace.SpanKindClient))
}

// StartListSpan starts a new span for a list operation (tools, prompts, resources)
func StartInitializeSpan(ctx context.Context, attrs ...attribute.KeyValue) (context.Context, trace.Span) {
return tracer.Start(ctx, "mcp.initialize",
trace.WithAttributes(attrs...),
trace.WithSpanKind(trace.SpanKindServer))
}

// StartListSpan starts a new span for a list operation (tools, prompts, resources)
func StartListSpan(ctx context.Context, listType string, attrs ...attribute.KeyValue) (context.Context, trace.Span) {
allAttrs := append([]attribute.KeyValue{
Expand Down Expand Up @@ -466,6 +487,25 @@ func RecordGatewayStart(ctx context.Context, transportMode string) {
))
}

func RecordInitialize(ctx context.Context, params *mcp.InitializeParams) {
if InitializeCounter == nil {
if os.Getenv("DOCKER_MCP_TELEMETRY_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "[MCP-TELEMETRY] WARNING: InitializeCounter is nil - metrics not initialized\n")
}
return // Telemetry not initialized
}

if os.Getenv("DOCKER_MCP_TELEMETRY_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "[MCP-TELEMETRY] Initialize called - adding to counter\n")
}

InitializeCounter.Add(ctx, 1,
metric.WithAttributes(
attribute.String("mcp.client.name", params.ClientInfo.Name),
attribute.String("mcp.client.version", params.ClientInfo.Version),
))
}

// RecordListTools records a list tools call
func RecordListTools(ctx context.Context) {
if ListToolsCounter == nil {
Expand Down
38 changes: 38 additions & 0 deletions cmd/docker-mcp/internal/telemetry/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,44 @@ func TestInitialization(t *testing.T) {
})
}

func TestStartInitializeSpan(t *testing.T) {
spanRecorder, _ := setupTestTelemetry(t)
Init()

ctx := context.Background()
clientName := "claude-ai"
clientVersion := "1.0.0"

// Start a tool call span
newCtx, span := StartInitializeSpan(ctx,
attribute.String("mcp.client.name", clientName),
attribute.String("mcp.client.version", clientVersion),
)

// Verify context was updated
assert.NotEqual(t, ctx, newCtx, "should return new context with span")

// End the span
span.End()

// Verify span attributes
spans := spanRecorder.Ended()
require.Len(t, spans, 1)

recordedSpan := spans[0]
assert.Equal(t, "mcp.initialize", recordedSpan.Name())

// Check attributes
attrs := recordedSpan.Attributes()
attrMap := make(map[string]string)
for _, attr := range attrs {
attrMap[string(attr.Key)] = attr.Value.AsString()
}

assert.Equal(t, clientName, attrMap["mcp.client.name"])
assert.Equal(t, clientVersion, attrMap["mcp.client.version"])
}

func TestStartToolCallSpan(t *testing.T) {
spanRecorder, _ := setupTestTelemetry(t)
Init()
Expand Down
5 changes: 5 additions & 0 deletions docs/telemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ AI Client (e.g., Claude Code)

#### Startup and Lifecycle
- **`mcp.gateway.starts`** - Records when the gateway starts, including transport mode (stdio/sse/streaming)
- **`mcp.initialize`** - Records when the host initializes a connection with the gateway

#### Discovery Operations
When the gateway connects to MCP servers, it discovers their capabilities:
Expand Down Expand Up @@ -115,6 +116,10 @@ All metrics include contextual attributes for filtering and aggregation:
- **`mcp.server.name`** - Name of the MCP server handling the operation
- **`mcp.server.type`** - Type of server (docker, stdio, sse, unknown)

### Initialize Attributes
- **`mcp.client.name`** - Name of the connecting client (e.g. `claude-ai`)
- **`mcp.client.version`** - Version of the connecting client (e.g. `0.1.0`)

### Operation-Specific Attributes
- **`mcp.tool.name`** - Name of the tool being called
- **`mcp.prompt.name`** - Name of the prompt being retrieved
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/mikefarah/yq/v4 v4.45.4
github.com/modelcontextprotocol/go-sdk v0.2.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/pkg/errors v0.9.1
github.com/sigstore/cosign/v2 v2.5.0
github.com/sigstore/sigstore v1.9.5
Expand Down Expand Up @@ -113,8 +115,6 @@ require (
github.com/oklog/ulid v1.3.1 // indirect
github.com/onsi/gomega v1.37.0 // indirect
github.com/open-policy-agent/opa v1.5.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
Expand Down
Loading