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:
const READ_ONLY_TOOLS = new Set([
'list_objects', 'describe_object', 'query_records',
'get_record', 'aggregate_records', 'aggregate_data',
]);
const DESTRUCTIVE_TOOLS = new Set(['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:
export function actionLooksDestructive(_deps: ActionExecutionDeps, action: any): boolean {
if (action?.ai?.requiresConfirmation !== undefined) return Boolean(action.ai.requiresConfirmation);
return Boolean(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.