Add MCP HTTP/SSE server and complete n8n integration

Major Changes:
- Implemented MCP HTTP/SSE transport server for n8n and web clients
- Created mcp_server/http_server.py with FastAPI for JSON-RPC 2.0 over HTTP
- Added health check endpoint (/health) for container monitoring
- Refactored mcp-server/ to mcp_server/ (Python module structure)
- Updated Dockerfile.mcp to run HTTP server with health checks

MCP Server Features:
- 7 memory tools exposed via MCP (add, search, get, update, delete)
- HTTP/SSE transport on port 8765 for n8n integration
- stdio transport for Claude Code integration
- JSON-RPC 2.0 protocol implementation
- CORS support for web clients

n8n Integration:
- Successfully tested with AI Agent workflows
- MCP Client Tool configuration documented
- Working webhook endpoint tested and verified
- System prompt optimized for automatic user_id usage

Documentation:
- Created comprehensive Mintlify documentation site
- Added docs/mcp/introduction.mdx - MCP server overview
- Added docs/mcp/installation.mdx - Installation guide
- Added docs/mcp/tools.mdx - Complete tool reference
- Added docs/examples/n8n.mdx - n8n integration guide
- Added docs/examples/claude-code.mdx - Claude Code setup
- Updated README.md with MCP HTTP server info
- Updated roadmap to mark Phase 1 as complete

Bug Fixes:
- Fixed synchronized delete operations across Supabase and Neo4j
- Updated memory_service.py with proper error handling
- Fixed Neo4j connection issues in delete operations

Configuration:
- Added MCP_HOST and MCP_PORT environment variables
- Updated .env.example with MCP server configuration
- Updated docker-compose.yml with MCP container health checks

Testing:
- Added test scripts for MCP HTTP endpoint verification
- Created test workflows in n8n
- Verified all 7 memory tools working correctly
- Tested synchronized operations across both stores

Version: 1.0.0
Status: Phase 1 Complete - Production Ready

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Code
2025-10-15 13:56:41 +02:00
parent 9bca2f4f47
commit 1998bef6f4
36 changed files with 3443 additions and 71 deletions

View File

@@ -6,6 +6,7 @@ import logging
from typing import List, Dict, Any, Optional
from mem0 import Memory
from config import mem0_config
from memory_cleanup import MemoryCleanup
logger = logging.getLogger(__name__)
@@ -15,6 +16,7 @@ class MemoryService:
_instance: Optional['MemoryService'] = None
_memory: Optional[Memory] = None
_cleanup: Optional[MemoryCleanup] = None
def __new__(cls):
if cls._instance is None:
@@ -27,6 +29,7 @@ class MemoryService:
logger.info("Initializing Mem0 with configuration")
try:
self._memory = Memory.from_config(config_dict=mem0_config)
self._cleanup = MemoryCleanup(self._memory)
logger.info("Mem0 initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize Mem0: {e}")
@@ -108,7 +111,7 @@ class MemoryService:
try:
logger.info(f"Searching memories: query='{query}', user_id={user_id}, limit={limit}")
results = self.memory.search(
result = self.memory.search(
query=query,
user_id=user_id,
agent_id=agent_id,
@@ -116,8 +119,33 @@ class MemoryService:
limit=limit
)
logger.info(f"Found {len(results)} matching memories")
return results
# In mem0 v0.1.118+, search returns dict with 'results' key
memories_list = result.get('results', []) if isinstance(result, dict) else result
# Handle both string and dict responses from mem0
formatted_results = []
for item in memories_list:
if isinstance(item, str):
# Convert string memory to dict format
formatted_results.append({
'id': '',
'memory': item,
'user_id': user_id,
'agent_id': agent_id,
'run_id': run_id,
'metadata': {},
'created_at': None,
'updated_at': None,
'score': None
})
elif isinstance(item, dict):
# Already in dict format
formatted_results.append(item)
else:
logger.warning(f"Unexpected memory format: {type(item)}")
logger.info(f"Found {len(formatted_results)} matching memories")
return formatted_results
except Exception as e:
logger.error(f"Failed to search memories: {e}")
@@ -178,14 +206,39 @@ class MemoryService:
try:
logger.info(f"Getting all memories: user_id={user_id}, agent_id={agent_id}")
results = self.memory.get_all(
result = self.memory.get_all(
user_id=user_id,
agent_id=agent_id,
run_id=run_id
)
logger.info(f"Retrieved {len(results)} memories")
return results
# In mem0 v0.1.118+, get_all returns dict with 'results' key
memories_list = result.get('results', []) if isinstance(result, dict) else result
# Handle both string and dict responses from mem0
formatted_results = []
for item in memories_list:
if isinstance(item, str):
# Convert string memory to dict format
formatted_results.append({
'id': '',
'memory': item,
'user_id': user_id,
'agent_id': agent_id,
'run_id': run_id,
'metadata': {},
'created_at': None,
'updated_at': None,
'score': None
})
elif isinstance(item, dict):
# Already in dict format
formatted_results.append(item)
else:
logger.warning(f"Unexpected memory format: {type(item)}")
logger.info(f"Retrieved {len(formatted_results)} memories")
return formatted_results
except Exception as e:
logger.error(f"Failed to get all memories: {e}")
@@ -261,6 +314,9 @@ class MemoryService:
"""
Delete all memories for a user/agent/run
IMPORTANT: This uses synchronized deletion to ensure both
Supabase (vector store) and Neo4j (graph store) are cleaned up.
Args:
user_id: User identifier filter
agent_id: Agent identifier filter
@@ -273,15 +329,16 @@ class MemoryService:
Exception: If deletion fails
"""
try:
logger.info(f"Deleting all memories: user_id={user_id}, agent_id={agent_id}")
logger.info(f"Deleting all memories (synchronized): user_id={user_id}, agent_id={agent_id}")
self.memory.delete_all(
# Use synchronized deletion to clean up both Supabase and Neo4j
result = self._cleanup.delete_all_synchronized(
user_id=user_id,
agent_id=agent_id,
run_id=run_id
)
logger.info("Successfully deleted all matching memories")
logger.info(f"Successfully deleted all matching memories: {result}")
return True
except Exception as e:

View File

@@ -194,10 +194,11 @@ async def get_user_memories(
"""Get all memories for a user"""
try:
memories = await service.get_all_memories(user_id=user_id)
logger.info(f"Received {len(memories)} memories from service, types: {[type(m) for m in memories]}")
return [format_memory_response(mem) for mem in memories]
except Exception as e:
logger.error(f"Error getting user memories: {e}")
logger.error(f"Error getting user memories: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)