A read-only MCP tool can still reveal information to someone who should not see it. Before releasing one, decide which records each caller may read, which fields the tool may return, and how those rules apply to search, pagination, and cached results. Enforce those decisions on the server for every call.

Starting with read-only capabilities can keep your first integration small. It does not remove the need to design access boundaries. A tool that cannot cancel an order may still expose another customer's address, an internal note, or the existence of a confidential project.

This guide develops an illustrative order-status tool. The examples are proposed designs for discussion and testing, not a customer case study or a claim about measured security results.

What does readOnlyHint actually mean?

In the MCP specification dated November 25, 2025, readOnlyHint: true describes a tool that does not modify its environment. Tool annotations are hints, and the specification warns clients against basing tool-use decisions on annotations from untrusted servers. The annotation does not implement a permission check. See the MCP ToolAnnotations reference.

For a product team, two separate questions follow: “Can this action change anything?” and “What can this action disclose?” Record both answers when choosing API actions for your initial MCP tools.

Our recommendation is to review the complete path behind a proposed read-only tool. An endpoint called get_report might also generate a new export, send an email, or update a business workflow. Its name alone cannot establish its behaviour.

Define a small permission contract

Imagine a purchasing service used by several companies. An employee asks an assistant, “When will order ORD-1042 arrive?” The service has an internal order record with delivery information, buyer contact details, commercial terms, and support notes.

For the first version, propose a tool named get_order_status with one required input, order_id. Write this contract before designing its response:

These are example product rules, not universal MCP requirements. Your service might allow a user to access several organizations or restrict orders to individual teams. Make that policy explicit instead of assuming that one login implies access to every record.

If an organization selector is necessary, treat its value as a requested context that must be checked against the caller's permissions. Do not trust an organization identifier simply because it arrived in a tool argument.

Check the record before returning it

OWASP describes broken object-level authorization as a failure to check whether the caller may perform the requested action on the specific object. Knowing or changing an identifier must not be enough to access someone else's record. This applies to UUIDs as well as sequential identifiers. See OWASP API1:2023.

For our illustrative service, the intended flow is:

  1. Resolve the caller from validated authentication context.
  2. Establish the organization and role permitted for this request.
  3. Retrieve the order through a query or service operation constrained by that access policy.
  4. Produce the explicitly allowed status fields.
  5. Return a consistent unavailable response when the caller cannot access the requested order.

The fifth step is a design choice: this example deliberately avoids confirming whether an inaccessible order exists. Apply your service's error policy consistently, including in diagnostic text. A message such as “That order belongs to Company B” would undo the intended boundary.

When the MCP server calls an existing API, establish where this policy is enforced and how caller identity reaches it. A broad backend credential plus an assistant-supplied order_id is insufficient evidence that the user may read that order. Reuse a proven authorization layer where possible, and test the complete MCP-to-API path.

Return a purpose-built view of the record

Record access and field access are separate decisions. OWASP's property-level authorization guidance covers sensitive properties exposed to callers who should not read them, even when they can access the surrounding object. See OWASP API3:2023.

For the delivery question, this proposed result is enough:

{
  "order_id": "ORD-1042",
  "status": "in_transit",
  "estimated_delivery_date": "2026-09-29",
  "updated_at": "2026-09-26T06:45:00Z"
}

The values are fictional. The useful design decision is the explicit selection of fields. Avoid forwarding the entire internal order object and asking the assistant to ignore its sensitive parts: the data would already have left the service.

Use the same field policy for structured output, explanatory text, errors, and linked attachments. A carefully limited JSON object does little good if a second text block repeats the buyer's address. Where additional detail serves a real workflow, define its permissions and purpose separately.

Document the tool's boundaries in its description so callers understand what it can answer. Our guide to writing MCP tool descriptions shows how to make those distinctions clear. The description communicates the rule; server behaviour enforces it.

Apply the boundary to discovery too

The direct lookup is only one route to information. Suppose the service also offers search_orders. In this example, its candidate records must be constrained to the caller's authorized scope before the search result is assembled.

Review every visible part of that result: titles, snippets, totals, filters, and continuation tokens. If the tool hides another company's order rows but reports a total that includes them, it may still disclose information beyond the intended scope.

For a first release, we recommend bounded pages and a documented maximum result size. Continuation requests must reapply the caller's current permissions. An opaque cursor is a navigation mechanism, not proof of access.

Include caches in the design review. If order summaries are cached, a cache hit must still respect the current caller's access. Decide which identity, organization, and policy context belongs in the cache boundary, and what happens after membership is revoked. Test the chosen behaviour instead of relying on a successful first request.

Test denied requests as deliberately as successful ones

Create synthetic fixtures for two organizations, two ordinary members, and one user whose access can be revoked. Write the expected outcomes before running tests. For our example contract:

Run these as direct tool calls as well as through representative assistant requests. A friendly assistant declining a request does not demonstrate that the service would reject the underlying call. Conversely, a strong server boundary should hold even when the caller supplies misleading arguments.

Keep test logs useful without copying private response bodies or credentials into them. For this example, record the scenario, expected decision, actual decision, and which output fields appeared. That gives reviewers concrete evidence of the boundary being tested.

Decide what belongs in the first release

A narrowly scoped order-status tool is easier to evaluate when its identity path, allowed records, and output fields are settled. If any of those decisions remains unresolved, keep that capability out of the initial release or reduce its scope to information intended to be public.

You can use the same review for read-only customer search, document retrieval, invoice lookup, or analytics summaries. The practical deliverable is a permission contract paired with representative allowed and denied requests.

If you are still deciding which capabilities to expose, start with the Agent Readiness Audit. It helps establish a proposed tool scope from your API documentation. Treat authorization verification as a separate implementation and testing responsibility: documentation alone cannot prove that a running service enforces its access rules.