| 349 | |
| 350 | |
| 351 | class McpClient: |
| 352 | def __init__(self, binary, env, stderr_path): |
| 353 | self.stderr_path = stderr_path |
| 354 | self.stderr_stream = stderr_path.open("w", encoding="utf-8") |
| 355 | self.process = subprocess.Popen( |
| 356 | [str(binary)], |
| 357 | stdin=subprocess.PIPE, |
| 358 | stdout=subprocess.PIPE, |
| 359 | stderr=self.stderr_stream, |
| 360 | text=True, |
| 361 | bufsize=1, |
| 362 | env=env, |
| 363 | ) |
| 364 | self.condition = threading.Condition() |
| 365 | self.responses = {} |
| 366 | self.other_output = [] |
| 367 | self.stdout_closed = False |
| 368 | self.reader = threading.Thread(target=self._read_stdout, daemon=True) |
| 369 | self.reader.start() |
| 370 | |
| 371 | def _read_stdout(self): |
| 372 | assert self.process.stdout is not None |
| 373 | for raw_line in self.process.stdout: |
| 374 | line = raw_line.strip() |
| 375 | if not line: |
| 376 | continue |
| 377 | try: |
| 378 | value = json.loads(line) |
| 379 | except json.JSONDecodeError: |
| 380 | value = None |
| 381 | with self.condition: |
| 382 | if isinstance(value, dict) and "id" in value: |
| 383 | self.responses[value["id"]] = value |
| 384 | else: |
| 385 | self.other_output.append(line) |
| 386 | self.condition.notify_all() |
| 387 | with self.condition: |
| 388 | self.stdout_closed = True |
| 389 | self.condition.notify_all() |
| 390 | |
| 391 | def send(self, value): |
| 392 | check(self.process.stdin is not None, "client stdin is already closed") |
| 393 | payload = json.dumps(value, separators=(",", ":")) |
| 394 | try: |
| 395 | self.process.stdin.write(payload + "\n") |
| 396 | self.process.stdin.flush() |
| 397 | except (BrokenPipeError, OSError) as exc: |
| 398 | raise SmokeFailure("thin client closed while sending: " + str(exc)) from exc |
| 399 | |
| 400 | def wait_response(self, request_id, timeout=START_TIMEOUT): |
| 401 | deadline = time.monotonic() + timeout |
| 402 | with self.condition: |
| 403 | while request_id not in self.responses: |
| 404 | remaining = deadline - time.monotonic() |
| 405 | if remaining <= 0 or self.stdout_closed: |
| 406 | break |
| 407 | self.condition.wait(remaining) |
| 408 | if request_id in self.responses: |
no outgoing calls