From 47e778e17803d97fa9893dedd07277178bee699b Mon Sep 17 00:00:00 2001 From: Dmitry Teryaev Date: Fri, 15 May 2026 22:50:49 +0300 Subject: [PATCH] point agent docs at resolve for identifier lookups PR-RESOLVE-2: five-tool prose sweep, describe FQN hint, sentinel test. Co-authored-by: Cursor --- .cursor/rules/project-overview.mdc | 2 +- AGENTS.md | 2 +- README.md | 7 ++--- docs/AGENT-GUIDE.md | 35 +++++++++++++++--------- docs/JAVA-CODEBASE-RAG-CLI.md | 2 +- docs/MANUAL-VERIFICATION-CHECKLIST.md | 4 +-- docs/skills/java-codebase-explore.md | 5 +++- mcp_v2.py | 5 ++-- server.py | 10 +++---- tests/test_mcp_v2.py | 39 ++++++++++++++++++++++++--- 10 files changed, 79 insertions(+), 32 deletions(-) diff --git a/.cursor/rules/project-overview.mdc b/.cursor/rules/project-overview.mdc index 50ca4cc..2b6aacc 100644 --- a/.cursor/rules/project-overview.mdc +++ b/.cursor/rules/project-overview.mdc @@ -19,7 +19,7 @@ when needed. ## Where to look - `README.md` — feature surface, env vars, ranking, capabilities, - MCP tools (`search` / `find` / `describe` / `neighbors`), `java-codebase-rag` CLI, + MCP tools (`search` / `find` / `describe` / `neighbors` / `resolve`), `java-codebase-rag` CLI, "Re-index required" callouts. The current `ontology_version` is **12** (`@CodebaseHttpClient` rename + shared `CodebaseHttpMethod` enum; inbound `@CodebaseHttpRoute` replaces same-method built-in HTTP rows; still diff --git a/AGENTS.md b/AGENTS.md index 3fb982a..e1e7a8e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ for tools that don't read `.cursor/rules/`. ## Where to look - `README.md` — feature surface, env vars, ranking, capabilities, - MCP tool list (now `search` / `find` / `describe` / `neighbors`), + MCP tool list (`search` / `find` / `describe` / `neighbors` / `resolve`), CLI ops (`java-codebase-rag --help`), and "Re-index required" callouts. **`ontology_version` is currently 12** (HTTP brownfield rename + `CodebaseHttpMethod` enum + inbound HTTP layer-C replace; see README graph section). - [`docs/JAVA-CODEBASE-RAG-CLI.md`](./docs/JAVA-CODEBASE-RAG-CLI.md) — operator guide for the `java-codebase-rag` CLI (`init` / `increment` / `reprocess` / `erase`, `meta`, `tables`, `diagnose-ignore`, `analyze-pr`; hidden `refresh` alias → `reprocess` — see that doc). diff --git a/README.md b/README.md index 771bd88..1b10f1a 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ A graph-native code intelligence layer for Java microservice estates, exposed to LLM agents via the **Model Context Protocol (MCP)**. -The system extracts a deterministic property graph from Java source (tree-sitter), stores it in **Kuzu** (graph) alongside a **LanceDB** vector index (chunks), and exposes a deliberately small MCP surface — **four tools**: `search`, `find`, `describe`, `neighbors` — that collapse onto three primitive agent operations: **locate**, **inspect**, **walk**. +The system extracts a deterministic property graph from Java source (tree-sitter), stores it in **Kuzu** (graph) alongside a **LanceDB** vector index (chunks), and exposes a deliberately small MCP surface — **five tools**: `search`, `find`, `describe`, `neighbors`, `resolve` — that collapse onto three primitive agent operations: **locate**, **inspect**, **walk**. > **What this MCP is:** a **GPS for code navigation**, not a reasoning engine. > Agents use a simple loop: > -> 1. **Locate** entry nodes (`search` / `find`) +> 1. **Locate** entry nodes (`search` / `find`, or identifier-shaped **`resolve`**) > 2. **Inspect** what a node is (`describe`) > 3. **Walk** one hop at a time (`neighbors`) until enough evidence is gathered > @@ -229,7 +229,7 @@ Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/ ### Driving the MCP from an agent -- **[`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md)** — copy-paste into `QWEN.md` / `CLAUDE.md` / `AGENTS.md`. Covers the four MCP tools, the shared `NodeFilter`, the edge-type taxonomy, required `neighbors` arguments, the ontology glossary (currently **v12**), the recovery playbook, and slash-style aliases. +- **[`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md)** — copy-paste into `QWEN.md` / `CLAUDE.md` / `AGENTS.md`. Covers the five MCP tools, the shared `NodeFilter`, the edge-type taxonomy, required `neighbors` arguments, the ontology glossary (currently **v12**), the recovery playbook, and slash-style aliases. - **[`docs/skills/java-codebase-explore.md`](./docs/skills/java-codebase-explore.md)** — exploration **strategy** (missions, fallbacks, anti-capabilities, stopping rules); AGENT-GUIDE remains the **operating manual** for tool shapes and recovery. - **[`docs/MANUAL-VERIFICATION-CHECKLIST.md`](./docs/MANUAL-VERIFICATION-CHECKLIST.md)** — 7-phase agent-driven verification you run after indexing your real project. Each item has a copy-paste prompt and calibration data from `tests/bank-chat-system`. - **[`automation/cursor_propose_only/README.md`](./automation/cursor_propose_only/README.md)** — optional proposal orchestration workflow (single-command autopilot, planning bundles, and automated execution/review loops). @@ -243,6 +243,7 @@ Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/ | `search` | Locate nodes by NL/code text. | `query: str`, `table: str="java"`, `hybrid: bool=False`, `limit: int=5`, `offset: int=0`, `path_contains: str \| None`, `filter: NodeFilter \| str \| None` | `{"query":"join operator flow","limit":5}` | | `find` | Locate nodes by structured filter. | `kind: "symbol"\|"route"\|"client"`, `filter: NodeFilter \| str`, `limit: int=25`, `offset: int=0` | `{"kind":"symbol","filter":{"role":"CONTROLLER"}}` | | `describe` | Full record + edge counts for one node. For **type** symbols, `edge_summary` may include composed dot-keys (`DECLARES.DECLARES_CLIENT`, `DECLARES.EXPOSES`); for **method** symbols it may include override-axis virtual keys (`OVERRIDDEN_BY`, `OVERRIDDEN_BY.DECLARES_CLIENT`, `OVERRIDDEN_BY.EXPOSES`, `OVERRIDES`). See [`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md) (`describe`). | `id: str` | `{"id":"sym:com.bank.chat.core.api.ChatController#joinOperator(JoinOperatorRequest)"}` | +| `resolve` | Identifier-shaped node lookup (symbol / route / client). Returns `status` `one`, `many`, or `none`; prefer over `describe(fqn=…)` when an FQN may collide. See [`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md) (`resolve`). | `identifier: str`, `hint_kind: "symbol"|"route"|"client" \| null` | `{"identifier":"com.bank.chat.core.api.ChatController","hint_kind":"symbol"}` | | `neighbors` | One-hop walk. **Required**: `direction` and `edge_types`. | `ids: str \| list[str]`, `direction: "in"\|"out"`, `edge_types: list[str]`, `limit: int=25`, `offset: int=0`, `filter: NodeFilter \| str \| None` | `{"ids":"route:chat-core:POST:/chat/joinOperator","direction":"in","edge_types":["HTTP_CALLS","ASYNC_CALLS"]}` | **`NodeFilter` notes:** diff --git a/docs/AGENT-GUIDE.md b/docs/AGENT-GUIDE.md index 8c0aec1..5b7c5ba 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: -> **four** MCP navigation tools, one shared **`NodeFilter`**, edge-type +> **five** MCP navigation tools, one shared **`NodeFilter`**, edge-type > taxonomy, 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. @@ -31,7 +31,7 @@ This MCP indexes Java enterprise projects into two stores: - **LanceDB** — vector + optional hybrid (FTS + vector) search over Java / SQL / YAML chunks. - **Kuzu graph** — exact structure: **node kinds** `Symbol`, `Route`, `Client` and **nine edge types** (see *Edge taxonomy* below). -**MCP surface (navigation only):** `search`, `find`, `describe`, `neighbors`. +**MCP surface (navigation only):** `search`, `find`, `describe`, `neighbors`, `resolve`. **Operator / diagnostics (not MCP):** use the **`java-codebase-rag`** CLI — lifecycle (`init`, `increment`, `reprocess`, `erase`) plus `meta`, `tables`, `diagnose-ignore`, `analyze-pr`. Rebuilds are slow; the coding agent should not pretend it can reindex via MCP. For lifecycle commands, subprocess progress is written to **stderr** (use **`--quiet`** to suppress it); **stdout** is only the structured result payload. @@ -65,7 +65,7 @@ When a method carries **`@CodebaseHttpRoute`** or **`@CodebaseHttpClient`** (inc **Workflow (GPS model):** -1. **Locate** — `search` (natural language / fragment) or `find` (structured `NodeFilter`). +1. **Locate** — `resolve` when you hold an identifier-shaped string; `search` (natural language / fragment) or `find` (structured `NodeFilter`) for discovery. 2. **Inspect** — `describe(id)` to see the full record and `edge_summary` (per stored edge label `in`/`out` counts, plus optional composed dot-keys for type Symbols — see `describe` below). 3. **Walk** — `neighbors` in a loop with explicit **`direction`** and **`edge_types`** until you have enough evidence. Multi-hop “trace” and “impact” are **your** reasoning, not a separate tool. @@ -75,7 +75,7 @@ Before every MCP tool call, output **one short line**: ``` Q-class: -Pick: Why: <≤8 words> +Pick: Why: <≤8 words> ``` Then check *Argument shapes* (real JSON arrays/objects, required `neighbors` fields). If the call returns nothing useful, do not thrash — use the **Recovery playbook**. @@ -128,7 +128,7 @@ When reading or comparing symbols, method identity uses **FQN + signature**: - Simple type names in parentheses (`String`, `List`), generics erased (`List` → `List`). - No spaces after commas. No-arg: `()`. Constructor: `#(...)`. -Use `search` to recover the stored symbol id / FQN if you only have a simple name. +Use `resolve` when the simple name (or FQN fragment) is identifier-shaped; use `search` for fuzzy ranked discovery if you need chunk context before you have a stable id. #### D. `neighbors` — required arguments @@ -161,9 +161,13 @@ The same `http_method` key filters HTTP verbs on **routes** (server-side declare - **`search.query` is not a DSL** — treat it as opaque text scored against the index. Structured predicates belong in `find`. - **`neighbors` filters neighbor rows by kind** — the first neighbor whose kind rejects the filter fails the whole call (no per-row silent skip). -### Identifier resolution (pre-`resolve`) +### Identifier resolution -For identifier-shaped lookups without a stable graph id or exact symbol FQN, use **`search(query=…)`** for ranked candidates, then **`describe(id=…)`** (or `describe(fqn=…)` when you have an exact FQN) on each promising row until you confirm the right node. A dedicated **`resolve`** tool is planned separately; until it ships, this multi-call pattern is the supported fallback. +Use **`resolve(identifier=…, hint_kind=…)`** for identifier-shaped inputs: canonical ids (`sym:` / `route:` / `client:`), symbol FQN or suffix, HTTP `METHOD /path`, route path template, client `target_service`, or `target_service` + path prefix pair. Omit `hint_kind` to match across all three node kinds when the string is enough to scope generators. + +Branch on **`status`** in the response: **`one`** → `describe(id=…)` on the returned `node`; **`many`** → inspect `candidates` (each has a closed **`reason`**, **`score`** for display only, and a **`NodeRef`** including `microservice` when the row has one), pick one, then `describe(id=…)`; **`none`** (well-formed miss) → fall back to **`search(query=…)`** for natural-language or fuzzy discovery. Malformed empty / whitespace identifiers return `success=false` first. + +Prefer **`resolve` → `describe(id=…)`** over **`describe(fqn=…)`** when an FQN might collide: `describe(fqn=…)` still returns the first graph row and a hint, but `resolve` makes ambiguity explicit. **`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. @@ -175,12 +179,13 @@ Exact allowed values for roles, capabilities, client kinds, etc. live in `java_o | User asks… | First step | Typical follow-up | | ---------- | ---------- | ----------------- | +| Identifier-shaped string (FQN, `sym:`/ `route:` / `client:` id, route path, client target, …) | `resolve` (optional `hint_kind`) | `describe` → `neighbors` | | Fuzzy / NL “where is X” | `search` | `describe` → `neighbors` | | All controllers in service S | `find(kind="symbol", filter={"microservice":S,"role":"CONTROLLER"})` | `neighbors` for `CALLS` / `EXPOSES` | | List interfaces in service S | `find(kind="symbol", filter={"microservice":S,"symbol_kind":"interface"})` | `neighbors` / `describe` | | List HTTP or Kafka entry points | `find(kind="route", filter={...})` | `describe` | | List Feign / HTTP clients | `find(kind="client", filter={...})` | `neighbors(..., out, ["HTTP_CALLS"])` if needed | -| Who calls method M? | Resolve id via `search` or `find` | `neighbors(ids=sym_id, direction="in", edge_types=["CALLS"])` | +| Who calls method M? | Stable symbol id via `resolve`, `find`, or `search` | `neighbors(ids=sym_id, direction="in", edge_types=["CALLS"])` | | What does M call? | Same | `neighbors(..., direction="out", edge_types=["CALLS"])` | | Who hits this route? | `find(kind="route", ...)` or route id from logs | `neighbors(ids=route_id, direction="in", edge_types=["HTTP_CALLS","ASYNC_CALLS","EXPOSES"])` | | Handler for a route | Have `route_id` | `neighbors(ids=route_id, direction="in", edge_types=["EXPOSES"])` | @@ -193,10 +198,10 @@ Exact allowed values for roles, capabilities, client kinds, etc. live in `java_o **Rules of thumb:** -1. **Graph beats vector for exact structural questions** — do not `search` for “who calls `Foo#bar`” if you can use `find` + `neighbors(in, [CALLS])`. +1. **Graph beats vector for exact structural questions** — do not `search` for “who calls `Foo#bar`” if you can use `resolve` / `find` + `neighbors(in, [CALLS])`. 2. **Vector beats graph for fuzzy discovery** — `search` first, then pivot to `describe` / `neighbors`. -### Tool reference — four tools +### Tool reference — five tools #### `search` @@ -214,7 +219,7 @@ Exact allowed values for roles, capabilities, client kinds, etc. live in `java_o #### `describe` - **Purpose:** Full node payload + `edge_summary`: `in` / `out` counts **per stored graph edge label** (what exists as edges in Kuzu). For **type** Symbols only (`class`, `interface`, `enum`, `record`, `annotation`), the same map may also include **describe-time composed** dot-keys — summaries of member edges, not stored labels — see the next bullets (`DECLARES.DECLARES_CLIENT`, `DECLARES.EXPOSES`); those keys are **not** valid in `neighbors(edge_types=…)`. For **method** Symbols, the map may include **override-axis** virtual keys (`OVERRIDDEN_BY`, `OVERRIDDEN_BY.DECLARES_CLIENT`, `OVERRIDDEN_BY.EXPOSES`, `OVERRIDES`); see **Override-axis keys (method Symbols)** below — also not `EdgeType` literals. -- **Args:** `id` (symbol, route, or client id) or **`fqn`** (exact symbol FQN when you do not have the graph id). When both are set, `id` wins. For ambiguous identifiers without an exact id/FQN, see **Identifier resolution (pre-`resolve`)** above. +- **Args:** `id` (symbol, route, or client id) or **`fqn`** (exact symbol FQN when you do not have the graph id). When both are set, `id` wins. For identifier-shaped inputs and FQN collision handling, see **Identifier resolution** above. **Composed `edge_summary` keys (type Symbols).** Keys use dot notation: `.`. Two are emitted today: @@ -237,6 +242,12 @@ Static methods suppress the entire override-axis rollup. Constructors do not rec These keys are **not** valid `EdgeType` literals — `neighbors(edge_types=["OVERRIDDEN_BY"])` fails at the Pydantic boundary. Use them as hop affordances only. +#### `resolve` + +- **Purpose:** Identifier-shaped lookup across symbols, routes, and clients; returns `status` **`one`**, **`many`**, or **`none`** with optional `node` / `candidates` (see **Identifier resolution**). +- **Args:** `identifier` (string), optional `hint_kind` (`symbol` | `route` | `client`) to constrain generators. +- **Tip:** On **`many`**, use per-candidate `NodeRef.id` and `reason`; follow with **`describe(id=…)`**. On **`none`**, use **`search`** for fuzzy discovery. + #### `neighbors` - **Purpose:** One hop over explicit edge types; returns **edges** with attributes (`confidence`, `strategy`, `match`, …) and the **`other`** node. @@ -264,7 +275,7 @@ Source of truth: `java_ontology.py`. Strings are case-sensitive. | ------- | ------------ | --- | | `neighbors` validation error | Missing `direction` or `edge_types` | Add both explicitly | | Empty `neighbors` | Wrong edge type for the node kind, or wrong direction | Check `describe.edge_summary`; `EXPOSES` is Symbol↔Route — direction matters | -| Cannot find symbol | Wrong id or stale index | `search` with distinctive string; verify `java-codebase-rag meta` (CLI) | +| Cannot find symbol | Wrong id or stale index | `resolve` / `search` with distinctive string; verify `java-codebase-rag meta` (CLI) | | `find` returns too much | Over-broad filter | Add `microservice`, `fqn_prefix`, `path_prefix`, etc. | | Route not found | Path mismatch | Use `path_prefix` on `find(kind="route", …)`; check README brownfield routes | | Need ontology / rebuild / PR analysis | Wrong layer | Use **`java-codebase-rag`** CLI, not MCP | diff --git a/docs/JAVA-CODEBASE-RAG-CLI.md b/docs/JAVA-CODEBASE-RAG-CLI.md index 18e17e6..cbc6c32 100644 --- a/docs/JAVA-CODEBASE-RAG-CLI.md +++ b/docs/JAVA-CODEBASE-RAG-CLI.md @@ -1,6 +1,6 @@ # `java-codebase-rag` CLI — operator guide -The **`java-codebase-rag`** command is the **operator surface** for this bundle: index lifecycle (`init` / `increment` / `reprocess` / `erase`), graph and Lance health (`meta`, `tables`), ignore diagnostics, and PR diff analysis. It is **not** the MCP tool surface (that is `search` / `find` / `describe` / `neighbors` only). For agents driving the MCP server, see [`AGENT-GUIDE.md`](./AGENT-GUIDE.md). +The **`java-codebase-rag`** command is the **operator surface** for this bundle: index lifecycle (`init` / `increment` / `reprocess` / `erase`), graph and Lance health (`meta`, `tables`), ignore diagnostics, and PR diff analysis. It is **not** the MCP navigation surface (that is `search` / `find` / `describe` / `neighbors` / `resolve` on the MCP server — this CLI is lifecycle and introspection only). For agents driving the MCP server, see [`AGENT-GUIDE.md`](./AGENT-GUIDE.md). ## Install and discovery diff --git a/docs/MANUAL-VERIFICATION-CHECKLIST.md b/docs/MANUAL-VERIFICATION-CHECKLIST.md index 31a8992..f753c8c 100644 --- a/docs/MANUAL-VERIFICATION-CHECKLIST.md +++ b/docs/MANUAL-VERIFICATION-CHECKLIST.md @@ -4,8 +4,8 @@ Use this **after** you've read `README.md` + `CODEBASE_REQUIREMENTS.md`, [`docs/AGENT-GUIDE.md`](./AGENT-GUIDE.md), applied any brownfield annotations, and built the index against your real project. The checklist mixes **shell** checks (`java-codebase-rag` CLI for graph health and Lance tables) with **MCP** -checks (`search` / `find` / `describe` / `neighbors` — the only navigation -tools). +checks (`search` / `find` / `describe` / `neighbors` / `resolve` — the MCP +navigation tools). Each item has: diff --git a/docs/skills/java-codebase-explore.md b/docs/skills/java-codebase-explore.md index 8a37277..1b0b3fa 100644 --- a/docs/skills/java-codebase-explore.md +++ b/docs/skills/java-codebase-explore.md @@ -45,6 +45,8 @@ On any new estate, **enumerate before you search-hunt**: 2. **`find(kind="client", …)`** — list outbound clients (Feign, `RestTemplate`, Kafka, etc.) and targets. 3. Optionally **`find(kind="symbol", filter={"role":"CONTROLLER"})`** (or equivalent `NodeFilter`) to anchor web entrypoints. +**Identifier-shaped** strings (FQN, `sym:` / `route:` / `client:` id, route path, client target): start with **`resolve`**, then **`describe(id=…)`**. Use **`search`** / **`find`** for discovery when you do not have a concrete identifier yet — not as the primary chain for identifier disambiguation. + You cannot reason reliably about cross-service behaviour until these surfaces exist in your working mental model (or you have consciously fallen back to non-MCP discovery). ## Mission catalogue @@ -218,8 +220,9 @@ disagreement as evidence of staleness, not as a contradiction. ## Cheat sheet (inline reference) -Four MCP tools: +Five MCP tools: +- `resolve(identifier, hint_kind)` — identifier-shaped lookup (`one` / `many` / `none`). - `search(query, table, hybrid, limit, filter)` — fuzzy locate. - `find(kind, filter, limit)` — structured listing; `filter` is required. - `describe(id)` — full node + `edge_summary`. diff --git a/mcp_v2.py b/mcp_v2.py index b0452fc..0618cf7 100644 --- a/mcp_v2.py +++ b/mcp_v2.py @@ -734,8 +734,9 @@ def describe_v2( node_id = str(rows[0]["id"] or "") if len(rows) > 1: hint_message = ( - "multiple symbols share this FQN; narrow with find(kind='symbol', filter including " - "microservice and fqn_prefix), then describe(id=...), or use search(query=...) for ranked candidates" + "multiple symbols share this FQN; use " + f"resolve(identifier={fqn_val!r}, hint_kind='symbol') to list candidates with reasons, " + "then describe(id=...) on the chosen node" ) kind = _resolve_node_kind(g, node_id) row = _load_node_record(g, node_id, kind) diff --git a/server.py b/server.py index ca276f3..9a9ae17 100644 --- a/server.py +++ b/server.py @@ -337,9 +337,9 @@ def create_mcp_server() -> FastMCP: "results are score-ranked, not boolean-matched. Optional `filter` uses the same NodeFilter " "schema as `find` but only **symbol-applicable** fields apply (strict frame). Wildcards " "(`*`, `?`) in prefix fields are rejected—use ranked `query` text instead. There is **no** " - "structured DSL inside `query`; structured predicates belong in `find`. For " - "identifier-shaped lookups without an exact symbol id/FQN, use `search(query=…)` and " - "`describe` on promising candidates until a dedicated `resolve` tool exists." + "structured DSL inside `query`; structured predicates belong in `find`. " + "For identifier-shaped lookups (FQN, id prefix, route/client identifiers, …), use `resolve` first; " + "use `search` for natural-language or ranked fuzzy discovery." ), ) async def search( @@ -416,8 +416,8 @@ async def find( "add override-axis virtual keys (OVERRIDDEN_BY, OVERRIDDEN_BY.DECLARES_CLIENT, OVERRIDDEN_BY.EXPOSES, " "OVERRIDES). Those dot-keys are read-only summaries—not valid `neighbors(edge_types=…)` values. " "Pass `id` for any kind, or exact `fqn` for Symbol lookup (`id` wins when both are set). " - "For identifier-shaped lookups without an exact id/FQN, use `search(query=…)` then `describe` per candidate " - "until `resolve` ships." + "`describe(fqn=…)` keeps the first graph row when multiple symbols share that FQN; when an FQN may collide, " + "prefer `resolve(identifier=…, hint_kind='symbol')` first, then `describe(id=…)` on the chosen node." ), ) async def describe( diff --git a/tests/test_mcp_v2.py b/tests/test_mcp_v2.py index da3974b..201e919 100644 --- a/tests/test_mcp_v2.py +++ b/tests/test_mcp_v2.py @@ -1,5 +1,7 @@ from __future__ import annotations +import asyncio +import re from collections import Counter from typing import Any @@ -20,6 +22,22 @@ search_v2, ) +_PR2_CHAIN_SEARCH_DESCRIBE = re.compile(r"search\(query=.*\).*describe") +_PR2_SENTINEL_PATTERNS: tuple[re.Pattern[str], ...] = ( + re.compile(r"per\.candidate"), + re.compile(r"until.*resolve"), + re.compile(r"promising candidates"), + _PR2_CHAIN_SEARCH_DESCRIBE, +) + + +def _assert_no_pr2_sentinels(label: str, text: str, *, is_resolve_tool: bool) -> None: + for pat in _PR2_SENTINEL_PATTERNS: + if is_resolve_tool and pat is _PR2_CHAIN_SEARCH_DESCRIBE: + continue + match = pat.search(text) + assert match is None, f"{label}: forbidden pattern {pat.pattern!r} matched {match.group(0)!r}" + def _method_id_with_calls(kuzu_graph, direction: str) -> str: if direction == "in": @@ -702,7 +720,7 @@ def test_describe_by_fqn_id_takes_precedence(kuzu_graph) -> None: assert str(out.record.data.get("role") or "") == "SERVICE" -def test_describe_by_fqn_duplicate_returns_first_with_disambiguation_hint() -> None: +def test_describe_by_fqn_duplicate_hint_points_to_resolve() -> None: class DupFqnGraph: def _rows(self, query: str, params: dict | None = None) -> list: p = params or {} @@ -745,9 +763,22 @@ def edge_counts_for(self, node_id: str) -> dict[str, dict[str, int]]: assert out.record.id == "sym:dupe-a" assert out.message assert "multiple symbols share this FQN" in out.message - assert "find(kind='symbol'" in out.message - assert "describe(id=..." in out.message - assert "search(query=..." in out.message + assert "resolve" in out.message + assert "hint_kind" in out.message + + +def test_server_tool_descriptions_no_pre_resolve_fallback() -> None: + from server import _INSTRUCTIONS, create_mcp_server + + async def _run() -> None: + mcp = create_mcp_server() + tools = await mcp.list_tools() + _assert_no_pr2_sentinels("_INSTRUCTIONS", _INSTRUCTIONS, is_resolve_tool=False) + for tool in tools: + desc = tool.description or "" + _assert_no_pr2_sentinels(f"tool {tool.name!r}", desc, is_resolve_tool=(tool.name == "resolve")) + + asyncio.run(_run()) def test_describe_by_fqn_requires_id_or_fqn(kuzu_graph) -> None: