Authentication for MCP servers

As you may have already seen, MCP servers have become the standard way of communication between agents and their tools. Many companies are now creating their own MCP servers and exposing them to be consumed by agents.

In this article, we'll explore how to use OAuth 2.1 to create an authentication layer on top of your existing remote MCP servers.

Prerequisites and things to consider before starting:

  • You should have the basics of how MCP servers work.
  • You should know how remote MCP servers work (over streamable HTTP).
  • We're going to be working with Python-based MCP servers here.

Also, we'll be using ScaleKit to make the process of integrating OAuth 2.1 much more transparent and easy. They have a great team working on this much-needed implementation of authentication for MCP.


Let's get right into it.

Why authentication?

Let's first talk about why we even need authentication with MCP servers.

As you may already know, MCP servers are a great way to connect your services to your agents. These services could be things like your Google Calendar, contacts application, email inbox, databases, documentation, etc.

It all depends on what you're using your agent for. If it's a personal assistant, you'll likely have tools similar to your email inbox and calendar. If it's your coding assistant, it will probably have more tools like access to your documentation or GitHub repositories.

In any case, these services often contain sensitive information that should be protected. This is where authentication comes in. Since agents now have access to more sensitive information, it makes sense to create a standard layer for authentication among agentic systems.

Local Authentication

Now, you may think we already have authentication services for MCP servers—and that's true, especially when you use local MCP servers over the STDIO transport.

All you have to do is go to your service provider (Google Calendar, Confluence, etc.), get your API key, plug it into your configuration file, and run your MCP server over local.std.io with your own API keys.

But that's not the way most people are going to authenticate.

I certainly don't see my grandma or even my non-technical friends going out to find their API keys and plugging them into a JSON configuration file just to get their integration working between their ChatGPT-like assistant and Google Calendar.

This is where OAuth 2.1 comes in. By using OAuth 2.1, we can create a more user-friendly authentication flow that doesn't require users to manually manage API keys. Instead, users can authenticate using their existing accounts (like Google or Microsoft) and grant permission for the MCP server to access their data without exposing sensitive information.

And the good news? OAuth 2.1 works great with remote MCP servers.

The Shift to Remote MCP Servers

So as we mentioned, local MCP servers are great, but they're not meant for the non-technical user.

Local MCP servers have multiple disadvantages, notably requiring more configuration from the user, and they're also less secure (since the code behind the MCP server is running on your machine).

On the other hand, remote MCP servers are more centralized. They run the code on an actual remote server, are stateless, scalable, and accessible to your AI agent via HTTP.

And on top of that, the teams responsible for building and maintaining applications are now developing their MCP servers themselves.

Teams Are Now Building MCP Servers

Up until now, application developers have been responsible for exposing the services delivered by their application via two different interfaces:

  • A graphical user interface for regular users
  • A programmatic interface (API) for developers

Now, we have a third interface that is becoming more necessary: MCP servers, conceived for serving AI agents specifically.

With application developers building and hosting MCP servers themselves, remote MCP servers are quickly becoming the norm.

Now we have the question: how are we going to authenticate with this new interface? Graphical user interfaces use regular GUI authentication, APIs use methods like API keys and bearer tokens, etc. MCP servers will need a new approach to authentication that balances security and usability.

Enter OAuth 2.1.

OAuth 2.1 for MCP Servers

OAuth 2.1 is the latest version of the OAuth protocol, simplifying the authentication process while maintaining a high level of security. It combines the best practices from previous versions and provides a more streamlined approach to authorization.

For MCP servers, OAuth 2.1 offers several advantages:

  1. User-Friendly Authentication: Authenticate using existing accounts (e.g., Google, Microsoft) without managing API keys manually. This makes the process more accessible to non-technical users.

  2. Granular Permissions: Grant specific permissions to the MCP server, limiting its access to only the data it needs. This principle of least privilege enhances security and builds user trust.

  3. Token-Based Authentication: Uses access tokens to authenticate requests, reducing the need to expose sensitive credentials. Tokens can be easily revoked if necessary, providing an additional layer of security.

  4. Support for Multiple Clients: Designed to work with various client types, including web applications, mobile apps, and server-to-server communication. This flexibility makes it an ideal choice for MCP servers that may need to interact with different types of clients.

