From f93781c30752125e855147184257bc8f589f0600 Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:03:58 +0000 Subject: [PATCH] fix(mcp): redact credentials in JSON logs JSON log handlers bypass String and serialize exported credentials. Implement LogValue so remote server entries use the same redacted representation in text and JSON logs. Extend the regression test to cover both handlers. Assisted-by: Codex:GPT-6 --- core/config/mcp_remote_server_test.go | 1 + core/config/model_config.go | 9 +++++++-- docs/content/features/mcp.md | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/config/mcp_remote_server_test.go b/core/config/mcp_remote_server_test.go index 4b15749fa..b745104c3 100644 --- a/core/config/mcp_remote_server_test.go +++ b/core/config/mcp_remote_server_test.go @@ -67,6 +67,7 @@ var _ = Describe("MCP remote server OAuth2 configuration", func() { OAuth2: &MCPOAuth2Config{TokenURL: "https://idp", ClientID: "id", ClientSecret: "oauth-secret"}, } logger.Info("cfg", "a", withToken, "b", withOAuth2) + slog.New(slog.NewJSONHandler(&buf, nil)).Info("cfg", "a", withToken, "b", withOAuth2) out := buf.String() + fmt.Sprintf("%v %+v", withToken, MCPRemoteServers{"s": withOAuth2}) Expect(out).To(ContainSubstring("https://x")) Expect(out).To(ContainSubstring("https://idp")) diff --git a/core/config/model_config.go b/core/config/model_config.go index 8f7008194..0efafbb85 100644 --- a/core/config/model_config.go +++ b/core/config/model_config.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "math" "os" "path/filepath" @@ -850,8 +851,7 @@ func exactlyOne(field, literal, env string) error { return nil } -// String keeps credentials out of logs: the MCP code logs whole server -// entries at debug level, and both xlog and slog format structs via fmt. +// String redacts credentials when server entries are formatted as text. func (s MCPRemoteServer) String() string { auth := "none" switch { @@ -863,6 +863,11 @@ func (s MCPRemoteServer) String() string { return fmt.Sprintf("{url: %s, auth: %s}", s.URL, auth) } +// LogValue also redacts credentials in structured JSON logs, which bypass String. +func (s MCPRemoteServer) LogValue() slog.Value { + return slog.StringValue(s.String()) +} + // @Description MCP STDIO server configuration type MCPSTDIOServer struct { Args []string `json:"args,omitempty"` diff --git a/docs/content/features/mcp.md b/docs/content/features/mcp.md index cf83a1bbd..f308b1d75 100644 --- a/docs/content/features/mcp.md +++ b/docs/content/features/mcp.md @@ -135,6 +135,7 @@ Behaviour: - It is refreshed proactively once **half** of its lifetime (`expires_in`) has elapsed. If the provider omits `expires_in`, a lifetime of 5 minutes is assumed. - If a refresh fails, the current token keeps being used until 30 seconds before it expires (retries wait at least 5 seconds after a failed fetch, including the first fetch). After that, requests to the MCP server fail with an error; LocalAI never falls back to an unauthenticated request. - Configuring both `token` and `oauth2` for the same server is rejected when the model configuration is loaded, as are missing or duplicated `client_id`/`client_secret` sources. An environment variable named in `*_env` that is unset or empty makes the connection to that server fail, and the server is reported with an error in the server listing. +- Debug logs of remote server entries redact static tokens and OAuth2 client secrets in both text and JSON formats. - Token caches and retry timers are local to each connection-owning process. Workers obtain and refresh their own tokens; this cache is not shared across the cluster. - The environment variables are read by the process that connects to the MCP server. In distributed mode that is the agent worker, so set them there.