Author(s): neel shah
Originally published on Towards AI.
As an AI engineer working with Message Control Protocol (MCP), I have implemented and evaluated three authentication methods to secure client-server communications: API key-based, JWT-based with a custom implementation, and JWT-based with FastMCP’s built-in authentication. Each method addresses different needs, ranging from simplicity to enterprise-grade security with role-based access control (RBAC). In this blog, I will provide an in-depth exploration of these approaches, including technical details, code snippets, use cases, and best practices, drawn from my practical experience building and testing these systems.
Understanding Authentication in MCP
Message Control Protocol (MCP) enables real-time, tool-based interactions between AI agents (MCP clients) and servers, often using Server-Sent Events (SSEs) for communication. Authentication ensures that only authorized customers can access tools such as TimeTool Or weather_tool. The three methods discussed—API key-based, custom JWT, and FastMCP’s JWT—provide different levels of security and complexity, meeting different application requirements.
1. API Key-Based Authentication
overview
The API key-based authentication uses a static key in the request header, which provides simplicity but limited security. It is ideal for quick prototyping or internal devices where ease of implementation is prioritized.
how it works
- client side: Customer sends one
x-api-keyheader (eg,secretkey) with SSE requests to the server (eg,http://localhost:8100/sse). Lists and invokes tools such as clientweather_tool“What’s the weather like in Dubai?” For questions like - server side:server validates it
x-api-keyheader in acheck_authThe function returns a 401 Unauthorized error if it is invalid. It also supports Basic Auth and Bearer Token for compatibility. - Implementation Details: Built with FastAPI and Starlet, routes requests to the tool after verifying the server key. equipment such as
TimeToolAndweather_toolGet time or weather data from an external API like OpenWeatherMap.
code snippet
# Client: Sending API key
headers = {"x-api-key": "secretkey"}
async with sse_client(url="http://localhost:8100/sse", headers=headers) as (in_stream, out_stream):
async with ClientSession(in_stream, out_stream) as session:
info = await session.initialize()
tools = await session.list_tools()
# Server: Validating API key
def check_auth(request: Request):
api_key = request.headers.get("x-api-key")
if api_key == "secretkey":
return True
raise HTTPException(status_code=401, detail="Unauthorized")
Pros
- Simplicity: Minimal setup for rapid development.
- low overhead: No token management or cryptographic operations.
- compatibility: Works with any HTTP client.
Shortcoming
- security risk: Static keys are vulnerable to leakage (for example, in logs or client code).
- no granularity: Lack of role-based access control (RBAC).
- Scalability issues:Managing multiple keys is cumbersome for large systems.
Example
Best for internal devices or low-security prototypes, such as a simple weather query service or time lookup tool.
2. JWT-based authentication with custom implementation
overview
This method uses JSON Web Tokens (JWT) with a custom token issuance endpoint, which provides time-limited, signed tokens for better security on API keys. It balances security and implementation complexity.
how it works
- client side: Customer sends one
POSTRequest tohttp://localhost:8100/tokenwithclient_id(As,test_client) Andclient_secret(As,secret_1234). The returned JWT is included as a bearer tokenAuthorizationHeaders for SSE requests. - server side: The server validates credentials against a mock client store (
CLIENTSdictionary), issues a JWT with an expiration of 60 minutes, and verifies the token using the HS256 algorithm and a secret key (my_super_secret_key). - Implementation Details: Client query tools like
weather_toolTo fetch the data, and enforce server token validity, ensuring that only authorized clients can access the tool.
code snippet
# Client: Fetching and using JWT
async def get_token():
payload = {"client_id": "test_client", "client_secret": "secret_1234"}
async with aiohttp.ClientSession() as session:
async with session.post("http://localhost:8100/token", json=payload) as resp:
data = await resp.json()
return data("access_token")headers = {"Authorization": f"Bearer {await get_token()}"}
async with sse_client(url="http://localhost:8100/sse", headers=headers) as (in_stream, out_stream):
async with ClientSession(in_stream, out_stream) as session:
info = await session.initialize()
# Server: Generating and validating JWT
@app.post("/token")
def generate_token(request: TokenRequest):
if request.client_id in CLIENTS and CLIENTS(request.client_id) == request.client_secret:
payload = {
"sub": request.client_id,
"exp": datetime.datetime.now() + datetime.timedelta(minutes=60)
}
token = jwt.encode(payload, "my_super_secret_key", algorithm="HS256")
return {"access_token": token}def check_auth(request: Request):
auth = request.headers.get("authorization", "")
if auth.startswith("Bearer "):
token = auth.split(" ", 1)(1)
try:
payload = jwt.decode(token, "my_super_secret_key", algorithms=("HS256"))
return True
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
Pros
- better security: Expiring tokens reduce long-term risk compared to stable keys.
- Adaptation: The payload may include user-specific data such as client ID or role.
- scalable: Centralized token issuance simplifies client authentication for large systems.
Shortcoming
- complexity: Requires a token endpoint and client store management.
- above ground: Token encoding and decoding adds a little latency.
- Limited RBAC: Provides basic access controls compared to advanced frameworks.
Example
Suitable for secure APIs with moderate complexity, such as authenticated weather or time services, where time-limited access is required.
3. JWT-based authentication with FastMCP’s built-in authentication
overview
fastmcp BearerAuthProvider Leverages RSA-signed JWTs with role-based access control (RBAC), making it ideal for enterprise applications requiring fine-grained permissions and strong security.
how it works
- dominant generation: a script (
generate_key.py) RSA creates public/private key pairs, storedmcp_auth/public.pemAndmcp_auth/private.pem. - client side: The client signs a JWT with the private key, including the claims
subject(As,alice),issuer,audienceAndjob_role(As,Manager). The token is sent inAuthorizationHeaders for SSE requests to servers likehttp://localhost:8001/sse. - server side: The server validates JWTs using the public key and implements RBAC, allowing administrators access
approval,inventoryAndorderServers, while officers are limitedorder. equipment such ascreate_project_taskAndget_project_tasksAre restricted based on roles. - Implementation Details: The
MCPServerAdapterManages multi-server connections, and security protocols prompt the user for confirmation before data modification (for example, creating a task).
code snippet
# Client: Generating RSA-signed JWT
with open("mcp_auth/private.pem", "r") as f:
private_key_pem = f.read()
key_pair = RSAKeyPair(private_key=SecretStr(private_key_pem), public_key=public_key_pem)
token = key_pair.create_token(subject="alice", issuer="https://dev-issuer.com", audience="my-mcp-server", additional_claims={"job_role": "Manager", "id": "123", "name": "Alice"})
headers = {"Authorization": f"Bearer {token}"}claims = jwt.decode(
token,
public_key_pem,
algorithms=("RS256"),
audience="my-mcp-server",
issuer="https://dev-issuer.com"
)
print('------------------------------')
print(claims)
print('--------------------------------')
role = claims.get("job_role", "")
print(f"Authenticated as {claims('name')} with role {role} with id {claims('id')}")
# Setup allowed servers by role
if role == "Manager":
servers = (
{"url": "http://localhost:8001/sse", "transport": "sse", "headers": headers}, # HR Management
{"url": "http://localhost:8002/sse", "transport": "sse", "headers": headers}, # Project Management
{"url": "http://localhost:8003/sse", "transport": "sse", "headers": headers}, # CRM
)
permitted_server = ('hr_management', 'project_management', 'crm')
non_permitted_server = ('NA')
elif role == "AssistantManager":
servers = (
{"url": "http://localhost:8001/sse", "transport": "sse", "headers": headers}, # HR Management
{"url": "http://localhost:8002/sse", "transport": "sse", "headers": headers}, # Project Management
)
permitted_server = ('hr_management', 'project_management')
non_permitted_server = ('crm')
else:
servers = ({"url": "http://localhost:8001/sse", "transport": "sse", "headers": headers}) # HR Management only
permitted_server = ('hr_management')
non_permitted_server = ('project_management', 'crm')
# Server: Configuring FastMCP
with open("mcp_auth/public.pem", "r") as f:
public_key = f.read()
auth = BearerAuthProvider(public_key=public_key, issuer="https://dev-issuer.com", audience="my-mcp-server")
mcp = FastMCP(name="ProjectManagementMCP", auth=auth)
Pros
- strong security: RSA signatures provide strong cryptographic guarantees.
- RBAC support: Fine-grained access control for complex systems.
- scalability:Supports multi-server setups with built-in security alerts for data modifications.
- enterprise ready: Designed for production-grade applications.
Shortcoming
- complexity: Requires key pair management and RBAC configuration.
- setup overhead: Generating and distributing RSA keys adds initial effort.
- performance cost: RSA operations are slower than HS256 or API key checking.
Example
Ideal for enterprise systems such as project management platforms, where different roles (for example, manager, assistant manager) need specific access to tools and servers.
Comparison of authentication methods

