fix(mcp): widget-bearing tools must be read-only: Claude.ai drops write-annotated interactive tools (#1961)
The SIE drop card kept flapping into Claude.ai's Interactive-tools list and vanishing, and the agent could never call it: every hypothesis (scope filter, tool-count cap, stale cache, schema shape) was eliminated against live data until one discriminator remained: all surviving widget tools carry readOnlyHint true and gnubok_create_sie_upload was the only one annotated as a write. Claude.ai accepts always-render widgets only on read-only tools and silently drops the tool otherwise. readOnlyHint is now true, which is also honest: the tool only mints a short-lived upload URL; the actual write is the staged gnubok_import_sie: the exact receipt_matcher shape (read-only widget tool, writes via separate approval-gated tools). A guard test pins the invariant for every future widget tool. 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:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
9bad3aee06
commit
ff84c8f316
@@ -41,4 +41,18 @@ describe('MCP tool inputSchema strictness', () => {
|
||||
|
||||
expect(missing).toEqual([])
|
||||
})
|
||||
|
||||
it('every widget-bearing tool is read-only: Claude.ai drops write-annotated interactive tools', () => {
|
||||
// A tool with definition-level _meta.ui renders on every call. Claude.ai
|
||||
// accepts that only for read-only tools and silently DROPS a
|
||||
// write-annotated one from the connector (E2E #9, 2026-08-26: the SIE
|
||||
// drop card flapped into the Interactive list and vanished). Widget
|
||||
// tools mint links/lists only; actual writes go through separate
|
||||
// approval-gated tools the widget calls.
|
||||
const writers = tools
|
||||
.filter((t) => (t as { _meta?: { ui?: unknown } })._meta?.ui !== undefined)
|
||||
.filter((t) => t.annotations.readOnlyHint !== true)
|
||||
.map((t) => t.name)
|
||||
expect(writers).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16387,7 +16387,15 @@ export const tools: McpTool[] = [
|
||||
},
|
||||
_meta: { ui: { resourceUri: 'ui://sie-drop/app.html' } },
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
// readOnlyHint MUST stay true on widget-bearing tools: Claude.ai
|
||||
// accepts always-render widgets only on read-only tools and DROPS a
|
||||
// write-annotated one from the connector entirely (the tool flapped
|
||||
// into the Interactive list and vanished, E2E #9 2026-08-26; every
|
||||
// surviving widget tool was readOnly). Honest too: this only mints a
|
||||
// short-lived upload URL; the actual write is the staged
|
||||
// gnubok_import_sie, same shape as receipt_matcher (read-only tool,
|
||||
// writes via separate approval-gated tools).
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: false,
|
||||
openWorldHint: false,
|
||||
|
||||
Reference in New Issue
Block a user