diff --git a/docker/.env.example b/docker/.env.example index ca3abde94..dc4252133 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -99,7 +99,9 @@ NEO4J_URI=bolt://localhost:7687 # required when backend=neo4j* NEO4J_USER=neo4j # required when backend=neo4j* NEO4J_PASSWORD=12345678 # required when backend=neo4j* NEO4J_DB_NAME=neo4j # required for shared-db mode -MOS_NEO4J_SHARED_DB=false +MOS_NEO4J_SHARED_DB=true # if true, all users share one DB; if false, each user gets their own DB +NEO4J_AUTO_CREATE=false # [IMPORTANT] set to false for Neo4j Community Edition +NEO4J_USE_MULTI_DB=false # alternative to MOS_NEO4J_SHARED_DB (logic is inverse) QDRANT_HOST=localhost QDRANT_PORT=6333 # For Qdrant Cloud / remote endpoint (takes priority if set): diff --git a/src/memos/api/mcp_serve.py b/src/memos/api/mcp_serve.py index 9eb1e59d0..838c2a76a 100644 --- a/src/memos/api/mcp_serve.py +++ b/src/memos/api/mcp_serve.py @@ -16,14 +16,88 @@ def load_default_config(user_id="default_user"): + """ + Load MOS configuration from environment variables. + + IMPORTANT for Neo4j Community Edition: + Community Edition does not support administrative commands like 'CREATE DATABASE'. + To avoid errors, ensure the following environment variables are set correctly: + - NEO4J_DB_NAME=neo4j (Must use the default database) + - NEO4J_AUTO_CREATE=false (Disable automatic database creation) + - NEO4J_USE_MULTI_DB=false (Disable multi-tenant database mode) + """ + # Define mapping between environment variables and configuration parameters + # We support both clean names and MOS_ prefixed names for compatibility + env_mapping = { + "OPENAI_API_KEY": "openai_api_key", + "OPENAI_API_BASE": "openai_api_base", + "MOS_TEXT_MEM_TYPE": "text_mem_type", + "NEO4J_URI": "neo4j_uri", + "NEO4J_USER": "neo4j_user", + "NEO4J_PASSWORD": "neo4j_password", + "NEO4J_DB_NAME": "neo4j_db_name", + "NEO4J_AUTO_CREATE": "neo4j_auto_create", + "NEO4J_USE_MULTI_DB": "use_multi_db", + "MOS_NEO4J_SHARED_DB": "mos_shared_db", # Special handle later + "MODEL_NAME": "model_name", + "MOS_CHAT_MODEL": "model_name", + "EMBEDDER_MODEL": "embedder_model", + "MOS_EMBEDDER_MODEL": "embedder_model", + "CHUNK_SIZE": "chunk_size", + "CHUNK_OVERLAP": "chunk_overlap", + "ENABLE_MEM_SCHEDULER": "enable_mem_scheduler", + "MOS_ENABLE_SCHEDULER": "enable_mem_scheduler", + "ENABLE_ACTIVATION_MEMORY": "enable_activation_memory", + "TEMPERATURE": "temperature", + "MOS_CHAT_TEMPERATURE": "temperature", + "MAX_TOKENS": "max_tokens", + "MOS_MAX_TOKENS": "max_tokens", + "TOP_P": "top_p", + "MOS_TOP_P": "top_p", + "TOP_K": "top_k", + "MOS_TOP_K": "top_k", + "SCHEDULER_TOP_K": "scheduler_top_k", + "MOS_SCHEDULER_TOP_K": "scheduler_top_k", + "SCHEDULER_TOP_N": "scheduler_top_n", + } + + kwargs = {"user_id": user_id} + for env_key, param_key in env_mapping.items(): + val = os.getenv(env_key) + if val is not None: + # Strip quotes if they exist (sometimes happens with .env) + if (val.startswith('"') and val.endswith('"')) or ( + val.startswith("'") and val.endswith("'") + ): + val = val[1:-1] + + # Handle boolean conversions + if val.lower() in ("true", "false"): + kwargs[param_key] = val.lower() == "true" + else: + # Try numeric conversions (int first, then float) + try: + if "." in val: + kwargs[param_key] = float(val) + else: + kwargs[param_key] = int(val) + except ValueError: + kwargs[param_key] = val + + # Logic handle for MOS_NEO4J_SHARED_DB vs use_multi_db + if "mos_shared_db" in kwargs: + kwargs["use_multi_db"] = not kwargs.pop("mos_shared_db") + + # Extract mandatory or special params + openai_api_key = kwargs.pop("openai_api_key", os.getenv("OPENAI_API_KEY")) + openai_api_base = kwargs.pop("openai_api_base", "https://api.openai.com/v1") + text_mem_type = kwargs.pop("text_mem_type", "tree_text") + config, cube = get_default( - openai_api_key=os.getenv("OPENAI_API_KEY"), - openai_api_base=os.getenv("OPENAI_API_BASE"), - text_mem_type=os.getenv("MOS_TEXT_MEM_TYPE"), - user_id=user_id, - neo4j_uri=os.getenv("NEO4J_URI"), - neo4j_user=os.getenv("NEO4J_USER"), - neo4j_password=os.getenv("NEO4J_PASSWORD"), + openai_api_key=openai_api_key, + openai_api_base=openai_api_base, + text_mem_type=text_mem_type, + **kwargs, ) return config, cube @@ -33,6 +107,7 @@ def __init__(self): self.mcp = FastMCP("MOS Memory System") config, cube = load_default_config() self.mos_core = MOS(config=config) + self.mos_core.register_mem_cube(cube) self._setup_tools() def _setup_tools(self): @@ -132,11 +207,14 @@ async def register_cube( """ try: if not os.path.exists(cube_name_or_path): - mos_config, cube_name_or_path = load_default_config(user_id=user_id) + _, cube = load_default_config(user_id=user_id) + cube_to_register = cube + else: + cube_to_register = cube_name_or_path self.mos_core.register_mem_cube( - cube_name_or_path, mem_cube_id=cube_id, user_id=user_id + cube_to_register, mem_cube_id=cube_id, user_id=user_id ) - return f"Cube registered successfully: {cube_id or cube_name_or_path}" + return f"Cube registered successfully: {cube_id or cube_to_register}" except Exception as e: return f"Error registering cube: {e!s}" @@ -489,14 +567,6 @@ def run(self, transport: str = "stdio", **kwargs): args = parser.parse_args() - # Set environment variables - os.environ["OPENAI_API_BASE"] = os.getenv("OPENAI_API_BASE") - os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") - os.environ["MOS_TEXT_MEM_TYPE"] = "tree_text" # "tree_text" need set neo4j - os.environ["NEO4J_URI"] = os.getenv("NEO4J_URI") - os.environ["NEO4J_USER"] = os.getenv("NEO4J_USER") - os.environ["NEO4J_PASSWORD"] = os.getenv("NEO4J_PASSWORD") - # Create and run MCP server server = MOSMCPStdioServer() server.run(transport=args.transport, host=args.host, port=args.port) diff --git a/src/memos/graph_dbs/neo4j.py b/src/memos/graph_dbs/neo4j.py index a0a4c6a50..debbb4e3c 100644 --- a/src/memos/graph_dbs/neo4j.py +++ b/src/memos/graph_dbs/neo4j.py @@ -1347,6 +1347,15 @@ def _ensure_database_exists(self): with self.driver.session(database="system") as session: session.run(f"CREATE DATABASE `{self.db_name}` IF NOT EXISTS") except ClientError as e: + if "Unsupported administration command" in str( + e + ) or "Unsupported administration" in str(e): + logger.warning( + f"Could not create database '{self.db_name}' because this Neo4j instance " + "(likely Community Edition) does not support administrative commands. " + "Please ensure the database exists manually or use the default 'neo4j' database." + ) + return if "ExistingDatabaseFound" in str(e): pass # Ignore, database already exists else: diff --git a/src/memos/mem_os/utils/default_config.py b/src/memos/mem_os/utils/default_config.py index bf9f847d0..edb7875d4 100644 --- a/src/memos/mem_os/utils/default_config.py +++ b/src/memos/mem_os/utils/default_config.py @@ -181,15 +181,22 @@ def get_default_cube_config( # Configure text memory based on type if text_mem_type == "tree_text": # Tree text memory requires Neo4j configuration + # NOTE: Neo4j Community Edition does NOT support multiple databases. + # It only has one default database named 'neo4j'. + # If you are using Community Edition: + # 1. Set 'use_multi_db' to False (default) + # 2. Set 'db_name' to 'neo4j' (default) + # 3. Set 'auto_create' to False to avoid 'CREATE DATABASE' permission errors. db_name = f"memos{user_id.replace('-', '').replace('_', '')}" if not kwargs.get("use_multi_db", False): - db_name = kwargs.get("neo4j_db_name", "defaultdb") + db_name = kwargs.get("neo4j_db_name", "neo4j") + neo4j_config = { "uri": kwargs.get("neo4j_uri", "bolt://localhost:7687"), "user": kwargs.get("neo4j_user", "neo4j"), "db_name": db_name, "password": kwargs.get("neo4j_password", "12345678"), - "auto_create": True, + "auto_create": kwargs.get("neo4j_auto_create", True), "use_multi_db": kwargs.get("use_multi_db", False), "embedding_dimension": kwargs.get("embedding_dimension", 3072), }