{"record":{"id":"2fddea188e8798b8","repo":"Tencent/WeKnora","slug":"principal-context-is-required-to-connect-to-an-oau","errorCode":null,"errorMessage":"principal context is required to connect to an OAuth MCP service","messagePattern":"principal context is required to connect to an OAuth MCP service","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/mcp/client.go","lineNumber":266,"sourceCode":"\n// buildOAuthConfig returns the OAuth configuration for an OAuth-enabled MCP\n// service, or (_, false, nil) when the service does not use OAuth. It loads\n// the dynamically-registered client_id and wires a per-user token store so\n// the transport injects the invoking user's bearer token and refreshes it.\nfunc buildOAuthConfig(config *ClientConfig, httpClient *http.Client) (transport.OAuthConfig, bool, error) {\n\tsvc := config.Service\n\tif !svc.AuthConfig.IsOAuth() {\n\t\treturn transport.OAuthConfig{}, false, nil\n\t}\n\tif config.OAuthRepo == nil {\n\t\treturn transport.OAuthConfig{}, false, fmt.Errorf(\"OAuth repository is required for OAuth MCP services\")\n\t}\n\tprincipal := config.Principal.Normalize()\n\tif !principal.Valid() && config.UserID != \"\" {\n\t\tprincipal = types.Principal{Type: types.PrincipalWebUser, ID: config.UserID}.Normalize()\n\t}\n\tif !principal.Valid() {\n\t\treturn transport.OAuthConfig{}, false, fmt.Errorf(\"principal context is required to connect to an OAuth MCP service\")\n\t}\n\tconfig.Principal = principal\n\n\toauthCfg := transport.OAuthConfig{\n\t\tScopes:                svc.AuthConfig.Scopes,\n\t\tTokenStore:            newManagedTokenStore(config.OAuthRepo, config.TenantID, principal, svc.ID),\n\t\tPKCEEnabled:           true,\n\t\tAuthServerMetadataURL: svc.AuthConfig.AuthServerMetadataURL,\n\t\tHTTPClient:            httpClient,\n\t}\n\tif regClient, err := config.OAuthRepo.GetClient(context.Background(), config.TenantID, svc.ID); err == nil && regClient != nil {\n\t\toauthCfg.ClientID = regClient.ClientID\n\t\toauthCfg.ClientSecret = regClient.ClientSecret\n\t\toauthCfg.RedirectURI = regClient.RedirectURI\n\t}\n\treturn oauthCfg, true, nil\n}\n","sourceCodeStart":248,"sourceCodeEnd":284,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/mcp/client.go#L248-L284","documentation":"OAuth token storage is keyed by a principal (user identity). buildOAuthConfig requires a valid Principal after normalization, optionally falling back to config.UserID as a web-user principal. If neither yields a valid principal, the client cannot know whose OAuth token to load/refresh, so it fails before connecting.","triggerScenarios":"NewMCPClient for an OAuth service where config.Principal is zero/invalid AND config.UserID is empty, or config.UserID is set but still produces an invalid normalized principal (e.g. empty ID).","commonSituations":"Background/system jobs calling the MCP client without a user context; request context lost between service layers so UserID was never propagated; machine-to-machine calls to a per-user OAuth service that has no service-account principal.","solutions":["Set config.Principal to the authenticated user's principal (type + ID) before creating the client","Or set config.UserID; it is converted to a PrincipalWebUser principal automatically","For non-interactive jobs, use a dedicated service-account principal or switch the service to a non-OAuth auth strategy","Propagate the user ID from the request context through to the MCP client config"],"exampleFix":"// before\ncfg := &ClientConfig{Service: svc, OAuthRepo: repo, TenantID: tid} // no principal/user\n// after\ncfg := &ClientConfig{Service: svc, OAuthRepo: repo, TenantID: tid,\n    Principal: types.Principal{Type: types.PrincipalWebUser, ID: userID}.Normalize()}","handlingStrategy":"validation","validationCode":"func requirePrincipal(cfg *ClientConfig) error {\n    if cfg.Service == nil || !cfg.Service.AuthConfig.IsOAuth() { return nil }\n    p := cfg.Principal.Normalize()\n    if !p.Valid() && cfg.UserID != \"\" {\n        p = types.Principal{Type: types.PrincipalWebUser, ID: cfg.UserID}.Normalize()\n    }\n    if !p.Valid() { return errors.New(\"valid principal or UserID required for OAuth MCP service\") }\n    return nil\n}","typeGuard":"func hasPrincipalContext(cfg *ClientConfig) bool {\n    return cfg.Principal.Normalize().Valid() || cfg.UserID != \"\"\n}","tryCatchPattern":"_, err := NewMCPClient(cfg)\nif err != nil && strings.Contains(err.Error(), \"principal context is required\") {\n    return fmt.Errorf(\"no user context available for OAuth service %s; cannot authorize\", cfg.Service.ID)\n}","preventionTips":["Always thread the authenticated user (principal or UserID) from the HTTP request into MCP client configs","For background jobs, configure a service-account principal or non-OAuth auth","Add a middleware that rejects MCP calls lacking user context for OAuth services"],"tags":["go","mcp","oauth","principal","configuration"],"backgroundTag":"missing-user-context","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}