Skip to content

Commit dd7fe38

Browse files
committed
feat: add socket transport implementation
This commit adds socket transport support to the MCP Python SDK: - Implement socket transport for both client and server - Add process management and cleanup - Support custom encoding and configuration - Add comprehensive error handling - Add tests for socket transport functionality
1 parent 6f43d1f commit dd7fe38

File tree

8 files changed

+1059
-3
lines changed

8 files changed

+1059
-3
lines changed

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,66 @@ mcp run server.py
635635

636636
Note that `mcp run` or `mcp dev` only supports server using FastMCP and not the low-level server variant.
637637

638+
### Socket Transport
639+
640+
Socket transport provides a simple and efficient communication channel between client and server, similar to stdio but without stdout pollution concerns. Unlike stdio transport which requires clean stdout for message passing, socket transport allows the server to freely use stdout for logging and other purposes.
641+
642+
The workflow is:
643+
1. Client creates a TCP server and gets an available port
644+
2. Client starts the server process, passing the port number
645+
3. Server connects back to the client's TCP server
646+
4. Client and server exchange messages over the TCP connection
647+
5. When done, client closes the connection and terminates the server process
648+
649+
This design maintains the simplicity of stdio transport while providing more flexibility for server output handling.
650+
651+
Example server setup:
652+
```python
653+
from mcp.server.fastmcp import FastMCP
654+
655+
# Create server with socket transport configuration
656+
mcp = FastMCP(
657+
"SocketServer",
658+
socket_host="127.0.0.1", # Optional, defaults to 127.0.0.1
659+
socket_port=3000, # Required when using socket transport
660+
)
661+
662+
# Run with socket transport
663+
mcp.run(transport="socket")
664+
```
665+
666+
Client usage:
667+
```python
668+
from mcp.client.session import ClientSession
669+
from mcp.client.socket_transport import SocketServerParameters, socket_client
670+
671+
# Create server parameters
672+
params = SocketServerParameters(
673+
command="python", # Server process to run
674+
args=["server.py"], # Server script and arguments
675+
# Port 0 means auto-assign an available port
676+
port=0, # Optional, defaults to 0 (auto-assign)
677+
host="127.0.0.1", # Optional, defaults to 127.0.0.1
678+
)
679+
680+
# Connect to server (this will start the server process)
681+
async with socket_client(params) as (read_stream, write_stream):
682+
async with ClientSession(read_stream, write_stream) as session:
683+
# Use the session...
684+
await session.initialize()
685+
result = await session.call_tool("echo", {"text": "Hello!"})
686+
```
687+
688+
The socket transport provides:
689+
- Freedom to use stdout without affecting message transport
690+
- Standard TCP socket-based communication
691+
- Automatic port assignment for easy setup
692+
- Connection retry logic for reliability
693+
- Clean process lifecycle management
694+
- Robust error handling
695+
696+
For a complete example, see [`examples/fastmcp/socket_example.py`](examples/fastmcp/socket_example.py).
697+
638698
### Streamable HTTP Transport
639699

640700
> **Note**: Streamable HTTP transport is superseding SSE transport for production deployments.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Socket Transport Examples
2+
3+
This directory contains examples demonstrating the socket transport feature of FastMCP. Socket transport provides a simple and efficient communication channel between client and server, similar to stdio but without stdout pollution concerns.
4+
5+
## Overview
6+
7+
The socket transport works by:
8+
1. Client creates a TCP server and gets an available port
9+
2. Client starts the server process, passing the port number
10+
3. Server connects back to the client's TCP server
11+
4. Client and server exchange messages over the TCP connection
12+
5. When done, client closes the connection and terminates the server process
13+
14+
## Files
15+
16+
- `client.py` - Example client that:
17+
- Creates a TCP server
18+
- Starts the server process
19+
- Establishes MCP session
20+
- Calls example tools
21+
22+
- `server.py` - Example server that:
23+
- Connects to client's TCP server
24+
- Sets up FastMCP environment
25+
- Provides example tools
26+
- Demonstrates logging usage
27+
28+
## Usage
29+
30+
1. Run with auto-assigned port (recommended):
31+
```bash
32+
python client.py
33+
```
34+
35+
2. Run with specific host and port:
36+
```bash
37+
python client.py --host localhost --port 3000
38+
```
39+
40+
3. Run server directly (for testing):
41+
```bash
42+
python server.py --name "Echo Server" --host localhost --port 3000 --log-level DEBUG
43+
```
44+
45+
## Configuration
46+
47+
### Client Options
48+
- `--host` - Host to bind to (default: 127.0.0.1)
49+
- `--port` - Port to use (default: 0 for auto-assign)
50+
51+
### Server Options
52+
- `--name` - Server name
53+
- `--host` - Host to connect to
54+
- `--port` - Port to connect to (required)
55+
- `--log-level` - Logging level (DEBUG/INFO/WARNING/ERROR)
56+
57+
## Implementation Details
58+
59+
### Client Features
60+
- Automatic port assignment
61+
- Server process management
62+
- Connection retry logic
63+
- Error handling
64+
- Clean shutdown
65+
66+
### Server Features
67+
- Connection retry logic
68+
- Custom text encoding support
69+
- Stdout/logging freedom
70+
- Error handling
71+
- Clean shutdown
72+
73+
### Error Handling
74+
The examples demonstrate handling of:
75+
- Connection failures and retries
76+
- Invalid JSON messages
77+
- Text encoding errors
78+
- Tool execution errors
79+
- Process lifecycle management
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"""
2+
Example of using socket transport with FastMCP.
3+
4+
This example demonstrates:
5+
1. Creating a FastMCP server that uses socket transport
6+
2. Creating a client that connects to the server using socket transport
7+
3. Exchanging messages between client and server
8+
4. Handling connection errors and retries
9+
5. Using custom encoding and configuration
10+
6. Verifying server process cleanup
11+
12+
Usage:
13+
python client.py [--host HOST] [--port PORT] [--log-level LEVEL]
14+
"""
15+
16+
import argparse
17+
import asyncio
18+
import logging
19+
import sys
20+
import psutil
21+
from pathlib import Path
22+
23+
from mcp.client.session import ClientSession
24+
from mcp.client.socket_transport import SocketServerParameters, socket_client
25+
from mcp.shared.exceptions import McpError
26+
27+
# Set up logging
28+
logger = logging.getLogger(__name__)
29+
30+
31+
async def verify_process_cleanup(pid: int) -> bool:
32+
"""
33+
Verify if a process with given PID exists.
34+
35+
Args:
36+
pid: Process ID to check
37+
38+
Returns:
39+
bool: True if process does not exist (cleaned up), False if still running
40+
"""
41+
try:
42+
process = psutil.Process(pid)
43+
return False # Process still exists
44+
except psutil.NoSuchProcess:
45+
return True # Process has been cleaned up
46+
47+
48+
async def main(host: str = "127.0.0.1", port: int = 0, log_level: str = "INFO"):
49+
"""
50+
Run the client which will start and connect to the server.
51+
52+
Args:
53+
host: The host to use for socket communication (default: 127.0.0.1)
54+
port: The port to use for socket communication (default: 0 for auto-assign)
55+
log_level: Logging level (default: INFO)
56+
"""
57+
# Configure logging
58+
logging.basicConfig(
59+
level=log_level,
60+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
61+
)
62+
63+
server_pid = None
64+
try:
65+
# Create server parameters with custom configuration
66+
params = SocketServerParameters(
67+
# The command to run the server
68+
command=sys.executable, # Use current Python interpreter
69+
# Arguments to start the server script
70+
args=[
71+
str(Path(__file__).parent / "server.py"), # Updated path
72+
"--name",
73+
"Echo Server",
74+
"--host",
75+
host,
76+
"--port",
77+
str(port),
78+
"--log-level",
79+
log_level,
80+
],
81+
# Socket configuration
82+
host=host,
83+
port=port,
84+
# Optional: customize encoding (defaults shown)
85+
encoding="utf-8",
86+
encoding_error_handler="strict",
87+
)
88+
89+
# Connect to server (this will start the server process)
90+
async with socket_client(params) as (read_stream, write_stream):
91+
# Create client session
92+
async with ClientSession(read_stream, write_stream) as session:
93+
try:
94+
# Initialize the session
95+
await session.initialize()
96+
logger.info("Session initialized successfully")
97+
98+
# Get server process PID for verification
99+
result = await session.call_tool("get_pid_tool", {})
100+
server_pid = result.structuredContent["result"]["pid"]
101+
logger.info(f"Server process PID: {server_pid}")
102+
103+
# List available tools
104+
tools = await session.list_tools()
105+
logger.info(f"Available tools: {[t.name for t in tools.tools]}")
106+
107+
# Call the echo tool with different inputs
108+
messages = [
109+
"Hello from socket transport!",
110+
"Testing special chars: 世界, мир, ♥",
111+
"Testing long message: " + "x" * 1000,
112+
]
113+
114+
for msg in messages:
115+
try:
116+
result = await session.call_tool("echo_tool", {"text": msg})
117+
logger.info(f"Echo result: {result}")
118+
except McpError as e:
119+
logger.error(f"Tool call failed: {e}")
120+
121+
except McpError as e:
122+
logger.error(f"Session error: {e}")
123+
sys.exit(1)
124+
125+
# After session ends, verify server process cleanup
126+
if server_pid:
127+
await asyncio.sleep(0.5) # Give some time for cleanup
128+
is_cleaned = await verify_process_cleanup(server_pid)
129+
if is_cleaned:
130+
logger.info(
131+
f"Server process (PID: {server_pid}) was successfully cleaned up"
132+
)
133+
else:
134+
logger.warning(f"Server process (PID: {server_pid}) is still running!")
135+
136+
except Exception as e:
137+
logger.error(f"Connection failed: {e}", exc_info=True)
138+
sys.exit(1)
139+
140+
141+
if __name__ == "__main__":
142+
parser = argparse.ArgumentParser(description="Socket transport example client")
143+
parser.add_argument("--host", default="127.0.0.1", help="Host to use")
144+
parser.add_argument("--port", type=int, default=0, help="Port to use (0 for auto)")
145+
parser.add_argument(
146+
"--log-level",
147+
default="INFO",
148+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
149+
help="Logging level",
150+
)
151+
152+
args = parser.parse_args()
153+
154+
# Run everything
155+
asyncio.run(main(host=args.host, port=args.port, log_level=args.log_level))

0 commit comments

Comments
 (0)