SESSION_MANAGEMENT_FIX_V2.md•8.48 kB
# Session Management Fix V2 - Global State Pattern
## Problem
Users were experiencing authentication errors after successfully logging in:
```json
{
"success": false,
"error": "Authentication token is missing. Please login first using the 'login' tool."
}
```
## Root Cause Analysis
### Initial Implementation (BROKEN ❌)
```python
# WRONG: Using id(ctx.session) as session key
_session_state = {}
def get_session_state(ctx: Context) -> dict:
session_id = id(ctx.session) # ❌ Different ID each tool call!
if session_id not in _session_state:
_session_state[session_id] = {}
return _session_state[session_id]
```
**Why it failed:**
1. Each MCP tool call receives a **new Context object**
2. `id(ctx.session)` returns a **different memory address** each time
3. Login stores token at `_session_state[12345]`
4. Next tool call looks for token at `_session_state[67890]` → **Not found!**
### Demonstration
```python
class MockSession:
pass
session1 = MockSession()
id1 = id(session1) # → 4308217744
session2 = MockSession()
id2 = id(session2) # → 4310387920
# id1 != id2 → Token lookup fails!
```
## Solution: Global State Pattern ✅
### MCP Architecture Understanding
In the Model Context Protocol:
- **One MCP server instance** = **One client connection** = **One session**
- Each client (like Claude Desktop) spawns its own dedicated server process
- The server process runs for the entire duration of the client connection
- Therefore: We can safely use a **single global state** for all tool calls
### Implementation
```python
# Global session state - shared across ALL tool calls
_session_state = {
"user_token": None,
"refresh_token": None,
"user_email": None,
"user_id": None,
"is_super_admin": False
}
def get_session_state(ctx: Context = None) -> dict:
"""
Get the global session state dictionary.
Context parameter kept for API compatibility but not used.
"""
return _session_state
```
### How It Works
```
┌─────────────────────────────────────────────────────────────┐
│ Claude Desktop Client │
│ │
│ Spawns dedicated MCP server process ──────────────────────┐│
└──────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ MCP Server Process (finizi-b4b) │
│ │
│ Global State (lives for entire session): │
│ { │
│ "user_token": "eyJhbGc...", ◄── Persists! │
│ "user_email": "admin@finizi.ai", │
│ "user_id": "e2e5fbf5-...", │
│ ... │
│ } │
│ │
│ Tool Call #1: login() │
│ → Stores token in global state ✓ │
│ │
│ Tool Call #2: list_entities() │
│ → Reads token from SAME global state ✓ │
│ │
│ Tool Call #3: get_invoice() │
│ → Reads token from SAME global state ✓ │
│ │
└─────────────────────────────────────────────────────────────┘
```
## Code Changes
### File: `src/finizi_b4b_mcp/auth/token_handler.py`
**Before:**
```python
_session_state = {} # Empty dict, keyed by session ID
def get_session_state(ctx: Context) -> dict:
session_id = id(ctx.session) # ❌ New ID each call
if session_id not in _session_state:
_session_state[session_id] = {}
return _session_state[session_id]
```
**After:**
```python
_session_state = { # Global state with defaults
"user_token": None,
"refresh_token": None,
"user_email": None,
"user_id": None,
"is_super_admin": False
}
def get_session_state(ctx: Context = None) -> dict:
return _session_state # ✅ Always returns same dict
```
## Testing Results
### Integration Test
```bash
$ uv run python test_full_integration.py
🔄 Starting full integration test...
✅ Session initialized!
✅ Found 15 tools
🔄 Testing login...
✅ Login result: { "success": true, "message": "Successfully logged in..." }
🔄 Testing whoami (authenticated call)...
✅ Whoami result: { "success": true, "user": {...} }
🔄 Testing list_entities (authenticated call)...
✅ Entities result: { "success": true, "data": [...] }
✅ All integration tests passed!
```
### Workflow Test
1. User calls `login("+84909495665", "Admin123@")`
- Token stored in global `_session_state["user_token"]` ✅
2. User calls `list_entities()`
- Reads token from global `_session_state["user_token"]` ✅
- Makes authenticated API request ✅
3. User calls `get_invoice(...)`
- Reads SAME token from global state ✅
- Works correctly ✅
## Why This is Safe
### Single-Tenant Design
- Each Claude Desktop user gets their own MCP server process
- User A's server process is completely isolated from User B's
- No cross-user contamination possible
### Process Lifecycle
```
Claude Desktop starts
→ Spawns MCP server process
→ Global state initialized
→ User logs in
→ Token stored in global state
→ User makes multiple tool calls (all use same token)
→ User closes Claude Desktop
→ MCP server process terminates
→ Global state destroyed
```
### Security Considerations
✅ **Safe:** Each user has isolated server process
✅ **Safe:** Token only stored in memory (not persisted)
✅ **Safe:** Token destroyed when process exits
✅ **Safe:** No token leakage between users
## Comparison with Per-Session Keying
| Aspect | Per-Session (id) | Global State |
|--------|-----------------|--------------|
| **Complexity** | High (tracking session IDs) | Low (single dict) |
| **Reliability** | ❌ Broken (new ID each call) | ✅ Works perfectly |
| **Memory** | Multiple dict entries | Single dict |
| **MCP Fit** | Mismatched architecture | Matches MCP design |
| **Debugging** | Harder (where's my token?) | Easier (one place) |
## Alternative Approaches Considered
### 1. Using ctx.session directly
```python
# Attempted but ServerSession has no .metadata attribute
ctx.session.metadata["token"] = token # ❌ AttributeError
```
### 2. Using session ID from client_params
```python
# client_params doesn't provide stable session ID
session_id = ctx.session.client_params.get("session_id") # ❌ Not available
```
### 3. Custom session ID in tool parameters
```python
# Would require passing session_id to every tool call
async def login(phone: str, password: str, session_id: str): # ❌ Bad UX
```
## Conclusion
The **global state pattern** is the correct approach for MCP session management because:
1. ✅ Matches MCP's one-process-per-client architecture
2. ✅ Simple and maintainable
3. ✅ Works reliably across all tool calls
4. ✅ Secure (process isolation)
5. ✅ Performant (no dictionary lookups)
## Migration Notes
If you were using the old version:
- No API changes required
- Existing tool calls work the same way
- Login → Tool calls now work correctly
- Just update and restart the MCP server
## Status
✅ **FIXED** - Session state now persists correctly across all tool calls.