diff --git a/internal/auth/header.go b/internal/auth/header.go index f6b0d122..321fb892 100644 --- a/internal/auth/header.go +++ b/internal/auth/header.go @@ -165,16 +165,3 @@ func ExtractSessionID(authHeader string) string { log.Print("Using plain API key as session ID") return authHeader } - -// TruncateSessionID returns a truncated session ID for safe logging (first 8 chars). -// Returns "(none)" for empty session IDs, and appends "..." for truncated values. -// This is useful for logging session IDs without exposing sensitive information. -func TruncateSessionID(sessionID string) string { - if sessionID == "" { - return "(none)" - } - if len(sessionID) <= 8 { - return sessionID - } - return sessionID[:8] + "..." -} diff --git a/internal/auth/header_test.go b/internal/auth/header_test.go index e531789e..311e2d21 100644 --- a/internal/auth/header_test.go +++ b/internal/auth/header_test.go @@ -1,346 +1,445 @@ package auth import ( - "testing" +"testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" +"github.com/stretchr/testify/assert" +"github.com/stretchr/testify/require" - "github.com/github/gh-aw-mcpg/internal/logger/sanitize" +"github.com/github/gh-aw-mcpg/internal/logger/sanitize" ) func TestTruncateSecret(t *testing.T) { - tests := []struct { - name string - input string - want string - }{ - { - name: "Empty string", - input: "", - want: "", - }, - { - name: "Single character", - input: "a", - want: "...", - }, - { - name: "Four characters", - input: "abcd", - want: "...", - }, - { - name: "Five characters", - input: "abcde", - want: "abcd...", - }, - { - name: "Long string", - input: "my-secret-api-key-12345", - want: "my-s...", - }, - { - name: "API key with Bearer prefix", - input: "Bearer my-token-123", - want: "Bear...", - }, - { - name: "Unicode characters", - input: "key-with-émojis-🔑", - want: "key-...", - }, - { - name: "Very long API key", - input: "my-super-long-api-key-with-many-characters-12345678901234567890", - want: "my-s...", - }, - { - name: "Special characters", - input: "key!@#$%^&*()", - want: "key!...", - }, - } +assert := assert.New(t) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := sanitize.TruncateSecret(tt.input) - assert.Equal(t, tt.want, got) - }) - } +tests := []struct { +name string +input string +want string +}{ +{ +name: "Empty string", +input: "", +want: "", +}, +{ +name: "Single character", +input: "a", +want: "...", +}, +{ +name: "Four characters", +input: "abcd", +want: "...", +}, +{ +name: "Five characters", +input: "abcde", +want: "abcd...", +}, +{ +name: "Long string", +input: "my-secret-api-key-12345", +want: "my-s...", +}, +{ +name: "API key with Bearer prefix", +input: "Bearer my-token-123", +want: "Bear...", +}, +{ +name: "Unicode characters", +input: "key-with-émojis-🔑", +want: "key-...", +}, +{ +name: "Very long API key", +input: "my-super-long-api-key-with-many-characters-12345678901234567890", +want: "my-s...", +}, +{ +name: "Special characters", +input: "key!@#$%^&*()", +want: "key!...", +}, +} + +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +got := sanitize.TruncateSecret(tt.input) +assert.Equal(tt.want, got) +}) +} } func TestParseAuthHeader(t *testing.T) { - tests := []struct { - name string - authHeader string - wantAPIKey string - wantAgentID string - wantErr error - }{ - { - name: "Empty header", - authHeader: "", - wantAPIKey: "", - wantAgentID: "", - wantErr: ErrMissingAuthHeader, - }, - { - name: "Plain API key (MCP spec 7.1)", - authHeader: "my-secret-api-key", - wantAPIKey: "my-secret-api-key", - wantAgentID: "my-secret-api-key", - wantErr: nil, - }, - { - name: "Bearer token (backward compatibility)", - authHeader: "Bearer my-token-123", - wantAPIKey: "my-token-123", - wantAgentID: "my-token-123", - wantErr: nil, - }, - { - name: "Agent format", - authHeader: "Agent agent-123", - wantAPIKey: "agent-123", - wantAgentID: "agent-123", - wantErr: nil, - }, - { - name: "Bearer with multiple spaces", - authHeader: "Bearer my-token", - wantAPIKey: " my-token", - wantAgentID: " my-token", - wantErr: nil, - }, - { - name: "Lowercase bearer (not supported)", - authHeader: "bearer my-token", - wantAPIKey: "bearer my-token", - wantAgentID: "bearer my-token", - wantErr: nil, - }, - { - name: "Agent with multiple spaces", - authHeader: "Agent agent-id", - wantAPIKey: " agent-id", - wantAgentID: " agent-id", - wantErr: nil, - }, - { - name: "Whitespace only header", - authHeader: " ", - wantAPIKey: " ", - wantAgentID: " ", - wantErr: nil, - }, - { - name: "API key with special characters", - authHeader: "key!@#$%^&*()", - wantAPIKey: "key!@#$%^&*()", - wantAgentID: "key!@#$%^&*()", - wantErr: nil, - }, - { - name: "Very long API key", - authHeader: "my-super-long-api-key-with-many-characters-12345678901234567890", - wantAPIKey: "my-super-long-api-key-with-many-characters-12345678901234567890", - wantAgentID: "my-super-long-api-key-with-many-characters-12345678901234567890", - wantErr: nil, - }, - { - name: "Bearer with trailing space", - authHeader: "Bearer my-token ", - wantAPIKey: "my-token ", - wantAgentID: "my-token ", - wantErr: nil, - }, - } +assert := assert.New(t) +require := require.New(t) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotAPIKey, gotAgentID, gotErr := ParseAuthHeader(tt.authHeader) +tests := []struct { +name string +authHeader string +wantAPIKey string +wantAgentID string +wantErr error +}{ +{ +name: "Empty header", +authHeader: "", +wantAPIKey: "", +wantAgentID: "", +wantErr: ErrMissingAuthHeader, +}, +{ +name: "Plain API key (MCP spec 7.1)", +authHeader: "my-secret-api-key", +wantAPIKey: "my-secret-api-key", +wantAgentID: "my-secret-api-key", +wantErr: nil, +}, +{ +name: "Bearer token (backward compatibility)", +authHeader: "Bearer my-token-123", +wantAPIKey: "my-token-123", +wantAgentID: "my-token-123", +wantErr: nil, +}, +{ +name: "Agent format", +authHeader: "Agent agent-123", +wantAPIKey: "agent-123", +wantAgentID: "agent-123", +wantErr: nil, +}, +{ +name: "Bearer with multiple spaces", +authHeader: "Bearer my-token", +wantAPIKey: " my-token", +wantAgentID: " my-token", +wantErr: nil, +}, +{ +name: "Lowercase bearer (not supported)", +authHeader: "bearer my-token", +wantAPIKey: "bearer my-token", +wantAgentID: "bearer my-token", +wantErr: nil, +}, +{ +name: "Agent with multiple spaces", +authHeader: "Agent agent-id", +wantAPIKey: " agent-id", +wantAgentID: " agent-id", +wantErr: nil, +}, +{ +name: "Whitespace only header", +authHeader: " ", +wantAPIKey: " ", +wantAgentID: " ", +wantErr: nil, +}, +{ +name: "API key with special characters", +authHeader: "key!@#$%^&*()", +wantAPIKey: "key!@#$%^&*()", +wantAgentID: "key!@#$%^&*()", +wantErr: nil, +}, +{ +name: "Very long API key", +authHeader: "my-super-long-api-key-with-many-characters-12345678901234567890", +wantAPIKey: "my-super-long-api-key-with-many-characters-12345678901234567890", +wantAgentID: "my-super-long-api-key-with-many-characters-12345678901234567890", +wantErr: nil, +}, +{ +name: "Bearer with trailing space", +authHeader: "Bearer my-token ", +wantAPIKey: "my-token ", +wantAgentID: "my-token ", +wantErr: nil, +}, +} - if tt.wantErr != nil { - require.ErrorIs(t, gotErr, tt.wantErr) - } else { - require.NoError(t, gotErr) - } +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +gotAPIKey, gotAgentID, gotErr := ParseAuthHeader(tt.authHeader) - assert.Equal(t, tt.wantAPIKey, gotAPIKey) - assert.Equal(t, tt.wantAgentID, gotAgentID) - }) - } +if tt.wantErr != nil { +require.ErrorIs(gotErr, tt.wantErr) +} else { +require.NoError(gotErr) +} + +assert.Equal(tt.wantAPIKey, gotAPIKey) +assert.Equal(tt.wantAgentID, gotAgentID) +}) +} } func TestValidateAPIKey(t *testing.T) { - tests := []struct { - name string - provided string - expected string - want bool - }{ - { - name: "Matching keys", - provided: "my-secret-key", - expected: "my-secret-key", - want: true, - }, - { - name: "Non-matching keys", - provided: "wrong-key", - expected: "correct-key", - want: false, - }, - { - name: "Empty expected (auth disabled)", - provided: "any-key", - expected: "", - want: true, - }, - { - name: "Empty provided with expected", - provided: "", - expected: "required-key", - want: false, - }, - { - name: "Both empty", - provided: "", - expected: "", - want: true, - }, - { - name: "Case sensitive - should not match", - provided: "My-Secret-Key", - expected: "my-secret-key", - want: false, - }, - { - name: "Keys with whitespace - exact match required", - provided: "key with spaces", - expected: "key with spaces", - want: true, - }, - { - name: "Keys with whitespace - trailing space different", - provided: "my-key ", - expected: "my-key", - want: false, - }, - } +assert := assert.New(t) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := ValidateAPIKey(tt.provided, tt.expected) - assert.Equal(t, tt.want, got) - }) - } +tests := []struct { +name string +provided string +expected string +want bool +}{ +{ +name: "Matching keys", +provided: "my-secret-key", +expected: "my-secret-key", +want: true, +}, +{ +name: "Non-matching keys", +provided: "wrong-key", +expected: "correct-key", +want: false, +}, +{ +name: "Empty expected (auth disabled)", +provided: "any-key", +expected: "", +want: true, +}, +{ +name: "Empty provided with expected", +provided: "", +expected: "required-key", +want: false, +}, +{ +name: "Both empty", +provided: "", +expected: "", +want: true, +}, +{ +name: "Case sensitive - should not match", +provided: "My-Secret-Key", +expected: "my-secret-key", +want: false, +}, +{ +name: "Keys with whitespace - exact match required", +provided: "key with spaces", +expected: "key with spaces", +want: true, +}, +{ +name: "Keys with whitespace - trailing space different", +provided: "my-key ", +expected: "my-key", +want: false, +}, +} + +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +got := ValidateAPIKey(tt.provided, tt.expected) +assert.Equal(tt.want, got) +}) +} } func TestExtractAgentID(t *testing.T) { - tests := []struct { - name string - authHeader string - want string - }{ - { - name: "Empty header returns default", - authHeader: "", - want: "default", - }, - { - name: "Plain API key", - authHeader: "my-api-key", - want: "my-api-key", - }, - { - name: "Bearer token", - authHeader: "Bearer my-token-123", - want: "my-token-123", - }, - { - name: "Agent format", - authHeader: "Agent agent-abc", - want: "agent-abc", - }, - { - name: "Long API key", - authHeader: "my-super-long-api-key-with-many-characters", - want: "my-super-long-api-key-with-many-characters", - }, - { - name: "API key with special characters", - authHeader: "key!@#$%^&*()", - want: "key!@#$%^&*()", - }, - } +assert := assert.New(t) + +tests := []struct { +name string +authHeader string +want string +}{ +{ +name: "Empty header returns default", +authHeader: "", +want: "default", +}, +{ +name: "Plain API key", +authHeader: "my-api-key", +want: "my-api-key", +}, +{ +name: "Bearer token", +authHeader: "Bearer my-token-123", +want: "my-token-123", +}, +{ +name: "Agent format", +authHeader: "Agent agent-abc", +want: "agent-abc", +}, +{ +name: "Long API key", +authHeader: "my-super-long-api-key-with-many-characters", +want: "my-super-long-api-key-with-many-characters", +}, +{ +name: "API key with special characters", +authHeader: "key!@#$%^&*()", +want: "key!@#$%^&*()", +}, +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := ExtractAgentID(tt.authHeader) - assert.Equal(t, tt.want, got) - }) - } +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +got := ExtractAgentID(tt.authHeader) +assert.Equal(tt.want, got) +}) +} } func TestExtractSessionID(t *testing.T) { - tests := []struct { - name string - authHeader string - want string - }{ - { - name: "Empty header returns empty string", - authHeader: "", - want: "", - }, - { - name: "Plain API key", - authHeader: "my-api-key", - want: "my-api-key", - }, - { - name: "Bearer token", - authHeader: "Bearer my-token-123", - want: "my-token-123", - }, - { - name: "Bearer token with trailing space (trimmed)", - authHeader: "Bearer my-token-123 ", - want: "my-token-123", - }, - { - name: "Bearer token with leading and trailing spaces (trimmed)", - authHeader: "Bearer my-token-123 ", - want: "my-token-123", - }, - { - name: "Agent format", - authHeader: "Agent agent-abc", - want: "agent-abc", - }, - { - name: "Long API key", - authHeader: "my-super-long-api-key-with-many-characters", - want: "my-super-long-api-key-with-many-characters", - }, - { - name: "API key with special characters", - authHeader: "key!@#$%^&*()", - want: "key!@#$%^&*()", - }, - { - name: "Whitespace only header", - authHeader: " ", - want: " ", - }, - } +assert := assert.New(t) + +tests := []struct { +name string +authHeader string +want string +}{ +{ +name: "Empty header returns empty string", +authHeader: "", +want: "", +}, +{ +name: "Plain API key", +authHeader: "my-api-key", +want: "my-api-key", +}, +{ +name: "Bearer token", +authHeader: "Bearer my-token-123", +want: "my-token-123", +}, +{ +name: "Bearer token with trailing space (trimmed)", +authHeader: "Bearer my-token-123 ", +want: "my-token-123", +}, +{ +name: "Bearer token with leading and trailing spaces (trimmed)", +authHeader: "Bearer my-token-123 ", +want: "my-token-123", +}, +{ +name: "Agent format", +authHeader: "Agent agent-abc", +want: "agent-abc", +}, +{ +name: "Long API key", +authHeader: "my-super-long-api-key-with-many-characters", +want: "my-super-long-api-key-with-many-characters", +}, +{ +name: "API key with special characters", +authHeader: "key!@#$%^&*()", +want: "key!@#$%^&*()", +}, +{ +name: "Whitespace only header", +authHeader: " ", +want: " ", +}, +{ +name: "Agent format with multiple spaces (trimmed)", +authHeader: "Agent agent-123 ", +want: " agent-123 ", +}, +{ +name: "Bearer with tab character", +authHeader: "Bearer\tmy-token", +want: "Bearer\tmy-token", +}, +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := ExtractSessionID(tt.authHeader) - assert.Equal(t, tt.want, got) - }) - } +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +got := ExtractSessionID(tt.authHeader) +assert.Equal(tt.want, got) +}) +} +} + +func TestTruncateSessionID(t *testing.T) { +assert := assert.New(t) + +tests := []struct { +name string +sessionID string +want string +}{ +{ +name: "Empty session ID returns (none)", +sessionID: "", +want: "(none)", +}, +{ +name: "Single character", +sessionID: "a", +want: "a", +}, +{ +name: "Short session ID (5 chars)", +sessionID: "abc12", +want: "abc12", +}, +{ +name: "Exactly 8 characters - not truncated", +sessionID: "abcd1234", +want: "abcd1234", +}, +{ +name: "Exactly 9 characters - truncated", +sessionID: "abcd12345", +want: "abcd1234...", +}, +{ +name: "Long session ID (>8 chars)", +sessionID: "my-session-id-12345", +want: "my-sessi...", +}, +{ +name: "Very long session ID", +sessionID: "my-super-long-session-id-with-many-characters-12345678901234567890", +want: "my-super...", +}, +{ +name: "Session ID with special characters", +sessionID: "key!@#$%^&*()", +want: "key!@#$%...", +}, +{ +name: "Session ID with unicode", +sessionID: "session-émojis-🔑", +want: "session-...", +}, +{ +name: "UUID format", +sessionID: "550e8400-e29b-41d4-a716-446655440000", +want: "550e8400...", +}, +{ +name: "Whitespace only (under 8 chars)", +sessionID: " ", +want: " ", +}, +{ +name: "Whitespace only (over 8 chars)", +sessionID: " ", +want: " ...", +}, +} + +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +got := sanitize.TruncateSessionID(tt.sessionID) +assert.Equal(tt.want, got) +}) +} } diff --git a/internal/config/validation_env.go b/internal/config/validation_env.go index 2f0eb9cc..e2190023 100644 --- a/internal/config/validation_env.go +++ b/internal/config/validation_env.go @@ -247,6 +247,12 @@ func validateContainerID(containerID string) error { // runDockerInspect is a helper function that executes docker inspect with a given format template. // It validates the container ID before running the command and returns the output as a string. // +// NOTE: This function is intentionally placed in the validation package despite being a Docker +// operation helper. It is tightly coupled to the containerized validation checks in this file +// and is only used within validation_env.go (5 call sites). Moving it to a separate package +// would increase complexity without providing clear benefits. If additional Docker helper +// functions emerge in the future (3+ helpers), consider creating an internal/docker/ package. +// // Security Note: This is an internal helper function that should only be called with // hardcoded format templates defined within this package. The formatTemplate parameter // is not validated as it is never exposed to user input. diff --git a/internal/logger/sanitize/sanitize.go b/internal/logger/sanitize/sanitize.go index 89a42b73..da8c8e75 100644 --- a/internal/logger/sanitize/sanitize.go +++ b/internal/logger/sanitize/sanitize.go @@ -8,11 +8,16 @@ // 2. Prefix truncation: TruncateSecret() and TruncateSecretMap() show only the first // 4 characters of values, making them safe for logging without exposing full secrets. // +// 3. Session ID truncation: TruncateSessionID() shows only the first 8 characters of +// session IDs for safe logging, with special handling for empty IDs. +// // Usage Guidelines: // // - Use TruncateSecret()/TruncateSecretMap() for auth headers and environment variables // where you want to preserve a hint of the value for debugging. // +// - Use TruncateSessionID() for logging session IDs in diagnostic messages and logs. +// // - Use SanitizeString()/SanitizeJSON() for full payload sanitization where secrets // may appear in various formats throughout the data. // @@ -21,6 +26,9 @@ // // For auth headers // log.Printf("Auth: %s", sanitize.TruncateSecret(authHeader)) // "ghp_..." instead of full token // +// // For session IDs +// log.Printf("Session: %s", sanitize.TruncateSessionID(sessionID)) // "session-..." instead of full ID +// // // For environment variables // log.Printf("Env: %v", sanitize.TruncateSecretMap(envVars)) // @@ -128,6 +136,19 @@ func SanitizeJSON(payloadBytes []byte) json.RawMessage { return json.RawMessage(compactBytes) } +// TruncateSessionID returns a truncated session ID for safe logging (first 8 chars). +// Returns "(none)" for empty session IDs, and appends "..." for truncated values. +// This is useful for logging session IDs without exposing sensitive information. +func TruncateSessionID(sessionID string) string { + if sessionID == "" { + return "(none)" + } + if len(sessionID) <= 8 { + return sessionID + } + return sessionID[:8] + "..." +} + // SanitizeArgs returns a sanitized version of command arguments for safe logging. // It specifically handles Docker-style environment variable arguments (-e VAR=VALUE) // by truncating the values to prevent exposing sensitive data like API tokens. diff --git a/internal/logger/sanitize/sanitize_test.go b/internal/logger/sanitize/sanitize_test.go index 574e0901..88b6a946 100644 --- a/internal/logger/sanitize/sanitize_test.go +++ b/internal/logger/sanitize/sanitize_test.go @@ -648,3 +648,59 @@ func TestSanitizeArgsDoesNotLeakSecrets(t *testing.T) { assert.Contains(t, resultStr, "GITHUB_TOKEN=ghp_...", "Truncated token should be present") assert.Contains(t, resultStr, "API_KEY=test...", "Truncated API key should be present") } + +func TestTruncateSessionID(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "Empty session ID", + input: "", + want: "(none)", + }, + { + name: "Short session ID (8 chars)", + input: "12345678", + want: "12345678", + }, + { + name: "Short session ID (less than 8 chars)", + input: "abc", + want: "abc", + }, + { + name: "Long session ID", + input: "session-id-with-many-characters-123456789", + want: "session-...", + }, + { + name: "UUID-like session ID", + input: "550e8400-e29b-41d4-a716-446655440000", + want: "550e8400...", + }, + { + name: "GitHub token as session ID", + input: "ghp_1234567890123456789012345678901234567890", + want: "ghp_1234...", + }, + { + name: "Nine characters", + input: "123456789", + want: "12345678...", + }, + { + name: "Exactly 8 characters", + input: "abcdefgh", + want: "abcdefgh", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := TruncateSessionID(tt.input) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/server/sdk_logging.go b/internal/server/sdk_logging.go index 8c09641e..840e1f70 100644 --- a/internal/server/sdk_logging.go +++ b/internal/server/sdk_logging.go @@ -10,6 +10,7 @@ import ( "github.com/github/gh-aw-mcpg/internal/auth" "github.com/github/gh-aw-mcpg/internal/logger" + "github.com/github/gh-aw-mcpg/internal/logger/sanitize" ) var logSDK = logger.New("server:sdk-frontend") @@ -51,7 +52,7 @@ func WithSDKLogging(handler http.Handler, mode string) http.Handler { // Log incoming request logSDK.Printf(">>> SDK Request [%s] session=%s mcp-session=%s method=%s path=%s", - mode, auth.TruncateSessionID(sessionID), auth.TruncateSessionID(mcpSessionID), r.Method, r.URL.Path) + mode, sanitize.TruncateSessionID(sessionID), sanitize.TruncateSessionID(mcpSessionID), r.Method, r.URL.Path) // Capture and log request body for POST requests var requestBody []byte @@ -67,7 +68,7 @@ func WithSDKLogging(handler http.Handler, mode string) http.Handler { if err := json.Unmarshal(requestBody, &jsonrpcReq); err == nil { logSDK.Printf(" JSON-RPC Request: method=%s id=%v", jsonrpcReq.Method, jsonrpcReq.ID) logger.LogDebug("sdk-frontend", "JSON-RPC request parsed: mode=%s, method=%s, id=%v, session=%s", - mode, jsonrpcReq.Method, jsonrpcReq.ID, auth.TruncateSessionID(sessionID)) + mode, jsonrpcReq.Method, jsonrpcReq.ID, sanitize.TruncateSessionID(sessionID)) } else { logSDK.Printf(" Failed to parse JSON-RPC request: %v", err) logSDK.Printf(" Raw body: %s", string(requestBody)) @@ -108,7 +109,7 @@ func WithSDKLogging(handler http.Handler, mode string) http.Handler { logSDK.Printf(" ⚠️ TOOL NOT FOUND ERROR") logger.LogWarn("client", "Tool not found: mode=%s, method=%s, session=%s, code=%d, message=%q", - mode, jsonrpcReq.Method, auth.TruncateSessionID(sessionID), errorCode, errorMsg) + mode, jsonrpcReq.Method, sanitize.TruncateSessionID(sessionID), errorCode, errorMsg) } // Log detailed error info for protocol state issues @@ -116,14 +117,14 @@ func WithSDKLogging(handler http.Handler, mode string) http.Handler { strings.Contains(errorMsg, "invalid during") { logSDK.Printf(" ⚠️ PROTOCOL STATE ERROR DETECTED") logSDK.Printf(" Request method was: %s", jsonrpcReq.Method) - logSDK.Printf(" Session ID: %s", auth.TruncateSessionID(sessionID)) - logSDK.Printf(" MCP-Session-Id header: %s", auth.TruncateSessionID(mcpSessionID)) + logSDK.Printf(" Session ID: %s", sanitize.TruncateSessionID(sessionID)) + logSDK.Printf(" MCP-Session-Id header: %s", sanitize.TruncateSessionID(mcpSessionID)) logSDK.Printf(" This error indicates SDK's StreamableHTTPHandler created fresh protocol state") logger.LogWarn("sdk-frontend", "Protocol state error: mode=%s, method=%s, session=%s, mcp_session=%s, error=%q", - mode, jsonrpcReq.Method, auth.TruncateSessionID(sessionID), - auth.TruncateSessionID(mcpSessionID), errorMsg) + mode, jsonrpcReq.Method, sanitize.TruncateSessionID(sessionID), + sanitize.TruncateSessionID(mcpSessionID), errorMsg) } else if (errorCode != -32602 && errorCode != -32601) || jsonrpcReq.Method != "tools/call" { // Only log as general error if not already logged above logger.LogError("sdk-frontend",