Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions examples/clients/simple-auth-client/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Simple Auth Client Example

A demonstration of how to use the MCP Python SDK with OAuth authentication over streamable HTTP transport.
A demonstration of how to use the MCP Python SDK with OAuth authentication over streamable HTTP or SSE transport.

## Features

- OAuth 2.0 authentication with PKCE
- Streamable HTTP transport
- Support for both StreamableHTTP and SSE transports
- Interactive command-line interface

## Installation
Expand All @@ -32,6 +32,9 @@ uv run mcp-simple-auth-client

# Or with custom server URL
MCP_SERVER_URL=http://localhost:3001 uv run mcp-simple-auth-client

# Use SSE transport
MCP_TRANSPORT_TYPE=sse uv run mcp-simple-auth-client
```

### 3. Complete OAuth flow
Expand Down Expand Up @@ -68,3 +71,4 @@ mcp> quit
## Configuration

- `MCP_SERVER_URL` - Server URL (default: http://localhost:3001)
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable_http` (default) or `sse`
68 changes: 41 additions & 27 deletions examples/clients/simple-auth-client/mcp_simple_auth_client/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from mcp.client.auth import OAuthClientProvider, TokenStorage
from mcp.client.session import ClientSession
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamablehttp_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken

Expand Down Expand Up @@ -149,8 +150,9 @@ def get_state(self):
class SimpleAuthClient:
"""Simple MCP client with auth support."""

def __init__(self, server_url: str):
def __init__(self, server_url: str, transport_type: str = "streamable_http"):
self.server_url = server_url
self.transport_type = transport_type
self.session: ClientSession | None = None

async def connect(self):
Expand Down Expand Up @@ -195,38 +197,48 @@ async def _default_redirect_handler(authorization_url: str) -> None:
callback_handler=callback_handler,
)

# Create streamable HTTP transport with auth handler
stream_context = streamablehttp_client(
url=self.server_url,
auth=oauth_auth,
timeout=timedelta(seconds=60),
)

print(
"📡 Opening transport connection (HTTPX handles auth automatically)..."
)
async with stream_context as (read_stream, write_stream, get_session_id):
print("🤝 Initializing MCP session...")
async with ClientSession(read_stream, write_stream) as session:
self.session = session
print("⚡ Starting session initialization...")
await session.initialize()
print("✨ Session initialization complete!")

print(f"\n✅ Connected to MCP server at {self.server_url}")
session_id = get_session_id()
if session_id:
print(f"Session ID: {session_id}")

# Run interactive loop
await self.interactive_loop()
# Create transport with auth handler based on transport type
if self.transport_type == "sse":
print("📡 Opening SSE transport connection with auth...")
async with sse_client(
url=self.server_url,
auth=oauth_auth,
timeout=60,
) as (read_stream, write_stream):
await self._run_session(read_stream, write_stream, None)
else:
print("📡 Opening StreamableHTTP transport connection with auth...")
async with streamablehttp_client(
url=self.server_url,
auth=oauth_auth,
timeout=timedelta(seconds=60),
) as (read_stream, write_stream, get_session_id):
await self._run_session(read_stream, write_stream, get_session_id)

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

traceback.print_exc()

async def _run_session(self, read_stream, write_stream, get_session_id):
"""Run the MCP session with the given streams."""
print("🤝 Initializing MCP session...")
async with ClientSession(read_stream, write_stream) as session:
self.session = session
print("⚡ Starting session initialization...")
await session.initialize()
print("✨ Session initialization complete!")

print(f"\n✅ Connected to MCP server at {self.server_url}")
if get_session_id:
session_id = get_session_id()
if session_id:
print(f"Session ID: {session_id}")

# Run interactive loop
await self.interactive_loop()

async def list_tools(self):
"""List available tools from the server."""
if not self.session:
Expand Down Expand Up @@ -327,12 +339,14 @@ async def main():
# Default server URL - can be overridden with environment variable
# Most MCP streamable HTTP servers use /mcp as the endpoint
server_url = os.getenv("MCP_SERVER_URL", "http://localhost:8000/mcp")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: might change this to be /sse if the transport is set since that's the default binding we get. might get a little funky making sure you don't overwrite it though.

https://github.com/modelcontextprotocol/python-sdk/blob/main/src/mcp/server/fastmcp/server.py#L93

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, something is weird, I thought I changed that to port!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't push it 😞

transport_type = os.getenv("MCP_TRANSPORT_TYPE", "streamable_http")

print("🚀 Simple MCP Auth Client")
print(f"Connecting to: {server_url}")
print(f"Transport type: {transport_type}")

# Start connection flow - OAuth will be handled automatically
client = SimpleAuthClient(server_url)
client = SimpleAuthClient(server_url, transport_type)
await client.connect()


Expand Down
10 changes: 9 additions & 1 deletion src/mcp/client/sse.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,20 @@ async def sse_client(
headers: dict[str, Any] | None = None,
timeout: float = 5,
sse_read_timeout: float = 60 * 5,
auth: httpx.Auth | None = None,
):
"""
Client transport for SSE.

`sse_read_timeout` determines how long (in seconds) the client will wait for a new
event before disconnecting. All other HTTP operations are controlled by `timeout`.

Args:
url: The SSE endpoint URL.
headers: Optional headers to include in requests.
timeout: HTTP timeout for regular operations.
sse_read_timeout: Timeout for SSE read operations.
auth: Optional HTTPX authentication handler.
"""
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]
Expand All @@ -45,7 +53,7 @@ async def sse_client(
async with anyio.create_task_group() as tg:
try:
logger.info(f"Connecting to SSE endpoint: {remove_request_params(url)}")
async with create_mcp_http_client(headers=headers) as client:
async with create_mcp_http_client(headers=headers, auth=auth) as client:
async with aconnect_sse(
client,
"GET",
Expand Down
Loading