Studio: pass raise_on_error=False on the stdio MCP call path (#7517)

This commit is contained in:
Nilay 2026-07-29 04:11:47 +05:30 committed by GitHub
commit 036fa60095
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 81 additions and 9 deletions

View file

@ -971,7 +971,12 @@ def _call_stdio_tool(
raise RuntimeError("MCP server connection is not available") raise RuntimeError("MCP server connection is not available")
else: else:
rem = _remaining() rem = _remaining()
coro = _race_tool_call(session.client.call_tool(name, args), rem, cancel_event) # raise_on_error=False for the same reason as the one-shot path.
coro = _race_tool_call(
session.client.call_tool(name, args, raise_on_error = False),
rem,
cancel_event,
)
return session.run(coro, rem) return session.run(coro, rem)
except (_MCPCancelled, asyncio.TimeoutError): except (_MCPCancelled, asyncio.TimeoutError):
# _race_tool_call cancels the pending call but cancellation is # _race_tool_call cancels the pending call but cancellation is

View file

@ -175,3 +175,46 @@ def test_call_tool_sync_passes_raise_on_error_false_and_keeps_error_images(monke
assert out.startswith("Error: boom") assert out.startswith("Error: boom")
assert MCP_IMAGES_SENTINEL in out assert MCP_IMAGES_SENTINEL in out
assert is_tool_error(out) assert is_tool_error(out)
def test_stdio_session_call_also_passes_raise_on_error_false(monkeypatch):
seen = {}
class _FakeStdioClient:
def __init__(self):
self.connected = False
self.transport = SimpleNamespace(_is_session_dead = lambda: False)
async def __aenter__(self):
self.connected = True
return self
async def __aexit__(self, *exc):
self.connected = False
def is_connected(self):
return self.connected
async def call_tool(
self,
name,
args,
raise_on_error = True,
):
seen["raise_on_error"] = raise_on_error
return _result(_text("boom"), _image(), is_error = True)
monkeypatch.setattr(
mcp_client, "_client", lambda url, headers, use_oauth = False: _FakeStdioClient()
)
try:
out = call_tool_sync(
"npx fake-stdio-server", None, "take_screenshot", {}, scope = "s=p:t=thread1"
)
finally:
mcp_client.close_stdio_sessions()
assert seen["raise_on_error"] is False
assert out.startswith("Error: boom")
assert MCP_IMAGES_SENTINEL in out
assert is_tool_error(out)

View file

@ -60,7 +60,12 @@ class FakeClient:
def is_connected(self) -> bool: def is_connected(self) -> bool:
return self.connected return self.connected
async def call_tool(self, name: str, args: dict): async def call_tool(
self,
name: str,
args: dict,
raise_on_error: bool = True,
):
if self.call_delay: if self.call_delay:
await asyncio.sleep(self.call_delay) await asyncio.sleep(self.call_delay)
if self.fail_next: if self.fail_next:
@ -120,10 +125,15 @@ def test_tool_error_does_not_recycle_session(fake_clients, monkeypatch):
from fastmcp.exceptions import ToolError from fastmcp.exceptions import ToolError
class ToolFailure(FakeClient): class ToolFailure(FakeClient):
async def call_tool(self, name, args): async def call_tool(
self,
name,
args,
raise_on_error = True,
):
if name == "boom": if name == "boom":
raise ToolError("tool exploded") # tool-level: session stays connected raise ToolError("tool exploded") # tool-level: session stays connected
return await super().call_tool(name, args) return await super().call_tool(name, args, raise_on_error)
monkeypatch.setattr( monkeypatch.setattr(
mcp_client, "_client", lambda url, headers, use_oauth = False: ToolFailure(url) mcp_client, "_client", lambda url, headers, use_oauth = False: ToolFailure(url)
@ -441,12 +451,17 @@ def test_overlapping_calls_serialize_on_shared_session(fake_clients, monkeypatch
active = 0 active = 0
max_active = 0 max_active = 0
async def call_tool(self, name, args): async def call_tool(
self,
name,
args,
raise_on_error = True,
):
OverlapDetect.active += 1 OverlapDetect.active += 1
OverlapDetect.max_active = max(OverlapDetect.max_active, OverlapDetect.active) OverlapDetect.max_active = max(OverlapDetect.max_active, OverlapDetect.active)
try: try:
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
return await super().call_tool(name, args) return await super().call_tool(name, args, raise_on_error)
finally: finally:
OverlapDetect.active -= 1 OverlapDetect.active -= 1
@ -473,9 +488,14 @@ def test_timeout_budget_spans_connect_and_call(fake_clients, monkeypatch):
await asyncio.sleep(0.4) await asyncio.sleep(0.4)
return await super().__aenter__() return await super().__aenter__()
async def call_tool(self, name, args): async def call_tool(
self,
name,
args,
raise_on_error = True,
):
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
return await super().call_tool(name, args) return await super().call_tool(name, args, raise_on_error)
monkeypatch.setattr(mcp_client, "_client", lambda url, headers, use_oauth = False: SlowBoth(url)) monkeypatch.setattr(mcp_client, "_client", lambda url, headers, use_oauth = False: SlowBoth(url))
start = time.monotonic() start = time.monotonic()
@ -565,7 +585,11 @@ def test_execute_tool_config_check_tracks_row(tmp_path, monkeypatch):
def test_multi_block_result_flattens_through_session(fake_clients): def test_multi_block_result_flattens_through_session(fake_clients):
async def _rich_call(name, args): async def _rich_call(
name,
args,
raise_on_error = True,
):
return SimpleNamespace( return SimpleNamespace(
content = [ content = [
SimpleNamespace(type = "text", text = "### Page"), SimpleNamespace(type = "text", text = "### Page"),