Jeremiah Lowin commited on
Commit
12847fe
·
1 Parent(s): f084918

Improved support for config dicts

Browse files
src/fastmcp/client/mcp_config.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeAlias
4
+
5
+ from pydantic import Field
6
+ from pydantic.dataclasses import dataclass
7
+
8
+ if TYPE_CHECKING:
9
+ from fastmcp.client.client import Client
10
+ from fastmcp.client.transports import (
11
+ SSETransport,
12
+ StdioTransport,
13
+ StreamableHttpTransport,
14
+ )
15
+
16
+
17
+ @dataclass
18
+ class LocalMCPServer:
19
+ command: str
20
+ args: list[str]
21
+ env: dict[str, Any] = Field(default_factory=dict)
22
+ cwd: str | None = None
23
+
24
+ def to_transport(self) -> StdioTransport:
25
+ from fastmcp.client.transports import StdioTransport
26
+
27
+ return StdioTransport(
28
+ command=self.command,
29
+ args=self.args,
30
+ env=self.env,
31
+ cwd=self.cwd,
32
+ )
33
+
34
+
35
+ @dataclass
36
+ class RemoteMCPServer:
37
+ url: str
38
+ transport: Literal["http", "sse"] | None = None
39
+ headers: dict[str, str] = Field(default_factory=dict)
40
+
41
+ def to_transport(self) -> StreamableHttpTransport | SSETransport:
42
+ from fastmcp.client.transports import SSETransport, StreamableHttpTransport
43
+
44
+ if self.transport in {"http", None}:
45
+ return StreamableHttpTransport(self.url, headers=self.headers)
46
+ else:
47
+ return SSETransport(self.url, headers=self.headers)
48
+
49
+
50
+ MCPServer: TypeAlias = LocalMCPServer | RemoteMCPServer
51
+
52
+
53
+ @dataclass
54
+ class MCPConfig:
55
+ mcp_servers: Annotated[dict[str, MCPServer], Field(alias="mcpServers")]
56
+
57
+ @classmethod
58
+ def from_dict(cls, config: dict[str, Any]) -> MCPConfig:
59
+ return cls(mcp_servers=config.get("mcpServers", config))
60
+
61
+ def to_transports(
62
+ self,
63
+ ) -> dict[str, StdioTransport | StreamableHttpTransport | SSETransport]:
64
+ return {
65
+ name: server.to_transport() for name, server in self.mcp_servers.items()
66
+ }
67
+
68
+ def to_clients(self) -> dict[str, Client]:
69
+ from fastmcp.client.client import Client
70
+
71
+ return {
72
+ name: Client(transport=transport)
73
+ for name, transport in self.to_transports().items()
74
+ }
src/fastmcp/client/transports.py CHANGED
@@ -6,7 +6,7 @@ import shutil
6
  import sys
7
  from collections.abc import AsyncIterator
8
  from pathlib import Path
9
- from typing import Any, TypedDict, cast
10
  from urllib.parse import urlparse
11
 
12
  from mcp import ClientSession, StdioServerParameters
@@ -24,9 +24,13 @@ from mcp.shared.memory import create_connected_server_and_client_session
24
  from pydantic import AnyUrl
25
  from typing_extensions import Unpack
26
 
 
27
  from fastmcp.server import FastMCP as FastMCPServer
28
  from fastmcp.utilities.logging import get_logger
29
 
 
 
 
30
  logger = get_logger(__name__)
31
 
32
 
@@ -470,7 +474,13 @@ class FastMCPTransport(ClientTransport):
470
 
471
 
472
  def infer_transport(
473
- transport: ClientTransport | FastMCPServer | AnyUrl | Path | dict[str, Any] | str,
 
 
 
 
 
 
474
  ) -> ClientTransport:
475
  """
476
  Infer the appropriate transport type from the given transport argument.
@@ -481,6 +491,8 @@ def infer_transport(
481
 
482
  For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`.
483
  """
 
 
484
  # the transport is already a ClientTransport
485
  if isinstance(transport, ClientTransport):
486
  return transport
@@ -511,34 +523,18 @@ def infer_transport(
511
  else:
512
  inferred_transport = StreamableHttpTransport(url=transport)
513
 
514
- ## if the transport is a config dict
515
- elif isinstance(transport, dict):
516
- if "mcpServers" not in transport:
517
- raise ValueError("Invalid transport dictionary: missing 'mcpServers' key")
518
  else:
519
- server = transport["mcpServers"]
520
- if len(list(server.keys())) > 1:
521
- raise ValueError(
522
- "Invalid transport dictionary: multiple servers found - only one expected"
523
- )
524
- server_name = list(server.keys())[0]
525
- # Stdio transport
526
- if "command" in server[server_name] and "args" in server[server_name]:
527
- inferred_transport = StdioTransport(
528
- command=server[server_name]["command"],
529
- args=server[server_name]["args"],
530
- env=server[server_name].get("env", None),
531
- cwd=server[server_name].get("cwd", None),
532
- )
533
-
534
- # HTTP transport
535
- elif "url" in server:
536
- inferred_transport = SSETransport(
537
- url=server["url"],
538
- headers=server.get("headers", None),
539
- )
540
-
541
- raise ValueError("Cannot determine transport type from dictionary")
542
 
543
  # the transport is an unknown type
544
  else:
 
6
  import sys
7
  from collections.abc import AsyncIterator
8
  from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any, TypedDict, cast
10
  from urllib.parse import urlparse
11
 
12
  from mcp import ClientSession, StdioServerParameters
 
24
  from pydantic import AnyUrl
25
  from typing_extensions import Unpack
26
 
27
+ from fastmcp.client.mcp_config import MCPConfig
28
  from fastmcp.server import FastMCP as FastMCPServer
29
  from fastmcp.utilities.logging import get_logger
30
 
31
+ if TYPE_CHECKING:
32
+ from fastmcp.client.mcp_config import MCPConfig
33
+
34
  logger = get_logger(__name__)
35
 
36
 
 
474
 
475
 
476
  def infer_transport(
477
+ transport: ClientTransport
478
+ | FastMCPServer
479
+ | AnyUrl
480
+ | Path
481
+ | MCPConfig
482
+ | dict[str, Any]
483
+ | str,
484
  ) -> ClientTransport:
485
  """
486
  Infer the appropriate transport type from the given transport argument.
 
491
 
492
  For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`.
493
  """
494
+ from fastmcp.client.mcp_config import MCPConfig
495
+
496
  # the transport is already a ClientTransport
497
  if isinstance(transport, ClientTransport):
498
  return transport
 
523
  else:
524
  inferred_transport = StreamableHttpTransport(url=transport)
525
 
526
+ # if the transport is a config dict or MCPConfig
527
+ elif isinstance(transport, dict | MCPConfig):
528
+ if isinstance(transport, dict):
529
+ config = MCPConfig.from_dict(transport)
530
  else:
531
+ config = transport
532
+ inferred_transports = config.to_transports()
533
+ if len(inferred_transports) > 1:
534
+ raise ValueError(
535
+ "Invalid transport dictionary: multiple servers found - only one expected"
536
+ )
537
+ inferred_transport = list(inferred_transports.values())[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
 
539
  # the transport is an unknown type
540
  else: