Skip to content

Commit 7997da7

Browse files
authored
412 fix assert in translate (#413)
* Fix bandit low issue in translate Signed-off-by: Mihai Criveti <[email protected]> * Fix bandit issues in translate Signed-off-by: Mihai Criveti <[email protected]> * Fix bandit issues in translate Signed-off-by: Mihai Criveti <[email protected]> --------- Signed-off-by: Mihai Criveti <[email protected]>
1 parent 6429d8d commit 7997da7

File tree

1 file changed

+38
-16
lines changed

1 file changed

+38
-16
lines changed

mcpgateway/translate.py

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
1919
>>> import asyncio
2020
>>> from mcpgateway.translate import start_stdio
21-
>>> asyncio.run(start_stdio("uvx mcp-server-git", 9000, "info", None)) # doctest: +SKIP
21+
>>> asyncio.run(start_stdio("uvx mcp-server-git", 9000, "info", None, "127.0.0.1")) # doctest: +SKIP
2222
2323
Usage:
2424
Command line usage::
@@ -207,8 +207,11 @@ async def start(self) -> None:
207207
Creates the subprocess and starts the stdout pump task. The subprocess
208208
is created with stdin/stdout pipes and stderr passed through.
209209
210+
Raises:
211+
RuntimeError: If the subprocess fails to create stdin/stdout pipes.
212+
210213
Examples:
211-
>>> import asyncio
214+
>>> import asyncio # doctest: +SKIP
212215
>>> async def test_start(): # doctest: +SKIP
213216
... pubsub = _PubSub()
214217
... stdio = StdIOEndpoint("cat", pubsub)
@@ -224,7 +227,11 @@ async def start(self) -> None:
224227
stdout=asyncio.subprocess.PIPE,
225228
stderr=sys.stderr, # passthrough for visibility
226229
)
227-
assert self._proc.stdin and self._proc.stdout
230+
231+
# Explicit error checking
232+
if not self._proc.stdin or not self._proc.stdout:
233+
raise RuntimeError(f"Failed to create subprocess with stdin/stdout pipes for command: {self._cmd}")
234+
228235
self._stdin = self._proc.stdin
229236
self._pump_task = asyncio.create_task(self._pump_stdout())
230237

