Direct answer

Use MCP tool annotations to label intent, but put enforcement in authorization, source scopes and the tool implementation itself. A tool with readOnlyHint true is still only advertising behavior; the runtime must decide whether the caller can invoke it, and the upstream system must be unable to mutate state through that path. Treat every unmarked or unknown tool as write-capable until policy proves otherwise.

Separate description from enforcement

The practical boundary is simple: annotations help an agent client understand risk, while permissions decide whether a call is allowed. MCP tools are discovered through JSON-RPC tools/list and invoked through tools/call. The tool list can include names, descriptions, input schemas and other metadata, but none of that metadata should be treated as a database grant, OAuth scope or firewall rule.

That distinction matters because the model does not call a database table directly. It calls a named tool such as postgres.query, github.create_issue or crm.update_contact. The model can choose poorly, a prompt-injected document can steer it toward a risky call, and an MCP server can be wrong about its own behavior. A robust setup therefore has three layers: the tool tells the client what it claims to do, the MCP gateway or host checks whether this credential may call that kind of tool, and the upstream credential is scoped so the call cannot exceed its intended boundary.

Use source-scoped agent credentials for the runtime layer and reserve annotations for routing, approval prompts and human-readable review. A read-only label without a narrower credential is convenient documentation, not a control.

Use annotations as risk hints

The current annotation vocabulary is intentionally small. The important field for this topic is readOnlyHint, which says the tool does not modify its environment. Related hints include destructiveHint, idempotentHint and openWorldHint. The cautious default is that a tool without annotations is not read-only, may be destructive, is not idempotent and may reach an open world.

{
  "name": "postgres.select_orders",
  "description": "Run an allowlisted SELECT against reporting.orders.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "status": { "type": "string" },
      "limit": { "type": "integer", "maximum": 100 }
    },
    "required": ["status"]
  },
  "annotations": {
    "readOnlyHint": true,
    "openWorldHint": false
  }
}

That example is useful because the client can show a lower-risk approval prompt or allow a stricter policy to auto-run the tool. It is not sufficient because the same MCP server could accidentally run UPDATE orders SET status = ... behind the tool name. The implementation still needs SQL parsing or allowlisting, a read-only database role and a transaction envelope that rejects writes. For untrusted servers, the safe behavior is to display annotations as claims and keep enforcement elsewhere.

Write-capable tools should be explicit. A tool named delete_file or send_invoice should not depend on the model inferring risk from prose. Mark it as non-read-only, make destructive behavior visible, and require a credential grant that is separate from ordinary read access.

Put hard walls below the tool

The upstream system should make the read boundary real. In Postgres, for example, a read-only MCP tool should use a role that can connect to one database, use only the approved schema and select only from approved relations. It should not own the tables, inherit the application role or receive broad grants. If the agent needs business-shaped data, expose views in a reporting schema rather than raw production tables.

CREATE ROLE agent_reader LOGIN PASSWORD 'replace-with-secret';

REVOKE ALL ON DATABASE appdb FROM PUBLIC;
GRANT CONNECT ON DATABASE appdb TO agent_reader;

GRANT USAGE ON SCHEMA reporting TO agent_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA reporting TO agent_reader;

ALTER DEFAULT PRIVILEGES IN SCHEMA reporting
  GRANT SELECT ON TABLES TO agent_reader;

Then keep the MCP tool contract narrower than the database role. Accept one parameterized SELECT, reject stacked statements, apply a row limit and run with a short timeout. A read tool that accepts arbitrary SQL and returns unlimited rows may still be read-only, but it is rarely safe. A read can leak secrets, burn capacity or combine data in ways the requesting user was not entitled to see.

The same pattern applies outside SQL. A GET-only REST connector should only generate tools from safe retrieval operations, not from POST or DELETE endpoints. A file connector should expose picked files, not an entire drive. A write tool should be opt-in both at the source credential and at the agent key.

Gate calls at runtime

When a client sends tools/call, evaluate the decision with the actual caller, source, tool and arguments. Do not rely on the tool list that was shown to the model earlier in the session. The MCP specification allows tool availability to vary by the authorization presented on the request, so a gateway can return one visible set for a read-only key and another for an admin key. The call path should still check again because credentials rotate, scopes change and clients may cache tool lists.

