Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]:
# for browser-based clients (ensures 500 errors get proper CORS headers)
starlette_app = CORSMiddleware(
starlette_app,
allow_origins=["*"], # Allow all origins - adjust as needed for production
# For local/dev browser clients, allow localhost origins. For production, use
# a strict allowlist of known UI origins.
allow_origin_regex=r"https?://(localhost|127\\.0\\.0\\.1)(:\\d+)?",
allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods
expose_headers=["Mcp-Session-Id"],
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]:
# for browser-based clients (ensures 500 errors get proper CORS headers)
starlette_app = CORSMiddleware(
starlette_app,
allow_origins=["*"], # Allow all origins - adjust as needed for production
# For local/dev browser clients, allow localhost origins. For production, use
# a strict allowlist of known UI origins.
allow_origin_regex=r"https?://(localhost|127\\.0\\.0\\.1)(:\\d+)?",
allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods
expose_headers=["Mcp-Session-Id"],
)
Expand Down
11 changes: 3 additions & 8 deletions examples/snippets/clients/url_elicitation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@

import asyncio
import json
import subprocess
import sys
import webbrowser
from typing import Any
from urllib.parse import urlparse
Expand Down Expand Up @@ -124,12 +122,9 @@ def extract_domain(url: str) -> str:
def open_browser(url: str) -> None:
"""Open URL in the default browser."""
try:
if sys.platform == "darwin":
subprocess.run(["open", url], check=False)
elif sys.platform == "win32":
subprocess.run(["start", url], shell=True, check=False)
else:
webbrowser.open(url)
if not webbrowser.open(url, new=2):
print("Could not open browser automatically.")
print(f"Please manually open: {url}")
except Exception as e:
print(f"Failed to open browser: {e}")
print(f"Please manually open: {url}")
Expand Down
3 changes: 2 additions & 1 deletion src/mcp/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ def _get_npx_command():
# Try both npx.cmd and npx.exe on Windows
for cmd in ["npx.cmd", "npx.exe", "npx"]:
try:
# `.cmd` wrappers are common on Windows, so use `shell=True` here.
subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=True)
return cmd
except subprocess.CalledProcessError:
except (subprocess.CalledProcessError, FileNotFoundError):
continue
return None
return "npx" # On Unix-like systems, just use npx
Expand Down
31 changes: 25 additions & 6 deletions tests/client/test_stdio.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,21 @@ class TestChildProcessCleanup:
This is a fundamental difference between Windows and Unix process termination.
"""

async def _wait_for_file_growth(self, file_path: str, initial_size: int, timeout_seconds: float = 2.0) -> int:
"""Wait until a file grows beyond initial_size.

These tests can be a bit timing-sensitive on Windows runners under load,
so prefer polling with a short timeout over a single fixed sleep.
"""
deadline = time.monotonic() + timeout_seconds
size = initial_size
while time.monotonic() < deadline:
await anyio.sleep(0.1)
size = os.path.getsize(file_path)
if size > initial_size:
break
return size

@pytest.mark.anyio
@pytest.mark.filterwarnings("ignore::ResourceWarning" if sys.platform == "win32" else "default")
async def test_basic_child_process_cleanup(self):
Expand All @@ -260,6 +275,13 @@ async def test_basic_child_process_cleanup(self):
parent_marker = f.name

try:
# Before starting any processes, the file should not grow. This also
# exercises the timeout/loop paths in _wait_for_file_growth for branch
# coverage.
initial_size = os.path.getsize(marker_file)
size_no_growth = await self._wait_for_file_growth(marker_file, initial_size, timeout_seconds=0.15)
assert size_no_growth == initial_size

# Parent script that spawns a child process
parent_script = textwrap.dedent(
f"""
Expand Down Expand Up @@ -305,8 +327,7 @@ async def test_basic_child_process_cleanup(self):
# Verify child is writing
if os.path.exists(marker_file): # pragma: no branch
initial_size = os.path.getsize(marker_file)
await anyio.sleep(0.3)
size_after_wait = os.path.getsize(marker_file)
size_after_wait = await self._wait_for_file_growth(marker_file, initial_size)
assert size_after_wait > initial_size, "Child process should be writing"
print(f"Child is writing (file grew from {initial_size} to {size_after_wait} bytes)")

Expand Down Expand Up @@ -405,8 +426,7 @@ async def test_nested_process_tree(self):
for file_path, name in [(parent_file, "parent"), (child_file, "child"), (grandchild_file, "grandchild")]:
if os.path.exists(file_path): # pragma: no branch
initial_size = os.path.getsize(file_path)
await anyio.sleep(0.3)
new_size = os.path.getsize(file_path)
new_size = await self._wait_for_file_growth(file_path, initial_size)
assert new_size > initial_size, f"{name} process should be writing"

# Terminate the whole tree
Expand Down Expand Up @@ -483,8 +503,7 @@ def handle_term(sig, frame):
# Verify child is writing
if os.path.exists(marker_file): # pragma: no branch
size1 = os.path.getsize(marker_file)
await anyio.sleep(0.3)
size2 = os.path.getsize(marker_file)
size2 = await self._wait_for_file_growth(marker_file, size1)
assert size2 > size1, "Child should be writing"

# Terminate - this will kill the process group even if parent exits first
Expand Down