Skip to content

Powered by Grav CMS + Helios Theme

Storefront MCP

A public MCP server at {base}/mcp that lets an AI assistant search the catalog, look up products, answer policy questions, build a cart and hand the buyer to checkout. This page is for switching it on, and for developers pointing an agent or an MCP client at a store.

It is a different thing from the admin MCP tools on Run the store from an AI assistant. Those operate the store, and every one of them needs a permission. These are what any shopper could do on the storefront, answered for an agent, and they need nothing.

The same endpoint is the mcp transport of the store's UCP profile, so a UCP platform (Google AI Mode and Gemini) and a plain MCP client (Claude, ChatGPT connectors, Cursor) call the same tools. See UCP checkout for the profile and the checkout.

Switch it on

  1. Set agentic.enabled: true. See Agentic commerce.
  2. Set storefront_mcp.enabled: true.
  3. Give your MCP client the URL https://your-store.example/shop/mcp (your storefront route plus /mcp) as a Streamable HTTP server. There is nothing to authenticate.

The client lists thirteen tools. Ask it to find something in your catalog to check the connection.

Key Default Effect
storefront_mcp.enabled false Answers at {base}/mcp. Off, it is a 404. Only in effect while agentic.enabled is on.
storefront_mcp.allow_anonymous true A client that sends no UCP profile may browse, build carts and hand the buyer to checkout. Off, every call needs meta["ucp-agent"].profile, and one without it is refused with -32001.
storefront_mcp.allowed_origins [] Browser origins, as https://app.example, allowed to call the endpoint from a web page. The site's own origin is always allowed. Server-to-server clients send no Origin and need nothing here.

The tools

Names are UCP's where UCP has one, so an agent that knows UCP or Shopify's UCP catalog server finds them, and Shopify's name for the policy tool. Input schemas follow the UCP OpenRPC at 2026-04-08, except that meta is optional. Every catalog and policy tool, and the two get_ tools, carries readOnlyHint: true.

Tool What it does
search_catalog Search the catalog as a guest sees it, by query or by category and price filters, 10 results a page by default and 50 at most
lookup_catalog Look up 1 to 50 products or variants by id, SKU or handle
get_product One product in full, with every picture and UCP's option narrowing: which option values exist and are available beside the ones selected
search_shop_policies_and_faqs Answer questions about returns, refunds, shipping, terms, privacy and the FAQ, from the pages you link in policies.*
create_cart Start a cart. Any variant a guest could put in the storefront cart, because the buyer finishes on your checkout
get_cart The cart as it stands
update_cart Replace the cart's lines, context and buyer
cancel_cart Close the cart and release any stock it held
create_checkout Start a checkout, from lines or from a cart, with the buyer, shipping address, shipping option and discount codes
get_checkout The checkout as it stands, and the order once it is completed
update_checkout Replace the checkout's lines, buyer, fulfillment and codes, and answer the new totals and status
complete_checkout Place the order with a payment instrument from a payment handler the platform negotiated
cancel_checkout Cancel the checkout and release any stock it held

Tool results use UCP's dual output: the payload in structuredContent, and the same JSON as one text block in content for clients that only read text. A business outcome, such as a product that is not found or a line that is out of stock, is a result with UCP messages and isError: false, never a JSON-RPC error.

What the catalog tools answer

A product carries id (kc-p{id}), handle (the slug), title, description, url, categories (your own category path and the Google category), price_range and list_price_range, media, options, variants, and rating when it has reviews.

A variant carries id (the same id the product feeds publish, and the one checkout takes as item.id), sku, barcodes (the GTIN), title, price, list_price, availability, options, its own media when it has a picture, and seller with the refund and shipping policy links.

  • Money is an integer in the store currency's minor units, with its currency.
  • price is what a guest pays today: a running sale's price when a sale covers the variant, else the regular price. list_price is the struck-through price.
  • Price lists and campaign-link coupons depend on who is looking, so they are applied at checkout rather than here.
  • availability is {available, status}, with status one of in_stock, backorder or out_of_stock. A stock quantity is never sent. Neither is a cost, an internal custom field, or anything else the payload does not name.

