Skip to content

Commit 7c65d76

Browse files
committed
Add example client and server for client_credentials flow
1 parent 873bbe0 commit 7c65d76

File tree

16 files changed

+1058
-27
lines changed

16 files changed

+1058
-27
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Simple Auth Client Example
2+
3+
A demonstration of how to use the MCP Python SDK with OAuth authentication over streamable HTTP or SSE transport.
4+
5+
## Features
6+
7+
- OAuth 2.0 authentication with PKCE
8+
- Support for both StreamableHTTP and SSE transports
9+
- Interactive command-line interface
10+
11+
## Installation
12+
13+
```bash
14+
cd examples/clients/simple-auth-client
15+
uv sync --reinstall
16+
```
17+
18+
## Usage
19+
20+
### 1. Start an MCP server with OAuth support
21+
22+
```bash
23+
# Example with mcp-simple-auth
24+
cd path/to/mcp-simple-auth
25+
uv run mcp-simple-auth --transport streamable-http --port 3001
26+
```
27+
28+
### 2. Run the client
29+
30+
```bash
31+
uv run mcp-simple-auth-client
32+
33+
# Or with custom server URL
34+
MCP_SERVER_PORT=3001 uv run mcp-simple-auth-client
35+
36+
# Use SSE transport
37+
MCP_TRANSPORT_TYPE=sse uv run mcp-simple-auth-client
38+
```
39+
40+
### 3. Complete OAuth flow
41+
42+
The client will open your browser for authentication. After completing OAuth, you can use commands:
43+
44+
- `list` - List available tools
45+
- `call <tool_name> [args]` - Call a tool with optional JSON arguments
46+
- `quit` - Exit
47+
48+
## Example
49+
50+
```
51+
🔐 Simple MCP Auth Client
52+
Connecting to: http://localhost:3001
53+
54+
Please visit the following URL to authorize the application:
55+
http://localhost:3001/authorize?response_type=code&client_id=...
56+
57+
✅ Connected to MCP server at http://localhost:3001
58+
59+
mcp> list
60+
📋 Available tools:
61+
1. echo - Echo back the input text
62+
63+
mcp> call echo {"text": "Hello, world!"}
64+
🔧 Tool 'echo' result:
65+
Hello, world!
66+
67+
mcp> quit
68+
👋 Goodbye!
69+
```
70+
71+
## Configuration
72+
73+
- `MCP_SERVER_PORT` - Server URL (default: 8000)
74+
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable_http` (default) or `sse`
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Simple OAuth client for MCP simple-auth server."""
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Simple MCP client example with OAuth authentication support.
4+
5+
This client connects to an MCP server using streamable HTTP transport with OAuth.
6+
7+
"""
8+
9+
import asyncio
10+
import os
11+
import threading
12+
import time
13+
import webbrowser
14+
from datetime import timedelta
15+
from http.server import BaseHTTPRequestHandler, HTTPServer
16+
from typing import Any
17+
from urllib.parse import parse_qs, urlparse
18+
19+
from mcp.client.auth import OAuthClientProvider, TokenStorage
20+
from mcp.client.session import ClientSession
21+
from mcp.client.sse import sse_client
22+
from mcp.client.streamable_http import streamablehttp_client
23+
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
24+
25+
26+
class InMemoryTokenStorage(TokenStorage):
27+
"""Simple in-memory token storage implementation."""
28+
29+
def __init__(self, client_id: str | None, client_secret: str | None):
30+
self._tokens: OAuthToken | None = None
31+
self._client_info = OAuthClientInformationFull(
32+
client_id=client_id,
33+
client_secret=client_secret,
34+
redirect_uris=None,
35+
)
36+
37+
async def get_tokens(self) -> OAuthToken | None:
38+
return self._tokens
39+
40+
async def set_tokens(self, tokens: OAuthToken) -> None:
41+
self._tokens = tokens
42+
43+
async def get_client_info(self) -> OAuthClientInformationFull | None:
44+
return self._client_info
45+
46+
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
47+
self._client_info = client_info
48+
49+
50+
class SimpleAuthClient:
51+
"""Simple MCP client with auth support."""
52+
53+
def __init__(self, server_url: str, transport_type: str = "streamable_http"):
54+
self.server_url = server_url
55+
self.transport_type = transport_type
56+
self.session: ClientSession | None = None
57+
58+
async def connect(self):
59+
"""Connect to the MCP server."""
60+
print(f"🔗 Attempting to connect to {self.server_url}...")
61+
62+
try:
63+
client_metadata_dict = {
64+
"client_name": "Simple Auth Client",
65+
"redirect_uris": None,
66+
"grant_types": ["client_credentials"],
67+
"response_types": ["code"],
68+
"token_endpoint_auth_method": "client_secret_basic",
69+
"scope": "identify"
70+
}
71+
72+
# Create OAuth authentication handler using the new interface
73+
oauth_auth = OAuthClientProvider(
74+
server_url=self.server_url.replace("/mcp", ""),
75+
client_metadata=OAuthClientMetadata.model_validate(
76+
client_metadata_dict
77+
),
78+
storage=InMemoryTokenStorage(
79+
client_id=os.environ.get("MCP_DISCORD_CLIENT_ID"),
80+
client_secret=os.environ.get("MCP_DISCORD_CLIENT_SECRET"),
81+
),
82+
)
83+
oauth_auth.context.client_info = OAuthClientInformationFull(
84+
redirect_uris=None,
85+
)
86+
87+
# Create transport with auth handler based on transport type
88+
if self.transport_type == "sse":
89+
print("📡 Opening SSE transport connection with auth...")
90+
async with sse_client(
91+
url=self.server_url,
92+
auth=oauth_auth,
93+
timeout=60,
94+
) as (read_stream, write_stream):
95+
await self._run_session(read_stream, write_stream, None)
96+
else:
97+
print("📡 Opening StreamableHTTP transport connection with auth...")
98+
async with streamablehttp_client(
99+
url=self.server_url,
100+
auth=oauth_auth,
101+
timeout=timedelta(seconds=60),
102+
) as (read_stream, write_stream, get_session_id):
103+
await self._run_session(read_stream, write_stream, get_session_id)
104+
105+
except Exception as e:
106+
print(f"❌ Failed to connect: {e}")
107+
import traceback
108+
109+
traceback.print_exc()
110+
111+
async def _run_session(self, read_stream, write_stream, get_session_id):
112+
"""Run the MCP session with the given streams."""
113+
print("🤝 Initializing MCP session...")
114+
async with ClientSession(read_stream, write_stream) as session:
115+
self.session = session
116+
print("⚡ Starting session initialization...")
117+
await session.initialize()
118+
print("✨ Session initialization complete!")
119+
120+
print(f"\n✅ Connected to MCP server at {self.server_url}")
121+
if get_session_id:
122+
session_id = get_session_id()
123+
if session_id:
124+
print(f"Session ID: {session_id}")
125+
126+
# Run interactive loop
127+
await self.interactive_loop()
128+
129+
async def list_tools(self):
130+
"""List available tools from the server."""
131+
if not self.session:
132+
print("❌ Not connected to server")
133+
return
134+
135+
try:
136+
result = await self.session.list_tools()
137+
if hasattr(result, "tools") and result.tools:
138+
print("\n📋 Available tools:")
139+
for i, tool in enumerate(result.tools, 1):
140+
print(f"{i}. {tool.name}")
141+
if tool.description:
142+
print(f" Description: {tool.description}")
143+
print()
144+
else:
145+
print("No tools available")
146+
except Exception as e:
147+
print(f"❌ Failed to list tools: {e}")
148+
149+
async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None = None):
150+
"""Call a specific tool."""
151+
if not self.session:
152+
print("❌ Not connected to server")
153+
return
154+
155+
try:
156+
result = await self.session.call_tool(tool_name, arguments or {})
157+
print(f"\n🔧 Tool '{tool_name}' result:")
158+
if hasattr(result, "content"):
159+
for content in result.content:
160+
if content.type == "text":
161+
print(content.text)
162+
else:
163+
print(content)
164+
else:
165+
print(result)
166+
except Exception as e:
167+
print(f"❌ Failed to call tool '{tool_name}': {e}")
168+
169+
async def interactive_loop(self):
170+
"""Run interactive command loop."""
171+
print("\n🎯 Interactive MCP Client")
172+
print("Commands:")
173+
print(" list - List available tools")
174+
print(" call <tool_name> [args] - Call a tool")
175+
print(" quit - Exit the client")
176+
print()
177+
178+
while True:
179+
try:
180+
command = input("mcp> ").strip()
181+
182+
if not command:
183+
continue
184+
185+
if command == "quit":
186+
break
187+
188+
elif command == "list":
189+
await self.list_tools()
190+
191+
elif command.startswith("call "):
192+
parts = command.split(maxsplit=2)
193+
tool_name = parts[1] if len(parts) > 1 else ""
194+
195+
if not tool_name:
196+
print("❌ Please specify a tool name")
197+
continue
198+
199+
# Parse arguments (simple JSON-like format)
200+
arguments = {}
201+
if len(parts) > 2:
202+
import json
203+
204+
try:
205+
arguments = json.loads(parts[2])
206+
except json.JSONDecodeError:
207+
print("❌ Invalid arguments format (expected JSON)")
208+
continue
209+
210+
await self.call_tool(tool_name, arguments)
211+
212+
else:
213+
print(
214+
"❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'"
215+
)
216+
217+
except KeyboardInterrupt:
218+
print("\n\n👋 Goodbye!")
219+
break
220+
except EOFError:
221+
break
222+
223+
224+
async def main():
225+
"""Main entry point."""
226+
# Default server URL - can be overridden with environment variable
227+
# Most MCP streamable HTTP servers use /mcp as the endpoint
228+
server_url = os.getenv("MCP_SERVER_PORT", 8000)
229+
transport_type = os.getenv("MCP_TRANSPORT_TYPE", "streamable_http")
230+
server_url = (
231+
f"http://localhost:{server_url}/mcp"
232+
if transport_type == "streamable_http"
233+
else f"http://localhost:{server_url}/sse"
234+
)
235+
236+
print("🚀 Simple MCP Auth Client")
237+
print(f"Connecting to: {server_url}")
238+
print(f"Transport type: {transport_type}")
239+
240+
# Start connection flow - OAuth will be handled automatically
241+
client = SimpleAuthClient(server_url, transport_type)
242+
await client.connect()
243+
244+
245+
def cli():
246+
"""CLI entry point for uv script."""
247+
asyncio.run(main())
248+
249+
250+
if __name__ == "__main__":
251+
cli()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[project]
2+
name = "mcp-simple-auth-client-client-credentials"
3+
version = "0.1.0"
4+
description = "A simple OAuth client for the MCP simple-auth server"
5+
readme = "README.md"
6+
requires-python = ">=3.10"
7+
authors = [{ name = "Anthropic" }]
8+
keywords = ["mcp", "oauth", "client", "auth"]
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 = ["click>=8.0.0", "mcp"]
18+
19+
[project.scripts]
20+
mcp-simple-auth-client-client-credentials = "mcp_simple_auth_client_client_credentials.main:cli"
21+
22+
[build-system]
23+
requires = ["hatchling"]
24+
build-backend = "hatchling.build"
25+
26+
[tool.hatch.build.targets.wheel]
27+
packages = ["mcp_simple_auth_client_client_credentials"]
28+
29+
[tool.uv]
30+
dev-dependencies = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"]

examples/clients/simple-auth-client/pyproject.toml

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ classifiers = [
1414
"Programming Language :: Python :: 3",
1515
"Programming Language :: Python :: 3.10",
1616
]
17-
dependencies = [
18-
"click>=8.0.0",
19-
"mcp>=1.0.0",
20-
]
17+
dependencies = ["click>=8.0.0", "mcp"]
2118

2219
[project.scripts]
2320
mcp-simple-auth-client = "mcp_simple_auth_client.main:cli"
@@ -44,9 +41,3 @@ target-version = "py310"
4441

4542
[tool.uv]
4643
dev-dependencies = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"]
47-
48-
[tool.uv.sources]
49-
mcp = { path = "../../../" }
50-
51-
[[tool.uv.index]]
52-
url = "https://pypi.org/simple"

0 commit comments

Comments
 (0)