Skip to content
Open
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
5 changes: 4 additions & 1 deletion graphify/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -1875,7 +1875,10 @@ def _tool_god_nodes(arguments: dict) -> str:
from graphify.analyze import god_nodes as _god_nodes
nodes = _god_nodes(G, top_n=int(arguments.get("top_n", 10)))
lines = ["God nodes (most connected):"]
lines += [f" {i}. {n['label']} - {n['degree']} edges" for i, n in enumerate(nodes, 1)]
lines += [
f" {i}. {sanitize_label(n['label'])} - {n['degree']} edges"
for i, n in enumerate(nodes, 1)
]
return "\n".join(lines)

def _tool_graph_stats(_: dict) -> str:
Expand Down
38 changes: 38 additions & 0 deletions tests/test_serve_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,44 @@ def test_normal_tool_result_is_not_iserror(tmp_path):
})
assert resp.status_code == 200
assert not resp.json()["result"].get("isError")


def test_god_nodes_sanitizes_labels_over_mcp(tmp_path):
"""The MCP tool must not expose control characters or unbounded labels."""
malicious_label = "hub\x00\x1f\x7f" + "x" * 300
graph = {
"directed": True,
"nodes": [
{"id": "hub", "label": malicious_label, "community": 0, "source_file": "hub.py"},
{"id": "ordinary", "label": "ordinary", "community": 0, "source_file": "ordinary.py"},
{"id": "leaf-a", "label": "leaf-a", "community": 0, "source_file": "leaf_a.py"},
{"id": "leaf-b", "label": "leaf-b", "community": 0, "source_file": "leaf_b.py"},
{"id": "leaf-c", "label": "leaf-c", "community": 0, "source_file": "leaf_c.py"},
],
"edges": [
{"source": "hub", "target": "ordinary", "relation": "calls", "confidence": "EXTRACTED"},
{"source": "hub", "target": "leaf-a", "relation": "calls", "confidence": "EXTRACTED"},
{"source": "hub", "target": "leaf-b", "relation": "calls", "confidence": "EXTRACTED"},
{"source": "hub", "target": "leaf-c", "relation": "calls", "confidence": "EXTRACTED"},
{"source": "ordinary", "target": "leaf-a", "relation": "calls", "confidence": "EXTRACTED"},
],
}
graph_path = tmp_path / "graph.json"
graph_path.write_text(json.dumps(graph), encoding="utf-8")

app = serve_mod._build_http_app(str(graph_path), json_response=True)
with _client(app) as client:
text = _call_tool(client, _init_session(client), "god_nodes", {"top_n": 2}, rid=2)

clean_label = "hub" + "x" * 253
assert text.splitlines() == [
"God nodes (most connected):",
f" 1. {clean_label} - 4 edges",
" 2. ordinary - 2 edges",
]
assert all(control not in text for control in ("\x00", "\x1f", "\x7f"))


def _ambiguous_graph_file(tmp_path: Path) -> str:
"""A graph where the label 'extract' matches two nodes in different files."""
graph = {
Expand Down