# Komers MCP Sites auth.md

Komers MCP Sites builds and hosts websites for people who ask an AI assistant. This file tells an agent how to get credentials to act for a person.

- Resource server (API): https://app.udx.my/mcp
- Authorization server: https://app.udx.my/

Two ways in, both ending in a standard OAuth access token that belongs to a person who signed in:

- **OAuth 2.1 with the person present** (sections 1–5): the agent registers as a client, the person signs in and approves in a browser.
- **Agent registration, user-claimed email flow** (section 6, `service_auth`): the agent sends the person's email, shows them a 6-digit code, and they confirm it after signing in. For agents that run without a browser (MCP servers, scripts).

- **Anonymous start** (section 7, `anonymous`): the agent gets a pre-claim assertion at once with a read-only `explore` scope, and claims it later with the person's email and the same code ceremony.
- **Agent-verified identity** (section 8, `identity_assertion`): a trusted agent provider's ID-JAG asserts who the person is; linking to an existing account still needs the person's confirmation.

Which of these are on is in the `agent_auth` block of https://app.udx.my/.well-known/oauth-authorization-server (`identity_types_supported`); a type that is off answers `<type>_not_enabled`. Every full-scope token belongs to a person who signed in.

## 1. Discover

1. Call the API without a token. The 401 carries `WWW-Authenticate: Bearer resource_metadata="https://app.udx.my/.well-known/oauth-protected-resource/mcp"`.
2. Fetch that Protected Resource Metadata (RFC 9728). Read `resource`, `authorization_servers`, `scopes_supported` and `bearer_methods_supported`. The same data for the whole origin is at https://app.udx.my/.well-known/oauth-protected-resource.
3. Fetch the Authorization Server metadata (RFC 8414) at https://app.udx.my/.well-known/oauth-authorization-server: `issuer`, `authorization_endpoint`, `token_endpoint`, `registration_endpoint`, `revocation_endpoint`, `code_challenge_methods_supported`.

The metadata documents are authoritative if anything here disagrees with them.

## 2. Register the agent (dynamic client registration, RFC 7591)

```http
POST /register HTTP/1.1
Host: app.udx.my
Content-Type: application/json

{"client_name": "My Agent", "redirect_uris": ["https://agent.example/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none"}
```

The response carries your `client_id` (and a `client_secret` when you asked for `client_secret_post`). Register once and reuse it.

## 3. Get the person's approval (authorization code + PKCE)

Send the person to:

```http
GET /authorize?response_type=code&client_id=<client_id>&redirect_uri=<redirect_uri>&scope=sites&code_challenge=<S256 challenge>&code_challenge_method=S256&state=<state>&resource=https%3A%2F%2Fapp.udx.my%2Fmcp HTTP/1.1
Host: app.udx.my
```

They sign in with their email (a 6-digit code) or Google and approve. You receive `code` at your redirect URI.

## 4. Exchange the code

```http
POST /token HTTP/1.1
Host: app.udx.my
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&redirect_uri=<redirect_uri>&client_id=<client_id>&code_verifier=<verifier>&resource=https%3A%2F%2Fapp.udx.my%2Fmcp
```

The response is a standard token envelope: `access_token`, `token_type`, `expires_in`, `refresh_token`, `scope`.

## 5. Use the token

```http
POST /mcp HTTP/1.1
Host: app.udx.my
Authorization: Bearer <access_token>
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
```

Tokens go in the Authorization header only. When the access token expires, use `grant_type=refresh_token` at /token. Revoke with POST /revoke (RFC 7009).

## 6. Agent registration (service_auth)

Metadata: the `agent_auth` block of https://app.udx.my/.well-known/oauth-authorization-server (`register_uri`, `claim_uri`, `identity_types_supported: ["service_auth"]`). The agent needs a `client_id` from step 2.

### 6a. Register with the person's email

```http
POST /agent/identity HTTP/1.1
Host: app.udx.my
Content-Type: application/json

{"type": "service_auth", "client_id": "<client_id>", "login_hint": "person@example.com"}
```

```json
{"type": "service_auth", "claim_token": "<opaque>", "claim": {"user_code": "482913", "verification_uri": "https://app.udx.my/dashboard/agent/claim", "verification_uri_complete": "https://app.udx.my/dashboard/agent/claim?code=482913", "expires_in": 900, "interval": 5}, "scopes": ["sites"]}
```

No assertion is issued yet. The service never emails the code: the agent shows `user_code` and `verification_uri` to the person in one message.

### 6b. The person confirms

They open the link, sign in (email code or Google) with the same email address, and type the code on the page. A code requested for one email cannot be confirmed by another account.

### 6c. Poll for completion

```http
POST /token HTTP/1.1
Host: app.udx.my
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&client_id=<client_id>&claim_token=<claim_token>
```

Responses: `authorization_pending` (keep polling every `interval` seconds), `slow_down`, `expired_token` (register again for a fresh code), or on success:

```json
{"access_token": "<token>", "token_type": "bearer", "expires_in": 3600, "scope": "sites", "identity_assertion": "<JWT>", "assertion_expires": "2026-12-06T00:00:00.000Z"}
```

### 6d. Exchange the assertion later

The access token expires in an hour; the identity assertion (a JWT signed by this service, keys at https://app.udx.my/.well-known/jwks.json) lasts 30 days and can be re-exchanged as often as needed. There is no refresh token on this path.

```http
POST /token HTTP/1.1
Host: app.udx.my
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id=<client_id>&assertion=<identity_assertion>
```

When the assertion expires or /token answers `invalid_grant`, start again at 6a. Revoke an access token with POST /revoke; the assertion survives, so the agent can re-exchange. The person can end the registration at any time (their AI assistant's `revoke_agent_access` tool), which also revokes the access tokens issued through it.

## 7. Anonymous start (anonymous)

```http
POST /agent/identity HTTP/1.1
Host: app.udx.my
Content-Type: application/json

{"type": "anonymous", "client_id": "<client_id>"}
```

The response carries a pre-claim `identity_assertion` (exchange it as in 6d; the access token has scope `explore`: `site_guide`, `get_capabilities`, `list_templates` and `whoami` only), plus `claim_token`, `claim_url` and `post_claim_scopes`. The registration lives 24 hours unclaimed.

To bind it to a person: `POST /agent/identity/claim` with `{"client_id": "<client_id>", "claim_token": "<claim_token>", "email": "person@example.com"}` returns a `claim_attempt` block (`user_code`, `verification_uri`, 10-minute window). Show it to the person; they sign in with that email and confirm (6b). Poll with the claim grant (6c): the response carries a new assertion with the person's email and a full-scope token. The pre-claim assertion and tokens stop working at that moment.

## 8. Agent-verified identity (identity_assertion)

For agents whose provider can mint an ID-JAG (`urn:ietf:params:oauth:token-type:id-jag`) for the person, signed by a provider in this service's trust list (`agent_auth.identity_assertion.trusted_issuers`). The ID-JAG must have `aud` = `https://app.udx.my/`, a verified `email`, a fresh `auth_time` and an unused `jti`.

```http
POST /agent/identity HTTP/1.1
Host: app.udx.my
Content-Type: application/json

{"type": "identity_assertion", "assertion_type": "urn:ietf:params:oauth:token-type:id-jag", "assertion": "<ID-JAG>", "client_id": "<client_id>"}
```

- Known person (a previous link, or a brand-new email): `200` with `identity_assertion` to exchange as in 6d.
- Existing account not yet linked to this provider identity: `401 interaction_required` with a `claim` block. The person confirms the code once (6b); poll with the claim grant (6c). Later presentations complete at once.
- Stale sign-in at the provider: `401 login_required` (`max_age` seconds). Re-authenticate there and present a fresh ID-JAG; nothing the person does here helps.
- Other errors: `invalid_issuer`, `invalid_signature`, `invalid_audience`, `expired`, `replay_detected`, `missing_verified_email`.

## Scopes

| Scope | What it allows |
| --- | --- |
| sites | Build, publish and manage the person's websites, shop and bookings, and spend their credits on hosting |

## Errors

| Where | Error | What to do |
| --- | --- | --- |
| /mcp | 401 invalid_token | Refresh the token, or start again at step 3 |
| /token | invalid_grant | The code or refresh token is used, expired or revoked: start again at step 3 |
| /token | invalid_client | Register again (step 2) |
| /token (claim grant) | authorization_pending / slow_down | Keep polling at the given interval |
| /token (claim grant) | expired_token | The person did not confirm in time: register again (6a) |
| /agent/identity | <type>_not_enabled | That registration type is off here: use one listed in agent_auth.identity_types_supported |
| /agent/identity | 401 interaction_required | The person must confirm the code in the claim block, then poll the claim grant |
| /agent/identity | 401 login_required | Re-authenticate the person at the provider and present a fresh ID-JAG |
| /agent/identity/claim | invalid_claim_token / claimed_or_in_flight / claim_expired | Register again |
| /mcp | 403 on a pre-claim token | Only the explore tools work before the claim; complete it |
| any | 429 | Wait a minute and retry |

## More

- API catalog: https://app.udx.my/.well-known/api-catalog
- OpenAPI description: https://app.udx.my/openapi.json
- Setup guide for people: https://app.udx.my/connect
- Terms: https://app.udx.my/terms · Privacy: https://app.udx.my/privacy · Support: https://app.udx.my/support