In summary, OAuth 2.1 provides a robust and user-friendly authentication mechanism for MCP servers, addressing the unique challenges posed by this new interface.

So let's see how we can implement OAuth 2.1 in our MCP server.

Implementation Steps

1. Create an account at ScaleKit

Sign up for a ScaleKit account and create a new project for your MCP server. Be sure to enable full stack authentication.

2. Register Your MCP Server

In the ScaleKit dashboard, register your MCP server as an OAuth 2.1 client. Under Resource Identifier, remember to add the complete URL of where your MCP server will be located.

Since in this example I'm using FastMCP (from the MCP SDK for Python), it automatically hosts my MCP server at the path /mcp of wherever I installed the server. So my Resource Identifier will be:

https://localhost:10000/mcp/

import contextlib
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .tavily_mcp import mcp as tavily_mcp_server

# Create a combined lifespan to manage the MCP session manager
@contextlib.asynccontextmanager
async def lifespan(app: FastAPI):
    async with tavily_mcp_server.session_manager.run():
        yield

app = FastAPI(lifespan=lifespan)

# Add CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # In production, specify your actual origins
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["*"],
)

# Create and mount the MCP server with authentication
mcp_server = tavily_mcp_server.streamable_http_app()
app.mount("/", mcp_server)


    """Main entry point for the MCP server."""
    uvicorn.run(app, host="localhost", port=settings.PORT, log_level="debug")

if __name__ == "__main__":
    main()

As you can see in the code above, I just hosted my MCP server at the root. But it will be accessible at the /mcp path. So I'm going to be registering my MCP identifier like this:

https://localhost:10000/mcp/

Remember to add that trailing dash at the end ☝️.

3. Create authentication middleware

The first thing to do is to create a middleware for your application that will Check if the request contains a valid authentication token. This middleware will do the following:

  1. Check for the presence of the Authorization header in the request.
  2. If the header is missing or invalid, return a 401 authentication error. In that case, include a WWW-Authenticate header in the response, pointing to the well-known endpoint.
  3. If the header is present and valid, extract the token and validate it using the ScaleKit client.

So your authentication middleware will look something like this:

import json
import logging
from fastapi import HTTPException, Request
from fastapi.security import HTTPBearer
from fastapi.responses import JSONResponse
from scalekit import ScalekitClient
from scalekit.common.scalekit import TokenValidationOptions
from starlette.middleware.base import BaseHTTPMiddleware

from .config import settings

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# Security scheme for Bearer token
security = HTTPBearer()

# Initialize ScaleKit client
scalekit_client = ScalekitClient(
    settings.SCALEKIT_ENVIRONMENT_URL,
    settings.SCALEKIT_CLIENT_ID,
    settings.SCALEKIT_CLIENT_SECRET
)

# Authentication middleware
class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        if request.url.path.startswith("/.well-known/"):
            return await call_next(request)

        try:
            auth_header = request.headers.get("Authorization")
            if not auth_header or not auth_header.startswith("Bearer "):
                raise HTTPException(status_code=401, detail="Missing or invalid authorization header")

            token = auth_header.split(" ")[1]

            request_body = await request.body()
            
            # Parse JSON from bytes
            try:
                request_data = json.loads(request_body.decode('utf-8'))
            except (json.JSONDecodeError, UnicodeDecodeError):
                request_data = {}
            
            validation_options = TokenValidationOptions(
              issuer=settings.SCALEKIT_ENVIRONMENT_URL,
              audience=[settings.SCALEKIT_AUDIENCE_NAME],
            )
            
            is_tool_call = request_data.get("method") == "tools/call"
            
            required_scopes = []
            if is_tool_call:
                required_scopes = ["mcp:tools:search:read"] # get required scope for your tool
                validation_options.required_scopes = required_scopes  
            
            try:
                scalekit_client.validate_access_token(token, options=validation_options)
                
            except Exception as e:
                raise HTTPException(status_code=401, detail="Token validation failed")

        except HTTPException as e:
            return JSONResponse(
                status_code=e.status_code,
                content={"error": "unauthorized" if e.status_code == 401 else "forbidden", "error_description": e.detail},
                headers={
                    "WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{settings.SCALEKIT_RESOURCE_METADATA_URL}"'
                }
            )

        return await call_next(request)

