Jeremiah Lowin commited on
Commit
de817a5
·
unverified ·
2 Parent(s): 10ef66b0dd7621

Merge pull request #527 from jlowin/mcp-config

Browse files

Support creating clients from MCP config dicts, including multi-server clients

README.md CHANGED
@@ -253,6 +253,29 @@ async def main():
253
  # ... use the client
254
  ```
255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  Learn more in the [**Client Documentation**](https://gofastmcp.com/clients/client) and [**Transports Documentation**](https://gofastmcp.com/clients/transports).
257
 
258
  ## Advanced Features
 
253
  # ... use the client
254
  ```
255
 
256
+ FastMCP also supports connecting to multiple servers through a single unified client using the standard MCP configuration format:
257
+
258
+ ```python
259
+ from fastmcp import Client
260
+
261
+ # Standard MCP configuration with multiple servers
262
+ config = {
263
+ "mcpServers": {
264
+ "weather": {"url": "https://weather-api.example.com/mcp"},
265
+ "assistant": {"command": "python", "args": ["./assistant_server.py"]}
266
+ }
267
+ }
268
+
269
+ # Create a client that connects to all servers
270
+ client = Client(config)
271
+
272
+ async def main():
273
+ async with client:
274
+ # Access tools and resources with server prefixes
275
+ forecast = await client.call_tool("weather_get_forecast", {"city": "London"})
276
+ answer = await client.call_tool("assistant_answer_question", {"query": "What is MCP?"})
277
+ ```
278
+
279
  Learn more in the [**Client Documentation**](https://gofastmcp.com/clients/client) and [**Transports Documentation**](https://gofastmcp.com/clients/transports).
280
 
281
  ## Advanced Features
docs/clients/{features.mdx → advanced-features.mdx} RENAMED
File without changes
docs/clients/client.mdx CHANGED
@@ -43,7 +43,8 @@ The following inference rules are used to determine the appropriate `ClientTrans
43
  * If it ends with `.js`: Creates a `NodeStdioTransport` to run the script using `node`.
44
  4. **`AnyUrl` or `str` pointing to a URL that begins with `http://` or `https://`**:
45
  * Creates a `StreamableHttpTransport`
46
- 5. **Other**: Raises a `ValueError` if the type cannot be inferred.
 
47
 
48
  ```python
49
  import asyncio
@@ -52,30 +53,100 @@ from fastmcp import Client, FastMCP
52
  # Example transports (more details in Transports page)
53
  server_instance = FastMCP(name="TestServer") # In-memory server
54
  http_url = "https://example.com/mcp" # HTTP server URL
55
- ws_url = "ws://localhost:9000" # WebSocket server URL
56
  server_script = "my_mcp_server.py" # Path to a Python server file
57
 
58
  # Client automatically infers the transport type
59
  client_in_memory = Client(server_instance)
60
  client_http = Client(http_url)
61
- client_ws = Client(ws_url)
62
  client_stdio = Client(server_script)
63
 
64
  print(client_in_memory.transport)
65
  print(client_http.transport)
66
- print(client_ws.transport)
67
  print(client_stdio.transport)
68
 
69
  # Expected Output (types may vary slightly based on environment):
70
  # <FastMCP(server='TestServer')>
71
  # <StreamableHttp(url='https://example.com/mcp')>
72
- # <WebSocket(url='ws://localhost:9000')>
73
  # <PythonStdioTransport(command='python', args=['/path/to/your/my_mcp_server.py'])>
74
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  <Tip>
76
  For more control over connection details (like headers for SSE, environment variables for Stdio), you can instantiate the specific `ClientTransport` class yourself and pass it to the `Client`. See the [Transports](/clients/transports) page for details.
77
  </Tip>
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  ## Client Usage
80
 
81
  ### Connection Lifecycle
 
43
  * If it ends with `.js`: Creates a `NodeStdioTransport` to run the script using `node`.
44
  4. **`AnyUrl` or `str` pointing to a URL that begins with `http://` or `https://`**:
45
  * Creates a `StreamableHttpTransport`
46
+ 5. **`MCPConfig` or dictionary matching MCPConfig schema**: Creates a client that connects to one or more MCP servers specified in the config.
47
+ 6. **Other**: Raises a `ValueError` if the type cannot be inferred.
48
 
49
  ```python
50
  import asyncio
 
53
  # Example transports (more details in Transports page)
54
  server_instance = FastMCP(name="TestServer") # In-memory server
55
  http_url = "https://example.com/mcp" # HTTP server URL
 
56
  server_script = "my_mcp_server.py" # Path to a Python server file
57
 
58
  # Client automatically infers the transport type
59
  client_in_memory = Client(server_instance)
60
  client_http = Client(http_url)
61
+
62
  client_stdio = Client(server_script)
63
 
64
  print(client_in_memory.transport)
65
  print(client_http.transport)
 
66
  print(client_stdio.transport)
67
 
68
  # Expected Output (types may vary slightly based on environment):
69
  # <FastMCP(server='TestServer')>
70
  # <StreamableHttp(url='https://example.com/mcp')>
 
71
  # <PythonStdioTransport(command='python', args=['/path/to/your/my_mcp_server.py'])>
72
  ```
73
+
74
+ You can also initialize a client from an MCP configuration dictionary or `MCPConfig` file:
75
+
76
+ ```python
77
+ from fastmcp import Client
78
+
79
+ config = {
80
+ "mcpServers": {
81
+ "local": {"command": "python", "args": ["local_server.py"]},
82
+ "remote": {"url": "https://example.com/mcp"},
83
+ }
84
+ }
85
+
86
+ client_config = Client(config)
87
+ ```
88
  <Tip>
89
  For more control over connection details (like headers for SSE, environment variables for Stdio), you can instantiate the specific `ClientTransport` class yourself and pass it to the `Client`. See the [Transports](/clients/transports) page for details.
90
  </Tip>
91
 
92
+ ### Multi-Server Clients
93
+
94
+ <VersionBadge version="2.3.6" />
95
+
96
+ FastMCP supports creating clients that connect to multiple MCP servers through a single client interface using a standard MCP configuration format (`MCPConfig`). This configuration approach makes it easy to connect to multiple specialized servers or create composable systems with a simple, declarative syntax.
97
+
98
+ <Note>
99
+ The MCP configuration format follows an emerging standard and may evolve as the specification matures. FastMCP will strive to maintain compatibility with future versions, but be aware that field names or structure might change.
100
+ </Note>
101
+
102
+ When you create a client with an `MCPConfig` containing multiple servers:
103
+
104
+ 1. FastMCP creates a composite client that internally mounts all servers using their config names as prefixes
105
+ 2. Tools and resources from each server are accessible with appropriate prefixes in the format `servername_toolname` and `protocol://servername/resource/path`
106
+ 3. You interact with this as a single unified client, with requests automatically routed to the appropriate server
107
+
108
+ ```python
109
+ from fastmcp import Client
110
+
111
+ # Create a standard MCP configuration with multiple servers
112
+ config = {
113
+ "mcpServers": {
114
+ # A remote HTTP server
115
+ "weather": {
116
+ "url": "https://weather-api.example.com/mcp",
117
+ "transport": "streamable-http"
118
+ },
119
+ # A local server running via stdio
120
+ "assistant": {
121
+ "command": "python",
122
+ "args": ["./my_assistant_server.py"],
123
+ "env": {"DEBUG": "true"}
124
+ }
125
+ }
126
+ }
127
+
128
+ # Create a client that connects to both servers
129
+ client = Client(config)
130
+
131
+ async def main():
132
+ async with client:
133
+ # Access tools from different servers with prefixes
134
+ weather_data = await client.call_tool("weather_get_forecast", {"city": "London"})
135
+ response = await client.call_tool("assistant_answer_question", {"question": "What's the capital of France?"})
136
+
137
+ # Access resources with prefixed URIs
138
+ weather_icons = await client.read_resource("weather://weather/icons/sunny")
139
+ templates = await client.read_resource("resource://assistant/templates/list")
140
+
141
+ print(f"Weather: {weather_data}")
142
+ print(f"Assistant: {response}")
143
+
144
+ if __name__ == "__main__":
145
+ asyncio.run(main())
146
+ ```
147
+
148
+ If your configuration has only a single server, FastMCP will create a direct client to that server without any prefixing.
149
+
150
  ## Client Usage
151
 
152
  ### Connection Lifecycle
docs/clients/transports.mdx CHANGED
@@ -317,4 +317,64 @@ async def main():
317
  asyncio.run(main())
318
  ```
319
 
320
- Communication happens through efficient in-memory queues, making it very fast and ideal for unit testing.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  asyncio.run(main())
318
  ```
319
 
320
+ Communication happens through efficient in-memory queues, making it very fast and ideal for unit testing.
321
+
322
+ ## Configuration-Based Transports
323
+
324
+ ### MCPConfig Transport
325
+
326
+ <VersionBadge version="2.3.6" />
327
+
328
+ - **Class:** `fastmcp.client.transports.MCPConfigTransport`
329
+ - **Inferred From:** An instance of `MCPConfig` or a dictionary matching the MCPConfig schema
330
+ - **Use Case:** Connecting to one or more MCP servers defined in a configuration object
331
+
332
+ MCPConfig follows an emerging standard for MCP server configuration but is subject to change as the specification evolves. The standard supports both local servers (running via stdio) and remote servers (accessed via HTTP).
333
+
334
+ ```python
335
+ from fastmcp import Client
336
+
337
+ # Configuration for multiple MCP servers (both local and remote)
338
+ config = {
339
+ "mcpServers": {
340
+ # Remote HTTP server
341
+ "weather": {
342
+ "url": "https://weather-api.example.com/mcp",
343
+ "transport": "streamable-http"
344
+ },
345
+ # Local stdio server
346
+ "assistant": {
347
+ "command": "python",
348
+ "args": ["./assistant_server.py"],
349
+ "env": {"DEBUG": "true"}
350
+ },
351
+ # Another remote server
352
+ "calendar": {
353
+ "url": "https://calendar-api.example.com/mcp",
354
+ "transport": "streamable-http"
355
+ }
356
+ }
357
+ }
358
+
359
+ # Create a transport from the config (happens automatically with Client)
360
+ client = Client(config)
361
+
362
+ async def main():
363
+ async with client:
364
+ # Tools are accessible with server name prefixes
365
+ weather = await client.call_tool("weather_get_forecast", {"city": "London"})
366
+ answer = await client.call_tool("assistant_answer_question", {"query": "What is MCP?"})
367
+ events = await client.call_tool("calendar_list_events", {"date": "2023-06-01"})
368
+
369
+ # Resources use prefixed URI paths
370
+ icons = await client.read_resource("weather://weather/icons/sunny")
371
+ docs = await client.read_resource("resource://assistant/docs/mcp")
372
+
373
+ asyncio.run(main())
374
+ ```
375
+
376
+ If your configuration has only a single server, the client will connect directly to that server without any prefixing. This makes it convenient to switch between single and multi-server configurations without changing your client code.
377
+
378
+ <Note>
379
+ The MCPConfig format is an emerging standard for MCP server configuration and may change as the MCP ecosystem evolves. While FastMCP aims to maintain compatibility with future versions, be aware that field names or structure might change.
380
+ </Note>
docs/docs.json CHANGED
@@ -67,8 +67,8 @@
67
  "group": "Clients",
68
  "pages": [
69
  "clients/client",
70
- "clients/features",
71
- "clients/transports"
72
  ]
73
  },
