MCP on behalf of the user
From dynamic client registration to an API call made as the user.
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.
Discover
An unauthenticated call gets a 401 pointing at the protected-resource metadata. The metadata names the resource and the authorization server.
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.
Receive Token A
The client authorizes with resource set to the MCP identifier and receives a token for the MCP audience.
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.
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.