Default MCP discovery to path views
This commit is contained in:
@@ -7469,12 +7469,13 @@ def get_backlinks_by_path(args):
|
|||||||
|
|
||||||
def list_project_nodes(args):
|
def list_project_nodes(args):
|
||||||
project_id = resolve_project_id(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)
|
filters = status_filter_from_args(args)
|
||||||
limit = normalize_int(args.get("limit"), DISCOVERY_DEFAULT_LIMIT, 1, discovery_max_limit(view))
|
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"}
|
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 = {key: value for key, value in args.items() if key in allowed and value not in (None, "")}
|
||||||
base_query["limit"] = limit
|
base_query["limit"] = limit
|
||||||
|
base_query["view"] = "summary" if view == "titles" else view
|
||||||
if filters["active_only"]:
|
if filters["active_only"]:
|
||||||
base_query["include_archived"] = False
|
base_query["include_archived"] = False
|
||||||
base_query["archived_only"] = False
|
base_query["archived_only"] = False
|
||||||
@@ -8614,7 +8615,7 @@ def search_memory(args):
|
|||||||
raise RuntimeError("q is required")
|
raise RuntimeError("q is required")
|
||||||
query_analysis = analyze_search_query(q)
|
query_analysis = analyze_search_query(q)
|
||||||
limit = max(1, min(int(args.get("limit", 25)), 100))
|
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)
|
filters = status_filter_from_args(args)
|
||||||
mode = str(args.get("mode") or "hybrid").strip().lower()
|
mode = str(args.get("mode") or "hybrid").strip().lower()
|
||||||
if mode not in {"hybrid", "server", "local"}:
|
if mode not in {"hybrid", "server", "local"}:
|
||||||
@@ -8693,7 +8694,7 @@ def search_memory(args):
|
|||||||
|
|
||||||
def recent_memory(args):
|
def recent_memory(args):
|
||||||
limit = max(1, min(int(args.get("limit", 25)), 100))
|
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}
|
query = {**args, "limit": limit, "view": view}
|
||||||
for key in ["folder_path", "tag", "updated_before", "updated_after", "cursor"]:
|
for key in ["folder_path", "tag", "updated_before", "updated_after", "cursor"]:
|
||||||
if args.get(key):
|
if args.get(key):
|
||||||
@@ -8749,7 +8750,7 @@ def get_memory_context(args):
|
|||||||
|
|
||||||
def find_related_nodes(args):
|
def find_related_nodes(args):
|
||||||
path = args["path"]
|
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))
|
limit = max(1, min(int(args.get("limit", 25)), 100))
|
||||||
target = get_node_by_path({"path": path}, allow_agent=is_agent_namespace_path(path))
|
target = get_node_by_path({"path": path}, allow_agent=is_agent_namespace_path(path))
|
||||||
related = []
|
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."},
|
"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"},
|
"mode": {"type": "string", "enum": ["hybrid", "server", "local"], "default": "hybrid"},
|
||||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
|
"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."},
|
"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},
|
"active_only": {"type": "boolean", "default": False},
|
||||||
"status": {"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]},
|
"status": {"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]},
|
||||||
@@ -10240,7 +10241,7 @@ def raw_tools():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nap_find",
|
"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": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -10249,7 +10250,7 @@ def raw_tools():
|
|||||||
"q": {"type": "string"},
|
"q": {"type": "string"},
|
||||||
"cursor": {"type": "string"},
|
"cursor": {"type": "string"},
|
||||||
"limit": {"type": "integer", "minimum": 1, "maximum": 10000},
|
"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_content": {"type": "boolean", "default": False, "description": "Legacy; use view='full' for body text. Ignored when view is set."},
|
||||||
"include_archived": {"type": "boolean", "default": False},
|
"include_archived": {"type": "boolean", "default": False},
|
||||||
"archived_only": {"type": "boolean", "default": False},
|
"archived_only": {"type": "boolean", "default": False},
|
||||||
@@ -10343,8 +10344,8 @@ def raw_tools():
|
|||||||
"properties": {
|
"properties": {
|
||||||
"path": {"type": "string"},
|
"path": {"type": "string"},
|
||||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
|
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||||
"view": {"type": "string", "enum": ["titles", "paths", "summary", "metadata", "full"], "default": "titles"},
|
"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 defaults to title-only output."},
|
"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},
|
"preview_chars": {"type": "integer", "minimum": 80, "maximum": 1000, "default": 240},
|
||||||
},
|
},
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
@@ -10352,7 +10353,7 @@ def raw_tools():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nap_recent",
|
"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": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -10362,8 +10363,8 @@ def raw_tools():
|
|||||||
"updated_before": {"type": "string"},
|
"updated_before": {"type": "string"},
|
||||||
"updated_after": {"type": "string"},
|
"updated_after": {"type": "string"},
|
||||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
|
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||||
"view": {"type": "string", "enum": ["titles", "paths", "summary", "metadata", "full"], "default": "titles"},
|
"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 defaults to title-only output."},
|
"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},
|
"active_only": {"type": "boolean", "default": False},
|
||||||
"status": {"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]},
|
"status": {"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]},
|
||||||
"exclude_status": {"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]},
|
"exclude_status": {"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]},
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ def test_node_discovery_allows_large_requested_limit(mod):
|
|||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_recent_memory_defaults_to_titles(mod):
|
def test_recent_memory_defaults_to_paths(mod):
|
||||||
recent = node(
|
recent = node(
|
||||||
"/notes/recent-title",
|
"/notes/recent-title",
|
||||||
"active",
|
"active",
|
||||||
@@ -93,17 +93,72 @@ def test_recent_memory_defaults_to_titles(mod):
|
|||||||
def fake_request(method, path, payload=None, **kwargs):
|
def fake_request(method, path, payload=None, **kwargs):
|
||||||
assert method == "GET"
|
assert method == "GET"
|
||||||
assert "/nodes?" in path
|
assert "/nodes?" in path
|
||||||
|
assert "view=paths" in path
|
||||||
return {"items": [recent], "next_cursor": None}
|
return {"items": [recent], "next_cursor": None}
|
||||||
|
|
||||||
mod.request_json = fake_request
|
mod.request_json = fake_request
|
||||||
result = mod.recent_memory({})
|
result = mod.recent_memory({})
|
||||||
assert result["ok"] is True
|
assert result["ok"] is True
|
||||||
assert result["view"] == "titles"
|
assert result["view"] == "paths"
|
||||||
assert result["items"] == [{"title": "Readable Recent Title", "full_path": "/notes/recent-title"}]
|
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 "content_text" not in result["items"][0]
|
||||||
assert "metadata_summary" 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):
|
def test_rest_query_string_serializes_booleans_lowercase(mod):
|
||||||
query = mod.rest_query_string({
|
query = mod.rest_query_string({
|
||||||
"folder_path": "/kanban",
|
"folder_path": "/kanban",
|
||||||
@@ -439,6 +494,12 @@ def run():
|
|||||||
mod = load_module()
|
mod = load_module()
|
||||||
test_node_discovery_allows_large_requested_limit(mod)
|
test_node_discovery_allows_large_requested_limit(mod)
|
||||||
mod = load_module()
|
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)
|
test_kanban_list_grouping_and_filters(mod)
|
||||||
mod = load_module()
|
mod = load_module()
|
||||||
test_kanban_dependency_summary_filters_ready_and_blocking(mod)
|
test_kanban_dependency_summary_filters_ready_and_blocking(mod)
|
||||||
|
|||||||
@@ -262,10 +262,9 @@ def run():
|
|||||||
mod.request_json = lambda method, path, payload=None, **kwargs: {"items": [moved]}
|
mod.request_json = lambda method, path, payload=None, **kwargs: {"items": [moved]}
|
||||||
compact = mod.list_project_nodes({"limit": 10})
|
compact = mod.list_project_nodes({"limit": 10})
|
||||||
assert compact["ok"] is True
|
assert compact["ok"] is True
|
||||||
assert compact["view"] == "summary"
|
assert compact["view"] == "paths"
|
||||||
assert "content_text" not in compact["items"][0]
|
assert "content_text" not in compact["items"][0]
|
||||||
assert compact["items"][0]["preview"] == "secret needle"
|
assert compact["items"][0]["full_path"] == "/renamed/alpha"
|
||||||
assert compact["items"][0]["metadata_summary"] == {}
|
|
||||||
full = mod.list_project_nodes({"limit": 10, "view": "full"})
|
full = mod.list_project_nodes({"limit": 10, "view": "full"})
|
||||||
assert full["items"][0]["content_text"] == "secret needle"
|
assert full["items"][0]["content_text"] == "secret needle"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user