Skip to content

Commit a2067c9

Browse files
authored
Merge branch 'main' into proxy_tool
2 parents 70d2d28 + b3d0f5b commit a2067c9

File tree

14 files changed

+622
-68
lines changed

14 files changed

+622
-68
lines changed

docs/clients/client.mdx

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,6 @@ The FastMCP Client architecture separates the protocol logic (`Client`) from the
1818
- **`Client`**: Handles sending MCP requests (like `tools/call`, `resources/read`), receiving responses, and managing callbacks.
1919
- **`Transport`**: Responsible for establishing and maintaining the connection to the server (e.g., via WebSockets, SSE, Stdio, or in-memory).
2020

21-
```python
22-
from fastmcp import Client, FastMCP
23-
from fastmcp.client import (
24-
RootsHandler,
25-
RootsList,
26-
LogHandler,
27-
MessageHandler,
28-
SamplingHandler,
29-
ProgressHandler # For handling progress notifications
30-
)
31-
```
32-
3321
### Transports
3422

3523
Clients must be initialized with a `transport`. You can either provide an already instantiated transport object, or provide a transport source and let FastMCP attempt to infer the correct transport to use.
@@ -282,7 +270,7 @@ These methods are especially useful for debugging or when you need to access met
282270

283271
### Additional Features
284272

285-
#### Pinging the server
273+
#### Pinging the Server
286274

287275
The client can be used to ping the server to verify connectivity.
288276

@@ -292,6 +280,29 @@ async with client:
292280
print("Server is reachable")
293281
```
294282

283+
#### Session Management
284+
285+
When using stdio transports, clients support a `keep_alive` feature (enabled by default) that maintains subprocess sessions between connection contexts. You can manually control this behavior using the client's `close()` method.
286+
287+
When `keep_alive=False`, the client will automatically close the session when the context manager exits.
288+
289+
```python
290+
from fastmcp import Client
291+
292+
client = Client("my_mcp_server.py") # keep_alive=True by default
293+
294+
async def example():
295+
async with client:
296+
await client.ping()
297+
298+
async with client:
299+
await client.ping() # Same subprocess as above
300+
```
301+
302+
<Note>
303+
For detailed examples and configuration options, see [Session Management in Transports](/clients/transports#session-management).
304+
</Note>
305+
295306
#### Timeouts
296307

297308
<VersionBadge version="2.3.4" />

docs/clients/transports.mdx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,63 @@ client = Client(transport)
160160

161161
These transports manage an MCP server running as a subprocess, communicating with it via standard input (stdin) and standard output (stdout). This is the standard mechanism used by clients like Claude Desktop.
162162

163+
### Session Management
164+
165+
All stdio transports support a `keep_alive` parameter (default: `True`) that controls session persistence across multiple client context managers:
166+
167+
- **`keep_alive=True` (default)**: The subprocess and session are maintained between client context exits and re-entries. This improves performance when making multiple separate connections to the same server.
168+
- **`keep_alive=False`**: A new subprocess is started for each client context, ensuring complete isolation between sessions.
169+
170+
When `keep_alive=True`, you can manually close the session using `await client.close()` if needed. This will terminate the subprocess and require a new one to be started on the next connection.
171+
172+
<CodeGroup>
173+
```python keep_alive=True
174+
from fastmcp import Client
175+
176+
# Client with keep_alive=True (default)
177+
client = Client("my_mcp_server.py")
178+
179+
async def example():
180+
# First session
181+
async with client:
182+
await client.ping()
183+
184+
# Second session - uses the same subprocess
185+
async with client:
186+
await client.ping()
187+
188+
# Manually close the session
189+
await client.close()
190+
191+
# Third session - will start a new subprocess
192+
async with client:
193+
await client.ping()
194+
195+
asyncio.run(example())
196+
```
197+
```python keep_alive=False
198+
from fastmcp import Client
199+
200+
# Client with keep_alive=False
201+
client = Client("my_mcp_server.py", keep_alive=False)
202+
203+
async def example():
204+
# First session
205+
async with client:
206+
await client.ping()
207+
208+
# Second session - will start a new subprocess
209+
async with client:
210+
await client.ping()
211+
212+
# Third session - will start a new subprocess
213+
async with client:
214+
await client.ping()
215+
216+
asyncio.run(example())
217+
```
218+
</CodeGroup>
219+
163220
### Python Stdio
164221

165222
- **Class:** `fastmcp.client.transports.PythonStdioTransport`
@@ -218,7 +275,7 @@ client = Client(node_server_script)
218275
# Option 2: Explicit transport
219276
transport = NodeStdioTransport(
220277
script_path=node_server_script,
221-
node_cmd="node" # Optional: specify path to Node executable
278+
node_cmd="node", # Optional: specify path to Node executable
222279
)
223280
client = Client(transport)
224281

docs/patterns/http-requests.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ async def safe_header_info() -> dict:
7777
}
7878
```
7979