As you can see, this middleware checks if the request has a valid bearer token. If it doesn't, it raises an HTTP 401 error with a WWW-Authenticate header that points to the well-known metadata endpoint.

4. Create a well-known metadata endpoint

To help clients understand the authentication requirements, you should create a well-known metadata endpoint that provides information about the expected authentication flows, required scopes, and any other relevant details. This endpoint should be accessible without authentication and return a JSON response with the necessary information.

Here's an example of how to implement this endpoint in your FastAPI application:

from fastapi import APIRouter

router = APIRouter()

@router.get("/.well-known/openid-configuration")
async def well_known_configuration():
    return {
        "issuer": settings.SCALEKIT_ENVIRONMENT_URL,
        "authorization_endpoint": f"{settings.SCALEKIT_ENVIRONMENT_URL}/authorize",
        "token_endpoint": f"{settings.SCALEKIT_ENVIRONMENT_URL}/token",
        "userinfo_endpoint": f"{settings.SCALEKIT_ENVIRONMENT_URL}/userinfo",
        "scopes_supported": ["openid", "profile", "email"],
        "response_types_supported": ["code", "token", "id_token"],
    }

This well-known endpoint provides essential information about the authentication server, including the URLs for various OAuth2 flows and the supported scopes. Clients can use this information to understand how to authenticate with your MCP server and what permissions are required for specific actions.

That is a standard way to use an authentication well-known endpoint. In our case, we're going to be personalizing it for our MCP use case. This is not mandatory, but it is recommended by the spec.

So we can just add another slash at the end and add "MCP," which is the actual path where our resource is located. Then we can just return exactly the same JSON file that Scalekit dashboard gives us—or you can construct it manually as we did in the previous example. My final endpoint looks something like this:


# MCP well-known endpoint
@app.get("/.well-known/oauth-protected-resource/mcp")
async def oauth_protected_resource_metadata():
    """
    OAuth 2.0 Protected Resource Metadata endpoint for MCP client discovery.
    Required by the MCP specification for authorization server discovery.
    """
    return {
        "authorization_servers": [settings.SCALEKIT_AUTHORIZATION_SERVERS],
        "bearer_methods_supported": ["header"],
        "resource": settings.SCALEKIT_RESOURCE_NAME,
        "resource_documentation": settings.SCALEKIT_RESOURCE_DOCS_URL,
        "scopes_supported": [
          "mcp:tools:search:read"
        ],
    }

Notice how I added the scopes right there too. You can set up your custom scopes in the Scalekit dashboard under the permissions section.

Test it out

Now that you have your well-known endpoint set up, you can test it out using a tool like curl or Postman. Simply make a GET request to the well-known endpoint URL:

GET /.well-known/oauth-protected-resource/mcp

You should receive a JSON response with the metadata information you defined in your FastAPI application. This will help you verify that your endpoint is working correctly and returning the expected data.

After you finish setting this up, you should be able to just connect your MCP server to your MCP client (for example, on Cursor or GitHub Copilot) and start using it after following the guided authentication that will open automatically.

Conclusion

In this guide, we covered the essential steps to implement OAuth 2.1 authentication for your MCP server using FastAPI. By setting up a well-known metadata endpoint and defining the necessary scopes, you can ensure a smooth integration with various MCP clients. Remember to test your implementation thoroughly and consult the Scalekit documentation for any specific requirements or best practices.