Skip to main content
This guide demonstrates how to write a MCP server, add Oauth based authentication to it using Okta as the identity provider and then integrate it with the Truefoundry gateway. The setup below explains both the user authentication and machine-to-machine authentication scenarios:
  • User Authentication: Authenticate specific users through the AI Gateway using the Authorization Code flow with refresh tokens
  • Machine-to-Machine Authentication: Enable programmatic access without user interaction using the Client Credentials grant flow
The entire code for the steps described below can be found in this Github link: https://github.com/truefoundry/getting-started-examples/tree/main/calculator-oauth-mcp-server

Guide to creating the MCP server and adding Oauth

1

Write a basic MCP Server and test it locally

Let’s start by writing a basic MCP server that provides a get_me tool.
server.py
from fastmcp import FastMCP

mcp = FastMCP("Demo 🚀")

@mcp.tool
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

@mcp.tool
def subtract(a: int, b: int) -> int:
    """Subtract two numbers"""
    return a - b

if __name__ == "__main__":
    mcp.run(transport="streamable-http", stateless_http=True)
Run the server locally:
python server.py
Your MCP server will be available at http://localhost:8000/mcp. Test the server using this Python script:
test.py
import asyncio
from fastmcp import Client

async def main():
    async with Client("http://127.0.0.1:8000/mcp") as client:
        tools = await client.list_tools()
        print(tools)
        result = await client.call_tool(
            name="add", 
            arguments={"a": 1, "b": 2}
        )
        print(result)

asyncio.run(main())
This MCP server is running without any authentication. We want to enable Oauth authentication on the MCP server. To enable Oauth, we will be creating an Okta App and Authorization server in the next step which will be used to authenticate users and applications.
2

Create an Okta Authorization Server

