Skip to content

Commit 12c666e

Browse files
propose: MCP-copyable hints and 500-char cap (#195) (#197)
* add propose for hints MCP call shape (#195) Documents combo 1+2+6: MCP-copyable hint templates, agent docs for ids, bare SHA1 symbol ids, and 500-char rendered hint cap (was 120). Co-authored-by: Cursor <cursoragent@cursor.com> * resolve open questions in hints MCP call shape propose Mark all five recommendations as chosen; wire CI lint and single-PR sequencing into tests and sequencing sections. Co-authored-by: Cursor <cursoragent@cursor.com> * lower hint rendered cap to 250 in MCP call shape propose Replaces 500-char limit with 250; documents rationale vs 120 and 500. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7406d1a commit 12c666e

1 file changed

Lines changed: 217 additions & 0 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# HINTS-MCP-CALL-SHAPE — copy-safe hint strings and honest node-id docs
2+
3+
## Status
4+
5+
**Proposal** — not yet implemented.
6+
7+
**Tracks:** [issue #195](https://github.com/HumanBean17/java-codebase-rag/issues/195) (battle-test: agents copy `neighbors(['<id>'],…)` from hints → `Unknown id prefix for \`['<id>']\``).
8+
9+
**Implements fix combo:** **1** (hint templates) + **2** (agent docs + `server.py` field descriptions) + **6** (align id-shape documentation with live graph ids). Does **not** implement runtime coercion (options 3–5), structured hints (7), or host-only rules (10).
10+
11+
**Amends (catalog lock):** [`propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`](./completed/HINTS-ROAD-SIGNS-PROPOSE.md) Appendix A, **§7.6 rendered-length cap (120 → 250)**, and follow-on hint proposes (v2/v3/v4, CALLS high-fanout strings). In-flight [`propose/DESCRIBE-HINTS-STRUCTURAL-PROPOSE.md`](./DESCRIBE-HINTS-STRUCTURAL-PROPOSE.md) must adopt the same call-shape convention and **250-char** cap for any **new** `TPL_DESCRIBE_*` rows landed after this work (or land structural templates in the same PR as this propose).
12+
13+
## Problem statement
14+
15+
Road-sign hints intentionally embed a **next-call sketch** so agents can chain tools without re-reading the schema. The locked v1 catalog uses **pseudo-Python** call syntax:
16+
17+
```text
18+
routes via members: neighbors(['{id}'],'out',['DECLARES.EXPOSES'])
19+
```
20+
21+
Agents treat that sketch as the literal MCP argument shape. Two failures follow:
22+
23+
| Agent copies | FastMCP `pre_parse_json` | `neighbors_v2` sees | Result |
24+
|--------------|---------------------------|---------------------|--------|
25+
| `"ids": "['<sha1>']"` | `json.loads` fails (single quotes) → string passed through | one origin id = the literal `['<sha1>']` | `Unknown id prefix for \`['<sha1>']\`` |
26+
| `"ids": "[\"<sha1>\"]"` | parses to `list[str]` | correct | works |
27+
| `"ids": "<sha1>"` or `"ids": ["<sha1>"]` | string or list | correct | works |
28+
29+
Root cause is **not** bad graph data or wrong ids from `resolve` / `describe``record.id` is already correct. The failure mode is **hint ergonomics** plus **misleading docs** that teach `sym:`-prefixed examples while the indexer emits **bare 40-character SHA1 hex** for `Symbol.id` (and short prefixed ids for route/client/producer — see §6).
30+
31+
Secondary confusion: README and [`docs/AGENT-GUIDE.md`](../docs/AGENT-GUIDE.md) show `sym:com.bank…#method(…)` in tool examples while battle tests and brownfield corpora return unprefixed symbol ids. Agents prefix ids that do not need prefixing, or distrust bare hex as “wrong.”
32+
33+
## Proposed solution
34+
35+
### 1 — Hint template dialect (option 1)
36+
37+
Replace pseudo-Python positional/`['{id}']` forms with an **MCP-copyable named-parameter sketch** aligned with the slash-alias JSON style already in AGENT-GUIDE § “Slash-style aliases”:
38+
39+
```text
40+
routes via members: neighbors(ids="{id}", direction="out", edge_types=["DECLARES.EXPOSES"])
41+
```
42+
43+
**Normative rules for every hint emission that references `neighbors` or `resolve`:**
44+
45+
| Rule | Rationale |
46+
|------|-----------|
47+
| **Single origin** | `ids="{id}"` (string), never `ids=['{id}']` or `ids="['{id}']"`. |
48+
| **Batch / placeholder lists** | `ids=member_ids` or `ids=client_ids` (bare placeholder token), not bracket-wrapped pseudo-lists. |
49+
| **`direction` / `edge_types`** | Always named; `edge_types` uses JSON double-quoted strings inside the sketch. |
50+
| **Two-hop chains** | Keep ` then ` between two `neighbors(…)` sketches when the catalog already uses a chain; each hop obeys the same rules. Prefer **dot-key** shortcuts where they are clearer; with the 250-char cap, re-expanding a two-hop chain is allowed when it improves copy clarity (see §1b). |
51+
| **`edge_filter` / `filter` in meta hints** | Use JSON object literals with double quotes, e.g. `edge_filter={"callee_declaring_role":"SERVICE"}`, not Python `{{…}}` / single-quoted dicts. |
52+
| **`resolve` / `find` / `search` sketches** | Same convention: `resolve(identifier="{identifier}", hint_kind="{kind}")`, `search(query="{q}")` — no Python-only quoting. |
53+
| **Rendered length** | See §1b (250 chars, not 120). |
54+
55+
Update `MCP_HINTS_FIELD_DESCRIPTION` in `mcp_hints.py` to state explicitly: *hints use MCP-copyable named-parameter sketches; copy `record.id` (or `results[i].id`) into `ids`, not Python list literals; each hint is at most 250 characters after placeholder substitution.*
56+
57+
### 1b — Hint length cap: 120 → 250
58+
59+
Road-sign discipline keeps **≤ 5 hints per output** and **no prose tutorials** (HINTS-ROAD-SIGNS §2–§3). The **per-hint rendered length** limit moves from **120** to **250** characters (measured on the string **after** `{id}`, `{identifier}`, and other placeholders are substituted with realistic values — same rule as v1 §7.6, new numeric cap).
60+
61+
**Why 250 (not 120 or 500)**
62+
63+
- **120** is too tight once hints use named MCP parameters and JSON `edge_filter` literals.
64+
- **500** was rejected as excessive for road-sign discipline (~1.25k chars if all five hints fire).
65+
- **250** fits one copyable `neighbors(…)` call plus a short filter example; HINTS-V4’s ~194-char combined N1 row still fits.
66+
67+
**Why raise from 120**
68+
69+
- MCP-copyable named-parameter sketches are longer than pseudo-Python `neighbors(['{id}'],…)`.
70+
- CALLS meta hints (`TPL_NEIGHBORS_CALLS_HIGH_FANOUT`, role-collision, unresolved) were written against the 120 cap with compressed grammar; 250 allows full `edge_filter={…}` examples without drop-on-overflow.
71+
- HINTS-V4 **rejected** a combined N1 dot-key line at ~194 chars (v4 propose); that row fits under 250 if we choose to emit it (optional, not required for #195).
72+
73+
**Implementation**
74+
75+
| Item | Change |
76+
|------|--------|
77+
| `mcp_hints.py` | Replace `_FIND_SUCCESS_MAX_CHARS`, `_RESOLVE_HINT_MAX_CHARS`, `_NEIGHBORS_SUCCESS_MAX_CHARS` (all `120`) with one module constant **`HINT_MAX_RENDERED_CHARS = 250`** used by every drop-on-overflow check. |
78+
| `MCP_HINTS_FIELD_DESCRIPTION` | Mention **250** chars per hint (still max **5** hints per output). |
79+
| `tests/test_mcp_hints.py` | Rename/update `test_hints_*_under_120_chars`**`test_hints_*_under_250_chars`**; assert `len(rendered) <= 250`. Remove or rewrite tests that **require** overflow past the cap (e.g. N1a rendered length `> 120` used only to justify split hints). |
80+
| Completed proposes | Not edited on disk; this file is the amendment for §7.6. |
81+
82+
**Drop-on-overflow unchanged:** if substitution still exceeds 250 chars, the hint is **not** emitted (no truncation, no ellipsis) — same policy as v2 §7.18.
83+
84+
**Files — template source of truth**
85+
86+
| File | Change |
87+
|------|--------|
88+
| `mcp_hints.py` | All `TPL_*` constants with `neighbors(['{id}']` or positional `neighbors(…)`; v4 success templates (`client_ids`, `route_ids`, …); CALLS meta hints with `edge_filter=`; **`HINT_MAX_RENDERED_CHARS = 250`**. |
89+
| `java_ontology.py` | `EDGE_SCHEMA[*].typical_traversals` strings (feed empty-`neighbors` structural hints via `canonical_traversal`). |
90+
| `scripts/generate_edge_navigation.py` + regenerate `docs/EDGE-NAVIGATION.md` | Keep generated doc aligned with `EDGE_SCHEMA` (same PR). |
91+
92+
**Inventory (grep-driven, implementation checklist)**
93+
94+
- `mcp_hints.py`: 16+ template lines with `neighbors(['{id}']` (describe/find/v4 success) plus v4 placeholder-id rows and CALLS filter hints.
95+
- `java_ontology.py`: `_SYMBOL_TYPE_TRAVERSAL`, `_COMPOSED_MEMBER_TYPE_TRAVERSAL`, and per-edge `member_subject` / `type_subject` strings (19 occurrences of `['{id}']` pattern).
96+
- Tests asserting verbatim template text: `tests/test_mcp_hints.py` (primary), any integration tests that golden-match hint substrings.
97+
98+
### 2 — Documentation and schema descriptions (option 2)
99+
100+
Add a short, normative **“Copying hints into tool calls”** subsection to [`docs/AGENT-GUIDE.md`](../docs/AGENT-GUIDE.md) (after “JSON, not stringified JSON” or inside the `neighbors` section):
101+
102+
- Hints are **advisory**; authoritative id is always `describe.record.id`, `find.results[i].id`, `neighbors.results[i].other.id`, or `resolve.candidates[i].id`.
103+
- For `neighbors`, pass that id as **`ids` string** for one origin, or **`ids` JSON array** for batch — never a string that looks like a Python list (`"['…']"`).
104+
- FastMCP may accept a **JSON-encoded array string** (`"[\"id\"]"`); that is host behavior, not something hints should require agents to manufacture.
105+
- Point to the new hint dialect (named parameters in hint text).
106+
107+
Tighten tool-facing prose:
108+
109+
| Location | Change |
110+
|----------|--------|
111+
| `server.py``neighbors``ids` `Field(description=…)` | State copy-safe shapes; warn against Python-list string for `ids`. |
112+
| `server.py``describe``id` (if present) | One line: ids are graph-native (hex or prefixed route/client/producer). |
113+
| `MCP_HINTS_FIELD_DESCRIPTION` | As in §1. |
114+
115+
No change to MCP tool **signatures** or Pydantic models beyond `description=` strings.
116+
117+
### 6 — Honest node-id documentation (option 6)
118+
119+
Correct the public docs so examples match what the graph and tools return.
120+
121+
| Kind | Live shape (indexer / Kuzu) | Docs today (misleading) |
122+
|------|----------------------------|-------------------------|
123+
| **Symbol** | 40-char lowercase SHA1 hex (`graph_enrich.symbol_id`) | `sym:` + FQN in README / AGENT-GUIDE tables |
124+
| **Route** | `r:` + 16 hex | `route:` or `r:` (partially OK) |
125+
| **Client** | `c:` + 16 hex | `client:` or `c:` (partially OK) |
126+
| **Producer** | `p:` + 16 hex | `producer:` or `p:` (partially OK) |
127+
| **UnresolvedCallSite** | `ucs:` + … | Documented; unchanged |
128+
129+
**Doc edits**
130+
131+
- [`docs/AGENT-GUIDE.md`](../docs/AGENT-GUIDE.md) § “Node ids”: Symbol row = **bare SHA1 hex**; note `sym:` is accepted on input via prefix convention but **not** what `describe` / `find` / `neighbors` emit for symbols.
132+
- [`README.md`](../README.md) MCP tool examples: replace `sym:…ChatController#…` symbol examples with a **placeholder** bare hex (or “40-char hex from `describe.record.id`”) while keeping prefixed examples for route/client/producer where accurate.
133+
- Optional one-line in `server.py` `describe` / `neighbors` descriptions: “Symbol ids are typically 40-char hex; route/client/producer use `r:`/`c:`/`p:` prefixes.”
134+
135+
**Runtime:** No change to `_resolve_node_kind` — it already accepts bare hex and optional prefixes. This slice is documentation + hints only.
136+
137+
## Scope
138+
139+
| In scope | Out of scope (issue #195 options) |
140+
|----------|-----------------------------------|
141+
| Rewrite hint + `EDGE_SCHEMA` traversal strings to MCP-copyable form | `_coerce_ids()` / Python-bracket unwrap (3, 5) |
142+
| **250-char** rendered hint cap (replaces 120) + unified `HINT_MAX_RENDERED_CHARS` | Raising **5 hints/output** cap |
143+
| AGENT-GUIDE + README id-shape + hint-copy section | Fail-loud-only teaching error (4) without coercion |
144+
| `server.py` `Field(description=…)` updates | `hints_structured` (7) |
145+
| Regenerate `docs/EDGE-NAVIGATION.md` | Shorter hints-only labels (8) |
146+
| `tests/test_mcp_hints.py` + sentinel regression test | FastMCP `ast.literal_eval` fallback (9) |
147+
| Cross-reference in-flight DESCRIBE-HINTS-STRUCTURAL | Cursor host rules only (10) |
148+
149+
## Schema / Ontology / Re-index impact
150+
151+
- **Ontology bump:** not required.
152+
- **Re-index required:** no (string-only hint and doc changes).
153+
- **Config / tool surface:** no new tools or env vars; optional `description=` text on existing fields only.
154+
155+
## Tests / validation
156+
157+
1. **`.venv/bin/ruff check .`**
158+
2. **`.venv/bin/python -m pytest tests/test_mcp_hints.py -v`** — existing template equality tests follow `mcp_hints.TPL_*` constants; update expectations when constants change.
159+
3. **`test_hint_templates_never_use_python_list_ids`** — assert no rendered catalog template contains `(['` or `"['"` (after format with a realistic 40-char hex id).
160+
4. **`test_hint_source_no_python_bracket_ids`** — source-level sentinel: no `neighbors(['` in `mcp_hints.py` or `java_ontology.py` (decision §5).
161+
5. **Char-cap sweep:** `test_hints_template_rendered_length_leq_250` (and v4 parametrized sibling) — every catalog template renders to **≤ 250** chars with a realistic 40-char hex `{id}` (and realistic long FQN for resolve/search templates).
162+
6. **Manual smoke (PR evidence):**
163+
- `describe` on a controller from `tests/bank-chat-system` → copy `record.id` into `neighbors(ids="<id>", direction="out", edge_types=["DECLARES.EXPOSES"])` via MCP host or `mcp_v2.neighbors_v2` with `list[str]` / bare string — success.
164+
- Confirm copying the **old** hint shape `"ids": "['<id>']"` still fails (documents why we changed hints; optional post-implementation note in PR).
165+
166+
Heavy / graph rebuild tests not required.
167+
168+
## Decisions (resolved)
169+
170+
All former open questions — **chosen** 2026-05-21 (align with issue #195 recommendations).
171+
172+
| # | Question | **Chosen** |
173+
|---|----------|------------|
174+
| 1 | Hint sketch vs strict JSON object | **Named-parameter sketch** in hint text (`neighbors(ids="…", direction="out", edge_types=[…])`). AGENT-GUIDE slash-alias table keeps JSON objects for hosts that prefer a single blob. |
175+
| 2 | Single PR vs docs-first | **One implementation PR** — templates, docs, and tests land together (no docs-only lead). |
176+
| 3 | DESCRIBE-HINTS-STRUCTURAL sequencing | Whichever lands first wins; the other rebases within the same week onto §1 rules + 250-char cap. **Never** merge new `TPL_DESCRIBE_*` using `['{id}']` syntax. |
177+
| 4 | Mechanical CI lint | **Yes**`test_hint_source_no_python_bracket_ids` (no `neighbors(['` in `mcp_hints.py` / `java_ontology.py`) plus rendered-output sentinels. |
178+
| 5 | Re-merge HINTS-V4 N1a+N1b | **Out of #195 MVP** — keep N1a + N1b separate; combined dot-key line remains optional follow-up only. |
179+
180+
## Out of scope
181+
182+
- Runtime acceptance of Python-literal `ids` strings in `mcp_v2.neighbors_v2` (options 3–5) — follow-up propose if battle tests still show copy errors after 1+2+6.
183+
- Changing FastMCP or MCP host JSON pre-parse behavior.
184+
- `hints_structured: [{tool, args}]` (issue option 7).
185+
- Bumping `ontology_version` or graph builder id assignment.
186+
- Editing completed propose files in `propose/completed/` (this file is the amendment record).
187+
188+
## Sequencing / Follow-ups
189+
190+
| Step | Deliverable |
191+
|------|-------------|
192+
| **Implementation PR (one)** | Templates + `EDGE_SCHEMA` traversals + EDGE-NAVIGATION regen + AGENT-GUIDE/README/server descriptions + tests (decision §2) |
193+
| **Follow-up (optional)** | Coercion / fail-loud hybrid (issue #195 options 4–5) if telemetry still shows `['…` id strings |
194+
195+
**Suggested branch:** `cursor/hints-mcp-call-shape-195` or `feat/hints-mcp-call-shape-195`.
196+
197+
## PR body template (implementation PR)
198+
199+
```markdown
200+
## Summary
201+
- Fixes #195 (combo 1+2+6): MCP-copyable hint sketches; docs state bare SHA1 symbol ids.
202+
- Implements `propose/HINTS-MCP-CALL-SHAPE-PROPOSE.md`.
203+
204+
## Highlights
205+
- Hint templates use `ids="{id}"`, named `direction` / `edge_types` (no `['{id}']`).
206+
- Per-hint rendered cap **250** chars (`HINT_MAX_RENDERED_CHARS`; was 120).
207+
- AGENT-GUIDE + README + server Field descriptions aligned with live id shapes.
208+
- EDGE_SCHEMA / EDGE-NAVIGATION traversal strings updated to match.
209+
210+
## Test plan
211+
- [ ] pytest tests/test_mcp_hints.py
212+
- [ ] new sentinel: no `(['` in hint sources / rendered templates
213+
- [ ] manual neighbors call with id from describe (bank-chat fixture)
214+
215+
## Re-index
216+
Not required.
217+
```

0 commit comments

Comments
 (0)