Skip to content
Open
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
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
.env
.venv
env/
venv/
.juno_task/
59 changes: 58 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,39 @@ Or with pip:
pip install mem0-mcp-server
```

### Provisional Deployment (from GitHub)

You can deploy directly from GitHub without installing:

```json
{
"mcpServers": {
"mem0": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/alfonsodg/[email protected]",
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The documentation references GitHub repository "alfonsodg/mem0-mcp.git" and tag "v0.2.3", which appears to be a personal fork. If this PR is intended to be merged into the upstream repository, these references should point to the official repository instead. This is inconsistent with a PR being submitted for the main project.

Suggested change
"git+https://github.com/alfonsodg/[email protected]",
"git+https://github.com/mem0ai/[email protected]",

Copilot uses AI. Check for mistakes.
"mem0-mcp-server",
"--api-key=YOUR_MEM0_API_KEY",
"--user-id=your-user-id"
],
"tools": ["*"]
}
}
}
```

Replace `YOUR_MEM0_API_KEY` with your actual Mem0 API key from https://app.mem0.ai

**Why CLI arguments instead of env vars?**
GitHub Copilot CLI (and some other MCP clients) have issues with environment variables in MCP server configurations. Using CLI arguments (`--api-key`, `--user-id`) ensures compatibility across all MCP clients.

### Client Configuration

Add this configuration to your MCP client:

**Option 1: Using environment variables (recommended)**

```json
{
"mcpServers": {
Expand All @@ -63,6 +92,23 @@ Add this configuration to your MCP client:
}
```

**Option 2: Using command-line arguments (for CLIs that don't support env)**

```json
{
"mcpServers": {
"mem0": {
"command": "uvx",
"args": [
"mem0-mcp-server",
"--api-key=m0-...",
"--user-id=your-handle"
]
}
}
}
```

### Test with the Python Agent

<details>
Expand Down Expand Up @@ -117,11 +163,19 @@ The Mem0 MCP server enables powerful memory capabilities for your AI application

### Environment Variables

- `MEM0_API_KEY` (required) – Mem0 platform API key.
- `MEM0_API_KEY` (required if not using `--api-key`) – Mem0 platform API key.
- `MEM0_DEFAULT_USER_ID` (optional) – default `user_id` injected into filters and write requests (defaults to `mem0-mcp`).
- `MEM0_ENABLE_GRAPH_DEFAULT` (optional) – Enable graph memories by default (defaults to `false`).
- `MEM0_DISABLE_DNS_REBINDING_PROTECTION` (optional) – Disable DNS rebinding protection for local development (defaults to `false`, protection enabled).
- `MEM0_MCP_AGENT_MODEL` (optional) – default LLM for the bundled agent example (defaults to `openai:gpt-4o-mini`).

### Command-Line Arguments

- `--api-key` – Mem0 API key (overrides `MEM0_API_KEY` env var)
- `--user-id` – Default user ID (overrides `MEM0_DEFAULT_USER_ID` env var)

CLI arguments take precedence over environment variables.

## Advanced Setup

<details>
Expand Down Expand Up @@ -206,6 +260,9 @@ mem0-mcp-server
# Or with uv
uv sync
uv run mem0-mcp-server

# Run tests
pytest
```

</details>
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ requires = ["hatchling>=1.27.0"]
build-backend = "hatchling.build"

[project]
name = "mem0-mcp-server"
version = "0.2.1"
name = "mem0-mcp-server-alfonsodg"
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The package name has been changed from "mem0-mcp-server" to "mem0-mcp-server-alfonsodg". This creates a fork with a different package name, which could cause confusion for users and may not be the intended behavior for a feature PR. If this is meant to be a personal fork, it should be clearly communicated. If this is meant to be merged upstream, the package name should remain unchanged.

Suggested change
name = "mem0-mcp-server-alfonsodg"
name = "mem0-mcp-server"

Copilot uses AI. Check for mistakes.
version = "0.2.3"
description = "Model Context Protocol server that exposes the Mem0 long-term memory API as tools"
readme = "README.md"
license = {text = "Apache-2.0"}
Expand Down
6 changes: 6 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
127 changes: 76 additions & 51 deletions src/mem0_mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

from __future__ import annotations

import argparse
import json
import logging
import os
import time
from functools import lru_cache
from typing import Annotated, Any, Callable, Dict, Optional, TypeVar

from dotenv import load_dotenv
Expand All @@ -14,26 +17,15 @@
from mem0.exceptions import MemoryError
from pydantic import Field

try: # Support both package (`python -m mem0_mcp.server`) and script (`python mem0_mcp/server.py`) runs.
from .schemas import (
AddMemoryArgs,
ConfigSchema,
DeleteAllArgs,
DeleteEntitiesArgs,
GetMemoriesArgs,
SearchMemoriesArgs,
ToolMessage,
)
except ImportError: # pragma: no cover - fallback for script execution
from schemas import (
AddMemoryArgs,
ConfigSchema,
DeleteAllArgs,
DeleteEntitiesArgs,
GetMemoriesArgs,
SearchMemoriesArgs,
ToolMessage,
)
from .schemas import (
AddMemoryArgs,
ConfigSchema,
DeleteAllArgs,
DeleteEntitiesArgs,
GetMemoriesArgs,
SearchMemoriesArgs,
ToolMessage,
)

load_dotenv()

Expand All @@ -60,6 +52,10 @@ def decorator(func: Callable[..., T]) -> Callable[..., T]: # type: ignore[type-
smithery = _SmitheryFallback() # type: ignore[assignment]


# CLI arguments override environment variables
_CLI_API_KEY: Optional[str] = None
_CLI_DEFAULT_USER_ID: Optional[str] = None
Comment on lines +55 to +57
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Using global mutable variables (_CLI_API_KEY and _CLI_DEFAULT_USER_ID) for configuration can lead to issues in multi-threaded or concurrent environments, or when running multiple instances. While the MCP server typically runs as a single process, this pattern is not thread-safe. Consider using a more robust configuration management approach, such as a configuration object that's passed through the call chain or storing these in the FastMCP server instance's state.

Copilot uses AI. Check for mistakes.

# graph remains off by default , also set the default user_id to "mem0-mcp" when nothing set
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The comment contains a spacing issue: "graph remains off by default , also" has a space before the comma, which should be removed for proper grammar.

Suggested change
# graph remains off by default , also set the default user_id to "mem0-mcp" when nothing set
# graph remains off by default, also set the default user_id to "mem0-mcp" when nothing set

Copilot uses AI. Check for mistakes.
ENV_API_KEY = os.getenv("MEM0_API_KEY")
ENV_DEFAULT_USER_ID = os.getenv("MEM0_DEFAULT_USER_ID", "mem0-mcp")
Expand All @@ -68,8 +64,19 @@ def decorator(func: Callable[..., T]) -> Callable[..., T]: # type: ignore[type-
"true",
"yes",
}
ENV_DISABLE_DNS_REBINDING_PROTECTION = os.getenv("MEM0_DISABLE_DNS_REBINDING_PROTECTION", "false").lower() in {
"1",
"true",
"yes",
}

