github/github-mcp-server · error

OAuth callback port %d is not available; another process may

Error message

OAuth callback port %d is not available; another process may be using it — free the port or set a different --oauth-callback-port: %w

What it means

The fixed callback port (m.config.CallbackPort != 0) failed to bind and begin treats it as fatal instead of downgrading to device flow. The comment is explicit: the port was deliberately chosen and registered on the OAuth app, so a bind failure may mean another process is positioned to intercept the authorization redirect — silently switching flows would mask that. The error chains errCallbackBind plus the OS bind error, and names both remediations.

Source

Thrown at internal/oauth/flow.go:59

// begin selects and prepares the appropriate flow. PKCE is preferred for its
// stronger security; device flow is the fallback. A random callback port inside
// Docker cannot be reached from the host browser, so that combination goes
// straight to device flow.
func (m *Manager) begin(prompter Prompter) (*flowPlan, error) {
	canPKCE := m.config.CallbackPort != 0 || !m.inDocker()
	if canPKCE {
		plan, err := m.beginPKCE(prompter)
		if err == nil {
			return plan, nil
		}
		// A fixed callback port that won't bind is fatal, not a cue to downgrade.
		// The port was chosen deliberately (and registered with the OAuth app), so
		// a bind failure means another process holds it — possibly one positioned
		// to intercept the authorization redirect. Silently switching to device
		// flow would mask that, so stop and make the user resolve it. Only genuine
		// bind failures qualify; other errors fall through to device flow.
		if m.config.CallbackPort != 0 && errors.Is(err, errCallbackBind) {
			return nil, fmt.Errorf("OAuth callback port %d is not available; another process may be using it — free the port or set a different --oauth-callback-port: %w", m.config.CallbackPort, err)
		}
		m.logger.Info("PKCE flow unavailable, falling back to device flow", "reason", err)
	} else {
		m.logger.Info("no callback port inside container; using device flow")
	}
	return m.beginDevice(prompter)
}

// beginPKCE prepares the authorization-code + PKCE flow. It binds the callback
// server and selects the most secure available display channel: browser
// auto-open, then URL elicitation, then a tool-response message. On a headless
// host with a random callback port it diverts to device flow, whose redirect
// does not depend on reaching this machine's localhost.
func (m *Manager) beginPKCE(prompter Prompter) (*flowPlan, error) {
	state, err := randomState()
	if err != nil {
		return nil, err
	}

View on GitHub (pinned to 0ea1f775a7)

Solutions

  1. Identify the holder and stop it: lsof -nP -iTCP:8085 -sTCP:LISTEN or ss -ltnp
  2. If the holder is legitimate, pick a new port and update BOTH --oauth-callback-port and the callback URL registered on the OAuth/GitHub App
  3. Do not treat this as a fallback cue — the design intentionally refuses device-flow downgrade here
  4. If interception is plausible (unknown process), investigate before re-running login

Example fix

// before
$ github-mcp-server --oauth-callback-port 8085
// error: OAuth callback port 8085 is not available; another process may be using it ...

// after
$ lsof -tiTCP:8085 -sTCP:LISTEN | xargs kill   # free the registered port
$ github-mcp-server --oauth-callback-port 8085
Defensive patterns

Strategy: validation

Validate before calling

func callbackPortAvailable(port int) bool {
    ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
    if err != nil { return false }
    _ = ln.Close()
    return true
}

if fixedPort != 0 && !callbackPortAvailable(fixedPort) {
    return fmt.Errorf("refusing to start: callback port %d held by another process", fixedPort)
}

Type guard

errors.Is(err, errCallbackBind) // inside the package: distinguish bind failures from other prep errors

Try / catch

// caller of begin(): this error is fatal by design — do NOT auto-downgrade to device flow
if err != nil && strings.Contains(err.Error(), "is not available") {
    os.Exit(1) // surface to the operator to free/replace the port
}

Prevention

When it happens

Trigger: listenCallback returns errCallbackBind (wrapped at flow.go:84) while m.config.CallbackPort is a configured non-zero port; the check at internal/oauth/flow.go:58 escalates it. Concretely: a previous server instance still holds --oauth-callback-port 8085, or an unrelated process squats the port registered as the OAuth app's callback URL.

Common situations: Duplicate server processes (systemd unit + manual run); orphaned container republishing the port; another developer tool claiming the same fixed localhost port; a stale process after a crash where the socket lingers.

Related errors


AI-assisted analysis of github/github-mcp-server@0ea1f775a7 (2026-08-15). Data as JSON: /api/errors/1e2218284eb13ed4. Report an issue: GitHub.