Skip to content

Return tool annotations from describe.tool - #1980

Open
daviesayo wants to merge 1 commit into
UsefulSoftwareCo:mainfrom
daviesayo:describe-tool-annotations
Open

Return tool annotations from describe.tool#1980
daviesayo wants to merge 1 commit into
UsefulSoftwareCo:mainfrom
daviesayo:describe-tool-annotations

Conversation

@daviesayo

@daviesayo daviesayo commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Summary

ToolSchemaView carries everything about a tool except the annotations a plugin declares on it, so code written inside execute cannot tell an approval-gated tool from a plain one except by reading its prose description. tools.schema now projects the three fields declared in ToolAnnotations, and describe.tool passes them through.

Linked issue

Part of #1979 (defect 3). Not Fixes, because that issue also covers two Cloudflare-host defects this PR does not touch.

Verification

Added describes a tool's declared annotations, and omits the key when it has none in packages/core/execution/src/tool-invoker.test.ts. It describes two tools on the same test connection: createContact, which declares requiresApproval and approvalDescription, and listContacts, which declares nothing. I reverted only the one-line change in tool-invoker.ts and re-ran, and the new test failed while the other 39 in the file passed, so the test observes the behaviour rather than restating it.

  • bun run format:check
  • bun run lint
  • bun run typecheck — 45/45
  • bun run test — 36/39 tasks green. The one failure is @executor-js/mcp-apps-shell mcp-app.browser.test.ts, which launches Chromium from /usr/bin/google-chrome. That path does not exist on macOS, so the suite never starts. It is unrelated to this diff and fails the same way on a clean checkout of this machine.
  • e2e — no scenario covers describe.tool's payload shape, and this change adds no user-visible surface. Happy to add one if you want it.

Checklist

  • Added a changeset (.changeset/describe-tool-annotations.md).
  • Added or updated tests for the new behaviour.
  • No secrets, credentials, or private data in the diff.

Design notes

The three fields are picked explicitly, not spread. Plugins keep private bookkeeping in the same column as the contract. packages/plugins/mcp/src/sdk/plugin.ts stamps { requiresApproval, approvalDescription?, mcp: { toolName, upstream?, _meta? } } and casts the result to ToolAnnotations, so a spread would hand a caller the upstream tool name and the server's _meta map. toolAnnotationsView copies only what packages/core/sdk/src/tool.ts declares.

One consequence worth naming: an MCP server's own hints (readOnlyHint, destructiveHint, title) still do not reach describe.tool, because the plugin nests them under mcp.upstream rather than the declared contract. Surfacing those is a separate decision about what the contract should be, and it is yours to make. If you want them, the smallest version is a declared upstream field on ToolAnnotations plus one line in toolAnnotationsView, and I will open a follow-up.

Both ToolSchemaView.make sites are covered — the static-tool branch and the persisted-row branch in toolSchema.

The key is absent when a tool declares nothing, so the describe payload does not grow for tools with nothing to say. The describe.tool self-description string in BUILTIN_TOOL_DESCRIPTIONS was updated to match.

Where this came from: I run apps/host-cloudflare in production and hit this while writing agent code that needed to branch on whether a tool was a read or a write.

🤖 Generated with Claude Opus 5

`ToolSchemaView` carried everything about a tool except the annotations a
plugin declares on it, so code written inside `execute` could not tell an
approval-gated tool from a plain one except by reading its prose description.
The data was already persisted on the tool row and already used by the
executor's own approval copy.

`tools.schema` now projects the three fields declared in `ToolAnnotations`
(`requiresApproval`, `approvalDescription`, `mayElicit`), and `describe.tool`
passes them through. The fields are picked explicitly rather than spread,
because plugins keep private bookkeeping alongside the contract: the mcp
plugin stores its upstream tool name and `_meta` map in the same column, and
none of that should reach a caller.

The key is omitted when a tool declares no annotations, so the describe
payload does not grow for the tools that have nothing to say.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant