OAuth client sends discovery/registration requests with no User-Agent, so WAF-fronted servers (Cloudflare) 403 the whole flow
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 72/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- python
- Domain
- api, authentication
Research direction
Start in mcp/client/auth/utils.py at create_oauth_metadata_request and create_client_registration_request, then trace how OAuthClientProvider.async_auth_flow sends their requests. Use the MockTransport reproduction to compare headers with client.get(); done means discovery and registration requests no longer lose the required User-Agent, with the resulting OAuth flow verified against tests or a comparable local transport.
Written by the indexing model from the issue text.
Description
Summary
OAuthClientProvider builds its OAuth discovery and registration requests as bare httpx.Request objects and sends them via client.send(). httpx merges a client's default headers only in build_request() (i.e. for client.get() / .post()), so these requests go out with no User-Agent and no Accept header.
Any MCP server behind a WAF that blocks user-agent-less traffic — Cloudflare's default bot management does — answers 403 to every one of them, making OAuth impossible to complete. The resulting error is also misattributed, which makes it hard to diagnose.
Reproduction
Against a Cloudflare-fronted MCP server (observed on https://mcp.services.biorender.com/mcp), with mcp==1.26.0:
POST /mcp -> 401 (expected; carries www-authenticate)
GET /.well-known/oauth-authorization-server -> 403 <-- blocked
GET /.well-known/oauth-protected-resource/mcp -> 403 <-- blocked
GET /.well-known/oauth-protected-resource -> 403 <-- blocked
GET /.well-known/oauth-authorization-server -> 403 <-- blocked
POST /register -> 403 <-- note the path
Isolating the trigger with curl against that same discovery URL:
normal curl (UA + Accept present) -> 200
curl -H 'User-Agent:' -H 'Accept:' -> 403
curl -H 'User-Agent:' -> 403 # UA alone is the trigger
curl -H 'Accept:' -> 200 # Accept is not
And confirming the header loss is structural, not server-specific:
import httpx, asyncio
seen = {}
def handler(req):
seen[req.url.path] = dict(req.headers)
return httpx.Response(200, json={})
async def main():
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as c:
await c.get('https://x.test/normal') # client.get()
await c.send(httpx.Request('GET', 'https://x.test/bare')) # what the SDK does
asyncio.run(main())
print('client.get() :', sorted(seen['/normal']))
print('client.send():', sorted(seen['/bare']))
client.get() : ['accept', 'accept-encoding', 'connection', 'host', 'user-agent']
client.send(): ['host']
Secondary problem: the failure is misreported
The 403 lands on metadata discovery, so context.oauth_metadata stays None. create_client_registration_request then falls back to urljoin(auth_base_url, "/register") — but this server's actual registration endpoint is /oauth/register, which its discovery document advertises correctly and which works fine when called with a User-Agent.
So the error surfaced to the user is Registration failed: 403 <cloudflare html>, pointing at a registration request to a path that was never the right one, when the real failure was four requests earlier. Anyone debugging this starts at the wrong end. (This is arguably worth addressing independently: a discovery failure could be reported as a discovery failure rather than silently degrading into a guessed-path registration.)
Affected code
mcp/client/auth/utils.py:211—create_oauth_metadata_request->Request("GET", url, headers={MCP_PROTOCOL_VERSION: ...})mcp/client/auth/utils.py:215-227—create_client_registration_request->Request("POST", registration_url, json=..., headers={"Content-Type": "application/json"})
Both construct Request outside the client, so neither inherits client defaults.
Suggested fix
Set a default User-Agent (e.g. mcp-python-sdk/<version>) on the requests these helpers build, or have OAuthClientProvider.async_auth_flow stamp one onto each request it yields when absent. Adding Accept: application/json to discovery would also be reasonable, though it is not what triggers the block here.
Workaround
Downstream clients can pass an httpx_client_factory that attaches a request event hook, since hooks fire for every request the client sends, including the auth flow's bare ones:
async def _stamp_user_agent(request: httpx.Request) -> None:
if "user-agent" not in request.headers:
request.headers["user-agent"] = "my-app/1.0"
def factory(headers=None, timeout=None, auth=None):
client = create_mcp_http_client(headers=headers, timeout=timeout, auth=auth)
client.event_hooks = {"request": [_stamp_user_agent], "response": []}
return client
Environment
mcp1.26.0,httpx0.28.1,httpx-sse0.4.1,anyio4.11.0- Python 3.11.13, Linux
- Dominant language
- Python
- Stars
- 24.3k
- Forks
- 4k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 30
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from modelcontextprotocol/python-sdk
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
modelcontextprotocol/python-sdk#3566 ·
-
v1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
modelcontextprotocol/python-sdk#3546 · 5 comments ·
-
v1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
modelcontextprotocol/python-sdk#3545 · 1 comment ·
-
v1 v2
Difficulty 1/5 Under an hour Newbie friendliness 91/100
modelcontextprotocol/python-sdk#3508 · 2 comments ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
modelcontextprotocol/python-sdk#3504 ·
All issues in modelcontextprotocol/python-sdk
Similar issues
-
essnmx good first issue
Difficulty 1/5 Under an hour Newbie friendliness 95/100
-
[Feature] 奇物选择添加优先级 Open
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
syfoud/Simulated_Scepter#174 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Giskard-AI/giskard-oss#2840 · 1 comment ·
-
A claim comment carrying the issue number is silently declined while the workflow reports success Openarea: repo bug perceived difficulty: 2
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
yeti-platform/yeti#1380 ·