Skip to content

Commit 2a3378a

Browse files
committed
RDCIST-3853: Add support to private gateway - SSE
1 parent b2c035a commit 2a3378a

File tree

4 files changed

+186
-45
lines changed

4 files changed

+186
-45
lines changed

examples/clients/simple-streamable-private-gateway/README.md

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Simple Streamable Private Gateway Example
22

3-
A demonstration of how to use the MCP Python SDK as a streamable private gateway without authentication over streamable HTTP or SSE transport.
3+
A demonstration of how to use the MCP Python SDK as a streamable private gateway without authentication over streamable HTTP or SSE transport with custom extensions for private gateway connectivity (SNI hostname support).
44

55
## Features
66

77
- No authentication required
8-
- Support StreamableHTTP
8+
- Supports both StreamableHTTP and SSE transports
9+
- Custom extensions for private gateway (SNI hostname) - **Both transports**
910
- Interactive command-line interface
1011
- Tool calling
1112

@@ -23,22 +24,30 @@ uv sync --reinstall
2324
You can use any MCP server that doesn't require authentication. For example:
2425

2526
```bash
26-
# Example with a simple tool server
27+
# Example with StreamableHTTP transport
2728
cd examples/servers/simple-tool
28-
uv run mcp-simple-tool --transport streamable-http --port 8000
29+
uv run mcp-simple-tool --transport streamable-http --port 8081
30+
31+
# Or with SSE transport
32+
cd examples/servers/simple-tool
33+
uv run mcp-simple-tool --transport sse --port 8081
2934

3035
# Or use any of the other example servers
3136
cd examples/servers/simple-resource
32-
uv run simple-resource --transport streamable-http --port 8000
37+
uv run simple-resource --transport streamable-http --port 8081
3338
```
3439

3540
### 2. Run the client
3641

3742
```bash
43+
# Default: StreamableHTTP transport
3844
uv run mcp-simple-streamable-private-gateway
3945

40-
# Or with custom server port
41-
MCP_SERVER_PORT=8000 uv run mcp-simple-streamable-private-gateway
46+
# Or with SSE transport
47+
MCP_TRANSPORT_TYPE=sse uv run mcp-simple-streamable-private-gateway
48+
49+
# Or with custom server port and hostname
50+
MCP_SERVER_PORT=8081 MCP_SERVER_HOSTNAME=mcp.deepwiki.com uv run mcp-simple-streamable-private-gateway
4251
```
4352

4453
### 3. Use the interactive interface
@@ -51,19 +60,22 @@ The client provides several commands:
5160

5261
## Examples
5362

54-
### Basic tool usage
63+
### StreamableHTTP Transport
5564

5665
```markdown
5766
🚀 Simple Streamable Private Gateway
58-
Connecting to: https://localhost:8000/mcp
59-
📡 Opening StreamableHTTP transport connection...
67+
Connecting to: https://localhost:8081/mcp
68+
Server hostname: mcp.deepwiki.com
69+
Transport type: streamable-http
70+
📡 Opening StreamableHTTP transport connection with extensions...
6071
🤝 Initializing MCP session...
6172
⚡ Starting session initialization...
6273
✨ Session initialization complete!
6374

64-
✅ Connected to MCP server at https://localhost:8000/mcp
75+
✅ Connected to MCP server at https://localhost:8081/mcp
76+
Session ID: abc123...
6577

66-
🎯 Interactive MCP Client
78+
🎯 Interactive MCP Client (Private Gateway)
6779
Commands:
6880
list - List available tools
6981
call <tool_name> [args] - Call a tool
@@ -82,7 +94,39 @@ mcp> quit
8294
👋 Goodbye!
8395
```
8496

97+
### SSE Transport
98+
99+
```markdown
100+
🚀 Simple Streamable Private Gateway
101+
Connecting to: https://localhost:8081/sse
102+
Server hostname: mcp.deepwiki.com
103+
Transport type: sse
104+
📡 Opening SSE transport connection with extensions...
105+
🤝 Initializing MCP session...
106+
⚡ Starting session initialization...
107+
✨ Session initialization complete!
108+
109+
✅ Connected to MCP server at https://localhost:8081/sse
110+
111+
🎯 Interactive MCP Client (Private Gateway)
112+
Commands:
113+
list - List available tools
114+
call <tool_name> [args] - Call a tool
115+
quit - Exit the client
116+
117+
mcp> list
118+
📋 Available tools:
119+
1. echo
120+
Description: Echo back the input text
121+
122+
mcp> quit
123+
👋 Goodbye!
124+
```
125+
85126
## Configuration
86127

