Skip to content

feat(agent): keep MCP tool annotations from tools/list (DEV-1374) - #148

Open
w0nche0l wants to merge 2 commits into
mainfrom
dennisjeong/dev-1374-mcp-tool-annotations
Open

w0nche0l wants to merge 2 commits into
mainfrom
dennisjeong/dev-1374-mcp-tool-annotations

Conversation

@w0nche0l

@w0nche0l w0nche0l commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Linear: DEV-1374

Problem

An MCP server reports behaviour hints per tool in tools/list (annotations: readOnlyHint, destructiveHint, idempotentHint, openWorldHint, title). listToolDefs copies only name, description, inputSchema, outputSchema and the loop key, so the hints are gone after discovery and the handle offers no other way to read them.

OpenRouter's MCP loop needs them for two things:

  • OpenAI's mcp_list_tools item lists every tool with annotations: { read_only }. The loop cannot fill that field.
  • The loop refuses require_approval and allowed_tools selectors that use read_only, because it cannot tell which tools are read-only.

Change

  • McpToolDef and SerializedMCPToolDef gain an optional annotations.
  • listToolDefs copies the hints the server defined. A tool with no annotations gets no field.
  • markMcp accepts annotations next to loopKey, and McpBranded exposes it as tool.annotations. McpToolAnnotations is exported from the package root.
  • Cache snapshots store and restore the hints. isSerializedMCPServer rejects a snapshot whose annotations is not an object, or holds a hint that is not a boolean, or a title that is not a string. A consumer may gate approval on a hint, so a wrong type must not read as truthy.

Nothing reaches the model: annotations are not part of the wire tool definition.

Behaviour to review

  • Snapshots written by this version carry annotations. Older versions ignore the extra field when they read such a snapshot. Snapshots written by older versions have no annotations, so tools rehydrated from them have none until the next refresh().
  • The hints come from the server and are not verified. The README says so.

Tests

tests/unit/mcp/annotations.test.ts:

  • createMCPTools exposes each tool with the hints its server reported, and none for a tool without annotations.
  • A snapshot from serialize(), after a JSON round trip, restores the same hints through rehydrateMCPTools without calling tools/list.
  • isSerializedMCPServer rejects malformed annotations and accepts well-formed ones.

I checked that the first two fail when the listToolDefs change is removed, and the second fails when the rehydrate change is removed.

Overlap

#146 also adds an option to markMcp (wireInputSchema) and edits the same lines in tool-wrapper.ts. Whichever lands second needs a small merge there.

listToolDefs dropped the annotations a server reports per tool, so a consumer could not read readOnlyHint or destructiveHint after discovery. Carry them on the tool definition, expose them on the MCP-branded tool, and store and restore them in cache snapshots.

Linear: DEV-1374
…hots for annotations

Also copies a snapshot's annotations on rehydrate so tools do not share the snapshot's object, and documents the five kept fields with an isMcpTool example.

Linear: DEV-1374
@w0nche0l

w0nche0l commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

AGENT-WRITTEN: Perry reviewed head 20c10d9 and found no blockers, but could not post on this repository (GitHub permissions error). Its notes are in this Slack thread. What dc17c1b does with each:

Perry's note Result
Snapshot and tools share one annotations object Fixed: snapshotToToolDefs copies it, as it already does for the schemas.
README should show isMcpTool narrowing Added an example.
Only the five standard fields are copied; say so README now says other keys are dropped.
Test emitProgress: false as well Discovery test now runs for both tool kinds.
Assert the hints are not sent to the model Added.
Strict equality so a missing tool cannot pass as undefined Switched to toStrictEqual.
Include idempotentHint in the fixture Added.
Exercise a malformed snapshot through a public loading path Added for rehydrateMCPTools (rejects with MCPCacheError).
Refresh and cache-write assertions Not added: both go through listToolDefs and serializeServer, which the discovery and snapshot tests already cover.

This branch has not been deployed

No deployments
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