Skip to content

serve/daemon path launches the system browser on 401 with no way to suppress it (MCPORTER_OAUTH_NO_BROWSER is auth-command-only) #283

Description

@vitalijssilins

Motivation

We run mcporter as the single MCP bridge for coding agents (Claude Code / Codex): one user-scope mcporter serve --stdio entry exposing ~9 remote servers. It works great — except when a token expires mid-session.

When any bridged tool call hits a 401, the daemon runs the interactive OAuth flow and unconditionally launches the system browser. From the user's perspective a browser tab steals focus mid-task with zero context about which server wants auth or why. In an agent harness the desired flow is the opposite: fail the tool call fast, let the agent mint a link via mcporter auth <name> --no-browser --json, and present it in chat where the user can click it deliberately (the flow #169 added — thank you for that one).

#169's --no-browser / MCPORTER_OAUTH_NO_BROWSER solved this for auth and config login, but the env var is parsed only in the auth command's argv handling. Setting it on the serve process does nothing.

Current behavior (0.12.4 and 0.13.0)

  • PersistentOAuthClientProvider.redirectToAuthorization (src/oauth.ts) calls openExternal(...) unless options.suppressBrowserLaunch is set — and the only caller that sets it is the auth command with --no-browser.
  • On the serve/daemon path there is no way to reach that option, so every 401 → browser launch.

It also compounds into a tab loop when auth keeps failing:

  • connectWithAuth makes up to 3 attempts (maxAttempts = 3), each re-entering interactive authorization;
  • a pending authorization older than INTERACTIVE_AUTHORIZATION_TTL_MS (5 min) is treated as abandoned, so the next request prompts again;
  • the duplicate-prompt guard from Serialize interactive OAuth authorization per shared provider/session #247 is per provider instance, and with one daemon per config hash (several repos/worktrees) each daemon prompts independently.

A persistently failing server (e.g. an IdP rejecting the client) produces an endless stream of context-free browser tabs.

Proposed fix

Honor browser suppression on the runtime/serve path. Smallest version:

  1. Read MCPORTER_OAUTH_NO_BROWSER (same semantics as Add a flag to suppress browser launch in auth / config login for headless workflows #169) when constructing the OAuth session in the runtime, and pass suppressBrowserLaunch: true through to PersistentOAuthClientProvider.
  2. When suppressed, fail the pending authorization promptly (or honor a short MCPORTER_OAUTH_TIMEOUT_MS as today) with an error that includes the server name, so the caller/agent can surface its own auth flow.

A per-server config field ("browser": false) or a serve --no-browser flag would also work; the env var is the minimal, consistent-with-#169 option.

Workaround we use today

A PATH shim replacing open for the bridge process tree (logs the URL instead of launching), plus MCPORTER_OAUTH_TIMEOUT_MS=20000 so the suppressed flow fails fast — works, but it depends on openExternal spawning a PATH-resolved open, which is an implementation detail we'd rather not lean on.

Happy to send a PR for option 1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal priority bug or improvement with limited blast radius.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:auth-providerThis issue is about auth, provider routing, model choice, or SecretRef resolution.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions