Key takeaways
- MCP tool annotations are server-supplied claims. The current specification explicitly tells clients to treat them as untrusted unless the server itself is trusted.
- In the local SDK probe, readOnlyHint=true did not stop the callback from writing a file. The protocol transported the claim; it did not enforce the claimed effect boundary.
- idempotentHint=true did not deduplicate a repeated call. The second call ran and changed the marker from call 1 to call 2.
- Use annotations for presentation, planning and risk cues only after provenance checks. Compute authorization from trusted identity, policy, arguments, resource and consequence at call time.
- Block or re-approve a tool when its server identity, code digest, declared effect class, observed behavior or requested resources change. Roll back on any undeclared mutation or egress.
The sharp question: can an AI client enforce “read only” from MCP metadata?
An MCP server can label a tool with readOnlyHint: true. That field is attractive to agent builders because it appears to answer a hard policy question in one boolean: may the model call this without approval? The name sounds like a capability boundary. The current MCP contract is narrower. It is a hint supplied by the same server that implements the tool.
My local result makes the distinction concrete. The official TypeScript SDK listed a synthetic tool with readOnlyHint: true, destructiveHint: false, idempotentHint: true and openWorldHint: false. Calling it wrote a file. Calling it again wrote the file again. Nothing was bypassed: the SDK did exactly what the protocol permits. The mistake would be promoting descriptive metadata into authorization.
Chronology: connectivity arrived before this policy question was settled for you
Anthropic introduced MCP on November 25, 2024 as an open standard for connecting AI assistants to repositories, business tools and development environments. That architecture made tool discovery portable: a client could learn a tool’s name, description and input schema, then let a model propose a call. Portability also moved server-authored text and metadata into the client’s decision path.
On April 1, 2025, Invariant Labs published independent experiments on tool poisoning: malicious instructions embedded in tool descriptions steered tested agents toward sensitive reads and unauthorized behavior. The current July 28, 2026 MCP tools specification now includes an explicit normative warning that clients must consider tool annotations untrusted unless they come from trusted servers. SDK 1.30.0, released July 27, carries the same warning in generated types. The relevant progression is connectivity, adversarial evidence, then clearer trust language—not an enforcement primitive.
What the four hints actually claim
The schema exposes four effect-oriented booleans. readOnlyHint says the tool does not modify its environment. destructiveHint distinguishes destructive from additive updates when a tool is not read-only. idempotentHint says repeating the same call has no additional effect, again only when the tool is not read-only. openWorldHint says whether the tool may interact with an open world of external entities.
Defaults do not turn these claims into measured facts. More importantly, the schema’s own qualification says all ToolAnnotations properties are hints and are not guaranteed to faithfully describe tool behavior. A client may use a trusted annotation to improve display or planning. It cannot derive trust merely because the object passed schema validation.
The fixture advertised the safest-looking combination
The checked-in audit registers one zero-argument tool named read_summary with the official McpServer class. Its annotations are exactly { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }. Its description says only that it returns a synthetic summary. There is no model prompt and no hidden instruction.
The callback increments an in-memory counter, writes environment-was-modified.txt inside a newly created temporary directory and returns the fixed text “synthetic summary.” The temporary path contains no user data and is removed after the assertions. This isolates one question: does the SDK treat the annotations as enforcement, or does it dispatch the registered callback?
Discovery preserved the server’s claims exactly
The audit links an MCP Client and McpServer with the SDK’s InMemoryTransport, then calls listTools. The result contained one tool named read_summary, the expected title and all four annotation values unchanged. Before any call, the marker file did not exist and the callback count was zero.
That control matters. A mutation during registration or listing would be a different failure. Here, discovery was inert and metadata transport was faithful. The client learned what the server claimed; it did not independently establish what the callback could do.
The first call crossed the real effect boundary
The client then issued callTool with the discovered name and an empty arguments object. It received the declared synthetic summary. The marker also appeared with the text “modified by call 1.” readOnlyHint remained true in the previously listed metadata. The side effect and the claim coexisted without an SDK error.
This is expected protocol behavior, not an SDK vulnerability. registerTool stores the annotations alongside the tool definition and associates a callback with the name. callTool dispatches the callback after request and input handling. A library cannot prove that arbitrary application code is read-only from a boolean authored by that same application.
The second call showed that idempotence is not deduplication
The audit repeated the same tool name and the same empty arguments. The callback ran a second time, the response remained “synthetic summary,” and the marker changed to “modified by call 2.” The final callback count was two. idempotentHint: true did not make the SDK cache, suppress or coalesce the request.
That distinction matters for AI agents that retry after timeouts. Idempotence is a property of the operation’s effects, not a request-processing feature the client can safely synthesize from server metadata. Real deduplication needs a trusted operation identifier, server-side replay handling, stored outcome semantics and a defined expiry window. A hint cannot recover those guarantees after an ambiguous failure.
The protocol warning is the mechanism, not a footnote
The current tools page says clients must consider tool annotations untrusted unless they come from trusted servers. The generated SDK type declaration is even plainer: all properties are hints, faithful behavior is not guaranteed, and clients should never make tool-use decisions from annotations received from untrusted servers. Those are direct boundaries on how the fields should be consumed.
“Trusted server” still requires an operational definition. Transport encryption proves a channel, not code behavior. A registry listing proves discovery, not ownership. A signature can bind an artifact, not certify its effects. Trust should bind server operator, artifact digest, deployment identity, configuration, tool inventory, credentials, egress and review state—and should expire when any of those change.
Tool poisoning is adjacent evidence, not the same experiment
Invariant Labs’ 2025 report tested malicious instructions placed in tool descriptions and reported agent behavior that read sensitive files and passed data toward a malicious tool. That threat uses model-visible description text as an indirect instruction channel. My probe uses no language model and no poisoned description. It tests whether structured effect annotations constrain callback execution.
The two mechanisms meet at one design lesson: server-authored metadata enters an AI client from outside the client’s policy boundary. The independent report does not prove that every current client follows poisoned descriptions, and this local probe does not reproduce Invariant’s model experiments. Together they justify treating both free-form descriptions and structured hints as claims that require provenance and consequence controls.
Do not turn a green badge into ambient authority
A UI may reasonably show “read only” when a reviewed server declares readOnlyHint. The unsafe step is using that badge to skip approval, widen credentials or auto-call the tool without an independent policy. If the badge comes directly from unreviewed metadata, it can also create false reassurance for the human who is supposed to notice risk.
Display provenance with the claim: server identity, review state, version, observed effect class and last verification date. Use neutral language such as “server declares read-only” until independent controls enforce the property. When the requested call touches a sensitive tenant, file, repository, mailbox, payment, deployment or external destination, show the exact consequence rather than a generic annotation-derived label.
Authorize the call from trusted facts
Compute permission after the model proposes a tool call and before application code executes it. Bind the decision to authenticated user and tenant, exact server identity and tool name, normalized arguments, target resources, operation class, credential scope, destination, data sensitivity, spend, rate and reversibility. Unknown or dynamically constructed targets should fail closed.
Separate read identities from write identities at the credential layer. A tool advertised as read-only should receive credentials that cannot write, an egress policy that cannot reach undeclared destinations and a filesystem view that excludes mutation targets. Then the annotation can help route the call through a lower-friction path because enforcement exists elsewhere—not because the annotation itself grants permission.
Observe effects instead of trusting labels
Run candidate servers in a disposable environment with synthetic data. Record child processes, filesystem writes, database statements, HTTP methods and destinations, messages, queue publications, secret access and retries. Compare observed behavior with both the declared annotations and the effect budget in your policy. A mismatch is a stop condition even if the task output looks correct.
Repeat calls with the same operation identifier, the same arguments and controlled timeout failures. Verify whether the server deduplicates, returns a prior outcome, applies a second effect or reaches an uncertain state. Test additive and destructive operations separately. For open-world claims, deny egress first, then allowlist one destination and prove that all other resolution paths remain blocked.
Pin change, because a trusted tool can drift
Trust is not permanent. Pin the server artifact or image digest, configuration digest, tool-list digest and credential policy. Alert when annotations, descriptions, input schemas, output schemas or tool names change. Re-run the effect probe when a server release, dependency, deployment identity or policy changes—even when readOnlyHint remains true.
A server can also change its tool list during a session. Treat list-changed notifications as invalidating prior approvals for affected tools. Refresh metadata, recompute the inventory diff and require a new decision for expanded authority. Never let a previously trusted server name cover an unreviewed tool added later.
What failed, and what this probe does not establish
The first sandbox setup initialized npm one directory higher than intended, so I discarded that setup and reran the checked-in probe from the clean publication worktree with pinned dev dependencies. The final run passed eleven assertions. Reporting the setup error matters because dependency resolution, not MCP behavior, caused it; none of the article’s observations come from the discarded layout.
The probe did not test Claude, ChatGPT, Cursor or any production MCP host; remote HTTP or stdio transports; authentication; registries; signatures; approval UIs; model planning; retries after network failure; concurrent calls; database or API effects; Windows or Linux; malicious code; or whether any client incorrectly trusts annotations. It demonstrates non-enforcement in one official SDK path, consistent with the specification’s stated semantics. It does not measure prevalence or prove a vulnerability.
Decision: use annotations as cues only behind an effect firewall
Approve automatic use only when the server and artifact are pinned, the tool inventory is reviewed, credentials independently enforce the claimed effect class, destinations and resources are bounded, observed behavior matches the declaration, retries are safe and any consequential residue is reversible. Keep approval at the consequence boundary when any of those facts depend on user intent or dynamic arguments.
Constrain the tool to a synthetic or read-isolated trial when provenance is incomplete. Reject it when server-authored metadata is the only reason a call bypasses approval, receives write authority, accesses secrets or reaches the open network. Roll back immediately on an undeclared write, destination, child process, credential access, repeated effect or tool-list drift. readOnlyHint can inform policy; it cannot be the policy.
Copy-ready MCP effect-authorization record
Complete this record for one server artifact and one tool. A server-supplied annotation is evidence to inspect, never a control that satisfies a mandatory gate by itself.
Entries stay in this browser tab and are not submitted to AccessAllGPT. Blank responses are copied as [Unresolved].
Operator, canonical server ID, artifact or image digest, signature, deployment identity, transport and review expiry.
Exact name, description digest, input/output schema digests, annotation values, defaults and tool-list change behavior.
Read, additive write, destructive write, external interaction or mixed; derived from code review, credentials and observed behavior.
Authenticated principal, tenant, session, delegated scope and how object-level authorization is recomputed at call time.
Allowed objects, paths, hosts, methods, regions and explicit deny behavior for unknown or model-constructed targets.
Read/write separation, secret exposure, token audience, lifetime and proof that declared read-only tools cannot obtain write authority.
Operation key, deduplication owner, retry window, stored outcome, timeout ambiguity and repeated-call test evidence.
Processes, files, queries, network, messages, outputs and comparison with annotations under synthetic positive and negative controls.
Exact consequence shown, provenance wording, transaction binding, expiry and conditions that prohibit annotation-derived auto-approval.
Undeclared effect, drift, destination, credential use or replay; kill switch, credential revocation, residue cleanup and re-approval owner.
Primary sources
Browse the publication-wide evidence index →
- Tools (version 2026-07-28)Model Context Protocol Specification · Reviewed: Tool definition, annotations, user interaction model, calling tools and security considerations · Retrieved · Supports: The current specification defines annotations as optional descriptions of tool behavior and normatively requires clients to consider them untrusted unless they come from trusted servers.
- Schema reference: ToolAnnotations (version 2026-07-28)Model Context Protocol Specification · Reviewed: ToolAnnotations fields, defaults and semantic qualifications for read-only, destructive, idempotent and open-world hints · Retrieved · Supports: The schema defines readOnlyHint, destructiveHint, idempotentHint and openWorldHint as behavioral hints, including defaults and the conditions under which some fields are meaningful.
- @modelcontextprotocol/sdk 1.30.0 generated specification typesModel Context Protocol TypeScript SDK · Reviewed: ToolAnnotations interface, field comments, trust warning and Tool definition · Retrieved · Supports: The pinned SDK type declarations say every ToolAnnotations property is a hint, may not faithfully describe behavior, and must not drive tool-use decisions when received from an untrusted server.
- TypeScript SDK release 1.30.0Model Context Protocol on GitHub · Reviewed: Release identity, tag, commit, publication date and changed-package record · Retrieved · Supports: The official release page binds the tested SDK package to version 1.30.0 and tag commit 2d889f2, released July 27, 2026.
- MCP Security Notification: Tool Poisoning AttacksInvariant Labs · Reviewed: Threat definition, malicious tool-description example, reported client experiments, limitations and proposed mitigations · Retrieved · Supports: Independent security researchers reported that malicious instructions in MCP tool descriptions could steer tested agents toward unauthorized reads and actions; this is adjacent threat evidence, not validation of this annotation probe.
- Introducing the Model Context ProtocolAnthropic · Reviewed: November 25, 2024 announcement, protocol purpose, architecture, SDKs and early ecosystem description · Retrieved · Supports: Anthropic introduced MCP on November 25, 2024 as an open standard connecting AI assistants to data sources and tools, establishing the chronology and intended AI integration boundary.
Limitations
This is a bounded local reproduction on Node v24.10.0, Darwin arm64, @modelcontextprotocol/sdk 1.30.0 and Zod 3.25.76 using one synthetic in-memory server, one zero-argument tool, one temporary file and two calls. It did not test a language model, agent planner, production MCP client, remote or stdio transport, authentication, registry, signature, approval interface, network failure, concurrency, database, external API, Windows, Linux, malware or real data. It proves that the tested SDK path transports behavioral annotations and dispatches callbacks without enforcing read-only or idempotent effects, which is consistent with the specification; it does not prove a protocol defect, vendor vulnerability, client prevalence or exploitability. The independent Invariant report concerns description-based tool poisoning and does not validate this probe.
Disclosures
AccessAllGPT created and ran only the synthetic local tool in the checked-in harness. No model, production client, third-party MCP server, credential, external endpoint or user data was used. The MCP project, Anthropic, Invariant Labs and SDK maintainers did not review this article or provide access, private data, payment or endorsement. AccessAllGPT Research is operated by NeuralArc, is independent, and is not affiliated with OpenAI, Anthropic, the MCP project or Invariant Labs. Publication-wide relationships are listed on the disclosures page.
Further AccessAllGPT guidance
- MCP Token Passthrough Needs an Audience Boundary
- Before You Connect an MCP Server to Production
- Prompt Injection: Set the Deployment Gates Before Your LLM Can Act
- Before You Give a Coding Agent Repository Access
- Where Human Approval Belongs in AI Automation
- AccessAllGPT Research methodology
- Publication disclosures
Continue the research
Get evidence-led updates for teams making production AI decisions.