Skip to main content
AI agents and MCP · Lesson 2 of 4 · 12 min

MCP Server Least Privilege: Read-Only Mode and Toolsets

MCP server least privilege from zero: a credential that cannot write, read-only mode, toolsets, allow lists and a client deny list, proven with tools/list.

By Matthews WongReviewed on Checked against MCP specification 2026-07-28

Key takeaway

Least privilege for an MCP server means the agent can reach only what the job needs. Give the server a credential that cannot write, turn on its read-only mode, load only the toolsets you need, deny risky tools in the client, and then prove it by listing the tools a client actually receives.

You will be able to

  • Rank the layers that limit an MCP server, and name the one that holds when every filter fails.
  • Shrink a server's tool list with read-only mode, toolsets, allow lists and a client deny list.
  • Prove the result by reading the tools/list a client actually receives.
Watch it work

Step through the diagram

Press Play, or step with the arrows. Every step is written out below the diagram.

Three owners of permission between an agent and a cluster

A delete request passes the client's rules and the server's filters, then meets the credential, which refuses it; a tools/list audit proves what the server really offers.

Reviewed on 17 October 2026 · MCP specification 2026-07-28

Step 1 of 6. An agent reaches a cluster through three owners of permission: the client's rules, the MCP server's own filters, and the credential the server holds. Each can say no.

