Uh oh!
There was an error while loading. Please reload this page.
fix(mcp): annotate tools with readOnlyHint so reads aren't treated as edit tools - #280
Conversation
… edit tools Tools were registered without MCP ToolAnnotations, so clients defaulted to treating every tool as mutating/destructive. This added friction for the 9 read-only tools and mis-categorized them as edit/write operations. Add an `annotations` field to all 10 tool registrations: Read-only (readOnlyHint: true, openWorldHint: true): search_container, get_container, get_container_route, get_container_transport_events, get_shipment_details, list_containers, list_shipments, list_tracking_requests Read-only, bounded catalog (readOnlyHint: true, openWorldHint: false): get_supported_shipping_lines Write (readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true): track_container — a create, not destructive, and idempotent (re-tracking the same number returns the existing tracking request, no duplicate). Adds a regression test (annotations.test.ts) asserting each tool's hints, and a note in docs/mcp/home.mdx clarifying the read/write split. No tool behavior, schemas, or handlers changed — annotations only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
dodeja
commented
Jun 26, 2026
Triaged review feedback on this PR: no inline review comments or review summaries were found. The only PR comments are automated deployment/preview bots (Vercel, Mintlify), which aren't actionable. Nothing to address; PR remains as-is. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:4cabef2745
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
track_container is not idempotent: when no container is found via search it always creates a new tracking request, and a pending request (no linked container yet) won't be matched by search on a subsequent call, so repeated calls can create additional tracking requests. Set idempotentHint: false so clients don't treat the write as safe to silently retry/replay, and correct the docs note accordingly. Also harden annotations.test.ts: assert openWorldHint: true for the open-world read tools (excluding the closed-world catalog), and guard the "annotates every tool" check against _registeredTools silently resolving to an empty/undefined map if SDK internals change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dodeja
commented
Jun 26, 2026
Triaged the automated review feedback and pushed Addressed
Skipped
Green gate from the worktree: SDK build + type-check pass, MCP build + type-check pass, SDK tests 51 pass / 2 skip, MCP tests 81 pass. SDK public surface unchanged, so no docs/sdk/reference regeneration needed. Only the three changed files were formatted (no mass oxfmt churn). |
Uh oh!
There was an error while loading. Please reload this page.
Root cause
The Terminal49 MCP tools were registered via
server.registerTool(name, config, handler)without anannotationsfield. With no MCPToolAnnotations, clients have no read-only signal and default to treating every tool as mutating/destructive. This mis-categorizes the 9 read-only lookups as edit/write operations, adding unnecessary confirmation friction and making the read tools look as risky as a write.The fix
Add an
annotationsfield to all 10 tool registrations inpackages/mcp/src/server.ts. No tool behavior, schemas, or handlers changed — annotations only. The MCP SDK (@modelcontextprotocol/sdk^1.29.0) supportsreadOnlyHint/destructiveHint/idempotentHint/openWorldHinton theregisterToolconfig object.Categorization
Read-only —
{ readOnlyHint: true, openWorldHint: true }(9 tools, but one differs on open-world, see below):search_containerget_containerget_container_routeget_container_transport_eventsget_shipment_detailslist_containerslist_shipmentslist_tracking_requestsRead-only, bounded catalog —
{ readOnlyHint: true, openWorldHint: false }:get_supported_shipping_lines— the supported-carriers catalog is closed/enumerable, soopenWorldHint: false.Write (the only one) —
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }:track_container— a create, not destructive, and idempotent: re-tracking the same number returns the existing tracking request rather than creating a duplicate.So: 9 read-only tools + 1 non-destructive idempotent write (
track_container).Tests & docs
packages/mcp/src/annotations.test.tsbuilds the server viacreateTerminal49McpServer('token'), reads(server as any)._registeredTools, and asserts each tool's hints (the 9 reads arereadOnlyHint===true;track_containerisreadOnlyHint===false,destructiveHint===false,idempotentHint===true;get_supported_shipping_linesisopenWorldHint===false).docs/mcp/home.mdxgets a<Note>at the top of the Tools reference section clarifying that every tool is read-only excepttrack_container, which creates a tracking request and is idempotent.Green gate
All gates pass:
(Baseline was SDK 51 pass / 2 skip, MCP 77 pass.)
Notes
server.tsis also touched by open PR fix(mcp): honest list filters/contract + meta.total guard + page-size cap + stop leaking errors + polish #276 (fix/mcp-server-list-and-polish). Both branch offmain; expect a small merge coordination onserver.tsbetween this PR and fix(mcp): honest list filters/contract + meta.total guard + page-size cap + stop leaking errors + polish #276. The diff here is intentionally minimal (oneannotations:line per tool) to keep that merge trivial. Note:oxfmt --checkreports pre-existing formatting issues onserver.tsthat also exist unchanged onmain(running--writewould rewrite ~700 lines and conflict with fix(mcp): honest list filters/contract + meta.total guard + page-size cap + stop leaking errors + polish #276), so formatting was deliberately left untouched; the added lines match existing style andoxlintis clean.🤖 Generated with Claude Code
Greptile Summary
This PR adds MCP
ToolAnnotationsto all 10 tool registrations inpackages/mcp/src/server.tsso clients can correctly distinguish read-only lookups from the single write operation (track_container). No tool handlers, schemas, or runtime behaviour are changed.server.ts: Oneannotations:field added perregisterToolcall — 9 tools markedreadOnlyHint: true,track_containermarkedreadOnlyHint: false, destructiveHint: false, idempotentHint: true, andget_supported_shipping_linesadditionally markedopenWorldHint: false.annotations.test.ts: New regression test file that introspects_registeredTools(SDK internal) to assert each tool's hint values, with separate cases for the idempotent write and the closed-world catalog.docs/mcp/home.mdx: Adds a<Note>clarifying the read-only vs write split at the top of the Tools reference section.Confidence Score: 4/5
Safe to merge — the change is additive metadata only; no handlers, schemas, or API calls are touched.
The production change in
server.tsis minimal and correct: every tool gets anannotationsblock matching the PR categorisation. The only concerns are in the test file: it couples itself to the SDK private_registeredToolsfield, and the bulk read-only loop omitsopenWorldHint: trueassertions for the eight open-world tools, leaving a gap a future regression could slip through undetected.packages/mcp/src/annotations.test.ts — private SDK field access and incomplete
openWorldHintcoverage.Important Files Changed
annotationsline per tool registration (10 tools total). No handler, schema, or behavior changes. All 9 read-only tools correctly getreadOnlyHint: true;track_containercorrectly getsreadOnlyHint: false, destructiveHint: false, idempotentHint: true;get_supported_shipping_linescorrectly getsopenWorldHint: false.(server as any)._registeredTools— a private SDK internal — which could silently break on SDK upgrades. The bulk read-only test also omitsopenWorldHintassertions for the 8 open-world tools.<Note>callout above the tools reference clarifying read-only vs write behaviour. Documentation-only change, accurate and concise.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD Client([MCP Client]) --> |calls tool| Dispatch{Tool annotation\nreadOnlyHint?} Dispatch -->|true| ReadPath[Read-only path\nNo confirmation needed] Dispatch -->|false| WriteCheck{destructiveHint?} WriteCheck -->|false + idempotentHint:true| IdempotentWrite[Non-destructive\nidempotent write\ntrack_container] WriteCheck -->|true| DestructiveWrite[Destructive write\nConfirmation required] ReadPath --> ReadTools["9 read tools:\nsearch_container / get_container / get_container_route\nget_container_transport_events / get_shipment_details\nget_supported_shipping_lines openWorldHint:false\nlist_containers / list_shipments / list_tracking_requests"] IdempotentWrite --> TrackAPI[Terminal49 API\nPOST /tracking_requests\nreturns existing if duplicate] ReadTools --> ReadAPI[Terminal49 API\nGET endpoints]%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD Client([MCP Client]) --> |calls tool| Dispatch{Tool annotation\nreadOnlyHint?} Dispatch -->|true| ReadPath[Read-only path\nNo confirmation needed] Dispatch -->|false| WriteCheck{destructiveHint?} WriteCheck -->|false + idempotentHint:true| IdempotentWrite[Non-destructive\nidempotent write\ntrack_container] WriteCheck -->|true| DestructiveWrite[Destructive write\nConfirmation required] ReadPath --> ReadTools["9 read tools:\nsearch_container / get_container / get_container_route\nget_container_transport_events / get_shipment_details\nget_supported_shipping_lines openWorldHint:false\nlist_containers / list_shipments / list_tracking_requests"] IdempotentWrite --> TrackAPI[Terminal49 API\nPOST /tracking_requests\nreturns existing if duplicate] ReadTools --> ReadAPI[Terminal49 API\nGET endpoints]Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(mcp): annotate tools with readOnlyHi..." | Re-trigger Greptile