Launching on Product Hunt today
Blog

Guide5 min read

How to make an MCP server read-only

Connect the server with a credential that can only read, turn on its read-only mode, and hide tools not marked read-only. Only the credential is enforced by the service.

Eldad FuxFounder and CEO, Appwrite

GitHub · read-only

  • github__get_file_contentsRead
  • github__search_issuesRead
  • github__create_issueWrite
  • github__merge_pull_requestWrite

GitHub · read-only

  • github__get_file_contentsRead
  • github__search_issuesRead
  • github__create_issueWrite
  • github__merge_pull_requestWrite

To make an MCP server read-only, connect it with a credential that can only read, turn on the server’s own read-only mode if it has one, and hide tools that aren’t marked read-only. The credential matters most, because it’s the only layer the service itself enforces. The other two depend on labels a server gives its own tools.

What makes an MCP tool read-only?

A tool is read-only when its server says so with the readOnlyHint annotation. The MCP spec defines it as a hint: true means the tool doesn’t modify its environment, and a tool with no annotation defaults to false, so it counts as a write. The spec also tells clients not to make decisions based on annotations from servers they don’t trust.

That leaves three places to enforce read-only access, and they fail in different ways:

LayerWhat it does
Credential
A token or database role with read permissions only. The service refuses writes whatever the tool is called or claims to be.
Server mode
A flag, header or URL that makes the server drop its write tools. Enforced by the server, but only as well as its authors classified each tool.
Tool filter
The client or a gateway hides tools without readOnlyHint. Only as good as the annotations.

Use all three where you can. If you can only do one, scope the credential.

How do I make the GitHub MCP server read-only?

GitHub’s remote server has a read-only mode. Send the X-MCP-Readonly header, and pair it with a fine-grained personal access token that has read access only, for example Contents, Issues and Pull requests set to Read:

mcp.json, the server entry
{
  "type": "http",
  "url": "https://api.githubcopilot.com/mcp/",
  "headers": {
    "Authorization": "Bearer <read-only fine-grained token>",
    "X-MCP-Readonly": "true"
  }
}

If your client can’t set headers, use the read-only URL instead:

URL
https://api.githubcopilot.com/mcp/x/all/readonly

According to GitHub’s server docs, read-only mode takes precedence over everything else: a write tool such as create_issue stays off even if you ask for its toolset by name. The token is the backstop if a tool is ever misclassified.

How do I make a database MCP server read-only?

Connect it as a database role that can only SELECT. Some hosted servers do this for you. Supabase’s server takes read_only=true in its URL and then runs every query as a read-only Postgres user:

URL
https://mcp.supabase.com/mcp?project_ref=<project-ref>&read_only=true

For a server you point at your own Postgres, create the role yourself and give the server only its connection string:

SQL
CREATE ROLE agent_ro LOGIN PASSWORD '<password>';
GRANT CONNECT ON DATABASE app TO agent_ro;
GRANT USAGE ON SCHEMA public TO agent_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO agent_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO agent_ro;

Checking the statement isn’t enough

A tool that only lets through statements starting with SELECT isn’t read-only. A SELECT can call a function that writes, which Supabase’s own docs point out. The database role is what stops it.

Why isn’t hiding write tools enough?

Filtering by annotation is useful, and it’s the easiest layer to turn on, but it has gaps:

  • Annotations are written by the server’s authors. A tool marked read-only can still write, and plenty of tools have no annotations at all.
  • A filter in one client covers that client. The same token used from another client, or a script, can still write.
  • Read-only isn’t the same as harmless. A tool that reads private data and a tool that fetches a URL can, together, send that data somewhere. Look at which servers share a connection, not only at each one.

Hiding tools still pays off: an agent can’t try a tool it never sees, and fewer tool definitions leave more of the context window for the work. Treat it as the convenience layer, and the credential as the control.

How do I check that it worked?

Ask the agent to list the server’s tools and look for anything that creates, updates, sends or deletes. Then ask it to make one harmless write, like a comment on a test issue. With a read-only credential the service should refuse it, even if you put the tool back on. If the write goes through, the credential is the problem, not the filter.

Read-only access in MCP1

MCP1 applies all three layers per group, so the same server can be read-only for one team and writable for another:

  • Read-only grants. A shared gateway is granted to a group with full or read-only access. On a read-only grant, only tools marked read-only are listed and callable, and a tool without the annotation counts as a write.
  • Read-only secrets. A server can hold a second token with read scopes only. People on read-only access call with it, so the service refuses their writes even if a rule in MCP1 were wrong.
  • Members are read-only. A workspace guardrail that holds everyone with the member role to read-only tools, whatever a gateway allows.
  • Each server’s page marks every tool as read-only, destructive or may write, so you can see how well a server annotates before you rely on it.

When someone asks why a tool is missing, their agent can call mcp1__check_access for the reason. Start with a server like GitHub or Supabase, and see 10 steps for rolling out MCP across your company for the order to open writes in.

Common questions

Is readOnlyHint enforced by the MCP protocol?

No. It’s a hint the server gives about its own tool. Clients and gateways can use it to hide tools, but only a read-only credential makes the service refuse writes.

How do I block MCP write tools in Claude Code?

Add deny rules for the tools, like mcp__github__create_issue, in your settings or with --disallowedTools. A deny rule with a bare tool name removes the tool from Claude’s context.

Does a read-only MCP server stop prompt injection?

It stops writes through that server. It doesn’t stop an agent from reading private data and passing it to another tool, so check which servers share a connection.

What if an MCP tool has no annotations?

Treat it as a write. The spec defaults readOnlyHint to false, so a tool without it isn’t read-only.

Give your team one URL

Create a workspace, install your servers and share a gateway with a group. Free for individuals.