Auth and Permissions
Take notes: Bearer tokens are enough for small internal servers; OAuth is what you reach for on public or multi-tenant servers. Write down the "who + what" split (auth answers who, permissions answer what) so both stay in your head when you start a new server.
The moment your MCP server serves more than one user or lives anywhere beyond your laptop, auth is a first-class concern. This lesson covers the patterns.
When You Need Auth
- stdio server on your own machine: no auth needed (the OS is your auth boundary)
- stdio server on a shared machine: auth via the OS user (whoever launched the client has the client's permissions)
- HTTP server on a private network with trusted users only: auth still recommended (defence in depth)
- HTTP server on the public internet: auth mandatory
The rule: if the network can reach your server and untrusted parties can traverse that network, you need auth.
Bearer Tokens: The Simple Case
For a small team server, bearer tokens are enough. The client sends a token in the Authorization header; the server validates:
from mcp.server.fastmcp import FastMCP, Context
import os
VALID_TOKENS = set(os.environ.get("MCP_TOKENS", "").split(","))
mcp = FastMCP("secure-server")
@mcp.tool()
async def whoami(ctx: Context) -> str:
token = ctx.request.headers.get("Authorization", "").removeprefix("Bearer ")
if token not in VALID_TOKENS:
return "Error: not authenticated"
return f"Authenticated with token starting {token[:8]}..."
The client config includes the token:
{
"mcpServers": {
"team-server": {
"url": "https://mcp.example.com/sse",
"headers": {
"Authorization": "Bearer sk_team_abc123"
}
}
}
}
Distribute the token securely. Rotate periodically. Log unauthenticated attempts.
OAuth: The Multi-User Case
For servers that need per-user identity and permissions, OAuth is the standard. The MCP protocol has a defined OAuth flow: the client discovers the server's auth endpoints, the user goes through an authorisation redirect, the client gets a per-user token.
The Python SDK handles most of the OAuth ceremony. You need to:
- Register your server as an OAuth provider (roll your own or use a hosted identity provider)
- Implement the token-verification callback
- Extract user identity from the request
from mcp.server.fastmcp import FastMCP, Context
mcp = FastMCP(
"multi-user-server",
auth=OAuthProvider(
issuer="https://auth.example.com",
jwks_uri="https://auth.example.com/.well-known/jwks.json",
),
)
@mcp.tool()
async def my_stuff(ctx: Context) -> str:
user_id = ctx.auth.user_id
return json.dumps(get_user_data(user_id))
OAuth is the right choice for public-facing MCP servers where every user has their own data.
Permissions: Not Just Who, But What
Auth answers "who is this?". Permissions answer "what can they do?". Bigger servers need both.
A permission model does not need to be complex. The simplest useful one: read vs write.
@mcp.tool()
def read_customer(customer_id: str, ctx: Context) -> str:
# Any authenticated user can read
return json.dumps(get_customer(customer_id))
@mcp.tool()
def delete_customer(customer_id: str, ctx: Context) -> str:
# Only admins can delete
if "admin" not in ctx.auth.roles:
return "Error: admin role required"
delete_customer(customer_id)
return f"Customer {customer_id} deleted"
Roles or scopes or explicit ACLs; pick a model that matches how your users think about permissions.
Per-Tool vs Per-Server
Some servers make every tool available to every authenticated user. Others enable specific tools only for users with the right role.
Per-server auth is simpler. Per-tool auth is more secure. Use per-tool when the server exposes a mix of harmless and dangerous tools.
Handling Missing or Invalid Auth
Never leak information in auth errors. Bad:
Error: no token found in Authorization header
Good:
Error: not authenticated
Bad:
Error: token not found in database
Good:
Error: not authenticated
Attackers can distinguish "we accept this token format" from "we do not" and iterate. Return a single generic error for any auth failure.
Logging Auth Events
Log every authenticated request with the user identity and the tool called. Log every failed auth attempt with the source IP. Aggregate the logs so you can spot:
- A single IP hitting the server with many failed auth attempts (brute force)
- A single valid token calling every tool in rapid succession (compromise, likely)
- Users calling tools their role does not usually reach for (worth investigating)
Key Takeaway
Auth is bearer tokens for small internal servers, OAuth for anything multi-user or public. Combine auth (who) with permissions (what). Return generic errors on any auth failure. Log everything. Never ship an HTTP MCP server without auth.