---
title: Secure a Cloudflare Workers MCP server with PingOne Advanced Identity Cloud
description: In this configuration, the Cloudflare Workers Model Context Protocol (MCP) server delegates authentication to PingOne Advanced Identity Cloud. This enables clients, such as AI agents, to call a protected API on behalf of an authenticated end user.
component: identity-for-ai
page_id: identity-for-ai:agents:idai-securing-cloudflare-aic
canonical_url: https://developer.pingidentity.com/identity-for-ai/agents/idai-securing-cloudflare-aic.html
section_ids:
  before-you-begin: Before you begin
  stack: Stack
  requirements: Requirements
  structure: Structure
  tasks: Tasks
  configuring-pingone-advanced-identity-cloud: Configuring PingOne Advanced Identity Cloud
  1-enable-dynamic-client-registration-dcr-for-mcp-client-onboarding: 1. Enable Dynamic Client Registration (DCR) for MCP client onboarding
  2-configure-mcp-server-for-token-exchange: 2. Configure MCP server for token exchange
  cors-setup: 3. Setup Cross-Origin Resource Sharing (CORS)
  deploying-to-cloudflare: Deploying to Cloudflare
  testing-the-mcp-server-with-mcp-inspector: Testing the MCP server with MCP Inspector
  accessing-the-remote-mcp-server-from-claude-desktop: Accessing the remote MCP server from Claude Desktop
---

# Secure a Cloudflare Workers MCP server with PingOne Advanced Identity Cloud

In this configuration, the Cloudflare Workers Model Context Protocol (MCP) server delegates authentication to PingOne Advanced Identity Cloud. This enables clients, such as AI agents, to call a protected API on behalf of an authenticated end user.

## Before you begin

