CRITICAL FIX - Database Parameter (graphiti_core):
- Fixed graphiti_core/driver/neo4j_driver.py execute_query method
- database_ parameter was incorrectly added to params dict instead of kwargs
- Now correctly passed as keyword argument to Neo4j driver
- Impact: All queries now execute in configured database (not default 'neo4j')
- Root cause: Violated Neo4j Python driver API contract
Technical Details:
Previous code (BROKEN):
params.setdefault('database_', self._database) # Wrong - in params dict
result = await self.client.execute_query(cypher_query_, parameters_=params, **kwargs)
Fixed code (CORRECT):
kwargs.setdefault('database_', self._database) # Correct - in kwargs
result = await self.client.execute_query(cypher_query_, parameters_=params, **kwargs)
FIX - Index Creation Error Handling (MCP server):
- Added graceful handling for Neo4j IF NOT EXISTS bug
- Prevents MCP server crash when indices already exist
- Logs warning instead of failing initialization
- Handles EquivalentSchemaRuleAlreadyExists error gracefully
Files Modified:
- graphiti_core/driver/neo4j_driver.py (3 lines changed)
- mcp_server/src/graphiti_mcp_server.py (12 lines added error handling)
- mcp_server/pyproject.toml (version bump to 1.0.5)
Testing:
- Python syntax validation: PASSED
- Ruff formatting: PASSED
- Ruff linting: PASSED
Closes issues with:
- Data being stored in wrong Neo4j database
- MCP server crashing on startup with EquivalentSchemaRuleAlreadyExists
- NEO4J_DATABASE environment variable being ignored
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
1009 lines
28 KiB
Markdown
1009 lines
28 KiB
Markdown
# Per-User Graph Isolation via HTTP Headers - Technical Analysis
|
|
|
|
**Date**: November 8, 2025
|
|
**Status**: INVESTIGATION PHASE - Not Yet Implemented
|
|
**Priority**: Medium - Enhancement for Multi-User LibreChat Deployments
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
This document analyzes the feasibility of implementing per-user graph isolation in the Graphiti MCP server using HTTP headers from LibreChat, allowing each user to have their own isolated knowledge graph without modifying tool signatures.
|
|
|
|
**Verdict**: **FEASIBLE BUT COMPLEX** - Technology supports this approach, but several critical issues must be addressed before implementation.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Background](#background)
|
|
2. [Proposed Solution](#proposed-solution)
|
|
3. [Critical Analysis](#critical-analysis)
|
|
4. [LibreChat Capabilities](#librechat-capabilities)
|
|
5. [FastMCP Middleware Support](#fastmcp-middleware-support)
|
|
6. [Implementation Approaches](#implementation-approaches)
|
|
7. [Known Issues & Risks](#known-issues--risks)
|
|
8. [Requirements for Implementation](#requirements-for-implementation)
|
|
9. [Alternative Approaches](#alternative-approaches)
|
|
10. [Next Steps](#next-steps)
|
|
|
|
---
|
|
|
|
## Background
|
|
|
|
### Current State
|
|
|
|
The Graphiti MCP server currently uses a single `group_id` for all users, meaning:
|
|
- All users share the same knowledge graph
|
|
- No data isolation between users
|
|
- Configured via `config.graphiti.group_id` or CLI argument
|
|
|
|
### Desired State
|
|
|
|
Enable per-user graph isolation where:
|
|
- Each LibreChat user has their own knowledge graph
|
|
- Isolation happens automatically via HTTP headers
|
|
- LLMs don't need to know about multi-user architecture
|
|
- Tools work identically for single and multi-user deployments
|
|
|
|
### Use Case
|
|
|
|
Multi-user LibreChat deployment where:
|
|
- User A's preferences/conversations → Graph A
|
|
- User B's preferences/conversations → Graph B
|
|
- No data leakage between users
|
|
|
|
---
|
|
|
|
## Proposed Solution
|
|
|
|
### High-Level Architecture
|
|
|
|
```
|
|
LibreChat (per-user session)
|
|
↓
|
|
Headers: { X-User-ID: "user_12345" }
|
|
↓
|
|
FastMCP Middleware
|
|
↓
|
|
Extracts user_id from headers
|
|
↓
|
|
Stores in request context
|
|
↓
|
|
MCP Tools (add_memory, search_nodes, etc.)
|
|
↓
|
|
Uses: group_id = explicit_param OR context_user_id OR config_default
|
|
↓
|
|
Graphiti Core (with user-specific group_id)
|
|
```
|
|
|
|
### Technical Approach
|
|
|
|
**Option A: Direct Header Access in Tools**
|
|
```python
|
|
@mcp.tool()
|
|
async def add_memory(
|
|
name: str,
|
|
episode_body: str,
|
|
group_id: str | None = None,
|
|
...
|
|
):
|
|
from fastmcp.server.dependencies import get_http_headers
|
|
|
|
headers = get_http_headers()
|
|
user_id = headers.get("x-user-id")
|
|
|
|
effective_group_id = group_id or user_id or config.graphiti.group_id
|
|
# ... rest of implementation
|
|
```
|
|
|
|
**Option B: Middleware + Context State** (Recommended)
|
|
```python
|
|
class UserContextMiddleware(Middleware):
|
|
async def on_request(self, context: MiddlewareContext, call_next):
|
|
headers = get_http_headers()
|
|
user_id = headers.get("x-user-id")
|
|
|
|
if context.fastmcp_context and user_id:
|
|
context.fastmcp_context.set_state("user_id", user_id)
|
|
logger.info(f"Request from user_id: {user_id}")
|
|
|
|
return await call_next(context)
|
|
|
|
mcp.add_middleware(UserContextMiddleware())
|
|
|
|
# In tools:
|
|
@mcp.tool()
|
|
async def add_memory(
|
|
name: str,
|
|
episode_body: str,
|
|
group_id: str | None = None,
|
|
ctx: Context | None = None,
|
|
...
|
|
):
|
|
user_id = ctx.get_state("user_id") if ctx else None
|
|
effective_group_id = group_id or user_id or config.graphiti.group_id
|
|
```
|
|
|
|
---
|
|
|
|
## Critical Analysis
|
|
|
|
A comprehensive architectural review identified several critical concerns:
|
|
|
|
### ✅ Valid Points
|
|
|
|
1. **LibreChat Officially Supports This Pattern**
|
|
- Headers with user context are a documented, core feature
|
|
- `{{LIBRECHAT_USER_ID}}` placeholder designed for this use case
|
|
- Per-user connection management built into LibreChat
|
|
|
|
2. **FastMCP Middleware Exists**
|
|
- Added in FastMCP v2.9.0
|
|
- Supports request interception and context injection
|
|
- `get_http_headers()` dependency function available
|
|
|
|
3. **Context State Management Available**
|
|
- Request-scoped state via `ctx.set_state()` / `ctx.get_state()`
|
|
- Async-safe context handling
|
|
|
|
### ⚠️ Critical Concerns
|
|
|
|
#### 1. **MCP Protocol Transport Coupling**
|
|
|
|
**Issue**: Using HTTP headers creates transport dependency
|
|
- MCP is designed to be transport-agnostic (stdio, sse, http)
|
|
- Header-based isolation only works with HTTP transports
|
|
- **Impact**: stdio/sse transports won't have per-user isolation
|
|
|
|
**Mitigation**:
|
|
- Document HTTP transport requirement
|
|
- Detect transport type at runtime
|
|
- Provide graceful fallback for non-HTTP transports
|
|
|
|
#### 2. **Queue Service Context Loss**
|
|
|
|
**Issue**: Background task spawning may lose context
|
|
```python
|
|
# From queue_service.py:45
|
|
asyncio.create_task(self._process_episode_queue(group_id))
|
|
```
|
|
|
|
Context state is request-scoped only:
|
|
> "Context is scoped to a single request; state set in one request will not be available in subsequent requests"
|
|
|
|
**Risk**: Episodes processed in background queues may use wrong/missing group_id
|
|
|
|
**Solution**: Pass user_id explicitly to queue service
|
|
```python
|
|
await queue_service.add_episode(
|
|
group_id=effective_group_id,
|
|
user_id=user_id, # Pass explicitly for background processing
|
|
...
|
|
)
|
|
```
|
|
|
|
#### 3. **Neo4j Driver Thread Pool Context Loss**
|
|
|
|
**Issue**: Neo4j async driver uses thread pools internally
|
|
- Python ContextVars may not propagate across thread boundaries
|
|
- Could cause context loss during database operations
|
|
|
|
**Testing Required**: Verify context preservation with concurrent Neo4j operations
|
|
|
|
#### 4. **Stale Context Bug in StreamableHTTP** ⚠️ BLOCKER
|
|
|
|
**Known FastMCP Issue**:
|
|
> "When using StreamableHTTP transport with multiple requests in the same session, MCP tool execution consistently receives stale HTTP request context from the first request"
|
|
|
|
**Impact**: User A's request might receive User B's context
|
|
**Severity**: CRITICAL - Data isolation violation
|
|
|
|
**Mitigation**:
|
|
- Test thoroughly with concurrent requests
|
|
- Add request ID logging to detect stale context
|
|
- Monitor FastMCP issue tracker for fix
|
|
- Consider request ID validation in middleware
|
|
|
|
#### 5. **Security & Fallback Behavior**
|
|
|
|
**Missing Header Scenario**:
|
|
```
|
|
Request with no X-User-ID
|
|
↓
|
|
user_id = None
|
|
↓
|
|
Falls back to config.graphiti.group_id = "main"
|
|
↓
|
|
SECURITY ISSUE: User writes to shared graph
|
|
```
|
|
|
|
**Header Injection Attack**:
|
|
```
|
|
Attacker sends: X-User-ID: admin
|
|
↓
|
|
Gains access to admin's graph
|
|
↓
|
|
PRIVILEGE ESCALATION
|
|
```
|
|
|
|
**Required Mitigations**:
|
|
- Validate header presence in multi-user mode
|
|
- Reject requests missing X-User-ID (401/403)
|
|
- Validate user_id format (alphanumeric, max length)
|
|
- Consider verifying user_id against auth token
|
|
- Add defense-in-depth even if LibreChat validates
|
|
|
|
#### 6. **Debugging & Observability**
|
|
|
|
**Problem**: Implicit state makes debugging difficult
|
|
|
|
**Required Logging**:
|
|
```python
|
|
logger.info(
|
|
f"Tool: add_memory | episode: {name} | "
|
|
f"group_id={effective_group_id} | "
|
|
f"source=(explicit={group_id}, context={user_id}, default={config.graphiti.group_id}) | "
|
|
f"request_id={ctx.request_id if ctx else 'N/A'}"
|
|
)
|
|
```
|
|
|
|
**Metrics Needed**:
|
|
- `tool_calls_by_user_id`
|
|
- `context_fallback_count` (when header missing)
|
|
- `stale_context_detected` (request ID mismatches)
|
|
|
|
#### 7. **LLM Override Behavior**
|
|
|
|
**Design Question**: What happens if LLM explicitly passes `group_id`?
|
|
|
|
```python
|
|
# LLM calls:
|
|
add_memory(name="...", episode_body="...", group_id="other_user")
|
|
```
|
|
|
|
**Options**:
|
|
1. **Explicit param wins** (flexible but risky)
|
|
```python
|
|
effective_group_id = group_id or user_id or config_default
|
|
```
|
|
|
|
2. **Header always wins** (strict isolation)
|
|
```python
|
|
effective_group_id = user_id or group_id or config_default
|
|
```
|
|
|
|
3. **Reject mismatch** (paranoid)
|
|
```python
|
|
if group_id and user_id and group_id != user_id:
|
|
raise PermissionError("Cannot access other user's graph")
|
|
```
|
|
|
|
**Decision Required**: Choose based on security requirements
|
|
|
|
---
|
|
|
|
## LibreChat Capabilities
|
|
|
|
### Headers Configuration
|
|
|
|
LibreChat supports dynamic header substitution in `librechat.yaml`:
|
|
|
|
```yaml
|
|
mcpServers:
|
|
graphiti-memory:
|
|
url: "http://graphiti-mcp:8000/mcp/"
|
|
headers:
|
|
X-User-ID: "{{LIBRECHAT_USER_ID}}"
|
|
X-User-Email: "{{LIBRECHAT_USER_EMAIL}}"
|
|
```
|
|
|
|
### Available User Placeholders
|
|
|
|
- `{{LIBRECHAT_USER_ID}}` - Unique user identifier
|
|
- `{{LIBRECHAT_USER_NAME}}` - User display name
|
|
- `{{LIBRECHAT_USER_EMAIL}}` - User email address
|
|
- `{{LIBRECHAT_USER_ROLE}}` - User role
|
|
- `{{LIBRECHAT_USER_PROVIDER}}` - Auth provider
|
|
- Social auth IDs (Google, GitHub, etc.)
|
|
|
|
### Multi-User Features
|
|
|
|
LibreChat provides:
|
|
- **Per-user connection management**: Separate MCP connections per user
|
|
- **User idle management**: Disconnects after 15 minutes inactivity
|
|
- **Connection lifecycle**: Proper setup/teardown per user session
|
|
- **Custom user variables**: Per-user credentials storage
|
|
|
|
### Transport Requirements
|
|
|
|
Headers only work with:
|
|
- `sse` (Server-Sent Events)
|
|
- `streamable-http` (HTTP with streaming)
|
|
|
|
Not supported:
|
|
- `stdio` (standard input/output)
|
|
|
|
---
|
|
|
|
## FastMCP Middleware Support
|
|
|
|
### Middleware System (v2.9.0+)
|
|
|
|
FastMCP provides a pipeline-based middleware system:
|
|
|
|
```python
|
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
from fastmcp.server.dependencies import get_http_headers
|
|
|
|
class UserContextMiddleware(Middleware):
|
|
async def on_request(self, context: MiddlewareContext, call_next):
|
|
# Extract headers
|
|
headers = get_http_headers()
|
|
user_id = headers.get("x-user-id")
|
|
|
|
# Validate and store
|
|
if user_id:
|
|
if not self._validate_user_id(user_id):
|
|
raise ValueError(f"Invalid user_id format: {user_id}")
|
|
|
|
if context.fastmcp_context:
|
|
context.fastmcp_context.set_state("user_id", user_id)
|
|
logger.info(f"User context set: {user_id}")
|
|
else:
|
|
logger.warning("Missing X-User-ID header")
|
|
|
|
return await call_next(context)
|
|
|
|
def _validate_user_id(self, user_id: str) -> bool:
|
|
import re
|
|
return bool(re.match(r'^[a-zA-Z0-9_-]{1,64}$', user_id))
|
|
|
|
# Add to server
|
|
mcp.add_middleware(UserContextMiddleware())
|
|
```
|
|
|
|
### Context Access in Tools
|
|
|
|
**Via Dependency Injection**:
|
|
```python
|
|
@mcp.tool()
|
|
async def my_tool(ctx: Context) -> str:
|
|
user_id = ctx.get_state("user_id")
|
|
return f"Processing for user: {user_id}"
|
|
```
|
|
|
|
**Direct Header Access**:
|
|
```python
|
|
from fastmcp.server.dependencies import get_http_headers
|
|
|
|
@mcp.tool()
|
|
async def my_tool() -> str:
|
|
headers = get_http_headers()
|
|
user_id = headers.get("x-user-id")
|
|
return f"User: {user_id}"
|
|
```
|
|
|
|
### Middleware Execution Order
|
|
|
|
```python
|
|
mcp.add_middleware(AuthMiddleware()) # Runs first
|
|
mcp.add_middleware(UserContextMiddleware()) # Runs second
|
|
mcp.add_middleware(LoggingMiddleware()) # Runs third
|
|
```
|
|
|
|
Order matters: First added runs first on request, last on response.
|
|
|
|
### Context Scope & Limitations
|
|
|
|
**Request-Scoped Only**:
|
|
- Each MCP request gets new context
|
|
- State doesn't persist between requests
|
|
- Background tasks may lose context
|
|
|
|
**Transport Compatibility**:
|
|
> "Middleware inspecting HTTP headers won't work with stdio transport"
|
|
|
|
**Known Breaking Changes**:
|
|
> "MCP middleware is a brand new concept and may be subject to breaking changes in future versions"
|
|
|
|
---
|
|
|
|
## Implementation Approaches
|
|
|
|
### Approach 1: Direct Header Access (Simple)
|
|
|
|
**Implementation**:
|
|
```python
|
|
@mcp.tool()
|
|
async def add_memory(
|
|
name: str,
|
|
episode_body: str,
|
|
group_id: str | None = None,
|
|
...
|
|
) -> SuccessResponse | ErrorResponse:
|
|
from fastmcp.server.dependencies import get_http_headers
|
|
|
|
headers = get_http_headers()
|
|
user_id = headers.get("x-user-id")
|
|
|
|
effective_group_id = group_id or user_id or config.graphiti.group_id
|
|
|
|
logger.info(f"add_memory: group_id={effective_group_id} (explicit={group_id}, header={user_id})")
|
|
|
|
# ... rest of implementation
|
|
```
|
|
|
|
**Pros**:
|
|
- Simple, no middleware needed
|
|
- Direct, explicit header access
|
|
- Easy to debug
|
|
|
|
**Cons**:
|
|
- Code duplication across 8-10 tools
|
|
- No centralized logging
|
|
- Harder to add validation
|
|
|
|
**Tools to Modify**: ~8-10 tools
|
|
- `add_memory`
|
|
- `search_nodes`
|
|
- `get_entities_by_type`
|
|
- `search_memory_facts`
|
|
- `compare_facts_over_time`
|
|
- `delete_entity_edge`
|
|
- `delete_episode`
|
|
- `get_entity_edge`
|
|
- `get_episodes`
|
|
- `clear_graph`
|
|
|
|
### Approach 2: Middleware + Context State (Recommended)
|
|
|
|
**Implementation**:
|
|
|
|
**Step 1: Add Middleware**
|
|
```python
|
|
# mcp_server/src/middleware/user_context.py
|
|
|
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
from fastmcp.server.dependencies import get_http_headers
|
|
import logging
|
|
import re
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class UserContextMiddleware(Middleware):
|
|
"""Extract user_id from X-User-ID header and store in context."""
|
|
|
|
def __init__(self, require_user_id: bool = False):
|
|
self.require_user_id = require_user_id
|
|
|
|
async def on_request(self, context: MiddlewareContext, call_next):
|
|
headers = get_http_headers()
|
|
user_id = headers.get("x-user-id")
|
|
|
|
if user_id:
|
|
# Validate format
|
|
if not self._validate_user_id(user_id):
|
|
logger.error(f"Invalid user_id format: {user_id}")
|
|
raise ValueError(f"Invalid X-User-ID format")
|
|
|
|
# Store in context
|
|
if context.fastmcp_context:
|
|
context.fastmcp_context.set_state("user_id", user_id)
|
|
logger.debug(f"User context established: {user_id}")
|
|
|
|
elif self.require_user_id:
|
|
logger.error("Missing required X-User-ID header")
|
|
raise ValueError("X-User-ID header is required for multi-user mode")
|
|
|
|
else:
|
|
logger.warning("X-User-ID header not provided, using default group_id")
|
|
|
|
# Log request with user context
|
|
method = context.method
|
|
logger.info(f"Request: {method} | user_id={user_id or 'default'}")
|
|
|
|
result = await call_next(context)
|
|
return result
|
|
|
|
@staticmethod
|
|
def _validate_user_id(user_id: str) -> bool:
|
|
"""Validate user_id format: alphanumeric, dash, underscore, 1-64 chars."""
|
|
return bool(re.match(r'^[a-zA-Z0-9_-]{1,64}$', user_id))
|
|
```
|
|
|
|
**Step 2: Register Middleware**
|
|
```python
|
|
# mcp_server/src/graphiti_mcp_server.py
|
|
|
|
from middleware.user_context import UserContextMiddleware
|
|
|
|
# After mcp = FastMCP(...) initialization
|
|
# Set require_user_id=True for strict multi-user mode
|
|
mcp.add_middleware(UserContextMiddleware(require_user_id=False))
|
|
```
|
|
|
|
**Step 3: Modify Tools**
|
|
```python
|
|
@mcp.tool()
|
|
async def add_memory(
|
|
name: str,
|
|
episode_body: str,
|
|
group_id: str | None = None,
|
|
ctx: Context | None = None,
|
|
...
|
|
) -> SuccessResponse | ErrorResponse:
|
|
# Extract user_id from context
|
|
user_id = ctx.get_state("user_id") if ctx else None
|
|
|
|
# Priority: explicit param > context user_id > config default
|
|
effective_group_id = group_id or user_id or config.graphiti.group_id
|
|
|
|
# Detailed logging
|
|
logger.info(
|
|
f"add_memory: episode={name} | "
|
|
f"group_id={effective_group_id} | "
|
|
f"source=(explicit={group_id}, context={user_id}, default={config.graphiti.group_id})"
|
|
)
|
|
|
|
# ... rest of implementation
|
|
```
|
|
|
|
**Pros**:
|
|
- Centralized user extraction and validation
|
|
- DRY (Don't Repeat Yourself)
|
|
- Consistent logging across all tools
|
|
- Easy to add security checks
|
|
- Single point for header validation
|
|
|
|
**Cons**:
|
|
- Requires adding `ctx: Context | None` to all tool signatures
|
|
- More complex initial setup
|
|
- Context state is request-scoped only
|
|
|
|
### Approach 3: Hybrid Approach
|
|
|
|
Combine both approaches:
|
|
- Use middleware for logging, validation, metrics
|
|
- Use direct header access in tools (simpler signatures)
|
|
|
|
**Implementation**:
|
|
```python
|
|
class UserContextMiddleware(Middleware):
|
|
async def on_request(self, context: MiddlewareContext, call_next):
|
|
headers = get_http_headers()
|
|
user_id = headers.get("x-user-id")
|
|
|
|
# Validate and log, but don't store
|
|
if user_id:
|
|
if not self._validate_user_id(user_id):
|
|
raise ValueError("Invalid user_id format")
|
|
logger.info(f"Request from user: {user_id}")
|
|
|
|
return await call_next(context)
|
|
|
|
# In tools: still access headers directly
|
|
@mcp.tool()
|
|
async def add_memory(...):
|
|
headers = get_http_headers()
|
|
user_id = headers.get("x-user-id")
|
|
effective_group_id = group_id or user_id or config.graphiti.group_id
|
|
```
|
|
|
|
**Pros**:
|
|
- No need to modify tool signatures
|
|
- Centralized validation and logging
|
|
- Simpler tool implementation
|
|
|
|
**Cons**:
|
|
- Still some duplication in tools
|
|
- Two places reading the same header
|
|
|
|
---
|
|
|
|
## Known Issues & Risks
|
|
|
|
### 🚨 Critical Blockers
|
|
|
|
1. **Stale Context in StreamableHTTP** - FastMCP bug
|
|
- **Severity**: CRITICAL
|
|
- **Impact**: User A receives User B's context
|
|
- **Status**: Known issue in FastMCP
|
|
- **Mitigation**: Thorough testing, request ID logging
|
|
- **Link**: GitHub issue tracking required
|
|
|
|
2. **Queue Service Context Loss**
|
|
- **Severity**: HIGH
|
|
- **Impact**: Background episodes use wrong group_id
|
|
- **Status**: Architectural limitation
|
|
- **Mitigation**: Pass user_id explicitly to queue
|
|
- **Testing**: Integration tests with concurrent episodes
|
|
|
|
3. **Neo4j Thread Pool Context**
|
|
- **Severity**: MEDIUM-HIGH
|
|
- **Impact**: Database operations may lose user context
|
|
- **Status**: Needs verification
|
|
- **Mitigation**: Test with concurrent database operations
|
|
- **Testing**: Load testing with multiple users
|
|
|
|
### ⚠️ Major Concerns
|
|
|
|
4. **Security - Missing Header Fallback**
|
|
- **Risk**: Users write to shared graph when header missing
|
|
- **Mitigation**: Require header in multi-user mode
|
|
- **Config**: Add `REQUIRE_USER_ID` environment variable
|
|
|
|
5. **Security - Header Injection**
|
|
- **Risk**: Attacker spoofs user_id to access other graphs
|
|
- **Mitigation**: Validate header format, trust LibreChat validation
|
|
- **Enhancement**: Add header signature verification
|
|
|
|
6. **Debugging Complexity**
|
|
- **Risk**: Difficult to troubleshoot which group_id was used
|
|
- **Mitigation**: Comprehensive structured logging
|
|
- **Metrics**: Expose per-user metrics
|
|
|
|
7. **LLM Override Ambiguity**
|
|
- **Risk**: Unclear behavior when LLM passes explicit group_id
|
|
- **Mitigation**: Choose and document priority strategy
|
|
- **Testing**: Test explicit param vs header precedence
|
|
|
|
### ⚙️ Minor Considerations
|
|
|
|
8. **Transport Limitation**
|
|
- **Impact**: Only works with HTTP transports
|
|
- **Mitigation**: Document requirement clearly
|
|
- **Detection**: Add runtime transport detection
|
|
|
|
9. **FastMCP API Stability**
|
|
- **Risk**: Breaking changes in middleware API
|
|
- **Mitigation**: Pin FastMCP version, monitor changelog
|
|
- **Version**: Test with FastMCP 2.9.0+
|
|
|
|
10. **Configuration Drift**
|
|
- **Risk**: Logs show default group_id but actual varies
|
|
- **Mitigation**: Log effective group_id per request
|
|
- **UX**: Update startup logging for multi-user mode
|
|
|
|
---
|
|
|
|
## Requirements for Implementation
|
|
|
|
### Mandatory Testing
|
|
|
|
- [ ] **Verify stale context bug** with StreamableHTTP
|
|
- Create integration test with concurrent requests
|
|
- Validate each request receives correct user_id
|
|
- Log request IDs to detect context reuse
|
|
|
|
- [ ] **Test queue service context propagation**
|
|
- Add episodes concurrently from different users
|
|
- Verify each episode uses correct group_id
|
|
- Check background task context inheritance
|
|
|
|
- [ ] **Test Neo4j thread pool behavior**
|
|
- Concurrent database operations from multiple users
|
|
- Verify context doesn't bleed across threads
|
|
- Load test with realistic concurrency
|
|
|
|
- [ ] **Test missing header scenarios**
|
|
- Request without X-User-ID header
|
|
- Verify fallback behavior (reject vs default)
|
|
- Test error messages and logging
|
|
|
|
- [ ] **Test explicit group_id override**
|
|
- LLM passes group_id parameter
|
|
- Verify precedence (param vs header)
|
|
- Test security implications
|
|
|
|
### Mandatory Implementation
|
|
|
|
- [ ] **Add UserContextMiddleware**
|
|
- Extract X-User-ID header
|
|
- Validate format (alphanumeric, 1-64 chars)
|
|
- Store in context state or use directly
|
|
- Add structured logging
|
|
|
|
- [ ] **Modify all tools** (8-10 tools)
|
|
- Add context parameter or direct header access
|
|
- Implement group_id priority logic
|
|
- Add detailed logging per tool
|
|
|
|
- [ ] **Update queue service**
|
|
- Pass user_id explicitly for background tasks
|
|
- Verify context doesn't get lost
|
|
- Add queue-specific logging
|
|
|
|
- [ ] **Add comprehensive logging**
|
|
- Log effective group_id for every operation
|
|
- Include source (explicit/context/default)
|
|
- Add request ID correlation
|
|
- Structured logging format
|
|
|
|
- [ ] **Add security validations**
|
|
- User ID format validation
|
|
- Header presence check (if required)
|
|
- Rate limiting per user_id
|
|
- Audit logging for security events
|
|
|
|
- [ ] **Update configuration**
|
|
- Add `REQUIRE_USER_ID` environment variable
|
|
- Add `MULTI_USER_MODE` flag
|
|
- Document HTTP transport requirement
|
|
- Update example configs
|
|
|
|
### Recommended Enhancements
|
|
|
|
- [ ] **Add observability**
|
|
- Prometheus metrics: `tool_calls_by_user{user_id="X"}`
|
|
- Context fallback counter
|
|
- Stale context detection counter
|
|
- Request duration per user
|
|
|
|
- [ ] **Add admin capabilities**
|
|
- Admin override for debugging
|
|
- View all users' graphs
|
|
- Cross-user search (admin only)
|
|
|
|
- [ ] **Add documentation**
|
|
- Update MCP server README
|
|
- Update LibreChat setup guide
|
|
- Add troubleshooting section
|
|
- Document security model
|
|
|
|
- [ ] **Add migration path**
|
|
- Script to split shared graph by user
|
|
- Backup/restore per user
|
|
- User data export
|
|
|
|
### Testing Checklist
|
|
|
|
**Unit Tests**:
|
|
- [ ] UserContextMiddleware header extraction
|
|
- [ ] User ID validation logic
|
|
- [ ] Group ID priority logic
|
|
- [ ] Error handling for missing headers
|
|
|
|
**Integration Tests**:
|
|
- [ ] Concurrent requests from different users
|
|
- [ ] Queue service with multiple users
|
|
- [ ] Database operations with user context
|
|
- [ ] All tools with user isolation
|
|
|
|
**Security Tests**:
|
|
- [ ] Header injection attempts
|
|
- [ ] Missing header handling
|
|
- [ ] Explicit group_id override attempts
|
|
- [ ] Rate limiting per user
|
|
|
|
**Performance Tests**:
|
|
- [ ] 10+ concurrent users
|
|
- [ ] 100+ episodes queued
|
|
- [ ] Context overhead measurement
|
|
- [ ] Database connection pooling
|
|
|
|
---
|
|
|
|
## Alternative Approaches
|
|
|
|
### Alternative 1: Explicit group_id Required
|
|
|
|
Make `group_id` a required parameter in all tools:
|
|
|
|
```python
|
|
@mcp.tool()
|
|
async def add_memory(
|
|
name: str,
|
|
episode_body: str,
|
|
group_id: str, # REQUIRED - no default
|
|
...
|
|
):
|
|
"""LibreChat must provide group_id in every call."""
|
|
# No fallback logic needed
|
|
```
|
|
|
|
**Pros**:
|
|
- Explicit, no hidden state
|
|
- Works with all transports (stdio, sse, http)
|
|
- Clear ownership in code
|
|
- Easy to debug and audit
|
|
|
|
**Cons**:
|
|
- Requires LibreChat plugin/modification
|
|
- More verbose tool calls
|
|
- LLM must know about multi-user architecture
|
|
|
|
**Implementation**: Requires LibreChat to inject group_id into tool parameters
|
|
|
|
### Alternative 2: LibreChat Proxy Layer
|
|
|
|
Create a thin proxy between LibreChat and Graphiti:
|
|
|
|
```
|
|
LibreChat → User-Aware Proxy → Graphiti MCP
|
|
(injects group_id)
|
|
```
|
|
|
|
**Pros**:
|
|
- Keeps Graphiti MCP clean and transport-agnostic
|
|
- Separation of concerns
|
|
- Easy to swap LibreChat for other clients
|
|
- No changes to Graphiti MCP server
|
|
|
|
**Cons**:
|
|
- Additional component to maintain
|
|
- Extra network hop (minimal overhead)
|
|
- More complex deployment
|
|
|
|
**Implementation**: Python/Node.js proxy that intercepts requests
|
|
|
|
### Alternative 3: Per-User MCP Instances
|
|
|
|
Run separate Graphiti MCP server instances per user/tenant:
|
|
|
|
```
|
|
LibreChat routes to:
|
|
- http://localhost:8000/mcp/ (User A)
|
|
- http://localhost:8001/mcp/ (User B)
|
|
- http://localhost:8002/mcp/ (User C)
|
|
```
|
|
|
|
**Pros**:
|
|
- Complete isolation (process boundaries)
|
|
- Simplest architecture
|
|
- No context management complexity
|
|
- Easy to scale horizontally
|
|
|
|
**Cons**:
|
|
- Resource intensive (N servers)
|
|
- Complex orchestration (start/stop/route)
|
|
- Overkill for most use cases
|
|
- Connection overhead
|
|
|
|
**Implementation**: Kubernetes/Docker Compose with dynamic routing
|
|
|
|
### Alternative 4: Database-Level Isolation
|
|
|
|
Use Neo4j multi-database feature (Enterprise only):
|
|
|
|
```python
|
|
# Each user gets their own database
|
|
graphiti_client_user_a = Graphiti(uri="bolt://neo4j:7687", database="user_a")
|
|
graphiti_client_user_b = Graphiti(uri="bolt://neo4j:7687", database="user_b")
|
|
```
|
|
|
|
**Pros**:
|
|
- Strong isolation at database level
|
|
- Better resource utilization than separate instances
|
|
- Leverages Neo4j native features
|
|
|
|
**Cons**:
|
|
- Requires Neo4j Enterprise
|
|
- Database management complexity
|
|
- Not supported with FalkorDB
|
|
|
|
**Implementation**: Dynamic database selection per request
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
### Phase 1: Investigation (Current)
|
|
|
|
- [x] Document LibreChat capabilities
|
|
- [x] Verify FastMCP middleware support
|
|
- [x] Identify critical issues and risks
|
|
- [x] Document implementation approaches
|
|
- [ ] **Test for stale context bug** ← NEXT STEP
|
|
- [ ] Create proof-of-concept implementation
|
|
- [ ] Test context propagation in queue service
|
|
|
|
### Phase 2: Proof of Concept
|
|
|
|
1. **Implement minimal middleware**
|
|
- Extract user_id from header
|
|
- Log to verify correct user per request
|
|
- Test with 2-3 concurrent users
|
|
|
|
2. **Test critical scenarios**
|
|
- Concurrent requests (detect stale context)
|
|
- Queue service background tasks
|
|
- Neo4j thread pool behavior
|
|
- Missing header handling
|
|
|
|
3. **Measure performance impact**
|
|
- Context overhead
|
|
- Logging overhead
|
|
- Additional parameter cost
|
|
|
|
### Phase 3: Full Implementation (If POC Successful)
|
|
|
|
1. **Implement full middleware**
|
|
- Validation, logging, metrics
|
|
- Security checks
|
|
- Error handling
|
|
|
|
2. **Modify all tools**
|
|
- Add context parameter
|
|
- Implement group_id priority
|
|
- Add comprehensive logging
|
|
|
|
3. **Update queue service**
|
|
- Explicit user_id passing
|
|
- Context preservation
|
|
|
|
4. **Add tests**
|
|
- Unit, integration, security, performance
|
|
- Automated CI/CD tests
|
|
|
|
5. **Update documentation**
|
|
- README, setup guide, troubleshooting
|
|
- Security model documentation
|
|
|
|
### Phase 4: Production Deployment
|
|
|
|
1. **Staged rollout**
|
|
- Deploy to test environment
|
|
- Limited user beta testing
|
|
- Monitor for issues
|
|
|
|
2. **Monitoring & metrics**
|
|
- Set up dashboards
|
|
- Configure alerts
|
|
- Track user isolation
|
|
|
|
3. **Security audit**
|
|
- Penetration testing
|
|
- Header injection testing
|
|
- Audit logging review
|
|
|
|
---
|
|
|
|
## Decision Log
|
|
|
|
| Date | Decision | Rationale | Status |
|
|
|------|----------|-----------|--------|
|
|
| 2025-11-08 | Document findings without implementation | Critical issues need investigation | ✅ Complete |
|
|
| TBD | Choose implementation approach | Pending POC testing | 🔄 Pending |
|
|
| TBD | Define group_id priority strategy | Pending security requirements | 🔄 Pending |
|
|
| TBD | Decide on REQUIRE_USER_ID default | Pending deployment model | 🔄 Pending |
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
### LibreChat Documentation
|
|
- [MCP Servers Configuration](https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/mcp_servers)
|
|
- User placeholders: `{{LIBRECHAT_USER_ID}}`
|
|
- Headers support for SSE and streamable-http
|
|
|
|
### FastMCP Documentation
|
|
- [Middleware Guide](https://gofastmcp.com/servers/middleware)
|
|
- [Context & Dependencies](https://gofastmcp.com/servers/context)
|
|
- `get_http_headers()` function
|
|
- Middleware added in v2.9.0
|
|
|
|
### Known Issues
|
|
- FastMCP #1233: Stale context in StreamableHTTP
|
|
- FastMCP #817: Access headers in middleware
|
|
- FastMCP #1291: HTTP request header access
|
|
|
|
### Related Files
|
|
- `/mcp_server/src/graphiti_mcp_server.py` - Main MCP server
|
|
- `/mcp_server/src/services/queue_service.py` - Background processing
|
|
- `/DOCS/Librechat.setup.md` - LibreChat setup guide
|
|
|
|
---
|
|
|
|
## Contact & Support
|
|
|
|
For questions about this analysis or implementation:
|
|
- Create GitHub issue in fork repository
|
|
- Reference this document in discussions
|
|
- Tag issues with `enhancement`, `multi-user`, `security`
|
|
|
|
---
|
|
|
|
**Document Version**: 1.0
|
|
**Last Updated**: 2025-11-08
|
|
**Next Review**: After POC testing
|