---
title: MCP on behalf of the user
description: From dynamic client registration to an API call made as the user.
sidebar:
  order: 3
---

The MCP Worker is stateless. It owns its tool definitions and schemas, and calls the API through the nested
`@retail-demo/api/client`. The owner-run `scripts/smoke/mcp.ts` walks this flow against a real tenant and prints the
authorization URLs to open in a browser. Supply the resource-linked `MCP_CLIENT_ID` and `MCP_CLIENT_SECRET`; it checks
discovery and allowed, unknown and human-only OBO scopes before calling `getCart`. Offline fixture tests run with the
root checks, but the tenant flow remains a manual acceptance check.

An MCP client such as Claude, ChatGPT or Cursor connects to the MCP server, registers itself, and asks the user for
consent. The MCP server never acts as itself on shop data: it exchanges the client's token for one that names the
user and records that an agent acted for them.

```mermaid
sequenceDiagram
  autonumber
  participant C as MCP client
  participant M as MCP server
  participant G as Proxy Auth0 (Auth0 custom-domain front)
  participant A0 as Auth0
  participant API as Shop API
  C->>M: POST https://mcp.tarnandtimber.com (no token)
  M-->>C: 401 WWW-Authenticate: Bearer resource_metadata="https://mcp.tarnandtimber.com/.well-known/oauth-protected-resource"
  C->>M: GET protected resource metadata
  M-->>C: { resource: MCP_ID, authorization_servers: [login-shop], scopes_supported }
  C->>G: AS metadata, then POST /oidc/register (DCR) on first use
  G->>A0: proxied (custom domain)
  A0-->>C: client_id (third-party, strict)
  C->>G: authorize (resource=MCP_ID, PKCE, consent)
  G->>A0: proxied (custom domain)
  A0-->>C: Token A (aud MCP_ID, user sub, client_id = DCR client)
  C->>M: tools/call addCartItem  Bearer Token A
  M->>M: verify Token A, tool needs cart:write
  M->>A0: token-exchange (subject_token = Token A, audience SHOP_API, scope cart:write)
  Note over M,A0: client_secret auth as the MCP server's Custom API client
  A0-->>M: Token B (aud SHOP_API, sub user, client_id MCP server, act {sub: MCP server, act {sub: DCR client}})
  M->>API: POST /v1/cart/items  Bearer Token B
  API->>API: act.sub = client_id = MCP server → delegated → agent (origin oauth)
  API-->>M: 200 Cart
  M-->>C: structured result + summary
```

1. **Discover**

    An unauthenticated call gets a 401 pointing at the protected-resource metadata. The metadata names the resource and
    the authorization server.

2. **Register and consent**

    The client registers through Dynamic Client Registration on first use. It becomes a strict third-party client: PKCE
    and a consent screen naming the client are required.

3. **Receive Token A**

    The client authorizes with `resource` set to the MCP identifier and receives a token for the MCP audience.

4. **Exchange for Token B**

    For each tool call the MCP server checks that Token A carries the tool's scopes, then exchanges it with Auth0
    (On-Behalf-Of) for a token for the Shop API audience. The new token names the user as `sub` and carries an `act`
    chain.

5. **Call the API**

    The API sees the outermost `act.sub` equals the MCP server's client, classifies the caller as `delegated`, and runs
    the operation as an agent with the user's account cart.

## The bare-domain identifier

The canonical MCP resource is `https://mcp.tarnandtimber.com`, with no path and **no trailing slash**. Its
protected-resource metadata is served at `/.well-known/oauth-protected-resource`, and its `resource` value equals the
identifier exactly. Auth0 matches `resource=` exactly, and token audiences are never normalized in code, because
that would accept tokens minted for a different identifier. There is no shop-domain MCP alias.

## Caching and credentials

OBO tokens are cached by a digest of Token A, the downstream audience and the normalized scopes, and never kept past
either token's expiry. OBO issues no refresh token, so the MCP server holds no long-lived user credential.

## Canonical resource

The server always verifies the configured `MCP_ORIGIN` audience. Request headers cannot select another resource, a token for any other audience fails verification, and the storefront serves no MCP metadata route.

| Condition                                              | HTTP result                                 | Client action               |
| ------------------------------------------------------ | ------------------------------------------- | --------------------------- |
| Missing or malformed bearer                            | 401, canonical `resource_metadata`          | Discover and authorize      |
| Invalid, expired, machine or already-delegated Token A | 401, `invalid_token`                        | Obtain a new user token     |
| Missing tool scopes                                    | 403, `insufficient_scope`, required `scope` | Consent to the named scopes |
| Signing directory unavailable                          | 503, `Retry-After: 5`                       | Retry later                 |

| OBO failure                                                                             | HTTP result               |
| --------------------------------------------------------------------------------------- | ------------------------- |
| `invalid_grant`, `invalid_token`, `access_denied`, `unauthorized_client` on 400/401/403 | 401, `invalid_token`      |
| `invalid_scope` or a successful response missing requested scopes                       | 403, `insufficient_scope` |
| Network failure, five-second timeout or upstream 5xx                                    | 503, `Retry-After: 5`     |

Initialize, tool listing and notifications make no OBO request. Known tool calls are scope-checked and exchanged before the SDK transport runs, so a refusal is a real HTTP 401, 403 or 503 with its challenge. API failures become safe `isError` tool replies: `<detail> (<error>)` from the API's problem, plus the scope suffix.

## Stages and tools

The MCP Worker has no dependency bag. Typed stages are built by plain functions in order: `resolve` (configuration and resource, chosen before any token is read), `authenticate` (Token A, the live Session for Poppy tokens, effective scopes), `authorizeTool` (account-scope check, then OBO or the narrow signed-out agent forwarder) and `callTool`. Each tool is defined in its own file under `apps/mcp/src/tools/`, with its own name, input/output schemas, permissions, annotations and response renderer. `tools/index.ts` supplies the same registry to authorization and SDK registration. Existing names remain camelCase (`searchProducts`, for example), and `_meta` carries `apiScopes` and `poppyScopes`. The tool selects a typed API operation; shared `callTool` supplies the prepared token and headers over the API service binding.
