graphiti/DOCS/Archived/Per-User-Graph-Isolation-Analysis.md
Lars Varming 341efd8c3d Fix: Critical database parameter bug + index creation error handling
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>
2025-11-10 11:37:16 +01:00

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