diff --git a/resources/scripts/napseer_mcp_server.py b/resources/scripts/napseer_mcp_server.py index 58b7a50..926f378 100755 --- a/resources/scripts/napseer_mcp_server.py +++ b/resources/scripts/napseer_mcp_server.py @@ -7469,12 +7469,13 @@ def get_backlinks_by_path(args): def list_project_nodes(args): project_id = resolve_project_id(args) - view, view_warnings = normalize_discovery_view(args) + view, view_warnings = normalize_discovery_view(args, default="paths") filters = status_filter_from_args(args) limit = normalize_int(args.get("limit"), DISCOVERY_DEFAULT_LIMIT, 1, discovery_max_limit(view)) allowed = {"tag", "folder_path", "q", "limit", "cursor", "updated_before", "updated_after", "type", "include_archived", "archived_only"} base_query = {key: value for key, value in args.items() if key in allowed and value not in (None, "")} base_query["limit"] = limit + base_query["view"] = "summary" if view == "titles" else view if filters["active_only"]: base_query["include_archived"] = False base_query["archived_only"] = False @@ -8614,7 +8615,7 @@ def search_memory(args): raise RuntimeError("q is required") query_analysis = analyze_search_query(q) limit = max(1, min(int(args.get("limit", 25)), 100)) - view, view_warnings = normalize_discovery_view(args) + view, view_warnings = normalize_discovery_view(args, default="paths") filters = status_filter_from_args(args) mode = str(args.get("mode") or "hybrid").strip().lower() if mode not in {"hybrid", "server", "local"}: @@ -8693,7 +8694,7 @@ def search_memory(args): def recent_memory(args): limit = max(1, min(int(args.get("limit", 25)), 100)) - view, _warnings = normalize_discovery_view(args, default="titles") + view, _warnings = normalize_discovery_view(args, default="paths") query = {**args, "limit": limit, "view": view} for key in ["folder_path", "tag", "updated_before", "updated_after", "cursor"]: if args.get(key): @@ -8749,7 +8750,7 @@ def get_memory_context(args): def find_related_nodes(args): path = args["path"] - view, _warnings = normalize_discovery_view(args) + view, _warnings = normalize_discovery_view(args, default="paths") limit = max(1, min(int(args.get("limit", 25)), 100)) target = get_node_by_path({"path": path}, allow_agent=is_agent_namespace_path(path)) related = [] @@ -10227,7 +10228,7 @@ def raw_tools(): "q": {"type": "string", "description": "Search text. Natural language is accepted; distinctive nouns, paths, tags, or quoted phrases produce better matches than full instructions."}, "mode": {"type": "string", "enum": ["hybrid", "server", "local"], "default": "hybrid"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}, - "view": {"type": "string", "enum": ["titles", "paths", "summary", "metadata", "full"], "default": "summary"}, + "view": {"type": "string", "enum": ["titles", "paths", "summary", "metadata", "full"], "default": "paths"}, "include_content": {"type": "boolean", "default": False, "description": "Legacy; use view='full' for body text. Ignored when view is set."}, "active_only": {"type": "boolean", "default": False}, "status": {"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]}, @@ -10240,7 +10241,7 @@ def raw_tools(): }, { "name": "nap_find", - "description": "Structured active memory search by folder, tag, path, or query. Returns compact summaries by default; use view='full' or nap_cat for body text.", + "description": "Structured active memory search by folder, tag, path, or query. Returns path/date rows by default; use view='full' or nap_cat for body text.", "inputSchema": { "type": "object", "properties": { @@ -10249,7 +10250,7 @@ def raw_tools(): "q": {"type": "string"}, "cursor": {"type": "string"}, "limit": {"type": "integer", "minimum": 1, "maximum": 10000}, - "view": {"type": "string", "enum": ["titles", "paths", "summary", "metadata", "full"], "default": "summary"}, + "view": {"type": "string", "enum": ["titles", "paths", "summary", "metadata", "full"], "default": "paths"}, "include_content": {"type": "boolean", "default": False, "description": "Legacy; use view='full' for body text. Ignored when view is set."}, "include_archived": {"type": "boolean", "default": False}, "archived_only": {"type": "boolean", "default": False}, @@ -10343,8 +10344,8 @@ def raw_tools(): "properties": { "path": {"type": "string"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}, - "view": {"type": "string", "enum": ["titles", "paths", "summary", "metadata", "full"], "default": "titles"}, - "include_content": {"type": "boolean", "default": False, "description": "Legacy; use view='full' for body text. Ignored when view is set. nap_recent defaults to title-only output."}, + "view": {"type": "string", "enum": ["titles", "paths", "summary", "metadata", "full"], "default": "paths"}, + "include_content": {"type": "boolean", "default": False, "description": "Legacy; use view='full' for body text. Ignored when view is set. nap_recent and nap_find default to path/date output."}, "preview_chars": {"type": "integer", "minimum": 80, "maximum": 1000, "default": 240}, }, "additionalProperties": False, @@ -10352,7 +10353,7 @@ def raw_tools(): }, { "name": "nap_recent", - "description": "List recently updated project memory as title/path pairs by default, with optional folder, tag, and cursor filters.", + "description": "List recently updated project memory as path/date rows by default, with optional folder, tag, and cursor filters.", "inputSchema": { "type": "object", "properties": { @@ -10362,8 +10363,8 @@ def raw_tools(): "updated_before": {"type": "string"}, "updated_after": {"type": "string"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}, - "view": {"type": "string", "enum": ["titles", "paths", "summary", "metadata", "full"], "default": "titles"}, - "include_content": {"type": "boolean", "default": False, "description": "Legacy; use view='full' for body text. Ignored when view is set. nap_recent defaults to title-only output."}, + "view": {"type": "string", "enum": ["titles", "paths", "summary", "metadata", "full"], "default": "paths"}, + "include_content": {"type": "boolean", "default": False, "description": "Legacy; use view='full' for body text. Ignored when view is set. nap_recent and nap_find default to path/date output."}, "active_only": {"type": "boolean", "default": False}, "status": {"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]}, "exclude_status": {"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]}, diff --git a/tests/test_mcp_memory_ergonomics_helpers.py b/tests/test_mcp_memory_ergonomics_helpers.py index be0c5a5..c9d9dfc 100644 --- a/tests/test_mcp_memory_ergonomics_helpers.py +++ b/tests/test_mcp_memory_ergonomics_helpers.py @@ -80,7 +80,7 @@ def test_node_discovery_allows_large_requested_limit(mod): assert len(calls) == 1 -def test_recent_memory_defaults_to_titles(mod): +def test_recent_memory_defaults_to_paths(mod): recent = node( "/notes/recent-title", "active", @@ -93,17 +93,72 @@ def test_recent_memory_defaults_to_titles(mod): def fake_request(method, path, payload=None, **kwargs): assert method == "GET" assert "/nodes?" in path + assert "view=paths" in path return {"items": [recent], "next_cursor": None} mod.request_json = fake_request result = mod.recent_memory({}) assert result["ok"] is True - assert result["view"] == "titles" - assert result["items"] == [{"title": "Readable Recent Title", "full_path": "/notes/recent-title"}] + assert result["view"] == "paths" + assert result["items"] == [{ + "full_path": "/notes/recent-title", + "type": "note", + "status": "active", + "updated_at": "2026-05-06T12:00:00Z", + }] assert "content_text" not in result["items"][0] assert "metadata_summary" not in result["items"][0] +def test_find_defaults_to_paths_and_requests_compact_rest_view(mod): + found = node( + "/plans/found", + "planned", + content="large body that should not be fetched by default", + ) + + def fake_request(method, path, payload=None, **kwargs): + assert method == "GET" + assert "/nodes?" in path + assert "folder_path=%2Fplans" in path + assert "view=paths" in path + return {"items": [found], "next_cursor": None} + + mod.request_json = fake_request + result = mod.list_project_nodes({"folder_path": "/plans"}) + assert result["ok"] is True + assert result["view"] == "paths" + assert result["items"] == [{ + "full_path": "/plans/found", + "type": "plan", + "status": "planned", + "updated_at": "2026-05-06T12:00:00Z", + }] + assert "content_text" not in result["items"][0] + + +def test_title_view_requests_summary_rest_view_without_content(mod): + found = node( + "/notes/titled", + "active", + metadata={"title": "Readable Title"}, + content="large body that should not be fetched by title view", + ) + + def fake_request(method, path, payload=None, **kwargs): + assert method == "GET" + assert "/nodes?" in path + assert "view=summary" in path + return {"items": [found], "next_cursor": None} + + mod.request_json = fake_request + result = mod.list_project_nodes({"view": "titles"}) + assert result["ok"] is True + assert result["view"] == "titles" + assert result["items"] == [{"title": "Readable Title", "full_path": "/notes/titled"}] + assert "content_text" not in result["items"][0] + + def test_rest_query_string_serializes_booleans_lowercase(mod): query = mod.rest_query_string({ "folder_path": "/kanban", @@ -439,6 +494,12 @@ def run(): mod = load_module() test_node_discovery_allows_large_requested_limit(mod) mod = load_module() + test_recent_memory_defaults_to_paths(mod) + mod = load_module() + test_find_defaults_to_paths_and_requests_compact_rest_view(mod) + mod = load_module() + test_title_view_requests_summary_rest_view_without_content(mod) + mod = load_module() test_kanban_list_grouping_and_filters(mod) mod = load_module() test_kanban_dependency_summary_filters_ready_and_blocking(mod) diff --git a/tests/test_mcp_memory_node_encryption.py b/tests/test_mcp_memory_node_encryption.py index 1d1b982..a47f601 100644 --- a/tests/test_mcp_memory_node_encryption.py +++ b/tests/test_mcp_memory_node_encryption.py @@ -262,10 +262,9 @@ def run(): mod.request_json = lambda method, path, payload=None, **kwargs: {"items": [moved]} compact = mod.list_project_nodes({"limit": 10}) assert compact["ok"] is True - assert compact["view"] == "summary" + assert compact["view"] == "paths" assert "content_text" not in compact["items"][0] - assert compact["items"][0]["preview"] == "secret needle" - assert compact["items"][0]["metadata_summary"] == {} + assert compact["items"][0]["full_path"] == "/renamed/alpha" full = mod.list_project_nodes({"limit": 10, "view": "full"}) assert full["items"][0]["content_text"] == "secret needle"