Studio: pass raise_on_error=False on the stdio MCP call path (#7517)
This commit is contained in:
parent
e662af769b
commit
036fa60095
3 changed files with 81 additions and 9 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue