modelcontextprotocol / modelcontextprotocol/python-sdk

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

オープン
#2,847 コメント 5 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

auth bug needs confirmation P1
主要言語
Python
スター
24.3k
フォーク
4k
平均マージ
1日 1時間
マージ済み PR(30日)
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. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

mcp/client/auth/oauth2.py と併せて PR #2660 を確認し、OAuthContext.lock と async_auth_flow に重点を置く。既存の OAuth テストを実行し、GET SSE と token-refresh のパスが yield ポイントをまたぐ場合でも、並行 OAuth repro が失敗しなくなったことを確認する。Done は、maintainer による fix の検証と draft PR のレビューが完了した状態とする。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
api, authentication
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
停滞
明瞭さ
おおむね明確
初心者へのやさしさ
20/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。