search_catalog runs the storefront's own listing, so it matches what the shop's search box matches, in the same order. filters.categories matches a category by its path (Clothing > Shirts), its title or its slug, and takes the categories under it. filters.price is in minor units of the currency the call is in, and a store that sells in one currency applies it when context.currency is absent or is that currency. A store that sells in more than one currency measures it in the call's currency, as described under Currencies.

search_shop_policies_and_faqs answers each linked policy with the page's text, Markdown stripped, at most 4,000 characters. Its raw Markdown is read and never rendered, so no Twig or shortcode runs for an agent. A page that is unpublished, not routable or behind an access or login rule, and a link to another site, answer with an empty content. The answer also carries the structured return policy and the per-country shipping summary. On a store that sells in more than one currency the call's context may carry currency and address_country: returns.fee and each shipping rate come back in that currency, and a policy with no amount in it is left out. See Product feeds.

What the cart and checkout tools answer

A cart has an id of cart_ plus 26 characters, its lines (each with a line id li_{n}), the buyer, discounts, totals, messages, the policy links, a continue_url to your checkout, and expires_at. The session lasts ucp.session_ttl, six hours as shipped.

A checkout (chk_ plus 26 characters) carries everything a cart does, plus status, fulfillment (the destination, the shipping options with their totals, and the selected one), payment once an instrument was sent, and after completion the order with its id, label (the order number) and permalink_url (the signed receipt).

  • Nothing is reserved or charged by a cart, and no customer record is created until a checkout completes.
  • A line that cannot be bought is reported with a message (item_unavailable, out_of_stock, quantity_limit) and the other lines still go in. A quantity the store lowered is a warning quantity_adjusted.
  • A checkout takes only variants an agent may check out. Any other line is an item_unavailable message saying the buyer can buy it on the store. See Agent checkout.
  • line_items, when sent on an update, is the whole basket. A line sent with its id keeps that row and takes the new quantity, a line without one is added, and a stored line left out is removed. A member left out entirely keeps what it had.
  • The status is incomplete while something is missing, and the messages say what: the buyer's email, the lines, a full shipping address or a shipping option. When nothing is missing but the checkout has no payment handler, which is always so for a plain MCP client, the status is requires_escalation with a payment_handoff message, and the buyer pays at the continue_url. With a negotiated handler it is ready_for_complete.
  • An id that is unknown, expired, or belongs to another platform answers the same unrecoverable not_found for all three, so an id tells a prober nothing.

UCP checkout covers the statuses, messages, totals and the handoff in full.

Currencies

A store that sells in more than one currency answers each catalog call in one of them, and its initialize instructions say which: the store's own currency, the others it sells in, and that a catalog tool's context.currency picks one. A store with one currency says what it always said and reads none of this.

Which currency a catalog call is in, first match wins:

  1. The context.currency the call names (search_catalog, lookup_catalog and get_product all take catalog.context), when the store sells in it. The code is three letters in any case.
  2. The currency currencies.map gives context.address_country.
  3. The store's own currency.

A currency the call names that the store does not sell in is not an error. The answer is in the store's own currency, with an info message currency_not_sold at $.catalog.context.currency that lists the currencies the store does sell in.

Every price, price_range, list_price and list_price_range in the answer is in that currency, with its code in currency, and a sale is worked out on the price in that currency. A variant with no price in it is left out of the product, and a product with none is left out of the answer. lookup_catalog says so for an id that named one, as an info message unavailable_in_currency at the id's path, and get_product answers ucp.status: "error" with an error message unavailable_in_currency at $.catalog.id, rather than the not_found that means there is no such product. An option value only an unpriced variant has comes back with exists: false. filters.price is in minor units of the call's currency, so a range typed in euro cents is measured against euro prices through the catalog index, and one in yen is whole yen. The index has to hold the currency's prices: after adding a currency, rebuild it as Currencies says, and until then a price range in that currency is not applied. A price filter in a currency the store does not sell in is ignored with price_filter_ignored.

A cart or a checkout is in the currency it was created in and says so in its currency; create_cart and create_checkout choose it from context as UCP describes. Nothing a later call sends moves it.

UCP metadata and plain MCP clients

