18
18
19
19
>>> import asyncio
20
20
>>> 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
22
22
23
23
Usage:
24
24
Command line usage::
@@ -207,8 +207,11 @@ async def start(self) -> None:
207
207
Creates the subprocess and starts the stdout pump task. The subprocess
208
208
is created with stdin/stdout pipes and stderr passed through.
209
209
210
+ Raises:
211
+ RuntimeError: If the subprocess fails to create stdin/stdout pipes.
212
+
210
213
Examples:
211
- >>> import asyncio
214
+ >>> import asyncio # doctest: +SKIP
212
215
>>> async def test_start(): # doctest: +SKIP
213
216
... pubsub = _PubSub()
214
217
... stdio = StdIOEndpoint("cat", pubsub)
@@ -224,7 +227,11 @@ async def start(self) -> None:
224
227
stdout = asyncio .subprocess .PIPE ,
225
228
stderr = sys .stderr , # passthrough for visibility
226
229
)
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
+
228
235
self ._stdin = self ._proc .stdin
229
236
self ._pump_task = asyncio .create_task (self ._pump_stdout ())
230
237
@@ -287,9 +294,12 @@ async def _pump_stdout(self) -> None:
287
294
to the pubsub system. Runs until EOF or exception.
288
295
289
296
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.
291
299
"""
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
+
293
303
reader = self ._proc .stdout
294
304
try :
295
305
while True :
@@ -480,6 +490,11 @@ def _parse_args(argv: Sequence[str]) -> argparse.Namespace:
480
490
9000
481
491
>>> args.logLevel
482
492
'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'
483
498
"""
484
499
p = argparse .ArgumentParser (
485
500
prog = "mcpgateway.translate" ,
@@ -491,6 +506,7 @@ def _parse_args(argv: Sequence[str]) -> argparse.Namespace:
491
506
src .add_argument ("--streamableHttp" , help = "[NOT IMPLEMENTED]" )
492
507
493
508
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)" )
494
510
p .add_argument (
495
511
"--logLevel" ,
496
512
default = "info" ,
@@ -513,7 +529,7 @@ def _parse_args(argv: Sequence[str]) -> argparse.Namespace:
513
529
return args
514
530
515
531
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 :
517
533
"""Run stdio to SSE bridge.
518
534
519
535
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:
524
540
port: The port to bind the HTTP server to.
525
541
log_level: The logging level to use. Defaults to "info".
526
542
cors: Optional list of CORS allowed origins.
543
+ host: The host interface to bind to. Defaults to "127.0.0.1" for security.
527
544
528
545
Examples:
529
546
>>> import asyncio # doctest: +SKIP
@@ -540,7 +557,7 @@ async def _run_stdio_to_sse(cmd: str, port: int, log_level: str = "info", cors:
540
557
app = _build_fastapi (pubsub , stdio , cors_origins = cors )
541
558
config = uvicorn .Config (
542
559
app ,
543
- host = "0.0.0.0" ,
560
+ host = host , # Changed from hardcoded "0.0.0.0"
544
561
port = port ,
545
562
log_level = log_level ,
546
563
lifespan = "off" ,
@@ -562,19 +579,20 @@ async def _shutdown() -> None:
562
579
with suppress (NotImplementedError ): # Windows lacks add_signal_handler
563
580
loop .add_signal_handler (sig , lambda : asyncio .create_task (_shutdown ()))
564
581
565
- LOGGER .info (f"Bridge ready → http://127.0.0.1 :{ port } /sse" )
582
+ LOGGER .info (f"Bridge ready → http://{ host } :{ port } /sse" )
566
583
await server .serve ()
567
584
await _shutdown () # final cleanup
568
585
569
586
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 :
571
588
"""Run SSE to stdio bridge.
572
589
573
590
Connects to a remote SSE endpoint and bridges it to local stdio.
574
591
575
592
Args:
576
593
url: The SSE endpoint URL to connect to.
577
594
oauth2_bearer: Optional OAuth2 bearer token for authentication.
595
+ timeout: HTTP client timeout in seconds. Defaults to 30.0.
578
596
579
597
Raises:
580
598
ImportError: If httpx package is not available.
@@ -595,7 +613,7 @@ async def _run_sse_to_stdio(url: str, oauth2_bearer: Optional[str]) -> None:
595
613
if oauth2_bearer :
596
614
headers ["Authorization" ] = f"Bearer { oauth2_bearer } "
597
615
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 :
599
617
process = await asyncio .create_subprocess_shell (
600
618
"cat" , # Placeholder command; replace with actual stdio server command if needed
601
619
stdin = asyncio .subprocess .PIPE ,
@@ -604,7 +622,9 @@ async def _run_sse_to_stdio(url: str, oauth2_bearer: Optional[str]) -> None:
604
622
)
605
623
606
624
async def read_stdout () -> None :
607
- assert process .stdout
625
+ if not process .stdout :
626
+ raise RuntimeError ("Process stdout not available" )
627
+
608
628
while True :
609
629
line = await process .stdout .readline ()
610
630
if not line :
@@ -624,7 +644,7 @@ async def pump_sse_to_stdio() -> None:
624
644
await asyncio .gather (read_stdout (), pump_sse_to_stdio ())
625
645
626
646
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 :
628
648
"""Start stdio bridge.
629
649
630
650
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]])
634
654
port: The port to bind the HTTP server to.
635
655
log_level: The logging level to use.
636
656
cors: Optional list of CORS allowed origins.
657
+ host: The host interface to bind to. Defaults to "127.0.0.1".
637
658
638
659
Returns:
639
660
None: This function does not return a value.
640
661
641
662
Examples:
642
663
>>> start_stdio("uvx mcp-server-git", 9000, "info", None) # doctest: +SKIP
643
664
"""
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 ))
645
666
646
667
647
- def start_sse (url : str , bearer : Optional [str ]) -> None :
668
+ def start_sse (url : str , bearer : Optional [str ], timeout : float = 30.0 ) -> None :
648
669
"""Start SSE bridge.
649
670
650
671
Entry point for starting an SSE to stdio bridge client.
651
672
652
673
Args:
653
674
url: The SSE endpoint URL to connect to.
654
675
bearer: Optional OAuth2 bearer token for authentication.
676
+ timeout: HTTP client timeout in seconds. Defaults to 30.0.
655
677
656
678
Returns:
657
679
None: This function does not return a value.
658
680
659
681
Examples:
660
682
>>> start_sse("http://example.com/sse", "token123") # doctest: +SKIP
661
683
"""
662
- return asyncio .run (_run_sse_to_stdio (url , bearer ))
684
+ return asyncio .run (_run_sse_to_stdio (url , bearer , timeout ))
663
685
664
686
665
687
def main (argv : Optional [Sequence [str ]] | None = None ) -> None :
@@ -690,7 +712,7 @@ def main(argv: Optional[Sequence[str]] | None = None) -> None:
690
712
)
691
713
try :
692
714
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 )
694
716
elif args .sse :
695
717
start_sse (args .sse , args .oauth2Bearer )
696
718
except KeyboardInterrupt :
0 commit comments