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.