Okta authorization server is the server that issues tokens to talk to the MCP server. You can either create a new authorization server per MCP server or reuse the same authorization server for all MCP servers.
We recommend keeping the same authorization server for all MCP servers unless you need to create a different one. You can assign different scopes for each MCP server in Okta. You can also do one authorization server per MCP server in case a single MCP server is defining a lot of scopes, but this need should be rare. You shouldn’t be making multiple authorization servers for the same MCP server corresponding to each client. Each client can just have a Okta app which will be added to the same authorization server.The authorization server is common for both user authentication and machine-to-machine authentication. You only need to create it once.
This authorization server will be used for both user authentication (via the Gateway) and machine-to-machine authentication.
  1. Navigate to Security > API in the Okta dashboard
  2. Click Add Authorization Server
  3. Configure with the following:
    • Name: CalculatorMCPAuthServer (or your preferred name)
    • Audience: https://calculator-mcp-server.example.com (this will be your MCP server’s identifier)
    • Description: Authorization server for Calculator MCP servers
  1. Click Save
  2. Note the Issuer URI from the Settings tab (e.g., https://dev-12345678.okta.com/oauth2/aus123abc)
The audience value is an identifier for your API/resource. It doesn’t have to be an actual URL, but using a URL format is a common convention.
  1. Configure Scopes: Define what permissions your application can request.
  • In your Authorization Server, go to the Scopes tab
  • Add custom scopes if needed (e.g., read:data, write:data)
  • For the calculator MCP server, we will add two scopes - calculator.add and calculator.subtract
Its important to add one scope for the MCP server - else Okta refuses to generate the token. If you are unsure about what scope to put, you can create a scope called default for now.
For production systems, define granular scopes that map to specific permissions in your MCP server.
For detailed instructions on customizing authorization servers, see the Okta documentation.
3

Create an Okta App

Okta Oauth Apps are used to authenticate with the authorization server to get the tokens from the authorization server to talk to the MCP server. Different teams or applications can have different apps with different scopes to talk to the same MCP server.
  1. Navigate to Applications > Applications in the Okta dashboard
  2. Click Create App Integration
  3. Select OIDC - OpenID Connect
  4. Select Web Application as the application type
If you only want Machine to Machine authentication, you can create an Okta app of type API Service Integration.
  1. Configure the application:
    • App integration name: CalculatorMCPClient
    • Grant type: Check:
      • Client Credentials (required for machine-to-machine authentication)
      • Authorization Code (required for user authentication via Gateway)
      • Refresh Token (required to enable automatic token refresh)
    • Sign-in redirect URIs: Add https://<your-tfy-control-plane-url>/api/svc/v1/llm-gateway/mcp-servers/oauth2/callback
Authorization Code and Refresh Token are required for the Gateway OAuth integration to enable automatic token refresh. We have also enabled Client Credentials since we will be using the same app in the steps below to allow an application to authenticate to the MCP server (machine-to-machine authentication). If you want to create a separate app for machine-to-machine authentication, you can do that by creating a new app and enabling Client Credentials in the app settings and disable the Client Credentials the app meant for user authentication.
  1. Click Save
  2. Note the Client ID and Client Secret from the application page
Keep your Client Secret secure. Never commit it to version control or expose it in client-side code.
4

Assign the Okta App to the Authorization Server

This will allows the Okta app we created in the previous step to get the tokens from the authorization server for the MCP server we created in Step 2. To enable this, we need to create an access policy and rule in the authorization server.
  1. In your Authorization Server, go to the Access Policies tab.
  1. Click Add New Access Policy
  2. Configure:
    • Name: CalculatorMCPAccessPolicy
    • Description: Policy for MCP server access
    • Assign to: Select your OAuth application
Important: The Assign to field is critical. You must select the OAuth application you created in the previous step. If you don’t assign the policy to your application, the application won’t be able to obtain tokens from this authorization server.
  1. Click Create Policy
  2. Click Add Rule to create a default rule:
    • Rule Name: Default Rule
    • Grant type is: Check Authorization Code and Device Authorization and Client Credentials
    • User is: Any user assigned the app
    • Scopes requested: Any scopes
    • Access token lifetime: 1 hour (or as per your requirements)
  3. Click Create Rule
We have enabled Client Credentials grant type since we will be using the same app in the steps below to allow an application to authenticate to the MCP server (machine-to-machine authentication). If you want to create a separate app for machine-to-machine authentication, you can do that by creating a new app and enabling Client Credentials in the app settings and disable the Client Credentials the app meant for user authentication.
5

Collect Necessary Information

Once you have the OAUTH_ISSUER from your authorization server Settings tab (e.g., https://dev-12345678.okta.com/oauth2/aus123abc), you can access the well-known URL:OAUTH_WELL_KNOWN_URL: {OAUTH_ISSUER}/.well-known/oauth-authorization-serverThe well-known endpoint provides the JWKS URI for token verification.You’ll also need:
  • OAUTH_AUDIENCE: The audience value you configured in the authorization server (e.g., calculator-mcp-server.example.com)
  • CLIENT_ID and CLIENT_SECRET: From your user-facing OAuth application (for Truefoundry MCPGateway integration)
Store these securely - you’ll use them in subsequent steps.
6

Modify MCP server code to add Oauth Token verification

Create a .env file to add the environment variables and modify the server.py file to add the JWT verification.
from fastmcp import FastMCP
import os
from fastmcp.server.auth.providers.jwt import JWTVerifier
from dotenv import load_dotenv

load_dotenv()

# Configure JWT verification using JWKS
token_verifier = JWTVerifier(
    jwks_uri=os.getenv("OAUTH_JWKS_URI"),
    issuer=os.getenv("OAUTH_ISSUER"),
    audience=os.getenv("OAUTH_AUDIENCE"),
)

# Bearer token authentication
mcp = FastMCP("Demo 🚀", auth=token_verifier)

# Forward .well-known/oauth-authorization-server to the actual OAuth server
@mcp.custom_route("/.well-known/oauth-authorization-server", methods=["GET", "HEAD", "OPTIONS"], include_in_schema=False)
async def oauth_well_known(request: Request):
    """Redirect to the upstream OAuth server's well-known endpoint."""
    return RedirectResponse(os.environ.get(f"OAUTH_ISSUER") + "/.well-known/oauth-authorization-server", status_code=307)

@mcp.tool
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

@mcp.tool
def subtract(a: int, b: int) -> int:
    """Subtract two numbers"""
    return a - b

if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000, stateless_http=True)
7

Get the token and call the MCP server in test.py (Machine-to-Machine authentication)

In Step 1, we had a script to test the MCP server locally. After adding the Oauth token verification to the MCP server in the previous step, we need to modify the script to get the token and then call the MCP server. If you call the MCP server without a token, it will return a 401 Unauthorized error.
test.py
import asyncio
from fastmcp import Client
import requests
import base64

# Configuration
TOKEN_ENDPOINT = "https://example.okta.com/oauth2/ksdhflsdjfla/v1/token"
M2M_CLIENT_ID = "klasjflsdjsd"
M2M_CLIENT_SECRET = "xxx-xxx-xxx"
AUDIENCE = "https://calculator-mcp-server.example.com"

# Encode credentials
credentials = base64.b64encode(f"{M2M_CLIENT_ID}:{M2M_CLIENT_SECRET}".encode()).decode()

# Request token
response = requests.post(
    TOKEN_ENDPOINT,
    headers={
        "Authorization": f"Basic {credentials}",
        "Content-Type": "application/x-www-form-urlencoded"
    },
    data={
        "grant_type": "client_credentials",
        "audience": AUDIENCE,
        "scope": "calculator.add"
    }
)

print(response.json())
response.raise_for_status()
token_data = response.json()
access_token = token_data["access_token"]

print(f"Access Token: {access_token}")
print(f"Expires in: {token_data['expires_in']} seconds")

# Call the MCP server
async def main():
    async with Client("https://calculator-mcp-abhay-8000.tfy-usea1-ctl.devtest.truefoundry.tech/mcp", auth=access_token) as client:
        tools = await client.list_tools()
        print(tools)
        result = await client.call_tool(
            name="add", 
            arguments={"a": 1, "b": 2}
        )
        print(result)

asyncio.run(main())
The test.py script above contains the code to get the Okta token and then call the MCP server.
The OAUTH_WELL_KNOWN_URL enables the MCP server to expose the /.well-known/oauth-authorization-server endpoint, which allows the AI Gateway to auto-discover OAuth configuration details. This endpoint redirects to your Okta authorization server’s well-known endpoint.
This is exactly how you will be doing Machine-to-Machine authentication to the MCP server. Code snippets to get the token in different ways are outlined below:
# Set your values
TOKEN_ENDPOINT="https://dev-12345678.okta.com/oauth2/aus123abc/v1/token"
M2M_CLIENT_ID="0oa123abc..."
M2M_CLIENT_SECRET="secret123..."
AUDIENCE="https://your-mcp-server.example.com"

# Encode client credentials
CREDENTIALS=$(echo -n "${M2M_CLIENT_ID}:${M2M_CLIENT_SECRET}" | base64)

# Request access token
curl -X POST "${TOKEN_ENDPOINT}" \
  -H "Authorization: Basic ${CREDENTIALS}" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "scope=my_custom_scope" \
  -d "audience=${AUDIENCE}"
Response:
{
  "access_token": "eyJraWQiOiJxMmFt...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "my_custom_scope"
}
When using a custom authorization server, you can use custom scopes defined in your authorization server. Make sure to include the audience parameter matching the audience configured in your authorization server.
8

Host the MCP server and get the endpoint URL

Now that we have tested the MCP server locally, we will add it to the Gateway to enable user authentication and allow the MCP server to be accessed via the Gateway. To add the MCP server to the Gateway, we need to get the endpoint URL of the MCP server. Hence, we need to host the MCP server on a public URL.
If you are using the Truefoundry AI Deployment product, this can be done by creating a service deployment, choosing your Github repository containing the MCP server code above. Otherwise, you can host it on a VM or a Kubernetes cluster or any hosting provider of your choice.
Remember to add the environment variables for the MCP server:
VariableValueDescription
OAUTH_JWKS_URIhttps://dev-12345678.okta.com/oauth2/aus123abc/v1/keysJSON Web Key Set URI for token verification
OAUTH_ISSUERhttps://dev-12345678.okta.com/oauth2/aus123abcAuthorization server issuer URI
OAUTH_AUDIENCEhttps://your-mcp-server.example.comAudience identifier for your API
The MCP server only needs these environment variables to validate OAuth tokens. It doesn’t need the Client ID or Client Secret since it’s only validating tokens, not generating them.
After deployment, you will have the endpoint URL of the MCP server. Let’s consider it https://calculator-oauth-mcp-server.example.com for the rest of the steps. After deploying, check once using the test.py script above by changing the MCP server URL to the deployed URL. You should be able to fetch the tools from the MCP server.
9

Add the MCP server to the Truefoundry AI Gateway

You will need to have a MCP server group to be able to add the MCP server to the Truefoundry AI Gateway. Please refer to the Getting Started guide to create a MCP server group.
  1. In your MCP Server Group, click Add MCP Server
  2. Select Remote MCP
  3. Configure the server:
    • Name: oauth-mcp-server
    • Description: OAuth-authenticated MCP server with Okta
    • URL: Your deployed service endpoint (e.g., https://calculator-oauth-mcp-server.example.com/mcp).
    • Transport: streamable-http
    • Authentication Type: Select OAuth2
  4. In the OAuth2 configuration section, provide the Okta credentials:
    • OAuth2 Client ID: Your Okta application client ID
    • OAuth2 Client Secret: Your Okta application client secret
The AI Gateway will automatically discover the OAuth2 Authorization URL, Token URL, and other configuration details from your MCP server’s /.well-known/oauth-authorization-server endpoint once you provide the MCP server URL.You can optionally configure:
  • OAuth2 Scopes: The scopes are prefilled, but you can change them if needed to use your custom scopes.
  • Include offline_access in the scopes to enable refresh tokens. This allows the Gateway to automatically refresh expired access tokens without requiring users to re-authenticate.
  • You can store the client id and secrets in truefoundry secrets and reference them by FQN in the configuration.
  1. Set access control: Select teams or users who should have access to this MCP server.
Managers of the MCP Server Group automatically have access to all servers in the group.
  1. Click Save to add the MCP server
    • The server will appear in your MCP Server Group
    • Users can now connect and use the server through the AI Gateway
10

Test the MCP server in the Playground

  1. Navigate to the Playground in the AI Gateway.
  2. Click Add Tool/MCP Servers
  3. Find your calculator-oauth-mcp-server in the list
  4. Click Connect Now to initiate OAuth authorization
  1. You’ll be redirected to Okta to authorize access
  2. Click Allow to grant access
  3. You’ll be redirected back to the Gateway
  4. The AI Gateway will store your OAuth tokens securely and refresh them automatically when they expire.
  5. You’ll see the add and subtract tools from your MCP server
  6. Select the tools and click Done
  7. Try sending a prompt like Add 1 and 2. Use the tools provided
  8. The tool will return the result from your MCP server

FAQ

Machine to Machine Authentication is a type of authentication that allows a machine to authenticate to the MCP server without user interaction. For M2M authentication, you can use the Oauth2 Client Credentials grant type to obtain access tokens directly.
In the guide above, we created an Okta app of type OIDC - OpenID Connect. If you only want Machine to Machine authentication, you can create an Okta app of type API Service Integration. API Service Integrations require custom scopes. OIDC scopes like openid, profile, and email won’t work for API Service Integrations. For detailed instructions on implementing machine-to-machine authentication using the Client Credentials grant type, see the Okta documentation.
Important: You must create at least one custom scope before creating API Service Integration apps.
The following diagram illustrates how multiple services can access the same MCP server, each with their own Okta app and different scopes for fine-grained access control:
Key Points:
  • One App Per Service: Each service accessing the MCP server has its own API Service Integration app, providing isolation and granular access control
  • Shared MCP Server: Multiple services can access the same MCP server, but each uses different Okta apps with different scopes
  • Custom Authorization Server: All apps use the same custom authorization server (not the Org Authorization Server, which is only for Okta APIs)
When using a custom authorization server with your API Service Integration, use the token endpoint and custom scopes. The token endpoint can be obtained from the well-known configuration at {OAUTH_ISSUER}/.well-known/oauth-authorization-server. The response includes the token_endpoint:
{
  "issuer": "https://dev-12345678.okta.com/oauth2/aus123abc",
  "authorization_endpoint": "https://dev-12345678.okta.com/oauth2/aus123abc/v1/authorize",
  "token_endpoint": "https://dev-12345678.okta.com/oauth2/aus123abc/v1/token",
  "jwks_uri": "https://dev-12345678.okta.com/oauth2/aus123abc/v1/keys",
  "grant_types_supported": [
    "authorization_code",
    "refresh_token",
    "client_credentials",
    ...
  ],
  "scopes_supported": [
    "backend",
    "openid",
    "profile",
    "email",
    ...
  ],
  ...
}
Use the token_endpoint value from this response in your token requests.
You can use the context variable in FastMCP to access the token details and user identity in the tool.
@mcp.tool()
def get_me(ctx: Context) -> dict:
    """
    Get authenticated user information from the verified JWT token.
    """    
    claims = get_access_token().claims
    
    return {
        "user_id": claims.get('sub', 'N/A'),
        "uid": claims.get('uid'),
        "issuer": claims.get('iss'),
        "audience": claims.get('aud'),
        "client_id": claims.get('cid'),
        "scopes": claims.get('scp', claims.get('scope', [])),
        "issued_at": datetime.fromtimestamp(claims['iat']).isoformat() if claims.get('iat') else None,
        "expires_at": datetime.fromtimestamp(claims['exp']).isoformat() if claims.get('exp') else None,
        "token_id": claims.get('jti'),
    }
Since Client Credentials doesn’t support refresh tokens, you’ll need to request a new access token when the current one expires. Here’s a generic TokenManager class that handles automatic token renewal (adapt the token endpoint URL and parameters to match your OAuth provider):
import time
import requests
import base64

class TokenManager:
    """Manages OAuth access tokens for machine-to-machine authentication."""

    def __init__(self, token_endpoint, client_id, client_secret, audience, scope=None, **kwargs):
        """
        Initialize the TokenManager.
        
        Args:
            token_endpoint: Your OAuth provider's token endpoint URL
            client_id: Your OAuth client ID
            client_secret: Your OAuth client secret
            audience: The audience value configured in your authorization server
            scope: The scopes for the token (optional, provider-specific)
            **kwargs: Additional provider-specific parameters (e.g., auth_server_id for Okta)
        """
        self.token_endpoint = token_endpoint
        self.client_id = client_id
        self.client_secret = client_secret
        self.audience = audience
        self.scope = scope
        self.extra_params = kwargs
        self.token = None
        self.token_expiry = 0

    def get_token(self):
        """
        Get a valid access token, requesting a new one if necessary.
        
        Returns:
            str: A valid access token
        """
        # Return cached token if still valid (with 5 minute buffer)
        if self.token and time.time() < self.token_expiry:
            return self.token

        # Fetch new token
        credentials = base64.b64encode(
            f"{self.client_id}:{self.client_secret}".encode()
        ).decode()

        data = {
            "grant_type": "client_credentials",
            "audience": self.audience
        }

        if self.scope:
            data["scope"] = " ".join(self.scope) if isinstance(self.scope, list) else self.scope

        # Add provider-specific parameters
        data.update(self.extra_params)

        response = requests.post(
            self.token_endpoint,
            headers={
                "Authorization": f"Basic {credentials}",
                "Content-Type": "application/x-www-form-urlencoded"
            },
            data=data
        )

        response.raise_for_status()
        token_data = response.json()
        self.token = token_data["access_token"]

        # Request new token 5 minutes before actual expiry
        expires_in = token_data.get("expires_in", 3600)
        self.token_expiry = time.time() + expires_in - 300

        return self.token

# Example usage with Okta
token_manager = TokenManager(
    token_endpoint="https://dev-12345678.okta.com/oauth2/aus123abc/v1/token",
    client_id="0oa123abc...",
    client_secret="secret123...",
    audience="https://your-mcp-server.example.com",
    scope=["my_scope"]
)

# Get a token (will be cached until near expiry)
access_token = token_manager.get_token()

# Use the token to make authenticated requests
import requests
response = requests.post(
    "http://localhost:8000/mcp",
    headers={
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    },
    json={
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list"
    }
)

Available Tools

get_me

Returns authenticated user information from the verified JWT token.Example Response:
{
  "user_id": "00u123abc...",
  "uid": "user@example.com",
  "issuer": "https://dev-12345678.okta.com/oauth2/aus123abc",
  "audience": "https://your-mcp-server.example.com",
  "client_id": "0oa123abc...",
  "scopes": ["openid", "profile", "email"],
  "issued_at": "2024-01-01T12:00:00",
  "expires_at": "2024-01-01T13:00:00",
  "token_id": "abc123..."
}