From 4d8d70b1e88f9b16f3fa722282e5f7ec20c77a1e Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Wed, 6 May 2026 18:45:51 +0300 Subject: [PATCH 1/3] feat: add list_clients MCP tool and client query surface (PR-LC3) Co-authored-by: Cursor --- README.md | 2 + kuzu_queries.py | 65 ++++++++++++ server.py | 60 +++++++++++ tests/test_list_clients.py | 206 +++++++++++++++++++++++++++++++++++++ tests/test_mcp_tools.py | 6 ++ 5 files changed, 339 insertions(+) create mode 100644 tests/test_list_clients.py diff --git a/README.md b/README.md index 82311d7..a58e7e2 100644 --- a/README.md +++ b/README.md @@ -167,12 +167,14 @@ The DB is dropped and rebuilt from scratch on each run (Phase 1 is a full rebuil | `diagnose_ignore` | Explain whether a path is excluded for indexing / graph walks and which rule layer won (`builtin_default`, `project_root`, `nested`, `gitignore`). | | `graph_meta` | Counts, ontology version, build timestamp, parse errors; route totals / `routes_by_framework` / `routes_resolved_pct` (v5+); `routes_from_brownfield_pct` / `routes_by_layer` (v6+). | | `list_routes` | Filterable listing of `Route` nodes (`microservice`, `framework`, `path_prefix`, `method`). | +| `list_clients` | Filterable listing of outbound `Client` nodes (`microservice`, `client_kind`, `target_service`, `path_prefix`, `method`). | | `find_route_handlers` | Endpoint symbols that `EXPOSES` a route id (confidence + resolution strategy on the edge); Feign consumer routes return empty. | | `get_route_by_path` | Lookup one `Route` by `microservice` + normalised `path_template` + optional HTTP method. | | `find_route_callers` | Callers that reach a route via `HTTP_CALLS` / `ASYNC_CALLS` (by route id or exact route tuple). | | `trace_request_flow` | Inbound caller + outbound handler flow around one route entrypoint. | HTTP mappings from literals are fully resolved (non-empty `path_template` / `path_regex`). Values containing Spring ``${…}`` SpEL, or non-string annotation arguments (constant references), are still stored as routes with lower confidence and empty template fields. Caller-side edges are now shipped via `HTTP_CALLS` / `ASYNC_CALLS` and exposed through `find_route_callers` and `trace_request_flow`. +Use `list_routes` for inbound service exposures and `list_clients` for outbound HTTP declarations (Feign methods and annotated imperative clients). `list_clients` requires graphs rebuilt with `ontology_version` 10+. **Example — `analyze_pr`:** pass the same unified diff text you would feed to `patch` (e.g. `git diff` output). Paths in the diff should match project-relative `Symbol.filename` values in the graph (e.g. `chat-assign/src/main/java/.../ChatManagementService.java`). A one-line edit inside `assign` returns JSON shaped like: diff --git a/kuzu_queries.py b/kuzu_queries.py index 7691884..d39e496 100644 --- a/kuzu_queries.py +++ b/kuzu_queries.py @@ -1377,6 +1377,71 @@ def trace_request_flow(self, entry_route_id: str, max_hops: int = 5) -> dict[str "outbound": outbound, } + # ---- outbound clients (LC3) ---- + + _CLIENT_RETURN = ( + "c.id AS id, c.client_kind AS client_kind, c.target_service AS target_service, " + "c.method AS method, c.path AS path, c.path_template AS path_template, " + "c.path_regex AS path_regex, c.member_fqn AS member_fqn, c.member_id AS member_id, " + "c.microservice AS microservice, c.module AS module, c.filename AS filename, " + "c.start_line AS start_line, c.end_line AS end_line, c.resolved AS resolved" + ) + + @staticmethod + def _row_to_client_dict(row: dict[str, Any]) -> dict[str, Any]: + return { + "id": str(row.get("id") or ""), + "client_kind": str(row.get("client_kind") or ""), + "target_service": str(row.get("target_service") or ""), + "method": str(row.get("method") or ""), + "path": str(row.get("path") or ""), + "path_template": str(row.get("path_template") or ""), + "path_regex": str(row.get("path_regex") or ""), + "member_fqn": str(row.get("member_fqn") or ""), + "member_id": str(row.get("member_id") or ""), + "microservice": str(row.get("microservice") or ""), + "module": str(row.get("module") or ""), + "filename": str(row.get("filename") or ""), + "start_line": int(row.get("start_line") or 0), + "end_line": int(row.get("end_line") or 0), + "resolved": bool(row.get("resolved", True)), + } + + def list_clients( + self, + *, + microservice: str | None = None, + client_kind: str | None = None, + target_service: str | None = None, + path_prefix: str | None = None, + method: str | None = None, + limit: int = 100, + ) -> list[dict[str, Any]]: + lim = max(1, min(int(limit), 500)) + params: dict[str, Any] = {"lim": lim} + preds: list[str] = [] + if microservice: + params["microservice"] = microservice + preds.append("c.microservice = $microservice") + if client_kind: + params["client_kind"] = client_kind + preds.append("c.client_kind = $client_kind") + if target_service: + params["target_service"] = target_service + preds.append("c.target_service = $target_service") + if path_prefix: + params["path_prefix"] = path_prefix + preds.append("c.path STARTS WITH $path_prefix") + if method is not None and method != "": + params["method"] = method + preds.append("c.method = $method") + where = (" WHERE " + " AND ".join(preds)) if preds else "" + q = ( + f"MATCH (c:Client){where} RETURN {self._CLIENT_RETURN} " + f"ORDER BY c.microservice, c.client_kind, c.path, c.method, c.id LIMIT $lim" + ) + return [self._row_to_client_dict(r) for r in self._rows(q, params)] + # ---- used by search_lancedb.graph_expand ---- def expand_fqns(self, fqns: list[str], *, depth: int = 1, diff --git a/server.py b/server.py index a40f218..f66848c 100644 --- a/server.py +++ b/server.py @@ -249,6 +249,30 @@ class RoutesListOutput(BaseModel): message: str | None = None +class ClientRowDto(BaseModel): + id: str = "" + client_kind: str = "" + target_service: str = "" + method: str = "" + path: str = "" + path_template: str = "" + path_regex: str = "" + member_fqn: str = "" + member_id: str = "" + microservice: str = "" + module: str = "" + filename: str = "" + start_line: int = 0 + end_line: int = 0 + resolved: bool = True + + +class ClientsListOutput(BaseModel): + success: bool + clients: list[ClientRowDto] = Field(default_factory=list) + message: str | None = None + + class RouteHandlerEntryDto(BaseModel): symbol: SymbolDto confidence: float = 0.0 @@ -560,6 +584,10 @@ def _route_dict_to_dto(d: dict[str, Any]) -> RouteRowDto: return RouteRowDto.model_validate(d) +def _client_dict_to_dto(d: dict[str, Any]) -> ClientRowDto: + return ClientRowDto.model_validate(d) + + def _clean_str_list(val: Any) -> list[str]: """Coerce a column value into list[str]. @@ -1029,6 +1057,38 @@ async def list_routes( ) return RoutesListOutput(success=True, routes=[_route_dict_to_dto(r) for r in rows]) + @mcp.tool( + name="list_clients", + description=( + "List outbound Client nodes from the Kuzu graph (Feign methods and " + "annotated RestTemplate/WebClient call declarations). Optional filters: " + "microservice, client_kind, target_service, path_prefix, HTTP method." + ), + ) + async def list_clients( + microservice: str | None = Field(default=None, description="Filter to one microservice key (the caller microservice)."), + client_kind: str | None = Field(default=None, description="Exact Client.client_kind match (feign_method, rest_template, web_client)."), + target_service: str | None = Field(default=None, description="Exact Client.target_service match."), + path_prefix: str | None = Field(default=None, description="Client.path STARTS WITH this string."), + method: str | None = Field(default=None, description="HTTP verb on Client.method (GET, POST, …); omit for any."), + limit: int = Field(default=100, description="Max rows to return (normalized to 1..500)."), + ) -> ClientsListOutput: + ok, graph, msg = _require_graph() + if not ok or graph is None: + return ClientsListOutput(success=False, message=msg) + lim = max(1, min(int(limit), 500)) + normalized_method = (method or "").strip().upper() + rows = await asyncio.to_thread( + graph.list_clients, + microservice=microservice, + client_kind=client_kind, + target_service=target_service, + path_prefix=path_prefix, + method=normalized_method, + limit=lim, + ) + return ClientsListOutput(success=True, clients=[_client_dict_to_dto(r) for r in rows]) + @mcp.tool( name="find_route_handlers", description=( diff --git a/tests/test_list_clients.py b/tests/test_list_clients.py new file mode 100644 index 0000000..ae3281c --- /dev/null +++ b/tests/test_list_clients.py @@ -0,0 +1,206 @@ +from __future__ import annotations + +import json +import os +import shutil +from pathlib import Path +from typing import Any + +import pytest + + +def _structured(result: Any) -> dict[str, Any]: + if isinstance(result, dict): + return result + if isinstance(result, tuple) and len(result) == 2 and isinstance(result[1], dict): + return result[1] + if hasattr(result, "__iter__"): + for block in result: + text = getattr(block, "text", None) + if isinstance(text, str): + try: + parsed = json.loads(text) + if isinstance(parsed, dict): + return parsed + except Exception: + continue + raise AssertionError(f"could not extract structured payload from {result!r}") + + +@pytest.fixture(scope="module") +def list_clients_mcp_server(tmp_path_factory): + from build_ast_graph import ( + GraphTables, + pass1_parse, + pass2_edges, + pass3_calls, + pass4_routes, + pass5_imperative_edges, + pass6_match_edges, + write_kuzu, + ) + from kuzu_queries import KuzuGraph + from server import create_mcp_server + + root = tmp_path_factory.mktemp("list_clients_graph") + stubs = Path(__file__).resolve().parent / "fixtures" / "brownfield_client_stubs" + shutil.copytree(stubs, root, dirs_exist_ok=True) + (root / "p" / "ClientSource.java").parent.mkdir(parents=True, exist_ok=True) + (root / "p" / "ClientSource.java").write_text( + ( + "package p; import com.example.rag.*; class ClientSource { " + "@CodebaseClient(clientKind=CodebaseClientKind.rest_template, targetService=\"chat-core\", path=\"/chat/joinOperator\", method=\"GET\") " + "void callA() {} " + "@CodebaseClient(clientKind=CodebaseClientKind.web_client, targetService=\"chat-assign\", path=\"/assign/run\", method=\"POST\") " + "void callB() {} }" + ), + encoding="utf-8", + ) + (root / "p" / "FeignApi.java").write_text( + ( + "package p; " + "import org.springframework.cloud.openfeign.FeignClient; " + "import org.springframework.web.bind.annotation.GetMapping; " + "@FeignClient(name=\"remote-users\", path=\"/users\") interface FeignApi { " + "@GetMapping(\"/{id}\") Object getById(String id); }" + ), + encoding="utf-8", + ) + + db_path = root / "graph.kuzu" + tables = GraphTables() + asts = pass1_parse(root, tables, verbose=False) + pass2_edges(tables, asts, verbose=False) + pass3_calls(tables, asts, verbose=False) + pass4_routes(tables, asts, source_root=root, verbose=False) + pass5_imperative_edges(tables, asts, source_root=root, verbose=False) + pass6_match_edges(tables, verbose=False) + write_kuzu(db_path, tables, source_root=root, verbose=False) + + saved_env = {k: os.environ.get(k) for k in ( + "KUZU_DB_PATH", + "LANCEDB_MCP_GRAPH_ENABLED", + "LANCEDB_URI", + "LANCEDB_MCP_PROJECT_ROOT", + )} + os.environ["KUZU_DB_PATH"] = str(db_path) + os.environ["LANCEDB_MCP_GRAPH_ENABLED"] = "1" + os.environ["LANCEDB_URI"] = str(root / "lance") + os.environ["LANCEDB_MCP_PROJECT_ROOT"] = str(root) + Path(os.environ["LANCEDB_URI"]).mkdir(parents=True, exist_ok=True) + KuzuGraph._instance = None + KuzuGraph._instance_path = None + server = create_mcp_server() + yield server + for key, value in saved_env.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + KuzuGraph._instance = None + KuzuGraph._instance_path = None + + +async def test_list_clients_returns_rows(list_clients_mcp_server) -> None: + out = _structured(await list_clients_mcp_server.call_tool("list_clients", {"limit": 50})) + assert out["success"] is True + assert out["clients"] + + +async def test_list_clients_filter_microservice(list_clients_mcp_server) -> None: + out = _structured( + await list_clients_mcp_server.call_tool( + "list_clients", + {"microservice": "p", "limit": 100}, + ) + ) + assert out["success"] is True + assert all(c["microservice"] == "p" for c in out["clients"]) + + +async def test_list_clients_filter_client_kind(list_clients_mcp_server) -> None: + out = _structured( + await list_clients_mcp_server.call_tool( + "list_clients", + {"client_kind": "feign_method", "limit": 100}, + ) + ) + assert out["success"] is True + assert all(c["client_kind"] == "feign_method" for c in out["clients"]) + + +async def test_list_clients_filter_target_service(list_clients_mcp_server) -> None: + base = _structured(await list_clients_mcp_server.call_tool("list_clients", {"limit": 200})) + assert base["success"] is True + target = next((c.get("target_service") for c in base["clients"] if c.get("target_service")), "") + assert target + out = _structured( + await list_clients_mcp_server.call_tool( + "list_clients", + {"target_service": target, "limit": 200}, + ) + ) + assert out["success"] is True + assert out["clients"] + assert all(c.get("target_service") == target for c in out["clients"]) + + +async def test_list_clients_filter_path_prefix(list_clients_mcp_server) -> None: + out = _structured( + await list_clients_mcp_server.call_tool( + "list_clients", + {"path_prefix": "/chat", "limit": 200}, + ) + ) + assert out["success"] is True + assert all((c.get("path") or "").startswith("/chat") for c in out["clients"]) + + +async def test_list_clients_filter_method(list_clients_mcp_server) -> None: + out_lower = _structured( + await list_clients_mcp_server.call_tool( + "list_clients", + {"method": "get", "limit": 200}, + ) + ) + out_upper = _structured( + await list_clients_mcp_server.call_tool( + "list_clients", + {"method": "GET", "limit": 200}, + ) + ) + assert out_lower["success"] is True + assert out_upper["success"] is True + assert out_lower["clients"] == out_upper["clients"] + assert all(c.get("method") == "GET" for c in out_lower["clients"]) + + +async def test_list_clients_empty_result_is_success_with_empty_clients(list_clients_mcp_server) -> None: + out = _structured( + await list_clients_mcp_server.call_tool( + "list_clients", + { + "microservice": "missing-service", + "target_service": "missing-target", + "path_prefix": "/definitely/missing", + }, + ) + ) + assert out["success"] is True + assert out["clients"] == [] + + +async def test_list_clients_limit_bounds_and_clamping_behavior(list_clients_mcp_server) -> None: + out_zero = _structured(await list_clients_mcp_server.call_tool("list_clients", {"limit": 0})) + out_one = _structured(await list_clients_mcp_server.call_tool("list_clients", {"limit": 1})) + out_500 = _structured(await list_clients_mcp_server.call_tool("list_clients", {"limit": 500})) + out_501 = _structured(await list_clients_mcp_server.call_tool("list_clients", {"limit": 501})) + + assert out_zero["success"] is True + assert out_one["success"] is True + assert out_500["success"] is True + assert out_501["success"] is True + assert len(out_zero["clients"]) == len(out_one["clients"]) + assert len(out_one["clients"]) <= 1 + assert len(out_501["clients"]) == len(out_500["clients"]) + assert len(out_500["clients"]) <= 500 diff --git a/tests/test_mcp_tools.py b/tests/test_mcp_tools.py index 5fa3390..109ad45 100644 --- a/tests/test_mcp_tools.py +++ b/tests/test_mcp_tools.py @@ -53,6 +53,12 @@ async def test_all_tools_have_non_empty_description(mcp_server) -> None: assert missing == [], f"Tools missing description: {missing}" +async def test_list_clients_tool_is_registered(mcp_server) -> None: + tools = await mcp_server.list_tools() + names = {tool.name for tool in tools} + assert "list_clients" in names + + # ---------------- list_code_index_tables ---------------- From 146d22879d9d2e2cc190590872b96891c3dea3de Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Wed, 6 May 2026 18:59:19 +0300 Subject: [PATCH 2/3] apply list_clients review follow-ups Co-authored-by: Cursor --- README.md | 2 +- build_ast_graph.py | 6 +++++- kuzu_queries.py | 4 +++- server.py | 1 + tests/test_list_clients.py | 19 +++++++++++-------- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a58e7e2..bf4ef12 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ The DB is dropped and rebuilt from scratch on each run (Phase 1 is a full rebuil | `trace_request_flow` | Inbound caller + outbound handler flow around one route entrypoint. | HTTP mappings from literals are fully resolved (non-empty `path_template` / `path_regex`). Values containing Spring ``${…}`` SpEL, or non-string annotation arguments (constant references), are still stored as routes with lower confidence and empty template fields. Caller-side edges are now shipped via `HTTP_CALLS` / `ASYNC_CALLS` and exposed through `find_route_callers` and `trace_request_flow`. -Use `list_routes` for inbound service exposures and `list_clients` for outbound HTTP declarations (Feign methods and annotated imperative clients). `list_clients` requires graphs rebuilt with `ontology_version` 10+. +Use `list_routes` for inbound service exposures and `list_clients` for outbound HTTP declarations (Feign methods and annotated imperative clients). `list_clients` rows include `source_layer` so brownfield-vs-builtin provenance is visible to callers. `list_clients` requires graphs rebuilt with `ontology_version` 10+. **Example — `analyze_pr`:** pass the same unified diff text you would feed to `patch` (e.g. `git diff` output). Paths in the diff should match project-relative `Symbol.filename` values in the graph (e.g. `chat-assign/src/main/java/.../ChatManagementService.java`). A one-line edit inside `assign` returns JSON shaped like: diff --git a/build_ast_graph.py b/build_ast_graph.py index 14c09c0..0919f7f 100644 --- a/build_ast_graph.py +++ b/build_ast_graph.py @@ -64,7 +64,7 @@ symbol_id, ) from path_filtering import LayeredIgnore, iter_java_source_files -from java_ontology import VALID_HTTP_CALL_MATCHES +from java_ontology import VALID_CLIENT_KINDS, VALID_HTTP_CALL_MATCHES log = logging.getLogger(__name__) @@ -1310,6 +1310,10 @@ def _client_id( def _client_source_layer(strategy: str) -> str: if strategy in {"layer_a_meta", "layer_b_ann", "layer_b_fqn", "layer_c_source"}: return strategy + # Some caller extraction paths emit client kind as strategy; treat those + # as builtin-source declarations instead of warning on every row. + if strategy in VALID_CLIENT_KINDS: + return "builtin" if strategy != "builtin": log.warning("unknown client source strategy %r, falling back to builtin", strategy) return "builtin" diff --git a/kuzu_queries.py b/kuzu_queries.py index d39e496..6eba924 100644 --- a/kuzu_queries.py +++ b/kuzu_queries.py @@ -1384,7 +1384,8 @@ def trace_request_flow(self, entry_route_id: str, max_hops: int = 5) -> dict[str "c.method AS method, c.path AS path, c.path_template AS path_template, " "c.path_regex AS path_regex, c.member_fqn AS member_fqn, c.member_id AS member_id, " "c.microservice AS microservice, c.module AS module, c.filename AS filename, " - "c.start_line AS start_line, c.end_line AS end_line, c.resolved AS resolved" + "c.start_line AS start_line, c.end_line AS end_line, c.resolved AS resolved, " + "c.source_layer AS source_layer" ) @staticmethod @@ -1405,6 +1406,7 @@ def _row_to_client_dict(row: dict[str, Any]) -> dict[str, Any]: "start_line": int(row.get("start_line") or 0), "end_line": int(row.get("end_line") or 0), "resolved": bool(row.get("resolved", True)), + "source_layer": str(row.get("source_layer") or "builtin"), } def list_clients( diff --git a/server.py b/server.py index f66848c..6d31d5b 100644 --- a/server.py +++ b/server.py @@ -265,6 +265,7 @@ class ClientRowDto(BaseModel): start_line: int = 0 end_line: int = 0 resolved: bool = True + source_layer: str = "builtin" class ClientsListOutput(BaseModel): diff --git a/tests/test_list_clients.py b/tests/test_list_clients.py index ae3281c..0aa8387 100644 --- a/tests/test_list_clients.py +++ b/tests/test_list_clients.py @@ -91,20 +91,23 @@ def list_clients_mcp_server(tmp_path_factory): KuzuGraph._instance = None KuzuGraph._instance_path = None server = create_mcp_server() - yield server - for key, value in saved_env.items(): - if value is None: - os.environ.pop(key, None) - else: - os.environ[key] = value - KuzuGraph._instance = None - KuzuGraph._instance_path = None + try: + yield server + finally: + for key, value in saved_env.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + KuzuGraph._instance = None + KuzuGraph._instance_path = None async def test_list_clients_returns_rows(list_clients_mcp_server) -> None: out = _structured(await list_clients_mcp_server.call_tool("list_clients", {"limit": 50})) assert out["success"] is True assert out["clients"] + assert all("source_layer" in c for c in out["clients"]) async def test_list_clients_filter_microservice(list_clients_mcp_server) -> None: From 03da660619f3f5ae14ee0b4891937071eeeab96f Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Wed, 6 May 2026 19:05:53 +0300 Subject: [PATCH 3/3] complete list_clients plan housekeeping; sync agent docs to ontology 10 Co-authored-by: Cursor --- .cursor/rules/agent-workflow.mdc | 2 +- .cursor/rules/project-overview.mdc | 9 +++-- .cursor/skills/plan/reference.md | 2 +- .cursor/skills/pr-open/pr-input.example.json | 2 +- .cursor/skills/propose/examples.md | 2 +- AGENTS.md | 7 ++-- CODEBASE_REQUIREMENTS.md | 5 +++ README.md | 2 +- docs/AGENT-GUIDE.md | 35 +++++++++++++++---- docs/MANUAL-VERIFICATION-CHECKLIST.md | 28 ++++++++++++--- .../CURSOR-PROMPTS-LIST-CLIENTS-MCP-TOOL.md | 28 +++++++-------- .../PLAN-LIST-CLIENTS-MCP-TOOL.md | 21 ++++------- propose/BROWNFIELD-ANNOTATIONS-V2-PROPOSE.md | 4 +-- .../LIST-CLIENTS-MCP-TOOL-PROPOSE.md | 9 ++--- 14 files changed, 98 insertions(+), 58 deletions(-) rename plans/{ => completed}/CURSOR-PROMPTS-LIST-CLIENTS-MCP-TOOL.md (89%) rename plans/{ => completed}/PLAN-LIST-CLIENTS-MCP-TOOL.md (94%) rename propose/{ => completed}/LIST-CLIENTS-MCP-TOOL-PROPOSE.md (97%) diff --git a/.cursor/rules/agent-workflow.mdc b/.cursor/rules/agent-workflow.mdc index f9998ae..6a1cc52 100644 --- a/.cursor/rules/agent-workflow.mdc +++ b/.cursor/rules/agent-workflow.mdc @@ -68,7 +68,7 @@ When you're given a per-PR task prompt from `plans/CURSOR-PROMPTS-*.md`: - Schema changes that affect the Lance index or Kuzu graph need a matching update to the README "Re-index required" callout. Bump `ontology_version` when enrichment semantics change. The current - version is **8**. + version is **10**. - Brownfield is a first-class surface: any new auto-detection (route, role, capability, http client, async producer) must compose with the matching `BrownfieldOverrides` layer. Last writer diff --git a/.cursor/rules/project-overview.mdc b/.cursor/rules/project-overview.mdc index 7707a97..32305e2 100644 --- a/.cursor/rules/project-overview.mdc +++ b/.cursor/rules/project-overview.mdc @@ -20,11 +20,10 @@ when needed. - `README.md` — feature surface, env vars, ranking, capabilities, tool list, "Re-index required" callouts. The current - `ontology_version` is **8** (HTTP_CALLS / ASYNC_CALLS caller - edges + brownfield client/producer composition + cross-service - resolution mode on GraphMeta). Earlier - ontology bumps are described inline in the README's - callouts list. + `ontology_version` is **10** (adds `Client` nodes, `DECLARES_CLIENT`, + `list_clients`, plus HTTP_CALLS / ASYNC_CALLS caller edges and + brownfield composition from earlier bumps). Earlier ontology bumps + are described inline in the README's callouts list. - `CODEBASE_REQUIREMENTS.md` — Java-repo assumptions and per-file map of what to edit when a target tree doesn't match defaults. - `tests/README.md` — testing philosophy. diff --git a/.cursor/skills/plan/reference.md b/.cursor/skills/plan/reference.md index 721832d..94d42b2 100644 --- a/.cursor/skills/plan/reference.md +++ b/.cursor/skills/plan/reference.md @@ -1,7 +1,7 @@ # Plan Style Reference (Repo-grounded) This reference distills what made recent plan PRs strong: -- `#39` (`plans/PLAN-LIST-CLIENTS-MCP-TOOL.md`) +- `#39` (`plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md`) - `#11` (`plans/PLAN-TIER1B-COMPLETION.md` + prompts file) - `#2` (`plans/PLAN-TIER1-COMPLETION.md`) diff --git a/.cursor/skills/pr-open/pr-input.example.json b/.cursor/skills/pr-open/pr-input.example.json index 82e3110..86bc73f 100644 --- a/.cursor/skills/pr-open/pr-input.example.json +++ b/.cursor/skills/pr-open/pr-input.example.json @@ -2,7 +2,7 @@ "title": "feat: retarget pass6 hint recovery to Client declarations (PR-LC2)", "base": "master", "draft": false, - "scope": "Implements PR-LC2 from `plans/PLAN-LIST-CLIENTS-MCP-TOOL.md` by retargeting pass6 hint recovery to caller-side `Client` declarations.", + "scope": "Implements PR-LC2 from `plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md` by retargeting pass6 hint recovery to caller-side `Client` declarations.", "what_changed": [ "Updated `build_ast_graph.py` pass6 fallback hint lookup to recover Feign hints from persisted `Client` rows.", "Preserved existing `HTTP_CALLS(Symbol -> Route)` semantics and match outcomes.", diff --git a/.cursor/skills/propose/examples.md b/.cursor/skills/propose/examples.md index 87e2ebe..32ef37d 100644 --- a/.cursor/skills/propose/examples.md +++ b/.cursor/skills/propose/examples.md @@ -77,7 +77,7 @@ Add a first-class outbound declaration surface: ## PR body (proposal-only) template ## What -Adds `propose/LIST-CLIENTS-MCP-TOOL-PROPOSE.md` describing outbound client declarations and a new `list_clients` MCP tool. +Adds `propose/completed/LIST-CLIENTS-MCP-TOOL-PROPOSE.md` describing outbound client declarations and a new `list_clients` MCP tool. ## Why now Outbound declaration discovery needs a first-class tool after direction-honest annotation reshaping. diff --git a/AGENTS.md b/AGENTS.md index ae2807c..bdf4f5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ for tools that don't read `.cursor/rules/`. - `README.md` — feature surface, env vars, ranking, capabilities, tool list, "Re-index required" callouts. **`ontology_version` is - currently 8.** + currently 10** (`Client` nodes + `list_clients`; see README callouts). - `CODEBASE_REQUIREMENTS.md` — Java-repo assumptions and tuning map. - `propose/` and `plans/` (plus their `completed/` subdirs) — in-flight scope and the rationale behind current design. @@ -35,7 +35,10 @@ for tools that don't read `.cursor/rules/`. `plans/completed/PLAN-BROWNFIELD-ROLE-OVERRIDES-design-fixes.md`, `plans/completed/PLAN-COCOINDEX-SYMLINK-FIX.md`, `plans/completed/PLAN-ENUM-ANNOTATION-FIXES.md`, - `plans/completed/PLAN-REMOTE-PROJECT-INDEXING.md`. Read these + `plans/completed/PLAN-REMOTE-PROJECT-INDEXING.md`, + `propose/completed/LIST-CLIENTS-MCP-TOOL-PROPOSE.md`, + `plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md`, + `plans/completed/CURSOR-PROMPTS-LIST-CLIENTS-MCP-TOOL.md`. Read these when you need the *why* behind current code. - `tests/README.md` — testing philosophy. diff --git a/CODEBASE_REQUIREMENTS.md b/CODEBASE_REQUIREMENTS.md index 44cec36..2cc5562 100644 --- a/CODEBASE_REQUIREMENTS.md +++ b/CODEBASE_REQUIREMENTS.md @@ -186,6 +186,11 @@ root (`role_overrides:`, `route_overrides:`, `http_client_overrides:`, `@CodebaseProducers` (method-level outbound HTTP / messaging) — see README §3c. +**MCP discovery:** after indexing, use `list_routes` for inbound HTTP and async +routes and `list_clients` for outbound HTTP `Client` declarations (Feign +methods plus annotated imperative clients). `list_clients` requires +`graph_meta.ontology_version` **10** or newer. + See **Brownfield overrides** in `README.md` for the full schema, usage examples, and execution order. diff --git a/README.md b/README.md index bf4ef12..1034edc 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The product vision for this tooling is proposed in [`propose/PRODUCT-VISION.md`] > **Driving this MCP from an agent:** > - [`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md) — copy-paste-into-`QWEN.md` / > `CLAUDE.md` block. Forced reasoning preamble, decision tree, full -> reference for all 22 tools, ontology glossary (v10), recovery playbook, +> reference for all 23 tools, ontology glossary (v10), recovery playbook, > slash-style aliases. Engineered for weak / mid models that otherwise > pick the wrong tool. > - [`docs/MANUAL-VERIFICATION-CHECKLIST.md`](./docs/MANUAL-VERIFICATION-CHECKLIST.md) diff --git a/docs/AGENT-GUIDE.md b/docs/AGENT-GUIDE.md index 78fb2fb..25291ea 100644 --- a/docs/AGENT-GUIDE.md +++ b/docs/AGENT-GUIDE.md @@ -3,7 +3,7 @@ > **How to use this file.** Copy the block between the `` markers below into your project's `QWEN.md`, > `CLAUDE.md`, `AGENTS.md`, or equivalent. The block is self-contained: -> all 22 MCP tools, the ontology glossary (v9), a forced reasoning +> all 23 MCP tools, the ontology glossary (v10), a forced reasoning > preamble, a decision tree, a recovery playbook, and slash-style prompt > aliases. Update by re-pulling from this repo when the ontology bumps. > @@ -12,7 +12,8 @@ > graph already knows exactly. This guide is engineered to keep them on > the rails. > -> Calibrated against ontology version **9** (see `java_ontology.py`). +> Calibrated against ontology version **10** (see `ast_java.ONTOLOGY_VERSION` / +> `java_ontology.py` valid sets). --- @@ -178,6 +179,7 @@ the `path` field from a result. | "What does method M call" | `find_callees` | `graph_neighbors` for type wiring | | "Show me the handler for HTTP path /foo/bar" | `get_route_by_path` then `find_route_handlers` | `trace_request_flow` | | "List all HTTP endpoints / Kafka topics" | `list_routes` (filter by `framework`) | `find_route_handlers` per id | +| "List outbound HTTP clients / Feign methods for this service" | `list_clients` (filter by `client_kind`, `target_service`, `path_prefix`) | `find_route_callers` for resolved call edges | | "Who calls route /foo/bar" | `find_route_callers` | `trace_request_flow` | | "All controllers / services / repositories in service X" | `list_by_role` | `list_by_role` + `capability=` filter | | "Everything annotated `@Transactional`" | `list_by_annotation` | `find_callers` per result | @@ -199,7 +201,7 @@ the `path` field from a result. work" should start with `codebase_search` (or `trace_flow`); the graph alone won't surface the right entry point. -### Tool reference — all 22 tools +### Tool reference — all 23 tools Grouped by purpose. Required arguments are **bold**; common mistakes are flagged with ⚠. @@ -273,6 +275,18 @@ flagged with ⚠. classify — usually annotation-only Kafka topic constants. If you expected an HTTP route here, check brownfield overrides. +##### `list_clients` — list outbound `Client` nodes (Feign, imperative HTTP) + +- **Args:** none required. Optionals: `microservice`, `client_kind` + (`feign_method`|`rest_template`|`web_client`), `target_service`, + `path_prefix`, `method`, `limit` (1–500). +- Returns rows with `path`, `path_template`, `member_fqn`, `source_layer` + (`builtin` vs brownfield layers), and other fields from the graph. Pair + with `find_route_callers` when you need **resolved** `HTTP_CALLS` edges, + not just declarations. +- ⚠ Requires a graph built with `ontology_version` **10+** — check + `graph_meta` first. + ##### `find_route_handlers` — symbols that EXPOSES a Route id - **Args:** **`route_id`** (e.g. `r:0a2bdd…`). @@ -368,7 +382,7 @@ flagged with ⚠. ##### `graph_meta` — Kuzu metadata: counts, ontology version, build timestamp - **Args:** none. First tool to run on a fresh index — confirms - `ontology_version=9` and surfaces build counts. + `ontology_version=10` and surfaces build counts. ##### `diagnose_ignore` — explain why a path is ignored @@ -382,7 +396,7 @@ flagged with ⚠. `LANCEDB_MCP_ALLOW_REFRESH=1`. - ⚠ Always call `graph_meta` after to verify the rebuild succeeded. -### Ontology glossary (version 9) +### Ontology glossary (version 10) Source of truth: `java_ontology.py`. Pass these strings verbatim (case-sensitive). @@ -393,7 +407,7 @@ Source of truth: `java_ontology.py`. Pass these strings verbatim `CLIENT`, `MAPPER`, `DTO`, `OTHER`. - `CLIENT` covers Feign clients (`@FeignClient`) and brownfield - `@CodebaseRole(CLIENT)`. As of ontology 9, plain `RestTemplate` + `@CodebaseRole(CLIENT)`. As of ontology 10, plain `RestTemplate` wrappers stay in their natural stereotype role (typically `SERVICE`) unless you explicitly tag them. - `OTHER` = the inference didn't recognise the type. Treat as a @@ -421,6 +435,11 @@ Source of truth: `java_ontology.py`. Pass these strings verbatim `http_endpoint`, `kafka_topic`, `rabbit_queue`, `jms_destination`, `stream_binding`. +#### Client node kind (`Client` rows / `list_clients`) + +`feign_method`, `rest_template`, `web_client` (`VALID_CLIENT_KINDS` in +`java_ontology.py`). + #### Client kind (on `HTTP_CALLS` / `ASYNC_CALLS` edges) `feign_method`, `rest_template`, `web_client`, `kafka_send`, @@ -447,6 +466,7 @@ Source of truth: `java_ontology.py`. Pass these strings verbatim | `path_template` filter returns nothing | Passed the raw annotation value, but the graph stores the concatenated servlet form | Run `list_routes({"path_prefix":"/your/prefix"})` and copy the exact `path` field, then retry | | Tool says "graph unavailable" | Index not built or `LANCEDB_MCP_PROJECT_ROOT` not set | Run `graph_meta` to confirm; `refresh_code_index({"confirm":true})` if needed | | Expected route is missing from `list_routes` | Framework not recognised by built-in extractor | Add `@CodebaseHttpRoute(path=…, method=…)` or `@CodebaseAsyncRoute(topic=…)` per README §3b, then `refresh_code_index` | +| `list_clients` returns no rows / errors | Stale graph (ontology below 10) or no outbound clients in index | Run `graph_meta`; rebuild with `refresh_code_index` if needed; tag call sites with `@CodebaseClient` per README §3c | | `list_by_role` shows a `*Controller` class as `OTHER` | Non-Spring web stack (JAX-RS, custom) | Add `@CodebaseRole(CodebaseRoleKind.CONTROLLER)` per README §3a, or `role_overrides.fqn` in YAML | | `cross_service_calls_total = 0` but you know there are inter-service calls | Resolution mode is `brownfield_only` and call sites have no brownfield tag, OR target services unindexed | Switch to `cross_service_resolution: auto` in YAML, or tag with `@CodebaseClient` | | `codebase_search` returns DTOs / config classes instead of behaviour | Default ranking; no role filter | Add `exclude_roles=["DTO","ENTITY","CONFIG","OTHER"]` | @@ -465,6 +485,7 @@ shorthand for the right tool + args. - `/who-calls ` → `find_callers({"fqn_or_signature":"","depth":1,"min_confidence":0.9})`. **Pass the full signed FQN** (e.g. `com.foo.Bar#baz(String,int)`) — see *Argument shapes §B* for format. If you only have the simple name, query that first and re-issue with the exact FQN. - `/calls-from ` → `find_callees({"fqn_or_signature":"","depth":1})`. Same FQN-with-signature rule — simple name will match all overloads but not let you target one. - `/route [microservice]` → `list_routes({"path_prefix":"","method":"","microservice":""})` +- `/clients [microservice]` → `list_clients({"microservice":"","limit":100})` — add `client_kind` / `path_prefix` when narrowing Feign vs imperative HTTP - `/handler ` → `find_route_handlers({"route_id":""})` - `/who-hits ` → `find_route_callers({"microservice":"","path_template":""})` - `/why-no-route ` → 1) `list_by_role({"role":"OTHER"})` to confirm the type wasn't classified, 2) `list_by_annotation` for any custom annotation, 3) suggest brownfield `@CodebaseHttpRoute` / `@CodebaseAsyncRoute` @@ -489,7 +510,7 @@ shorthand for the right tool + args. ## Maintenance notes (for the repo, not the agent) - Bump the **ontology version** sentence at the top of the BEGIN block - whenever `ONTOLOGY_VERSION` changes in `kuzu_queries.py`. + whenever `ONTOLOGY_VERSION` changes in `ast_java.py`. - When a new MCP tool is added in `server.py`, add it to (a) the decision tree, (b) the tool reference, (c) a slash alias if the use case is common. diff --git a/docs/MANUAL-VERIFICATION-CHECKLIST.md b/docs/MANUAL-VERIFICATION-CHECKLIST.md index dc25dc1..20341e3 100644 --- a/docs/MANUAL-VERIFICATION-CHECKLIST.md +++ b/docs/MANUAL-VERIFICATION-CHECKLIST.md @@ -199,10 +199,11 @@ stubs. No `*Service` / `*Repository` classes in OTHER. 0 `@FeignClient` classes (it uses RestTemplate); on real projects, counts should match exactly. -**If failing → fix:** as of ontology 9 (PR-H1), `@FeignClient` → +**If failing → fix:** as of ontology 9+ (PR-H1), `@FeignClient` → `role=CLIENT` + `capability=HTTP_CLIENT`. If you see drift, run -`graph_meta` and confirm `ontology_version=9`. If yes and still -broken, re-index — may be a stale graph. +`graph_meta` and confirm `ontology_version` is current (10 as of the +`Client` / `list_clients` work). If yes and still broken, re-index — may +be a stale graph. ### 2.4 ☐ Message listeners and producers are detected @@ -254,7 +255,7 @@ your custom stereotypes. --- -## Phase 3 — Routes (4 items) +## Phase 3 — Routes and outbound clients (5 items) ### 3.1 ☐ Route count and framework distribution @@ -328,6 +329,22 @@ literal name of, use brownfield route override: `@CodebaseAsyncRoute(topic="my.topic")` on the listener method (README §3b). +### 3.5 ☐ Outbound HTTP clients surface via `list_clients` + +**Verification prompt:** + +> Call `graph_meta()` and note `ontology_version` and `clients_total`. +> Then `list_clients({"limit":200})`. Every row should include +> `client_kind`, `target_service`, `path`, `method`, and `source_layer`. + +**Expected (calibration):** `ontology_version=10`, `clients_total=2`, and +both clients are `client_kind=rest_template` (imperative HTTP in the +fixture — no Feign interfaces). + +**If failing → fix:** `ontology_version` below 10 → full rebuild. Zero +clients on a project you know has Feign or tagged `@CodebaseClient` → +check brownfield stubs (README §3c) and that those sources are indexed. + ### Red flags for Phase 3 - `routes_total = 0` → no controllers were classified or framework not @@ -335,7 +352,8 @@ on the listener method (README §3b). - HTTP routes with empty `method` → annotation extractor didn't see `@GetMapping` / `@PostMapping` - `routes_from_brownfield_pct` jumped after a refactor → you broke a - built-in extraction; check that ontology version is still 9 + built-in extraction; check that `ontology_version` in `graph_meta` + matches the bundle you expect (10 as of `Client` / `list_clients`) --- diff --git a/plans/CURSOR-PROMPTS-LIST-CLIENTS-MCP-TOOL.md b/plans/completed/CURSOR-PROMPTS-LIST-CLIENTS-MCP-TOOL.md similarity index 89% rename from plans/CURSOR-PROMPTS-LIST-CLIENTS-MCP-TOOL.md rename to plans/completed/CURSOR-PROMPTS-LIST-CLIENTS-MCP-TOOL.md index 3dfcc6d..53f65f9 100644 --- a/plans/CURSOR-PROMPTS-LIST-CLIENTS-MCP-TOOL.md +++ b/plans/completed/CURSOR-PROMPTS-LIST-CLIENTS-MCP-TOOL.md @@ -1,7 +1,7 @@ # Cursor task prompts — list_clients MCP tool (PR-LC1 → PR-LC3) -Status: **active (ready for implementation)**. Generated from -`plans/PLAN-LIST-CLIENTS-MCP-TOOL.md`. +Status: **completed** (LC1–LC3 landed). Source plan: +`plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md`. Use these prompts in landing order: **LC1 -> LC2 -> LC3**. If prompt text conflicts with plan text, the plan wins. @@ -12,12 +12,12 @@ If prompt text conflicts with plan text, the plan wins. **Branch:** `feat/list-clients-lc1-client-schema` off `master`. **Base:** `master`. -**Plan section:** `plans/PLAN-LIST-CLIENTS-MCP-TOOL.md` § `PR-LC1 - Client schema + extraction + persistence`. +**Plan section:** `plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md` § `PR-LC1 - Client schema + extraction + persistence`. **Estimated diff size:** ~4-5 files, ~350-700 LOC. **Attach (`@-files`):** -- `@plans/PLAN-LIST-CLIENTS-MCP-TOOL.md` -- `@propose/LIST-CLIENTS-MCP-TOOL-PROPOSE.md` +- `@plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md` +- `@propose/completed/LIST-CLIENTS-MCP-TOOL-PROPOSE.md` - `@README.md` - `@build_ast_graph.py` - `@ast_java.py` @@ -28,7 +28,7 @@ If prompt text conflicts with plan text, the plan wins. **Prompt:** ```` -You are implementing PR-LC1 from `plans/PLAN-LIST-CLIENTS-MCP-TOOL.md`. +You are implementing PR-LC1 from `plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md`. Read the PR-LC1 section first. The plan is the source of truth. ## Scope @@ -93,12 +93,12 @@ Expected: targeted tests pass; full suite passes. **Branch:** `feat/list-clients-lc2-pass6-client-hints` off `feat/list-clients-lc1-client-schema` (or `master` if LC1 merged). **Base:** PR-LC1 merged (or LC1 branch). -**Plan section:** `plans/PLAN-LIST-CLIENTS-MCP-TOOL.md` § `PR-LC2 - pass6 hint recovery migration to Client`. +**Plan section:** `plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md` § `PR-LC2 - pass6 hint recovery migration to Client`. **Estimated diff size:** ~2-3 files, ~180-350 LOC. **Attach (`@-files`):** -- `@plans/PLAN-LIST-CLIENTS-MCP-TOOL.md` -- `@propose/LIST-CLIENTS-MCP-TOOL-PROPOSE.md` +- `@plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md` +- `@propose/completed/LIST-CLIENTS-MCP-TOOL-PROPOSE.md` - `@README.md` - `@build_ast_graph.py` - `@kuzu_queries.py` @@ -107,7 +107,7 @@ Expected: targeted tests pass; full suite passes. **Prompt:** ```` -You are implementing PR-LC2 from `plans/PLAN-LIST-CLIENTS-MCP-TOOL.md`. +You are implementing PR-LC2 from `plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md`. Read the PR-LC2 section first. The plan is the source of truth. ## Scope @@ -167,12 +167,12 @@ Expected: targeted tests pass; full suite passes. **Branch:** `feat/list-clients-lc3-mcp-surface` off `feat/list-clients-lc2-pass6-client-hints` (or latest merged predecessor). **Base:** PR-LC2 merged (or LC2 branch). -**Plan section:** `plans/PLAN-LIST-CLIENTS-MCP-TOOL.md` § `PR-LC3 - list_clients MCP tool + query surface + docs`. +**Plan section:** `plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md` § `PR-LC3 - list_clients MCP tool + query surface + docs`. **Estimated diff size:** ~3-4 files, ~220-450 LOC. **Attach (`@-files`):** -- `@plans/PLAN-LIST-CLIENTS-MCP-TOOL.md` -- `@propose/LIST-CLIENTS-MCP-TOOL-PROPOSE.md` +- `@plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md` +- `@propose/completed/LIST-CLIENTS-MCP-TOOL-PROPOSE.md` - `@README.md` - `@server.py` - `@kuzu_queries.py` @@ -182,7 +182,7 @@ Expected: targeted tests pass; full suite passes. **Prompt:** ```` -You are implementing PR-LC3 from `plans/PLAN-LIST-CLIENTS-MCP-TOOL.md`. +You are implementing PR-LC3 from `plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md`. Read the PR-LC3 section first. The plan is the source of truth. ## Scope diff --git a/plans/PLAN-LIST-CLIENTS-MCP-TOOL.md b/plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md similarity index 94% rename from plans/PLAN-LIST-CLIENTS-MCP-TOOL.md rename to plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md index 4eba3bb..801509a 100644 --- a/plans/PLAN-LIST-CLIENTS-MCP-TOOL.md +++ b/plans/completed/PLAN-LIST-CLIENTS-MCP-TOOL.md @@ -1,13 +1,10 @@ # Plan: `list_clients` MCP tool + `Client` graph node -Status: **active (planning)**. This plan implements -[`propose/LIST-CLIENTS-MCP-TOOL-PROPOSE.md`](../propose/LIST-CLIENTS-MCP-TOOL-PROPOSE.md) -as a multi-PR sequence. It is intentionally **plan-only** and does not -implement code. +Status: **completed — shipped via PR-LC1 → PR-LC3** (merged 2026-05). Pairs with +[`propose/completed/LIST-CLIENTS-MCP-TOOL-PROPOSE.md`](../propose/completed/LIST-CLIENTS-MCP-TOOL-PROPOSE.md). -Depends on: brownfield annotations v2 (the data-shape move that separates -outbound client declarations from `Route` rows). This plan assumes that -dependency is merged first. +Depends on: brownfield annotations v2 (outbound client declarations separated +from `Route` rows), merged before LC1. ## Goal @@ -241,10 +238,6 @@ Existing tool-suite test update: # Tracking -When each PR lands, append status notes here: - -- `PR-LC1`: _pending_ -- `PR-LC2`: _pending_ -- `PR-LC3`: _pending_ - -Once all are shipped, move this file to `plans/completed/`. +- `PR-LC1`: merged (Client schema + extraction + persistence). +- `PR-LC2`: merged (pass6 hint recovery on `Client`). +- `PR-LC3`: merged (`list_clients` MCP tool + query surface + docs/tests). diff --git a/propose/BROWNFIELD-ANNOTATIONS-V2-PROPOSE.md b/propose/BROWNFIELD-ANNOTATIONS-V2-PROPOSE.md index bb6dab6..7c80ef1 100644 --- a/propose/BROWNFIELD-ANNOTATIONS-V2-PROPOSE.md +++ b/propose/BROWNFIELD-ANNOTATIONS-V2-PROPOSE.md @@ -304,7 +304,7 @@ that resolve to a remote endpoint should still produce a `Client` projection (or a new column on `Symbol`) to store the client-kind metadata that previously lived on the `http_consumer` `Route`. **Detailed design for the `Client` projection is a -separate proposal (see `propose/LIST-CLIENTS-MCP-TOOL-PROPOSE.md`).** +separate proposal (see `propose/completed/LIST-CLIENTS-MCP-TOOL-PROPOSE.md`).** ### `list_routes` no longer returns Feign rows @@ -415,7 +415,7 @@ failure. propose.** That's a follow-up — the propose covers only the annotation shape and the resolver flow change. The persistence shape for outbound client metadata is in - `propose/LIST-CLIENTS-MCP-TOOL-PROPOSE.md`. + `propose/completed/LIST-CLIENTS-MCP-TOOL-PROPOSE.md`. - **No source-stub Maven dependency.** Stubs remain copy-paste source files; simple-name matching is preserved. - **No infer-default for `clientKind`.** Rejected — keep field diff --git a/propose/LIST-CLIENTS-MCP-TOOL-PROPOSE.md b/propose/completed/LIST-CLIENTS-MCP-TOOL-PROPOSE.md similarity index 97% rename from propose/LIST-CLIENTS-MCP-TOOL-PROPOSE.md rename to propose/completed/LIST-CLIENTS-MCP-TOOL-PROPOSE.md index e7ac5ab..afc71d4 100644 --- a/propose/LIST-CLIENTS-MCP-TOOL-PROPOSE.md +++ b/propose/completed/LIST-CLIENTS-MCP-TOOL-PROPOSE.md @@ -2,10 +2,11 @@ ## Status -Proposal — depends on the brownfield annotations v2 propose -(`propose/BROWNFIELD-ANNOTATIONS-V2-PROPOSE.md`). This propose -defines a new MCP tool plus the persistence shape it queries; the -v2 annotations propose creates the data the tool consumes. +**Completed** — landed via PR-LC1, PR-LC2, and PR-LC3 (2026-05). Depended on +the brownfield annotations v2 propose +(`propose/BROWNFIELD-ANNOTATIONS-V2-PROPOSE.md`). This propose defined the MCP +tool plus the persistence shape it queries; the v2 annotations propose created +the data the tool consumes. ## Problem Statement