Skip to content
Scalekit Docs
Talk to an Engineer Dashboard

Limit an agent to read-only tools

Mint a read-only session token so an agent sees and calls only the tools that don't change anything, on a Virtual MCP server or a connection's MCP server.

A read-only session token gives an agent only the tools that read data, such as listing, searching and fetching. Tools that send, create, update or delete are left out of the agent’s tool list and refused if it calls them. You set this when you mint the token, for a Virtual MCP server or a connection’s MCP server, with no change to the server.

  • Least privilege per run. An agent that summarizes a user’s inbox or answers questions about their CRM never needs to send an email or edit a record. A read-only token gives it only what the task needs.
  • Prompt injection can’t trigger a write. Agents read content written by other people: emails, tickets, documents. If that content tells the agent to forward a thread or delete a file, the agent has no tool to do it with. Write tools aren’t in its tool list, and a call to one is refused.
  • One field, not a separate server. You don’t build and maintain a second, read-only server for each agent. The same server serves full and read-only runs, and the token decides.
  • Different access per run. Mint a read-only token for a scheduled digest and a full token for a run where the user has approved an action. Tokens are independent, so one run’s access never changes another’s.
  • Same user isolation. A read-only token is still bound to one user and one server, so the agent reads only that user’s data, through their own connected accounts.

Tools are marked by what they do. On a connector’s page, such as Gmail, each tool is labeled Read-only, Write or Destructive. A tool without a read-only marking counts as a write. With a read-only token:

The agentWhat happens
Lists tools (tools/list)Only tools marked read-only are returned, plus connect_account, so the agent can still ask the user to connect an app.
Calls a read-only toolThe call runs as the user, as with a full token.
Calls any other toolThe call is refused with JSON-RPC error -32602, unknown tool "<name>". Nothing reaches the app.

On a Virtual MCP server, the token gets the read-only tools among the tools you chose for that server. On a connection’s MCP server, it gets the read-only tools of that connection.

Tools from an MCP connector you bring are read-only when the upstream MCP server marks them that way, with readOnlyHint. A tool without that marking is left out of a read-only token.

  1. Add the access level when you mint the token. Use READ_ONLY in capitals: the value is case-sensitive.

    For a Virtual MCP server:

    from datetime import timedelta
    session = actions.mcp.create_session_token(
    mcp_config_id=config_id,
    identifier="user_123",
    expiry=timedelta(minutes=15),
    access_level="READ_ONLY",
    )
    session_token = session.token

    For a connection’s MCP server:

    from datetime import timedelta
    session = actions.mcp.create_session_token(
    connection_name="gmail",
    identifier="user_123",
    expiry=timedelta(minutes=15),
    access_level="READ_ONLY",
    )
    session_token = session.token

    Leave the access level out, or set it to FULL, for a token with every tool the server serves.

  2. Pass the token to the MCP client as Authorization: Bearer <session token>, exactly as with a full token. Mint session tokens shows the MCP SDK clients.

List the tools with the read-only token, then with a full one, and compare:

Terminal window
curl -sS "$MCP_SERVER_URL" \
-H "Authorization: Bearer $SESSION_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' |
sed -n 's/^data: //p' | jq -r '.result.tools[].name'

The read-only list has only tools that read, such as gmail_fetch_mails, plus connect_account. Tools such as gmail_send_message appear only with the full token.

To see which tools a read-only token gets before you mint it, read each tool’s read_only_hint from the tool list:

from google.protobuf.json_format import MessageToDict
page, _ = actions.tools.list_scoped_tools(
identifier="user_123",
filter={"connection_names": ["gmail"]},
page_size=100,
)
definitions = [MessageToDict(t.tool)["definition"] for t in page.tools]
print([d["name"] for d in definitions if d.get("annotations", {}).get("read_only_hint") is True])

unknown tool "<name>" when the agent calls a tool

Section titled “unknown tool "<name>" when the agent calls a tool”

The tool isn’t read-only, so a read-only token doesn’t include it. If the agent needs it for this run, mint a full token. Otherwise, tell the agent in its instructions that it can only read.

The value must be exactly FULL or READ_ONLY, in capitals. read_only and other spellings are rejected.

None of the tools on this server are read-only. On a Virtual MCP server, add read-only tools, such as list and search tools, or mint a full token.

API reference

The endpoints this page's code calls, with every field and the SDK method for each: