
This lesson is part of Learn MCP, our free course on the Model Context Protocol, within The MCP Guide.
In this lesson you will apply least privilege to an MCP server. Least privilege means every part of the system gets only the access it needs for the job in front of it, and nothing more. You will cut the server's reach at four levels: tools, OAuth scopes, downstream credentials and the local machine.
In Lesson 24 you listed threats. Most of them get smaller when the server can do less. A stolen token with read-only access to one folder is a bad day. A stolen token with admin:* can be a disaster.
You will need the server you built in the Building module, or any small server you can edit.
Key Takeaways
- Least privilege in MCP works at four levels: which tools exist, which OAuth scopes a token carries, which downstream credentials the server holds, and what the process can reach on the machine.
- Split read tools from write tools, and mark them with tool annotations so honest hosts can ask for confirmation.
- The spec's Security Best Practices recommend a minimal starting scope set and step-up authorization when a privileged tool is first used.
- Give each server its own narrowly scoped token, passed through its own config, not a broad personal token.
- Annotations and config allowlists are cooperative hints; real enforcement lives in your server code and in sandboxing.
Step 1: Inventory what the server can do today
List every tool, resource and prompt your server exposes. Next to each, write the verbs it performs: read, create, update, delete, send, pay. Then list every credential the server holds and every directory or network host it can reach.
Be honest. Many first servers expose one big run_query or call_api tool. That single tool can do anything the credential behind it can do.
Step 2: Cut and split your tools
Remove anything the use case does not need. Then split what remains so each tool does one kind of thing:
- Separate
list_invoices(read) fromvoid_invoice(destructive). - Replace a free-form
run_sqltool with narrow tools such asget_customer_balance(customer_id). - Use enums and ranges in input schemas so the model can't pass surprising values. Lesson 23 covers schema design.
Narrow tools also help the model pick the right one, so security and usability point the same way here.
Step 3: Mark tools with annotations
The MCP schema defines four tool annotation hints: readOnlyHint, destructiveHint, idempotentHint and openWorldHint, plus a human-readable title. Hosts can use them to decide when to ask the user for confirmation.
In the Python SDK v2, the tool() decorator accepts an annotations argument, and the Python field names are snake_case. This sketch is adapted from the Python SDK 2.3.0 source on PyPI. We did not run it in this session.
from mcp.server import MCPServer
from mcp.types import ToolAnnotations
mcp = MCPServer("Invoices")
@mcp.tool(annotations=ToolAnnotations(read_only_hint=True))
def list_overdue_invoices(customer_id: str) -> list[dict]:
"""List overdue invoices for one customer. Does not change anything."""
...
@mcp.tool(
annotations=ToolAnnotations(
read_only_hint=False, destructive_hint=True, idempotent_hint=True
)
)
def void_invoice(invoice_id: str, reason: str) -> str:
"""Void one invoice. This cannot be undone."""
...
Two warnings from the spec. The Tools page says clients must treat annotations as untrusted unless they come from trusted servers. And the schema calls them "hints" that may not describe the tool faithfully. So annotations help honest hosts. They never replace checks inside void_invoice itself.
Step 4: Design minimal OAuth scopes
If your server is remote and uses OAuth, scopes are the main privilege boundary. The spec's Security Best Practices page lists the mistakes to avoid:
- Publishing every possible scope in
scopes_supported. - Wildcard or omnibus scopes such as
*,allorfull-access. - Bundling unrelated privileges "to preempt future prompts."
- Treating the scopes in a token as enough, without server-side authorization logic.
Instead, start with a small read-only scope and ask for more only when a privileged tool is used. The authorization spec shows the server's side of that step-up: a 403 with an insufficient_scope error that names only what the operation needs.
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
scope="files:write",
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
error_description="File write permission required for this operation"
The client then requests a new token that adds files:write to what it already had. The best-practices page also says servers should accept reduced-scope tokens and log elevation events. Lesson 22 covers the OAuth setup itself.
Step 5: Narrow the credentials your server holds
Your server also holds credentials for the APIs behind it. Apply the same rule there:
- Use a service account or API key limited to the data the tools touch. For example, a key that can read invoices but not change bank details.
- Never forward the user's MCP token to the downstream API. The spec says servers "MUST NOT accept any tokens that were not explicitly issued for the MCP server." See why token passthrough is forbidden.
- For stdio servers, the authorization spec says to retrieve credentials from the environment instead of running the OAuth flow.
The official Local Server Security guide adds practical rules. Pass secrets per server in the client's config, not in your shell profile, so each server sees only its own. Give each server its own token "with the smallest scope that works." Prefer the operating system's secret store over plaintext config files.
Step 6: Fence off the machine
A local server is an ordinary process. The same guide says it can read anything your account can read, including SSH keys and cloud credentials, and open outbound connections anywhere. So:
- Files: grant one project directory, read-only if possible. Never your home directory.
- Network: deny network access if the server needs none, or restrict it to the API hosts it wraps.
- Isolation: run the server in a container or sandbox where the allowed folder is the only filesystem it can see.
The guide warns that config-level allowlists are "cooperative boundaries." An honest server respects them. A malicious one can ignore them. Enforcement comes from the sandbox.
One spec change matters here. The 2026-07-28 spec deprecated the Roots feature, which clients used to tell servers which folders were in scope. The changelog suggests passing directories through tool parameters, resource URIs or server configuration instead.
Step 7: Bind state to the user
If your server hands out IDs for carts, drafts or workflows, treat them as data, not as keys. The spec says servers "MUST NOT treat possession of a state handle as authentication." Generate handles with a secure random generator and store state keyed by the authenticated user ID, so a guessed handle is useless to anyone else.
Try it
Take the server from your Lesson 24 threat model.
- Fill in this table for every tool:
| Tool | Verbs | Annotations | Scope needed | Downstream permission |
|---|---|---|---|---|
list_overdue_invoices | read | read-only | invoices:read | read invoices |
void_invoice | delete | destructive, idempotent | invoices:write | void invoices |
- Find one tool you can delete or split.
- Write the smallest scope a new user needs on day one.
- Open your client config and check that each server has its own environment block and its own token.
If any row needs admin or a wildcard, redesign that tool before you move on.
Recap
You inventoried what your server can do, removed and split tools, and added annotations so hosts can ask before risky actions. You designed a minimal scope set with step-up, narrowed downstream credentials, fenced off files and network, and bound state handles to users. Each layer shrinks the damage of a single mistake.
In Lesson 26 you will make sure you can see what your server does, with logging and monitoring.
Course navigation
Previous lesson: Lesson 24: Threat-Model Your Server.
Next lesson: Lesson 26: Logging and Monitoring.
Back to the Learn MCP course outline.
Frequently asked questions
Isn't least privilege annoying for users?
It can be if done badly. The step-up pattern in the spec is designed to reduce that friction: users grant basic access first and see a consent prompt only when they use a privileged feature. The spec also notes that long scope lists cause "consent abandonment," so asking for less can help adoption.
Do annotations make my server safe?
No. They are hints for hosts, and the spec tells clients not to trust them from untrusted servers. Your server must still validate inputs, check permissions and rate limit calls, which the Tools page makes a server MUST.
What if my downstream API only offers one all-powerful key?
Put your own limits in the server code: allow only specific operations, validate every argument, and log every call. Then ask the vendor about scoped keys. Note the gap in your threat model as an accepted risk.
Does least privilege apply to remote servers I host?
Yes. The same logic applies to the hosting account, the database user and the cloud role your server runs as. Our guide to excessive permissions in MCP servers covers common patterns.
Where can I find more on MCP security?
The MCP Security Center collects our threat pages, checklists and templates in one place.











