Skip to content

Commit c69d205

Browse files
clarify describe FQN collision hint and cover wildcard branches
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1ee4906 commit c69d205

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

mcp_v2.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,10 @@ def describe_v2(
650650
return DescribeOutput(success=False, message=f"No Symbol found for fqn='{fqn_val}'")
651651
node_id = str(rows[0]["id"] or "")
652652
if len(rows) > 1:
653-
hint_message = "multiple symbols share this FQN; pass microservice to disambiguate"
653+
hint_message = (
654+
"multiple symbols share this FQN; narrow with find(kind='symbol', filter including "
655+
"microservice and fqn_prefix), then describe(id=...), or use search(query=...) for ranked candidates"
656+
)
654657
kind = _resolve_node_kind(g, node_id)
655658
row = _load_node_record(g, node_id, kind)
656659
if row is None:

tests/test_mcp_v2.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,38 @@ def test_wildcard_question_mark_in_fqn_prefix_rejected(kuzu_graph) -> None:
639639
assert "fqn_prefix" in out.message
640640

641641

642+
def test_search_wildcard_in_fqn_prefix_rejected_without_run_search(monkeypatch, kuzu_graph) -> None:
643+
calls: list[int] = []
644+
645+
def boom(*_a, **_k):
646+
calls.append(1)
647+
return _fake_search_rows()
648+
649+
monkeypatch.setattr("mcp_v2.run_search", boom)
650+
out = search_v2("anything", filter={"fqn_prefix": "com.*"}, graph=kuzu_graph)
651+
assert out.success is False
652+
assert out.message
653+
assert "fqn_prefix" in out.message
654+
assert calls == []
655+
656+
657+
def test_neighbors_wildcard_in_filter_rejected_before_graph_query(kuzu_graph) -> None:
658+
class ExplodeGraph:
659+
def _rows(self, *_a, **_k) -> list:
660+
raise AssertionError("graph must not be queried when wildcard rejects filter")
661+
662+
out = neighbors_v2(
663+
"sym:unused",
664+
direction="out",
665+
edge_types=["CALLS"],
666+
filter={"fqn_prefix": "com.*"},
667+
graph=ExplodeGraph(), # type: ignore[arg-type]
668+
)
669+
assert out.success is False
670+
assert out.message
671+
assert "fqn_prefix" in out.message
672+
673+
642674
def test_describe_by_fqn_returns_symbol(kuzu_graph) -> None:
643675
symbol = kuzu_graph.list_by_role("SERVICE", limit=1)[0]
644676
out = describe_v2(fqn=symbol.fqn, graph=kuzu_graph)
@@ -665,6 +697,54 @@ def test_describe_by_fqn_id_takes_precedence(kuzu_graph) -> None:
665697
assert str(out.record.data.get("role") or "") == "SERVICE"
666698

667699

700+
def test_describe_by_fqn_duplicate_returns_first_with_disambiguation_hint() -> None:
701+
class DupFqnGraph:
702+
def _rows(self, query: str, params: dict | None = None) -> list:
703+
p = params or {}
704+
if "WHERE s.fqn = $fqn" in query:
705+
if p.get("fqn") == "com.fixture.DupeName":
706+
return [{"id": "sym:dupe-a"}, {"id": "sym:dupe-b"}]
707+
if "MATCH (n:Symbol)" in query and "WHERE n.id = $id" in query:
708+
if p.get("id") == "sym:dupe-a":
709+
return [
710+
{
711+
"id": "sym:dupe-a",
712+
"kind": "file",
713+
"name": "DupeName",
714+
"fqn": "com.fixture.DupeName",
715+
"package": "com.fixture",
716+
"module": "fixture",
717+
"microservice": "svc-a",
718+
"filename": "DupeName.java",
719+
"start_line": 1,
720+
"end_line": 1,
721+
"start_byte": 0,
722+
"end_byte": 0,
723+
"modifiers": [],
724+
"annotations": [],
725+
"capabilities": [],
726+
"role": "",
727+
"signature": "",
728+
"parent_id": "",
729+
"resolved": True,
730+
}
731+
]
732+
return []
733+
734+
def edge_counts_for(self, node_id: str) -> dict[str, dict[str, int]]:
735+
return {}
736+
737+
out = describe_v2(fqn="com.fixture.DupeName", graph=DupFqnGraph()) # type: ignore[arg-type]
738+
assert out.success is True
739+
assert out.record is not None
740+
assert out.record.id == "sym:dupe-a"
741+
assert out.message
742+
assert "multiple symbols share this FQN" in out.message
743+
assert "find(kind='symbol'" in out.message
744+
assert "describe(id=..." in out.message
745+
assert "search(query=..." in out.message
746+
747+
668748
def test_describe_by_fqn_requires_id_or_fqn(kuzu_graph) -> None:
669749
out = describe_v2(graph=kuzu_graph)
670750
assert out.success is False

0 commit comments

Comments
 (0)