_CLIENT_CACHE: Dict[str, MemoryClient] = {}

@lru_cache(maxsize=100)
def _mem0_client(api_key: str) -> MemoryClient:
"""Create and cache MemoryClient instances with LRU eviction."""
if not api_key.startswith("m0-") or len(api_key) < 10:
raise ValueError("Invalid MEM0_API_KEY format. Expected format: m0-...")
Comment on lines +77 to +78
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The API key validation only checks for the "m0-" prefix and a minimum length of 10 characters. This is a weak validation that could accept malformed keys like "m0-1234567" (which meets the requirements but may not be a valid format). Consider implementing more robust validation, such as checking for the expected total length or format of actual Mem0 API keys, or documenting the rationale for this minimal validation approach.

Copilot uses AI. Check for mistakes.
return MemoryClient(api_key=api_key)
Comment on lines +75 to +79
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The API key validation is performed inside the cached function, which means that invalid API keys will be cached by the LRU cache decorator. Once an invalid key is validated and raises an exception, subsequent calls with the same invalid key will retrieve the cached exception behavior. However, since lru_cache caches based on function arguments and the function raises an exception (doesn't return a value), this actually won't cache the exception itself - it will re-raise it each time. But this design is inefficient: validation should occur before attempting to cache. Consider validating the API key before calling the cached client creation function.

Suggested change
def _mem0_client(api_key: str) -> MemoryClient:
"""Create and cache MemoryClient instances with LRU eviction."""
if not api_key.startswith("m0-") or len(api_key) < 10:
raise ValueError("Invalid MEM0_API_KEY format. Expected format: m0-...")
return MemoryClient(api_key=api_key)
def _cached_mem0_client(api_key: str) -> MemoryClient:
"""Create and cache MemoryClient instances with LRU eviction."""
return MemoryClient(api_key=api_key)
def _mem0_client(api_key: str) -> MemoryClient:
"""Validate the API key, then obtain a cached MemoryClient instance."""
if not api_key.startswith("m0-") or len(api_key) < 10:
raise ValueError("Invalid MEM0_API_KEY format. Expected format: m0-...")
return _cached_mem0_client(api_key)

Copilot uses AI. Check for mistakes.


def _config_value(source: Any, field: str):
Expand Down Expand Up @@ -98,47 +105,51 @@ def _with_default_filters(


def _mem0_call(func, *args, **kwargs):
try:
result = func(*args, **kwargs)
except MemoryError as exc: # surface structured error back to MCP client
logger.error("Mem0 call failed: %s", exc)
# returns the erorr to the model
return json.dumps(
{
"error": str(exc),
"status": getattr(exc, "status", None),
"payload": getattr(exc, "payload", None),
},
ensure_ascii=False,
)
return json.dumps(result, ensure_ascii=False)
max_retries = 3
base_delay = 1.0

for attempt in range(max_retries):
try:
result = func(*args, **kwargs)
return json.dumps(result, ensure_ascii=False)
except MemoryError as exc:
status = getattr(exc, "status", None)

# Retry on 5xx errors or network issues
if status and 500 <= status < 600 and attempt < max_retries - 1:
delay = base_delay * (2 ** attempt)
logger.warning(f"Mem0 call failed (attempt {attempt + 1}/{max_retries}), retrying in {delay}s: {exc}")
time.sleep(delay)
continue
Comment on lines +118 to +123
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The retry logic only retries on MemoryError exceptions with 5xx status codes. However, network-related errors (connection timeouts, DNS failures, etc.) that don't result in a MemoryError with a status code will not be retried. The comment on line 118 mentions "or network issues" but the code doesn't actually handle generic network exceptions. Consider catching additional exception types like requests.exceptions.RequestException or adding a broader exception handler for true network resilience.

Copilot uses AI. Check for mistakes.

# Final failure or non-retryable error
logger.error("Mem0 call failed: %s", exc)
return json.dumps(
{
"error": str(exc),
"status": status,
"payload": getattr(exc, "payload", None),
},
ensure_ascii=False,
)


def _resolve_settings(ctx: Context | None) -> tuple[str, str, bool]:
session_config = getattr(ctx, "session_config", None)
api_key = _config_value(session_config, "mem0_api_key") or ENV_API_KEY
api_key = _CLI_API_KEY or _config_value(session_config, "mem0_api_key") or ENV_API_KEY
if not api_key:
raise RuntimeError(
"MEM0_API_KEY is required (via Smithery config, session config, or environment) to run the Mem0 MCP server."
"MEM0_API_KEY is required (via CLI args, Smithery config, session config, or environment) to run the Mem0 MCP server."
)

default_user = _config_value(session_config, "default_user_id") or ENV_DEFAULT_USER_ID
default_user = _CLI_DEFAULT_USER_ID or _config_value(session_config, "default_user_id") or ENV_DEFAULT_USER_ID
enable_graph_default = _config_value(session_config, "enable_graph_default")
if enable_graph_default is None:
enable_graph_default = ENV_ENABLE_GRAPH_DEFAULT

return api_key, default_user, enable_graph_default


# init the client
def _mem0_client(api_key: str) -> MemoryClient:
client = _CLIENT_CACHE.get(api_key)
if client is None:
client = MemoryClient(api_key=api_key)
_CLIENT_CACHE[api_key] = client
return client


def _default_enable_graph(enable_graph: Optional[bool], default: bool) -> bool:
if enable_graph is None:
return default
Expand All @@ -151,17 +162,19 @@ def create_server() -> FastMCP:

# When running inside Smithery, the platform probes the server without user-provided
# session config, so we defer the hard requirement for MEM0_API_KEY until a tool call.
if not ENV_API_KEY:
if not ENV_API_KEY and not _CLI_API_KEY:
logger.warning(
"MEM0_API_KEY is not set; Smithery health checks will pass, but every tool "
"invocation will fail until a key is supplied via session config or env vars."
"invocation will fail until a key is supplied via session config, CLI args, or env vars."
)

server = FastMCP(
"mem0",
host=os.getenv("HOST", "0.0.0.0"),
port=int(os.getenv("PORT", "8081")),
transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False),
transport_security=TransportSecuritySettings(
enable_dns_rebinding_protection=not ENV_DISABLE_DNS_REBINDING_PROTECTION
),
Comment on lines +175 to +177
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The DNS rebinding protection is enabled by default (when ENV_DISABLE_DNS_REBINDING_PROTECTION is false), but the double-negative logic makes this confusing. The setting is named "DISABLE_DNS_REBINDING_PROTECTION" and then negated with "not", which is hard to reason about. Consider renaming the environment variable to MEM0_ENABLE_DNS_REBINDING_PROTECTION and removing the negation, or add a comment explaining the logic clearly.

Copilot uses AI. Check for mistakes.
)

# graph is disabled by default to make queries simpler and fast
Expand Down Expand Up @@ -476,9 +489,21 @@ def memory_assistant() -> str:

def main() -> None:
"""Run the MCP server over stdio."""
global _CLI_API_KEY, _CLI_DEFAULT_USER_ID

parser = argparse.ArgumentParser(description="Mem0 MCP Server")
parser.add_argument("--api-key", help="Mem0 API key (overrides MEM0_API_KEY env var)")
parser.add_argument("--user-id", help="Default user ID (overrides MEM0_DEFAULT_USER_ID env var)")

args = parser.parse_args()

if args.api_key:
_CLI_API_KEY = args.api_key
if args.user_id:
_CLI_DEFAULT_USER_ID = args.user_id

server = create_server()
logger.info("Starting Mem0 MCP server (default user=%s)", ENV_DEFAULT_USER_ID)
logger.info("Starting Mem0 MCP server (default user=%s)", _CLI_DEFAULT_USER_ID or ENV_DEFAULT_USER_ID)
server.run(transport="stdio")


Expand Down
33 changes: 33 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Tests

## Running Tests

```bash
# Run all tests
pytest

# Run with verbose output
pytest -v

# Run specific test
pytest tests/test_basic.py::TestAPIKeyValidation
```

## Test Coverage

**test_basic.py** - Core logic validation (13 tests)
- API key format validation
- Default filter injection logic
- Enable graph default behavior
- CLI argument parsing format
- Retry logic calculations
- Error code detection (4xx vs 5xx)

## Test Results

```
13 passed in 0.43s
```

All tests validate the core business logic without requiring external dependencies.

1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for mem0-mcp-server."""
Loading
Loading