SignalUse it forDo not use it for
readOnlyHintRisk display, default approvals, policy inputProof that no mutation is possible
Agent key grantAllowing read tools or write toolsBypassing upstream authorization
Source scopeLimiting which systems the agent can reachDeciding whether a specific operation is safe
Upstream credentialFinal capability boundaryExplaining intent to the model

A useful policy shape is call:tools for tools that are trusted and marked read-only, plus call:write_tools for mutating tools. If a tool is unannotated, treat it as write-capable until reviewed. If the source is untrusted, require explicit approval or deny the call even if the source says the tool is read-only.

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "postgres.select_orders",
    "arguments": {
      "status": "past_due",
      "limit": 25
    }
  }
}

Log the decision as much as the call: actor, source, tool, annotation snapshot, grant checked, result, latency and whether a human approved it. That makes later review possible when a user asks why an agent could send an email but not update a CRM record.

Configure clients with one narrow endpoint

Client configuration should carry the MCP endpoint and an agent credential, not broad upstream secrets. Claude Code can add a remote HTTP server from the CLI and pass an authorization header. Cursor can use a project-level .cursor/mcp.json with mcpServers. VS Code supports workspace MCP configuration, including a top-level servers object in .vscode/mcp.json.

claude mcp add --transport http company-tools https://api.example.com/agent/mcp --header "Authorization: Bearer $AGENT_MCP_KEY"
{
  "mcpServers": {
    "company-tools": {
      "url": "https://api.example.com/agent/mcp",
      "headers": {
        "Authorization": "Bearer ${env:AGENT_MCP_KEY}"
      }
    }
  }
}
{
  "servers": {
    "company-tools": {
      "type": "http",
      "url": "https://api.example.com/agent/mcp"
    }
  }
}

The important part is not the syntax. It is the credential behind the syntax. Give a coding agent the read-only key for diagnostics and database inspection. Give a release automation agent a separate key that can call narrowly defined write tools. Avoid a single bearer token that can read every source and mutate every connected system because it turns every model mistake into an account-wide incident.

How SPYN3 handles this

A SPYN3 key with call:tools reaches only tools known to be read-only: upstream tools that carry readOnlyHint, plus the Postgres and REST tools SPYN3 generates. Everything else counts as a write and needs call:write_tools on the key. The hint is the upstream server's own claim, so trust it as far as you trust that server. For a read-only key, list_toolsets reports how many tools it is withholding, so the agent knows they exist rather than concluding they don't.

Review the mixed cases

The hard calls are not obvious delete buttons. A tool that creates a draft email writes local state even if it does not send the message. A search tool may be read-only against its own index but open-world because it fetches untrusted external content. A retry of create_ticket may duplicate work unless the tool is idempotent. A report export may be read-only in the database and still risky because it moves private data into a public URL.

Before letting agents use a source, classify each tool with a short review checklist: what state can it modify, what private data can it read, can it communicate outside the workspace, can retries duplicate side effects, and which credential would stop the worst case? Link that review to tool-call audit logs so policy decisions have evidence. If a tool changes state, even gently, put it behind the write grant. If a tool only reads but can expose sensitive records, keep it behind source scopes and upstream read limits. The goal is not to make every read tool frictionless; it is to make the boundary predictable enough that engineers know what a key can actually do.

Built for the next step

Make write access explicit

Use separate read and write grants instead of relying on tool names alone.

Review auth
About SPYN3

SPYN3 is a consolidated MCP layer: one hosted endpoint in front of every system you connect — MCP servers, Postgres, REST APIs, Google Drive — with tools foldered by source and read/write permissions per key. Switching agents means moving one URL. Free plan available; current capabilities are documented on the product facts page.

Sources

  1. MCP specification: tools — tool discovery, invocation and security considerations. Verified 14 Oct 2026
  2. MCP project: tool annotations — risk hints and their enforcement limits. Verified 14 Oct 2026
  3. PostgreSQL documentation: GRANT — object privileges including SELECT and schema-level grants. Verified 14 Oct 2026
  4. Anthropic docs: Claude Code MCP setup — Claude Code MCP commands, scopes, transports and JSON configuration. Verified 14 Oct 2026
  5. VS Code documentation: add and manage MCP servers — VS Code MCP configuration, trust and logging behavior. Verified 14 Oct 2026
  6. Cursor documentation: MCP integrations — Cursor MCP configuration locations, remote URL entries, headers and approval behavior. Verified 14 Oct 2026