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
39 changes: 24 additions & 15 deletions mcp_hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,24 +570,33 @@ def _trace_structured_hints(payload: dict[str, Any]) -> tuple[list[_StructuredHi
# (c) Cross-service boundary hint (no stats dependency).
xs_edges = [e for e in edges if e.get("cross_service_boundary")]
if xs_edges:
for xe in xs_edges[:3]:
to_id = str(xe.get("to_id") or "")
from_id = str(xe.get("from_id") or "")
confidence = xe.get("attrs", {}).get("confidence") if isinstance(xe.get("attrs"), dict) else None
conf_str = f"confidence={confidence}" if confidence is not None else "low confidence"
# When cross_service=True, BFS already continued through boundaries.
# Emit a lighter informational advisory instead of an action hint.
was_seamless = bool(payload.get("cross_service"))
if was_seamless:
advisories.append((
PRIORITY_META,
f"Cross-service boundary: {from_id} -> {to_id} ({conf_str}). "
f"Use trace('{to_id}', 'out', ['EXPOSES','CALLS'], max_depth=4) to continue in the downstream service, "
f"or describe('{to_id}') for route details.",
))
struct_pairs.append(_StructuredHint(
"trace",
{"ids": [to_id], "direction": "out", "edge_types": ["EXPOSES", "CALLS"], "max_depth": 4},
True, PRIORITY_LEAF_FOLLOWUP,
LABEL_CROSS_SERVICE_BOUNDARY,
f"cross-service boundary: {from_id} -> {to_id}",
f"trace crossed {len(xs_edges)} service boundary(ies).",
))
else:
for xe in xs_edges[:3]:
to_id = str(xe.get("to_id") or "")
from_id = str(xe.get("from_id") or "")
confidence = xe.get("attrs", {}).get("confidence") if isinstance(xe.get("attrs"), dict) else None
conf_str = f"confidence={confidence}" if confidence is not None else "low confidence"
advisories.append((
PRIORITY_META,
f"Cross-service boundary: {from_id} -> {to_id} ({conf_str}). "
f"Use trace('{to_id}', 'out', ['EXPOSES','CALLS'], max_depth=4) to continue in the downstream service, "
f"or describe('{to_id}') for route details.",
))
struct_pairs.append(_StructuredHint(
"trace",
{"ids": [to_id], "direction": "out", "edge_types": ["EXPOSES", "CALLS"], "max_depth": 4},
True, PRIORITY_LEAF_FOLLOWUP,
LABEL_CROSS_SERVICE_BOUNDARY,
f"cross-service boundary: {from_id} -> {to_id}",
))

return (finalize_structured_hints(struct_pairs), finalize_advisories(advisories))

Expand Down
26 changes: 20 additions & 6 deletions mcp_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ def trace_v2(
fan_out_cap: int = 5,
collapse_trivial: bool = True,
include_unresolved: bool = False,
cross_service: bool = False,
graph: KuzuGraph | None = None,
) -> TraceOutput:
"""Multi-hop BFS traversal with pruning."""
Expand Down Expand Up @@ -601,6 +602,12 @@ def trace_v2(
# Determine if cross-service detection is active.
cross_service_active = bool(set(edge_types) & _CROSS_SERVICE_EDGE_TYPES)

# Effective scaffolding set: when cross_service=True, EXPOSES is also scaffolding
# so Route -> Handler is followed automatically in downstream services.
effective_scaffolding = _SCAFFOLDING_EDGE_TYPES
if cross_service:
effective_scaffolding = _SCAFFOLDING_EDGE_TYPES | frozenset({"EXPOSES"})

# BFS state.
visited: set[str] = set(seed_ids)
frontier: list[str] = list(seed_ids)
Expand Down Expand Up @@ -639,7 +646,7 @@ def trace_v2(
query_edge_types = list(edge_types)
# Cross-service: also query scaffolding edges when cross-service is active.
if cross_service_active:
for scaffold_et in _SCAFFOLDING_EDGE_TYPES:
for scaffold_et in effective_scaffolding:
if scaffold_et not in query_edge_types:
query_edge_types.append(scaffold_et)

Expand Down Expand Up @@ -672,7 +679,7 @@ def trace_v2(

for row in src_rows:
et = str(row.get("edge_type") or "")
if et in _SCAFFOLDING_EDGE_TYPES:
if et in effective_scaffolding:
scaffolding_rows.append(row)
else:
signal_rows.append(row)
Expand Down Expand Up @@ -703,7 +710,7 @@ def trace_v2(
edge_type = str(row.get("edge_type") or "")

# --- Cross-service boundary detection ---
if edge_type in _SCAFFOLDING_EDGE_TYPES and cross_service_active:
if edge_type in effective_scaffolding and cross_service_active:
# Follow scaffolding edge to Client/Producer node.
# Record the scaffolding edge and include the node.
try:
Expand Down Expand Up @@ -791,9 +798,16 @@ def trace_v2(
edges.append(cross_edge)
edge_id_map[cross_edge_id] = cross_edge
visited.add(cross_target_id)
# Do NOT add downstream node to frontier — boundary-stop.

# Do NOT add Client/Producer to frontier either.
# Track incoming edge for downstream node.
if cross_target_id not in node_to_incoming_edge_id:
node_to_incoming_edge_id[cross_target_id] = cross_edge_id
# When cross_service=True, add downstream node to frontier
# so BFS continues into the downstream service.
if cross_service:
new_frontier.add(cross_target_id)

# Do NOT add Client/Producer to frontier — its cross-service
# edges were already queried inline above.
continue

# --- Standard edge processing ---
Expand Down
9 changes: 7 additions & 2 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,8 +587,8 @@ async def resolve(
"are exempt. `collapse_trivial` merges wrapper chains (A→B→C where B is trivial). "
"Result: `nodes` dict (id → NodeRef), `edges` list with BFS metadata (hop, parent_edge_id, "
"collapsed, cross_service_boundary), ranked `paths` (root-to-leaf), and `stats` with pruning counts. "
"Cross-service boundary: BFS records the cross-service edge and includes the downstream Route/Producer "
"in `nodes` but stops the frontier — the agent decides whether to continue."
"Cross-service boundary: by default BFS stops at service boundaries. "
"Set `cross_service=True` to continue traversal through HTTP_CALLS/ASYNC_CALLS boundaries."
),
)
async def trace(
Expand Down Expand Up @@ -628,6 +628,10 @@ async def trace(
default=False,
description="Include UnresolvedCallSite edges (CALLS out only)",
),
cross_service: bool = Field(
default=False,
description="Continue BFS through service boundaries (HTTP_CALLS/ASYNC_CALLS). Default: stop at boundaries.",
),
) -> mcp_trace.TraceOutput:
return await asyncio.to_thread(
mcp_trace.trace_v2,
Expand All @@ -643,6 +647,7 @@ async def trace(
fan_out_cap if fan_out_cap is not None else 5,
collapse_trivial,
include_unresolved,
cross_service,
None,
)

Expand Down
4 changes: 2 additions & 2 deletions skills/explore-codebase/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ Prefer **`resolve` -> `describe(id=...)`** over **`describe(fqn=...)`** when an
| "What happens when route R is called?" | `find(kind="route")` then `trace(route_id, "out", ["EXPOSES","CALLS"], max_depth=4)` | `describe` on key nodes |
| "Impact of changing method M" | `resolve` / `find` then `trace(id, "in", ["CALLS","OVERRIDES"], max_depth=3)` | `describe` on callers |
| "Trace from X to database" | `trace(id, "out", ["CALLS"], max_depth=4, prune_roles=["DTO","EXCEPTION"])` | `neighbors` for pruned detail |
| "What calls this across services?" | `trace(id, "out", ["CALLS","HTTP_CALLS","ASYNC_CALLS"], max_depth=5)` | `trace` on downstream route_id if needed |
| "What calls this across services?" | `trace(id, "out", ["CALLS","HTTP_CALLS","ASYNC_CALLS"], max_depth=5, cross_service=True)` | `describe` on downstream routes |

**Rules of thumb:**

Expand Down Expand Up @@ -216,7 +216,7 @@ Returns **edges** with `attrs` (`confidence`, `strategy`, `match`, ... on cross-

### `trace`

Multi-hop BFS with pruning. Args: `ids` (string or list), **`direction`**, **`edge_types`** (stored labels only — no composed dot-keys), `max_depth` (default 3, clamped 1–5), `max_paths` (default 20), `max_nodes_discovered` (default 500, clamped 100–2000), optional `filter` (NodeFilter), optional `edge_filter` (CALLS only), optional `prune_roles` (soft gate — edges recorded, frontier stops), `fan_out_cap` (default 5, scaffolding edges exempt), `collapse_trivial` (default true), `include_unresolved` (default false).
Multi-hop BFS with pruning. Args: `ids` (string or list), **`direction`**, **`edge_types`** (stored labels only — no composed dot-keys), `max_depth` (default 3, clamped 1–5), `max_paths` (default 20), `max_nodes_discovered` (default 500, clamped 100–2000), optional `filter` (NodeFilter), optional `edge_filter` (CALLS only), optional `prune_roles` (soft gate — edges recorded, frontier stops), `fan_out_cap` (default 5, scaffolding edges exempt), `collapse_trivial` (default true), `include_unresolved` (default false), `cross_service` (default false — set true to continue BFS through HTTP_CALLS/ASYNC_CALLS boundaries into downstream services).

Returns `TraceOutput`: `success`, `seed_ids`, `direction`, `edge_types`, `actual_depth`, `nodes` (dict of id→NodeRef), `edges` (list of `TraceEdge`), `paths` (list of `TracePath`), `stats` (`TraceStats`), `advisories`.

Expand Down
120 changes: 117 additions & 3 deletions tests/test_mcp_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,9 +923,123 @@ def test_trace_cross_service_boundary_stops(kuzu_graph: KuzuGraph) -> None:
assert len(downstream_edges) == 0


# ---------------------------------------------------------------------------
# PR-TRACE-2 tests: MCP tool registration
# ---------------------------------------------------------------------------
def test_trace_cross_service_seamless_http(kuzu_graph: KuzuGraph) -> None:
"""cross_service=True: BFS continues through HTTP_CALLS boundary into downstream service."""
seed_id = _find_method_with_declares_client(kuzu_graph)
if seed_id is None:
pytest.skip("No method with DECLARES_CLIENT in fixture")

out = trace_v2(
ids=seed_id,
direction="out",
edge_types=["CALLS", "HTTP_CALLS"],
max_depth=5,
fan_out_cap=0,
cross_service=True,
graph=kuzu_graph,
)
assert out.success is True

# Should have cross-service boundary edges.
xs_edges = [e for e in out.edges if e.cross_service_boundary]
if not xs_edges:
pytest.skip("No cross-service edges in result")

for xe in xs_edges:
assert xe.edge_type in ("HTTP_CALLS", "ASYNC_CALLS")
# Downstream node should be in nodes.
assert xe.to_id in out.nodes

# Key difference from boundary-stop: downstream Route should have edges FROM it
# (EXPOSES to handler, then CALLS from handler) because BFS continued through.
for xe in xs_edges:
downstream_edges = [e for e in out.edges if e.from_id == xe.to_id]
# At least one edge (EXPOSES to handler) should exist from the downstream Route.
if downstream_edges:
exposes_edges = [e for e in downstream_edges if e.edge_type == "EXPOSES"]
assert len(exposes_edges) >= 1, (
f"Expected EXPOSES edges from {xe.to_id}, got: {[e.edge_type for e in downstream_edges]}"
)


def test_trace_cross_service_seamless_async(kuzu_graph: KuzuGraph) -> None:
"""cross_service=True: BFS continues through ASYNC_CALLS boundary into downstream service."""
seed_id = _find_method_with_declares_producer(kuzu_graph)
if seed_id is None:
pytest.skip("No method with DECLARES_PRODUCER in fixture")

out = trace_v2(
ids=seed_id,
direction="out",
edge_types=["CALLS", "ASYNC_CALLS"],
max_depth=5,
fan_out_cap=0,
cross_service=True,
graph=kuzu_graph,
)
assert out.success is True

xs_edges = [e for e in out.edges if e.cross_service_boundary]
if not xs_edges:
pytest.skip("No cross-service edges in result")

for xe in xs_edges:
assert xe.edge_type in ("HTTP_CALLS", "ASYNC_CALLS")
assert xe.to_id in out.nodes


def test_trace_cross_service_seamless_respects_budget(kuzu_graph: KuzuGraph) -> None:
"""cross_service=True still respects max_nodes_discovered budget."""
seed_id = _find_method_with_declares_client(kuzu_graph)
if seed_id is None:
pytest.skip("No method with DECLARES_CLIENT in fixture")

out = trace_v2(
ids=seed_id,
direction="out",
edge_types=["CALLS", "HTTP_CALLS"],
max_depth=5,
max_nodes_discovered=100,
fan_out_cap=0,
cross_service=True,
graph=kuzu_graph,
)
assert out.success is True
# Budget may or may not have been hit depending on graph size,
# but if it was, the stats should reflect it.
if out.stats.budget_hit:
assert out.stats.total_nodes_discovered >= 100


def test_trace_cross_service_seamless_exposes_as_scaffolding(kuzu_graph: KuzuGraph) -> None:
"""EXPOSES edges from downstream Routes are exempt from fan_out_cap when cross_service=True."""
seed_id = _find_method_with_declares_client(kuzu_graph)
if seed_id is None:
pytest.skip("No method with DECLARES_CLIENT in fixture")

# Use fan_out_cap=1 — very tight, but EXPOSES should still come through.
out = trace_v2(
ids=seed_id,
direction="out",
edge_types=["CALLS", "HTTP_CALLS"],
max_depth=5,
fan_out_cap=1,
cross_service=True,
graph=kuzu_graph,
)
assert out.success is True

xs_edges = [e for e in out.edges if e.cross_service_boundary]
if xs_edges:
# Even with fan_out_cap=1, EXPOSES edges from downstream Routes should appear
# (they're scaffolding, exempt from cap).
for xe in xs_edges:
downstream_edges = [e for e in out.edges if e.from_id == xe.to_id]
if downstream_edges:
exposes_edges = [e for e in downstream_edges if e.edge_type == "EXPOSES"]
assert len(exposes_edges) >= 1, (
f"EXPOSES should be exempt from fan_out_cap, but got: {[e.edge_type for e in downstream_edges]}"
)


async def test_trace_registered_as_mcp_tool(mcp_server) -> None:
Expand Down
Loading