feat: add list_clients MCP tool and client query surface (PR-LC3)#44
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
Review: PR-LC3 —
|
| Sentinel | Status |
|---|---|
ONTOLOGY_VERSION bump |
✅ 0 hits — correctly untouched |
CREATE NODE TABLE / CREATE REL TABLE |
✅ 0 hits — no schema delta |
pass5 / pass6 modifications |
✅ 2+2 hits, all in the new test fixture builder (calling existing pass functions, no behavioural edits) |
ClientRow / DeclaresClientRow dataclass touches |
✅ 0 hits |
_client_id (LC1 deterministic-id contract) |
✅ 0 hits — id contract preserved |
find_route_callers |
✅ 2 hits, both in README context lines describing existing tool table |
Real-code touch is exactly the LC3 plan §165-189 set:
kuzu_queries.py:1377-1442—list_clientshelper +_CLIENT_RETURNprojection +_row_to_client_dictserver.py:251-272—ClientRowDto,ClientsListOutputPydantic modelsserver.py:1057-1090—@mcp.tool(name="list_clients")registration with method normalization and limit clampingREADME.md— tool table row + directional callout + reindex requirementtests/test_list_clients.py— 8 plan-mandated teststests/test_mcp_tools.py:53-58—test_list_clients_tool_is_registeredregistration smoke
Plan compliance
| # | Plan item (PR-LC3 §) | Verified |
|---|---|---|
| 1 | kuzu_queries.py query helper with optional filters: microservice, client_kind, target_service, path_prefix, method, limit |
✅ all six on KuzuGraph.list_clients(); path_prefix correctly translates to c.path STARTS WITH $path_prefix Cypher predicate |
| 2 | Deterministic ordering and limit enforcement | ✅ ORDER BY c.microservice, c.client_kind, c.path, c.method, c.id LIMIT $lim — five-key tuple; lim = max(1, min(int(limit), 500)) clamps below the query parameter bind |
| 3 | server.py adds ClientRowDto and ClientsListOutput |
✅ both defined at server.py:252-272; field set matches Client schema from PR-LC1 minus source_layer (intentional — see observation 4 below) |
| 4 | Register @mcp.tool(name="list_clients", ...) |
✅ at server.py:1057-1090; description names all five filter axes |
| 5 | Method normalization | ✅ normalized_method = (method or "").strip().upper() at the tool boundary (server.py:1085) — test test_list_clients_filter_method proves "get" and "GET" produce identical results |
| 6 | Safe default limit=100, bounded 1..500 |
✅ both in the tool signature (limit: int = Field(default=100, ...)) and re-clamped at the helper layer (max(1, min(int(limit), 500))) — defense in depth |
| 7 | Empty results return success with empty list (not an error) | ✅ test_list_clients_empty_result_is_success_with_empty_clients asserts success=True, clients=[] for a triple-AND-impossible filter |
| 8 | README documents new tool + outbound/inbound split + reindex callout | ✅ tool table row added at README.md:170; directional sentence at README.md:175 ("list_routes for inbound service exposures and list_clients for outbound HTTP declarations"); ontology v10 prerequisite called out inline |
| 9 | All 8 plan-mandated tests | ✅ all present with exact names: test_list_clients_returns_rows, test_list_clients_filter_microservice, test_list_clients_filter_client_kind, test_list_clients_filter_target_service, test_list_clients_filter_path_prefix, test_list_clients_filter_method, test_list_clients_empty_result_is_success_with_empty_clients, test_list_clients_limit_bounds_and_clamping_behavior |
| 10 | Existing tool-suite smoke updated | ✅ tests/test_mcp_tools.py::test_list_clients_tool_is_registered added |
Tests
311 passed, 4 skipped in 79.87s
Exactly +9 over PR-LC2's 302 baseline (8 filter/limit tests + 1 registration smoke). All green locally.
Manual evidence reproduced
Built graph against tests/bank-chat-system and queried via list_clients:
total: 2
by_kind: {'rest_template': 2}
by_micro: {'chat-assign': 1, 'chat-core': 1}
sample row: {'id': 'c:0d35b35467cebb06', 'client_kind': 'rest_template', 'target_service': '',
'method': 'POST', 'path': '', ...
'microservice': 'chat-assign', 'module': 'chat-assign', ...
'member_fqn': 'com.bank.chat.assign.integration.ChatCoreJoinClient#joinOperator(...)',
'member_id': 'fd7df239339e3c95ee1e810e9fb596a189336308',
'resolved': False}
filter client_kind=feign_method → 0 (correct, no Feign synthesized in this fixture)
filter path_prefix=/chat → 0 (correct, paths empty for these imperative rows)
limit=0 → 1 row (correctly clamped to 1)
limit=600 → 2 rows (correctly clamped to 500, but only 2 exist)
c: id prefix carries through from PR-LC1's deterministic-id contract ✅. member_id linkage round-trips through the persisted graph ✅. Limit clamping verified in both directions (lower and upper) ✅.
Notes that earned my trust
- Deterministic ordering tuple (5 keys:
microservice, client_kind, path, method, id) — explicitly tie-breaks onid, the SHA1-derived stable identifier from LC1. Two rows with identical(microservice, client_kind, path, method)would otherwise have non-deterministic order; theidtie-breaker prevents flakey golden-file tests downstream. - Limit clamping is defence-in-depth — clamped at the Pydantic boundary in the tool signature and at the helper's parameter bind (
lim = max(1, min(int(limit), 500))). Caller bypassing the MCP shell still gets the same bound. path_prefixusesSTARTS WITH $paramparameter binding, not string interpolation — Cypher injection-safe.- Method normalization at the tool boundary, not the helper — keeps the helper's contract honest (caller-side normalization in tests can bypass it; the test suite's
_lower vs _uppercomparison is what guarantees normalization is wired in the production path). _row_to_client_dictis defensive on every field:str(row.get("id") or ""),int(row.get("start_line") or 0),bool(row.get("resolved", True)). Kuzu rarely returnsNonefor non-nullable columns but the dict-coercion idiom matches_row_to_route_dict(consistency with the existing route-side code).ClientsListOutputfailure mode returnssuccess=False, message=msgwhen_require_graph()fails — symmetric withRoutesListOutput. Empty-but-graph-present case correctly still returnssuccess=True, clients=[].- Test fixture builder builds the graph from scratch (full pass1-6 + write_kuzu) rather than synthesising rows directly — true integration coverage including pass5/pass6 idempotency.
- Test fixture creates both
@CodebaseClientsource-layer rows AND a@FeignClientinterface sofeign_methodandrest_template/web_clientfilter paths are both exercisable. The bank-chat-system manual run only hadrest_template; this fixture is broader.
Observations (non-blocking)
-
Pre-existing warning surfaces during graph build:
unknown client source strategy 'rest_template', falling back to builtin(printed twice during bank-chat-system rebuild). I confirmed this also reproduces on master (02a4c0c), so it's not introduced by this PR. It's the same_client_source_layerwhitelist-vs-strategy issue from PR-LC1's observation Add per-PR Cursor task prompts for Tier 1 completion #4: whenpass5_imperative_edgesresolves a strategy ofrest_template(aclient_kind, not asource_layer), the LC1 whitelist{layer_a_meta, layer_b_ann, layer_b_fqn, layer_c_source, builtin}doesn't match and the row getsbuiltin. Worth a follow-up PR to either (a) extend the whitelist with theclient_kind-as-strategy synonyms or (b) silence the warning when the strategy is a knownclient_kind. Out of scope for LC3. -
Client.source_layeris not exposed throughClientRowDto— present on the underlying schema (LC1) but not in_CLIENT_RETURNprojection or the DTO. Possibly intentional to keep the public surface narrow, but agents usinglist_clientsto debug brownfield-override behaviour will find it useful. Consider adding in a follow-up PR alongsideconfidence(currently onDECLARES_CLIENTedge but not surfaced anywhere). Note that the testtest_client_source_layer_reflects_winning_override_layer(LC1) still locks the persisted value, so this is purely a public-API gap. -
Limit clamping returns
min=1forlimit=0instead of0. The plan says "bounded1..500" so this matches spec. But agents that passlimit=0to mean "metadata-only / count" will get one row back unexpectedly. The semantics are documented in the field description ("normalized to 1..500") so caller can adapt; mentioning here only for visibility. -
descriptiontext on the tool says "Feign methods and annotated RestTemplate/WebClient call declarations" — this matches PR-LC1'sCodebaseClientKindenum{feign_method, rest_template, web_client}. If a future kind lands, this string drifts. Trivially fixable; flag for whoever extends the enum. -
KuzuGraph._instancereset in fixture teardown is good (prevents singleton bleed) but doesn't wrap intry/finally— if a test panics mid-fixture-yield, env vars stay polluted. Same pattern as existingtests/test_*files in this repo, so consistent rather than regressive.
Plan deltas needed
None. PR-LC3 done-criteria all met:
- ✅
list_clientstool registered and callable - ✅ Filter behaviour test-covered and stable
- ✅ Docs updated for outbound discovery entry point and v10 reindex requirement
- ✅ Full test suite green (311 passed, 4 skipped)
The whole-plan done definition (plans/PLAN-LIST-CLIENTS-MCP-TOOL.md §233) is also now satisfied:
- ✅ PR-LC1/LC2/LC3 ready to merge in order
- ✅ Graph contains
Clientnodes andDECLARES_CLIENTedges with deterministic ids - ✅ pass6 hint recovery reads
Clientdeclaration metadata (LC2) - ✅
list_clientstool available with full filter coverage - ✅
README.mdand ontology/reindex callouts updated consistently - ✅
pytest tests -vpasses
Ready to merge. The list_clients plan is done after this lands. After merge, mark plans/PLAN-LIST-CLIENTS-MCP-TOOL.md §242 "Tracking" block with the three PR-LCx merge commits and move the file to plans/completed/ per the plan's own §250 directive. Suggested follow-up PRs (not blockers):
_client_source_layerwhitelist extension to silence theunknown client source strategy 'rest_template'warning (or a separate strategy-synonym map).- Surface
Client.source_layerthroughClientRowDtofor brownfield-debug agents. - The four companion tools and
Producerprojection sketched inpropose/LIST-CLIENTS-MCP-TOOL-PROPOSE.md(still punted to follow-up proposals per plan §227-229).
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
KuzuGraph.list_clientsand deterministic filterable query semantics (microservice,client_kind,target_service,path_prefix,method,limit).list_clientsinserver.pywithClientRowDto/ClientsListOutput, method normalization, and bounded limit behavior (100default, clamped to1..500).README.mdand add LC3 test coverage (tests/test_list_clients.pyplus tool-registration smoke intests/test_mcp_tools.py).Test plan
ruff check .pytest tests/test_list_clients.py -vpytest tests/test_mcp_tools.py -vpytest tests -vMade with Cursor