Skip to content

fix(mcp): surface 401s as an actionable "needs sign-in" state (#3719)#3733

Merged
sanil-23 merged 3 commits into
tinyhumansai:mainfrom
sanil-23:fix/3719-mcp-unreliable-auth
Jun 17, 2026
Merged

fix(mcp): surface 401s as an actionable "needs sign-in" state (#3719)#3733
sanil-23 merged 3 commits into
tinyhumansai:mainfrom
sanil-23:fix/3719-mcp-unreliable-auth

Conversation

@sanil-23

@sanil-23 sanil-23 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • MCP HTTP-remote servers that reject the connect handshake with HTTP 401 are now reported as a distinct Unauthorized ("needs sign-in") state instead of a generic Error carrying the raw transport string.
  • The raw 401 message (which leaked the internal oauth-protected-resource URL) is no longer surfaced to users; the UI shows a localized, plain-language notice.
  • The detail view shows an amber "Sign in needed" badge + actionable notice; the primary button relabels to "Sign in" and opens the existing auth modal (browser OAuth / token entry).
  • Status polling now continues for unauthorized/error/connecting servers, so state recovers after sign-in or a background-supervisor reconnect (no manual refresh).
  • The connection-health toolbar buckets unauthorized under "disconnected" so "Retry all" never blindly re-401s an auth-gated server.

Problem

Per #3719, connecting an OAuth-protected MCP server (e.g. Brave Search) showed an Error badge immediately with a raw string — MCP unauthorized for https://brave.run.tools` (HTTP 401; resource metadata: …/.well-known/oauth-protected-resource) — no tools, no re-auth prompt, and a "Connect" button that looped back to the same failure. Root cause: a 401 was indistinguishable from any other connect failure (ServerStatus` had no auth variant), so the raw transport error was passed straight through to the UI and the server was stranded in a dead-end.

Solution

  • core (mcp_client/client.rs): read_response returns a typed McpUnauthorizedError on HTTP 401 instead of bail!-ing a bare string, so the connect path classifies by type (chain downcast), not by message text.
  • core (mcp_registry/connections.rs): a separate AUTH_REQUIRED set records 401s; all_status delegates to a pure, fully-tested classify_server_status (priority Disabled > Connected > Unauthorized > Error > Disconnected) and withholds the raw error for the Unauthorized case.
  • core (mcp_registry/types.rs): new ServerStatus::Unauthorized (serializes "unauthorized").
  • ui: badge, graceful notice, "Sign in" relabel, polling fix, health-toolbar bucketing; ServerStatus union + 3 new i18n keys across all 14 locales.

Scope note: the OAuth detection internals (detect↔begin DCR requirement mismatch; discover_authorization mislabeling OAuth servers as token on a metadata-fetch failure) are intentionally out of scope here — this PR makes the 401 actionable and removes the raw-error dead-end; hardening detection is a tracked follow-up.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) — Rust: typed-401 classification + full classify_server_status branch coverage + ServerStatus::as_str; Vitest: unauthorized badge + graceful-notice rendering (asserts no raw HTTP 401 leaks).
  • Diff coverage ≥ 80% — changed Rust decision lines covered via the extracted pure helper; changed UI lines covered by new/existing Vitest. Ran pnpm test (MCP suite, 68 passing) and targeted cargo test locally.
  • N/A: Coverage matrix updated — behaviour change to an existing feature (MCP server connect/auth), no new/removed feature row.
  • N/A: Affected feature IDs listed under ## Related — no matrix feature IDs apply to this bug fix.
  • No new external network dependencies introduced — no new outbound calls; transport/auth paths unchanged in shape.
  • N/A: Manual smoke checklist — does not touch release-cut surfaces beyond existing MCP UI already in the matrix.
  • Linked issue closed via Closes #3719 below.

Impact

  • Desktop (UI + core): 401-gated MCP servers become recoverable instead of dead-ended; tools load after sign-in. No migration, no schema change beyond an additive enum variant ("unauthorized") on the existing mcp_clients_status RPC output — additive and backward-compatible (older clients fall back to the disconnected style).
  • Security: stops leaking the internal OAuth resource_metadata URL into user-facing error text.
  • No performance or compatibility concerns.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/3719-mcp-unreliable-auth
  • Commit SHA: 602d04949eaddf0a942c034fe8a5c508b9158e8a

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests: MCP Vitest (68 passing); cargo test mcp_registry::connections (8), mcp_client::client (15)
  • Rust fmt/check (if changed): cargo check --lib clean
  • N/A: Tauri fmt/check — no app/src-tauri files changed

Validation Blocked

  • command: pnpm rust:check (cargo check --manifest-path app/src-tauri/Cargo.toml) — pre-push hook
  • error: failed to load source for dependency \tauri`— the vendoredapp/src-tauri/vendor/tauri-cef` git submodule is not initialized in this worktree
  • impact: none on this change — it touches 0 app/src-tauri files; the core lib + frontend compile clean. Pre-push hook was bypassed with --no-verify per the repo's sanctioned policy for pre-existing unrelated breakage; CI runs the full Tauri check with the submodule present.

Behavior Changes

  • Intended behavior change: an MCP 401 → Unauthorized status (was generic Error).
  • User-visible effect: amber "Sign in needed" badge + plain-language notice + "Sign in" action; no raw HTTP/OAuth-URL string.

Parity Contract

  • Legacy behavior preserved: non-401 connect failures still map to Error with their message; connected/disconnected/disabled unchanged.
  • Guard/fallback/dispatch parity checks: classify_server_status preserves prior ordering; unknown statuses fall back to the disconnected style in the UI.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: this PR
  • Resolution: N/A

Summary by CodeRabbit

  • New Features

    • Added dedicated "unauthorized" status for MCP servers requiring authentication, with distinct UI handling separate from generic connection errors
    • Updated status badges and alerts to display "Sign in needed" and prompt users to authenticate when required
    • Modified action button labels to reflect authentication flow instead of standard connection actions
  • Localization

    • Added authentication-related translations across 14 languages

@sanil-23 sanil-23 requested a review from a team June 16, 2026 21:43
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@sanil-23, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 13 minutes and 50 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 148bdd86-edf3-4d35-8de9-8ed9cd405666

📥 Commits

Reviewing files that changed from the base of the PR and between f3086ca and 01c820f.

📒 Files selected for processing (26)
  • app/src/components/channels/mcp/InstalledServerDetail.test.tsx
  • app/src/components/channels/mcp/InstalledServerDetail.tsx
  • app/src/components/channels/mcp/InstalledServerList.tsx
  • app/src/components/channels/mcp/McpConnectionHealthToolbar.tsx
  • app/src/components/channels/mcp/McpServersTab.tsx
  • app/src/components/channels/mcp/McpStatusBadge.test.tsx
  • app/src/components/channels/mcp/McpStatusBadge.tsx
  • app/src/components/channels/mcp/types.ts
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • src/openhuman/mcp_client/client.rs
  • src/openhuman/mcp_client/mod.rs
  • src/openhuman/mcp_registry/connections.rs
  • src/openhuman/mcp_registry/types.rs
📝 Walkthrough

Walkthrough

Adds a distinct unauthorized MCP server status to handle HTTP 401 responses. The Rust backend introduces a typed McpUnauthorizedError, a new ServerStatus::Unauthorized enum variant, per-server auth-required tracking in the connection registry, and a classify_server_status priority helper. The TypeScript frontend extends the ServerStatus union, updates status badges, dot indicators, polling logic, and InstalledServerDetail to show a "Sign in needed" notice with a dedicated authenticate button. Thirteen locale files receive the new translation keys.

Changes

MCP Unauthorized Status (HTTP 401)

Layer / File(s) Summary
McpUnauthorizedError struct and ServerStatus::Unauthorized variant
src/openhuman/mcp_client/client.rs, src/openhuman/mcp_client/mod.rs, src/openhuman/mcp_registry/types.rs
McpUnauthorizedError is defined with Display/Error impls and optional resource_metadata; the 401 branch in read_response now returns it as a typed error instead of a string bail!; it is re-exported from mcp_client; ServerStatus gains an Unauthorized variant with lowercase serde and as_str mapping.
ConnectFailure record, needs_auth, and classify_server_status
src/openhuman/mcp_registry/connections.rs
Per-server error tracking is replaced by a unified ConnectFailure { message, unauthorized } record; is_unauthorized_error performs typed downcast; needs_auth is exposed publicly; connect/disconnect/clear_last_error manage the auth flag; classify_server_status encodes the new priority order; all_status delegates to it and suppresses last_error for Unauthorized. New unit tests cover classifier priority and error detection.
Frontend ServerStatus type and status presentation components
app/src/components/channels/mcp/types.ts, app/src/components/channels/mcp/McpStatusBadge.tsx, app/src/components/channels/mcp/McpStatusBadge.test.tsx, app/src/components/channels/mcp/InstalledServerList.tsx, app/src/components/channels/mcp/McpServersTab.tsx, app/src/components/channels/mcp/McpConnectionHealthToolbar.tsx
ServerStatus TypeScript union gains 'unauthorized'; McpStatusBadge adds amber STATUS_META entry; InstalledServerList and McpServersTab add unauthorized dot/i18n mappings; McpServersTab polling now treats unauthorized as an active state; McpConnectionHealthToolbar routes unauthorized to the disconnected bucket, not errorIds.
InstalledServerDetail auth-required notice and test
app/src/components/channels/mcp/InstalledServerDetail.tsx, app/src/components/channels/mcp/InstalledServerDetail.test.tsx
A dedicated auth-required alert is shown when status === 'unauthorized', the generic error alert is suppressed for that state, and the Connect button label switches to "authenticate". Test asserts the "Sign in needed" badge, auth message, absence of raw "HTTP 401" text, and "Sign in" button label.
i18n strings across all locales
app/src/lib/i18n/en.ts, app/src/lib/i18n/ar.ts, app/src/lib/i18n/bn.ts, app/src/lib/i18n/de.ts, app/src/lib/i18n/es.ts, app/src/lib/i18n/fr.ts, app/src/lib/i18n/hi.ts, app/src/lib/i18n/id.ts, app/src/lib/i18n/it.ts, app/src/lib/i18n/ko.ts, app/src/lib/i18n/pl.ts, app/src/lib/i18n/pt.ts, app/src/lib/i18n/ru.ts, app/src/lib/i18n/zh-CN.ts
Adds mcp.detail.authenticate, mcp.detail.authRequired, and mcp.status.unauthorized to all 14 locale files.

Sequence Diagram(s)

sequenceDiagram
  participant MCP_Server as MCP Server (HTTP)
  participant read_response as read_response (client.rs)
  participant connections as mcp_registry/connections
  participant all_status as all_status / classify_server_status
  participant Frontend as InstalledServerDetail (React)

  MCP_Server-->>read_response: HTTP 401 + WWW-Authenticate header
  read_response->>connections: Err(McpUnauthorizedError { endpoint, resource_metadata })
  connections->>connections: is_unauthorized_error? → ConnectFailure { unauthorized: true }
  connections->>all_status: snapshot failure records
  all_status-->>Frontend: ConnStatus { status: "unauthorized", last_error: None }
  Frontend->>Frontend: render auth-required alert + "Sign in" button
  Note over Frontend: No raw "HTTP 401" string shown to user
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • tinyhumansai/openhuman#3480: Modifies the same McpServersTab.tsx polling and status dot logic that this PR extends to include the unauthorized state.

Suggested reviewers

  • senamakel

🐇 A hop, a skip, a 401 no more,
The bunny found the sign-in door!
"Auth required?" — no raw error in sight,
Amber badge glowing, the status just right.
🔐 Sign in, dear user, and tools will appear,
No HTTP strings — just messages clear! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: surfacing HTTP 401 errors as a distinct 'needs sign-in' state rather than a generic error.
Linked Issues check ✅ Passed The PR fully addresses all acceptance criteria from #3719: typed 401 classification [src/openhuman/mcp_client/], unauthorized state UI display with 'Sign in needed' badge [InstalledServerDetail.tsx, McpStatusBadge.tsx], relabeled 'Sign in' button [InstalledServerDetail.tsx], plain-language error messaging without raw HTTP strings [all UI components], continued polling for recovery [McpServersTab.tsx], i18n support [14 locales], and comprehensive testing [connections.rs, component tests].
Out of Scope Changes check ✅ Passed All changes are scoped to the MCP 401 handling objective: backend typing and classification, UI state handling, i18n entries, status polling logic, and test coverage. No unrelated refactoring or feature additions detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 602d04949e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openhuman/agent_registry/agents/orchestrator/prompt.rs Outdated
@sanil-23

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot added bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jun 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/openhuman/mcp_registry/connections.rs (1)

503-507: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve typed 401s during connected tool calls.

read_response now returns McpUnauthorizedError for any HTTP 401, including tools/call, but this path immediately stringifies it and leaves the stale connection in the live map. If a token expires after connection, status can remain Connected and the raw unauthorized diagnostic can still surface instead of triggering the sign-in flow.

Handle auth expiry before stringifying the tool-call error
-    conn.client
-        .call_tool(tool_name, arguments)
-        .await
-        .map(|r| r.raw_result)
-        .map_err(|e| e.to_string())
+    match conn.client.call_tool(tool_name, arguments).await {
+        Ok(result) => Ok(result.raw_result),
+        Err(err) if is_unauthorized_error(&err) => {
+            let removed = {
+                let mut map = connections().write().await;
+                map.remove(server_id)
+            };
+            if let Some(conn) = removed {
+                let _ = conn.client.close_session().await;
+            }
+            connect_failures().write().await.insert(
+                server_id.to_string(),
+                ConnectFailure {
+                    message: err.to_string(),
+                    auth_required: true,
+                },
+            );
+            Err("[mcp-registry] server requires authentication; please sign in again".to_string())
+        }
+        Err(err) => Err(err.to_string()),
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_registry/connections.rs` around lines 503 - 507, The error
handling in the call_tool path stringifies errors with .map_err(|e|
e.to_string()), which loses the typed McpUnauthorizedError information that
read_response returns for HTTP 401 responses. This prevents the caller from
detecting and properly handling 401 auth expiry errors. Instead of stringifying
the error, preserve the actual error type so that McpUnauthorizedError can be
properly identified and handled by the caller to trigger sign-in flows and
remove stale connections from the live map. Modify the error mapping in the
conn.client.call_tool() chain to return the error type rather than converting it
to a string.
app/src/lib/i18n/es.ts (1)

1368-1388: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Tighten the Spanish auth copy.

"conectarse" makes the server the subject here, which reads awkwardly in UI copy. Use second-person phrasing instead, and make the status label a bit more natural.

💡 Suggested copy
-    'Este servidor necesita que inicies sesión o agregues un token de acceso antes de poder conectarse. Haz clic en “Iniciar sesión” para autenticarte.',
+    'Este servidor necesita que inicies sesión o agregues un token de acceso antes de poder conectarte. Haz clic en “Iniciar sesión” para autenticarte.',
-  'mcp.status.unauthorized': 'Inicio de sesión necesario',
+  'mcp.status.unauthorized': 'Se requiere iniciar sesión',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/i18n/es.ts` around lines 1368 - 1388, In the
mcp.detail.authRequired translation string, rephrase the Spanish text to use
second-person phrasing instead of making the server the grammatical subject,
particularly replacing the awkward "conectarse" construction. Additionally, in
the mcp.status.unauthorized translation string, adjust the label to sound more
natural in Spanish UI copy while maintaining the meaning of login being
required.
src/openhuman/workflows/README.md (1)

17-28: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update stale Called by bullets that still describe removed injection flow.

This section still references “propagates injected skills” and a per-turn injection point, which conflicts with this PR’s event-driven refresh model and inject-module removal.

Suggested patch
-- `src/openhuman/agent/harness/fork_context.rs` — fork context propagates injected skills.
-- `src/openhuman/agent/harness/session/turn.rs` — per-turn injection point.
+- `src/openhuman/agent/harness/fork_context.rs` — fork context propagates installed workflow metadata to sub-agent runs.
+- `src/openhuman/agent/harness/session/turn/{tools,core}.rs` — event-driven installed-skill refresh and one-shot announcement wiring.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/workflows/README.md` around lines 17 - 28, The "Called by"
section in the README.md file contains outdated references to a removed
injection flow. Remove or update the two bullets that reference "fork_context
propagates injected skills" (from src/openhuman/agent/harness/fork_context.rs)
and the per-turn injection point (from
src/openhuman/agent/harness/session/turn.rs) since these mechanisms have been
replaced by the new event-driven refresh model. Keep the remaining bullets that
accurately describe the current skill catalog interactions.
🧹 Nitpick comments (1)
app/src/components/channels/mcp/InstalledServerDetail.test.tsx (1)

282-285: ⚡ Quick win

Strengthen the non-leak assertion for OAuth metadata URLs.

Great coverage here; consider also asserting that the .well-known/oauth-protected-resource style URL is not rendered, since that’s the specific leak pattern described in the auth-401 bug.

Suggested test addition
     expect(screen.getByText('Sign in needed')).toBeInTheDocument();
     expect(screen.getByText(/needs you to sign in or add an access token/i)).toBeInTheDocument();
     expect(screen.queryByText(/HTTP 401/i)).not.toBeInTheDocument();
+    expect(screen.queryByText(/oauth-protected-resource|\.well-known/i)).not.toBeInTheDocument();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/channels/mcp/InstalledServerDetail.test.tsx` around lines
282 - 285, Strengthen the test assertions by adding a check to verify that the
`.well-known/oauth-protected-resource` style OAuth metadata URL is not rendered
in the document. After the existing assertions in the test case (which already
check for "Sign in needed" text and verify "HTTP 401" is not present), add an
additional assertion using screen.queryByText to ensure this specific OAuth
metadata URL pattern does not appear, as this is the exact leak pattern
described in the auth-401 bug being tested.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/components/channels/mcp/InstalledServerDetail.tsx`:
- Around line 259-262: The error display condition in InstalledServerDetail.tsx
allows the error variable to render even when status is 'unauthorized', which
exposes raw error messages that should be hidden in that state. Modify the
conditional check to prevent the error from displaying when status is
'unauthorized' by adding the same status check to the error variable as is
already applied to connStatus?.last_error. Apply the condition status !==
'unauthorized' to the error variable so that both error sources are suppressed
when the unauthorized state is active.

In `@src/openhuman/agent/harness/session/turn/mod.rs`:
- Around line 133-135: The skill announcement message in the frozen catalog
context contains contradictory information: it states newly installed skills
"are in your `## Installed Skills` list" but that list is intentionally frozen
mid-session, which can confuse tool selection. Revise the message text (starting
with the string starting at line 133) to remove the claim that skills are
already visible in the frozen `## Installed Skills` section, and instead focus
on the fact that they are installed and immediately available to use via the
`run_skill` command without needing the block to be unfrozen first.

In `@src/openhuman/mcp_registry/connections.rs`:
- Around line 273-297: The issue is that `connect` updates `LAST_ERRORS` and
`AUTH_REQUIRED` under separate locks, creating a race condition where
`all_status` can observe inconsistent state. Replace the separate locks with a
single atomic failure state structure that stores both the error message and the
failure classification together. In the error handling branch of the `connect`
function (around the `Err(err)` match arm), instead of separately calling
`last_errors().write().await.insert()` and then `auth_required().write().await`
conditionally, insert a single atomic failure state that contains both the error
information and whether it is unauthorized (from `is_unauthorized_error(err)`).
Then update `all_status` (at lines 561-585) to read from this single atomic
structure instead of reading `LAST_ERRORS` and `AUTH_REQUIRED` separately,
ensuring status classification always sees a consistent snapshot of the failure
state.

In `@src/openhuman/skill_runtime/agent/skill_executor/prompt.md`:
- Around line 35-43: The fenced code block example in prompt.md starting with
the "## Completed" section is missing a language identifier, which violates
markdownlint rule MD040. Add the language tag `text` to the opening triple
backticks of this code block by changing ``` to ```text to specify that the
content is plain text output.

---

Outside diff comments:
In `@app/src/lib/i18n/es.ts`:
- Around line 1368-1388: In the mcp.detail.authRequired translation string,
rephrase the Spanish text to use second-person phrasing instead of making the
server the grammatical subject, particularly replacing the awkward "conectarse"
construction. Additionally, in the mcp.status.unauthorized translation string,
adjust the label to sound more natural in Spanish UI copy while maintaining the
meaning of login being required.

In `@src/openhuman/mcp_registry/connections.rs`:
- Around line 503-507: The error handling in the call_tool path stringifies
errors with .map_err(|e| e.to_string()), which loses the typed
McpUnauthorizedError information that read_response returns for HTTP 401
responses. This prevents the caller from detecting and properly handling 401
auth expiry errors. Instead of stringifying the error, preserve the actual error
type so that McpUnauthorizedError can be properly identified and handled by the
caller to trigger sign-in flows and remove stale connections from the live map.
Modify the error mapping in the conn.client.call_tool() chain to return the
error type rather than converting it to a string.

In `@src/openhuman/workflows/README.md`:
- Around line 17-28: The "Called by" section in the README.md file contains
outdated references to a removed injection flow. Remove or update the two
bullets that reference "fork_context propagates injected skills" (from
src/openhuman/agent/harness/fork_context.rs) and the per-turn injection point
(from src/openhuman/agent/harness/session/turn.rs) since these mechanisms have
been replaced by the new event-driven refresh model. Keep the remaining bullets
that accurately describe the current skill catalog interactions.

---

Nitpick comments:
In `@app/src/components/channels/mcp/InstalledServerDetail.test.tsx`:
- Around line 282-285: Strengthen the test assertions by adding a check to
verify that the `.well-known/oauth-protected-resource` style OAuth metadata URL
is not rendered in the document. After the existing assertions in the test case
(which already check for "Sign in needed" text and verify "HTTP 401" is not
present), add an additional assertion using screen.queryByText to ensure this
specific OAuth metadata URL pattern does not appear, as this is the exact leak
pattern described in the auth-401 bug being tested.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5647486a-c129-4a46-ba0a-dcce69727aca

📥 Commits

Reviewing files that changed from the base of the PR and between f3086ca and 602d049.

📒 Files selected for processing (43)
  • app/src/components/channels/mcp/InstalledServerDetail.test.tsx
  • app/src/components/channels/mcp/InstalledServerDetail.tsx
  • app/src/components/channels/mcp/InstalledServerList.tsx
  • app/src/components/channels/mcp/McpConnectionHealthToolbar.tsx
  • app/src/components/channels/mcp/McpServersTab.tsx
  • app/src/components/channels/mcp/McpStatusBadge.test.tsx
  • app/src/components/channels/mcp/McpStatusBadge.tsx
  • app/src/components/channels/mcp/types.ts
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • src/core/event_bus/events.rs
  • src/core/event_bus/events_tests.rs
  • src/openhuman/agent/harness/session/builder/setters.rs
  • src/openhuman/agent/harness/session/tests.rs
  • src/openhuman/agent/harness/session/turn/core.rs
  • src/openhuman/agent/harness/session/turn/mod.rs
  • src/openhuman/agent/harness/session/turn/tools.rs
  • src/openhuman/agent/harness/session/turn_tests.rs
  • src/openhuman/agent/harness/session/types.rs
  • src/openhuman/agent_registry/agents/orchestrator/prompt.md
  • src/openhuman/agent_registry/agents/orchestrator/prompt.rs
  • src/openhuman/mcp_client/client.rs
  • src/openhuman/mcp_client/mod.rs
  • src/openhuman/mcp_registry/connections.rs
  • src/openhuman/mcp_registry/types.rs
  • src/openhuman/skill_runtime/agent/skill_executor/prompt.md
  • src/openhuman/workflows/README.md
  • src/openhuman/workflows/inject.rs
  • src/openhuman/workflows/mod.rs
  • src/openhuman/workflows/ops_create.rs
  • src/openhuman/workflows/ops_install.rs
💤 Files with no reviewable changes (1)
  • src/openhuman/workflows/inject.rs

Comment thread app/src/components/channels/mcp/InstalledServerDetail.tsx Outdated
Comment thread src/openhuman/agent/harness/session/turn/mod.rs Outdated
Comment thread src/openhuman/mcp_registry/connections.rs Outdated
Comment thread src/openhuman/skill_runtime/agent/skill_executor/prompt.md Outdated
sanil-23 added 3 commits June 17, 2026 04:15
…mansai#3719)

MCP HTTP-remote servers that reject the connect handshake with HTTP 401
were collapsed into a generic `Error` carrying the raw transport string
(incl. the internal OAuth metadata URL), leaving the user with no usable
path forward and no tools.

Distinguish auth failures from generic ones end to end:

- core: typed `McpUnauthorizedError` returned from the transport on 401
  (instead of a bare `bail!` string) so the connect path classifies by
  type, not text; a new `AUTH_REQUIRED` set drives a `ServerStatus::
  Unauthorized` in `all_status` and intentionally withholds the raw error
  (no leaked OAuth URL).
- ui: amber "Sign in needed" badge + a graceful, actionable notice
  replacing the raw 401; the primary button relabels to "Sign in" and
  opens the existing auth modal (browser OAuth / token entry); status
  polling now continues for unauthorized/error/connecting servers so the
  state recovers after sign-in or a supervisor reconnect; the health
  toolbar buckets unauthorized under "disconnected" so "Retry all" never
  blindly re-401s it.
- i18n: 3 new keys translated across all 14 locales.

Tests: new Rust classification test + types coverage; new Vitest cases
for the unauthorized badge/notice rendering.
…nches

Factor the per-server status decision out of `all_status` into a pure
`classify_server_status` so the priority order (Disabled > Connected >
Unauthorized > Error > Disconnected) is unit-testable without a live
connection registry or DB — covers the changed lines for the diff
coverage gate.
…en unauthorized

- connections: collapse the separate LAST_ERRORS (message) and AUTH_REQUIRED
  (flag) stores into a single `ConnectFailure { message, unauthorized }` record
  under one lock, so a concurrent `all_status` can never read a torn snapshot
  (message updated but auth flag stale). `connect` writes once; `all_status`
  reads one snapshot.
- InstalledServerDetail: suppress the raw-error block entirely while
  `unauthorized` (covers the local `error` state too, e.g. a reconfigure
  reconnect that re-hits the 401), so the amber sign-in notice is the only
  message shown and no raw transport/OAuth text can re-surface.
@sanil-23 sanil-23 force-pushed the fix/3719-mcp-unreliable-auth branch from 602d049 to 01c820f Compare June 16, 2026 22:50
@sanil-23

Copy link
Copy Markdown
Contributor Author

@coderabbitai I addressed the review and rebased the branch to scope this PR to the #3719 MCP change only:

  • connections.rs (Major — atomic snapshot): fixed in 01c820f1bLAST_ERRORS and AUTH_REQUIRED are now a single ConnectFailure { message, unauthorized } record under one lock, so all_status can no longer read a torn message/flag pair.
  • InstalledServerDetail.tsx (hide error while unauthorized): fixed in 01c820f1b — the raw-error block is fully suppressed while unauthorized (including the local error state), so a reconfigure reconnect that re-hits the 401 cannot re-expose raw text.
  • turn/mod.rs & skill_executor/prompt.md: these came from unrelated prior skills commits that were accidentally on the branch base. The branch has been rebased onto upstream/main to contain only the MCP commits, so both files are now out of this diff (their threads are outdated). They can be handled in a separate skills PR.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

@sanil-23 Thanks for the detailed update! All three concerns look well addressed:

  • Atomic snapshot — unifying LAST_ERRORS + AUTH_REQUIRED into a single ConnectFailure { message, unauthorized } record under one lock is the right fix; it eliminates the TOCTOU window in all_status entirely.
  • Raw-error suppression — fully gating the error block (including local error state) on status !== 'unauthorized' is the clean approach.
  • Scope cleanup — good call rebasing to drop the unrelated skills commits; keeping this PR focused on #3719 makes it easier to review and revert if needed.

I'll kick off a fresh full review against the rebased diff.

🐇 ✨

✅ Action performed

Full review finished.

@oxoxDev oxoxDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve

Walkthrough. A remote MCP server rejecting the connect handshake with HTTP 401 was stranded in a generic Error state and rendered the raw transport string (leaking the internal oauth-protected-resource URL) with no re-auth path. This PR makes 401 a first-class, recoverable Unauthorized state: typed McpUnauthorizedError from the transport, classified via downcast (not message text), withheld raw message, plus an amber "Sign in needed" badge + re-auth button + extended polling, with 3 i18n keys across all locales.

Prior CodeRabbit concerns are STALE — already fixed in head:

  • "separate locks" race — no longer two maps; single ConnectFailure { message, unauthorized } under one lock, written in one insert / read in one snapshot (ConnectFailure Disabled > Connected > Unauthorized > Error > Disconnected).
  • "raw error leak at :259" — error block now gated off entirely when unauthorized (dedicated amber notice renders instead); new test asserts no HTTP 401 text leaks.

Low-sev nitpicks (non-blocking):

  • needs_auth() accessor appears unused by surfaced paths — confirm a caller exists or drop the dead public API.
  • The unauthorized gate also hides a genuine non-auth reconfigure error if the server happens to be unauthorized — acceptable, and called out in a code comment.

Question. For a pure-OAuth server (no declared env fields) — does the re-auth modal present a browser-OAuth affordance, or only a token field? That's the boundary between "401 is actionable" (this PR's stated scope, met) and "resolves end-to-end" (the deferred ACs). Not a blocker.

Scope (make 401 actionable + stop the leak) is delivered with typed classification and a leak-asserting test. CI green.

@sanil-23 sanil-23 merged commit c818f02 into tinyhumansai:main Jun 17, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] MCP server connections are unreliable — auth errors, 401s, and tools unavailable after connect

2 participants