From 74f8507342c0116e8344467574f9550da290f0dc Mon Sep 17 00:00:00 2001 From: Cody Rigney Date: Wed, 10 Sep 2025 15:54:50 -0400 Subject: [PATCH 1/2] Add telemetry for initialize. --- .../internal/interceptors/telemetry.go | 5 +++ .../internal/telemetry/telemetry.go | 40 +++++++++++++++++++ docs/telemetry/README.md | 5 +++ go.mod | 4 +- 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/cmd/docker-mcp/internal/interceptors/telemetry.go b/cmd/docker-mcp/internal/interceptors/telemetry.go index e2f483a4b..567ac7b5f 100644 --- a/cmd/docker-mcp/internal/interceptors/telemetry.go +++ b/cmd/docker-mcp/internal/interceptors/telemetry.go @@ -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) diff --git a/cmd/docker-mcp/internal/telemetry/telemetry.go b/cmd/docker-mcp/internal/telemetry/telemetry.go index c522a3a33..200246627 100644 --- a/cmd/docker-mcp/internal/telemetry/telemetry.go +++ b/cmd/docker-mcp/internal/telemetry/telemetry.go @@ -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" @@ -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 @@ -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")) @@ -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{ @@ -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 { diff --git a/docs/telemetry/README.md b/docs/telemetry/README.md index 5dd47f61a..d4d61f241 100644 --- a/docs/telemetry/README.md +++ b/docs/telemetry/README.md @@ -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: @@ -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 diff --git a/go.mod b/go.mod index e0577fc21..54238cd05 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 From 8d25a9ade9b014804090b3a02b2bef51b00c0a63 Mon Sep 17 00:00:00 2001 From: Cody Rigney Date: Thu, 11 Sep 2025 09:49:33 -0400 Subject: [PATCH 2/2] Add intitialize span test. --- .../internal/telemetry/telemetry_test.go | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/cmd/docker-mcp/internal/telemetry/telemetry_test.go b/cmd/docker-mcp/internal/telemetry/telemetry_test.go index 943dfee34..d2b517ea0 100644 --- a/cmd/docker-mcp/internal/telemetry/telemetry_test.go +++ b/cmd/docker-mcp/internal/telemetry/telemetry_test.go @@ -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()