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
12 changes: 11 additions & 1 deletion app/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,20 @@ def add_memory(content: str, agent_id: str | None = None, metadata: dict | None

@mcp.tool
def search_memories(
query: str, limit: int = 10, recency_weight: float = 0.0, mode: str = "semantic"
query: str, limit: int = 15, recency_weight: float = 0.0, mode: str = "semantic"
) -> dict:
"""Search long-term memory.

Searches the single shared memory store for the user, across all agents.

limit (1-100, default 15) is how many memories to return — size it to the
breadth of the query. Use a small limit (~5) for a narrow lookup about one
specific thing, and a larger one (~20-25) for a broad or exploratory
question — a person's overall preferences, everything relevant to a
project — where wider context helps. Stored memories are short atomic
facts, so a larger limit adds little cost; prefer erring high on broad
queries over missing relevant context.
Comment thread
imonroe marked this conversation as resolved.

mode: "semantic" (default) ranks by meaning/similarity. Use "keyword" for
a case-insensitive substring match when you need an exact term the
semantic search may miss — a name, identifier, URL, or rare token.
Expand All @@ -49,6 +57,8 @@ def search_memories(
toward more recently created or updated memories. Leave it at 0 for pure
relevance; raise it (e.g. 0.3) when the user asks what is *latest*.
"""
if not 1 <= limit <= 100:
raise ValueError(f"limit must be between 1 and 100, got {limit}")
if mode not in ("semantic", "keyword"):
raise ValueError(f"mode must be 'semantic' or 'keyword', got {mode!r}")
if mode == "keyword":
Expand Down
6 changes: 5 additions & 1 deletion app/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ class SearchRequest(BaseModel):
user_id: str | None = None
agent_id: str | None = None
run_id: str | None = None
limit: int = Field(default=10, ge=1, le=100)
# Default 15 (not the list default of 50): sized so broad/exploratory
# queries get enough context without the caller having to tune it, while a
# narrow lookup can pass a small limit. Atomic-fact memories make a larger
# limit cheap; under-fetching a broad query costs more than over-fetching.
limit: int = Field(default=15, ge=1, le=100)
# "semantic" (default, vector similarity) or "keyword" (case-insensitive
# substring match for exact terms semantic search misses).
mode: Literal["semantic", "keyword"] = "semantic"
Expand Down
4 changes: 2 additions & 2 deletions docs/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ class SearchRequest(BaseModel):
query: str
user_id: Optional[str] = None
agent_id: Optional[str] = None
limit: int = Field(default=10, ge=1, le=100)
limit: int = Field(default=15, ge=1, le=100)
```

### 7.3 `/healthz` behavior
Expand Down Expand Up @@ -398,7 +398,7 @@ def build_mcp() -> FastMCP:
def search_memories(
query: str,
agent_id: str | None = None,
limit: int = 10,
limit: int = 15,
) -> dict:
"""Search long-term memory by semantic similarity."""
kwargs = {"user_id": default_user, "limit": limit}
Expand Down
14 changes: 13 additions & 1 deletion docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,9 @@ You have a persistent memory store available through the mem0 MCP server. Use it

- **At the start of a task**, call `search_memories` with a query about the topic to recall any
relevant preferences, decisions, or context before you respond.
- **Size the search to the question.** For a narrow lookup about one specific thing, a small
`limit` (~5) is enough; for a broad or exploratory question — someone's overall preferences,
everything about a project — raise `limit` (~20-25) so you get enough context to work from.
- **When the user shares** a durable preference, decision, project convention, or fact they'll
likely want recalled later, call `add_memory` to save it. Keep each memory a single clear fact.
- **When something changes**, find the existing memory (`search_memories` / `list_memories`) and
Expand Down Expand Up @@ -739,6 +742,8 @@ at session start. Drop in a tool-agnostic version:
A shared long-term memory store is available via the mem0 MCP server. Behavior:

1. Recall: at the start of a task, search memory for context relevant to the request before acting.
Size the search to the question — a small limit (~5) for a specific lookup, a larger one
(~20-25) for a broad or exploratory one.
2. Persist: save durable facts, preferences, decisions, and conventions as they arise.
3. Reconcile: update an existing memory when it changes; avoid near-duplicates.
4. Safety: never store secrets, credentials, or sensitive personal data.
Expand Down Expand Up @@ -1027,7 +1032,14 @@ curl -X POST https://mem0.your-domain.com/api/v1/memories \

### Search memories — `POST /api/v1/memories/search`

Semantic search. Optional `agent_id`, `run_id`, `user_id`, and `limit` (1–100, default 10).
Semantic search. Optional `agent_id`, `run_id`, `user_id`, and `limit` (1–100, default 15).
Comment thread
imonroe marked this conversation as resolved.

Size `limit` to the breadth of the query: a small value (~5) for a narrow lookup about one
specific thing, a larger one (~20–25) for a broad or exploratory question where wider context
helps. Stored memories are short atomic facts, so a larger limit costs little — under-fetching a
broad query loses relevant context, while over-fetching a narrow one only appends weaker matches.
The MCP `search_memories` tool takes the same `limit`; its tool description guides connected agents
to size it this way (the server does not resize it for you).

```bash
curl -X POST https://mem0.your-domain.com/api/v1/memories/search \
Expand Down
22 changes: 21 additions & 1 deletion tests/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,17 @@ async def test_search_with_recency_weight_invokes_mem(mcp, mem):
await client.call_tool("search_memories", {"query": "x", "recency_weight": 0.5})
_, kwargs = mem.search.call_args
assert kwargs["filters"] == {"user_id": "default-user"}
assert kwargs["top_k"] == 10
assert kwargs["top_k"] == 15


async def test_search_default_limit_is_15(mcp, mem):
# Default sized for broad queries; the model narrows it explicitly when
# doing a specific lookup (see the tool docstring).
mem.search.return_value = {"results": []}
async with Client(mcp) as client:
await client.call_tool("search_memories", {"query": "x"})
_, kwargs = mem.search.call_args
assert kwargs["top_k"] == 15


async def test_list_memories_tool(mcp, mem):
Expand Down Expand Up @@ -133,6 +143,16 @@ async def test_list_memories_tool_rejects_bad_paging(mcp, mem):
mem.get_all.assert_not_called()


async def test_search_memories_tool_rejects_bad_limit(mcp, mem):
# The MCP tool enforces the same 1-100 range the REST API validates via
# pydantic, so an out-of-range limit never reaches the backend.
async with Client(mcp) as client:
for args in ({"query": "x", "limit": 0}, {"query": "x", "limit": 101}):
with pytest.raises(ToolError):
await client.call_tool("search_memories", args)
mem.search.assert_not_called()


async def test_delete_memory_tool(mcp, mem):
async with Client(mcp) as client:
await client.call_tool("delete_memory", {"memory_id": "xyz"})
Expand Down
9 changes: 9 additions & 0 deletions tests/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ def test_search(app_instance, mem, auth_header):
assert kwargs["filters"]["user_id"] == "default-user"


def test_search_default_limit_is_15(app_instance, mem, auth_header):
mem.search.return_value = {"results": []}
c = _client(app_instance)
resp = c.post("/api/v1/memories/search", json={"query": "where"}, headers=auth_header)
assert resp.status_code == 200
_, kwargs = mem.search.call_args
assert kwargs["top_k"] == 15


def test_list(app_instance, mem, auth_header):
mem.get_all.return_value = {"results": []}
c = _client(app_instance)
Expand Down
Loading