Skip to content

Commit 881f402

Browse files
authored
fix: harden example auto-runs against PATH and port conflicts (#2770)
1 parent 9088496 commit 881f402

File tree

3 files changed

+119
-6
lines changed

3 files changed

+119
-6
lines changed

examples/mcp/sse_example/main.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
11
import asyncio
22
import os
33
import shutil
4+
import socket
45
import subprocess
56
import time
6-
from typing import Any
7+
from typing import Any, cast
78

89
from agents import Agent, Runner, gen_trace_id, trace
910
from agents.mcp import MCPServer, MCPServerSse
1011
from agents.model_settings import ModelSettings
1112

13+
SSE_HOST = os.getenv("SSE_HOST", "127.0.0.1")
14+
15+
16+
def _choose_port() -> int:
17+
env_port = os.getenv("SSE_PORT")
18+
if env_port:
19+
return int(env_port)
20+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
21+
s.bind((SSE_HOST, 0))
22+
address = cast(tuple[str, int], s.getsockname())
23+
return address[1]
24+
25+
26+
SSE_PORT = _choose_port()
27+
os.environ.setdefault("SSE_PORT", str(SSE_PORT))
28+
SSE_URL = f"http://{SSE_HOST}:{SSE_PORT}/sse"
29+
1230

1331
async def run(mcp_server: MCPServer):
1432
agent = Agent(
@@ -41,7 +59,7 @@ async def main():
4159
async with MCPServerSse(
4260
name="SSE Python Server",
4361
params={
44-
"url": "http://localhost:8000/sse",
62+
"url": SSE_URL,
4563
},
4664
) as server:
4765
trace_id = gen_trace_id()
@@ -58,16 +76,19 @@ async def main():
5876
)
5977

6078
# We'll run the SSE server in a subprocess. Usually this would be a remote server, but for this
61-
# demo, we'll run it locally at http://localhost:8000/sse
79+
# demo, we'll run it locally at SSE_URL.
6280
process: subprocess.Popen[Any] | None = None
6381
try:
6482
this_dir = os.path.dirname(os.path.abspath(__file__))
6583
server_file = os.path.join(this_dir, "server.py")
6684

67-
print("Starting SSE server at http://localhost:8000/sse ...")
85+
print(f"Starting SSE server at {SSE_URL} ...")
6886

6987
# Run `uv run server.py` to start the SSE server
70-
process = subprocess.Popen(["uv", "run", server_file])
88+
env = os.environ.copy()
89+
env.setdefault("SSE_HOST", SSE_HOST)
90+
env.setdefault("SSE_PORT", str(SSE_PORT))
91+
process = subprocess.Popen(["uv", "run", server_file], env=env)
7192
# Give it 3 seconds to start
7293
time.sleep(3)
7394

examples/mcp/sse_example/server.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import os
12
import random
23

34
from mcp.server.fastmcp import FastMCP
45

6+
SSE_HOST = os.getenv("SSE_HOST", "127.0.0.1")
7+
SSE_PORT = int(os.getenv("SSE_PORT", "8000"))
8+
59
# Create server
6-
mcp = FastMCP("Echo Server")
10+
mcp = FastMCP("Echo Server", host=SSE_HOST, port=SSE_PORT)
711

812

913
@mcp.tool()

examples/run_examples.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import argparse
1515
import datetime
16+
import functools
1617
import os
1718
import re
1819
import shlex
@@ -32,13 +33,32 @@
3233
RERUN_FILE_DEFAULT = ROOT_DIR / ".tmp" / "examples-rerun.txt"
3334
DEFAULT_MAIN_LOG = LOG_DIR_DEFAULT / f"main_{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}.log"
3435

36+
COMMON_PATH_HINTS = (
37+
Path.home() / ".local" / "bin",
38+
Path("/opt/homebrew/bin"),
39+
Path("/opt/homebrew/sbin"),
40+
Path("/usr/local/bin"),
41+
Path("/usr/local/sbin"),
42+
)
43+
44+
DISCOVERY_EXCLUDE = {
45+
"examples/run_examples.py",
46+
}
47+
3548
# Examples that are noisy, require extra credentials, or hang in auto runs.
3649
DEFAULT_AUTO_SKIP = {
3750
"examples/agent_patterns/llm_as_a_judge.py",
3851
"examples/agent_patterns/routing.py",
3952
"examples/customer_service/main.py",
4053
"examples/hosted_mcp/connectors.py",
4154
"examples/mcp/git_example/main.py",
55+
# These are helper daemons or multi-process components exercised by sibling examples.
56+
"examples/mcp/manager_example/app.py",
57+
"examples/mcp/manager_example/mcp_server.py",
58+
"examples/mcp/prompt_server/server.py",
59+
"examples/mcp/sse_example/server.py",
60+
"examples/mcp/streamablehttp_custom_client_example/server.py",
61+
"examples/mcp/streamablehttp_example/server.py",
4262
"examples/model_providers/custom_example_agent.py",
4363
"examples/model_providers/custom_example_global.py",
4464
"examples/model_providers/custom_example_provider.py",
@@ -84,6 +104,63 @@ def normalize_relpath(relpath: str) -> str:
84104
return str(PurePosixPath(normalized))
85105

86106

107+
def split_path_entries(path_value: str) -> list[str]:
108+
return [entry for entry in path_value.split(os.pathsep) if entry]
109+
110+
111+
def dedupe_existing_paths(paths: Iterable[str]) -> list[str]:
112+
deduped: list[str] = []
113+
seen: set[str] = set()
114+
for entry in paths:
115+
expanded = os.path.expanduser(entry)
116+
if not expanded or expanded in seen:
117+
continue
118+
if not Path(expanded).exists():
119+
continue
120+
deduped.append(expanded)
121+
seen.add(expanded)
122+
return deduped
123+
124+
125+
@functools.lru_cache(maxsize=1)
126+
def interactive_shell_path() -> str | None:
127+
shell = os.environ.get("SHELL")
128+
if not shell:
129+
return None
130+
131+
shell_name = Path(shell).name
132+
if shell_name not in {"bash", "zsh"}:
133+
return None
134+
135+
try:
136+
result = subprocess.run(
137+
[shell, "-lic", 'printf "%s" "$PATH"'],
138+
capture_output=True,
139+
check=True,
140+
cwd=ROOT_DIR,
141+
text=True,
142+
)
143+
except (OSError, subprocess.SubprocessError):
144+
return None
145+
146+
path_value = result.stdout.strip()
147+
return path_value or None
148+
149+
150+
def build_command_path(base_path: str | None = None) -> str:
151+
candidates: list[str] = []
152+
if base_path is None:
153+
base_path = os.environ.get("PATH", "")
154+
candidates.extend(split_path_entries(base_path))
155+
156+
shell_path = interactive_shell_path()
157+
if shell_path:
158+
candidates.extend(split_path_entries(shell_path))
159+
160+
candidates.extend(str(path) for path in COMMON_PATH_HINTS)
161+
return os.pathsep.join(dedupe_existing_paths(candidates))
162+
163+
87164
def parse_args() -> argparse.Namespace:
88165
parser = argparse.ArgumentParser(description="Run example scripts sequentially.")
89166
parser.add_argument(
@@ -221,6 +298,10 @@ def discover_examples(filters: Iterable[str]) -> list[ExampleScript]:
221298
if not MAIN_PATTERN.search(source):
222299
continue
223300

301+
relpath = normalize_relpath(str(path.relative_to(ROOT_DIR)))
302+
if relpath in DISCOVERY_EXCLUDE:
303+
continue
304+
224305
if filters_lower and not any(
225306
f in str(path.relative_to(ROOT_DIR)).lower() for f in filters_lower
226307
):
@@ -351,6 +432,11 @@ def run_examples(examples: Sequence[ExampleScript], args: argparse.Namespace) ->
351432
buffer_output = not args.no_buffer_output and os.environ.get(
352433
"EXAMPLES_BUFFER_OUTPUT", "1"
353434
).lower() not in {"0", "false", "no", "off"}
435+
command_path = build_command_path()
436+
path_augmented = command_path != os.environ.get("PATH", "")
437+
438+
if path_augmented:
439+
print("Augmented subprocess PATH using interactive shell/common tool directories.")
354440

355441
def safe_write_main(line: str) -> None:
356442
with main_log_lock:
@@ -363,6 +449,7 @@ def run_single(example: ExampleScript) -> ExampleResult:
363449
ensure_dirs(log_path, is_file=True)
364450

365451
env = os.environ.copy()
452+
env["PATH"] = command_path
366453
if auto_mode:
367454
env["EXAMPLES_INTERACTIVE_MODE"] = "auto"
368455
env["APPLY_PATCH_AUTO_APPROVE"] = "1"
@@ -441,6 +528,7 @@ def run_single(example: ExampleScript) -> ExampleResult:
441528
safe_write_main(f"# logs_dir: {logs_dir}")
442529
safe_write_main(f"# jobs: {jobs}")
443530
safe_write_main(f"# buffer_output: {buffer_output}")
531+
safe_write_main(f"# path_augmented: {path_augmented}")
444532

445533
run_list: list[ExampleScript] = []
446534

0 commit comments

Comments
 (0)