80-
By default, `get_http_headers()` excludes problematic headers like `content-length`. To include all headers, use `get_http_headers(include_all=True)`.
80+
By default, `get_http_headers()` excludes problematic headers like `host` and `content-length`. To include all headers, use `get_http_headers(include_all=True)`.
8181

8282
## Important Notes
8383

src/fastmcp/cli/cli.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,7 @@ def _get_npx_command():
5050
def _parse_env_var(env_var: str) -> tuple[str, str]:
5151
"""Parse environment variable string in format KEY=VALUE."""
5252
if "=" not in env_var:
53-
logger.error(
54-
f"Invalid environment variable format: {env_var}. Must be KEY=VALUE"
55-
)
53+
logger.error("Invalid environment variable format. Must be KEY=VALUE")
5654
sys.exit(1)
5755
key, value = env_var.split("=", 1)
5856
return key.strip(), value.strip()

src/fastmcp/client/client.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import datetime
22
from contextlib import AsyncExitStack, asynccontextmanager
33
from pathlib import Path
4-
from typing import Any, cast
4+
from typing import Any, Generic, cast, overload
55

66
import anyio
77
import mcp.types
@@ -28,7 +28,18 @@
2828
from fastmcp.utilities.exceptions import get_catch_handlers
2929
from fastmcp.utilities.mcp_config import MCPConfig
3030

31-
from .transports import ClientTransport, SessionKwargs, infer_transport
31+
from .transports import (
32+
ClientTransportT,
33+
FastMCP1Server,
34+
FastMCPTransport,
35+
MCPConfigTransport,
36+
NodeStdioTransport,
37+
PythonStdioTransport,
38+
SessionKwargs,
39+
SSETransport,
40+
StreamableHttpTransport,
41+
infer_transport,
42+
)
3243

3344
__all__ = [
3445
"Client",
@@ -41,7 +52,7 @@
4152
]
4253

4354

44-
class Client:
55+
class Client(Generic[ClientTransportT]):
4556
"""
4657
MCP client that delegates connection management to a Transport instance.
4758
@@ -78,9 +89,45 @@ class Client:
7889
```
7990
"""
8091

92+
@overload
93+
def __new__(
94+
cls,
95+
transport: ClientTransportT,
96+
**kwargs: Any,
97+
) -> "Client[ClientTransportT]": ...
98+
99+
@overload
100+
def __new__(
101+
cls, transport: AnyUrl, **kwargs
102+
) -> "Client[SSETransport|StreamableHttpTransport]": ...
103+
104+
@overload
105+
def __new__(
106+
cls, transport: FastMCP | FastMCP1Server, **kwargs
107+
) -> "Client[FastMCPTransport]": ...
108+
109+
@overload
110+
def __new__(
111+
cls, transport: Path, **kwargs
112+
) -> "Client[PythonStdioTransport|NodeStdioTransport]": ...
113+
114+
@overload
115+
def __new__(
116+
cls, transport: MCPConfig | dict[str, Any], **kwargs
117+
) -> "Client[MCPConfigTransport]": ...
118+
119+
@overload
120+
def __new__(
121+
cls, transport: str, **kwargs
122+
) -> "Client[PythonStdioTransport|NodeStdioTransport|SSETransport|StreamableHttpTransport]": ...
123+
124+
def __new__(cls, transport, **kwargs) -> "Client":
125+
instance = super().__new__(cls)
126+
return instance
127+
81128
def __init__(
82129
self,
83-
transport: ClientTransport
130+
transport: ClientTransportT
84131
| FastMCP
85132
| AnyUrl
86133
| Path
@@ -96,7 +143,7 @@ def __init__(
96143
timeout: datetime.timedelta | float | int | None = None,
97144
init_timeout: datetime.timedelta | float | int | None = None,
98145
):
99-
self.transport = infer_transport(transport)
146+
self.transport = cast(ClientTransportT, infer_transport(transport))
100147
self._session: ClientSession | None = None
101148
self._exit_stack: AsyncExitStack | None = None
102149
self._nesting_counter: int = 0
@@ -147,6 +194,7 @@ def session(self) -> ClientSession:
147194
raise RuntimeError(
148195
"Client is not connected. Use the 'async with client:' context manager first."
149196
)
197+
150198
return self._session
151199

152200
@property
@@ -184,6 +232,8 @@ async def _context_manager(self):
184232
with anyio.fail_after(self._init_timeout):
185233
self._initialize_result = await self._session.initialize()
186234
yield
235+
except anyio.ClosedResourceError:
236+
raise RuntimeError("Server session was closed unexpectedly")
187237
except TimeoutError:
188238
raise RuntimeError("Failed to initialize server session")
189239
finally:
@@ -216,6 +266,11 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
216266
finally:
217267
self._exit_stack = None
218268

269+
async def close(self):
270+
await self.transport.close()
271+
self._session = None
272+
self._initialize_result = None
273+
219274
# --- MCP Client Methods ---
220275

221276
async def ping(self) -> bool:

0 commit comments

Comments
 (0)