charmbracelet/crush · error

mcp '%s' already has an authentication in progress

Error message

mcp '%s' already has an authentication in progress

What it means

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.

Source

Thrown at internal/agent/tools/mcp/init.go:437

// surfacing the authorization URL (via [MCPAuthURL]) to the user. It returns
// a finish function that must be called exactly once with the request
// context: finish blocks until the flow completes and returns the result.
//
// Only one browser-suppressed flow per server may be in progress. The
// returned cancel function aborts the flow without waiting; use it when the
// caller's context is cancelled.
func BeginAuth(cfg *config.ConfigStore, name string) (finish func(ctx context.Context) error, cancel context.CancelFunc, err error) {
	m, exists := cfg.Config().MCP[name]
	if !exists {
		return nil, nil, fmt.Errorf("mcp '%s' not found in configuration", name)
	}
	if !m.OAuth || m.Type != config.MCPHttp {
		return nil, nil, fmt.Errorf("mcp '%s' does not use OAuth authentication", name)
	}

	lock := suppressLock(name)
	if !lock.TryLock() {
		return nil, nil, fmt.Errorf("mcp '%s' already has an authentication in progress", name)
	}

	flowCtx, flowCancel := context.WithCancel(context.Background())
	flowCtx = mcpoauth.WithInteractive(flowCtx)
	flowCtx = context.WithValue(flowCtx, suppressBrowserKey{}, true)

	finish = func(ctx context.Context) error {
		defer lock.Unlock()
		defer flowCancel()

		done := make(chan error, 1)
		go func() {
			done <- runAuthFlow(flowCtx, cfg, name, m)
		}()

		select {
		case err := <-done:
			return err

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Wait for the in-flight auth flow to complete before starting another for the same server.
  2. Call the flow's cancel function to abort a stuck flow, then retry BeginAuth.
  3. De-duplicate callers: have MCPAuthenticate check whether a flow is already pending before calling BeginAuth.
  4. If a leaked lock persists after a crash, restart the process to reset in-memory locks.

Example fix

// before
finish, cancel, err := mcp.BeginAuth(cfg, "github") // concurrent call
// after
finish, cancel, err := mcp.BeginAuth(cfg, "github")
if err != nil && strings.Contains(err.Error(), "already has an authentication in progress") {
	return nil // flow already running; wait for it
}
Defensive patterns

Strategy: validation

Validate before calling

// check state before starting a new flow
if state := mcp.GetState(name); state == mcp.StateNeedsAuth && authInFlight[name] {
	return nil // flow already running
}

Try / catch

finish, cancel, err := mcp.BeginAuth(cfg, name)
if err != nil && strings.Contains(err.Error(), "already has an authentication in progress") {
	// wait for existing flow instead of starting a duplicate
	return waitForExistingAuth(ctx, name)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/bc481cbc6358b2c5. Report an issue: GitHub.