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.
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
Keys: ← → step · Home / End · K play or pause
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.
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)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 → noread_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 = trueLoad 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)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)Show less
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)Show less
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)Show less
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.
The real config behind this lesson
Excerpts from the repository that builds and deploys this website, quoted as they are.
# 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 # noFrom 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.
# 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.
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.
| Layer | Setting | Enforced by |
|---|---|---|
| Credential | A view role and a short-lived token | The system the server talks to, here the Kubernetes API server |
| Read-only mode | read_only | The MCP server, from tool annotations |
| Toolsets and allow lists | toolsets, enabled_tools, disabled_tools | The MCP server |
| Resource deny list | denied_resources | The MCP server |
| Client deny list | Permission rules such as mcp__server__tool | The MCP client, for example Claude Code |
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.
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.txtList 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/listAsk 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/hostsRegister 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" ] } }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."
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.
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.
Answer in your head, then check
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.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.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.
Related deep dives on this site
Sources
Primary sources checked on the research date. Follow them for the current version.
- Kubernetes MCP Server docs: configuration (read_only, toolsets, denied_resources)
- Kubernetes docs: RBAC authorization and the view role
- MCP specification 2026-07-28: tools and untrusted annotations
- MCP specification 2026-07-28: security best practices (scope minimization)
- Claude Code docs: permission rules for MCP tools