{"record":{"id":"1e2218284eb13ed4","repo":"github/github-mcp-server","slug":"oauth-callback-port-d-is-not-available-another-p","errorCode":null,"errorMessage":"OAuth callback port %d is not available; another process may be using it — free the port or set a different --oauth-callback-port: %w","messagePattern":"OAuth callback port (.+?) is not available; another process may be using it — free the port or set a different --oauth-callback-port: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/oauth/flow.go","lineNumber":59,"sourceCode":"// begin selects and prepares the appropriate flow. PKCE is preferred for its\n// stronger security; device flow is the fallback. A random callback port inside\n// Docker cannot be reached from the host browser, so that combination goes\n// straight to device flow.\nfunc (m *Manager) begin(prompter Prompter) (*flowPlan, error) {\n\tcanPKCE := m.config.CallbackPort != 0 || !m.inDocker()\n\tif canPKCE {\n\t\tplan, err := m.beginPKCE(prompter)\n\t\tif err == nil {\n\t\t\treturn plan, nil\n\t\t}\n\t\t// A fixed callback port that won't bind is fatal, not a cue to downgrade.\n\t\t// The port was chosen deliberately (and registered with the OAuth app), so\n\t\t// a bind failure means another process holds it — possibly one positioned\n\t\t// to intercept the authorization redirect. Silently switching to device\n\t\t// flow would mask that, so stop and make the user resolve it. Only genuine\n\t\t// bind failures qualify; other errors fall through to device flow.\n\t\tif m.config.CallbackPort != 0 && errors.Is(err, errCallbackBind) {\n\t\t\treturn 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)\n\t\t}\n\t\tm.logger.Info(\"PKCE flow unavailable, falling back to device flow\", \"reason\", err)\n\t} else {\n\t\tm.logger.Info(\"no callback port inside container; using device flow\")\n\t}\n\treturn m.beginDevice(prompter)\n}\n\n// beginPKCE prepares the authorization-code + PKCE flow. It binds the callback\n// server and selects the most secure available display channel: browser\n// auto-open, then URL elicitation, then a tool-response message. On a headless\n// host with a random callback port it diverts to device flow, whose redirect\n// does not depend on reaching this machine's localhost.\nfunc (m *Manager) beginPKCE(prompter Prompter) (*flowPlan, error) {\n\tstate, err := randomState()\n\tif err != nil {\n\t\treturn nil, err\n\t}","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/github/github-mcp-server/blob/0ea1f775a7c73eff1bd2e25904d01136756bbfe2/internal/oauth/flow.go#L41-L77","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Identify the holder and stop it: lsof -nP -iTCP:8085 -sTCP:LISTEN or ss -ltnp","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","Do not treat this as a fallback cue — the design intentionally refuses device-flow downgrade here","If interception is plausible (unknown process), investigate before re-running login"],"exampleFix":"// before\n$ github-mcp-server --oauth-callback-port 8085\n// error: OAuth callback port 8085 is not available; another process may be using it ...\n\n// after\n$ lsof -tiTCP:8085 -sTCP:LISTEN | xargs kill   # free the registered port\n$ github-mcp-server --oauth-callback-port 8085","handlingStrategy":"validation","validationCode":"func callbackPortAvailable(port int) bool {\n    ln, err := net.Listen(\"tcp\", fmt.Sprintf(\"127.0.0.1:%d\", port))\n    if err != nil { return false }\n    _ = ln.Close()\n    return true\n}\n\nif fixedPort != 0 && !callbackPortAvailable(fixedPort) {\n    return fmt.Errorf(\"refusing to start: callback port %d held by another process\", fixedPort)\n}","typeGuard":"errors.Is(err, errCallbackBind) // inside the package: distinguish bind failures from other prep errors","tryCatchPattern":"// caller of begin(): this error is fatal by design — do NOT auto-downgrade to device flow\nif err != nil && strings.Contains(err.Error(), \"is not available\") {\n    os.Exit(1) // surface to the operator to free/replace the port\n}","preventionTips":["Run exactly one server instance per fixed callback port","Pre-check the port at startup and fail with a clear message before any OAuth state is created","If the port must change, update the OAuth app's registered callback URL in the same change"],"tags":["oauth","port","security","configuration","startup"],"backgroundTag":null,"analyzedSha":"0ea1f775a7c73eff1bd2e25904d01136756bbfe2","analyzedAt":"2026-08-15T18:10:19.804Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}