Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +102 to +104
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configuration has both MOS_NEO4J_SHARED_DB=true and NEO4J_USE_MULTI_DB=false, which according to the comment have "inverse logic". Setting MOS_NEO4J_SHARED_DB=true means users share one DB (no multi-db), and NEO4J_USE_MULTI_DB=false also means no multi-db. However, if someone changes one without changing the other, they could end up with conflicting settings. The example should either use only one of these variables or include a comment warning about keeping them synchronized.

Suggested change
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)
MOS_NEO4J_SHARED_DB=true # if true, all users share one DB (no multi-db); if false, each user gets their own DB (multi-db)
NEO4J_AUTO_CREATE=false # [IMPORTANT] set to false for Neo4j Community Edition
NEO4J_USE_MULTI_DB=false # inverse of MOS_NEO4J_SHARED_DB: when MOS_NEO4J_SHARED_DB=true, this MUST be false; when MOS_NEO4J_SHARED_DB=false, this MUST be true

Copilot uses AI. Check for mistakes.
QDRANT_HOST=localhost
QDRANT_PORT=6333
# For Qdrant Cloud / remote endpoint (takes priority if set):
Expand Down
106 changes: 88 additions & 18 deletions src/memos/api/mcp_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The environment variable mapping is missing the MOS-prefixed variant for SCHEDULER_TOP_N. While SCHEDULER_TOP_N is mapped, there's no MOS_SCHEDULER_TOP_N mapping for consistency with other scheduler parameters like MOS_SCHEDULER_TOP_K. This breaks the pattern of supporting both clean names and MOS-prefixed names mentioned in the comment at line 30.

Suggested change
"SCHEDULER_TOP_N": "scheduler_top_n",
"SCHEDULER_TOP_N": "scheduler_top_n",
"MOS_SCHEDULER_TOP_N": "scheduler_top_n",

Copilot uses AI. Check for mistakes.
}
Comment on lines +31 to +62
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The env_mapping dictionary contains duplicate entries that map to the same parameter. For example, both MODEL_NAME and MOS_CHAT_MODEL map to "model_name" (lines 42-43). If both environment variables are set, the later one (MOS_CHAT_MODEL) will silently overwrite the earlier one (MODEL_NAME), which may lead to unexpected behavior. The comment states this is for "compatibility", but the implementation doesn't properly handle precedence when both are present.

Copilot uses AI. Check for mistakes.

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("'")
Comment on lines +69 to +70
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quote stripping logic doesn't handle single character values properly. If an environment variable is set to a single quote character (val = "'") or a single double quote character (val = '"'), the conditions will pass but val[1:-1] will result in an empty string, which may not be the intended behavior. While this is an edge case, it could cause silent failures.

Suggested change
if (val.startswith('"') and val.endswith('"')) or (
val.startswith("'") and val.endswith("'")
if len(val) > 1 and (
(val.startswith('"') and val.endswith('"'))
or (val.startswith("'") and val.endswith("'"))

Copilot uses AI. Check for mistakes.
):
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
Comment on lines +74 to +85
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The numeric type conversion logic attempts to convert all non-boolean string values to numbers. This will incorrectly convert string values that happen to be numeric but should remain strings. For example, if someone sets NEO4J_DB_NAME="123" or NEO4J_PASSWORD="456", these will be converted to integers instead of remaining as strings.

The conversion should only be attempted for parameters where numeric values are expected (e.g., chunk_size, chunk_overlap, max_tokens, etc.), not for all parameters universally.

Copilot uses AI. Check for mistakes.

# 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

Expand All @@ -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):
Expand Down Expand Up @@ -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)
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When calling load_default_config with a user_id parameter in the register_cube function, the user_id might be None. The load_default_config function will then create a configuration with user_id=None, which gets passed to kwargs["user_id"] at line 64. This could lead to unexpected behavior if the downstream functions don't handle None user_id properly.

Copilot uses AI. Check for mistakes.
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}"
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return message for cube registration is inconsistent. When a new cube is created (path doesn't exist), it returns cube_id or cube (the object), but when registering an existing cube, it returns cube_id or cube_name_or_path (the string path). If cube_id is None and a new cube was created, this will return a string representation of the cube object instead of a meaningful identifier.

Copilot uses AI. Check for mistakes.
except Exception as e:
return f"Error registering cube: {e!s}"

Expand Down Expand Up @@ -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)
9 changes: 9 additions & 0 deletions src/memos/graph_dbs/neo4j.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 9 additions & 2 deletions src/memos/mem_os/utils/default_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment at line 189 instructs users to "Set 'auto_create' to False", but the default value for neo4j_auto_create at line 199 is True. This creates a contradiction between the documentation and the actual default behavior. If Community Edition users don't explicitly set NEO4J_AUTO_CREATE=false in their environment, they will still encounter the "CREATE DATABASE" permission errors that this PR aims to prevent.

Suggested change
"auto_create": kwargs.get("neo4j_auto_create", True),
"auto_create": kwargs.get("neo4j_auto_create", False),

Copilot uses AI. Check for mistakes.
"use_multi_db": kwargs.get("use_multi_db", False),
"embedding_dimension": kwargs.get("embedding_dimension", 3072),
}
Expand Down