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

28 KiB

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
  2. Proposed Solution
  3. Critical Analysis
  4. LibreChat Capabilities
  5. FastMCP Middleware Support
  6. Implementation Approaches
  7. Known Issues & Risks
  8. Requirements for Implementation
  9. Alternative Approaches
  10. 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

@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)

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

# 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

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:

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?

# LLM calls:
add_memory(name="...", episode_body="...", group_id="other_user")

Options:

  1. Explicit param wins (flexible but risky)

    effective_group_id = group_id or user_id or config_default
    
  2. Header always wins (strict isolation)

    effective_group_id = user_id or group_id or config_default
    
  3. Reject mismatch (paranoid)

    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:

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:

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:

@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:

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

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:

@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

Implementation:

Step 1: Add Middleware

# 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

# 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

@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:

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

  1. 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
  2. Security - Header Injection

    • Risk: Attacker spoofs user_id to access other graphs
    • Mitigation: Validate header format, trust LibreChat validation
    • Enhancement: Add header signature verification
  3. Debugging Complexity

    • Risk: Difficult to troubleshoot which group_id was used
    • Mitigation: Comprehensive structured logging
    • Metrics: Expose per-user metrics
  4. 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

  1. Transport Limitation

    • Impact: Only works with HTTP transports
    • Mitigation: Document requirement clearly
    • Detection: Add runtime transport detection
  2. FastMCP API Stability

    • Risk: Breaking changes in middleware API
    • Mitigation: Pin FastMCP version, monitor changelog
    • Version: Test with FastMCP 2.9.0+
  3. 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
  • 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:

@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):

# 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)

  • Document LibreChat capabilities
  • Verify FastMCP middleware support
  • Identify critical issues and risks
  • 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

FastMCP Documentation

Known Issues

  • FastMCP #1233: Stale context in StreamableHTTP
  • FastMCP #817: Access headers in middleware
  • FastMCP #1291: HTTP request header access
  • /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