UCP puts request metadata in a meta object inside the tool's arguments. meta["ucp-agent"].profile is the platform's UCP profile URL, and meta["idempotency-key"] makes a retry safe.

  • A call that carries a profile is a UCP platform (ucp_mcp). The checkout tools negotiate against its profile, and it must send an idempotency key on cancel_cart, cancel_checkout and complete_checkout.
  • A call without one is a plain MCP client (mcp), served while storefront_mcp.allow_anonymous is on. It may browse, build carts and create checkouts, and its buyer pays on your own checkout through the continue_url. That gives nothing away the public storefront does not.

context.language in a tool's catalog, cart or checkout object (a BCP 47 tag such as de or fr-CA) answers in that language when the site runs it. Otherwise the answer is in the language of the URL (/de/shop/mcp).

The transport

MCP Streamable HTTP, JSON-RPC 2.0, stateless:

  • POST with one JSON-RPC message as application/json. The answer is application/json, never a stream.
  • GET and DELETE answer 405. There is no MCP session, so no Mcp-Session-Id is issued. Carts and checkouts are named by the ids the tools answer.
  • initialize answers protocol 2025-11-25 or 2025-06-18. serverInfo.name is kahunacart-storefront, and instructions names the store and its currency and says to call search_catalog first. ping, tools/list and tools/call are the other methods.
  • A body larger than 256 KB is refused with 413. JSON nested more than 32 levels deep, and a JSON-RPC batch, are refused with 400.
  • A request carrying an Origin that is neither the site nor in storefront_mcp.allowed_origins is refused with 403. An allowed origin gets CORS headers.
  • The Grav session is never read or written. No cookie is needed and none is set.

Errors

JSON-RPC code HTTP When
-32700 400 The body is not JSON
-32600 400, 413, 415, 406 Not a JSON-RPC 2.0 request, a batch, nested too deeply, too large, not application/json, or an Accept header that rules out JSON
-32601 200 A method this server does not have
-32602 200 An unknown tool, or arguments that break the tool's schema or limits. The message names the place in the arguments, never the value. Also a missing idempotency key where one is required
-32001 400, 424, 422 A profile that is not an https URL, a store that serves UCP platforms only, or a platform profile the store could not fetch or use
-32000 403 The Origin is not allowed, or the platform is not one the store transacts with
-32000 409 An idempotency key reused with different arguments, or a checkout another request is completing
-32000 429 Rate limited. Retry-After and error.data.retry_after say how long to wait
-32000 503 The store is coming soon or paused (store_closed). Creating or updating a cart or a checkout, and completing one, are refused with the store's message as content, retry_after in error.data and a Retry-After header. Reading and cancelling still work, and so does a completion retried for a checkout that already went through or whose payment challenge started before the pause. See Configuration → Store status
-32000 503 The store cannot fetch another platform profile or check an idempotency key right now
-32603 500 Anything unexpected. The cause goes to the Grav log, never into the answer

A card number anywhere in a tool's arguments is refused with -32602 before the tool runs.

Rate limits

Every call counts against one of the agent buckets in security.rate_limits, keyed by the client address. Set security.trusted_proxies behind a proxy; see Hardening.

Bucket Default Counts
agent_read 120 a minute initialize, ping, tools/list, the catalog and policy tools, get_cart, get_checkout
agent_write 60 a minute create_cart, update_cart, cancel_cart, create_checkout, update_checkout, cancel_checkout
agent_complete 10 a minute complete_checkout

Security

  • Every payload is built from an allowlist of fields: no stock quantities, no costs, no internal custom fields, no admin data.
  • Arguments are checked against the tool's input schema with length and count caps before the tool runs, and errors never echo what was sent.
  • An agent sees exactly what a logged-out storefront visitor sees. Drafts, unlisted products and members-only products are invisible to every tool, lookup by id included.
  • No session is read or written, so an agent cannot reach a shopper's cart and a shopper's browser cannot be made to act through this endpoint.
  • Cart and checkout ids are 128 random bits, bound to the platform that created them. An anonymous session is reachable only anonymously.
  • Keep {base}/mcp out of "block AI bots" rules and JavaScript challenges at your CDN, and do not cache it. See Hardening → Agent endpoints.