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:
co-authored by
Claude Opus 5
Jakob Wennberg
parent
37e50c272d
commit
b643e6ce64
@@ -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.
|
||||
|
||||
@@ -304,12 +304,64 @@ describe('tools/list payload size guard', () => {
|
||||
// the staging envelope; the description is one sentence per fact).
|
||||
// Same deliberate skip of the read-demotion rule as the entry above:
|
||||
// the demotion needs prod usage data, not a guess inside this PR.
|
||||
// * 65K to 61.3K by demoting ten rarely-called READ tools to
|
||||
// search-only (2026-08-31). This is the demotion the two entries above
|
||||
// deferred for want of prod usage data, done as its own change and
|
||||
// ratcheting the ceiling back down as they asked.
|
||||
//
|
||||
// Picked from 60 days of mcp.tool_called: default-catalog reads with
|
||||
// <= 25 calls, excluding anything in RECOMMENDED_WORKFLOW_LOADOUTS.
|
||||
// Deliberately NOT demoted despite low counts:
|
||||
// - gnubok_call_tool, which IS the search-only bridge;
|
||||
// - gnubok_receipt_matcher and gnubok_vat_review_widget, which are
|
||||
// WIDGET tools: the host renders them from the _meta.ui they
|
||||
// publish in tools/list, so search-only makes the widget
|
||||
// unrenderable. Their own tests catch this; both were demoted in a
|
||||
// first pass and put back.
|
||||
// - the connect_* onboarding tools (#1936 put them in the catalog on
|
||||
// purpose: a fresh agent has not learned to search yet);
|
||||
// - gnubok_lookup_company, the org-number-first onboarding entry;
|
||||
// - every arsredovisning / dispositioner / depreciation / accrual
|
||||
// PROPOSAL tool. A 60-day window ending in August cannot see
|
||||
// bokslut season at all: most Swedish companies close on 31 Dec and
|
||||
// do the work Jan-Jun, so summer counts understate them to near
|
||||
// zero. Demoting those would hide the year-end flow exactly when it
|
||||
// is needed. Usage data is necessary here, not sufficient.
|
||||
//
|
||||
// Long-term answer to growth is no longer a ceiling bump. gnubok_call_tool
|
||||
// makes `catalogVisibility: 'search'` usable for READ tools on hosts that
|
||||
// can only invoke what tools/list showed them, which is the constraint that
|
||||
// forced gnubok_reconcile_match back into the default catalog on
|
||||
// 2026-08-26. Demote a read to search-only before proposing a bump.
|
||||
expect(approxTokens).toBeLessThan(65_000)
|
||||
//
|
||||
// Only READ tools may be demoted: gnubok_call_tool refuses writes, so a
|
||||
// search-only WRITE is uncallable on Claude.ai. That is why the three
|
||||
// bumps above happened instead of demotions.
|
||||
expect(approxTokens).toBeLessThan(61_600)
|
||||
})
|
||||
|
||||
/**
|
||||
* The trap that catches anyone reclaiming budget by demoting reads.
|
||||
*
|
||||
* A widget tool publishes _meta.ui in tools/list, and that is how the host
|
||||
* knows to render it. Search-only hides it from tools/list, so the widget
|
||||
* silently stops rendering while the tool still "works" when called. Two
|
||||
* widget tools were demoted in the 2026-08-31 pass and put back; their own
|
||||
* suites caught it, but only because those suites happen to exist. This
|
||||
* makes the rule hold for every widget tool added later.
|
||||
*/
|
||||
it('never lets a widget tool fall out of the default catalog', () => {
|
||||
const hiddenWidgets = tools
|
||||
.filter((t) => (t as { _meta?: { ui?: unknown } })._meta?.ui)
|
||||
.filter((t) => !isDefaultCatalogTool(t))
|
||||
.map((t) => t.name)
|
||||
|
||||
expect(
|
||||
hiddenWidgets,
|
||||
`Widget tools publish _meta.ui in tools/list and the host renders them from it. ` +
|
||||
`catalogVisibility: 'search' hides them there, so the widget stops rendering: ` +
|
||||
hiddenWidgets.join(', '),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps the accounted_* namespace as the measured worst case', () => {
|
||||
|
||||
@@ -7838,6 +7838,7 @@ export const tools: McpTool[] = [
|
||||
|
||||
{
|
||||
name: 'gnubok_get_counterparty_templates',
|
||||
catalogVisibility: 'search',
|
||||
keywords: ['motpart', 'konteringsmall', 'mallar'],
|
||||
title: 'List Counterparty Templates',
|
||||
description: 'List active counterparty categorization templates: learned patterns from prior categorizations used for auto-matching new transactions.',
|
||||
@@ -8394,6 +8395,7 @@ export const tools: McpTool[] = [
|
||||
|
||||
{
|
||||
name: 'gnubok_list_dimension_values',
|
||||
catalogVisibility: 'search',
|
||||
keywords: ['dimensionsvärden', 'kostnadsställe', 'projekt'],
|
||||
title: 'List Dimension Values',
|
||||
description: 'List values (SIE #OBJEKT codes) for one dimension, optionally fuzzy-matched by query. Use to find the right kostnadsställe/projekt code before tagging lines. sie_dim_no: 1 = kostnadsställe, 6 = projekt.',
|
||||
@@ -8964,6 +8966,7 @@ export const tools: McpTool[] = [
|
||||
|
||||
{
|
||||
name: 'gnubok_get_dimension_pnl',
|
||||
catalogVisibility: 'search',
|
||||
keywords: ['projektresultat', 'kostnadsställe', 'resultat per projekt'],
|
||||
title: 'P&L per Dimension (Resultat per projekt)',
|
||||
description: 'Resultat per projekt/kostnadsställe: P&L matrix over one SIE dimension: each value with activity becomes a column plus an untagged bucket, and the Totalt column reconciles exactly with the resultatrapport. sie_dim_no: 1 = kostnadsställe, 6 = projekt.',
|
||||
@@ -14297,6 +14300,7 @@ export const tools: McpTool[] = [
|
||||
|
||||
{
|
||||
name: 'gnubok_agi_status',
|
||||
catalogVisibility: 'search',
|
||||
keywords: ['arbetsgivardeklaration'],
|
||||
title: 'AGI Declaration Status (Arbetsgivardeklaration)',
|
||||
description: "Fetch AGI filing status for a salary run: run-scoped filing_state and kvittensnummer (a correction run never inherits the superseded original's receipt), plus live Skatteverket kvittenser.",
|
||||
@@ -14384,6 +14388,7 @@ export const tools: McpTool[] = [
|
||||
},
|
||||
{
|
||||
name: 'gnubok_get_employee',
|
||||
catalogVisibility: 'search',
|
||||
keywords: ['anställd', 'personal'],
|
||||
title: 'Get Employee',
|
||||
description: 'Get one employee\'s full payroll config: salary, tax table/column, jamkning, F-skatt, vacation rule, vaxa-stod, bank details, dimensions. Personnummer masked. Use after gnubok_list_employees to drill into one employee before payroll work.',
|
||||
@@ -14481,6 +14486,7 @@ export const tools: McpTool[] = [
|
||||
},
|
||||
{
|
||||
name: 'gnubok_get_payslip',
|
||||
catalogVisibility: 'search',
|
||||
keywords: ['lönebesked', 'lönespecifikation', 'lönespec', 'lön'],
|
||||
title: 'Get Payslip (Lönebesked)',
|
||||
description: 'Get one employee\'s payslip in a salary run: gross, tax, avgifter, net, every line item and the step-by-step calculation breakdown. Personnummer masked. Use after gnubok_get_salary_run to verify how one employee\'s pay was computed.',
|
||||
@@ -14574,6 +14580,7 @@ export const tools: McpTool[] = [
|
||||
},
|
||||
{
|
||||
name: 'gnubok_list_absence',
|
||||
catalogVisibility: 'search',
|
||||
keywords: ['frånvaro', 'sjukfrånvaro', 'semester', 'vab'],
|
||||
title: 'List Absence (Frånvaro)',
|
||||
description: 'List an employee\'s registered absence days (sick, vab, parental, ...) in a date range, max 92 days. These per-day rows drive karensavdrag and sjuklön at calculation time. Use before gnubok_register_absence to see what is already registered.',
|
||||
@@ -15447,6 +15454,7 @@ export const tools: McpTool[] = [
|
||||
},
|
||||
{
|
||||
name: 'gnubok_get_vacation_balance',
|
||||
catalogVisibility: 'search',
|
||||
keywords: ['semester', 'semestersaldo', 'semesterdagar'],
|
||||
title: 'Get Vacation Balance (Semestersaldo)',
|
||||
description: 'Get one employee\'s open vacation balance: entitled/taken/remaining days, sparade dagar per origin year, forced payouts and estimated semesterlöneskuld in SEK. Use before gnubok_close_vacation_year.',
|
||||
@@ -17704,6 +17712,7 @@ export const tools: McpTool[] = [
|
||||
|
||||
{
|
||||
name: 'gnubok_sie_preflight',
|
||||
catalogVisibility: 'search',
|
||||
keywords: ['sie', 'sie-fil', 'kontrollera sie'],
|
||||
title: 'SIE Preflight Scan',
|
||||
description:
|
||||
@@ -18889,6 +18898,7 @@ export const tools: McpTool[] = [
|
||||
|
||||
{
|
||||
name: 'gnubok_list_accrual_schedules',
|
||||
catalogVisibility: 'search',
|
||||
keywords: ['periodisering', 'periodiseringar'],
|
||||
title: 'List Periodiseringar',
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user