fix(mcp): promote gnubok_update_customer to the default tools/list catalog (#1986)

* fix(mcp): promote gnubok_update_customer to the default tools/list catalog

The tool existed since #1876 but was catalogVisibility 'search', which
means it appeared only through gnubok_search_tools. Claude.ai (and other
list-driven clients) can only call tools present in tools/list, so on the
primary connector the tool was uncallable and the reporter of #1706 read
it as missing twice.

- server.ts: drop the 'search' flag on gnubok_update_customer; no change
  to schema, executor, scopes, risk tier, or staging path
- update-customer.test.ts: pin the tools/list projection
  (isDefaultCatalogTool) instead of the flag; keep the search test
- payload-size.bench.test.ts: ceiling 63_400 to 64_100 (measured 64 043,
  +761 over main) with a progression bullet
- README.md: note that search-only tools are not callable from Claude.ai
- DECISIONS.md: supersede the 2026-08-25 keep-search-only entry

Closes #1706

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkUfWtuFCUkNtRAgMQCse2

* test(mcp): re-measure the tools/list ceiling after #1993 landed on main

#1993 declared line_type and revenue_account on the create item schema,
which moved the accounted projection to 63 761 with gnubok_update_customer
promoted; ceiling 63_800 keeps the same headroom as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkUfWtuFCUkNtRAgMQCse2

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-27 22:28:14 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent 17caf9d80a
commit 89d0e1b994
5 changed files with 15 additions and 6 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ For internal Anthropic API usage (the SDK is called from `lib/ai/provider.ts` an
## Payload-size watchdog
`payload-size.bench.test.ts` enforces a `tools/list` JSON payload ceiling. If the test fires, the right answer is rarely "raise the ceiling". Instead, trim descriptions or set specialized wide tools to `catalogVisibility: 'search'`. Those tools remain discoverable with full schemas through `gnubok_search_tools` and callable through `tools/call` without bloating the default catalog.
`payload-size.bench.test.ts` enforces a `tools/list` JSON payload ceiling. If the test fires, the right answer is rarely "raise the ceiling". Instead, trim descriptions or set specialized wide tools to `catalogVisibility: 'search'`. Those tools remain discoverable with full schemas through `gnubok_search_tools` and callable through `tools/call` on the wire without bloating the default catalog. Claude.ai only calls tools present in `tools/list`, so a tool that a user or a skill must call directly stays in the default catalog.
## Where things live
@@ -278,12 +278,20 @@ describe('tools/list payload size guard', () => {
// skatteverket_connection, recommended_tools: agent-briefing.test.ts
// pins their RUNTIME shape, so nothing was left unguarded), against
// +~245 for the new gnubok_call_tool.
// * 63.1K to 63.8K with gnubok_update_customer promoted to the default
// catalog (#1706, #1876). gnubok_call_tool (above) only bridges READ
// tools and refuses writes, so a search-only WRITE tool is still
// uncallable on Claude.ai; the reporter read the tool as missing twice.
// The +761 tokens are the 19-property partial-update wire contract plus
// the staging envelope; the description is already 152 chars.
// Measured 63 761 on the accounted projection after merging #1993
// (line_type and revenue_account declared on the create item schema).
// 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(63_100)
expect(approxTokens).toBeLessThan(63_800)
})
it('keeps the accounted_* namespace as the measured worst case', () => {
@@ -4,7 +4,7 @@ import { TOOL_SCOPE_MAP } from '@/lib/auth/api-keys'
import { OPERATION_RISK_TIERS } from '@/lib/pending-operations/risk-tiers'
import { hashRequest } from '@/lib/api/idempotency'
import { decryptPersonnummer, encryptPersonnummer } from '@/lib/salary/personnummer'
import { tools } from '../server'
import { tools, isDefaultCatalogTool } from '../server'
const CUSTOMER_ID = '11111111-1111-4111-8111-111111111111'
const tool = () => tools.find((candidate) => candidate.name === 'gnubok_update_customer')!
@@ -38,7 +38,8 @@ describe('gnubok_update_customer: registration', () => {
expect(tool().inputSchema.additionalProperties).toBe(false)
expect(tool().annotations.readOnlyHint).toBe(false)
expect(tool().annotations.idempotentHint).toBe(true)
expect(tool().catalogVisibility).toBe('search')
expect(isDefaultCatalogTool(tool())).toBe(true)
expect(tools.filter(isDefaultCatalogTool).map((t) => t.name)).toContain('gnubok_update_customer')
expect(TOOL_SCOPE_MAP.gnubok_update_customer).toBe('customers:write')
expect(OPERATION_RISK_TIERS.update_customer).toBe('low')
})
@@ -48,7 +49,7 @@ describe('gnubok_update_customer: registration', () => {
expect(properties.personal_number).toMatchObject({ type: ['string', 'null'] })
})
it('keeps the wide write schema discoverable through tool search', async () => {
it('is also discoverable through tool search', async () => {
const search = tools.find((candidate) => candidate.name === 'gnubok_search_tools')!
const result = (await search.execute(
{
-1
View File
@@ -5776,7 +5776,6 @@ export const tools: McpTool[] = [
idempotentHint: true,
openWorldHint: false,
},
catalogVisibility: 'search',
async execute(args, companyId, userId, supabase, actor) {
const changes: Record<string, unknown> = {}
for (const key of [