Skip to content
Tarn & Timber Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

Was this page helpful?