A Cross App Access (XAA / SEP-990) demo built with the MCP TypeScript SDK, registered as an MCP Requesting App on xaa.dev.
A new hire signs in once with company SSO. Their onboarding checklist loads immediately, pulled live from the task system, with no "Connect your account" screens, no per-app OAuth consent flows. IT wired up the connection at the IdP level; Cross App Access propagates the user's identity across the trust boundary on each request.
The dashboard shows:
- A progress ring and stats for the onboarding checklist
- An Up next card suggesting the next step (required steps first)
- The checklist itself, with priorities mapped to Required / Recommended / Optional
- A Behind the scenes panel showing all four XAA steps live, with timings, decoded
ID-JAG and access-token claims, and a button to re-run the flow via
CrossAppAccessProvider
All checklist data is fetched at request time from xaa.dev's protected MCP server
(todo0://todos) with read-only scopes (todos.read mcp.access). Nothing is seeded or mocked.
| Step | Protocol | Endpoint | SDK API |
|---|---|---|---|
| 1. Company SSO | OIDC auth code + PKCE | idp.xaa.dev/authorize |
(plain OIDC) |
| 2. ID token → ID-JAG | RFC 8693 | idp.xaa.dev/token |
discoverAndRequestJwtAuthGrant() / requestJwtAuthorizationGrant() |
| 3. ID-JAG → access token | RFC 7523 | auth.resource.xaa.dev/token |
CrossAppAccessProvider token request |
| 4. Fetch checklist | MCP Streamable HTTP | mcp.xaa.dev/mcp |
Client + StreamableHTTPClientTransport |
The flow uses CrossAppAccessProvider: 401 → RFC 9728 discovery → assertion callback → RFC 7523
exchange → retry, fully SDK-orchestrated.
- Go to the requesting app registration page.
- Enter your email address. It filters which registered apps are visible to you.
- Select + Register New App and fill in the form:
- Application Name: any label, for example
Onboarding App - Local Dev - Redirect URIs:
http://localhost:3001/callback(the match is exact, including scheme, host, port, and path) - Connect to Resource: select the Todo MCP server resource (
todo0-mcp) and keep thetodos.readandmcp.accessscopes
- Application Name: any label, for example
- Save the credentials from the confirmation modal.
Registration creates two separate OAuth clients, and mixing them up is the most common XAA mistake:
| Client | Credentials | Used in |
|---|---|---|
| Main client | client_id / client_secret |
Step 1 (sign-in) and step 2 (token exchange) at the IdP |
| Resource client | resource_client_id / resource_client_secret (the ID looks like client_xxx-at-todo0-mcp) |
Step 3 (JWT bearer grant) at the resource's authorization server |
Why two? Step 3 crosses a trust boundary. The IdP and the resource's authorization server are separate trust domains, so your app holds a separate identity at each. Using the main client's credentials in step 3 fails with invalid_client.
git clone https://github.com/oktadev/okta-xaa-typescript-mcp-sdk-example.git
cd okta-xaa-typescript-mcp-sdk-examplecp .env.example .envFill in .env with the credential pairs from the registration modal: app pair → XAA_*;
token-exchange pair → EXCHANGE_* (only if your registration issued a separate pair; leave
blank to fall back to the app pair); resource pair client_xxx-at-todo0-mcp → MCP_*.
npm install
npm start # http://localhost:3001Note: uses port 3001. Stop any other app using that port before starting this one, since both share the registered redirect URI.
- Open
http://localhost:3001and select Sign in with company SSO. xaa.dev's IdP, IdenX, accepts any email address, so no real credentials are involved. - After sign-in, the flow runs automatically via
CrossAppAccessProvider, which discovers the authorization server through RFC 9728 resource metadata (no hardcoded authorization server URL): the four steps light up in order in the Behind the scenes panel with real timings, and the checklist renders as soon as step 4 delivers the data. - Select any step card to expand its decoded token. Check three things while you are there:
- The ID-JAG's
audis the authorization server and itsresourceis the MCP server URL - The access token's
scopeclaim containstodos.read mcp.access - The access token's
audmatches the ID-JAG'sresource, byte for byte
- The ID-JAG's
- Click 🔄 Re-run (SDK discovers the auth server) to replay the flow and see fresh timings and tokens.
client_secret_postrequired for developer-registered clients (SDK defaults to Basic auth)scopemust be sent explicitly in the RFC 7523 request or the access token is issued scope-less- No trailing slashes in
audience(exact string match at the IdP) - IdP publishes OIDC discovery only; the SDK's metadata-discovery fallback handles it
src/config.ts # env config (trailing-slash safe)
src/xaa.ts # XAA steps built on the SDK (ID-JAG request, MCP fetch, CrossAppAccessProvider orchestration)
src/server.ts # Express: OIDC login + SSE endpoint streaming steps 2–4 live
public/index.html # Employee onboarding UI (ring, up-next, checklist, behind-the-scenes)
This example uses the following open standards and libraries:
- @modelcontextprotocol/client
- Express
- RFC 8693: OAuth 2.0 Token Exchange
- RFC 7523: JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants
- RFC 9728: OAuth 2.0 Protected Resource Metadata
Please post any questions as comments on the blog post, or visit our Okta Developer Forums.
Apache 2.0, see LICENSE.