Skip to content
Blog

Securing MCP Servers: Tokens, Scopes, and Least Privilege

Secure Model Context Protocol (MCP) servers with token-based authentication, granular OAuth scopes, and least-privilege tool execution policies.

Published on September 11, 2026

AI Assistant

The Model Context Protocol (MCP) has emerged as the universal standard for connecting AI models and agents to external tools, databases, and enterprise systems. However, exposing databases and internal microservices via MCP introduces significant attack surfaces. If an unauthenticated or over-privileged MCP server is connected to an agent, a prompt injection vulnerability in the agent can result in unauthorized data exfiltration or remote system modification.

Securing enterprise MCP deployments requires strict Token Authentication, Granular Tool Scopes, and Least-Privilege Authorization.

Security Architecture for MCP Servers

An enterprise-ready MCP server must never expose raw tools without identity verification. The security model relies on three layers:

  1. Transport Layer Security (TLS/mTLS): Enforcing encrypted communication channels for SSE or HTTP transports.
  2. Bearer Token Authentication: Verifying JWT tokens issued by your identity provider (Keycloak, Auth0, Okta) on every incoming MCP request.
  3. Scope-Based Tool Authorization: Restricting available tool capabilities based on the verified caller’s granted scopes.
[Agent Client] --(Bearer JWT + Scopes)--> [MCP Gateway] --> [Token Validator] --> [Tool Execution]

Implementing OAuth Scope Validation in Python MCP Servers

Using the official FastMCP / MCP Python SDK, we construct authenticated tool endpoints that enforce scope boundaries:

from mcp.server.fastmcp import FastMCP, Context
from typing import Optional
import jwt

mcp = FastMCP("Enterprise Database MCP Server")

JWT_SECRET = "YOUR_ENTERPRISE_JWT_SECRET_KEY" # Load from secret manager in production

def verify_token_and_scope(auth_header: Optional[str], required_scope: str) -> dict:
    if not auth_header or not auth_header.startswith("Bearer "):
        raise PermissionError("Missing or invalid Authorization header")
    
    token = auth_header.split(" ")[1]
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
        user_scopes = payload.get("scopes", [])
        if required_scope not in user_scopes:
            raise PermissionError(f"Insufficient privileges. Required scope: '{required_scope}'")
        return payload
    except jwt.PyJWTError as e:
        raise PermissionError(f"Invalid JWT Token: {str(e)}")

@mcp.tool()
def query_customer_orders(customer_id: str, ctx: Context) -> str:
    """Read-only tool: Requires 'mcp:orders:read' scope"""
    # Extract authorization header from request meta
    auth_header = ctx.request_context.meta.get("authorization")
    token_data = verify_token_and_scope(auth_header, "mcp:orders:read")

    # Proceed with scoped execution
    return f"Retrieved order history for customer {customer_id} (Caller: {token_data['sub']})"

@mcp.tool()
def process_order_refund(order_id: str, amount: float, ctx: Context) -> str:
    """Mutating tool: Requires 'mcp:orders:write' scope"""
    auth_header = ctx.request_context.meta.get("authorization")
    token_data = verify_token_and_scope(auth_header, "mcp:orders:write")

    return f"Refund of ${amount} approved for Order {order_id} by {token_data['sub']}"

Best Practices for Least-Privilege MCP Tool Execution

  • Separate Read and Write MCP Servers: Deploy read-only resources and mutating action tools on separate MCP server instances with distinct network policies.
  • Short-Lived Service Tokens: Issue short-lived JWT tokens (e.g., 15-minute expiration) to agent runtimes rather than long-lived API keys.
  • Audit All Tool Invocations: Log tool names, caller identities, target resource parameters, and execution outcomes to centralized SIEM tools.

To review the full spec on transport security, capabilities negotiation, and client-server specs, visit the official Model Context Protocol Specification.