Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ Example:
{"kind":"symbol","filter":{"microservice":"chat-core","symbol_kind":"interface"}}
```

**MCP v2 response extras (`hints`, pagination echo):** On success, `search`, `find`, `describe`, `neighbors`, and `resolve` return a `hints` field (`list[str]`, capped at five unique strings) with short, templated suggestions for likely next tool calls; hints are advisory. `hints` is always empty when `success` is false. `resolve` additionally echoes `resolved_identifier` (post-validation trimmed identifier) on every `success=true` response; it is `null` when `success` is false. Resolve hints fire only on `status: none` or `status: many` (not on `status: one`). `search` and `find` additionally echo the request’s `limit` and `offset` on success; on failure those echoed fields are omitted (`null` in JSON). The find page-full hint fires only when another page may exist (handler over-fetches by one row; not exposed on the output model). `neighbors` echoes `requested_edge_types` (deduped edge labels from the request) on success for empty-result hints and diagnostics. See [`propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`](./propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md) Appendix A for the locked v1 template catalog; see [`propose/HINTS-V2-PROPOSE.md`](./propose/HINTS-V2-PROPOSE.md) for v2 additions (`resolve` rules; neighbors fuzzy-strategy in a follow-up PR).
**MCP v2 response extras (`hints`, pagination echo):** On success, `search`, `find`, `describe`, `neighbors`, and `resolve` return a `hints` field (`list[str]`, capped at five unique strings) with short, templated suggestions for likely next tool calls; hints are advisory. `hints` is always empty when `success` is false. `resolve` additionally echoes `resolved_identifier` (post-validation trimmed identifier) on every `success=true` response; it is `null` when `success` is false. Resolve hints fire only on `status: none` or `status: many` (not on `status: one`). `search` and `find` additionally echo the request’s `limit` and `offset` on success; on failure those echoed fields are omitted (`null` in JSON). The find page-full hint fires only when another page may exist (handler over-fetches by one row; not exposed on the output model). `neighbors` echoes `requested_edge_types` (deduped edge labels from the request) on success for empty-result hints and diagnostics; when any result edge carries a brownfield/fallback `attrs.strategy` (see `FUZZY_STRATEGY_SET` in `java_ontology.py`), a single meta-tier fuzzy-strategy hint may also appear. See [`propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`](./propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md) Appendix A for the locked v1 template catalog; see [`propose/HINTS-V2-PROPOSE.md`](./propose/HINTS-V2-PROPOSE.md) for v2 additions (`resolve` rules and neighbors fuzzy-strategy hint).

---

Expand Down
11 changes: 11 additions & 0 deletions java_ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@
"client_target_path",
))

# Brownfield / fallback edge resolution strategies (hints v2 neighbors fuzzy signal).
FUZZY_STRATEGY_SET: frozenset[str] = frozenset({
"layer_c_source",
"layer_b_fqn",
"phantom",
"chained_receiver",
"overload_ambiguous",
"implicit_super",
})

ResolveReason = Literal[
"exact_id",
"exact_fqn",
Expand All @@ -107,5 +117,6 @@
"VALID_ASYNC_CALL_STRATEGIES",
"VALID_HTTP_CALL_MATCHES",
"VALID_RESOLVE_REASONS",
"FUZZY_STRATEGY_SET",
"ResolveReason",
]
19 changes: 18 additions & 1 deletion mcp_hints.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
"""Pure MCP v2 road-sign hint generation (no graph I/O, no search, no LLM).

Locked v1 catalog: ``propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`` Appendix A.
v2 resolve catalog: ``propose/HINTS-V2-PROPOSE.md`` Appendix A.
v2 resolve + neighbors fuzzy-strategy catalog: ``propose/HINTS-V2-PROPOSE.md`` Appendix A.
Priority cap: same propose §7.12 / ``plans/completed/PLAN-HINTS.md`` principles.
"""

from __future__ import annotations

from typing import Any, Literal

from java_ontology import FUZZY_STRATEGY_SET

# Normative schema description (propose §3.1) — imported by ``mcp_v2`` for Field(description=...).
MCP_HINTS_FIELD_DESCRIPTION = (
"Road-sign hints pointing to likely next calls. Each hint is a short string "
Expand Down Expand Up @@ -65,6 +67,10 @@
_RESOLVE_HINT_MAX_CHARS = 120
_RESOLVE_WILDCARDS = ("*", "?")

TPL_NEIGHBORS_FUZZY_STRATEGY = (
"some edges resolved via brownfield/fallback strategy — check attrs.strategy on each row"
)

# §7.12 priority: DECLARES.* type rollups > OVERRIDDEN_BY.* > leaf follow-ups > meta.
PRIORITY_DECLARES_TYPE_ROLLUP = 4
PRIORITY_OVERRIDDEN_AXIS = 3
Expand Down Expand Up @@ -99,6 +105,15 @@ def _symbol_declaration_kind(record: dict[str, Any]) -> str | None:
return None


def _any_fuzzy_strategy(edges: list[dict[str, Any]]) -> bool:
for e in edges:
attrs = e.get("attrs") if isinstance(e.get("attrs"), dict) else {}
s = attrs.get("strategy") if isinstance(attrs, dict) else None
if isinstance(s, str) and s in FUZZY_STRATEGY_SET:
return True
return False


def _find_has_identifier_shaped_filter(kind: str, flt: dict[str, Any]) -> bool:
for name in _IDENTIFIER_FILTER_FIELDS.get(kind, ()):
val = flt.get(name)
Expand Down Expand Up @@ -215,6 +230,8 @@ def generate_hints(
n_types = len([x for x in req_types if str(x).strip()])
if not results and n_types > 0:
pairs.append((PRIORITY_META, TPL_NEIGHBORS_EMPTY_KIND_CHECK))
elif _any_fuzzy_strategy(results):
pairs.append((PRIORITY_META, TPL_NEIGHBORS_FUZZY_STRATEGY))
return finalize_hint_list(pairs)

if output_kind == "describe":
Expand Down
3 changes: 2 additions & 1 deletion server.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,8 @@ async def describe(
"Optional `filter` applies to each neighbor endpoint row; populated fields must be applicable to that "
"neighbor's kind—mixed-kind result sets fail on the first inapplicable neighbor (strict frame). "
"Wildcards in prefix fields are rejected. Unknown NodeFilter keys return success=false. "
"Successful responses echo `requested_edge_types` and may include `hints` (advisory next-step strings)."
"Successful responses echo `requested_edge_types` and may include `hints` (advisory next-step strings). "
"Each edge's `attrs.strategy` indicates resolution quality (brownfield/fallback vs primary paths)."
),
)
async def neighbors(
Expand Down
96 changes: 96 additions & 0 deletions tests/test_mcp_hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
finalize_hint_list,
generate_hints,
)
from java_ontology import FUZZY_STRATEGY_SET
from mcp_v2 import FindOutput, SearchOutput, describe_v2, find_v2, neighbors_v2, resolve_v2, search_v2

_TYPE_KINDS = frozenset({"class", "interface", "enum", "record", "annotation"})
Expand Down Expand Up @@ -253,6 +254,100 @@ def test_hints_neighbors_empty_with_edge_types_emits_kind_check(kuzu_graph) -> N
assert mcp_hints.TPL_NEIGHBORS_EMPTY_KIND_CHECK in out.hints


def _neighbors_hint_payload(
results: list[dict[str, Any]],
*,
requested_edge_types: list[str] | None = None,
) -> dict[str, Any]:
return {
"success": True,
"results": results,
"requested_edge_types": requested_edge_types or ["DECLARES_CLIENT"],
}


def _edge_result(*, strategy: str | None = None, edge_type: str = "DECLARES_CLIENT") -> dict[str, Any]:
attrs: dict[str, Any] = {}
if strategy is not None:
attrs["strategy"] = strategy
return {
"origin_id": "sym:pkg.Type#m()",
"edge_type": edge_type,
"direction": "out",
"other": {"id": "client:svc:feign:t:GET:/p", "kind": "client"},
"attrs": attrs,
}


def _method_id_with_fuzzy_calls(kuzu_graph) -> str:
rows = kuzu_graph._rows( # noqa: SLF001
"MATCH (m:Symbol)-[e:CALLS]->() "
"WHERE e.strategy IN $strategies "
"RETURN m.id AS id LIMIT 1",
{"strategies": sorted(FUZZY_STRATEGY_SET)},
)
if not rows:
pytest.fail("no CALLS edge with fuzzy strategy in bank fixture")
return str(rows[0]["id"])


def test_hints_neighbors_fuzzy_strategy_layer_c_source_emits() -> None:
payload = _neighbors_hint_payload([_edge_result(strategy="layer_c_source")])
hints = generate_hints("neighbors", payload)
assert mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY in hints
assert "attrs.strategy" in hints[0]


def test_hints_neighbors_fuzzy_strategy_annotation_absent() -> None:
payload = _neighbors_hint_payload([_edge_result(strategy="annotation")])
assert generate_hints("neighbors", payload) == []


def test_hints_neighbors_fuzzy_strategy_calls_phantom_emits() -> None:
payload = _neighbors_hint_payload(
[_edge_result(strategy="phantom", edge_type="CALLS")],
requested_edge_types=["CALLS"],
)
hints = generate_hints("neighbors", payload)
assert mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY in hints


def test_hints_neighbors_declares_no_strategy_attrs_empty() -> None:
payload = _neighbors_hint_payload(
[_edge_result(edge_type="DECLARES")],
requested_edge_types=["DECLARES"],
)
assert generate_hints("neighbors", payload) == []


def test_hints_neighbors_multi_origin_fuzzy_emits_once() -> None:
payload = _neighbors_hint_payload(
[
_edge_result(strategy="phantom", edge_type="CALLS"),
_edge_result(strategy="annotation", edge_type="CALLS"),
],
requested_edge_types=["CALLS"],
)
hints = generate_hints("neighbors", payload)
assert hints.count(mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY) == 1


def test_hints_neighbors_layer_a_meta_no_fuzzy_hint() -> None:
payload = _neighbors_hint_payload([_edge_result(strategy="layer_a_meta")])
assert generate_hints("neighbors", payload) == []


def test_hints_neighbors_fuzzy_strategy_neighbors_v2_round_trip(kuzu_graph) -> None:
mid = _method_id_with_fuzzy_calls(kuzu_graph)
out = neighbors_v2(mid, direction="out", edge_types=["CALLS"], graph=kuzu_graph, limit=50)
assert out.success is True
assert out.results
strategies = [e.attrs.get("strategy") for e in out.results]
assert any(s in FUZZY_STRATEGY_SET for s in strategies if isinstance(s, str))
assert mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY in out.hints
assert "brownfield/fallback strategy" in out.hints[0]


def test_hints_search_weak_structural_signal_emits(monkeypatch, kuzu_graph) -> None:
rows = [
{
Expand Down Expand Up @@ -735,6 +830,7 @@ def test_hints_pagination_none_skips_page_derived_hints() -> None:
),
(mcp_hints.TPL_RESOLVE_NONE_TRY_FIND_CLIENT, {"seed": "smartcare-assign-chat"}),
(mcp_hints.TPL_RESOLVE_MANY_TIGHTEN, {"n": 10}),
(mcp_hints.TPL_NEIGHBORS_FUZZY_STRATEGY, {}),
],
)
def test_hints_template_rendered_length_leq_120(template: str, fmt: dict[str, Any]) -> None:
Expand Down
Loading