aspect API key-based JWT Custom JWT FastMCP Security Low (static keys) Medium (expiring tokens) High (RSA, RBAC) complexity low medium high scalability limited good excellent access control None Basic Advanced (RBAC) Example Prototype Secure API Enterprise System
Practical Ideas for AI Engineers
security best practices
- secure storage: Store the API key, JWT secret, and RSA key in environment variables or a secure vault (e.g., AWS Secrets Manager).
- token expiration: With a refresh mechanism for JWTs, use short-term tokens (for example, 60 minutes) to limit exposure.
- HTTPS: Enforce HTTPS for all MCP communications to prevent interception of keys or tokens.
- logging: Avoid logging sensitive data; Use structured logging for audit trails.
Scalability Tips
- centralized authenticator: For JWT-based methods, use a dedicated token issuance service to streamline client authentication.
- caching: Cache RSA public key or token validation results to reduce latency.
- rate limiting: Enforce rate limits on authentication endpoints to prevent abuse.
Implementation Challenges
- API Key: It is difficult to turn and cancel keys in larger systems.
- JWT Custom: Requires robust token refresh logic and client store management.
- FastMCP JWT: Complex key management and RBAC policy design can be time consuming.
choosing the right way
- API key-based: Best for rapid prototyping or internal devices with minimal security requirements.
- JWT Custom: Suitable for secure APIs of moderate complexity, such as certified weather services.
- JWT FastMCP: Ideal for enterprise applications requiring RBAC, such as project management systems.
reference code
Github link: https://github.com/NeelDevenShah/MCP-Auth-Demo
conclusion
Selecting the right authentication method for MCP depends on the security, scalability, and complexity requirements of your application. API key-based authentication provides simplicity but lacks robustness, making it suitable for prototyping. Custom JWT implementation provides a balance of security and flexibility for secure APIs. FastMCP’s RSA-based JWT with RBAC is the gold standard for enterprise-grade systems that require fine-grained access control. By understanding the trade-offs and applying best practices, you can create secure, scalable MCP applications tailored to the needs of your project, from simple tools to complex AI-powered platforms.
Published via Towards AI
