{"record":{"id":"bc481cbc6358b2c5","repo":"charmbracelet/crush","slug":"mcp-s-already-has-an-authentication-in-progress","errorCode":null,"errorMessage":"mcp '%s' already has an authentication in progress","messagePattern":"mcp '(.+?)' already has an authentication in progress","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/agent/tools/mcp/init.go","lineNumber":437,"sourceCode":"// surfacing the authorization URL (via [MCPAuthURL]) to the user. It returns\n// a finish function that must be called exactly once with the request\n// context: finish blocks until the flow completes and returns the result.\n//\n// Only one browser-suppressed flow per server may be in progress. The\n// returned cancel function aborts the flow without waiting; use it when the\n// caller's context is cancelled.\nfunc BeginAuth(cfg *config.ConfigStore, name string) (finish func(ctx context.Context) error, cancel context.CancelFunc, err error) {\n\tm, exists := cfg.Config().MCP[name]\n\tif !exists {\n\t\treturn nil, nil, fmt.Errorf(\"mcp '%s' not found in configuration\", name)\n\t}\n\tif !m.OAuth || m.Type != config.MCPHttp {\n\t\treturn nil, nil, fmt.Errorf(\"mcp '%s' does not use OAuth authentication\", name)\n\t}\n\n\tlock := suppressLock(name)\n\tif !lock.TryLock() {\n\t\treturn nil, nil, fmt.Errorf(\"mcp '%s' already has an authentication in progress\", name)\n\t}\n\n\tflowCtx, flowCancel := context.WithCancel(context.Background())\n\tflowCtx = mcpoauth.WithInteractive(flowCtx)\n\tflowCtx = context.WithValue(flowCtx, suppressBrowserKey{}, true)\n\n\tfinish = func(ctx context.Context) error {\n\t\tdefer lock.Unlock()\n\t\tdefer flowCancel()\n\n\t\tdone := make(chan error, 1)\n\t\tgo func() {\n\t\t\tdone <- runAuthFlow(flowCtx, cfg, name, m)\n\t\t}()\n\n\t\tselect {\n\t\tcase err := <-done:\n\t\t\treturn err","sourceCodeStart":419,"sourceCodeEnd":455,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/agent/tools/mcp/init.go#L419-L455","documentation":"BeginAuth enforces one in-flight browser-suppressed auth flow per server using a TryLock on a per-name lock. If a flow is already running for that server, TryLock fails and the function returns this error rather than queuing a second concurrent flow.","triggerScenarios":"Calling BeginAuth for a server whose suppressLock is already held — e.g. MCPAuthenticate deferring auth while a startup retry also began auth, or two concurrent MCPAuthenticate invocations for the same server; the previous flow's cancel/finish was never called.","commonSituations":"Duplicate auth attempts fired by retry logic and user command simultaneously; a crashed flow leaked the lock; tests running concurrent BeginAuth calls for the same name.","solutions":["Wait for the in-flight auth flow to complete before starting another for the same server.","Call the flow's cancel function to abort a stuck flow, then retry BeginAuth.","De-duplicate callers: have MCPAuthenticate check whether a flow is already pending before calling BeginAuth.","If a leaked lock persists after a crash, restart the process to reset in-memory locks."],"exampleFix":"// before\nfinish, cancel, err := mcp.BeginAuth(cfg, \"github\") // concurrent call\n// after\nfinish, cancel, err := mcp.BeginAuth(cfg, \"github\")\nif err != nil && strings.Contains(err.Error(), \"already has an authentication in progress\") {\n\treturn nil // flow already running; wait for it\n}","handlingStrategy":"validation","validationCode":"// check state before starting a new flow\nif state := mcp.GetState(name); state == mcp.StateNeedsAuth && authInFlight[name] {\n\treturn nil // flow already running\n}","typeGuard":null,"tryCatchPattern":"finish, cancel, err := mcp.BeginAuth(cfg, name)\nif err != nil && strings.Contains(err.Error(), \"already has an authentication in progress\") {\n\t// wait for existing flow instead of starting a duplicate\n\treturn waitForExistingAuth(ctx, name)\n}","preventionTips":["Serialize auth attempts per server in caller code (single-flight).","Always invoke cancel to abort and release a stuck flow.","Debounce retry logic so it doesn't race user-initiated auth.","Restart the process if an in-memory lock leaks after a crash."],"tags":["mcp","oauth","concurrency","go"],"backgroundTag":"operation-already-in-progress","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}