modelcontextprotocol / modelcontextprotocol/python-sdk
`RuntimeError: Attempted to exit cancel scope in a different task` when cleaning up multiple MCPClient instances out-of-order
まだ誰も着手していません。
- 主要言語
- Python
- スター
- 24.3k
- フォーク
- 4k
- 平均マージ
- 1日 1時間
- マージ済み PR(30日)
- 31
説明
Describe the bug
If two MCPClient objects are instantiated and cleaned up in non-FILO order (i.e., the first-created client is cleaned up before the second), teardown fails with a cascade of RuntimeError/CancelledError exceptions coming from anyio and mcp.client.stdio.
To Reproduce
Minimal repro:
import os, asyncio, json
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.types import TextContent
from mcp.client.stdio import stdio_client
class MCPClient:
def __init__(self, command: str, args: list[str], env: Optional[dict] = None):
self.session: Optional[ClientSession] = None
self.command, self.args, self.env = command, args, env
self._cleanup_lock = asyncio.Lock()
self.exit_stack: Optional[AsyncExitStack] = None
async def connect_to_server(self):
await self.cleanup()
self.exit_stack = AsyncExitStack()
server_params = StdioServerParameters(
command=self.command, args=self.args, env=self.env
)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(self.stdio, self.write)
)
await self.session.initialize()
async def cleanup(self):
if self.exit_stack:
async with self._cleanup_lock:
await self.exit_stack.aclose()
self.session = None
self.exit_stack = None
async def main():
cfg = {
"command": "npx",
"args": ["-y", "@adenot/mcp-google-search"],
"env": {
"GOOGLE_API_KEY": os.environ["GOOGLE_API_KEY"],
"GOOGLE_SEARCH_ENGINE_ID": os.environ["GOOGLE_SEARCH_ENGINE_ID"],
},
}
c1, c2 = MCPClient(**cfg), MCPClient(**cfg)
await c1.connect_to_server()
await c2.connect_to_server()
# Works (FILO)
# await c2.cleanup()
# await c1.cleanup()
# Fails (FIFO)
await c1.cleanup() # <-- boom
await c2.cleanup()
if __name__ == "__main__":
asyncio.run(main())
Expected behavior
cleanup() should succeed regardless of the order in which multiple MCPClient instances are closed, as long as each instance’s own exit_stack is intact. A single client ought to manage its own lifetime without depending on external FILO discipline.
Actual Traceback
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
...
asyncio.exceptions.CancelledError: Cancelled by cancel scope ...
...
RuntimeError: Attempted to exit a cancel scope that isn't the current task's current cancel scope
Environment
| Item | Version |
|---|---|
| mcp | 1.6.0 |
| Python | 3.12.10 |
| anyio | 4.9.0 |
| OS | macOS 14.4 (Apple Silicon) |
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
調査の方向性
まず issue にある最小再現を実行し、次に MCPClient.cleanup()、AsyncExitStack、stdio_client、ClientSession の teardown の動作を調べます。独立して作成されたクライアントを RuntimeError や CancelledError なしで FIFO 順にクリーンアップでき、FILO 順のクリーンアップも引き続き機能すれば、修正は完了です。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- python
- 領域
- api, backend
- issue の種類
- バグ
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 活発さ
- 静か
- 明瞭さ
- おおむね明確
- 初心者へのやさしさ
- 48/100