Skip to content

MCP server with static bearer token blocked by OAuth discovery probe #1350

@petabridge-netclaw

Description

@petabridge-netclaw

Problem

When an operator configures a static Authorization header on an HTTP MCP server via netclaw mcp add --header "Authorization: Bearer ...", the daemon blocks the connection entirely with an "Awaiting Auth" status if the server responds to the OAuth discovery probe with a 401 + WWW-Authenticate header containing resource_metadata=.

This means users who want to authenticate via a static bearer token are forced into an interactive OAuth flow that they did not request and may not be able to complete.

Steps to Reproduce

  1. Add an HTTP MCP server with a static bearer token:

    netclaw mcp add --url https://mcp.example.com \
      --transport http \
      --header "Authorization: Bearer my-static-token"
  2. The MCP server returns 401 Unauthorized with WWW-Authenticate: Bearer resource_metadata="..." when probed (common for servers that support both OAuth and Bearer auth, or servers that return 401 on unauthenticated requests).

  3. The daemon detects OAuth metadata in the probe response and blocks the connection with status "AwaitingAuth" and the message "MCP server ... requires OAuth authorization".

  4. The configured static Authorization header is never sent — the connection attempt is aborted before CreateTransport() is reached.

Root Cause

In McpClientManager.CreateClientAsync() (lines 513-528):

// For HTTP transports without cached tokens, check if OAuth is needed
// before attempting a connection that would fail with 401.
if (entry.Transport is not "stdio" && updateStatusOnAuthFailure && entry.Url is not null)
{
    var hasTokens = _oauthService.GetTokenSet(name) is not null;  // ← only checks cached OAuth tokens
    if (!hasTokens)
    {
        var metadata = await _oauthService.TryDiscoverMetadataAsync(name, entry.Url, ct);
        if (metadata is not null)
        {
            _statuses[name] = CreateAwaitingAuthStatus(name);
            _logger.LogWarning("MCP server '{Name}' requires OAuth authorization", name.Value);
            EmitAuthAlert(name, $"MCP server '{name.Value}' requires OAuth authorization. Run: netclaw mcp auth {name.Value}", "authorization_required");
            return null;  // ← BLOCKS THE CONNECTION, never reaches CreateTransport()
        }
    }
}

The check at line 516 only looks for cached OAuth tokens (_oauthService.GetTokenSet(name)). It does not check whether the user has configured a static Authorization header on the server entry (entry.Headers).

Expected Behavior

If the operator has configured a static Authorization header on the MCP server entry, the OAuth discovery probe should be skipped and the configured header should be used for the connection attempt.

Proposed Fix

The condition should skip OAuth discovery if the user has already configured static auth headers:

var hasTokens = _oauthService.GetTokenSet(name) is not null;
var hasStaticHeaders = entry.Headers?.Any(h => 
    h.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase)) == true;

if (!hasTokens && !hasStaticHeaders)  // ← skip discovery if headers are configured
{
    var metadata = await _oauthService.TryDiscoverMetadataAsync(name, entry.Url, ct);
    if (metadata is not null)
    {
        // ... existing OAuth blocking logic
    }
}

Missing Test Coverage

The existing SmokeMcpServerHttpHeaderTests tests headers against a server that does not return OAuth metadata. A test that starts a server returning both OAuth metadata AND expects the static header to be sent would have caught this. See new test McpOAuthHeaderConflictTests.cs in this PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmcpModel context protocol server / client issues.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions