Files
2026-07-13 12:42:18 +08:00

185 lines
5.6 KiB
Python

"""MCP tool wrappers for graph analysis features."""
from __future__ import annotations
from typing import Any
from ..analysis import (
find_bridge_nodes,
find_hub_nodes,
find_knowledge_gaps,
find_surprising_connections,
generate_suggested_questions,
)
from ._common import _get_store
def get_hub_nodes_func(
repo_root: str | None = None,
top_n: int = 10,
) -> dict[str, Any]:
"""Find the most connected nodes in the codebase graph.
Hub nodes have the highest total degree (in + out edges).
These are architectural hotspots -- changes to them have
disproportionate blast radius.
Args:
repo_root: Repository root (auto-detected if omitted).
top_n: Number of top hubs to return (default 10).
"""
store, _root = _get_store(repo_root or None)
try:
hubs = find_hub_nodes(store, top_n=top_n)
return {
"hub_nodes": hubs,
"count": len(hubs),
"next_tool_suggestions": [
"get_impact_radius -- check blast radius of a hub",
"query_graph callers_of -- see what calls a hub",
"get_bridge_nodes -- find architectural chokepoints",
],
}
finally:
store.close()
def get_bridge_nodes_func(
repo_root: str | None = None,
top_n: int = 10,
) -> dict[str, Any]:
"""Find architectural chokepoints via betweenness centrality.
Bridge nodes sit on the shortest paths between many node
pairs. If they break, multiple code regions lose
connectivity.
Args:
repo_root: Repository root (auto-detected if omitted).
top_n: Number of top bridges to return (default 10).
"""
store, _root = _get_store(repo_root or None)
try:
bridges = find_bridge_nodes(store, top_n=top_n)
return {
"bridge_nodes": bridges,
"count": len(bridges),
"next_tool_suggestions": [
"get_hub_nodes -- find most connected nodes",
"get_impact_radius -- check blast radius",
"detect_changes -- see if bridges are affected",
],
}
finally:
store.close()
def get_knowledge_gaps_func(
repo_root: str | None = None,
) -> dict[str, Any]:
"""Identify structural weaknesses in the codebase.
Finds: isolated nodes (disconnected), thin communities
(< 3 members), untested hotspots (high-degree, no tests),
and single-file communities.
Args:
repo_root: Repository root (auto-detected if omitted).
"""
store, _root = _get_store(repo_root or None)
try:
gaps = find_knowledge_gaps(store)
total = sum(len(v) for v in gaps.values())
return {
"gaps": gaps,
"total_gaps": total,
"summary": {
"isolated_nodes": len(gaps["isolated_nodes"]),
"thin_communities": len(
gaps["thin_communities"]
),
"untested_hotspots": len(
gaps["untested_hotspots"]
),
"single_file_communities": len(
gaps["single_file_communities"]
),
},
"next_tool_suggestions": [
"refactor dead_code -- find unused symbols",
"get_hub_nodes -- find high-impact nodes",
"get_suggested_questions -- review prompts",
],
}
finally:
store.close()
def get_surprising_connections_func(
repo_root: str | None = None,
top_n: int = 15,
) -> dict[str, Any]:
"""Find unexpected architectural coupling in the codebase.
Scores edges by surprise factors: cross-community,
cross-language, peripheral-to-hub, cross-test-boundary.
Args:
repo_root: Repository root (auto-detected if omitted).
top_n: Number of top surprises to return (default 15).
"""
store, _root = _get_store(repo_root or None)
try:
surprises = find_surprising_connections(
store, top_n=top_n
)
return {
"surprising_connections": surprises,
"count": len(surprises),
"next_tool_suggestions": [
"get_architecture_overview -- community structure",
"query_graph callers_of -- trace the coupling",
"get_bridge_nodes -- find chokepoints",
],
}
finally:
store.close()
def get_suggested_questions_func(
repo_root: str | None = None,
) -> dict[str, Any]:
"""Auto-generate review questions from graph analysis.
Produces questions about: bridge nodes, untested hubs,
surprising connections, thin communities, and untested
hotspots.
Args:
repo_root: Repository root (auto-detected if omitted).
"""
store, _root = _get_store(repo_root or None)
try:
questions = generate_suggested_questions(store)
by_priority: dict[str, list[dict[str, Any]]] = {
"high": [], "medium": [], "low": [],
}
for q in questions:
prio = q.get("priority", "medium")
if prio in by_priority:
by_priority[prio].append(q)
return {
"questions": questions,
"count": len(questions),
"by_priority": {
k: len(v) for k, v in by_priority.items()
},
"next_tool_suggestions": [
"get_knowledge_gaps -- structural weaknesses",
"detect_changes -- risk-scored review",
"get_architecture_overview -- community map",
],
}
finally:
store.close()