| 95 | |
| 96 | |
| 97 | def wait_response( |
| 98 | process: subprocess.Popen[bytes], |
| 99 | responses: "queue.Queue[dict[str, Any]]", |
| 100 | request_id: int, |
| 101 | timeout: float, |
| 102 | accept_tool_error: bool = False, |
| 103 | ) -> dict[str, Any]: |
| 104 | deadline = time.monotonic() + timeout |
| 105 | while True: |
| 106 | if process.poll() is not None and responses.empty(): |
| 107 | raise SmokeFailure( |
| 108 | f"MCP server exited {process.returncode} before response id={request_id}" |
| 109 | ) |
| 110 | try: |
| 111 | remaining = deadline - time.monotonic() |
| 112 | if remaining <= 0: |
| 113 | raise queue.Empty |
| 114 | message = responses.get(timeout=remaining) |
| 115 | except queue.Empty as error: |
| 116 | raise SmokeFailure(f"timed out waiting for MCP response id={request_id}") from error |
| 117 | if message.get("id") != request_id: |
| 118 | continue |
| 119 | if "result" not in message and "error" not in message: |
| 120 | # An echoed request is not a JSON-RPC response and must never count |
| 121 | # as proof that the server dispatched the request. |
| 122 | continue |
| 123 | if "error" in message: |
| 124 | raise SmokeFailure( |
| 125 | f"MCP response id={request_id} returned JSON-RPC error: {message['error']!r}" |
| 126 | ) |
| 127 | result = message.get("result") |
| 128 | if ( |
| 129 | isinstance(result, dict) |
| 130 | and result.get("isError") is True |
| 131 | and not accept_tool_error |
| 132 | ): |
| 133 | rendered = json.dumps(message, separators=(",", ":"), ensure_ascii=False) |
| 134 | raise SmokeFailure( |
| 135 | f"MCP tool response id={request_id} reported isError=true: {rendered}" |
| 136 | ) |
| 137 | return message |
| 138 | |
| 139 | |
| 140 | def request( |