Skip to content

Commit 8a94289

Browse files
committed
RDCIST-3853: Add example client
1 parent 90a647a commit 8a94289

File tree

5 files changed

+1121
-0
lines changed

5 files changed

+1121
-0
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Simple Streamable Private Gateway Example
2+
3+
A demonstration of how to use the MCP Python SDK as a streamable private gateway without authentication over streamable HTTP or SSE transport.
4+
5+
## Features
6+
7+
- No authentication required
8+
- Support StreamableHTTP
9+
- Interactive command-line interface
10+
- Tool calling
11+
12+
## Installation
13+
14+
```bash
15+
cd examples/clients/simple-streamable-private-gateway
16+
uv sync --reinstall
17+
```
18+
19+
## Usage
20+
21+
### 1. Start an MCP server without authentication
22+
23+
You can use any MCP server that doesn't require authentication. For example:
24+
25+
```bash
26+
# Example with a simple tool server
27+
cd examples/servers/simple-tool
28+
uv run mcp-simple-tool --transport streamable-http --port 8000
29+
30+
# Or use any of the other example servers
31+
cd examples/servers/simple-resource
32+
uv run simple-resource --transport streamable-http --port 8000
33+
```
34+
35+
### 2. Run the client
36+
37+
```bash
38+
uv run mcp-simple-streamable-private-gateway
39+
40+
# Or with custom server port
41+
MCP_SERVER_PORT=8000 uv run mcp-simple-streamable-private-gateway
42+
```
43+
44+
### 3. Use the interactive interface
45+
46+
The client provides several commands:
47+
48+
- `list` - List available tools
49+
- `call <tool_name> [args]` - Call a tool with optional JSON arguments
50+
- `quit` - Exit
51+
52+
## Examples
53+
54+
### Basic tool usage
55+
56+
```markdown
57+
🚀 Simple Streamable Private Gateway
58+
Connecting to: https://localhost:8000/mcp
59+
📡 Opening StreamableHTTP transport connection...
60+
🤝 Initializing MCP session...
61+
⚡ Starting session initialization...
62+
✨ Session initialization complete!
63+
64+
✅ Connected to MCP server at https://localhost:8000/mcp
65+
66+
🎯 Interactive MCP Client
67+
Commands:
68+
list - List available tools
69+
call <tool_name> [args] - Call a tool
70+
quit - Exit the client
71+
72+
mcp> list
73+
📋 Available tools:
74+
1. echo
75+
Description: Echo back the input text
76+
77+
mcp> call echo {"text": "Hello, world!"}
78+
🔧 Tool 'echo' result:
79+
Hello, world!
80+
81+
mcp> quit
82+
👋 Goodbye!
83+
```
84+
85+
## Configuration
86+
87+
- `MCP_SERVER_PORT` - Server port (default: 8000)
88+
- `MCP_SERVER_HOSTNAME` - Server hostname (default: localhost)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Simple MCP streamable private gateway client example without authentication."""
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Simple MCP streamable private gateway client example without authentication.
4+
5+
This client connects to an MCP server using streamable HTTP or SSE transport.
6+
7+
"""
8+
9+
import asyncio
10+
import os
11+
from datetime import timedelta
12+
from typing import Any
13+
14+
from mcp.client.session import ClientSession
15+
from mcp.client.streamable_http import streamablehttp_client
16+
17+
18+
class SimpleStreamablePrivateGateway:
19+
"""Simple MCP streamable private gateway client without authentication."""
20+
21+
def __init__(self, server_url: str, server_hostname: str, transport_type: str = "streamable-http"):
22+
self.server_url = server_url
23+
self.server_hostname = server_hostname
24+
self.transport_type = transport_type
25+
self.session: ClientSession | None = None
26+
27+
async def connect(self):
28+
"""Connect to the MCP server."""
29+
print(f"🔗 Attempting to connect to {self.server_url}...")
30+
31+
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)
43+
44+
except Exception as e:
45+
print(f"❌ Failed to connect: {e}")
46+
import traceback
47+
48+
traceback.print_exc()
49+
50+
async def _run_session(self, read_stream, write_stream, get_session_id):
51+
"""Run the MCP session with the given streams."""
52+
print("🤝 Initializing MCP session...")
53+
async with ClientSession(read_stream, write_stream) as session:
54+
self.session = session
55+
print("⚡ Starting session initialization...")
56+
await session.initialize()
57+
print("✨ Session initialization complete!")
58+
59+
print(f"\n✅ Connected to MCP server at {self.server_url}")
60+
if get_session_id:
61+
session_id = get_session_id()
62+
if session_id:
63+
print(f"Session ID: {session_id}")
64+
65+
# Run interactive loop
66+
await self.interactive_loop()
67+
68+
async def list_tools(self):
69+
"""List available tools from the server."""
70+
if not self.session:
71+
print("❌ Not connected to server")
72+
return
73+
74+
try:
75+
result = await self.session.list_tools()
76+
if hasattr(result, "tools") and result.tools:
77+
print("\n📋 Available tools:")
78+
for i, tool in enumerate(result.tools, 1):
79+
print(f"{i}. {tool.name}")
80+
if tool.description:
81+
print(f" Description: {tool.description}")
82+
print()
83+
else:
84+
print("No tools available")
85+
except Exception as e:
86+
print(f"❌ Failed to list tools: {e}")
87+
88+
async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None = None):
89+
"""Call a specific tool."""
90+
if not self.session:
91+
print("❌ Not connected to server")
92+
return
93+
94+
try:
95+
result = await self.session.call_tool(tool_name, arguments or {})
96+
print(f"\n🔧 Tool '{tool_name}' result:")
97+
if hasattr(result, "content"):
98+
for content in result.content:
99+
if content.type == "text":
100+
print(content.text)
101+
else:
102+
print(content)
103+
else:
104+
print(result)
105+
except Exception as e:
106+
print(f"❌ Failed to call tool '{tool_name}': {e}")
107+
108+
async def interactive_loop(self):
109+
"""Run interactive command loop."""
110+
print("\n🎯 Interactive Streamable Private Gateway")
111+
print("Commands:")
112+
print(" list - List available tools")
113+
print(" call <tool_name> [args] - Call a tool")
114+
print(" quit - Exit the client")
115+
print()
116+
117+
while True:
118+
try:
119+
command = input("mcp> ").strip()
120+
121+
if not command:
122+
continue
123+
124+
if command == "quit":
125+
print("👋 Goodbye!")
126+
break
127+
128+
elif command == "list":
129+
await self.list_tools()
130+
131+
elif command.startswith("call "):
132+
parts = command.split(maxsplit=2)
133+
tool_name = parts[1] if len(parts) > 1 else ""
134+
135+
if not tool_name:
136+
print("❌ Please specify a tool name")
137+
continue
138+
139+
# Parse arguments (simple JSON-like format)
140+
arguments = {}
141+
if len(parts) > 2:
142+
import json
143+
144+
try:
145+
arguments = json.loads(parts[2])
146+
except json.JSONDecodeError:
147+
print("❌ Invalid arguments format (expected JSON)")
148+
continue
149+
150+
await self.call_tool(tool_name, arguments)
151+
152+
else:
153+
print("❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'")
154+
155+
except KeyboardInterrupt:
156+
print("\n\n👋 Goodbye!")
157+
break
158+
except EOFError:
159+
print("\n👋 Goodbye!")
160+
break
161+
162+
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+
172+
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}")
176+
177+
# Start connection flow
178+
client = SimpleStreamablePrivateGateway(server_url, server_hostname, transport_type)
179+
await client.connect()
180+
181+
182+
def cli():
183+
"""CLI entry point for uv script."""
184+
asyncio.run(main())
185+
186+
187+
if __name__ == "__main__":
188+
cli()
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
[project]
2+
name = "mcp-simple-streamable-private-gateway"
3+
version = "0.1.0"
4+
description = "A simple streamable private gateway client for MCP servers without authentication"
5+
readme = "README.md"
6+
requires-python = ">=3.10"
7+
authors = [{ name = "Anthropic" }]
8+
keywords = ["mcp", "client", "streamable", "private", "gateway"]
9+
license = { text = "MIT" }
10+
classifiers = [
11+
"Development Status :: 4 - Beta",
12+
"Intended Audience :: Developers",
13+
"License :: OSI Approved :: MIT License",
14+
"Programming Language :: Python :: 3",
15+
"Programming Language :: Python :: 3.10",
16+
]
17+
dependencies = [
18+
"click>=8.2.0",
19+
"mcp",
20+
]
21+
22+
[project.scripts]
23+
mcp-simple-streamable-private-gateway = "mcp_simple_streamable_private_gateway.main:cli"
24+
25+
[build-system]
26+
requires = ["hatchling"]
27+
build-backend = "hatchling.build"
28+
29+
[tool.hatch.build.targets.wheel]
30+
packages = ["mcp_simple_streamable_private_gateway"]
31+
32+
[tool.pyright]
33+
include = ["mcp_simple_streamable_private_gateway"]
34+
venvPath = "."
35+
venv = ".venv"
36+
37+
[tool.ruff.lint]
38+
select = ["E", "F", "I"]
39+
ignore = []
40+
41+
[tool.ruff]
42+
line-length = 120
43+
target-version = "py310"
44+
45+
[tool.uv]
46+
dev-dependencies = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"]

0 commit comments

Comments
 (0)