modelcontextprotocol / modelcontextprotocol/python-sdk

Bug: anyio.Lock in async_auth_flow causes RuntimeError under concurrent OAuth MCP connections

Aperta
#2,847 5 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

auth bug needs confirmation P1
Lingua principale
Python
Stelle
24.3k
Fork
4k
Merge medio
1g 1h
PR unite (30g)
31

Descrizione

Summary

Confirmed production occurrence of the bug described in #2644 (self-closed by the original reporter before they could follow through). Filing to provide a second validated repro and draw attention to the existing draft fix in #2660.

Environment

  • mcp==1.26.0
  • anyio==4.13.0
  • httpx==0.28.1
  • Python 3.11.14, macOS

What happens

When a gateway session starts and connects to multiple OAuth-authenticated MCP servers concurrently (Notion + TinyFish, both OAuth 2.1 PKCE), Notion fails intermittently:

ERROR asyncio: Task exception was never retrieved
RuntimeError: The current task is not holding this lock
  File ".../mcp/client/auth/oauth2.py", line 503, in async_auth_flow
  File ".../mcp/client/auth/oauth2.py", line 484, in async_auth_flow
    raise RuntimeError("The current task is not holding this lock")

WARNING tools.mcp_tool: MCP server 'notion' connection lost (attempt 1/5), reconnecting in 1s
WARNING tools.mcp_tool: Failed to connect to MCP server 'notion': CancelledError
INFO tools.mcp_tool: MCP: registered 114 tool(s) from 4 server(s) (1 failed)

Notion is affected more often than TinyFish because its OAuth token refreshes frequently (~every 15–60 min), consistently triggering the refresh yield path in async_auth_flow. TinyFish tokens expire less often and typically take the happy path (add header, yield once).

Root cause

OAuthContext.lock is anyio.Lock, which records task identity at acquire() and enforces same-task release(). async_auth_flow is an async generator that holds this lock across yield points. When httpx drives the generator from a different task during concurrent connections, anyio.Lock.release() throws.

Existing draft fix

PR #2660 addresses this correctly by narrowing lock scope so no lock is held across yields — GET SSE long-polls and token refresh yields both run outside any lock. It has full test coverage (100% on oauth2.py, 1177 passed) and no breaking changes, but has been sitting as a draft without maintainer review since May 22.

Workaround applied locally

Replacing anyio.Lock with asyncio.Lock in OAuthContext stops the error since asyncio.Lock does not enforce task identity on release. This is a bandaid — it loses trio portability — but unblocks asyncio deployments. Note: this is a mechanical fix and has not been load-tested to exhaustion; the intermittent nature of the bug means full verification requires sustained concurrent load.

Request

Could a maintainer review and merge PR #2660? The fix is principled and well-tested.

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Esaminare la PR #2660 insieme a mcp/client/auth/oauth2.py, concentrandosi su OAuthContext.lock e async_auth_flow. Eseguire i test OAuth esistenti e verificare che la riproduzione concorrente di OAuth non fallisca più quando i percorsi GET SSE e di aggiornamento del token attraversano punti di yield. Il lavoro è completato quando il maintainer ha convalidato il fix e revisionato la PR in bozza.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
python
Ambito
api, authentication
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Ferma
Chiarezza
Abbastanza chiara
Idoneità per principianti
20/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.