Approve AI clients to act on someone's behalf: administrators on a page Vendure ships, customers on a consent page you build on your storefront.
When an agent requests the admin toolset, the authorize step redirects to the
dashboard consent page at adminConsentPath, which defaults to
/dashboard/mcp/authorize. The page shows the client name, the client's redirect URI,
and the requested toolset. A signed-in administrator picks Approve or Deny, using
their existing dashboard session. Behind the button the page calls the
authorizeMcpClient mutation on the Admin API. There is nothing to build.
Two rules apply beyond being signed in:
UpdateMcpServer permission. Approving changes
who can reach your store, so it is gated like the other MCP mutations, in the
dashboard route and on the server.oauth.issuer. The authorize
step redirects to adminConsentPath on that origin, which is where DashboardPlugin
serves the dashboard. An approval that carries an Origin header is refused unless
that origin matches, which stops a page on another site from submitting an approval
with the administrator's own session cookie. A dashboard on a separate origin cannot
approve admin clients. This includes the Vite dev server opened on its own port and
when the issuer names . A programmatic approval must send no
header at all: absent is allowed, arbitrary is refused.127.0.0.1localhostOriginBuilding the customer consent page needs:
apiOptions.cors. The page reads a lookup
endpoint and posts to the Shop API from that origin, exactly like every other Shop
API call your storefront makes.When a client asks for the shop toolset, the authorize step redirects the browser to a
page you host, carrying one query parameter: ?request_token=<token>. To build that
page:
Set oauth.storefrontConsentUrl to the absolute URL of your page:
storefrontConsentUrl (string, optional, no default) — The storefront page that
approves customer consent. Required only if customers will authorize clients. A
store that uses the admin toolset alone starts up without it.Under NODE_ENV=production two more rules apply. The URL must use HTTPS. It must not
be a loopback address such as localhost or 127.0.0.1. The server refuses to boot
otherwise.
Fetch the request details before asking the shopper to sign in. The lookup needs no credential, so the shopper can see what they are allowing before typing a password.
It returns:
client_uri and logo_uri are omitted entirely when the client did not register
them. client_id_source tells you how far to trust the details:
client_id_source | What it means |
|---|---|
cimd | The client_id is a Client ID Metadata Document URL. The details were fetched from that document, so its hostname is a verifiable identity |
dcr | The client registered itself through Dynamic Client Registration. Every detail is self-asserted |
Any response other than a success means the request token is unknown, has timed out, or has already been decided. Treat all three as an expired flow.
These helpers hold the calls, so they work whether your page renders in the browser or on your server:
Your page can now show who is asking, before any sign-in.
The decision is a Shop API mutation:
Send the browser to the redirectUrl it returns. On approval that URL hands an
authorization code back to the client. On denial it carries the refusal back instead.
Four rules apply at this step:
vendure-token header if your store has more than one sales channel,
exactly as the rest of your storefront does. On a single-channel store there is
nothing to do. What goes wrong without it is covered in
How the channel is recorded.client_name is
a value a stranger chose. Present it as unverified.The whole decision flow:
An example consent page, using TanStack Query:
After the mutation, the browser lands on the client's redirect address carrying the authorization code, or the refusal.
A request from your own assistant needs no screen: the shopper already chose to use
it. The sample above implements this with OWN_CLIENT_ID: set it to the client id
your assistant registered, and matching requests are approved directly after sign-in.
The comparison is on client_id, which the server generated, never on the name.
A signed-in customer can list and end their own grants on the Shop API, which suits a "connected assistants" page in your account area.
activeMcpClientGrants — Returns the customer's active grants. Each carries its id,
the client name, and its timestamps. It spans every channel the grants were made on,
because the consent belongs to the person rather than the storefront they used.revokeMcpClientGrant(id: ID!) — Ends one of those grants. Its tokens stop working
immediately and its dedicated Vendure session is deleted. This is the same effect as
the dashboard's admin-side revoke.Both operations require a signed-in customer. Both act only on that customer's own grants.
The first decision consumes it. A shopper who reloads the page after approving finds the lookup fails. Your page cannot restart something the client began, so the only useful message is "go back to your assistant and connect again".
An approval that carries an Origin header is refused unless that origin matches
oauth.storefrontConsentUrl. An approval with no Origin header is allowed. This
stops a page on another site from submitting an approval with a signed-in shopper's
session.
| How your page submits | What the server sees |
|---|---|
| From your server | No Origin header. Node attaches none, so the approval passes |
| From the browser | The consent page's own origin, which matches. The browser always attaches Origin to a cross-site POST, and page code cannot suppress it, so a stranger's page cannot make it match |
For a page that submits from the browser, set oauth.storefrontConsentUrl to the
origin the page is served from. Any other value makes your own page's approvals fail.
The mutation takes the channel from the request, exactly like every other Shop API
call. Without the vendure-token header, Vendure resolves the request to the default
channel, with two effects:
The grant stays bound to the channel used for approval. To use another channel, revoke the grant and authorize again from the intended channel.
The lookup call never needs the token, because it reads nothing channel-specific.
| What you see | Cause | Fix |
|---|---|---|
| The lookup fails after the shopper reloads the page | The request token is single-use and the first decision consumed it | Show "This request has expired. Go back to your assistant and connect again." |
The client is sent back to its redirect address with error=server_error and its state, and the server log names oauth.storefrontConsentUrl | A customer authorization arrived but oauth.storefrontConsentUrl is not set | Set oauth.storefrontConsentUrl |
| Every approval from a preview or staging deployment is refused | The deployment's origin differs from oauth.storefrontConsentUrl | Update the setting to that deployment's origin |
| A signed-in shopper's approval is refused | The user also holds an administrator record | Expected. Approve with a customer-only account |
| Every administrator approval fails | The dashboard is open on a different origin than oauth.issuer, for example 127.0.0.1 when the issuer names localhost | Open the dashboard on the issuer's origin |
client_id metadata document, token lifetimes, sessions, and refusalsArchitecture reviews, custom plugin work, migrations, ongoing support. Get a hand from the team that builds Vendure.
Talk to the team