87-
- `MCP_SERVER_PORT` - Server port (default: 8000)
88-
- `MCP_SERVER_HOSTNAME` - Server hostname (default: localhost)
128+
Environment variables:
129+
130+
- `MCP_SERVER_PORT` - Server port (default: 8081)
131+
- `MCP_SERVER_HOSTNAME` - Server hostname for SNI (default: mcp.deepwiki.com)
132+
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable-http` or `sse` (default: streamable-http)

examples/clients/simple-streamable-private-gateway/mcp_simple_streamable_private_gateway/main.py

Lines changed: 91 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,30 @@
22
"""
33
Simple MCP streamable private gateway client example without authentication.
44
5-
This client connects to an MCP server using streamable HTTP or SSE transport.
5+
This client connects to an MCP server using streamable HTTP or SSE transport
6+
with custom extensions for private gateway connectivity (SNI hostname support).
67
78
"""
89

910
import asyncio
10-
import os
11+
from collections.abc import Callable
1112
from datetime import timedelta
1213
from typing import Any
1314

15+
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
16+
1417
from mcp.client.session import ClientSession
18+
from mcp.client.sse import sse_client
1519
from mcp.client.streamable_http import streamablehttp_client
20+
from mcp.shared.message import SessionMessage
1621

1722

1823
class SimpleStreamablePrivateGateway:
19-
"""Simple MCP streamable private gateway client without authentication."""
24+
"""Simple MCP private gateway client supporting StreamableHTTP and SSE transports.
25+
26+
This client demonstrates how to use custom extensions (e.g., SNI hostname) for
27+
private gateway connectivity with both transport types.
28+
"""
2029

2130
def __init__(self, server_url: str, server_hostname: str, transport_type: str = "streamable-http"):
2231
self.server_url = server_url
@@ -29,25 +38,42 @@ async def connect(self):
2938
print(f"🔗 Attempting to connect to {self.server_url}...")
3039

3140
try:
32-
print("📡 Opening StreamableHTTP transport connection...")
33-
# Note: terminate_on_close=False prevents SSL handshake failures during exit
34-
# Some servers may not handle session termination gracefully over SSL
35-
async with streamablehttp_client(
36-
url=self.server_url,
37-
headers={"Host": self.server_hostname},
38-
extensions={"sni_hostname": self.server_hostname},
39-
timeout=timedelta(seconds=60),
40-
terminate_on_close=False, # Skip session termination to avoid SSL errors
41-
) as (read_stream, write_stream, get_session_id):
42-
await self._run_session(read_stream, write_stream, get_session_id)
41+
# Create transport based on transport type
42+
if self.transport_type == "sse":
43+
print("📡 Opening SSE transport connection with extensions...")
44+
# SSE transport with custom extensions for private gateway
45+
async with sse_client(
46+
url=self.server_url,
47+
headers={"Host": self.server_hostname},
48+
extensions={"sni_hostname": self.server_hostname},
49+
timeout=60,
50+
) as (read_stream, write_stream):
51+
await self._run_session(read_stream, write_stream, None)
52+
else:
53+
print("📡 Opening StreamableHTTP transport connection with extensions...")
54+
# Note: terminate_on_close=False prevents SSL handshake failures during exit
55+
# Some servers may not handle session termination gracefully over SSL
56+
async with streamablehttp_client(
57+
url=self.server_url,
58+
headers={"Host": self.server_hostname},
59+
extensions={"sni_hostname": self.server_hostname},
60+
timeout=timedelta(seconds=60),
61+
terminate_on_close=False, # Skip session termination to avoid SSL errors
62+
) as (read_stream, write_stream, get_session_id):
63+
await self._run_session(read_stream, write_stream, get_session_id)
4364

4465
except Exception as e:
4566
print(f"❌ Failed to connect: {e}")
4667
import traceback
4768

4869
traceback.print_exc()
4970

50-
async def _run_session(self, read_stream, write_stream, get_session_id):
71+
async def _run_session(
72+
self,
73+
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
74+
write_stream: MemoryObjectSendStream[SessionMessage],
75+
get_session_id: Callable[[], str | None] | None,
76+
):
5177
"""Run the MCP session with the given streams."""
5278
print("🤝 Initializing MCP session...")
5379
async with ClientSession(read_stream, write_stream) as session:
@@ -107,7 +133,7 @@ async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None = Non
107133

108134
async def interactive_loop(self):
109135
"""Run interactive command loop."""
110-
print("\n🎯 Interactive Streamable Private Gateway")
136+
print("\n🎯 Interactive MCP Client (Private Gateway)")
111137
print("Commands:")
112138
print(" list - List available tools")
113139
print(" call <tool_name> [args] - Call a tool")
@@ -160,23 +186,57 @@ async def interactive_loop(self):
160186
break
161187

162188

163-
async def main():
164-
"""Main entry point."""
165-
# Default server URL - can be overridden with environment variable
166-
# Most MCP streamable HTTP servers use /mcp as the endpoint
167-
server_port = os.getenv("MCP_SERVER_PORT", "8081")
168-
server_hostname = os.getenv("MCP_SERVER_HOSTNAME", "mcp.deepwiki.com")
169-
transport_type = "streamable-http"
170-
server_url = f"https://localhost:{server_port}/mcp"
171-
189+
def get_user_input():
190+
"""Get server configuration from user input."""
172191
print("🚀 Simple Streamable Private Gateway")
173-
print(f"Connecting to: {server_url}")
174-
print(f"Server hostname: {server_hostname}")
175-
print(f"Transport type: {transport_type}")
192+
print("\n📝 Server Configuration")
193+
print("=" * 50)
194+
195+
# Get server port
196+
server_port = input("Server port [8081]: ").strip() or "8081"
197+
198+
# Get server hostname
199+
server_hostname = input("Server hostname [mcp.deepwiki.com]: ").strip() or "mcp.deepwiki.com"
200+
201+
# Get transport type
202+
print("\nTransport type:")
203+
print(" 1. streamable-http (default)")
204+
print(" 2. sse")
205+
transport_choice = input("Select transport [1]: ").strip() or "1"
206+
207+
if transport_choice == "2":
208+
transport_type = "sse"
209+
else:
210+
transport_type = "streamable-http"
211+
212+
print("=" * 50)
213+
214+
return server_port, server_hostname, transport_type
215+
176216

177-
# Start connection flow
178-
client = SimpleStreamablePrivateGateway(server_url, server_hostname, transport_type)
179-
await client.connect()
217+
async def main():
218+
"""Main entry point."""
219+
try:
220+
# Get configuration from user input
221+
server_port, server_hostname, transport_type = get_user_input()
222+
223+
# Set URL endpoint based on transport type
224+
# StreamableHTTP servers typically use /mcp, SSE servers use /sse
225+
endpoint = "/mcp" if transport_type == "streamable-http" else "/sse"
226+
server_url = f"https://localhost:{server_port}{endpoint}"
227+
228+
print(f"\n🔗 Connecting to: {server_url}")
229+
print(f"📡 Server hostname: {server_hostname}")
230+
print(f"🚀 Transport type: {transport_type}\n")
231+
232+
# Start connection flow
233+
client = SimpleStreamablePrivateGateway(server_url, server_hostname, transport_type)
234+
await client.connect()
235+
236+
except KeyboardInterrupt:
237+
print("\n\n👋 Goodbye!")
238+
except EOFError:
239+
print("\n👋 Goodbye!")
180240

181241

182242
def cli():

src/mcp/client/sse.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def remove_request_params(url: str) -> str:
2525
async def sse_client(
2626
url: str,
2727
headers: dict[str, Any] | None = None,
28+
extensions: dict[str, str] | None = None,
2829
timeout: float = 5,
2930
sse_read_timeout: float = 60 * 5,
3031
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
@@ -39,6 +40,7 @@ async def sse_client(
3940
Args:
4041
url: The SSE endpoint URL.
4142
headers: Optional headers to include in requests.
43+
extensions: Optional extensions to include in requests (e.g., for SNI hostname).
4244
timeout: HTTP timeout for regular operations.
4345
sse_read_timeout: Timeout for SSE read operations.
4446
auth: Optional HTTPX authentication handler.
@@ -51,6 +53,9 @@ async def sse_client(
5153

5254
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
5355
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
56+
57+
# Prepare extensions (copy to avoid mutation)
58+
request_extensions = extensions.copy() if extensions else {}
5459

5560
async with anyio.create_task_group() as tg:
5661
try:
@@ -62,6 +67,7 @@ async def sse_client(
6267
client,
6368
"GET",
6469
url,
70+
extensions=request_extensions,
6571
) as event_source:
6672
event_source.response.raise_for_status()
6773
logger.debug("SSE connection established")
@@ -127,6 +133,7 @@ async def post_writer(endpoint_url: str):
127133
mode="json",
128134
exclude_none=True,
129135
),
136+
extensions=request_extensions,
130137
)
131138
response.raise_for_status()
132139
logger.debug(f"Client message sent successfully: {response.status_code}")

uv.lock

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)