perf(mcp): demote ten unused catalog reads, reclaiming 3 763 tokens of budget (#2089)

* perf(mcp): demote ten unused catalog reads, reclaiming 3 763 tokens of budget

The tools/list payload bench sat about 20 tokens under its 65 000 ceiling, so
the next tool or field anyone added failed CI. That is not hypothetical: it
already cost the agent-briefing field in #2079, which was built and then
removed for want of roughly 85 tokens.

The bench's own comments deferred the fix twice, in the same words both times:
picking which read to demote needs prod usage data, not a guess inside an
unrelated PR, so do the demotion as its own change and ratchet the ceiling back
down. This is that change.

Selection is from 60 days of mcp.tool_called: default-catalog READ tools with
25 or fewer calls, excluding anything named in RECOMMENDED_WORKFLOW_LOADOUTS.
Ten qualified. Measured 65 046 -> 61 283, ceiling ratcheted to 61 600.

Usage data is necessary but not sufficient, and three classes were kept in the
catalog despite low counts:

- gnubok_call_tool, which IS the search-only bridge.
- the connect_* onboarding tools and gnubok_lookup_company. A fresh agent has
  not learned to search yet, which is exactly why #1936 put them in the catalog.
- every arsredovisning, dispositioner, depreciation and accrual PROPOSAL tool.
  A 60-day window ending in August cannot see bokslut season: most Swedish
  companies close on 31 December and do the work January to June, so summer
  counts understate these to near zero. Demoting them would hide the year-end
  flow precisely when it is needed.

Widget tools are a hard exclusion, and I found that by tripping over it:
gnubok_receipt_matcher and gnubok_vat_review_widget were demoted in a first
pass and their own suites failed. A widget is rendered from the _meta.ui it
publishes in tools/list, so search-only leaves it callable but unrenderable.
Both are back in the catalog, and a general invariant now holds the rule for
every widget tool added later, verified by demoting one and watching it fail.

Only READ tools are demotable at all: gnubok_call_tool refuses writes, so a
search-only WRITE is uncallable on Claude.ai. Demoted reads stay callable
through the bridge; they are no longer listed up front.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: code-span the connect_* identifier in DECISIONS

A bare asterisk opens an emphasis span in Markdown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-31 23:01:17 +02:00
committed by GitHub
co-authored by Claude Opus 5 Jakob Wennberg
parent 37e50c272d
commit b643e6ce64
3 changed files with 64 additions and 1 deletions
+1
View File
@@ -1414,4 +1414,5 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-08-31] Connector usage metering redacts opaque path segments to ':id' before insert (skeptic refutation on PR #1751): the proxied bank paths carry the raw EB session id and account uid as segments, so persisting the raw pathname in connector_usage_events put the cleartext handle next to the ledger that exists precisely to store only sha256(handle). redactEndpoint() replaces UUID/long-hex/long-base64url segments; literal route words survive so metering keys stay useful.
[2026-08-31] Bank-proxy review batch (PR #1751): EB base URL must be https (JWT in Authorization; lazy check so a bad env 500s the request, never the build); forwardToEb reads the body inside the abort-timeout window (headers-then-stall no longer wedges the request); the per-company quota uses the pending row as a reservation (pre-count, insert, re-count, roll back own row on loss) with fresh-pending rows holding quota for the 15-min consent window, closing the concurrent-auth TOCTOU without a new RPC; DELETE revokes the ledger row only on upstream success or 404 (a transient EB error no longer strands a live remote session unreachable).
[2026-08-31] POST /sessions binds the code exchange to its state's own pending row (Superagent P1 on PR #1751): verified signature + key/service match + existing pending row are preconditions for calling EB, and a concurrent consumption of the same state after exchange closes the upstream session and answers 409 instead of handing out a session the ledger never recorded. Full code-to-state binding at the callback (recording a code hash on the pending row) would need a column and is deferred; the pre-exchange binding plus one-shot pending->active activation removes the cross-key and stateless-exchange paths, and same-key crossover between an instance's own concurrent flows only relabels its own sessions.
[2026-08-31] Ten rarely-called READ tools demoted to catalogVisibility 'search', reclaiming 3 763 tokens of the tools/list context budget (65 046 -> 61 283) and ratcheting the bench ceiling 65 000 -> 61 600. This is the demotion the payload bench deferred twice, explicitly, for want of prod usage data. Picked from 60 days of mcp.tool_called: default-catalog reads with <= 25 calls, excluding anything in RECOMMENDED_WORKFLOW_LOADOUTS. Usage data is necessary but NOT sufficient, and three classes were kept despite low counts: gnubok_call_tool (it IS the search-only bridge), the `connect_*` and `lookup_company` onboarding entries (a fresh agent has not learned to search yet, which is why #1936 put them in the catalog), and every arsredovisning/dispositioner/depreciation/accrual proposal tool (a 60-day window ending in August cannot see bokslut season, since most Swedish companies close 31 Dec and do the work Jan-Jun, so summer counts understate them to near zero). Widget tools are a hard exclusion, found the hard way: gnubok_receipt_matcher and gnubok_vat_review_widget were demoted in a first pass and their own suites failed, because a widget is rendered from the _meta.ui it publishes in tools/list and search-only hides it there. Added a general invariant so the next widget tool cannot repeat it.
[2026-08-31] SKV broker hardening (two skeptic refutations on PR #1757, same classes as the bank fixes): the token route's code exchange now requires a verified connector state (signature, key, svc 'skv') plus an existing pending row before spending Arcim's client secret, and a concurrently consumed state withholds the tokens with 409 (SKV has no revoke endpoint; the pair expires unused); refresh requires the presented token's hash to match an ACTIVE ledger row under the presenting key (it was an open refresh oracle for any leaked token); the metering redaction gains a 10+-digit rule because personnummer/orgnr/redovisare12 in SKV data-proxy paths slipped the EB-tuned thresholds and rested in cleartext; authorize-url adopts the countHeldConnections reservation re-count; all SKV base URLs are https-only (loopback excepted). SELF-HOSTING.md's connector section collapsed from three contradictory copies (accreted across the stack merges) to one.