Comprehensive security features and best practices for the Mem0 MCP Server.
The Mem0 MCP Server implements multiple layers of security:
- Token-Based Authentication - MCP-level authentication with PostgreSQL storage
- Memory Ownership Validation - User-scoped access control for all memory operations
- Project Isolation - Automatic memory segregation by project/user
- Audit Logging - Complete authentication and access tracking
Every memory operation validates ownership to ensure users can only access their own data:
User Request → Token Auth → User ID Validation → Ownership Check → Action
All memory operations require ownership validation:
| Operation | Endpoint | Protection |
|---|---|---|
| Read Memory | GET /memories/{id} |
✅ User ID required |
| Update Memory | PUT /memories/{id} |
✅ User ID required |
| Delete Memory | DELETE /memories/{id} |
✅ User ID required |
| View History | GET /memories/{id}/history |
✅ User ID required |
| Search | POST /search |
✅ User ID scoped |
| Get All | GET /memories |
✅ User ID scoped |
# User A creates a memory
curl -X POST http://localhost:8000/memories \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Secret data"}],"user_id":"user_a"}'
# Returns: {"results":[{"id":"abc123",...}]}
# User B tries to read User A's memory
curl http://localhost:8000/memories/abc123?user_id=user_b
# Returns: 403 Forbidden
# {"detail":"Access denied: Memory abc123 does not belong to user user_b"}All MCP tools automatically validate ownership:
# delete_memory tool
@mcp.tool()
async def delete_memory(memory_id: str) -> str:
# Authentication validated from headers
auth_result = await validate_auth()
# Ownership validated via user_id parameter
uid = config.CURRENT_PROJECT_ID
response = await http_client.delete(
f"/memories/{memory_id}",
params={"user_id": uid} # ← Ownership check
)Token Format:
- Prefix:
mcp_ - Length: 43 characters (256-bit security)
- Generation:
secrets.token_urlsafe(32)
Token Lifecycle:
Create → Active → [Optional: Revoke] → [Optional: Re-enable] → Delete
Security Features:
- ✅ Cryptographically secure random generation
- ✅ Expiration dates (default: 365 days)
- ✅ Revocation support
- ✅ Last-used tracking
- ✅ Audit logging
❌ Never:
# Don't commit tokens to git
git add .env # BAD!
# Don't hardcode in code
TOKEN = "mcp_abc123..." # BAD!✅ Always:
# Use environment variables
export MEM0_TOKEN='mcp_...'
# Add to .gitignore
echo ".env" >> .gitignore
# Use password manager for backup
1password add "Mem0 Token" --vault=WorkRotate tokens regularly:
# Create new token
NEW_TOKEN=$(python3 scripts/mcp-token.py create \
--user-id [email protected] \
--name "Your Name" \
--email [email protected] | grep "Token:" | cut -d' ' -f2)
# Update environment
export MEM0_TOKEN="$NEW_TOKEN"
# Update Claude Code
claude mcp remove mem0
claude mcp add mem0 http://localhost:8080/mcp/ -t http \
-H "X-MCP-Token: ${MEM0_TOKEN}" \
-H "X-MCP-UserID: ${MEM0_USER_ID}"
# Verify working
claude mcp list
# Revoke old token
python3 scripts/mcp-token.py revoke $OLD_TOKENEach team member should have their own token:
# Team Lead creates tokens
for user in [email protected] [email protected] [email protected]; do
python3 scripts/mcp-token.py create \
--user-id "$user" \
--name "$(echo $user | cut -d'@' -f1)" \
--email "$user"
done
# Each user configures their environment
# Alice's ~/.zshrc
export MEM0_TOKEN='mcp_alice_...'
export MEM0_USER_ID='[email protected]'
# Bob's ~/.zshrc
export MEM0_TOKEN='mcp_bob_...'
export MEM0_USER_ID='[email protected]'1. Auto Mode (Recommended for Multi-Project)
PROJECT_ID_MODE=autoEach directory gets unique project ID:
/projects/app1→project_app1_abc123/projects/app2→project_app2_def456
Memories are isolated per project + user.
2. Manual Mode (Recommended for Single Project)
PROJECT_ID_MODE=manual
DEFAULT_USER_ID=my_project_nameExplicit project ID for all memories.
3. Global Mode (Shared Memories)
PROJECT_ID_MODE=global
DEFAULT_USER_ID=shared_knowledge_baseAll users share the same memory pool.
| Mode | Isolation | Use Case |
|---|---|---|
| auto | High - Per project+user | Multiple projects, multiple users |
| manual | Medium - Per user | Single project, multiple users |
| global | Low - Shared | Team knowledge base |
Every authentication attempt is logged:
SELECT * FROM mcp_auth_audit_log ORDER BY timestamp DESC LIMIT 5;Logged Information:
- Timestamp
- User ID
- Event type (success/auth_failed)
- Token used
- Error message (if failed)
- IP address (from request)
View recent activity:
python3 scripts/mcp-token.py audit --days 7Check user statistics:
python3 scripts/mcp-token.py stats [email protected]Alert on suspicious activity:
# Check for failed attempts
docker compose exec postgres psql -U postgres -d postgres -c \
"SELECT user_id, COUNT(*) as failed_attempts
FROM mcp_auth_audit_log
WHERE event_type='auth_failed'
AND timestamp > NOW() - INTERVAL '1 hour'
GROUP BY user_id
HAVING COUNT(*) > 5;"Docker Compose Configuration:
services:
mcp:
ports:
- "127.0.0.1:8080:8080" # Only localhost
mem0:
ports:
- "127.0.0.1:8000:8000" # Only localhost
postgres:
# No external ports
networks:
- mem0_networkReverse Proxy (nginx):
server {
listen 443 ssl http2;
server_name mem0.company.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
location /mcp/ {
proxy_pass http://127.0.0.1:8080/mcp/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Rate limiting
limit_req zone=mcp_limit burst=10 nodelay;
}
}Strong Passwords:
# Generate secure passwords
POSTGRES_PASSWORD=$(openssl rand -base64 32)
NEO4J_PASSWORD=$(openssl rand -base64 32)Restrict Access:
services:
postgres:
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
networks:
- mem0_network # Internal onlyRegular Backups:
# Backup script
#!/bin/bash
docker compose exec postgres pg_dump -U postgres postgres | \
gzip > "backup-$(date +%Y%m%d).sql.gz"Expiration Policy:
# Short-lived tokens for sensitive environments
python3 scripts/mcp-token.py create \
--user-id [email protected] \
--expires-days 30 # 30 days instead of 365Automated Rotation:
#!/bin/bash
# Monthly token rotation
0 0 1 * * /usr/local/bin/rotate-tokens.shHealth Checks:
# Add to monitoring (Prometheus/Grafana)
curl -f http://localhost:8080/ || alert "MCP server down"
curl -f http://localhost:8000/health || alert "Mem0 server down"Failed Auth Alerts:
# Alert on 10+ failed attempts in 1 hour
*/15 * * * * /usr/local/bin/check-failed-auth.sh# Ownership validation
./tests/test_ownership_simple.sh
# Authentication
./tests/test_auth.sh
# Full test suite
./scripts/test.sh1. Test Ownership Isolation:
# Create memory as User A
MEMORY_ID=$(curl -s -X POST http://localhost:8000/memories \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Secret"}],"user_id":"user_a"}' | \
jq -r '.results[0].id')
# Try to access as User B (should fail)
curl -s "http://localhost:8000/memories/$MEMORY_ID?user_id=user_b" | \
jq '.detail'
# Expected: "Access denied: Memory {id} does not belong to user user_b"2. Test Authentication:
# Invalid token (should fail)
curl -s http://localhost:8080/mcp \
-H "X-MCP-Token: invalid" \
-H "X-MCP-UserID: test" | \
jq '.error'
# No token (should fail)
curl -s http://localhost:8080/mcp | jq '.error'3. Test Token Revocation:
# Create and revoke token
TOKEN=$(python3 scripts/mcp-token.py create --user-id [email protected] --name Test --email [email protected] | grep Token | cut -d' ' -f2)
python3 scripts/mcp-token.py revoke $TOKEN
# Try to use revoked token (should fail)
curl -s http://localhost:8080/mcp \
-H "X-MCP-Token: $TOKEN" \
-H "X-MCP-UserID: [email protected]" | \
jq '.error'1. Immediate Actions:
# Revoke all tokens
python3 scripts/mcp-token.py list | grep "mcp_" | \
while read token rest; do
python3 scripts/mcp-token.py revoke "$token"
done
# Check audit log
python3 scripts/mcp-token.py audit --days 30 > security-audit.log
# Review suspicious activity
grep "auth_failed" security-audit.log2. Investigation:
# Check database for unauthorized access
docker compose exec postgres psql -U postgres -d postgres <<EOF
SELECT user_id, COUNT(*)
FROM mcp_auth_audit_log
WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY user_id;
EOF
# Review memory access patterns
docker compose logs mcp --since 24h | grep "Access denied"3. Recovery:
# Create new tokens for legitimate users
for user in $(cat authorized_users.txt); do
python3 scripts/mcp-token.py create --user-id "$user" --name "$user"
done
# Notify users to update tokens
./scripts/notify-users.sh "Please update your MEM0_TOKEN"All data stays local:
- ✅ Vector embeddings: PostgreSQL (local)
- ✅ Graph relationships: Neo4j (local)
- ✅ Authentication tokens: PostgreSQL (local)
- ✅ LLM inference: Ollama (local) or Cloud (configurable)
Right to be Forgotten:
# Delete all user data
python3 scripts/delete-user-data.py --user-id [email protected]Data Export:
# Export user's memories
curl "http://localhost:8000/[email protected]" > user_data.jsonAudit Trail:
# Export audit log for user
python3 scripts/mcp-token.py audit --user-id [email protected] > audit.log- Change default PostgreSQL password
- Change default Neo4j password
- Enable token authentication
- Create individual tokens for each user
- Configure firewall rules
- Enable HTTPS (production)
- Rotate tokens every 90 days
- Review audit logs weekly
- Update dependencies monthly
- Test backups quarterly
- Security audit annually
- Run security tests
- Configure rate limiting
- Setup monitoring/alerts
- Document incident response plan
- Train team on security practices
- AUTHENTICATION.md - Detailed authentication guide
- TROUBLESHOOTING.md - Security-related troubleshooting
- CLAUDE.md - Development security notes
- MCP Specification - MCP security standards
If you discover a security vulnerability:
- Do NOT open a public GitHub issue
- Email: [email protected] (or create private security advisory)
- Include: Description, impact, reproduction steps
- We'll respond within 48 hours
Last Updated: 2025-10-10 Version: 1.1.0