Skip to content

Commit db8ab3d

Browse files
authored
Merge pull request #72 from keycardai/larry/age-16-client-connection-example
feat(examples): add MCP client connection example with OAuth
2 parents adbe2d8 + 6a2735d commit db8ab3d

4 files changed

Lines changed: 481 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.10
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# MCP Client Connection with Keycard OAuth
2+
3+
A complete example demonstrating how to connect to an MCP server as a client using OAuth authentication with `StarletteAuthCoordinator`.
4+
5+
## Why Keycard?
6+
7+
Keycard enables secure OAuth authentication for MCP connections. This example shows the client-side flow:
8+
9+
- **Connect to authenticated MCP servers** using OAuth 2.0
10+
- **Handle auth challenges** with automatic redirect URL generation
11+
- **Persist tokens** across application restarts with SQLite storage
12+
13+
## Prerequisites
14+
15+
Before running this example:
16+
17+
### 1. Sign up at [keycard.ai](https://keycard.ai)
18+
19+
### 2. Create a Zone
20+
21+
Create an authentication zone in the Keycard console.
22+
23+
### 3. Start an Authenticated MCP Server
24+
25+
This example connects to an MCP server. Start the `hello_world_server` example first:
26+
27+
```bash
28+
cd ../hello_world_server
29+
export KEYCARD_ZONE_ID="your-zone-id"
30+
uv sync && uv run python main.py
31+
```
32+
33+
The server will start on `http://localhost:8000`.
34+
35+
## Quick Start
36+
37+
### 1. Set Environment Variables (Optional)
38+
39+
```bash
40+
# Default values work for local development
41+
export MCP_SERVER_URL="http://localhost:8000/mcp"
42+
export CALLBACK_HOST="localhost"
43+
export CALLBACK_PORT="8080"
44+
```
45+
46+
### 2. Install Dependencies
47+
48+
```bash
49+
cd packages/mcp/examples/client_connection
50+
uv sync
51+
```
52+
53+
### 3. Run the Client
54+
55+
```bash
56+
uv run python main.py
57+
```
58+
59+
### 4. Open in Browser
60+
61+
Navigate to http://localhost:8080/ and follow the authentication flow:
62+
63+
1. Click "Authenticate" to start OAuth flow
64+
2. Complete authentication with Keycard
65+
3. Refresh the page to see connected status
66+
4. Test calling the `hello_world` tool
67+
68+
## How It Works
69+
70+
### Connection Status Lifecycle
71+
72+
```
73+
INITIALIZING
74+
|
75+
v
76+
CONNECTING ---> AUTHENTICATING ---> AUTH_PENDING (waiting for user)
77+
| |
78+
v v
79+
CONNECTION_FAILED CONNECTED (after OAuth callback)
80+
```
81+
82+
### OAuth Flow
83+
84+
```
85+
Client Browser Keycard MCP Server
86+
| | | |
87+
|-- connect() ------------>| | |
88+
| | |<-- 401 Unauthorized --|
89+
|<-- AUTH_PENDING ---------| | |
90+
| | | |
91+
|-- get_auth_challenges() -| | |
92+
|-- (show auth URL) ------>| | |
93+
| |-- User authenticates ->| |
94+
| |<-- Redirect to callback| |
95+
|<-- OAuth callback -------| | |
96+
| | | |
97+
|-- (auto-reconnect) ----->| | |
98+
|<-- CONNECTED ------------| |<-- Authenticated -----|
99+
```
100+
101+
1. Client attempts to connect to MCP server
102+
2. Server returns 401, triggering OAuth challenge
103+
3. Client generates authorization URL and sets status to `AUTH_PENDING`
104+
4. User authenticates in browser
105+
5. OAuth callback is received at `/oauth/callback`
106+
6. Tokens are stored, session auto-reconnects
107+
7. Status becomes `CONNECTED`
108+
109+
## Session Status Properties
110+
111+
| Property | Description |
112+
|----------|-------------|
113+
| `is_operational` | `True` when fully connected and ready to call tools |
114+
| `requires_user_action` | `True` when waiting for OAuth completion |
115+
| `is_failed` | `True` when in a failure state |
116+
| `can_retry` | `True` when reconnection is possible |
117+
118+
## Key Patterns Demonstrated
119+
120+
### 1. Setting up StarletteAuthCoordinator
121+
122+
```python
123+
from keycardai.mcp.client import StarletteAuthCoordinator, SQLiteBackend
124+
125+
storage = SQLiteBackend("client_auth.db")
126+
coordinator = StarletteAuthCoordinator(
127+
backend=storage,
128+
redirect_uri="http://localhost:8080/oauth/callback"
129+
)
130+
```
131+
132+
### 2. Creating an OAuth-Enabled Client
133+
134+
```python
135+
from keycardai.mcp.client import Client
136+
137+
SERVERS = {
138+
"my-server": {
139+
"url": "http://localhost:8000/mcp",
140+
"transport": "streamable-http",
141+
"auth": {"type": "oauth"},
142+
}
143+
}
144+
145+
client = Client(
146+
servers=SERVERS,
147+
storage_backend=storage,
148+
auth_coordinator=coordinator,
149+
)
150+
await client.connect()
151+
```
152+
153+
### 3. Checking Session Status
154+
155+
```python
156+
session = client.sessions["my-server"]
157+
158+
if session.requires_user_action:
159+
# User needs to authenticate
160+
challenges = await client.get_auth_challenges()
161+
auth_url = challenges[0]["authorization_url"]
162+
print(f"Please authenticate: {auth_url}")
163+
164+
elif session.is_operational:
165+
# Ready to call tools
166+
result = await client.call_tool("hello_world", {"name": "World"})
167+
```
168+
169+
### 4. Registering the Callback Endpoint
170+
171+
```python
172+
from starlette.routing import Route
173+
174+
app = Starlette(routes=[
175+
Route("/oauth/callback", coordinator.get_completion_endpoint()),
176+
])
177+
```
178+
179+
## Environment Variables Reference
180+
181+
| Variable | Required | Default | Description |
182+
|----------|----------|---------|-------------|
183+
| `MCP_SERVER_URL` | No | `http://localhost:8000/mcp` | URL of MCP server to connect to |
184+
| `CALLBACK_HOST` | No | `localhost` | Host for callback server |
185+
| `CALLBACK_PORT` | No | `8080` | Port for callback server |
186+
187+
## Troubleshooting
188+
189+
### "Session stuck in AUTH_PENDING"
190+
191+
- Ensure you completed the OAuth flow in the browser
192+
- Check that the callback URL matches what's configured in Keycard
193+
- Refresh the page after authentication
194+
195+
### "Connection refused to MCP server"
196+
197+
- Verify the `hello_world_server` is running on port 8000
198+
- Check `MCP_SERVER_URL` environment variable
199+
200+
### "OAuth callback not received"
201+
202+
- Ensure `CALLBACK_HOST` and `CALLBACK_PORT` are accessible
203+
- For remote development, use a tunnel (ngrok, Cloudflare Tunnel)
204+
205+
## Learn More
206+
207+
- [Keycard Documentation](https://docs.keycard.ai)
208+
- [MCP Client SDK Documentation](https://docs.keycard.ai/sdk/python/client)
209+
- [Hello World Server Example](../hello_world_server/)

0 commit comments

Comments
 (0)