Skip to content

Commit b0d51a9

Browse files
committed
feat(keycardai-mcp): session callback notification
1 parent 40f687f commit b0d51a9

File tree

5 files changed

+561
-32
lines changed

5 files changed

+561
-32
lines changed

packages/mcp/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ The Keycard MCP SDK also includes **MCP Client** for connecting to MCP servers w
108108
### Key Features
109109

110110
-**OAuth 2.0 Support**: Automatic OAuth flow handling for protected MCP servers
111+
-**Graceful Connection Handling**: Non-throwing connection behavior with comprehensive status tracking
112+
-**Multi-Server Support**: Connect to multiple servers with independent status tracking
111113
-**AI Agent Integrations**: Pre-built integrations with LangChain and OpenAI Agents SDK
112114

113115
### Quick Start
@@ -126,6 +128,12 @@ servers = {
126128

127129
async def main():
128130
async with Client(servers) as client:
131+
# Connection failures are communicated via session status, not exceptions
132+
session = client.sessions["my-server"]
133+
if not session.is_operational:
134+
print(f"Server not available: {session.status}")
135+
return
136+
129137
# List available tools
130138
tools = await client.list_tools()
131139

packages/mcp/src/keycardai/mcp/client/CONTRIBUTORS.md

Lines changed: 198 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Welcome to the MCP Client implementation! This document provides a comprehensive
66

77
- [Overview](#overview)
88
- [Core Architecture](#core-architecture)
9+
- [Session Status Lifecycle](#session-status-lifecycle)
910
- [Storage Architecture](#storage-architecture)
1011
- [Key Use Cases & Flows](#key-use-cases--flows)
1112
- [Architectural Decisions](#architectural-decisions)
@@ -336,6 +337,196 @@ This example demonstrates:
336337
Each primitive solves a specific architectural challenge. You can mix and match them based on your deployment model (CLI vs web vs serverless).
337338

338339

340+
---
341+
342+
## Session Status Lifecycle
343+
344+
Sessions track their connection state through a comprehensive status lifecycle. This allows applications to handle connection failures gracefully without exceptions.
345+
346+
**Design Philosophy:**
347+
348+
The `connect()` method **does not raise exceptions** for connection failures. Instead, it sets the appropriate session status. This design enables:
349+
- Connecting to multiple servers where some may be unavailable
350+
- Graceful failure handling without try/catch blocks
351+
- Clear status inspection to determine appropriate actions
352+
353+
### Status States
354+
355+
Sessions can be in one of the following states:
356+
357+
| State | Category | Description |
358+
|-------|----------|-------------|
359+
| `INITIALIZING` | Initial | Session created but not yet connected |
360+
| `CONNECTING` | Active | Establishing connection to server |
361+
| `AUTHENTICATING` | Active | Connection established, authentication in progress |
362+
| `AUTH_PENDING` | Pending | Waiting for external auth (e.g., OAuth callback) |
363+
| `CONNECTED` | Active | Fully connected and operational |
364+
| `RECONNECTING` | Recovery | Attempting to reconnect after failure |
365+
| `DISCONNECTING` | Disconnection | Gracefully closing connection |
366+
| `DISCONNECTED` | Terminal | Clean disconnect, can reconnect |
367+
| `CONNECTION_FAILED` | Failure | Failed to establish connection |
368+
| `SERVER_UNREACHABLE` | Failure | Server not responding/not available |
369+
| `AUTH_FAILED` | Failure | Authentication failed |
370+
| `FAILED` | Failure | General failure state |
371+
372+
### Status Categories
373+
374+
Status states are organized into logical groups:
375+
376+
**Active States:** Session is attempting or maintaining a connection
377+
- `CONNECTING`, `AUTHENTICATING`, `CONNECTED`, `RECONNECTING`
378+
379+
**Failure States:** Session encountered an error
380+
- `AUTH_FAILED`, `CONNECTION_FAILED`, `SERVER_UNREACHABLE`, `FAILED`
381+
382+
**Recoverable States:** Failure states that can be retried
383+
- `CONNECTION_FAILED`, `SERVER_UNREACHABLE`, `AUTH_FAILED`
384+
385+
**Terminal States:** Session is not connected and requires action
386+
- `DISCONNECTED`, `FAILED`
387+
388+
**Pending States:** Session is waiting for external action
389+
- `AUTH_PENDING`
390+
391+
### Status Properties
392+
393+
Sessions provide convenient properties for checking status:
394+
395+
```python
396+
session = client.sessions["my-server"]
397+
398+
# Check if ready to use
399+
if session.is_operational: # Only true when CONNECTED
400+
result = await client.call_tool("my_tool", {})
401+
402+
# Check for failures
403+
if session.is_failed: # Any failure state
404+
print(f"Connection failed: {session.status}")
405+
if session.can_retry: # Recoverable failure
406+
await client.connect(server="my-server", force_reconnect=True)
407+
408+
# Check if waiting for user action
409+
if session.requires_user_action: # AUTH_PENDING
410+
challenges = await client.get_auth_challenges()
411+
# Handle OAuth flow
412+
413+
# Check if currently connecting
414+
if session.is_connecting: # CONNECTING, AUTHENTICATING, or RECONNECTING
415+
print("Connection in progress...")
416+
417+
# Check connection health
418+
is_healthy = await session.check_connection_health()
419+
if not is_healthy:
420+
print(f"Connection unhealthy: {session.status}")
421+
```
422+
423+
### Auto-Reconnection on Auth Completion
424+
425+
**Important:** Sessions automatically reconnect when authentication completes. You **do not** need to manually call `connect()` after OAuth finishes.
426+
427+
**How it works:**
428+
1. Session enters `AUTH_PENDING` state when authentication is required
429+
2. Session subscribes to the coordinator's completion events
430+
3. When OAuth callback completes, the coordinator notifies all subscribers
431+
4. Session receives notification and automatically reconnects
432+
5. Status updates from `AUTH_PENDING``CONNECTING``AUTHENTICATING``CONNECTED`
433+
434+
### Usage Patterns
435+
436+
**Pattern 1: Check Status After Connection**
437+
438+
```python
439+
await client.connect()
440+
441+
for server_name, session in client.sessions.items():
442+
if session.is_operational:
443+
print(f"{server_name}: Ready")
444+
elif session.is_failed:
445+
print(f"{server_name}: Failed - {session.status}")
446+
elif session.requires_user_action:
447+
print(f"{server_name}: Requires authentication")
448+
```
449+
450+
**Pattern 2: Handle Mixed Server Availability**
451+
452+
```python
453+
# Connect to all servers (some may fail)
454+
await client.connect()
455+
456+
# Use only operational servers
457+
operational_servers = [
458+
name for name, session in client.sessions.items()
459+
if session.is_operational
460+
]
461+
462+
if operational_servers:
463+
tools = await client.list_tools()
464+
# Work with available servers
465+
else:
466+
print("No servers available")
467+
```
468+
469+
**Pattern 3: Retry Failed Connections**
470+
471+
```python
472+
await client.connect()
473+
474+
for server_name, session in client.sessions.items():
475+
if session.is_failed and session.can_retry:
476+
print(f"Retrying {server_name}...")
477+
await client.connect(server=server_name, force_reconnect=True)
478+
```
479+
480+
**Pattern 4: Health Check for Long-Running Sessions**
481+
482+
```python
483+
# Periodically check connection health
484+
async def monitor_connections(client):
485+
while True:
486+
await asyncio.sleep(60) # Check every minute
487+
488+
for server_name, session in client.sessions.items():
489+
if session.is_operational:
490+
is_healthy = await session.check_connection_health()
491+
if not is_healthy:
492+
print(f"{server_name} unhealthy, reconnecting...")
493+
await client.connect(server=server_name, force_reconnect=True)
494+
```
495+
496+
### State Transition Examples
497+
498+
**Successful Connection (No Auth):**
499+
```
500+
INITIALIZING → CONNECTING → AUTHENTICATING → CONNECTED
501+
```
502+
503+
**Connection with OAuth:**
504+
```
505+
INITIALIZING → CONNECTING → AUTHENTICATING → AUTH_PENDING
506+
(user completes OAuth)
507+
→ CONNECTING → AUTHENTICATING → CONNECTED
508+
```
509+
510+
**Server Unreachable:**
511+
```
512+
INITIALIZING → CONNECTING → SERVER_UNREACHABLE
513+
(retry)
514+
→ CONNECTING → AUTHENTICATING → CONNECTED
515+
```
516+
517+
**Graceful Disconnect:**
518+
```
519+
CONNECTED → DISCONNECTING → DISCONNECTED
520+
```
521+
522+
**Connection Lost During Operation:**
523+
```
524+
CONNECTED → CONNECTION_FAILED
525+
(health check detects failure)
526+
```
527+
528+
See [`SESSION_STATUS_DESIGN.md`](SESSION_STATUS_DESIGN.md) and [`SESSION_LIFECYCLE.md`](SESSION_LIFECYCLE.md) for detailed state transitions and sequence diagrams.
529+
339530
---
340531

341532
## Storage Architecture
@@ -635,11 +826,13 @@ sequenceDiagram
635826
636827
CallbackProcess-->>Browser: "Authorization successful"
637828
638-
Note over Session,CallbackProcess: Process C - Reconnection
829+
Note over Session,CallbackProcess: Process C - Auto-Reconnection (Event-Driven)
639830
640-
Session->>Session: await connect()
831+
Coordinator->>Session: on_completion_handled(event)
832+
Note over Session: Session was subscribed when AUTH_PENDING
833+
Session->>Session: Automatically await connect()
641834
Session->>Storage: get("tokens")
642-
Storage-->>Session: Tokens available, connect with auth
835+
Storage-->>Session: Tokens available, CONNECTED!
643836
```
644837

645838
**Key Points:**
@@ -649,6 +842,7 @@ sequenceDiagram
649842
- Different process can handle callback
650843
- StarletteAuthCoordinator **doesn't block** on redirect (returns HTTP response)
651844
- Same strategy code as LocalAuthCoordinator, different coordinator behavior
845+
- **Auto-reconnection**: Session subscribes to completion events and reconnects automatically (no manual `connect()` needed)
652846

653847
---
654848

@@ -1120,6 +1314,7 @@ Clear boundaries help maintain clean architecture.
11201314
- Manage lifecycle (connect/disconnect)
11211315
- Create server-specific namespace
11221316
- Detect and expose auth challenges
1317+
- Subscribe to auth completion events (auto-reconnect)
11231318
- Delegate to upstream MCP ClientSession
11241319
- **Does NOT**:
11251320
- Implement transport (delegates to Connection)

0 commit comments

Comments
 (0)