|
49 | 49 | ) |
50 | 50 |
|
51 | 51 |
|
| 52 | +def _write_mcp_stdio_server_script(path: Path) -> None: |
| 53 | + path.write_text( |
| 54 | + "\n".join( |
| 55 | + ( |
| 56 | + "from __future__ import annotations as _annotations", |
| 57 | + "", |
| 58 | + "from mcp.server.fastmcp import FastMCP", |
| 59 | + "", |
| 60 | + 'mcp = FastMCP("test-mcp", instructions="Be a helpful assistant.")', |
| 61 | + "", |
| 62 | + "@mcp.tool()", |
| 63 | + "def ping() -> str:", |
| 64 | + ' return "pong"', |
| 65 | + "", |
| 66 | + 'if __name__ == "__main__":', |
| 67 | + ' mcp.run("stdio")', |
| 68 | + "", |
| 69 | + ) |
| 70 | + ), |
| 71 | + encoding="utf-8", |
| 72 | + ) |
| 73 | + |
| 74 | + |
| 75 | +def _build_mcp_stdio_test_env( |
| 76 | + *, |
| 77 | + executable: str, |
| 78 | + base_executable: str | None, |
| 79 | + sys_path: list[str], |
| 80 | + environ: dict[str, str], |
| 81 | +) -> tuple[str, dict[str, str]]: |
| 82 | + executable_path = Path(executable) |
| 83 | + python_executable = ( |
| 84 | + str(executable_path) if executable_path.exists() else base_executable or executable |
| 85 | + ) |
| 86 | + python_path_entries = [entry for entry in sys_path if entry] |
| 87 | + mcp_env = dict(environ) |
| 88 | + if python_path_entries: |
| 89 | + existing_pythonpath = mcp_env.get("PYTHONPATH") |
| 90 | + combined_pythonpath = os.pathsep.join( |
| 91 | + ( |
| 92 | + *python_path_entries, |
| 93 | + *([existing_pythonpath] if existing_pythonpath else []), |
| 94 | + ) |
| 95 | + ) |
| 96 | + mcp_env["PYTHONPATH"] = combined_pythonpath |
| 97 | + return python_executable, mcp_env |
| 98 | + |
| 99 | + |
52 | 100 | def test_thread_executor_bridge_runs_sync_tools_on_configured_executor( |
53 | 101 | tmp_path: Path, |
54 | 102 | ) -> None: |
@@ -386,48 +434,18 @@ def test_mcp_toolset_include_instructions_reaches_model_request(tmp_path: Path) |
386 | 434 | from pydantic_ai.mcp import MCPServerStdio |
387 | 435 |
|
388 | 436 | server_script = tmp_path / "mcp_stdio_server.py" |
389 | | - server_script.write_text( |
390 | | - "\n".join( |
391 | | - ( |
392 | | - "from __future__ import annotations as _annotations", |
393 | | - "", |
394 | | - "from mcp.server.fastmcp import FastMCP", |
395 | | - "", |
396 | | - 'mcp = FastMCP("test-mcp", instructions="Be a helpful assistant.")', |
397 | | - "", |
398 | | - "@mcp.tool()", |
399 | | - "def ping() -> str:", |
400 | | - ' return "pong"', |
401 | | - "", |
402 | | - 'if __name__ == "__main__":', |
403 | | - ' mcp.run("stdio")', |
404 | | - "", |
405 | | - ) |
406 | | - ), |
407 | | - encoding="utf-8", |
408 | | - ) |
| 437 | + _write_mcp_stdio_server_script(server_script) |
409 | 438 |
|
410 | 439 | def return_instructions(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse: |
411 | 440 | del messages |
412 | 441 | return ModelResponse(parts=[TextPart(info.instructions or "")]) |
413 | 442 |
|
414 | | - executable_path = Path(sys.executable) |
415 | | - python_executable = ( |
416 | | - str(executable_path) |
417 | | - if executable_path.exists() |
418 | | - else getattr(sys, "_base_executable", sys.executable) or sys.executable |
| 443 | + python_executable, mcp_env = _build_mcp_stdio_test_env( |
| 444 | + executable=sys.executable, |
| 445 | + base_executable=getattr(sys, "_base_executable", sys.executable) or sys.executable, |
| 446 | + sys_path=list(sys.path), |
| 447 | + environ=dict(os.environ), |
419 | 448 | ) |
420 | | - python_path_entries = [entry for entry in sys.path if entry] |
421 | | - mcp_env = dict(os.environ) |
422 | | - if python_path_entries: |
423 | | - existing_pythonpath = mcp_env.get("PYTHONPATH") |
424 | | - combined_pythonpath = os.pathsep.join( |
425 | | - ( |
426 | | - *python_path_entries, |
427 | | - *([existing_pythonpath] if existing_pythonpath else []), |
428 | | - ) |
429 | | - ) |
430 | | - mcp_env["PYTHONPATH"] = combined_pythonpath |
431 | 449 |
|
432 | 450 | agent = Agent( |
433 | 451 | FunctionModel(return_instructions), |
@@ -464,6 +482,34 @@ def return_instructions(messages: list[ModelMessage], info: AgentInfo) -> ModelR |
464 | 482 | ) |
465 | 483 |
|
466 | 484 |
|
| 485 | +def test_mcp_stdio_test_helpers_cover_script_and_env_fallbacks(tmp_path: Path) -> None: |
| 486 | + server_script = tmp_path / "mcp_stdio_server.py" |
| 487 | + _write_mcp_stdio_server_script(server_script) |
| 488 | + assert 'instructions="Be a helpful assistant."' in server_script.read_text(encoding="utf-8") |
| 489 | + |
| 490 | + existing_executable = tmp_path / "python" |
| 491 | + existing_executable.write_text("", encoding="utf-8") |
| 492 | + existing_python, existing_env = _build_mcp_stdio_test_env( |
| 493 | + executable=str(existing_executable), |
| 494 | + base_executable="/fallback-python", |
| 495 | + sys_path=["/repo/src", "", "/repo/tests"], |
| 496 | + environ={"PYTHONPATH": "/already/set"}, |
| 497 | + ) |
| 498 | + assert existing_python == str(existing_executable) |
| 499 | + assert existing_env["PYTHONPATH"] == os.pathsep.join( |
| 500 | + ("/repo/src", "/repo/tests", "/already/set") |
| 501 | + ) |
| 502 | + |
| 503 | + missing_python, missing_env = _build_mcp_stdio_test_env( |
| 504 | + executable=str(tmp_path / "missing-python"), |
| 505 | + base_executable="/fallback-python", |
| 506 | + sys_path=[""], |
| 507 | + environ={}, |
| 508 | + ) |
| 509 | + assert missing_python == "/fallback-python" |
| 510 | + assert "PYTHONPATH" not in missing_env |
| 511 | + |
| 512 | + |
467 | 513 | def test_capability_bridge_helper_and_metadata_edge_paths( |
468 | 514 | monkeypatch: pytest.MonkeyPatch, |
469 | 515 | ) -> None: |
|
0 commit comments