# Cross App Access quickstart

This quickstart builds a working Cross App Access (XAA) gateway entirely in the
Zuplo Portal. An MCP client connects to the gateway with ordinary OAuth; the
gateway then performs the XAA token exchange against
[Okta's hosted xaa.dev playground](https://xaa.dev) and reaches the protected
Todo0 MCP server on the user's behalf.

There is no repository to clone and no identity tenant to stand up. Everything
runs in the portal against the public xaa.dev playground.

## Why xaa.dev

[xaa.dev](https://xaa.dev) is Okta's hosted Cross App Access playground: a
public identity provider, resource authorization server, and Todo0 MCP server
that are already wired together for the XAA token exchange. Standing up your own
XAA stack means provisioning an IdP, a resource authorization server, and a
protected MCP server — and configuring the trust relationships between all
three. The playground gives you all of that for free, so today it's the fastest
way to see a real Cross App Access flow end to end. Once the gateway works
against xaa.dev, swapping in your own identity provider and upstream is just a
change of endpoints and credentials.

## What you'll build

A single gateway route with two policies does all the work:

- An **MCP OAuth** policy (`mcp-oauth-inbound`) secures the client → gateway
  leg. The client authenticates to the gateway with plain MCP OAuth — this leg
  is not XAA.
- A **token-exchange** policy (`mcp-token-exchange-inbound`) in `id-jag` mode
  performs the outbound XAA exchange to the upstream: it mints an ID-JAG at the
  playground identity provider (IdenX), redeems it at the resource authorization
  server, and calls the Todo0 MCP server with the resulting access token.

The XAA exchange happens entirely on the gateway's outbound side. See
[the overview](./overview.mdx#how-the-flow-works) for the full sequence.

## Prerequisites

- A [Zuplo account](https://portal.zuplo.com)
- A free account on [xaa.dev](https://xaa.dev)
- [MCP Jam](https://www.mcpjam.com) — a browser-based MCP client used to drive
  the flow. Any MCP client that supports OAuth works.

## Steps

<Stepper>

1. **Create a Zuplo project.**

   In the [Zuplo Portal](https://portal.zuplo.com/+/account/projects), select
   **New Project** and create an empty **API Gateway** project. Name it
   `xaa-quick-start`.

   On the project **Overview** page, copy the deployment URL shown at the top
   (for example, `https://xaa-quick-start-main-abc1234.d2.zuplo.dev`). This is
   your gateway's public origin — you'll need it for the next two steps.

   :::note

   Throughout this guide, replace `https://your-gateway.zuplo.dev` with your
   project's actual deployment URL.

   :::

2. **Register a requesting app on xaa.dev.**

   Sign in to [xaa.dev](https://xaa.dev) and open
   [the developer registration page](https://xaa.dev/developer/register). Enter
   your email, then select **Register New App** and fill in:
   - **Application Name** — anything, for example `xaa-quick-start gateway`.
   - **Redirect URIs** — your gateway's OAuth callback:

     ```text
     https://your-gateway.zuplo.dev/__zuplo/oauth/callback
     ```

   Under **Resource Connections**, select **Todo0 MCP Server**, keep both scopes
   (`todos.read` and `mcp.access`) checked, and select **Add Connection**. Then
   select **Register App**.

   <BrowserScreenshot url="https://xaa.dev/developer/register">

   ![Registered requesting app on xaa.dev](../../../public/media/cross-app-access/xaa-playground.png)

   </BrowserScreenshot>

   Registration shows four values. Copy all of them — the secrets are shown only
   once:

   | xaa.dev value          | Used for                        |
   | ---------------------- | ------------------------------- |
   | Client ID              | `XAA_CLIENT_ID`                 |
   | Client Secret          | `XAA_CLIENT_SECRET`             |
   | Resource Client ID     | `XAA_RESOURCE_AS_CLIENT_ID`     |
   | Resource Client Secret | `XAA_RESOURCE_AS_CLIENT_SECRET` |

   :::tip

   The playground identity provider (IdenX) accepts any email with no password,
   so you can sign in as any test user when you drive the flow.

   :::

3. **Add the gateway configuration.**

   Open the project's code editor (the **Code** tab). The gateway needs three
   files.

   First, define the two policies. Open `config/policies.json` (use the raw
   `policies.json` tab, not the visual Policy List) and replace its contents:

   ```json title="config/policies.json"
   {
     "policies": [
       {
         "name": "xaa-inbound",
         "policyType": "mcp-oauth-inbound",
         "handler": {
           "module": "$import(@zuplo/runtime/mcp-gateway)",
           "export": "McpOAuthInboundPolicy",
           "options": {
             "oidc": {
               "issuer": "https://idp.xaa.dev",
               "jwksUrl": "https://idp.xaa.dev/jwks",
               "audience": "$env(GATEWAY_AUDIENCE)"
             },
             "browserLogin": {
               "url": "https://idp.xaa.dev/authorize",
               "tokenUrl": "https://idp.xaa.dev/token",
               "clientId": "$env(XAA_CLIENT_ID)",
               "clientSecret": "$env(XAA_CLIENT_SECRET)",
               "scope": "openid profile email",
               "audience": "$env(GATEWAY_AUDIENCE)",
               "pkce": "S256"
             }
           }
         }
       },
       {
         "name": "id-jag-upstream",
         "policyType": "mcp-token-exchange-inbound",
         "handler": {
           "module": "$import(@zuplo/runtime/mcp-gateway)",
           "export": "McpTokenExchangeInboundPolicy",
           "options": {
             "id": "id-jag-upstream",
             "displayName": "Todo0",
             "summary": "xaa.dev Todo0 MCP server, reached via Cross App Access (ID-JAG).",
             "authMode": "id-jag",
             "idJag": {
               "scopes": ["todos.read", "mcp.access"],
               "idp": {
                 "tokenUrl": "https://idp.xaa.dev/token",
                 "clientAuth": {
                   "method": "client_secret_post",
                   "clientId": "$env(XAA_CLIENT_ID)",
                   "clientSecret": "$env(XAA_CLIENT_SECRET)"
                 }
               },
               "resourceAs": {
                 "tokenUrl": "https://auth.resource.xaa.dev/token",
                 "audience": "https://auth.resource.xaa.dev",
                 "resource": "https://mcp.xaa.dev/mcp",
                 "clientAuth": {
                   "method": "client_secret_post",
                   "clientId": "$env(XAA_RESOURCE_AS_CLIENT_ID)",
                   "clientSecret": "$env(XAA_RESOURCE_AS_CLIENT_SECRET)"
                 }
               }
             }
           }
         }
       }
     ]
   }
   ```

   The IdP, resource authorization server, and upstream endpoints are wired to
   the playground here. Only the credentials and the gateway audience are
   environment-driven, so you can plug in your own xaa.dev app. For every
   option, see the [configuration reference](./policy-reference.mdx).

   Next, add the route that exposes the gateway. Open `config/routes.oas.json`
   (the raw `routes.oas.json` tab) and replace its contents:

   ```json title="config/routes.oas.json"
   {
     "openapi": "3.1.0",
     "info": {
       "version": "1.0.0",
       "title": "XAA MCP Gateway",
       "description": "MCP gateway that bridges to the xaa.dev Todo0 MCP server via Cross App Access (ID-JAG)."
     },
     "paths": {
       "/mcp/todo0": {
         "post": {
           "operationId": "todo0Bridge",
           "summary": "Bridge to the xaa.dev Todo0 MCP server",
           "x-zuplo-route": {
             "corsPolicy": "none",
             "handler": {
               "module": "$import(@zuplo/runtime/mcp-gateway)",
               "export": "McpProxyHandler",
               "options": {
                 "rewritePattern": "https://mcp.xaa.dev/mcp"
               }
             },
             "policies": {
               "inbound": ["xaa-inbound", "id-jag-upstream"]
             }
           }
         }
       }
     }
   }
   ```

   The `McpProxyHandler` forwards the request to the upstream Todo0 server, and
   both policies run inbound — first the MCP OAuth check, then the XAA exchange.

   Finally, register the MCP Gateway plugin. In the file tree, right-click the
   `modules` folder, select **New Runtime Extension**, and replace the generated
   file's contents:

   ```typescript title="modules/zuplo.runtime.ts"
   import { RuntimeExtensions } from "@zuplo/runtime";
   import { McpGatewayPlugin } from "@zuplo/runtime/mcp-gateway";

   // Registers the MCP Gateway, which adds the OAuth and upstream-connection
   // routes used to expose and secure MCP servers through your gateway.
   // Docs: https://zuplo.com/docs/mcp-server/introduction
   export function runtimeInit(runtime: RuntimeExtensions) {
     runtime.addPlugin(new McpGatewayPlugin());
   }
   ```

   Save each file. The build fails until the environment variables exist —
   that's the next step.

4. **Set the environment variables.**

   Open **Settings →
   [Environment Variables](https://portal.zuplo.com/+/account/project/settings/environment-variables)**
   and add each variable below with **Add variable**. Mark the two `*_SECRET`
   values as **Secret** so they're encrypted and hidden after saving. Leave all
   three environments (Production, Preview, Development) selected.

   | Variable                        | Value                                                                         | Secret |
   | ------------------------------- | ----------------------------------------------------------------------------- | ------ |
   | `GATEWAY_AUDIENCE`              | Your gateway's deployment URL (for example, `https://your-gateway.zuplo.dev`) | No     |
   | `XAA_CLIENT_ID`                 | Client ID from the xaa.dev app                                                | No     |
   | `XAA_CLIENT_SECRET`             | Client Secret from the xaa.dev app                                            | Yes    |
   | `XAA_RESOURCE_AS_CLIENT_ID`     | Resource Client ID from the Todo0 connection                                  | No     |
   | `XAA_RESOURCE_AS_CLIENT_SECRET` | Resource Client Secret from the Todo0 connection                              | Yes    |

   Saving an environment variable triggers a new deployment. Once it finishes,
   the gateway is live.

5. **Connect with MCP Jam and list the todos.**

   Open the [MCP Jam web inspector](https://www.mcpjam.com), go to **Connect**,
   and select **Add Server**:
   - **Server Name** — `xaa-quick-start`
   - **Connection Type** — `HTTPS`, with the route URL
     `https://your-gateway.zuplo.dev/mcp/todo0`
   - **Authentication** — `OAuth`

   <ModalScreenshot>

   ![Add the gateway as an MCP server in MCP Jam](../../../public/media/cross-app-access/add-mcp-server.png)

   </ModalScreenshot>

   Select **Add Server**. MCP Jam starts the OAuth flow and redirects you to
   sign in. Sign in at the IdenX screen (any email, no password) and approve the
   consent screen. MCP Jam returns to the inspector and the server shows as
   **Connected**.

   <Framed>

   ![The gateway connected in MCP Jam](../../../public/media/cross-app-access/connected-card.png)

   </Framed>

   Open the **Resources** panel and select **List all todos**. The todos come
   back as JSON — the agent never touched the XAA protocol. Behind the scenes
   the gateway minted an ID-JAG from the playground IdP, redeemed it at the
   resource authorization server, and called the Todo0 MCP server with the
   resulting access token.

   <Framed>

   ![Todo0 resources and todos returned through the gateway in MCP Jam](../../../public/media/cross-app-access/todo-resources.png)

   </Framed>

</Stepper>

## Verify from the command line (optional)

To confirm the gateway is deployed and secured without a client, check its OAuth
metadata and the protected route:

```bash
# Protected-resource metadata is published for the route (expect 200)
curl -s https://your-gateway.zuplo.dev/.well-known/oauth-protected-resource/mcp/todo0

# The MCP route rejects unauthenticated calls (expect 401)
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
  https://your-gateway.zuplo.dev/mcp/todo0 \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1.0.0"}}}'
```

## Next steps

- [Configuration reference](./policy-reference.mdx) — the full `idJag` option
  set.
- [Overview](./overview.mdx) — the protocol and the gateway's role explained.
