Skip to content

MCP invocation falls through empty OAuth2 token to HTTP field, diverging from discovery #7646

Description

@CorgiBoyG

Description

Follow-up to #7587 / #7590.

In McpTool._get_headers(), the OAuth2 branch currently reads:

if credential.oauth2 and credential.oauth2.access_token:
    headers = {"Authorization": f"Bearer {credential.oauth2.access_token}"}
elif credential.http:
    ...

When an AuthCredential carries an OAuth2 object with an empty/None access_token and an HTTP credential on the same object, the OAuth2 condition is falsy, so control falls through to the elif credential.http: branch and emits an Authorization header from the HTTP field.

The canonical discovery helper build_auth_headers() (used by McpToolset) instead keeps the credential on the OAuth2 branch and emits no header for an empty token:

if credential.oauth2:
    if credential.oauth2.access_token:
        headers = {"Authorization": f"Bearer {credential.oauth2.access_token}"}
elif credential.http:
    ...

Impact

Discovery (McpToolset._get_auth_headers → build_auth_headers) and invocation (McpTool._get_headers) resolve the same credential to different Authorization headers for this mixed shape. Both feed _merge_auth_and_provider_headers, and the MCP session pool key is derived from the merged headers, so the two paths can bind to different pooled sessions — the path-consistency concern that motivated #7548 / #7550.

This is reachable: a custom auth provider may return such a mixed credential (the non-consent path in CredentialManager returns it without clearing fields). It is a narrow edge, not the default OAuth2/HTTP flow.

Reproduction

With an OAuth2 credential whose access_token is empty and an HTTP bearer field present, invocation yields Authorization: Bearer <http-token> while discovery yields no OAuth header. A focused regression test at the create_session() boundary fails on current main with:

Expected: create_session(headers={'Authorization': 'Bearer provider-token'})
Actual:   create_session(headers={'Authorization': 'Bearer stale-http-token'})

Proposed fix

Restore the nested structure so the OAuth2 credential stays on its own branch and an empty token simply emits no header, matching build_auth_headers(). One-line structural change plus a regression test for the mixed-credential case.

I reproduced this on current main and have the fix and verification ready; I'd like to take this follow-up.

Activity

  1. CorgiBoyG commented on Oct 21, 2026

    @CorgiBoyG
    ContributorAuthor

    I'd like to take this. I've reproduced it on current main with a focused regression test that fails with Bearer stale-http-token, and have the minimal structural fix (plus the mixed-credential regression test) verified against the MCP, tools, and auth suites. PR incoming.

  2. added a commit that references this issue on Oct 22, 2026
    14f104b
  3. added
    mcp[Component] This issues is related to MCP support
    on Oct 22, 2026
  4. sanketpatil06 commented on Oct 22, 2026

    @sanketpatil06

    Hi @CorgiBoyG, thanks for the report and the quick fix.

    With an empty OAuth2 token, McpTool._get_headers() falls through to the HTTP field. build_auth_headers() doesn't, so discovery and invocation can bind to different MCP sessions.

    Your PR #7647 fixes this and is under review. We will share updates on its status.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

mcp[Component] This issues is related to MCP support

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions