From 08b01ef465de961ce6a0001a3c2c5e6e72fdac56 Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Thu, 14 May 2026 23:12:40 +0300 Subject: [PATCH] rename NodeFilter client_method to shared http_method Co-authored-by: Cursor --- docs/AGENT-GUIDE.md | 8 +++++++- mcp_v2.py | 7 +++---- tests/test_mcp_v2.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/docs/AGENT-GUIDE.md b/docs/AGENT-GUIDE.md index 9ded241..2045ed5 100644 --- a/docs/AGENT-GUIDE.md +++ b/docs/AGENT-GUIDE.md @@ -150,7 +150,13 @@ One object shape everywhere. **For `find`, `filter` is required** — use at lea | `microservice`, `module`, `source_layer` | All kinds (`source_layer` mainly **client**: `builtin` / brownfield) | | `role`, `exclude_roles`, `annotation`, `capability`, `fqn_prefix`, `symbol_kind`, `symbol_kinds` | **symbol** (ignored for route/client) | | `http_method`, `path_prefix`, `framework` | **route** | -| `client_kind`, `target_service`, `target_path_prefix`, `client_method` | **client** | +| `client_kind`, `target_service`, `target_path_prefix`, `http_method` | **client** | + +The same `http_method` key filters HTTP verbs on **routes** (server-side declared method) and on **clients** (caller-side method on the outbound call). It is not applicable to **symbol** rows. + +**`source_layer` vs `role`:** On **Client** nodes, `source_layer` records which brownfield or built-in layer produced the client declaration (`builtin`, `layer_a_meta`, `layer_b_ann`, `layer_c_source`, `layer_b_fqn`, …). On **Symbol** nodes, `role` is the inferred architectural stereotype (`CONTROLLER`, `SERVICE`, `REPO`, …). They answer different questions; names stay distinct. + +**`target_service` vs `microservice`:** `microservice` is the service **where the node lives** (home service / owning module). `target_service` (clients only) is the **remote service being called**. A client in `operator-api` may list `partner-api` as `target_service`. Exact allowed values for roles, capabilities, client kinds, etc. live in `java_ontology.py`. diff --git a/mcp_v2.py b/mcp_v2.py index 27fe570..6ac32fe 100644 --- a/mcp_v2.py +++ b/mcp_v2.py @@ -75,7 +75,6 @@ class NodeFilter(BaseModel): client_kind: str | None = None target_service: str | None = None target_path_prefix: str | None = None - client_method: str | None = None _NODEFILTER_FIELD_ORDER: tuple[str, ...] = tuple(NodeFilter.model_fields.keys()) @@ -106,7 +105,7 @@ class NodeFilter(BaseModel): "client_kind", "target_service", "target_path_prefix", - "client_method", + "http_method", ), } @@ -475,7 +474,7 @@ def _node_matches_filter(kind: Literal["symbol", "route", "client"], row: dict[s path = str(row.get("path") or "") if not path.startswith(f.target_path_prefix): return False - if f.client_method and str(row.get("method") or "") != f.client_method: + if f.http_method and str(row.get("method") or "") != f.http_method: return False return True @@ -581,7 +580,7 @@ def find_v2( client_kind=nf.client_kind, target_service=nf.target_service, path_prefix=nf.target_path_prefix, - method=nf.client_method, + method=nf.http_method, limit=max(500, limit + offset), ) rows = [r for r in rows if _node_matches_filter("client", r, nf)] diff --git a/tests/test_mcp_v2.py b/tests/test_mcp_v2.py index e1bfc5c..797daf2 100644 --- a/tests/test_mcp_v2.py +++ b/tests/test_mcp_v2.py @@ -238,6 +238,43 @@ def test_nodefilter_applicability_table_covers_all_fields() -> None: assert declared == covered +def test_http_method_field_applies_to_route_kind(kuzu_graph) -> None: + routes = kuzu_graph.list_routes(limit=2000) + post_ids = {str(r["id"]) for r in routes if str(r.get("method") or "").upper() == "POST"} + if not post_ids: + pytest.skip("fixture has no POST routes") + out = find_v2("route", {"http_method": "POST"}, graph=kuzu_graph, limit=500) + assert out.success is True + assert out.results + assert {r.id for r in out.results} <= post_ids + assert all(r.fqn == "POST" or r.fqn.startswith("POST ") for r in out.results) + + +def test_http_method_field_applies_to_client_kind(kuzu_graph) -> None: + clients = kuzu_graph.list_clients(limit=2000) + post_ids = {str(r["id"]) for r in clients if str(r.get("method") or "").upper() == "POST"} + if not post_ids: + pytest.skip("fixture has no POST clients") + out = find_v2("client", {"http_method": "POST"}, graph=kuzu_graph, limit=500) + assert out.success is True + assert out.results + assert {r.id for r in out.results} <= post_ids + + +def test_http_method_field_inapplicable_to_symbol(kuzu_graph) -> None: + out = find_v2("symbol", {"http_method": "POST"}, graph=kuzu_graph) + assert out.success is False + assert out.message is not None + assert "http_method" in out.message + assert "kind='symbol'" in out.message + + +def test_nodefilter_rejects_old_client_method_field() -> None: + with pytest.raises(ValidationError) as excinfo: + NodeFilter.model_validate({"client_method": "POST"}) + assert any("client_method" in str(e.get("loc", ())) for e in excinfo.value.errors()) + + async def test_find_missing_filter_rejected(mcp_server) -> None: with pytest.raises(ToolError, match="Field required"): await mcp_server.call_tool("find", {"kind": "symbol"})