Skip to main content

ADR-026: Tool-call best-practices conformance

The Decision section is deliberately unfilled. This ADR measures, researches and argues. The disposition is the owner's, as with ADR-021 and ADR-023.

Context and Problem Statementโ€‹

ADR-023 asked which of the tools should exist and cut the supported surface to 54. This ADR asks a different, orthogonal question about the tools that survive: do the tool calls themselves conform to current MCP practice?

The distinction matters because the two are independent. A tool can be the right tool to keep (ADR-023) and still expose a poor call contract โ€” no typed output, no behaviour hints, discoverable only through a mechanism the spec now assigns to the host. Removing tools does not fix the calls that remain.

The full evidence base is docs/reference/tool-surface-map.md.

Measured, on main at 2026-09-02โ€‹

Every figure is reproducible by the command beside it.

measurementvaluehow
tools over the wire72MCP_TOOL_SCHEMAS.length (src/tools/mcp-tool-schemas.ts), served by getToolListForMCP({ mode: 'full' }) at src/mcp-adr-analysis-server.ts:235
tools declared in TOOL_CATALOG68rg -c "^TOOL_CATALOG.set\('" src/tools/tool-catalog.ts (72 at runtime โ€” see next row)
wire tools backfilled into the catalog4get_gaps, search_codebase, set_project_path, update_knowledge โ€” folded into TOOL_CATALOG at load by the loop at tool-catalog.ts:2103 with default utility metadata
tools declaring outputSchema0rg -n "outputSchema" src โ†’ only prompt-composition.ts:232-255 (unrelated)
tools declaring any MCP annotation0rg -c "readOnlyHint|destructiveHint|idempotentHint|openWorldHint" src
CE-MCP directive tools1212 case entries in ce-mcp-tools.ts:1406-1447; hasCEMCPDirective: true ร—12 in the catalog
host-native tools marked deprecated8deprecated: true in tool-catalog.ts + [DEPRECATED host-native, ADR-023] on their wire descriptions (#1639)
tools/list payload~94,571 B / ~24K tokensmeasured in ADR-023:38
categories in search_tools enum10 of 11mcp-tool-schemas.ts:19-30 (inside getSearchToolsDefinition) omits aggregator

Decision Driversโ€‹

  • The spec moved and this server predates it. The 2026-07-28 Client Best Practices assign progressive discovery to the host and describe programmatic ("code mode") calling that needs outputSchema. This server ships neither contract.
  • The tool count is above Anthropic's own selection-accuracy band. Anthropic's guidance (Writing effective tools for agents, Tool search tool) is that Claude's ability to pick the right tool degrades past ~30โ€“50 available tools; under ~10 the full list is fine; above the band, defer to host-native tool search rather than a custom one. This server exposes 72; ADR-023's supported 54 still sits at the top of the band, and only consolidating the deferred clusters (ADR-024) brings it comfortably inside.
  • Zero output schemas opts the server out of code mode. The spec is explicit: "the real fix is for server authors to provide outputSchema." With none, every host generating typed stubs falls back to any.
  • Zero annotations hides safety-relevant behaviour. read_file is read-only; write_file and smart_git_push are destructive. A host cannot tell without readOnlyHint/destructiveHint, so it must treat all calls as equally risky.
  • The lightweight listing exists and is already wired in โ€” it is just configured to full. getToolListForMCP({mode}) (src/tools/tool-dispatcher.ts:139-171) is the live tools/list source (#1416): src/mcp-adr-analysis-server.ts:235 calls it. But it passes mode: 'full', returning all 72 full MCP_TOOL_SCHEMAS, when the same function already offers a mode: 'lightweight' path (:151-166) that would emit search_tools plus name/category stubs. The oversized wire payload is therefore a one-line configuration choice, not dead code โ€” a stronger finding than the earlier draft's (which wrongly called getToolListForMCP unused).
  • Catalog integrity is broken in two ways. Four wire tools (get_gaps, search_codebase, set_project_path, update_knowledge) carry no explicit catalog entry; a backfill loop (tool-catalog.ts:2103) folds them into TOOL_CATALOG at load with default utility metadata, so search_tools does surface them โ€” but mis-categorised and under-described, and get_gaps duplicates the properly catalogued analyze_gaps. Separately, the search_tools category filter silently omits aggregator, leaving its 10 tools unfilterable by category.

The spec movedโ€‹

Client Best Practices (2026-07-28) recommends switching to progressive discovery once tool definitions consume 1โ€“5% of the context window and notes OpenAI and Anthropic ship tool search natively. At ~24K tokens this server is 3โ€“12ร— that threshold โ€” the same finding ADR-023 used to remove search_tools/load_prompt from the supported surface. It also describes code mode, where the host builds a typed API from outputSchema; this server declares none.

Zero annotationsโ€‹

The Tools spec defines readOnlyHint, destructiveHint, idempotentHint, openWorldHint and title as optional behaviour hints (clients treat them as untrusted unless from a verified source). None appear anywhere in src/. The clearest cost is safety legibility: destructive tools (write_file, smart_git_push, sync_to_aggregator) are indistinguishable from read-only ones at the protocol level.

Considered Optionsโ€‹

  1. Status quo. Keep 72 full-schema tools, no outputSchema, no annotations, a custom search_tools. For: no work. Against: forces the progressive-discovery mitigation on every host, opts out of code mode, and leaves the catalog bugs.

  2. Full conformance pass on the survivors. Add outputSchema and annotations to the supported set, flip the wire listing from mode: 'full' to mode: 'lightweight' (the path already exists), fix the aggregator enum omission, and give the 4 backfilled tools proper catalog entries (dedupe get_gaps/analyze_gaps). For: closes every gap; unblocks code mode. Against: largest effort; outputSchema for ~54 tools is real work, not mechanical.

  3. Schemas + annotations only; defer discovery to the host. Add outputSchema + annotations, fix the catalog bugs, but do not build more discovery machinery โ€” rely on host-native tool search. For: highest value per unit effort; aligns with the spec putting discovery on the host. Against: leaves the oversized wire payload until ADR-023's removals land.

  4. Defer discovery entirely to the host and drop search_tools/load_prompt. Fold the discovery meta-tools into ADR-023's removal track and lean on host-native search. For: smallest long-term surface; stops reimplementing a host feature. Against: couples this ADR to ADR-023's deprecation/retirement timeline; a host without native tool search loses discovery until it catches up.

Consequencesโ€‹

Good (whichever of 2โ€“4 is chosen)

  • Tools gain typed outputs (code mode becomes possible) and safety-legible annotations.
  • The catalog stops lying by omission โ€” every wire tool is discoverable and every category filterable.

Bad / cost

  • outputSchema across the surviving tools is per-tool design work, not a codemod.
  • Switching the wire listing changes what every connected host sees; it interacts with prompt caching (adding/removing tool defs mid-conversation invalidates the cached tools prefix) and must be validated against real clients.

Neutral

  • This ADR does not change which tools exist (ADR-023) or consolidate the deferred clusters (ADR-024). It is about the call contract of whatever survives.

Decisionโ€‹

Confirmationโ€‹

Verifiable now:

  • Every measurement in the table reproduces via the command beside it.
  • The host-native removal track (ADR-023) has already moved one step: the 8 host-native tools carry deprecated: true and a [DEPRECATED host-native, ADR-023] marker on their wire descriptions (#1639). The next step is per-asset retirement.py, not another edit here.
  • bash scripts/check-adr-drift.sh stays at its baseline with this ADR added.

Deliberately not claimed: that any tool is safe to remove, or that adding outputSchema/annotations is mechanical. Execution of any chosen option is separate work, admitted on its own, and โ€” for anything that removes a tool โ€” gated by retirement.py per ADR-023.

More Informationโ€‹