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:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user