> ## Documentation Index
> Fetch the complete documentation index at: https://www.truefoundry.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Add Okta OAuth to a Calculator MCP Server

> Learn how to create and deploy an OAuth2-authenticated MCP server using Okta and FastMCP, then integrate it with TrueFoundry AI Gateway.

This guide demonstrates how to write an MCP server, add Oauth based authentication to it using Okta as the identity provider, and then integrate it with the TrueFoundry AI 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

<Info>
  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](https://github.com/truefoundry/getting-started-examples/tree/main/calculator-oauth-mcp-server)
</Info>

## Guide to creating the MCP server and adding Oauth

<Steps>
  <Step title="Write a basic MCP Server and test it locally">
    Let's start by writing a basic MCP server that provides a `get_me` tool.

    ```python server.py expandable lines theme={"dark"}
    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:

    ```bash theme={"dark"}
    python server.py
    ```

    Your MCP server will be available at `http://localhost:8000/mcp`. Test the server using this Python script:

    ```python test.py theme={"dark"}
    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. Enable OAuth next by registering the server in Okta, then adding JWT verification to the code.
  </Step>

  <Step title="Register the MCP server in Okta">
    Create an authorization server, OAuth app, access policy, and scopes in Okta. Follow [Okta app setup](/docs/ai-gateway/mcp/okta-app-setup) and collect:

    * **OAUTH\_ISSUER** and **OAUTH\_JWKS\_URI** (from the authorization server)
    * **OAUTH\_AUDIENCE**
    * **CLIENT\_ID** and **CLIENT\_SECRET** (for TrueFoundry; the MCP server itself only needs issuer, JWKS, and audience to verify tokens)
  </Step>

  <Step title="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.

    <CodeGroup>
      ```python server.py highlight={6-22} theme={"dark"}
      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)
      ```

      ```yaml .env theme={"dark"}
      OAUTH_JWKS_URI=https://dev-12345678.okta.com/oauth2/aus123abc/v1/keys
      OAUTH_ISSUER=https://dev-12345678.okta.com/oauth2/aus123abc
      OAUTH_AUDIENCE=https://your-mcp-server.example.com
      ```
    </CodeGroup>
  </Step>

  <Step title="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.

    ```python test.py theme={"dark"}
    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.

    <Note>
      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.
    </Note>

    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:

    <Tabs>
      <Tab title="Using cURL">
        ```bash theme={"dark"}
        # 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:

        ```json theme={"dark"}
        {
          "access_token": "eyJraWQiOiJxMmFt...",
          "token_type": "Bearer",
          "expires_in": 3600,
          "scope": "my_custom_scope"
        }
        ```

        <Note>
          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.
        </Note>
      </Tab>

      <Tab title="Using Python">
        ```python theme={"dark"}
        import requests
        import base64

        # Configuration
        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 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",
                "scope": "my_custom_scope",
                "audience": AUDIENCE
            }
        )

        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")
        ```
      </Tab>

      <Tab title="Using Python with requests-oauthlib">
        ```python theme={"dark"}
        from oauthlib.oauth2 import BackendApplicationClient
        from requests_oauthlib import OAuth2Session

        # Configuration
        TOKEN_ENDPOINT = "https://dev-12345678.okta.com/oauth2/aus123abc/v1/token"
        M2M_CLIENT_ID = "0oa123abc..."
        M2M_CLIENT_SECRET = "secret123..."

        # Create OAuth2 session
        client = BackendApplicationClient(client_id=M2M_CLIENT_ID)
        oauth = OAuth2Session(client=client)

        # Fetch token
        token = oauth.fetch_token(
            token_url=TOKEN_ENDPOINT,
            client_id=M2M_CLIENT_ID,
            client_secret=M2M_CLIENT_SECRET,
            scope=["my_custom_scope"]
        )

        access_token = token["access_token"]
        print(f"Access Token: {access_token}")
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Host the MCP server and get the endpoint URL">
    Now that we have tested the MCP server locally, we will add it to the AI Gateway to enable user authentication and allow the MCP server to be accessed via the AI Gateway.
    To add the MCP server to the AI Gateway, we need to get the endpoint URL of the MCP server. Hence, we need to host the MCP server on a public URL.

    <Tip>
      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.
    </Tip>

    Remember to add the environment variables for the MCP server:

    | Variable         | Value                                                    | Description                                 |
    | ---------------- | -------------------------------------------------------- | ------------------------------------------- |
    | `OAUTH_JWKS_URI` | `https://dev-12345678.okta.com/oauth2/aus123abc/v1/keys` | JSON Web Key Set URI for token verification |
    | `OAUTH_ISSUER`   | `https://dev-12345678.okta.com/oauth2/aus123abc`         | Authorization server issuer URI             |
    | `OAUTH_AUDIENCE` | `https://your-mcp-server.example.com`                    | Audience identifier for your API            |

    <Note>
      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.
    </Note>

    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.
  </Step>

  <Step title="Add the MCP server to the TrueFoundry AI Gateway">
    <Note>
      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](/docs/ai-gateway/mcp/mcp-server-getting-started) guide to create a MCP server group.
    </Note>

    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

    <Note>
      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 AI 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.
    </Note>

    5. Set **access control**: Select teams or users who should have access to this MCP server.

    <Note>
      Managers of the MCP Server Group automatically have access to all servers in the group.
    </Note>

    <img src="https://mintcdn.com/truefoundry/m83hJ1C-_e4gZrFO/images/docs/ai-gateway/add-calculator-mcp-server.png?fit=max&auto=format&n=m83hJ1C-_e4gZrFO&q=85&s=e5e311a9e8c0e597dbb8fc4421a4d3f9" width="3840" height="1868" data-path="images/docs/ai-gateway/add-calculator-mcp-server.png" />

    6. 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
  </Step>

  <Step title="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

    <img src="https://mintcdn.com/truefoundry/m83hJ1C-_e4gZrFO/images/docs/ai-gateway/connect-calculator-oauth-mcp-server.png?fit=max&auto=format&n=m83hJ1C-_e4gZrFO&q=85&s=9a63b455d68d82cac5262d7eacfbb28e" width="3840" height="1866" data-path="images/docs/ai-gateway/connect-calculator-oauth-mcp-server.png" />

    5. You'll be redirected to Okta to authorize access
    6. Click **Allow** to grant access
    7. You'll be redirected back to the AI Gateway
    8. The AI Gateway will store your OAuth tokens securely and refresh them automatically when they expire
    9. You'll see the `add` and `subtract` tools from your MCP server
    10. Select the tools and click **Done**
    11. Try sending a prompt like `Add 1 and 2. Use the tools provided`
    12. The tool will return the result from your MCP server

    <img src="https://mintcdn.com/truefoundry/m83hJ1C-_e4gZrFO/images/docs/ai-gateway/chat-with-calculator-oauth-mcp-server.png?fit=max&auto=format&n=m83hJ1C-_e4gZrFO&q=85&s=b34cac868bf4666aa36115c974180e1e" width="3836" height="1864" data-path="images/docs/ai-gateway/chat-with-calculator-oauth-mcp-server.png" />
  </Step>
</Steps>

## Okta setup for On-Behalf-Of (OBO) token exchange

For Okta authorization servers, native app, API Services app, Token Exchange grant, and the manual curl check, see [Okta app setup](/docs/ai-gateway/mcp/okta-app-setup#okta-setup-for-on-behalf-of-obo-token-exchange). Gateway-side manifests and troubleshooting are in [Scenario 7: Okta OBO Token Exchange](/docs/ai-gateway/mcp/mcp-gateway-auth-security#end-to-end-authentication-scenarios).
