modelcontextprotocol / modelcontextprotocol/python-sdk

Bug: OAuth provider holds context.lock for the whole request, so the standalone GET SSE stream stalls the first tools/call by ~15s

Đang mở
#3,209 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

P2 v1 v2
Ngôn ngữ chính
Python
Star
24.3k
Fork
4k
Merge trung bình
1 ngày 1 giờ
Pull request đã merge (30 ngày)
31

Mô tả

Summary

With auth=OAuthClientProvider on a streamable-HTTP connection, the first tools/call of every session blocks for ~15 s. No error, no warning — just latency. Nothing is concurrent here: one server, one client, one session, sequential calls.

Root cause is the same context.lock-held-across-yield problem as #2847 / #2644, but the symptom is different enough that it seems worth recording separately: #2847 is about RuntimeError: The current task is not holding this lock under concurrent OAuth connections, while this is a silent latency tax paid by every OAuth streamable-HTTP user, concurrency or not. Both are fixed by #2858.

Environment

  • mcp==1.29.0, anyio==4.13.0, httpx==0.28.1
  • Python 3.14.6, macOS arm64
  • Server: FastMCP behind Google Cloud Run, streamable HTTP, OAuth 2.1 + PKCE

Measurement

Same server, same tool, one fresh session per call, three calls per arm. The only variable is how the bearer token reaches the request:

auth=OAuthClientProvider   : 15.68s / 15.74s / 15.80s
static Authorization header:  0.70s /  0.62s /  0.67s

Raw HTTP JSON-RPC against the same endpoint (no SDK) answers the same tool in ~0.07 s, so the useful work is ~100 ms and effectively all of the 15.7 s is the wait.

Repro:

import time, anyio
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def one(*, use_provider: bool) -> float:
    headers = None if use_provider else {"Authorization": f"Bearer {ACCESS_TOKEN}"}
    auth = build_provider() if use_provider else None   # OAuthClientProvider
    t0 = time.perf_counter()
    async with streamablehttp_client(URL, headers=headers, auth=auth) as (r, w, _):
        async with ClientSession(r, w) as session:
            await session.initialize()
            await session.call_tool("get_last_modified", {})
    return time.perf_counter() - t0

Why it happens

  1. async_auth_flow takes context.lock and holds it for the whole lifetime of the request, not just while it obtains a token — the async with self.context.lock: at src/mcp/client/auth/oauth2.py:492 wraps response = yield request.
  2. After initialized, streamablehttp_client opens the long-lived standalone GET SSE stream.
  3. That GET goes through async_auth_flow, takes the lock, and does not release it until the stream ends — 15–60 s in our logs, dictated by the server's/proxy's idle timeout, not by the client.
  4. The first tools/call therefore waits on the lock for as long as the GET stream lives.
  5. Once the stream closes, subsequent calls run at ~100 ms until the next stream is opened.

So the stall is not a fixed 15 s: it is "however long the server keeps an idle SSE stream open", which for Cloud Run happens to be ~15 s and for other deployments in this thread ~60 s.

The fix

#2858 (rebase of #2660 by @peisuke) already does the right thing: no HTTP yield — including the standalone GET SSE long-poll — runs while context.lock is held. Verified locally that with the lock out of the request path the same call is ~0.4–0.7 s.

Our workaround in the meantime is to keep OAuthClientProvider out of the request path entirely: read the token from storage and pass it as a plain Authorization header, refreshing it ourselves. That is a fine escape hatch for an application, but it means giving up the SDK's auth handling, which most users reasonably will not want to do.

Would a maintainer be able to look at #2858? It fixes a correctness bug for concurrent users and a ~15 s per-session latency bug for everyone else.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu với src/mcp/client/auth/oauth2.py quanh dòng 492 và entry point streamablehttp_client, sau đó so sánh với bản sửa được đề xuất trong #2858. Tái hiện luồng SSE GET độc lập bằng script được cung cấp và xác minh rằng request không còn chờ context lock của OAuth, đồng thời lệnh gọi tools/call đầu tiên tránh được độ trễ do vòng đời của stream.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python
Lĩnh vực
api, authentication
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Đình trệ
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
25/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.