Keys: ← → step · Home / End · K play or pause

  1. An agent reaches a cluster through three owners of permission: the client's rules, the MCP server's own filters, and the credential the server holds. Each can say no.

  2. With the core and helm toolsets, the Kubernetes MCP server v0.0.67 advertised 22 tools in this site's audit. Eight of them can change the cluster.

    helm_uninstall · pods_delete · pods_exec · pods_run · …Predict the next step If every server filter were misconfigured, which layer would still refuse a delete? (The server's read-only mode · The credential the server holds)
  3. Bind the server's identity to a role that cannot write, such as Kubernetes' built-in view role. The API server refuses any verb that identity lacks, whatever the agent asks.

    kubectl auth can-i delete pods --as=…:mcp-viewer → no
  4. read_only = true makes the server offer only tools annotated readOnlyHint. The audit's 22 tools became 14. Annotations are labels someone wrote, so this is a second wall.

    read_only = true
  5. Load only the toolsets the job needs, and deny risky tools in the client. In Claude Code, a deny rule naming an MCP tool removes it from the model's context.

    toolsets = core, helm · deny: mcp__kubernetes__helm_uninstallPredict the next step A config file says readonly = true, one underscore short. What did v0.0.67 do? (Refused to start · Started and offered 20 tools)
  6. On v0.0.67 that typo started silently and offered 20 tools, pods_delete among them. Only the list a client receives is evidence, so audit tools/list in CI.

    fail the job if any tool lacks readOnlyHint: true

What does least privilege mean for an MCP server?

An MCP server holds a credential and offers tools that use it. Least privilege means both are only as big as the job: a server that explains why a pod keeps crashing needs to read pods, logs and events, not to delete them or read Secrets. Everything beyond the job is blast radius, the damage one wrong or hijacked call can do.

Read more (2)

The tool list is the blast radius you can see. The site's Kubernetes MCP article audited containers/kubernetes-mcp-server v0.0.67 with the core and helm toolsets: it advertised 22 tools, and 8 of them could change the cluster, including helm_uninstall, pods_delete and pods_exec. An agent asked only to explain a crash loop was being offered all of them.

Least privilege has two halves. One is what the credential can do, enforced by the system the server talks to. The other is what the server offers, enforced by the server and by your client. The first is the boundary; the second keeps the model from even seeing options it should never use.

Which layers say no, and which one is the real boundary?

Put the hard limit in the credential. Give the server its own identity with a read-only role, such as a Kubernetes ServiceAccount bound to the built-in view role with a short-lived token, and keep write credentials somewhere the agent cannot reach. The API server then refuses any verb the identity lacks, whatever the agent asks and whatever the server's filters say.

Read more (3)

Then shrink what the server offers. In the Kubernetes MCP server, read_only = true exposes only tools annotated readOnlyHint=true; toolsets loads only the groups you name, with core and config as the default; enabled_tools and disabled_tools allow or deny tools by name; and denied_resources blocks kinds such as Secret even if the credential is broader than intended. These run inside the server, so they are a second wall, not the first.

Annotations are labels, not physics. The MCP specification says clients must treat tool annotations as untrusted unless they come from a trusted server, and in the audit, helm_install and pods_run were write tools that were not flagged destructive. That is why disable_destructive left 16 tools, two of them able to run any image or chart, while read_only left 14.

Finally, the client can refuse too. In Claude Code, permission rules name MCP tools as mcp__server__tool, deny rules are checked before ask and allow, and a denied tool is removed from the model's context. A client deny list is cheap insurance for the day a server upgrade adds a tool nobody reviewed.

How do you prove the server exposes only what you meant?

Ask the server. A tools/list request returns exactly what a client will offer the model, so list it with the configuration you deploy and fail the check if any tool lacks readOnlyHint=true. A config file is a claim; the tool list is the evidence.

Read more (2)

The site's audit caught the failure this check exists for. A file with readonly = true, one underscore short, started v0.0.67 without complaint and advertised 20 tools under the default toolsets, pods_delete and pods_exec among them. The project's current documentation says unknown keys now fail the load, so the same typo breaks in a different direction depending on the version you pinned.

Prove the credential the same way. kubectl auth can-i answers whether an identity may perform a verb, so check yes for listing pods and no for deleting pods, reading Secrets and exec before the agent ever sees the token. When an incident needs writes, grant them as a separate, time-boxed configuration, then remove it.

How this site does it

The real config behind this lesson

Excerpts from the repository that builds and deploys this website, quoted as they are.

app/components/blogs/KubernetesMcpServerReadOnlyToolsets.tsx:218-221
# Prove the boundary BEFORE the agent ever sees it. These are the checks that matter:
kubectl auth can-i list pods   --as=system:serviceaccount:mcp:mcp-viewer --all-namespaces  # yes
kubectl auth can-i delete pods --as=system:serviceaccount:mcp:mcp-viewer --all-namespaces  # no
kubectl auth can-i get secrets --as=system:serviceaccount:mcp:mcp-viewer --all-namespaces  # no

From the site's Kubernetes MCP article: questions asked of the agent's ServiceAccount before it holds a token, with the answers that should come back. If any no comes back yes, the credential is too broad, and no server setting can make up for that.

app/components/blogs/KubernetesMcpServerReadOnlyToolsets.tsx:233-245
# Only tools annotated readOnlyHint=true are exposed. On v0.0.67 with core + helm
# this left 14 of 22 tools (measured with tools/list, see the verification section).
read_only = true

# Load only what the job needs. The default is ["core", "config"]; helm, kiali,
# kubevirt, tekton and the observability sets are opt-in.
toolsets = ["core", "helm"]

# Belt and braces: even a credential broader than "view" cannot read these kinds.
[[denied_resources]]
group = ""
version = "v1"
kind = "Secret"

The base configuration from the same article. read_only and toolsets shrink what the agent is offered; denied_resources keeps Secrets out of reach even if someone later binds a broader role to the identity. The counts in the comment are the article's own tools/list measurements on v0.0.67.

What breaks

Mistakes that cost real time

  • Treating disable_destructive as read-only

    It hides only tools flagged destructiveHint. On v0.0.67 it left helm_install and pods_run, which can install any chart or run any image the credential allows. Use read_only, or disable those two by name.

  • A typo that fails open

    readonly instead of read_only loaded silently on v0.0.67 and exposed 20 tools. Newer versions reject unknown keys instead. Either way, audit tools/list after every config change and every upgrade.

  • One credential for reading and writing

    If the agent's server holds the same token you deploy with, the filters are all that stand between a prompt and a delete. Mint a separate read-only identity with a short-lived token, and keep write access out of the agent's reach.

  • An allow rule wider than the job

    An allow rule such as mcp__kubernetes__* pre-approves every tool that server offers, including write tools a later version adds. Allow named read tools, and deny the write tools explicitly.

The layers that limit an MCP server, and who enforces each
CredentialA view role and a short-lived tokenThe system the server talks to, here the Kubernetes API server
Read-only moderead_onlyThe MCP server, from tool annotations
Toolsets and allow liststoolsets, enabled_tools, disabled_toolsThe MCP server
Resource deny listdenied_resourcesThe MCP server
Client deny listPermission rules such as mcp__server__toolThe MCP client, for example Claude Code
Try it yourself

Do it on your own machine

You need Node.js 22.19 or newer for the MCP Inspector, and Claude Code for the last two steps. The filesystem reference server stands in for a server that holds access: its allowed directories play the part of the credential, and it refuses paths outside them.

  1. Make a practice folder that holds nothing you care about. It is the only place the server will be allowed to touch.

    mkdir -p ~/mcp-practice && echo hello > ~/mcp-practice/note.txt
  2. List the server's tools and sort them by their readOnlyHint annotation. Note the write tools: write_file, edit_file, move_file and create_directory.

    npx -y @modelcontextprotocol/inspector --cli npx @modelcontextprotocol/server-filesystem ~/mcp-practice --method tools/list
  3. Ask for a file outside the allowed folder. The server should refuse with an error, because /etc is outside its allowed directories.

    npx -y @modelcontextprotocol/inspector --cli npx @modelcontextprotocol/server-filesystem ~/mcp-practice --method tools/call --tool-name read_text_file --tool-arg path=/etc/hosts
  4. Register the server in Claude Code for this project, then deny the four write tools in .claude/settings.json.

    claude mcp add fs -- npx -y @modelcontextprotocol/server-filesystem ~/mcp-practice
    
    {
      "permissions": {
        "deny": [
          "mcp__fs__write_file",
          "mcp__fs__edit_file",
          "mcp__fs__move_file",
          "mcp__fs__create_directory"
        ]
      }
    }
  5. Start Claude Code and ask which fs tools it can call. The four denied tools should be missing, because a deny rule removes a tool from the model's context.

    claude "List every tool you can call from the fs MCP server."
Quick quiz

Three questions before you move on

Pick an answer to see why it is right or wrong. Nothing is graded, so guess freely; a miss is kept in this browser so the course can ask it again later.

  1. 1.Your MCP server's filters are perfect, but the token it holds can delete pods. What really limits a hijacked agent?
  2. 2.What does disable_destructive actually hide?
  3. 3.How do you know read_only actually took effect?
From earlier lessons

One question from an earlier lesson

Recalling an older lesson after a gap is what makes it stick. Answer from memory before you look anything up.

  1. 1.Which transport launches the server as a child process on your machine?

From: 1. What Is an MCP Server? How One Tool Call Travels

Think it through

Answer in your head, then check

  1. 1.Why keep a separate write credential instead of one token plus a read-only server setting?Show the answer

    Answer:A setting inside the server is only as correct as its config and its tool labels. A credential that cannot write is enforced by the system itself, so a typo, an upgrade or a mislabelled tool cannot turn into a delete.

  2. 2.A server upgrade adds a new write tool. Which layers catch it, and which let it through?Show the answer

    Answer:The read-only credential still refuses what the tool tries, and a tools/list audit in CI flags the new name. A broad allow rule such as mcp__server__* pre-approves it silently, which is why named allow rules and explicit deny rules age better.

  3. 3.Why set denied_resources for Secret when the view role already cannot read Secrets?Show the answer

    Answer:It fails safe when the credential drifts. If someone later binds a broader role to the agent's identity, the server still refuses that kind.

Go deeper

Related deep dives on this site