From 298ec5de9e880f8a51052c88720ddb57da225987 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 16:37:51 +0000 Subject: [PATCH 1/3] feat: guide adaptive search limits and raise default to 15 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both search surfaces already expose `limit`; this makes it useful adaptively rather than a fixed compromise. - mcp_server.py: enrich the search_memories docstring (the model reads it as the tool description) to size `limit` to query breadth — ~5 for a narrow lookup, ~20-25 for a broad/exploratory one — noting atomic-fact memories make a larger limit cheap and under-fetching a broad query costs more than over-fetching a narrow one. - Raise the default search limit 10 -> 15 on both the MCP tool and REST SearchRequest, reflecting that asymmetry. (List default stays 50.) - USER_GUIDE: update the search reference to default 15 + sizing guidance, and add an adaptive-limit line to the CLAUDE.md and AGENTS.md prompt blocks. - Tests: assert the new default (15) on both MCP and REST search. No server-side auto-sizing: score-threshold/elbow approaches are brittle to calibrate (and now embed-model-dependent with Ollama), and the calling agent already holds the query-breadth intent the server would have to guess. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XQXUARh5h5hRFg67toenEX --- app/mcp_server.py | 10 +++++++++- app/rest.py | 6 +++++- docs/USER_GUIDE.md | 14 +++++++++++++- tests/test_mcp.py | 12 +++++++++++- tests/test_rest.py | 9 +++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/app/mcp_server.py b/app/mcp_server.py index 396a9fa..450d7a2 100644 --- a/app/mcp_server.py +++ b/app/mcp_server.py @@ -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. + 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. diff --git a/app/rest.py b/app/rest.py index aa48bcd..6f2970f 100644 --- a/app/rest.py +++ b/app/rest.py @@ -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" diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index e3eb83f..0e1bb07 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -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 @@ -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. @@ -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). + +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` and is documented to size it this way +automatically. ```bash curl -X POST https://mem0.your-domain.com/api/v1/memories/search \ diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 51cfbaa..05ded22 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -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): diff --git a/tests/test_rest.py b/tests/test_rest.py index 101bb4f..19705a8 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -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) From a8cac52f690808b8e641e7cdb9bb6c2a2a8eae40 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 16:47:55 +0000 Subject: [PATCH 2/3] fix: validate MCP search limit and correct docs wording (PR review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mcp_server.py: enforce 1-100 on search_memories' limit, mirroring list_memories and the REST pydantic bound. The docstring claimed the range but nothing checked it, so a 0 or huge limit reached the backend. Adds a test. - USER_GUIDE: reword "documented to size it this way automatically" — the server does not auto-resize limit; the tool description guides the agent. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XQXUARh5h5hRFg67toenEX --- app/mcp_server.py | 2 ++ docs/USER_GUIDE.md | 4 ++-- tests/test_mcp.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/mcp_server.py b/app/mcp_server.py index 450d7a2..076e493 100644 --- a/app/mcp_server.py +++ b/app/mcp_server.py @@ -57,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": diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 0e1bb07..179fdb4 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -1038,8 +1038,8 @@ Size `limit` to the breadth of the query: a small value (~5) for a narrow lookup 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` and is documented to size it this way -automatically. +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 \ diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 05ded22..bfecc92 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -143,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"}) From 38252938174ed3dbe8f4d2dd67cd6acc544ddc2e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 16:55:41 +0000 Subject: [PATCH 3/3] docs: sync PRD search limit default 10 -> 15 The PRD is the source of truth for design; update its SearchRequest and search_memories snippets to the new default so the spec doesn't drift from the code (PR review follow-up). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XQXUARh5h5hRFg67toenEX --- docs/PRD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/PRD.md b/docs/PRD.md index e3de042..42af055 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -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 @@ -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}