From 9a6bffb04440714dbbaeb4799f2ba721f41a8be0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:50:32 +0000 Subject: [PATCH 1/2] fix(config): add headers field to JSON stdin opentelemetry config The TOML gateway.opentelemetry.headers field had no equivalent in the JSON stdin format. This meant that users of the JSON stdin config path (e.g. gh-aw workflows) could not provide OTLP export headers inline; they had to rely on the OTEL_EXPORTER_OTLP_HEADERS environment variable as a workaround. Changes: - Add Headers field to StdinOpenTelemetryConfig struct - Add 'headers' property to the JSON schema opentelemetryConfig definition - Wire StdinOpenTelemetryConfig.Headers through to TracingConfig.Headers in the stdin converter registered in config_tracing.go - Update TestStdinConverter_OTelConfig to assert Headers is empty when omitted - Add TestStdinConverter_OTelConfig_Headers to verify end-to-end wiring - Update TestLoadFromStdin_OpenTelemetryHeaders to verify headers are accepted and wired through (previously tested that headers were rejected) The docs/CONFIGURATION.md JSON stdin example already showed 'headers' in the opentelemetry block but the schema enforced additionalProperties:false, causing any use of the documented field to fail at runtime. Closes #8461 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- internal/config/config_stdin.go | 5 +++++ internal/config/config_tracing.go | 5 +++-- internal/config/config_tracing_test.go | 21 +++++++++++++++++++ .../config/load_from_stdin_coverage_test.go | 17 +++++++++------ .../schema/mcp-gateway-config.schema.json | 5 +++++ 5 files changed, 45 insertions(+), 8 deletions(-) diff --git a/internal/config/config_stdin.go b/internal/config/config_stdin.go index adad12cb..8ad4fec4 100644 --- a/internal/config/config_stdin.go +++ b/internal/config/config_stdin.go @@ -76,6 +76,11 @@ type StdinOpenTelemetryConfig struct { // Endpoint is the OTLP/HTTP collector URL. MUST be HTTPS. Supports ${VAR} expansion. Endpoint string `json:"endpoint"` + // Headers is a comma-separated list of key=value HTTP headers sent with every OTLP + // export request (e.g. "Authorization=Bearer ${MY_TOKEN}"). Supports ${VAR} expansion. + // Mirrors the TOML gateway.opentelemetry.headers field. + Headers string `json:"headers,omitempty"` + // TraceID is the parent trace ID (32-char lowercase hex, W3C format). Supports ${VAR}. TraceID string `json:"traceId,omitempty"` diff --git a/internal/config/config_tracing.go b/internal/config/config_tracing.go index d7084078..7362cb3c 100644 --- a/internal/config/config_tracing.go +++ b/internal/config/config_tracing.go @@ -92,13 +92,14 @@ func init() { return } otel := stdinCfg.Gateway.OpenTelemetry - logTracingCfg.Printf("Converting OpenTelemetry config from stdin: hasEndpoint=%v, hasServiceName=%v, hasTraceID=%v", - otel.Endpoint != "", otel.ServiceName != "", otel.TraceID != "") + logTracingCfg.Printf("Converting OpenTelemetry config from stdin: hasEndpoint=%v, hasHeaders=%v, hasServiceName=%v, hasTraceID=%v", + otel.Endpoint != "", otel.Headers != "", otel.ServiceName != "", otel.TraceID != "") if cfg.Gateway == nil { cfg.Gateway = &GatewayConfig{} } cfg.Gateway.Tracing = &TracingConfig{ Endpoint: otel.Endpoint, + Headers: otel.Headers, TraceID: otel.TraceID, SpanID: otel.SpanID, ServiceName: otel.ServiceName, diff --git a/internal/config/config_tracing_test.go b/internal/config/config_tracing_test.go index 8e764cf4..8d58a88f 100644 --- a/internal/config/config_tracing_test.go +++ b/internal/config/config_tracing_test.go @@ -378,6 +378,27 @@ func TestStdinConverter_OTelConfig(t *testing.T) { assert.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", cfg.Gateway.Tracing.TraceID) assert.Equal(t, "00f067aa0ba902b7", cfg.Gateway.Tracing.SpanID) assert.Equal(t, "my-service", cfg.Gateway.Tracing.ServiceName) + assert.Empty(t, cfg.Gateway.Tracing.Headers, "Headers must be empty when not provided") +} + +// TestStdinConverter_OTelConfig_Headers verifies that the headers field in the stdin +// JSON opentelemetry config is wired through to TracingConfig.Headers. +func TestStdinConverter_OTelConfig_Headers(t *testing.T) { + cfg := &Config{Gateway: &GatewayConfig{}} + stdinCfg := &StdinConfig{ + Gateway: &StdinGatewayConfig{ + OpenTelemetry: &StdinOpenTelemetryConfig{ + Endpoint: "https://otel.example.com", + Headers: "Authorization=Bearer token123,X-Custom=value", + ServiceName: "my-service", + }, + }, + } + applyStdinConverters(cfg, stdinCfg) + + require.NotNil(t, cfg.Gateway.Tracing, "TracingConfig must be populated by the stdin converter") + assert.Equal(t, "Authorization=Bearer token123,X-Custom=value", cfg.Gateway.Tracing.Headers, + "headers from stdin JSON must be copied to TracingConfig.Headers") } // TestStdinConverter_OTelConfig_DefaultsServiceName verifies that the stdin converter diff --git a/internal/config/load_from_stdin_coverage_test.go b/internal/config/load_from_stdin_coverage_test.go index a7acacb8..2a552491 100644 --- a/internal/config/load_from_stdin_coverage_test.go +++ b/internal/config/load_from_stdin_coverage_test.go @@ -182,8 +182,9 @@ func TestLoadFromStdin_ValidateGatewayConfigError_AllZeroTraceId(t *testing.T) { } // TestLoadFromStdin_OpenTelemetryHeaders verifies that gateway.opentelemetry.headers -// is rejected by schema validation (spec §4.1.3.7 v1.14.0 Breaking Change). -// Authentication headers must be provided via OTEL_EXPORTER_OTLP_HEADERS env var. +// is accepted by schema validation and wired through to TracingConfig.Headers. +// The field was added in spec §4.1.3.6 to allow header-based OTLP auth in JSON stdin +// configs (mirrors the TOML gateway.opentelemetry.headers field). func TestLoadFromStdin_OpenTelemetryHeaders(t *testing.T) { jsonConfig := `{ "mcpServers": { @@ -197,18 +198,22 @@ func TestLoadFromStdin_OpenTelemetryHeaders(t *testing.T) { "agentId": "test-key", "opentelemetry": { "endpoint": "https://otel-collector.example.com", - "headers": "X-Test=value" + "headers": "Authorization=Bearer mytoken" } } }` + var cfg *Config var loadErr error stdinFromString(t, jsonConfig, func() { - _, loadErr = LoadFromStdin() + cfg, loadErr = LoadFromStdin() }) - require.Error(t, loadErr, "headers field in opentelemetry config must be rejected") - assert.ErrorContains(t, loadErr, "headers") + require.NoError(t, loadErr, "headers field in opentelemetry config must be accepted") + require.NotNil(t, cfg) + require.NotNil(t, cfg.Gateway.Tracing) + assert.Equal(t, "Authorization=Bearer mytoken", cfg.Gateway.Tracing.Headers, + "headers must be wired through to TracingConfig.Headers") } // TestNormalizeLocalType_NonObjectServerValue covers the continue branch (line 616-617) diff --git a/internal/config/schema/mcp-gateway-config.schema.json b/internal/config/schema/mcp-gateway-config.schema.json index 101d5ed0..86c2b95a 100644 --- a/internal/config/schema/mcp-gateway-config.schema.json +++ b/internal/config/schema/mcp-gateway-config.schema.json @@ -456,6 +456,11 @@ "description": "Logical service name reported in the 'service.name' resource attribute of all emitted spans. Identifies the gateway in the tracing backend. Defaults to 'mcp-gateway' when not specified.", "minLength": 1, "default": "mcp-gateway" + }, + "headers": { + "type": "string", + "description": "Comma-separated list of key=value HTTP headers to send with every OTLP export request (e.g. 'Authorization=Bearer ${MY_TOKEN}'). Supports variable expressions using '${VARIABLE_NAME}' syntax.", + "minLength": 1 } }, "required": [ From a826fc76f6777aa107ee9e5598c84273f01f4efd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:28:07 +0000 Subject: [PATCH 2/2] revert: remove headers from JSON stdin opentelemetry config per spec v1.14.0 --- internal/config/config_stdin.go | 5 ----- internal/config/config_tracing.go | 5 ++--- internal/config/config_tracing_test.go | 21 ------------------- .../config/load_from_stdin_coverage_test.go | 17 ++++++--------- .../schema/mcp-gateway-config.schema.json | 5 ----- 5 files changed, 8 insertions(+), 45 deletions(-) diff --git a/internal/config/config_stdin.go b/internal/config/config_stdin.go index 8ad4fec4..adad12cb 100644 --- a/internal/config/config_stdin.go +++ b/internal/config/config_stdin.go @@ -76,11 +76,6 @@ type StdinOpenTelemetryConfig struct { // Endpoint is the OTLP/HTTP collector URL. MUST be HTTPS. Supports ${VAR} expansion. Endpoint string `json:"endpoint"` - // Headers is a comma-separated list of key=value HTTP headers sent with every OTLP - // export request (e.g. "Authorization=Bearer ${MY_TOKEN}"). Supports ${VAR} expansion. - // Mirrors the TOML gateway.opentelemetry.headers field. - Headers string `json:"headers,omitempty"` - // TraceID is the parent trace ID (32-char lowercase hex, W3C format). Supports ${VAR}. TraceID string `json:"traceId,omitempty"` diff --git a/internal/config/config_tracing.go b/internal/config/config_tracing.go index 7362cb3c..d7084078 100644 --- a/internal/config/config_tracing.go +++ b/internal/config/config_tracing.go @@ -92,14 +92,13 @@ func init() { return } otel := stdinCfg.Gateway.OpenTelemetry - logTracingCfg.Printf("Converting OpenTelemetry config from stdin: hasEndpoint=%v, hasHeaders=%v, hasServiceName=%v, hasTraceID=%v", - otel.Endpoint != "", otel.Headers != "", otel.ServiceName != "", otel.TraceID != "") + logTracingCfg.Printf("Converting OpenTelemetry config from stdin: hasEndpoint=%v, hasServiceName=%v, hasTraceID=%v", + otel.Endpoint != "", otel.ServiceName != "", otel.TraceID != "") if cfg.Gateway == nil { cfg.Gateway = &GatewayConfig{} } cfg.Gateway.Tracing = &TracingConfig{ Endpoint: otel.Endpoint, - Headers: otel.Headers, TraceID: otel.TraceID, SpanID: otel.SpanID, ServiceName: otel.ServiceName, diff --git a/internal/config/config_tracing_test.go b/internal/config/config_tracing_test.go index 8d58a88f..8e764cf4 100644 --- a/internal/config/config_tracing_test.go +++ b/internal/config/config_tracing_test.go @@ -378,27 +378,6 @@ func TestStdinConverter_OTelConfig(t *testing.T) { assert.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", cfg.Gateway.Tracing.TraceID) assert.Equal(t, "00f067aa0ba902b7", cfg.Gateway.Tracing.SpanID) assert.Equal(t, "my-service", cfg.Gateway.Tracing.ServiceName) - assert.Empty(t, cfg.Gateway.Tracing.Headers, "Headers must be empty when not provided") -} - -// TestStdinConverter_OTelConfig_Headers verifies that the headers field in the stdin -// JSON opentelemetry config is wired through to TracingConfig.Headers. -func TestStdinConverter_OTelConfig_Headers(t *testing.T) { - cfg := &Config{Gateway: &GatewayConfig{}} - stdinCfg := &StdinConfig{ - Gateway: &StdinGatewayConfig{ - OpenTelemetry: &StdinOpenTelemetryConfig{ - Endpoint: "https://otel.example.com", - Headers: "Authorization=Bearer token123,X-Custom=value", - ServiceName: "my-service", - }, - }, - } - applyStdinConverters(cfg, stdinCfg) - - require.NotNil(t, cfg.Gateway.Tracing, "TracingConfig must be populated by the stdin converter") - assert.Equal(t, "Authorization=Bearer token123,X-Custom=value", cfg.Gateway.Tracing.Headers, - "headers from stdin JSON must be copied to TracingConfig.Headers") } // TestStdinConverter_OTelConfig_DefaultsServiceName verifies that the stdin converter diff --git a/internal/config/load_from_stdin_coverage_test.go b/internal/config/load_from_stdin_coverage_test.go index 2a552491..a7acacb8 100644 --- a/internal/config/load_from_stdin_coverage_test.go +++ b/internal/config/load_from_stdin_coverage_test.go @@ -182,9 +182,8 @@ func TestLoadFromStdin_ValidateGatewayConfigError_AllZeroTraceId(t *testing.T) { } // TestLoadFromStdin_OpenTelemetryHeaders verifies that gateway.opentelemetry.headers -// is accepted by schema validation and wired through to TracingConfig.Headers. -// The field was added in spec §4.1.3.6 to allow header-based OTLP auth in JSON stdin -// configs (mirrors the TOML gateway.opentelemetry.headers field). +// is rejected by schema validation (spec §4.1.3.7 v1.14.0 Breaking Change). +// Authentication headers must be provided via OTEL_EXPORTER_OTLP_HEADERS env var. func TestLoadFromStdin_OpenTelemetryHeaders(t *testing.T) { jsonConfig := `{ "mcpServers": { @@ -198,22 +197,18 @@ func TestLoadFromStdin_OpenTelemetryHeaders(t *testing.T) { "agentId": "test-key", "opentelemetry": { "endpoint": "https://otel-collector.example.com", - "headers": "Authorization=Bearer mytoken" + "headers": "X-Test=value" } } }` - var cfg *Config var loadErr error stdinFromString(t, jsonConfig, func() { - cfg, loadErr = LoadFromStdin() + _, loadErr = LoadFromStdin() }) - require.NoError(t, loadErr, "headers field in opentelemetry config must be accepted") - require.NotNil(t, cfg) - require.NotNil(t, cfg.Gateway.Tracing) - assert.Equal(t, "Authorization=Bearer mytoken", cfg.Gateway.Tracing.Headers, - "headers must be wired through to TracingConfig.Headers") + require.Error(t, loadErr, "headers field in opentelemetry config must be rejected") + assert.ErrorContains(t, loadErr, "headers") } // TestNormalizeLocalType_NonObjectServerValue covers the continue branch (line 616-617) diff --git a/internal/config/schema/mcp-gateway-config.schema.json b/internal/config/schema/mcp-gateway-config.schema.json index 86c2b95a..101d5ed0 100644 --- a/internal/config/schema/mcp-gateway-config.schema.json +++ b/internal/config/schema/mcp-gateway-config.schema.json @@ -456,11 +456,6 @@ "description": "Logical service name reported in the 'service.name' resource attribute of all emitted spans. Identifies the gateway in the tracing backend. Defaults to 'mcp-gateway' when not specified.", "minLength": 1, "default": "mcp-gateway" - }, - "headers": { - "type": "string", - "description": "Comma-separated list of key=value HTTP headers to send with every OTLP export request (e.g. 'Authorization=Bearer ${MY_TOKEN}'). Supports variable expressions using '${VARIABLE_NAME}' syntax.", - "minLength": 1 } }, "required": [