modelcontextprotocol / modelcontextprotocol/python-sdk

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

未關閉
#2,847 5 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

還沒有人認領這個 Issue。

auth bug needs confirmation P1
主要語言
Python
星號
24.3k
分支
4k
平均合併
1 天 1 小時
30 天內合併 PR
31

描述

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.

貢獻指南

開啟貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

研究方向

結合 mcp/client/auth/oauth2.py 審查 PR #2660,重點關注 OAuthContext.lock 和 async_auth_flow。執行現有的 OAuth 測試,並驗證當 GET SSE 和 token-refresh 路徑跨越 yield 點時,並行 OAuth repro 不再失敗。完成的標準是 maintainer 驗證 fix 並審查 draft PR。

由索引模型根據 Issue 內容生成。

評估

技術堆疊
python
領域
api, authentication
Issue 類型
缺陷
難度
4/5
預估耗時
3-5 天
活躍度
停滯
描述清晰度
基本清楚
新手友好度
20/100

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。