Skip to main content
Glama

Finizi B4B MCP Server

by finizi-app
MIT License
SESSION_MANAGEMENT_FIX_V2.md8.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.

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/finizi-app/finizi-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server