Found while fixing #13271 (the same call site, a different dropped key). Filed rather than folded: #13271's scope is parameters only, and this touches the safety annotations, which are a separately-gradeable behaviour change on the same published surface.
Measured at a876ebe6
packages/mcp/src/mcp-server-runtime.ts, registerToolFromDefinition, passes the SDK an annotations object built entirely from the tool's name:
annotations: {destructiveHint: this.isDestructiveTool(tool.name),readOnlyHint: this.isReadOnlyTool(tool.name),openWorldHint: false,},Both predicates are membership tests against two literal sets at the top of the same file:
constREAD_ONLY_TOOLS=newSet(['list_objects','describe_object','query_records','get_record','aggregate_records','aggregate_data',]);constDESTRUCTIVE_TOOLS=newSet(['delete_field']);
⇒ a bridged tool whose name is not one of those seven literals is served to every MCP client as readOnlyHint: false, destructiveHint: false — "not read-only, and not destructive", the least informative and most permissive combination the annotation pair can express. That is every tool an app registers under its own name, and every action-backed tool (delete_opportunity, void_invoice, archive_account, …).
This matters because destructiveHint is not decoration: it is the field an MCP host reads to decide whether to interrupt the user before a call. A destructive action-backed tool currently arrives flagged as safe.
Why it reads as a defect rather than a design choice
The definition the bridge is handed already carries a destructiveness signal it does not read. AIToolDefinition in packages/spec/src/contracts/ai-service.ts:
/** * Whether invoking this tool requires human-in-the-loop confirmation. * Action-backed tools set this from the action's confirmation policy * (`action.ai.requiresConfirmation`, or the destructive-action default). */
requiresConfirmation?: boolean;
git grep -n requiresConfirmation -- 'packages/mcp/src/**' returns nothing, against tool.name (5 sites) and tool.description (1 site) live in the same file — the same reverse-check shape #13271 used, and the same real absence.
Meanwhile the framework has a maintainer-ruled definition of what "destructive" means for an action, in packages/runtime/src/action-execution.ts:
exportfunctionactionLooksDestructive(_deps: ActionExecutionDeps,action: any): boolean{if(action?.ai?.requiresConfirmation!==undefined)returnBoolean(action.ai.requiresConfirmation);returnBoolean(action?.mode==='delete'||action?.variant==='danger');}whose docblock records the ruling behind it (#7828, Option A: mode: 'delete' / variant: 'danger' are the closed, declared signals; confirmText deliberately is not). So the repo has one declared answer to this question and the MCP bridge asks a different one — a seven-name allowlist that no metadata author can extend.
⛔ Not a re-litigation of #3715
#3715 removed ToolSchema.requiresConfirmation — the metadata key — under ADR-0033, because nothing enforced it. That ruling stands and nothing here asks for it back. The key named above is AIToolDefinition.requiresConfirmation, the runtime contract member, which #3715 did not touch and which is still declared and documented as carried by action-backed tools. Any fix must keep those two apart.
What a fix would need to decide
- Whether the bridge should read
AIToolDefinition.requiresConfirmation for destructiveHint, reuse actionLooksDestructive's ruling, or take a third source — and what a tool that declares nothing should then be annotated as. Note the MCP spec's own defaults are readOnlyHint: false and destructiveHint: **true**, so the current "declare nothing" path is not merely uninformative, it inverts the protocol's own conservative default. - Whether
readOnlyHint has any declared source at all today, or whether the six-name set is the only thing that has ever populated it — in which case the honest interim shape may be to omit the hint rather than assert false. - Whether the two literal sets should survive as a fallback for the built-in names or be deleted once a declared source exists.
Either way, an annotation asserted from a name allowlist should stop being presented as a property of the tool.
Found while fixing #13271 (the same call site, a different dropped key). Filed rather than folded: #13271's scope is
parametersonly, and this touches the safety annotations, which are a separately-gradeable behaviour change on the same published surface.Measured at
a876ebe6packages/mcp/src/mcp-server-runtime.ts,registerToolFromDefinition, passes the SDK anannotationsobject built entirely from the tool's name:Both predicates are membership tests against two literal sets at the top of the same file:
⇒ a bridged tool whose name is not one of those seven literals is served to every MCP client as
readOnlyHint: false, destructiveHint: false— "not read-only, and not destructive", the least informative and most permissive combination the annotation pair can express. That is every tool an app registers under its own name, and every action-backed tool (delete_opportunity,void_invoice,archive_account, …).This matters because
destructiveHintis not decoration: it is the field an MCP host reads to decide whether to interrupt the user before a call. A destructive action-backed tool currently arrives flagged as safe.Why it reads as a defect rather than a design choice
The definition the bridge is handed already carries a destructiveness signal it does not read.
AIToolDefinitioninpackages/spec/src/contracts/ai-service.ts:git grep -n requiresConfirmation -- 'packages/mcp/src/**'returns nothing, againsttool.name(5 sites) andtool.description(1 site) live in the same file — the same reverse-check shape #13271 used, and the same real absence.Meanwhile the framework has a maintainer-ruled definition of what "destructive" means for an action, in
packages/runtime/src/action-execution.ts:whose docblock records the ruling behind it (#7828, Option A:
mode: 'delete'/variant: 'danger'are the closed, declared signals;confirmTextdeliberately is not). So the repo has one declared answer to this question and the MCP bridge asks a different one — a seven-name allowlist that no metadata author can extend.⛔ Not a re-litigation of #3715
#3715 removed
ToolSchema.requiresConfirmation— the metadata key — under ADR-0033, because nothing enforced it. That ruling stands and nothing here asks for it back. The key named above isAIToolDefinition.requiresConfirmation, the runtime contract member, which #3715 did not touch and which is still declared and documented as carried by action-backed tools. Any fix must keep those two apart.What a fix would need to decide
AIToolDefinition.requiresConfirmationfordestructiveHint, reuseactionLooksDestructive's ruling, or take a third source — and what a tool that declares nothing should then be annotated as. Note the MCP spec's own defaults arereadOnlyHint: falseanddestructiveHint: **true**, so the current "declare nothing" path is not merely uninformative, it inverts the protocol's own conservative default.readOnlyHinthas any declared source at all today, or whether the six-name set is the only thing that has ever populated it — in which case the honest interim shape may be to omit the hint rather than assertfalse.Either way, an annotation asserted from a name allowlist should stop being presented as a property of the tool.