19
19
from haystack .core .serialization import generate_qualified_class_name , import_class_by_name
20
20
from haystack .tools import Tool
21
21
from haystack .tools .errors import ToolInvocationError
22
+ from haystack .utils import Secret , deserialize_secrets_inplace
23
+ from haystack .utils .auth import SecretType
22
24
from haystack .utils .url_validation import is_valid_http_url
23
25
24
26
from mcp import ClientSession , StdioServerParameters , types
@@ -356,7 +358,7 @@ class StdioClient(MCPClient):
356
358
MCP client that connects to servers using stdio transport.
357
359
"""
358
360
359
- def __init__ (self , command : str , args : list [str ] | None = None , env : dict [str , str ] | None = None ) -> None :
361
+ def __init__ (self , command : str , args : list [str ] | None = None , env : dict [str , str | Secret ] | None = None ) -> None :
360
362
"""
361
363
Initialize a stdio MCP client.
362
364
@@ -367,7 +369,12 @@ def __init__(self, command: str, args: list[str] | None = None, env: dict[str, s
367
369
super ().__init__ ()
368
370
self .command : str = command
369
371
self .args : list [str ] = args or []
370
- self .env : dict [str , str ] | None = env
372
+ # Resolve Secret values in environment variables
373
+ self .env : dict [str , str ] | None = None
374
+ if env :
375
+ self .env = {
376
+ key : value .resolve_value () if isinstance (value , Secret ) else value for key , value in env .items ()
377
+ }
371
378
logger .debug (f"PROCESS: Created StdioClient for command: { command } { ' ' .join (self .args or [])} " )
372
379
373
380
async def connect (self ) -> list [Tool ]:
@@ -400,7 +407,9 @@ def __init__(self, server_info: "SSEServerInfo") -> None:
400
407
# in post_init we validate the url and set the url field so it is guaranteed to be valid
401
408
# safely ignore the mypy warning here
402
409
self .url : str = server_info .url # type: ignore[assignment]
403
- self .token : str | None = server_info .token
410
+ self .token : str | None = (
411
+ server_info .token .resolve_value () if isinstance (server_info .token , Secret ) else server_info .token
412
+ )
404
413
self .timeout : int = server_info .timeout
405
414
406
415
async def connect (self ) -> list [Tool ]:
@@ -431,7 +440,9 @@ def __init__(self, server_info: "StreamableHttpServerInfo") -> None:
431
440
super ().__init__ ()
432
441
433
442
self .url : str = server_info .url
434
- self .token : str | None = server_info .token
443
+ self .token : str | None = (
444
+ server_info .token .resolve_value () if isinstance (server_info .token , Secret ) else server_info .token
445
+ )
435
446
self .timeout : int = server_info .timeout
436
447
437
448
async def connect (self ) -> list [Tool ]:
@@ -441,6 +452,13 @@ async def connect(self) -> list[Tool]:
441
452
:returns: List of available tools on the server
442
453
:raises MCPConnectionError: If connection to the server fails
443
454
"""
455
+ if streamablehttp_client is None :
456
+ message = (
457
+ "Streamable HTTP client is not available. "
458
+ "This may require a newer version of the mcp package that includes mcp.client.streamable_http"
459
+ )
460
+ raise MCPConnectionError (message = message , operation = "streamable_http_connect" )
461
+
444
462
headers = {"Authorization" : f"Bearer { self .token } " } if self .token else None
445
463
streamablehttp_transport = await self .exit_stack .enter_async_context (
446
464
streamablehttp_client (url = self .url , headers = headers , timeout = timedelta (seconds = self .timeout ))
@@ -475,8 +493,16 @@ def to_dict(self) -> dict[str, Any]:
475
493
result = {"type" : generate_qualified_class_name (type (self ))}
476
494
477
495
# Add all fields from the dataclass
478
- for field in fields (self ):
479
- result [field .name ] = getattr (self , field .name )
496
+ for dataclass_field in fields (self ):
497
+ value = getattr (self , dataclass_field .name )
498
+ if hasattr (value , "to_dict" ):
499
+ result [dataclass_field .name ] = value .to_dict ()
500
+ elif isinstance (value , dict ):
501
+ result [dataclass_field .name ] = {
502
+ k : v .to_dict () if hasattr (v , "to_dict" ) else v for k , v in value .items ()
503
+ }
504
+ else :
505
+ result [dataclass_field .name ] = value
480
506
481
507
return result
482
508
@@ -492,6 +518,26 @@ def from_dict(cls, data: dict[str, Any]) -> "MCPServerInfo":
492
518
data_copy = data .copy ()
493
519
data_copy .pop ("type" , None )
494
520
521
+ secret_types = {e .value for e in SecretType }
522
+ field_names = {f .name for f in fields (cls )}
523
+
524
+ # Iterate over a static list of items to avoid mutation issues
525
+ for name , value in list (data_copy .items ()):
526
+ if name not in field_names or not isinstance (value , dict ):
527
+ continue
528
+
529
+ # Top-level secret?
530
+ if value .get ("type" ) in secret_types :
531
+ deserialize_secrets_inplace (data_copy , keys = [name ])
532
+ continue
533
+
534
+ # Nested secrets (one level deep)
535
+ nested_keys : list [str ] = [
536
+ k for k , v in value .items () if isinstance (v , dict ) and v .get ("type" ) in secret_types
537
+ ]
538
+ if nested_keys :
539
+ deserialize_secrets_inplace (value , keys = nested_keys )
540
+
495
541
# Create an instance of the class with the remaining fields
496
542
return cls (** data_copy )
497
543
@@ -501,6 +547,16 @@ class SSEServerInfo(MCPServerInfo):
501
547
"""
502
548
Data class that encapsulates SSE MCP server connection parameters.
503
549
550
+ For authentication tokens containing sensitive data, you can use Secret objects
551
+ for secure handling and serialization:
552
+
553
+ ```python
554
+ server_info = SSEServerInfo(
555
+ url="https://my-mcp-server.com",
556
+ token=Secret.from_env_var("API_KEY"),
557
+ )
558
+ ```
559
+
504
560
:param url: Full URL of the MCP server (including /sse endpoint)
505
561
:param base_url: Base URL of the MCP server (deprecated, use url instead)
506
562
:param token: Authentication token for the server (optional)
@@ -509,7 +565,7 @@ class SSEServerInfo(MCPServerInfo):
509
565
510
566
url : str | None = None
511
567
base_url : str | None = None # deprecated
512
- token : str | None = None
568
+ token : str | Secret | None = None
513
569
timeout : int = 30
514
570
515
571
def __post_init__ (self ):
@@ -553,13 +609,23 @@ class StreamableHttpServerInfo(MCPServerInfo):
553
609
"""
554
610
Data class that encapsulates streamable HTTP MCP server connection parameters.
555
611
612
+ For authentication tokens containing sensitive data, you can use Secret objects
613
+ for secure handling and serialization:
614
+
615
+ ```python
616
+ server_info = StreamableHttpServerInfo(
617
+ url="https://my-mcp-server.com",
618
+ token=Secret.from_env_var("API_KEY"),
619
+ )
620
+ ```
621
+
556
622
:param url: Full URL of the MCP server (streamable HTTP endpoint)
557
623
:param token: Authentication token for the server (optional)
558
624
:param timeout: Connection timeout in seconds
559
625
"""
560
626
561
627
url : str
562
- token : str | None = None
628
+ token : str | Secret | None = None
563
629
timeout : int = 30
564
630
565
631
def __post_init__ (self ):
@@ -585,11 +651,29 @@ class StdioServerInfo(MCPServerInfo):
585
651
:param command: Command to run (e.g., "python", "node")
586
652
:param args: Arguments to pass to the command
587
653
:param env: Environment variables for the command
654
+
655
+ For environment variables containing sensitive data, you can use Secret objects
656
+ for secure handling and serialization:
657
+
658
+ ```python
659
+ server_info = StdioServerInfo(
660
+ command="uv",
661
+ args=["run", "my-mcp-server"],
662
+ env={
663
+ "WORKSPACE_PATH": "/path/to/workspace", # Plain string
664
+ "API_KEY": Secret.from_env_var("API_KEY"), # Secret object
665
+ }
666
+ )
667
+ ```
668
+
669
+ Secret objects will be properly serialized and deserialized without exposing
670
+ the secret value, while plain strings will be preserved as-is. Use Secret objects
671
+ for sensitive data that needs to be handled securely.
588
672
"""
589
673
590
674
command : str
591
675
args : list [str ] | None = None
592
- env : dict [str , str ] | None = None
676
+ env : dict [str , str | Secret ] | None = None
593
677
594
678
def create_client (self ) -> MCPClient :
595
679
"""
0 commit comments