Spaces:
Running
Running
Merge pull request #527 from jlowin/mcp-config
Browse filesSupport creating clients from MCP config dicts, including multi-server clients
- README.md +23 -0
- docs/clients/{features.mdx → advanced-features.mdx} +0 -0
- docs/clients/client.mdx +76 -5
- docs/clients/transports.mdx +61 -1
- docs/docs.json +2 -2
- docs/servers/composition.mdx +4 -0
- docs/servers/proxy.mdx +60 -2
- src/fastmcp/client/client.py +9 -1
- src/fastmcp/client/transports.py +119 -14
- src/fastmcp/server/proxy.py +0 -8
- src/fastmcp/server/server.py +2 -0
- src/fastmcp/utilities/mcp_config.py +0 -14
- tests/client/test_client.py +27 -6
- tests/utilities/test_mcp_config.py +50 -0
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. **
|
|
|
|
| 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 |
-
|
| 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/
|
| 71 |
-
"clients/
|
| 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—such as another `FastMCP` instance
|
| 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—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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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.
|
| 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.
|
| 467 |
**session_kwargs,
|
| 468 |
) as session:
|
| 469 |
yield session
|
| 470 |
|
| 471 |
def __repr__(self) -> str:
|
| 472 |
-
return f"<FastMCP(server='{self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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,
|
| 656 |
-
assert transport.
|
| 657 |
-
assert transport.
|
|
|
|
| 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,
|
| 670 |
-
assert transport.
|
| 671 |
-
assert transport.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|