74
  {
 
67
  "group": "Clients",
68
  "pages": [
69
  "clients/client",
70
+ "clients/transports",
71
+ "clients/advanced-features"
72
  ]
73
  },
74
  {
docs/servers/composition.mdx CHANGED
@@ -35,6 +35,10 @@ The choice of importing or mounting depends on your use case and requirements.
35
 
36
  FastMCP supports [MCP proxying](/patterns/proxy), which allows you to mirror a local or remote server in a local FastMCP instance. Proxies are fully compatible with both importing and mounting.
37
 
 
 
 
 
38
  ## Importing (Static Composition)
39
 
40
  The `import_server()` method copies all components (tools, resources, templates, prompts) from one `FastMCP` instance (the *subserver*) into another (the *main server*). A `prefix` is added to avoid naming conflicts.
 
35
 
36
  FastMCP supports [MCP proxying](/patterns/proxy), which allows you to mirror a local or remote server in a local FastMCP instance. Proxies are fully compatible with both importing and mounting.
37
 
38
+ <VersionBadge version="2.3.6" />
39
+
40
+ You can also create proxies from configuration dictionaries that follow the MCPConfig schema, which is useful for quickly connecting to one or more remote servers. See the [Proxy Servers documentation](/servers/proxy#configuration-based-proxies) for details on configuration-based proxying. Note that MCPConfig follows an emerging standard and its format may evolve over time.
41
+
42
  ## Importing (Static Composition)
43
 
44
  The `import_server()` method copies all components (tools, resources, templates, prompts) from one `FastMCP` instance (the *subserver*) into another (the *main server*). A `prefix` is added to avoid naming conflicts.
docs/servers/proxy.mdx CHANGED
@@ -10,7 +10,7 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
10
 
11
  FastMCP provides a powerful proxying capability that allows one FastMCP server instance to act as a frontend for another MCP server (which could be remote, running on a different transport, or even another FastMCP instance). This is achieved using the `FastMCP.as_proxy()` class method.
12
 
13
- `as_proxy()` accepts either an existing `Client` or any argument that can be passed to a `Client` as its `transport` parameter&mdash;such as another `FastMCP` instance or a URL to a remote server.
14
 
15
  ## What is Proxying?
16
 
@@ -46,7 +46,7 @@ from fastmcp import FastMCP
46
 
47
  # Provide the backend in any form accepted by Client
48
  proxy_server = FastMCP.as_proxy(
49
- "backend_server.py", # Could also be a FastMCP instance or a remote URL
50
  name="MyProxyServer" # Optional settings for the proxy
51
  )
52
 
@@ -104,6 +104,64 @@ proxy = FastMCP.as_proxy(
104
  # requests to original_server
105
  ```
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  ## `FastMCPProxy` Class
108
 
109
  Internally, `FastMCP.as_proxy()` uses the `FastMCPProxy` class. You generally don't need to interact with this class directly, but it's available if needed.
 
10
 
11
  FastMCP provides a powerful proxying capability that allows one FastMCP server instance to act as a frontend for another MCP server (which could be remote, running on a different transport, or even another FastMCP instance). This is achieved using the `FastMCP.as_proxy()` class method.
12
 
13
+ `as_proxy()` accepts either an existing `Client` or any argument that can be passed to a `Client` as its `transport` parameter&mdash;such as another `FastMCP` instance, a URL to a remote server, or an MCP configuration dictionary.
14
 
15
  ## What is Proxying?
16
 
 
46
 
47
  # Provide the backend in any form accepted by Client
48
  proxy_server = FastMCP.as_proxy(
49
+ "backend_server.py", # Could also be a FastMCP instance, config dict, or a remote URL
50
  name="MyProxyServer" # Optional settings for the proxy
51
  )
52
 
 
104
  # requests to original_server
105
  ```
106
 
107
+ ### Configuration-Based Proxies
108
+
109
+ <VersionBadge version="2.3.6" />
110
+
111
+ You can create a proxy directly from a configuration dictionary that follows the MCPConfig schema. This is useful for quickly setting up proxies to remote servers without manually configuring each connection detail.
112
+
113
+ ```python
114
+ from fastmcp import FastMCP
115
+
116
+ # Create a proxy directly from a config dictionary
117
+ config = {
118
+ "mcpServers": {
119
+ "default": { # For single server configs, 'default' is commonly used
120
+ "url": "https://example.com/mcp",
121
+ "transport": "streamable-http"
122
+ }
123
+ }
124
+ }
125
+
126
+ # Create a proxy to the configured server
127
+ proxy = FastMCP.as_proxy(config, name="Config-Based Proxy")
128
+
129
+ # Run the proxy with stdio transport for local access
130
+ if __name__ == "__main__":
131
+ proxy.run()
132
+ ```
133
+
134
+ <Note>
135
+ The MCPConfig format follows an emerging standard for MCP server configuration and may evolve as the specification matures. While FastMCP aims to maintain compatibility with future versions, be aware that field names or structure might change.
136
+ </Note>
137
+
138
+ You can also use MCPConfig to create a proxy to multiple servers. When multiple servers are specified, they are automatically mounted with their config names as prefixes, providing a unified interface to all servers:
139
+
140
+ ```python
141
+ from fastmcp import FastMCP
142
+
143
+ # Multi-server configuration
144
+ config = {
145
+ "mcpServers": {
146
+ "weather": {
147
+ "url": "https://weather-api.example.com/mcp",
148
+ "transport": "streamable-http"
149
+ },
150
+ "calendar": {
151
+ "url": "https://calendar-api.example.com/mcp",
152
+ "transport": "streamable-http"
153
+ }
154
+ }
155
+ }
156
+
157
+ # Create a proxy to multiple servers
158
+ composite_proxy = FastMCP.as_proxy(config, name="Composite Proxy")
159
+
160
+ # Tools and resources are accessible with prefixes:
161
+ # - weather_get_forecast, calendar_add_event
162
+ # - weather://weather/icons/sunny, calendar://calendar/events/today
163
+ ```
164
+
165
  ## `FastMCPProxy` Class
166
 
167
  Internally, `FastMCP.as_proxy()` uses the `FastMCPProxy` class. You generally don't need to interact with this class directly, but it's available if needed.
src/fastmcp/client/client.py CHANGED
@@ -25,6 +25,7 @@ from fastmcp.client.sampling import SamplingHandler, create_sampling_callback
25
  from fastmcp.exceptions import ToolError
26
  from fastmcp.server import FastMCP
27
  from fastmcp.utilities.exceptions import get_catch_handlers
 
28
 
29
  from .transports import ClientTransport, SessionKwargs, infer_transport
30
 
@@ -53,6 +54,7 @@ class Client:
53
  - FastMCP: In-process FastMCP server
54
  - AnyUrl | str: URL to connect to
55
  - Path: File path for local socket
 
56
  - dict: Transport configuration
57
  roots: Optional RootsList or RootsHandler for filesystem access
58
  sampling_handler: Optional handler for sampling requests
@@ -77,7 +79,13 @@ class Client:
77
 
78
  def __init__(
79
  self,
80
- transport: ClientTransport | FastMCP | AnyUrl | Path | dict[str, Any] | str,
 
 
 
 
 
 
81
  # Common args
82
  roots: RootsList | RootsHandler | None = None,
83
  sampling_handler: SamplingHandler | None = None,
 
25
  from fastmcp.exceptions import ToolError
26
  from fastmcp.server import FastMCP
27
  from fastmcp.utilities.exceptions import get_catch_handlers
28
+ from fastmcp.utilities.mcp_config import MCPConfig
29
 
30
  from .transports import ClientTransport, SessionKwargs, infer_transport
31
 
 
54
  - FastMCP: In-process FastMCP server
55
  - AnyUrl | str: URL to connect to
56
  - Path: File path for local socket
57
+ - MCPConfig: MCP server configuration
58
  - dict: Transport configuration
59
  roots: Optional RootsList or RootsHandler for filesystem access
60
  sampling_handler: Optional handler for sampling requests
 
79
 
80
  def __init__(
81
  self,
82
+ transport: ClientTransport
83
+ | FastMCP
84
+ | AnyUrl
85
+ | Path
86
+ | MCPConfig
87
+ | dict[str, Any]
88
+ | str,
89
  # Common args
90
  roots: RootsList | RootsHandler | None = None,
91
  sampling_handler: SamplingHandler | None = None,
src/fastmcp/client/transports.py CHANGED
@@ -24,6 +24,7 @@ from pydantic import AnyUrl
24
  from typing_extensions import Unpack
25
 
26
  from fastmcp.server import FastMCP as FastMCPServer
 
27
  from fastmcp.utilities.logging import get_logger
28
  from fastmcp.utilities.mcp_config import MCPConfig, infer_transport_type_from_url
29
 
@@ -74,7 +75,7 @@ class ClientTransport(abc.ABC):
74
  A mcp.ClientSession instance.
75
  """
76
  raise NotImplementedError
77
- yield None # type: ignore
78
 
79
  def __repr__(self) -> str:
80
  # Basic representation for subclasses
@@ -455,7 +456,7 @@ class FastMCPTransport(ClientTransport):
455
  """
456
 
457
  def __init__(self, mcp: FastMCPServer):
458
- self._fastmcp = mcp # Can be FastMCP or MCPServer
459
 
460
  @contextlib.asynccontextmanager
461
  async def connect_session(
@@ -463,13 +464,95 @@ class FastMCPTransport(ClientTransport):
463
  ) -> AsyncIterator[ClientSession]:
464
  # create_connected_server_and_client_session manages the session lifecycle itself
465
  async with create_connected_server_and_client_session(
466
- server=self._fastmcp._mcp_server,
467
  **session_kwargs,
468
  ) as session:
469
  yield session
470
 
471
  def __repr__(self) -> str:
472
- return f"<FastMCP(server='{self._fastmcp.name}')>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
 
474
 
475
  def infer_transport(
@@ -488,7 +571,38 @@ def infer_transport(
488
  argument, handling various input types and converting them to the appropriate
489
  ClientTransport subclass.
490
 
 
 
 
 
 
 
 
491
  For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
  """
493
  from fastmcp.utilities.mcp_config import MCPConfig
494
 
@@ -519,16 +633,7 @@ def infer_transport(
519
 
520
  # if the transport is a config dict or MCPConfig
521
  elif isinstance(transport, dict | MCPConfig):
522
- if isinstance(transport, dict):
523
- config = MCPConfig.from_dict(transport)
524
- else:
525
- config = transport
526
- inferred_transports = config.to_transports()
527
- if len(inferred_transports) > 1:
528
- raise ValueError(
529
- "Invalid transport dictionary: multiple servers found - only one expected"
530
- )
531
- inferred_transport = list(inferred_transports.values())[0]
532
 
533
  # the transport is an unknown type
534
  else:
 
24
  from typing_extensions import Unpack
25
 
26
  from fastmcp.server import FastMCP as FastMCPServer
27
+ from fastmcp.server.server import FastMCP
28
  from fastmcp.utilities.logging import get_logger
29
  from fastmcp.utilities.mcp_config import MCPConfig, infer_transport_type_from_url
30
 
 
75
  A mcp.ClientSession instance.
76
  """
77
  raise NotImplementedError
78
+ yield # type: ignore
79
 
80
  def __repr__(self) -> str:
81
  # Basic representation for subclasses
 
456
  """
457
 
458
  def __init__(self, mcp: FastMCPServer):
459
+ self.server = mcp # Can be FastMCP or MCPServer
460
 
461
  @contextlib.asynccontextmanager
462
  async def connect_session(
 
464
  ) -> AsyncIterator[ClientSession]:
465
  # create_connected_server_and_client_session manages the session lifecycle itself
466
  async with create_connected_server_and_client_session(
467
+ server=self.server._mcp_server,
468
  **session_kwargs,
469
  ) as session:
470
  yield session
471
 
472
  def __repr__(self) -> str:
473
+ return f"<FastMCP(server='{self.server.name}')>"
474
+
475
+
476
+ class MCPConfigTransport(ClientTransport):
477
+ """Transport for connecting to one or more MCP servers defined in an MCPConfig.
478
+
479
+ This transport provides a unified interface to multiple MCP servers defined in an MCPConfig
480
+ object or dictionary matching the MCPConfig schema. It supports two key scenarios:
481
+
482
+ 1. If the MCPConfig contains exactly one server, it creates a direct transport to that server.
483
+ 2. If the MCPConfig contains multiple servers, it creates a composite client by mounting
484
+ all servers on a single FastMCP instance, with each server's name used as its mounting prefix.
485
+
486
+ In the multi-server case, tools are accessible with the prefix pattern `{server_name}_{tool_name}`
487
+ and resources with the pattern `protocol://{server_name}/path/to/resource`.
488
+
489
+ This is particularly useful for creating clients that need to interact with multiple specialized
490
+ MCP servers through a single interface, simplifying client code.
491
+
492
+ Examples:
493
+ ```python
494
+ from fastmcp import Client
495
+ from fastmcp.utilities.mcp_config import MCPConfig
496
+
497
+ # Create a config with multiple servers
498
+ config = {
499
+ "mcpServers": {
500
+ "weather": {
501
+ "url": "https://weather-api.example.com/mcp",
502
+ "transport": "streamable-http"
503
+ },
504
+ "calendar": {
505
+ "url": "https://calendar-api.example.com/mcp",
506
+ "transport": "streamable-http"
507
+ }
508
+ }
509
+ }
510
+
511
+ # Create a client with the config
512
+ client = Client(config)
513
+
514
+ async with client:
515
+ # Access tools with prefixes
516
+ weather = await client.call_tool("weather_get_forecast", {"city": "London"})
517
+ events = await client.call_tool("calendar_list_events", {"date": "2023-06-01"})
518
+
519
+ # Access resources with prefixed URIs
520
+ icons = await client.read_resource("weather://weather/icons/sunny")
521
+ ```
522
+ """
523
+
524
+ def __init__(self, config: MCPConfig | dict):
525
+ from fastmcp.client.client import Client
526
+
527
+ if isinstance(config, dict):
528
+ config = MCPConfig.from_dict(config)
529
+ self.config = config
530
+
531
+ # if there's exactly one server, create a client for that server
532
+ if len(self.config.mcpServers) == 1:
533
+ self.transport = list(self.config.mcpServers.values())[0].to_transport()
534
+
535
+ # otherwise create a composite client
536
+ else:
537
+ composite_server = FastMCP()
538
+
539
+ for name, server in self.config.mcpServers.items():
540
+ server_client = Client(transport=server.to_transport())
541
+ composite_server.mount(
542
+ prefix=name, server=FastMCP.as_proxy(server_client)
543
+ )
544
+
545
+ self.transport = FastMCPTransport(mcp=composite_server)
546
+
547
+ @contextlib.asynccontextmanager
548
+ async def connect_session(
549
+ self, **session_kwargs: Unpack[SessionKwargs]
550
+ ) -> AsyncIterator[ClientSession]:
551
+ async with self.transport.connect_session(**session_kwargs) as session:
552
+ yield session
553
+
554
+ def __repr__(self) -> str:
555
+ return f"<MCPConfig(config='{self.config}')>"
556
 
557
 
558
  def infer_transport(
 
571
  argument, handling various input types and converting them to the appropriate
572
  ClientTransport subclass.
573
 
574
+ The function supports these input types:
575
+ - ClientTransport: Used directly without modification
576
+ - FastMCPServer: Creates an in-memory FastMCPTransport
577
+ - Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js)
578
+ - AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints)
579
+ - MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers
580
+
581
  For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`.
582
+
583
+ For MCPConfig with multiple servers, a composite client is created where each server
584
+ is mounted with its name as prefix. This allows accessing tools and resources from multiple
585
+ servers through a single unified client interface, using naming patterns like
586
+ `servername_toolname` for tools and `protocol://servername/path` for resources.
587
+ If the MCPConfig contains only one server, a direct connection is established without prefixing.
588
+
589
+ Examples:
590
+ ```python
591
+ # Connect to a local Python script
592
+ transport = infer_transport("my_script.py")
593
+
594
+ # Connect to a remote server via HTTP
595
+ transport = infer_transport("http://example.com/mcp")
596
+
597
+ # Connect to multiple servers using MCPConfig
598
+ config = {
599
+ "mcpServers": {
600
+ "weather": {"url": "http://weather.example.com/mcp"},
601
+ "calendar": {"url": "http://calendar.example.com/mcp"}
602
+ }
603
+ }
604
+ transport = infer_transport(config)
605
+ ```
606
  """
607
  from fastmcp.utilities.mcp_config import MCPConfig
608
 
 
633
 
634
  # if the transport is a config dict or MCPConfig
635
  elif isinstance(transport, dict | MCPConfig):
636
+ inferred_transport = MCPConfigTransport(config=transport)
 
 
 
 
 
 
 
 
 
637
 
638
  # the transport is an unknown type
639
  else:
src/fastmcp/server/proxy.py CHANGED
@@ -25,7 +25,6 @@ from fastmcp.server.context import Context
25
  from fastmcp.server.server import FastMCP
26
  from fastmcp.tools.tool import Tool
27
  from fastmcp.utilities.logging import get_logger
28
- from fastmcp.utilities.mcp_config import MCPConfig
29
 
30
  if TYPE_CHECKING:
31
  from fastmcp.server import Context
@@ -178,13 +177,6 @@ class FastMCPProxy(FastMCP):
178
  super().__init__(**kwargs)
179
  self.client = client
180
 
181
- @classmethod
182
- async def from_mcp_config(cls, config: MCPConfig | dict) -> FastMCPProxy:
183
- if isinstance(config, dict):
184
- config = MCPConfig.from_dict(config)
185
- clients = config.to_clients()
186
- return cls(client=clients[list(clients.keys())[0]])
187
-
188
  async def get_tools(self) -> dict[str, Tool]:
189
  tools = await super().get_tools()
190
 
 
25
  from fastmcp.server.server import FastMCP
26
  from fastmcp.tools.tool import Tool
27
  from fastmcp.utilities.logging import get_logger
 
28
 
29
  if TYPE_CHECKING:
30
  from fastmcp.server import Context
 
177
  super().__init__(**kwargs)
178
  self.client = client
179
 
 
 
 
 
 
 
 
180
  async def get_tools(self) -> dict[str, Tool]:
181
  tools = await super().get_tools()
182
 
src/fastmcp/server/server.py CHANGED
@@ -58,6 +58,7 @@ from fastmcp.tools.tool import Tool
58
  from fastmcp.utilities.cache import TimedCache
59
  from fastmcp.utilities.decorators import DecoratedFunction
60
  from fastmcp.utilities.logging import get_logger
 
61
 
62
  if TYPE_CHECKING:
63
  from fastmcp.client import Client
@@ -1206,6 +1207,7 @@ class FastMCP(Generic[LifespanResultT]):
1206
  | FastMCP[Any]
1207
  | AnyUrl
1208
  | Path
 
1209
  | dict[str, Any]
1210
  | str,
1211
  **settings: Any,
 
58
  from fastmcp.utilities.cache import TimedCache
59
  from fastmcp.utilities.decorators import DecoratedFunction
60
  from fastmcp.utilities.logging import get_logger
61
+ from fastmcp.utilities.mcp_config import MCPConfig
62
 
63
  if TYPE_CHECKING:
64
  from fastmcp.client import Client
 
1207
  | FastMCP[Any]
1208
  | AnyUrl
1209
  | Path
1210
+ | MCPConfig
1211
  | dict[str, Any]
1212
  | str,
1213
  **settings: Any,
src/fastmcp/utilities/mcp_config.py CHANGED
@@ -6,7 +6,6 @@ from urllib.parse import urlparse
6
  from pydantic import AnyUrl, BaseModel, Field
7
 
8
  if TYPE_CHECKING:
9
- from fastmcp.client.client import Client
10
  from fastmcp.client.transports import (
11
  SSETransport,
12
  StdioTransport,
@@ -75,16 +74,3 @@ class MCPConfig(BaseModel):
75
  @classmethod
76
  def from_dict(cls, config: dict[str, Any]) -> MCPConfig:
77
  return cls(mcpServers=config.get("mcpServers", config))
78
-
79
- def to_transports(
80
- self,
81
- ) -> dict[str, StdioTransport | StreamableHttpTransport | SSETransport]:
82
- return {name: server.to_transport() for name, server in self.mcpServers.items()}
83
-
84
- def to_clients(self) -> dict[str, Client]:
85
- from fastmcp.client.client import Client
86
-
87
- return {
88
- name: Client(transport=transport)
89
- for name, transport in self.to_transports().items()
90
- }
 
6
  from pydantic import AnyUrl, BaseModel, Field
7
 
8
  if TYPE_CHECKING:
 
9
  from fastmcp.client.transports import (
10
  SSETransport,
11
  StdioTransport,
 
74
  @classmethod
75
  def from_dict(cls, config: dict[str, Any]) -> MCPConfig:
76
  return cls(mcpServers=config.get("mcpServers", config))
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/client/test_client.py CHANGED
@@ -9,6 +9,7 @@ from pydantic import AnyUrl
9
  from fastmcp.client import Client
10
  from fastmcp.client.transports import (
11
  FastMCPTransport,
 
12
  SSETransport,
13
  StdioTransport,
14
  StreamableHttpTransport,
@@ -652,9 +653,10 @@ class TestInferTransport:
652
  }
653
  }
654
  transport = infer_transport(config)
655
- assert isinstance(transport, SSETransport)
656
- assert transport.url == "http://localhost:8000/sse"
657
- assert transport.headers == {"Authorization": "Bearer 123"}
 
658
 
659
  def test_infer_local_transport_from_config(self):
660
  config = {
@@ -666,6 +668,25 @@ class TestInferTransport:
666
  }
667
  }
668
  transport = infer_transport(config)
669
- assert isinstance(transport, StdioTransport)
670
- assert transport.command == "echo"
671
- assert transport.args == ["hello"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  from fastmcp.client import Client
10
  from fastmcp.client.transports import (
11
  FastMCPTransport,
12
+ MCPConfigTransport,
13
  SSETransport,
14
  StdioTransport,
15
  StreamableHttpTransport,
 
653
  }
654
  }
655
  transport = infer_transport(config)
656
+ assert isinstance(transport, MCPConfigTransport)
657
+ assert isinstance(transport.transport, SSETransport)
658
+ assert transport.transport.url == "http://localhost:8000/sse"
659
+ assert transport.transport.headers == {"Authorization": "Bearer 123"}
660
 
661
  def test_infer_local_transport_from_config(self):
662
  config = {
 
668
  }
669
  }
670
  transport = infer_transport(config)
671
+ assert isinstance(transport, MCPConfigTransport)
672
+ assert isinstance(transport.transport, StdioTransport)
673
+ assert transport.transport.command == "echo"
674
+ assert transport.transport.args == ["hello"]
675
+
676
+ def test_infer_composite_client(config):
677
+ config = {
678
+ "mcpServers": {
679
+ "local": {
680
+ "command": "echo",
681
+ "args": ["hello"],
682
+ },
683
+ "remote": {
684
+ "url": "http://localhost:8000/sse",
685
+ "headers": {"Authorization": "Bearer 123"},
686
+ },
687
+ }
688
+ }
689
+ transport = infer_transport(config)
690
+ assert isinstance(transport, MCPConfigTransport)
691
+ assert isinstance(transport.transport, FastMCPTransport)
692
+ assert len(transport.transport.server._mounted_servers) == 2
tests/utilities/test_mcp_config.py CHANGED
@@ -1,3 +1,9 @@
 
 
 
 
 
 
1
  from fastmcp.client.transports import (
2
  SSETransport,
3
  StdioTransport,
@@ -90,3 +96,47 @@ def test_parse_multiple_servers():
90
  assert mcp_config.mcpServers["test_server_2"].command == "echo"
91
  assert mcp_config.mcpServers["test_server_2"].args == ["hello"]
92
  assert mcp_config.mcpServers["test_server_2"].env == {"TEST": "test"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+ from pathlib import Path
3
+
4
+ from mcp.types import TextContent
5
+
6
+ from fastmcp.client.client import Client
7
  from fastmcp.client.transports import (
8
  SSETransport,
9
  StdioTransport,
 
96
  assert mcp_config.mcpServers["test_server_2"].command == "echo"
97
  assert mcp_config.mcpServers["test_server_2"].args == ["hello"]
98
  assert mcp_config.mcpServers["test_server_2"].env == {"TEST": "test"}
99
+
100
+
101
+ async def test_multi_client(tmp_path: Path):
102
+ server_script = inspect.cleandoc("""
103
+ from fastmcp import FastMCP
104
+
105
+ mcp = FastMCP()
106
+
107
+ @mcp.tool()
108
+ def add(a: int, b: int) -> int:
109
+ return a + b
110
+
111
+ if __name__ == '__main__':
112
+ mcp.run()
113
+ """)
114
+
115
+ script_path = tmp_path / "test.py"
116
+ script_path.write_text(server_script)
117
+
118
+ config = {
119
+ "mcpServers": {
120
+ "test_1": {
121
+ "command": "python",
122
+ "args": [str(script_path)],
123
+ },
124
+ "test_2": {
125
+ "command": "python",
126
+ "args": [str(script_path)],
127
+ },
128
+ }
129
+ }
130
+
131
+ client = Client(config)
132
+
133
+ async with client:
134
+ tools = await client.list_tools()
135
+ assert len(tools) == 2
136
+
137
+ result_1 = await client.call_tool("test_1_add", {"a": 1, "b": 2})
138
+ result_2 = await client.call_tool("test_2_add", {"a": 1, "b": 2})
139
+ assert isinstance(result_1[0], TextContent)
140
+ assert result_1[0].text == "3"
141
+ assert isinstance(result_2[0], TextContent)
142
+ assert result_2[0].text == "3"