ClientSession.call_tool issues a tools/list after every tools/call when the output-schema cache is empty — no opt-out; doubles round-trips on per-call sessions
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức phù hợp với người mới
- 72/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- python
- Lĩnh vực
- api, performance
Hướng nghiên cứu
Bắt đầu trong src/mcp/client/session.py với ClientSession.call_tool và _validate_tool_result, sau đó xem xét list_tools và _absorb_tool_listing để hiểu cách cache được nạp. Chạy bản tái hiện Streamable HTTP và xác nhận chuỗi request hiện tại. Hoàn tất khi hành vi được chọn tránh được round-trip tools/list không cần thiết mà vẫn giữ nguyên việc validation schema đầu ra theo dự định.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
ClientSession.call_tool() issues a tools/list request after every successful tools/call whenever the called tool is not in the session's output-schema cache. On a short-lived session — one ClientSession per tools/call, the pattern gateways and proxies use when the downstream caller is stateless — the cache is empty on every call, so every call_tool costs two round-trips instead of one (initialize + notifications/initialized + tools/call + tools/list = 4 POSTs on Streamable HTTP instead of 3).
When the server behind the session is itself an aggregator whose tools/list fans out to N backends, the extra request is N upstream calls, and the slowest backend's tools/list latency is added to every call_tool — including calls to tools that declare no outputSchema and have nothing to validate.
There is no way to opt out short of subclassing ClientSession or patching the method.
Where
mcp 1.29.1, src/mcp/client/session.py:
# :386-415
async def call_tool(self, name, arguments=None, read_timeout_seconds=None, progress_callback=None, *, meta=None):
...
result = await self.send_request(...)
if not result.isError:
await self._validate_tool_result(name, result)
return result
# :417-421
async def _validate_tool_result(self, name: str, result: types.CallToolResult) -> None:
"""Validate the structured content of a tool result against its output schema."""
if name not in self._tool_output_schemas:
# refresh output schema cache
await self.list_tools()
...
Still present on main @ 6affe5c0 (2026-09-16) as the public validate_tool_result, :1118-1127 — the cache is populated only by list_tools() (_absorb_tool_listing), and _tool_output_schemas is per-ClientSession, so a fresh session always pays the refresh.
Reproduction
import anyio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
async with streamablehttp_client("http://127.0.0.1:8000/mcp") as (r, w, _):
async with ClientSession(r, w) as s:
await s.initialize()
await s.call_tool("echo", {"text": "hi"}) # server access log: POST initialize, POST initialized, POST tools/call, POST tools/list
anyio.run(main)
Any FastMCP server with a tool that returns unstructured content shows the fourth POST. Measured against a proxy whose tools/list fans out to six backends: call_tool wall time p50 ≈ 3 s / p99 ≈ 36 s through the proxy vs p99 0.24 s calling the backend directly — the gap is entirely the post-result tools/list waiting on the slowest backend.
Proposed change (any of these would do)
- A constructor opt-out, e.g.
ClientSession(..., validate_tool_results: bool = True); whenFalse,call_toolreturns the result without callingvalidate_tool_result. Callers that already validate structured output elsewhere (a gateway with its own schema plugin, a server that validates before responding) can turn the client-side re-validation off. - Do not refresh on an empty cache. If the session has never listed tools,
validate_tool_resultcannot know whether the tool has anoutputSchema; today it spends a round-trip to find out. Skipping the refresh whennot self._tool_output_schemas(and keeping it when the cache is populated but lacks the tool — a tool added since the last listing) removes the cost on per-call sessions while leaving long-lived sessions unchanged. A DEBUG log line on the skip keeps it observable. - Validate only when the result carries
structuredContent. A result with nostructuredContentfrom a tool with no cached schema has nothing to check; the refresh then only serves to raiseRuntimeError("… has an output schema but did not return structured content")for a tool the client never listed — a stricter contract than the server side enforces.
Option 2 is what we are running as a build-time patch on a vendored 1.29.1 (one three-line hunk in _validate_tool_result); happy to open a PR for whichever shape the maintainers prefer.
Environment
mcp1.29.1 (Python 3.12); also reproduces onmain@6affe5c0- Transport: Streamable HTTP, stateless server (no
Mcp-Session-Id), oneClientSessionper call
- Ngôn ngữ chính
- Python
- Star
- 24.3k
- Fork
- 4k
- Merge trung bình
- 1 ngày 19 phút
- Pull request đã merge (30 ngày)
- 29
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- 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.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của modelcontextprotocol/python-sdk
-
Streamable HTTP client logs a WARNING for valid 202 Accepted on session termination (DELETE) Đang mởv1 v2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
modelcontextprotocol/python-sdk#3546 · 5 bình luận ·
-
v1 v2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
modelcontextprotocol/python-sdk#3545 · 1 bình luận ·
-
v1 v2
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 91/100
modelcontextprotocol/python-sdk#3508 · 2 bình luận ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 64/100
modelcontextprotocol/python-sdk#3504 ·
-
v1 v2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
modelcontextprotocol/python-sdk#3492 · 1 bình luận ·
Tất cả issue của modelcontextprotocol/python-sdk
Issue tương tự
-
documentation help wanted
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 90/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 90/100
simonw/sqlite-utils#872 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100