tinyhumansai/openhuman · error · McpUnauthorizedError

MCP unauthorized for `{}` (HTTP 401)

Error message

MCP unauthorized for `{}` (HTTP 401)

What it means

The MCP HTTP client received HTTP 401 from a remote MCP server, meaning the stored OAuth/session credential was rejected or is missing/expired. The client parses the WWW-Authenticate challenge (RFC 9728 resource metadata) to support re-auth. This error maps to McpUnauthorizedError, which observability classifies as McpServerNeedsAuth so the UI can prompt re-authorization rather than showing a generic failure.

Source

Thrown at src/openhuman/mcp/http_client/client.rs:770

};

impl McpHttpClient {
    async fn read_response(&self, response: reqwest::Response) -> anyhow::Result<ResponseEnvelope> {
        let status = response.status();
        let headers = response.headers().clone();
        let content_type = headers
            .get(reqwest::header::CONTENT_TYPE)
            .and_then(|v| v.to_str().ok())
            .unwrap_or("")
            .to_string();
        if status == reqwest::StatusCode::UNAUTHORIZED {
            let resource_metadata = parse_www_authenticate_challenge(&headers)
                .and_then(|challenge| challenge.resource_metadata);
            // Return a TYPED error (not a string `bail!`) so callers can
            // `downcast_ref::<McpUnauthorizedError>()` and surface an
            // actionable "needs authentication" state (#3719) rather than a
            // generic failure. `anyhow` preserves the root type through `?`.
            return Err(anyhow::Error::new(McpUnauthorizedError {
                endpoint: redact_endpoint(&self.endpoint),
                resource_metadata,
            }));
        }
        if !status.is_success() {
            let text = response.text().await.unwrap_or_default();
            anyhow::bail!("MCP HTTP {} — {}", status.as_u16(), text);
        }

        let payload: Value = if content_type.starts_with("text/event-stream") {
            // Read the SSE body incrementally and return as soon as the single
            // JSON-RPC reply frame arrives, instead of buffering to stream-close
            // (`response.text().await`). A server that holds the stream open
            // after replying would otherwise stall this call until the request
            // timeout, and those stalls compound across a skill's tool chain
            // (#4195). The request-level reqwest timeout still bounds the worst
            // case (a server that never replies).
            let mut raw: Vec<u8> = Vec::new();

View on GitHub (pinned to 7491200858)

Solutions

  1. Re-run the OAuth/connect flow for that MCP server from connections settings to obtain a fresh token
  2. Check the server's token expiry requirements and refresh-token rotation; revoke stale tokens server-side if needed
  3. Verify the resource-metadata URL in the WWW-Authenticate challenge is reachable if the client must discover the authorization server
Defensive patterns

Strategy: retry

When it happens

Trigger: Thrown at src/openhuman/mcp/http_client/client.rs:770 when the library encounters an invalid state.

Common situations: See trigger scenarios.

Understand the failure class


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/5aaa83a776a8b39c. Report an issue: GitHub.