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.
| Signal | Use it for | Do not use it for |
|---|---|---|
readOnlyHint | Risk display, default approvals, policy input | Proof that no mutation is possible |
| Agent key grant | Allowing read tools or write tools | Bypassing upstream authorization |
| Source scope | Limiting which systems the agent can reach | Deciding whether a specific operation is safe |
| Upstream credential | Final capability boundary | Explaining 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 authSPYN3 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
- MCP specification: tools — tool discovery, invocation and security considerations. Verified 14 Oct 2026
- MCP project: tool annotations — risk hints and their enforcement limits. Verified 14 Oct 2026
- PostgreSQL documentation: GRANT — object privileges including SELECT and schema-level grants. Verified 14 Oct 2026
- Anthropic docs: Claude Code MCP setup — Claude Code MCP commands, scopes, transports and JSON configuration. Verified 14 Oct 2026
- VS Code documentation: add and manage MCP servers — VS Code MCP configuration, trust and logging behavior. Verified 14 Oct 2026
- Cursor documentation: MCP integrations — Cursor MCP configuration locations, remote URL entries, headers and approval behavior. Verified 14 Oct 2026