@@ -287,9 +294,12 @@ async def _pump_stdout(self) -> None:
287294
to the pubsub system. Runs until EOF or exception.
288295
289296
Raises:
290-
Exception: For any error encountered while pumping stdout.
297+
RuntimeError: If process or stdout is not properly initialized.
298+
Exception: For any other error encountered while pumping stdout.
291299
"""
292-
assert self._proc and self._proc.stdout
300+
if not self._proc or not self._proc.stdout:
301+
raise RuntimeError("Process not properly initialized: missing stdout")
302+
293303
reader = self._proc.stdout
294304
try:
295305
while True:
@@ -480,6 +490,11 @@ def _parse_args(argv: Sequence[str]) -> argparse.Namespace:
480490
9000
481491
>>> args.logLevel
482492
'info'
493+
>>> args.host
494+
'127.0.0.1'
495+
>>> args = _parse_args(["--stdio", "cat"]) # Test default parameters
496+
>>> args.host
497+
'127.0.0.1'
483498
"""
484499
p = argparse.ArgumentParser(
485500
prog="mcpgateway.translate",
@@ -491,6 +506,7 @@ def _parse_args(argv: Sequence[str]) -> argparse.Namespace:
491506
src.add_argument("--streamableHttp", help="[NOT IMPLEMENTED]")
492507

493508
p.add_argument("--port", type=int, default=8000, help="HTTP port to bind")
509+
p.add_argument("--host", default="127.0.0.1", help="Host interface to bind (default: 127.0.0.1)")
494510
p.add_argument(
495511
"--logLevel",
496512
default="info",
@@ -513,7 +529,7 @@ def _parse_args(argv: Sequence[str]) -> argparse.Namespace:
513529
return args
514530

515531

516-
async def _run_stdio_to_sse(cmd: str, port: int, log_level: str = "info", cors: Optional[List[str]] = None) -> None:
532+
async def _run_stdio_to_sse(cmd: str, port: int, log_level: str = "info", cors: Optional[List[str]] = None, host: str = "127.0.0.1") -> None:
517533
"""Run stdio to SSE bridge.
518534
519535
Starts a subprocess and exposes it via HTTP/SSE endpoints. Handles graceful
@@ -524,6 +540,7 @@ async def _run_stdio_to_sse(cmd: str, port: int, log_level: str = "info", cors:
524540
port: The port to bind the HTTP server to.
525541
log_level: The logging level to use. Defaults to "info".
526542
cors: Optional list of CORS allowed origins.
543+
host: The host interface to bind to. Defaults to "127.0.0.1" for security.
527544
528545
Examples:
529546
>>> import asyncio # doctest: +SKIP
@@ -540,7 +557,7 @@ async def _run_stdio_to_sse(cmd: str, port: int, log_level: str = "info", cors:
540557
app = _build_fastapi(pubsub, stdio, cors_origins=cors)
541558
config = uvicorn.Config(
542559
app,
543-
host="0.0.0.0",
560+
host=host, # Changed from hardcoded "0.0.0.0"
544561
port=port,
545562
log_level=log_level,
546563
lifespan="off",
@@ -562,19 +579,20 @@ async def _shutdown() -> None:
562579
with suppress(NotImplementedError): # Windows lacks add_signal_handler
563580
loop.add_signal_handler(sig, lambda: asyncio.create_task(_shutdown()))
564581

565-
LOGGER.info(f"Bridge ready → http://127.0.0.1:{port}/sse")
582+
LOGGER.info(f"Bridge ready → http://{host}:{port}/sse")
566583
await server.serve()
567584
await _shutdown() # final cleanup
568585

569586

570-
async def _run_sse_to_stdio(url: str, oauth2_bearer: Optional[str]) -> None:
587+
async def _run_sse_to_stdio(url: str, oauth2_bearer: Optional[str], timeout: float = 30.0) -> None:
571588
"""Run SSE to stdio bridge.
572589
573590
Connects to a remote SSE endpoint and bridges it to local stdio.
574591
575592
Args:
576593
url: The SSE endpoint URL to connect to.
577594
oauth2_bearer: Optional OAuth2 bearer token for authentication.
595+
timeout: HTTP client timeout in seconds. Defaults to 30.0.
578596
579597
Raises:
580598
ImportError: If httpx package is not available.
@@ -595,7 +613,7 @@ async def _run_sse_to_stdio(url: str, oauth2_bearer: Optional[str]) -> None:
595613
if oauth2_bearer:
596614
headers["Authorization"] = f"Bearer {oauth2_bearer}"
597615

598-
async with httpx.AsyncClient(headers=headers, timeout=None) as client:
616+
async with httpx.AsyncClient(headers=headers, timeout=httpx.Timeout(timeout=timeout, connect=10.0)) as client:
599617
process = await asyncio.create_subprocess_shell(
600618
"cat", # Placeholder command; replace with actual stdio server command if needed
601619
stdin=asyncio.subprocess.PIPE,
@@ -604,7 +622,9 @@ async def _run_sse_to_stdio(url: str, oauth2_bearer: Optional[str]) -> None:
604622
)
605623

606624
async def read_stdout() -> None:
607-
assert process.stdout
625+
if not process.stdout:
626+
raise RuntimeError("Process stdout not available")
627+
608628
while True:
609629
line = await process.stdout.readline()
610630
if not line:
@@ -624,7 +644,7 @@ async def pump_sse_to_stdio() -> None:
624644
await asyncio.gather(read_stdout(), pump_sse_to_stdio())
625645

626646

627-
def start_stdio(cmd: str, port: int, log_level: str, cors: Optional[List[str]]) -> None:
647+
def start_stdio(cmd: str, port: int, log_level: str, cors: Optional[List[str]], host: str = "127.0.0.1") -> None:
628648
"""Start stdio bridge.
629649
630650
Entry point for starting a stdio to SSE bridge server.
@@ -634,32 +654,34 @@ def start_stdio(cmd: str, port: int, log_level: str, cors: Optional[List[str]])
634654
port: The port to bind the HTTP server to.
635655
log_level: The logging level to use.
636656
cors: Optional list of CORS allowed origins.
657+
host: The host interface to bind to. Defaults to "127.0.0.1".
637658
638659
Returns:
639660
None: This function does not return a value.
640661
641662
Examples:
642663
>>> start_stdio("uvx mcp-server-git", 9000, "info", None) # doctest: +SKIP
643664
"""
644-
return asyncio.run(_run_stdio_to_sse(cmd, port, log_level, cors))
665+
return asyncio.run(_run_stdio_to_sse(cmd, port, log_level, cors, host))
645666

646667

647-
def start_sse(url: str, bearer: Optional[str]) -> None:
668+
def start_sse(url: str, bearer: Optional[str], timeout: float = 30.0) -> None:
648669
"""Start SSE bridge.
649670
650671
Entry point for starting an SSE to stdio bridge client.
651672
652673
Args:
653674
url: The SSE endpoint URL to connect to.
654675
bearer: Optional OAuth2 bearer token for authentication.
676+
timeout: HTTP client timeout in seconds. Defaults to 30.0.
655677
656678
Returns:
657679
None: This function does not return a value.
658680
659681
Examples:
660682
>>> start_sse("http://example.com/sse", "token123") # doctest: +SKIP
661683
"""
662-
return asyncio.run(_run_sse_to_stdio(url, bearer))
684+
return asyncio.run(_run_sse_to_stdio(url, bearer, timeout))
663685

664686

665687
def main(argv: Optional[Sequence[str]] | None = None) -> None:
@@ -690,7 +712,7 @@ def main(argv: Optional[Sequence[str]] | None = None) -> None:
690712
)
691713
try:
692714
if args.stdio:
693-
start_stdio(args.stdio, args.port, args.logLevel, args.cors)
715+
start_stdio(args.stdio, args.port, args.logLevel, args.cors, args.host)
694716
elif args.sse:
695717
start_sse(args.sse, args.oauth2Bearer)
696718
except KeyboardInterrupt:

0 commit comments

Comments
 (0)