Skip to content

Commit a080e39

Browse files
add v4 find success-path hints (F1–F3)
Emit handler/HTTP/async neighbors follow-ups on non-page-full find results; document v4 dot-key reversal in HINTS-ROAD-SIGNS appendix. S1 search single-hit describe deferred per plan. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 759a14e commit a080e39

3 files changed

Lines changed: 110 additions & 5 deletions

File tree

mcp_hints.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@
5858

5959
TPL_FIND_EMPTY_RESOLVE = "no matches — try resolve(identifier, hint_kind='{kind}') for canonical lookup"
6060
TPL_FIND_PAGE_FULL = "result page full at {limit} — narrow filter or paginate"
61+
TPL_FIND_SUCCESS_HANDLER = "handler: neighbors(['{id}'],'in',['EXPOSES'])"
62+
TPL_FIND_SUCCESS_HTTP_TARGETS = "HTTP targets: neighbors(['{id}'],'out',['HTTP_CALLS'])"
63+
TPL_FIND_SUCCESS_ASYNC_TARGETS = "async targets: neighbors(['{id}'],'out',['ASYNC_CALLS'])"
64+
65+
_FIND_SUCCESS_MAX_CHARS = 120
6166

6267
TPL_NEIGHBORS_WRONG_SUBJECT_KIND = (
6368
"0 results — '{edge}' connects {src_kind} → {dst_kind}; "
@@ -394,6 +399,41 @@ def neighbors_success_hints(payload: dict[str, Any]) -> list[tuple[int, str]]:
394399
return pairs
395400

396401

402+
def _find_is_page_full(payload: dict[str, Any], results: list[dict[str, Any]]) -> bool:
403+
lim = payload.get("limit")
404+
return (
405+
lim is not None
406+
and len(results) >= int(lim)
407+
and payload.get("has_more_results") is True
408+
)
409+
410+
411+
def _append_find_success_hint(pairs: list[tuple[int, str]], text: str) -> None:
412+
if text and len(text) <= _FIND_SUCCESS_MAX_CHARS:
413+
pairs.append((PRIORITY_LEAF_FOLLOWUP, text))
414+
415+
416+
def find_success_hints(payload: dict[str, Any]) -> list[tuple[int, str]]:
417+
"""v4 non-empty find follow-ups (F1–F3); no graph I/O."""
418+
if not payload.get("success"):
419+
return []
420+
results = list(payload.get("results") or [])
421+
if not results or _find_is_page_full(payload, results):
422+
return []
423+
node_id = str(results[0].get("id") or "")
424+
if not node_id:
425+
return []
426+
kind = str(payload.get("kind") or "")
427+
pairs: list[tuple[int, str]] = []
428+
if kind == "route":
429+
_append_find_success_hint(pairs, TPL_FIND_SUCCESS_HANDLER.format(id=node_id))
430+
elif kind == "client":
431+
_append_find_success_hint(pairs, TPL_FIND_SUCCESS_HTTP_TARGETS.format(id=node_id))
432+
elif kind == "producer":
433+
_append_find_success_hint(pairs, TPL_FIND_SUCCESS_ASYNC_TARGETS.format(id=node_id))
434+
return pairs
435+
436+
397437
def _any_fuzzy_strategy(edges: list[dict[str, Any]]) -> bool:
398438
for e in edges:
399439
attrs = e.get("attrs") if isinstance(e.get("attrs"), dict) else {}
@@ -503,12 +543,9 @@ def generate_hints(
503543
lim = payload.get("limit")
504544
if not results and _find_has_identifier_shaped_filter(kind, flt):
505545
pairs.append((PRIORITY_META, TPL_FIND_EMPTY_RESOLVE.format(kind=kind)))
506-
if (
507-
lim is not None
508-
and len(results) >= int(lim)
509-
and payload.get("has_more_results") is True
510-
):
546+
if _find_is_page_full(payload, results) and lim is not None:
511547
pairs.append((PRIORITY_META, TPL_FIND_PAGE_FULL.format(limit=int(lim))))
548+
pairs.extend(find_success_hints(payload))
512549
return finalize_hint_list(pairs)
513550

514551
if output_kind == "neighbors":

propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@ File placement (`mcp_hints.py`), function decomposition, integration points in `
280280

281281
**Amendment (2026-05-16, issue #161 / PR #164)** — five describe templates for the producer axis and override-route rollup, symmetric with the client/route rows above: `DECLARES.DECLARES_PRODUCER`, `DECLARES_PRODUCER`, `OVERRIDDEN_BY.DECLARES_PRODUCER`, `OVERRIDDEN_BY.EXPOSES`, and `kind == producer` declaring-method hint. No ontology or re-index change.
282282

283+
**Amendment (v4 success-path, issue #163)** — second partial dot-key emission reversal: non-empty `neighbors` success hints on type Symbol origins may recommend `DECLARES.DECLARES_CLIENT`, `DECLARES.DECLARES_PRODUCER`, and `DECLARES.EXPOSES` (matching describe rollups). v3 empty structural `neighbors` hints still never use dot-keys (`_filter_neighbors_dotkey_hints` applies to the empty branch only). `OVERRIDDEN_BY.*` dot-keys remain describe-only. No ontology or re-index change.
284+
283285
**What stayed unchanged from the first draft**
284286

285287
- §1 frame statement; §2 principles 1–8; §3.1 field shape; §3.2 generation contract; §5 "deliberately does NOT do" table; §8 risks table.

tests/test_mcp_hints.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,9 @@ def test_hints_neighbors_v2_declares_success_emits_dot_key_clients(kuzu_graph) -
10261026
mcp_hints.TPL_DESCRIBE_TYPE_ROUTES_VIA_MEMBERS,
10271027
{"id": "sym:com.example.bank.chat.Controller"},
10281028
),
1029+
(mcp_hints.TPL_FIND_SUCCESS_HANDLER, {"id": "route:svc:GET:/api/v1/chat"}),
1030+
(mcp_hints.TPL_FIND_SUCCESS_HTTP_TARGETS, {"id": "client:svc:feign:target:GET:/p"}),
1031+
(mcp_hints.TPL_FIND_SUCCESS_ASYNC_TARGETS, {"id": "producer:svc:kafka:topic:t"}),
10291032
],
10301033
)
10311034
def test_hints_all_v4_templates_under_120_chars(template: str, substitutions: dict[str, str]) -> None:
@@ -1159,6 +1162,69 @@ def test_hints_cap_same_priority_keeps_emission_order() -> None:
11591162
assert "e-meta" not in got
11601163

11611164

1165+
def _find_success_payload(
1166+
kind: str,
1167+
node_id: str,
1168+
*,
1169+
limit: int | None = None,
1170+
has_more_results: bool = False,
1171+
) -> dict[str, Any]:
1172+
payload: dict[str, Any] = {
1173+
"success": True,
1174+
"kind": kind,
1175+
"results": [{"id": node_id, "kind": kind}],
1176+
"filter": {},
1177+
"offset": 0,
1178+
}
1179+
if limit is not None:
1180+
payload["limit"] = limit
1181+
payload["has_more_results"] = has_more_results
1182+
return payload
1183+
1184+
1185+
def test_hints_find_route_success_emits_handler() -> None:
1186+
rid = "route:svc:GET:/api/v1/chat"
1187+
payload = _find_success_payload("route", rid)
1188+
want = mcp_hints.TPL_FIND_SUCCESS_HANDLER.format(id=rid)
1189+
assert want in generate_hints("find", payload)
1190+
1191+
1192+
def test_hints_find_client_success_emits_http_calls() -> None:
1193+
cid = "client:svc:feign:target:GET:/p"
1194+
payload = _find_success_payload("client", cid)
1195+
want = mcp_hints.TPL_FIND_SUCCESS_HTTP_TARGETS.format(id=cid)
1196+
assert want in generate_hints("find", payload)
1197+
1198+
1199+
def test_hints_find_producer_success_emits_async_calls() -> None:
1200+
pid = "producer:svc:kafka:topic:t"
1201+
payload = _find_success_payload("producer", pid)
1202+
want = mcp_hints.TPL_FIND_SUCCESS_ASYNC_TARGETS.format(id=pid)
1203+
assert want in generate_hints("find", payload)
1204+
1205+
1206+
def test_hints_find_success_suppressed_when_page_full() -> None:
1207+
rid = "route:svc:GET:/api/v1/chat"
1208+
payload = _find_success_payload("route", rid, limit=1, has_more_results=True)
1209+
hints = generate_hints("find", payload)
1210+
assert mcp_hints.TPL_FIND_PAGE_FULL.format(limit=1) in hints
1211+
assert mcp_hints.TPL_FIND_SUCCESS_HANDLER.format(id=rid) not in hints
1212+
1213+
1214+
def test_hints_find_success_uses_first_result_id_when_multiple() -> None:
1215+
first = "route:svc:GET:/first"
1216+
second = "route:svc:GET:/second"
1217+
payload = _find_success_payload("route", first)
1218+
payload["results"] = [
1219+
{"id": first, "kind": "route"},
1220+
{"id": second, "kind": "route"},
1221+
]
1222+
want = mcp_hints.TPL_FIND_SUCCESS_HANDLER.format(id=first)
1223+
hints = generate_hints("find", payload)
1224+
assert want in hints
1225+
assert mcp_hints.TPL_FIND_SUCCESS_HANDLER.format(id=second) not in hints
1226+
1227+
11621228
def test_hints_find_page_full_requires_has_more_results_flag() -> None:
11631229
full_page = {
11641230
"success": True,

0 commit comments

Comments
 (0)