TL;DR:
- The MCP specification includes an OAuth 2.0 authorization framework, but the majority of deployed MCP servers still operate without authentication
- Unauthenticated MCP servers expose your internal tools — databases, APIs, file systems — to any agent that can reach them on the network
- Implementing token-based auth on your MCP servers is straightforward and should be standard practice before any enterprise production deployment
The Model Context Protocol has become the dominant standard for connecting AI agents to external tools. The idea is clean: define a server, expose tools, and any MCP-compatible agent can use them. The problem is that “any agent that can reach the server” is doing a lot of work in that sentence.
Research published earlier this year identified more than 1,800 MCP servers exposed without authentication — many of them inside enterprise networks, providing access to databases, internal APIs, and file systems. The protocol specification always supported authentication, but the tooling was immature, the documentation sparse, and most teams shipped servers without it and left them that way.
That’s changing. The MCP spec now ships with a proper OAuth 2.0 authorization framework, tooling is maturing, and enterprise deployments are starting to enforce it. If you’re running MCP servers in production or planning to, this is what you need to know.
How MCP Authorization Works
MCP’s authorization model follows the OAuth 2.0 framework. At the transport level, MCP-over-HTTP uses bearer tokens — the client includes an Authorization: Bearer <token> header with every request. The server validates the token before processing any tool call.
The spec defines two primary flows:
Authorization Code Flow with PKCE — for user-delegated access, where the agent acts on behalf of a logged-in user. This is appropriate when your agents perform actions on behalf of specific users and need their permissions, not blanket service access.
Client Credentials Flow — for service-to-service access, where the agent itself is the principal. This is the right model for most agent-to-tool authentication: the agent has its own identity, credentials, and permission set, independent of any end user.
The MCP authorization spec also defines a metadata discovery mechanism. Your MCP server exposes a /.well-known/oauth-authorization-server endpoint that tells clients where the authorization server is, what scopes are available, and how to request tokens. This allows agents to discover authorization requirements dynamically rather than requiring hardcoded configuration.
Setting Up Token Validation in a Python MCP Server
Here’s a minimal token validation middleware for a FastMCP-based server:
from fastmcp import FastMCP
from functools import wraps
import httpx
import os
mcp = FastMCP("internal-tools")
# Your authorization server's token introspection endpoint
INTROSPECTION_URL = os.environ["OAUTH_INTROSPECTION_URL"]
CLIENT_ID = os.environ["OAUTH_CLIENT_ID"]
CLIENT_SECRET = os.environ["OAUTH_CLIENT_SECRET"]
async def validate_token(token: str) -> dict | None:
"""Validate a bearer token against the authorization server."""
async with httpx.AsyncClient() as client:
response = await client.post(
INTROSPECTION_URL,
data={"token": token},
auth=(CLIENT_ID, CLIENT_SECRET),
)
if response.status_code != 200:
return None
payload = response.json()
if not payload.get("active"):
return None
return payload
def require_scope(required_scope: str):
"""Decorator to enforce scope-based access control on tools."""
def decorator(func):
@wraps(func)
async def wrapper(*args, token_claims: dict | None = None, **kwargs):
if token_claims is None:
raise PermissionError("Authentication required")
scopes = token_claims.get("scope", "").split()
if required_scope not in scopes:
raise PermissionError(
f"Insufficient scope: required '{required_scope}'"
)
return await func(*args, **kwargs)
return wrapper
return decorator
With this in place, you can scope individual tools to specific permissions:
@mcp.tool()
@require_scope("read:database")
async def query_customer_records(query: str) -> list[dict]:
"""Query the customer database."""
# Tool implementation
...
@mcp.tool()
@require_scope("write:crm")
async def update_customer_status(customer_id: str, status: str) -> dict:
"""Update a customer status in the CRM."""
# Tool implementation
...
Issuing Tokens with Client Credentials
For agent-to-tool authentication, your agents authenticate using client credentials — a client ID and secret that identify the agent rather than a user.
If you’re using an identity provider like Auth0, Okta, or Keycloak, you can configure an application for each agent or agent deployment:
import httpx
import os
async def get_mcp_token(scope: str) -> str:
"""Fetch an access token for MCP tool access."""
async with httpx.AsyncClient() as client:
response = await client.post(
os.environ["OAUTH_TOKEN_URL"],
data={
"grant_type": "client_credentials",
"client_id": os.environ["AGENT_CLIENT_ID"],
"client_secret": os.environ["AGENT_CLIENT_SECRET"],
"scope": scope,
},
)
response.raise_for_status()
return response.json()["access_token"]
# Use in your agent's MCP client configuration
async def create_authenticated_mcp_client():
token = await get_mcp_token("read:database write:crm")
# Pass the token to your MCP client
# Exact API depends on your MCP client library
return MCPClient(
url="https://internal-tools.company.com/mcp",
headers={"Authorization": f"Bearer {token}"},
)
Cache your tokens and refresh before expiry — avoid fetching a new token on every tool call.
Scoping Your MCP Server
The token grants access to the server; scopes control which tools within the server the token can invoke. Design your scope structure to match your access control requirements:
read:crm — read-only CRM access
write:crm — CRM mutation operations
read:database — database query access
admin:config — server configuration and management
Assign the minimal scope set to each agent or workflow. An agent that only reads customer records should have read:crm, not write:crm. If the agent is compromised or produces unexpected behaviour, the damage radius is bounded by its scope.
Deploying Behind a Gateway
For enterprise deployments, consider putting your MCP servers behind an API gateway that handles token validation centrally, rather than implementing validation in every server individually. Tools like Kong, Nginx with OAuth modules, or cloud provider gateways (AWS API Gateway, Azure APIM) can validate tokens, enforce rate limits, and log all tool calls at the network layer.
This centralises your access control policy and makes it easier to rotate credentials, revoke access, or audit what specific agents did during a session — all things that matter when agents are taking real actions in your environment.
The Practical Bottom Line
If you’re running MCP servers that provide access to anything sensitive — databases, internal APIs, code execution, file systems — unauthenticated access is not acceptable in production. The tooling to add OAuth authentication now exists and is not complex. The cost of adding it is an hour or two. The cost of running without it is an agent, prompt injection payload, or compromised session reaching everything your MCP server exposes.
Start with client credentials for service-to-service authentication, scope your tools, and put token validation between the network and your tool handlers. That’s the minimum viable security posture for production MCP deployments.