This tutorial uses the `remote-mcp-pingone-aic` directory in the Ping Identity [cloudflare-mcp Git repository](https://github.com/pingidentity/cloudflare-mcp/tree/main).

### Stack

| Role            | Name                                                                            | Description                                                 |
| --------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| Platform        | [Cloudflare Workers](https://workers.cloudflare.com/)                           | Serverless execution                                        |
| Framework       | [Hono](https://hono.dev/)                                                       | Lightweight API endpoints                                   |
| Agent Execution | [Cloudflare Agents SDK](https://developers.cloudflare.com/agents)               | Base class for implementing the stateful MCP server         |
| Session State   | [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects) | Provides stateful, isolated storage for each MCP connection |

### Requirements

* Node.js v20 or later

* A PingOne Advanced Identity Cloud tenant

* A Cloudflare account and [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update) enabled

* [Todo API](https://github.com/pingidentity/cloudflare-mcp/tree/main/remote-mcp-pingone-aic/api) deployed

### Structure

```
mcp/
├── package.json               # Dependencies and scripts
├── tsconfig.json              # TypeScript compiler settings
├── wrangler.jsonc             # Worker configuration
└── src/
    ├── index.ts               # Defines the HTTP interface, handling MCP server discovery and MCP server routing
    ├── mcp.ts                 # Stateful MCP server as a cloudflare McpAgent (durable object)
    ├── config.ts              # Worker bindings and durable object session data
    ├── auth.ts                # Manages auth middleware and executes token exchange (delegation grant)
    └── todoApi.client.ts      # HTTP client to the downstream Todo API
```

## Tasks

### Configuring PingOne Advanced Identity Cloud

#### 1. Enable Dynamic Client Registration (DCR) for MCP client onboarding

This step allows MCP clients to automatically register themselves as public OpenID Connect (OIDC) clients and request the necessary scopes to call the downstream Todo API. The MCP client uses the MCP server's protected resource metadata to determine how to register and which scopes to request.

1. In the PingOne Advanced Identity Cloud **Access Management** console, go to Services > OAuth2 Provider.

2. On the **Client Dynamic Registration** tab, enable **Allow Open Dynamic Client Registration**.

3. On the **Advanced** tab, allow the following Todo API scopes:

   * `todo_api:read`

   * `todo_api:write`

#### 2. Configure MCP server for token exchange

This step configures delegation and authorizes the MCP server to act on the end user's behalf. This allows the MCP server to swap the end user's token for a new token used to call the downstream Todo API. This new token is properly audienced for the target API and includes the `act` claim, identifying the MCP server as the trusted intermediary.

1. Enable grant types and scopes:

   1. From the PingOne Advanced Identity Cloud **Access Management** console, go to Applications > OAuth2 > Clients.

   2. Create a new client for the MCP server.

   3. On the **Core** tab, set the scopes to maximum allowable permissions.

   4. On the **Advanced** tab, add the **Client Credentials** and **Token Exchange** grant types to allow the MCP server to request its own token and perform the delegation, respectively.

2. Flag eligible tokens for token exchange with the `may_act` claim:

   Run a script upon user token issuance to inject a claim designating the MCP server as the trusted delegate. Because DCR prevents you from knowing the MCP client's client ID, the script will be applied to all tokens but include safeguards to only target access tokens intended for the MCP server.

   1. In the PingOne Advanced Identity Cloud **Access Management** console, go to Services > OAuth2 Provider > Scripts.

   2. Create a new script of type **OAuth2 May Act**.

   3. Copy and paste the following script, replacing placeholder values with your configuration values:

      ```javascript
      (function () {
        var frJava = JavaImporter(org.forgerock.json.JsonValue);
        var normalizeUrl = function(url) { return url.replace(/\/$/, '') };

        var mcpServerClientId = 'MCP_SERVER_CLIENT_ID';  // placeholder
        var mcpServerUrl = 'URL_OF_DEPLOYED_MCP_SERVER'; // placeholder

        try {
          var requestedResourceList = requestProperties.requestParams.get('resource');
          var requestedResource = String(requestedResourceList.get(0));

          // Only modify tokens intended for the MCP server
          if (normalizeUrl(requestedResource) === normalizeUrl(mcpServerUrl)) {

            // Explicitly set the MCP server as the audience of this token
            token.setField("aud", mcpServerUrl);

            // Authorize MCP server to exchange this token
            var mayAct = frJava.JsonValue.json(frJava.JsonValue.object());
            mayAct.put('client_id', mcpServerClientId);
            mayAct.put('sub', mcpServerClientId);
            token.setMayAct(mayAct);
          };
        } catch (error) {};
      }());
      ```

   4. In the PingOne Advanced Identity Cloud **Access Management** console, go to Services > OAuth2 Provider.

   5. On the **Core** tab, set the **OAuth2 Access Token May Act Script**.

3. Control exchanged token audience with an access token modification script.

   This step ensures the downstream API accepts the exchanged token. Because Advanced Identity Cloud doesn't automatically set the necessary audience claim during token exchange, use an access token modification script tied specifically to the MCP server's client profile.

   1. In the Advanced Identity Cloud **Access Management** console, go to Services > OAuth2 Provider > Scripts.

   2. Create a new script of type **OAuth2 Access Token Modification (Next Gen)**.

   3. Copy and paste the following script, replacing the placeholder value with your downstream API URL:

      ```javascript
      (function () {
        var frJava = JavaImporter(org.forgerock.json.JsonValue);
        var normalizeUrl = function(url) { return url.replace(/\/$/, '') };

        // Only modify MCP server access tokens granted with token exchange
        var grantType = requestProperties.requestParams.get('grant_type');
        if (!grantType || grantType.toString() !== '[urn:ietf:params:oauth:grant-type:token-exchange]') {
          return;
        };

        // Only allow MCP server to call these APIs
        var whitelistedAPIs = [
          'URL_OF_DEPLOYED_API', // placeholder value
        ];

        try {
          var requestedResourceList = requestProperties.requestParams.get('resource');
          var requestedResource = JSON.stringify(requestedResourceList).slice(2, -2);

          for (var i = 0; i < whitelistedAPIs.length; i++) {
            var whitelistedApi = normalizeUrl(whitelistedAPIs[i])
            if (normalizeUrl(requestedResource) === whitelistedApi) {
              // Resulting token will be audienced for the requested & whitelisted API
              accessToken.setField("aud", whitelistedApi);
              break;
            };
          };
        } catch (error) {};
      }());
      ```

#### 3. Setup Cross-Origin Resource Sharing (CORS)

For external MCP clients to communicate with Advanced Identity Cloud during the OAuth flow, explicitly allow the MCP inspector.

1. In the PingOne Advanced Identity Cloud dashboard, go to Tenant Settings > Global Settings.

2. Click **Cross-Origin Resource Sharing (CORS)**, and then **Add a CORS Configuration**.

3. Configure the rule for the local MCP Inspector using the following values:

   1. In the **Accepted Origins** field, enter `http://localhost:6277`.

   2. In the **Accepted Methods** field, enter `GET` and `POST`.

   3. In the **Accepted Headers** field, enter `authorization` and `content-type`.

4. Click **Save CORS Configuration**.

### Deploying to Cloudflare

1. Install dependencies and deploy the MCP server:

   ```
   npm install
   npm run deploy
   ```

2. Use the Wrangler CLI to configure the remote environment variables:

   | Name                        | Description                                        | Example                                             |
   | --------------------------- | -------------------------------------------------- | --------------------------------------------------- |
   | PING\_AIC\_ISSUER           | PingOne Advanced Identity Cloud environment domain | `https://<ENV>.forgeblocks.com:443/am/oauth2/alpha` |
   | MCP\_SERVER\_URL            | URL of the deployed MCP server                     | `https://remote-mcp-ping-aic.<ENV>.workers.dev`     |
   | MCP\_SERVER\_CLIENT\_ID     | ID of the MCP server client                        | `mcp_server`                                        |
   | MCP\_SERVER\_CLIENT\_SECRET | Secret of the MCP server client                    | `[A long, random, alphanumeric string]`             |
   | API\_URL                    | URL of the downstream Todo API                     | `https://todo-api-ping-aic.<ENV>.workers.dev`       |

   ```
   wrangler secret put PING_AIC_ISSUER
   wrangler secret put MCP_SERVER_URL
   wrangler secret put MCP_SERVER_CLIENT_ID
   wrangler secret put MCP_SERVER_CLIENT_SECRET
   wrangler secret put API_URL
   ```

3. Ensure the `mcpServerUrl` is correct in the Advanced Identity Cloud `may_act` script that you created in [step 2.2](#may-act).

### Testing the MCP server with MCP Inspector

1. To validate the deployed MCP server, launch MCP Inspector:

   ```
   npx @modelcontextprotocol/inspector
   ```

   MCP Inspector starts on port 6277, which you allowed in the [CORS configuration](#cors-setup). To confirm that DCR is operational, the Inspector acts as an unregistered client and establishes a secure connection.

   ![A screenshot of the MCP Inspector initiating authentication and self-registering with Advanced Identity Cloud.](_images/idai-mcp-inspector-connect.png)

   > **Collapse: Authentication flow**
   >
   > 1. The Inspector attempts an initial, unauthenticated request to the MCP endpoint (`<mcp_server>/mcp`).
   >
   > 2. The MCP server responds with the protected resource metadata, triggering the discovery process.
   >
   > 3. The MCP inspector consumes the metadata, performs discovery of the authorization server, and self-registers with Advanced Identity Cloud as a public OIDC client.
   >
   > 4. The MCP Inspector initiates the OAuth authorization flow, securing the necessary user consent and establishing a stateful MCP connection.

2. To validate token exchange, observe how the MCP server acquires a new token audienced for the downstream API. Use the `who_am_I` and `peek_api_token_claims` tools to compare the two tokens and confirm the following behavior:

   * Both tokens share the same `sub` claim, confirming they identify the same authenticated end user.

   * The subject token contains the `may_act` claim, explicitly authorizing the MCP server for exchange. The resulting delegation token contains the `act` claim, identifying the MCP server as the trusted intermediary acting on the user's behalf.

   * The subject token is audienced for the MCP server, while the delegation token is audienced for the downstream API.

   * The subject token retains the user's full, broader permissions. The delegation token is limited to only whats necessary for its target audience.

   ![The original end user token and the MCP server delegation token shown side-by-side.](_images/idai-token-exchange-tokens.png)

### Accessing the remote MCP server from Claude Desktop

1. In Claude Desktop, click your profile icon, and then click **Settings**.

2. Go to **Connectors** and then click **Add Custom Connector**.

3. In the **Remote MCP server URL** field, enter the URL in this format: `https://remote-mcp-ping-federate.<ENV>.workers.dev/mcp`

   No OAuth Client ID or Secret is required, as Claude will perform Dynamic Client Registration.

4. Click **Save**.

5. Connect to the MCP server and authenticate with PingFederate.

6. After they're connected, you can ask Claude: "Can you tell me what is in my Todo list?"

7. Claude sees its connected tools and calls the appropriate tool after asking for consent.

**Video (Brightcove)**

\<https\://players.brightcove.net/771836189001/default\_default/index.html?videoId=6387779240112>
