Skip to content
Docs

How to build a Shopify MCP agent on Vercel

Build a Shopify MCP agent on Vercel with AI SDK 7. Connect to Shopify's UCP MCP servers, update carts safely, and hand the buyer a checkout URL.

Vercel

Build a shopping agent that searches a Shopify store, builds a cart, and sends the buyer to checkout from a Vercel Function. Shopify exposes catalog, cart, checkout, and order tools through Model Context Protocol (MCP) servers that follow the Universal Commerce Protocol (UCP), an open standard for agentic commerce.

AI SDK 7 connects to those servers with one MCP client, and AI Gateway gives the agent model access without an API key to manage on Vercel. Shopify has removed its original Storefront MCP catalog and cart tools, so code that still calls search_shop_catalog fails with Tool not found, and this guide uses the current UCP tools at /api/ucp/mcp.

Copy link to headingOverview

In this guide, you'll learn how to:

  • Host a UCP agent profile and authenticate your agent with Shopify
  • Wrap Shopify's MCP tools so every call carries your agent profile
  • Update a cart without losing line items, context, or attribution
  • Run a capped, staged agent loop in a Vercel Function
  • Check Shopify's tool list in CI and protect the endpoint in production

Copy link to headingWhat you need to build a Shopify MCP agent

Get these in place before the first tool call:

  • Node.js 22 or later: AI SDK 7 requires Node.js 22 and is tested on Node.js 22, 24, and 26. Use Node.js 24 LTS for production.
  • Next.js 16 with the App Router: The code in this guide uses App Router route handlers. AI SDK 7 packages are ESM-only, so load them with import rather than require().
  • AI SDK packages: Install them with npm install ai @ai-sdk/mcp zod. This guide uses ai 7 and @ai-sdk/mcp 2.
  • Shopify API credentials: In the Shopify Dev Dashboard, select Catalogs in the sidebar, click Get an API key, and copy the client ID and client secret.
  • Shopify store without a storefront password: The store's storefront can't be password-protected, which rules out development stores. The next section covers the two setups that work.
  • Vercel plan that fits the loop: Hobby functions run for up to 300s, and Pro and Enterprise functions run for up to 800s.
  • Model access through AI Gateway: AI Gateway is the default provider in AI SDK 7, so a model string such as anthropic/claude-opus-5 routes through it. Deployed functions authenticate with an OpenID Connect (OIDC) token that Vercel generates, and local development uses an AI_GATEWAY_API_KEY environment variable.

Copy link to headingChoose a Shopify store you can test against

Shopify's storefront UCP endpoint sits behind the same password gate as the Online Store. When a store's storefront password is on, /api/ucp/mcp answers every request with a 302 redirect to /password, and Shopify offers no token or header that bypasses it.

Shopify development stores are always password-protected, and you can remove the password only after you transfer the store to a merchant or switch it to a paid plan, so a development store can't serve as the store your agent calls.

Two setups work:

  • Staging store without a password: Use a store on a paid plan, load it with test products, and turn off the password in Online Store > Preferences by deselecting Restrict access to visitors with the password in the Restrict store access section. Carts and checkouts on this store touch nothing real.
  • Live merchant from the Global Catalog: Shopify's own agent tutorials search the Global Catalog, build a cart and checkout at the merchant that sells the product, and cancel the checkout at the end. If you test this way, cancel every cart and checkout your tests create, and never call complete_checkout against a live merchant.

Copy link to headingWhat Shopify's MCP servers expose to an agent

Shopify serves catalog, cart, checkout, and order tools for each store from one endpoint, https://{store-domain}/api/ucp/mcp. Every server speaks JSON-RPC 2.0, a remote procedure call convention carried over HTTP POST.

ServerEndpointTools
Storefront Catalog MCPhttps://{store-domain}/api/ucp/mcpsearch_catalog, lookup_catalog, get_product
Global Catalog MCPhttps://catalog.shopify.com/api/ucp/mcpsearch_catalog, lookup_catalog, get_product across stores
Cart MCPhttps://{store-domain}/api/ucp/mcpcreate_cart, get_cart, update_cart, cancel_cart
Checkout MCPhttps://{store-domain}/api/ucp/mcpcreate_checkout, get_checkout, update_checkout, complete_checkout, cancel_checkout
Order MCPhttps://{store-domain}/api/ucp/mcpget_order

Sending tools/list to a store's UCP endpoint returns all 13 store-scoped tools, with or without credentials. Shopify enforces access when your agent calls a tool, based on its authentication tier and the capabilities negotiated from its agent profile.

Copy link to headingWhat each Shopify authentication tier can call

Shopify classifies UCP traffic into three tiers, and stronger identification earns higher rate limits and access to more sensitive tools.

TierHow the agent identifies itselfCatalog, cart, and checkout toolscomplete_checkoutget_order
TokenBearer JSON Web Token (JWT) minted from Dev Dashboard credentialsYes, at the highest rate limitsOnly when Shopify grants the token permission to complete purchasesOnly with the read_global_api_orders scope
SignedHTTP Message Signatures (RFC 9421) with ECDSA P-256 keysYes, at lower rate limitsNoNo
AnonymousNo credentialsYes, at the lowest rate limitsNoNo

Shopify rate-limits Checkout MCP more strictly than Cart MCP at every tier. Keep browsing and line-item edits on Cart MCP, and call checkout tools once the buyer is ready to buy.

Copy link to headingHow a Shopify agent hands the buyer to checkout

Shopify supports three handoff paths for server-side agents, and the merchant stays the merchant of record on all of them:

  • Checkout MCP: Convert a cart with create_checkout and send the buyer to the returned continue_url. Shopify recommends this path, and this guide builds it.
  • Cart continue URL: The create_cart response includes a continue_url the buyer can open to pick up the cart on the storefront without a checkout session.
  • Cart permalinks: Catalog results include a checkout_url for each variant, which suits flows that don't manage a cart at all.

Agents that run in the buyer's browser use Checkout WebMCP instead, which registers checkout tools on Shopify's checkout page.

Copy link to headingWhere to run the Shopify MCP client on Vercel

Run the MCP client in a Node.js Vercel Function on Fluid Compute. Next.js deprecates the Edge runtime and uses Node.js as the default for route handlers, so leave the runtime export out of your route files.

Function duration settles the choice, because a Vercel Function on the Edge runtime must begin sending a response within 25 seconds. Searching, building a cart, and opening a checkout can take an agent loop past that mark before it has anything to send. Node.js functions default to 300s, which is also the Hobby maximum. Pro and Enterprise allow up to 800s, and an extended maximum of 1,800s is in beta when you configure it per function.

Agent loops spend most of their runtime waiting on Shopify and the model. Active CPU pricing charges CPU rates only while your code runs and a lower memory rate while it waits. In AI Gateway's first month, about 14,800 of its roughly 16,000 runtime hours went to waiting on providers, so it paid CPU rates for less than 8% of its runtime.

Copy link to headingHow to wire Shopify MCP tools into your agent loop

The build takes seven steps across five files:

FilePurpose
app/.well-known/ucp/route.tsServes your UCP agent profile
lib/shopify.tsMints a Shopify token and opens the MCP client
lib/cart.tsBuilds complete update_cart payloads
lib/shop-tools.tsDefines the tools the model can call
app/api/agent/route.tsRuns the agent loop

Copy link to heading1. Host your agent profile

Every UCP tool call carries the URL of your agent profile, a JSON document that declares the UCP version and capabilities your agent supports. Shopify fetches the profile, may cache it, and intersects its capabilities with the store's before running a tool. Converting a cart into a checkout requires both the cart and checkout capabilities in that intersection.

Create app/.well-known/ucp/route.ts:

const version = '2026-08-25';
export function GET() {
return Response.json({
ucp: {
version,
services: {
'dev.ucp.shopping': [
{
version,
spec: `https://ucp.dev/${version}/specification/overview`,
transport: 'mcp',
schema: `https://ucp.dev/${version}/services/shopping/mcp.openrpc.json`,
},
],
},
capabilities: {
'dev.ucp.shopping.catalog.search': [{ version }],
'dev.ucp.shopping.catalog.lookup': [{ version }],
'dev.shopify.catalog': [
{
version,
extends: ['dev.ucp.shopping.catalog.lookup', 'dev.ucp.shopping.catalog.search'],
},
],
'dev.ucp.shopping.cart': [{ version }],
'dev.ucp.shopping.checkout': [{ version }],
},
},
});
}

Shopify storefronts support UCP 2026-08-25, the version this profile declares. After you deploy, set UCP_AGENT_PROFILE_URL to https://your-domain.com/.well-known/ucp.

Point every environment, including Preview, at the profile on your production domain. Shopify fetches the profile from its own servers, so it can't read one hosted on a preview URL behind Deployment Protection, and Standard Protection covers every deployment except production domains. Before your first production deploy, you can use Shopify's example profile at https://shopify.dev/ucp/agent-profiles/2026-08-25/valid-with-capabilities.json.

To confirm Shopify can use the profile after you deploy, open https://your-domain.com/.well-known/ucp in a private browser window and check that it returns the profile JSON without a login prompt. Then export SHOPIFY_STORE_DOMAIN and UCP_AGENT_PROFILE_URL in your shell and run a catalog search against your staging store with the profile attached:

curl -s -X POST "https://${SHOPIFY_STORE_DOMAIN}/api/ucp/mcp" \
-H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"search_catalog\",\"arguments\":{\"meta\":{\"ucp-agent\":{\"profile\":\"${UCP_AGENT_PROFILE_URL}\"}},\"catalog\":{\"query\":\"shoes\"}}}}" \
| jq '.error // (.result.structuredContent.ucp.capabilities | keys)'

Shopify returns only the capabilities that both your profile and the store support. For the profile in this step, the output should include dev.ucp.shopping.catalog.search, dev.ucp.shopping.cart, and dev.ucp.shopping.checkout. Each failure produces different output:

OutputMeaning
Error -32001 with code profile_unreachableShopify couldn't fetch the profile URL. Check that the deployment is public and the path is correct.
Error -32001 with code invalid_profile_urlThe request carried no profile URL. Check that UCP_AGENT_PROFILE_URL is set in your shell.
Error -32602 with Tool not found: search_catalogThe profile loaded but doesn't declare the catalog capabilities.
Capability list without dev.ucp.shopping.cart or dev.ucp.shopping.checkoutThe profile loaded, but negotiation dropped that capability. Compare your profile with Shopify's example profile.

Copy link to heading2. Store Shopify credentials as Secrets

Vercel environment variables come in two types. Config values stay readable after saving, while Secret values reach your deployments but can't be viewed or retrieved again. Variables previously marked Sensitive are treated as Secrets.

VariableTypeValue
SHOPIFY_STORE_DOMAINConfigYour store domain, such as your-store.myshopify.com
SHOPIFY_CLIENT_IDSecretThe client ID from the Dev Dashboard
SHOPIFY_CLIENT_SECRETSecretThe client secret from the Dev Dashboard
UCP_AGENT_PROFILE_URLConfigThe profile URL from step 1

Add a Secret from the Vercel CLI by passing --visibility secret, then enter the value when prompted:

vercel env add SHOPIFY_CLIENT_SECRET production --visibility secret

Never prefix these variables with NEXT_PUBLIC_, because Next.js inlines those values into the JavaScript it sends to the browser.

Copy link to heading3. Authenticate and open the MCP client

Shopify Bearer tokens expire after 60 minutes, so mint a fresh token at the start of each request instead of caching one. Cart tools accept unauthenticated requests, but the token tier gets the highest rate limits on every server, so send the token on every call.

Create lib/shopify.ts:

import { createMCPClient } from '@ai-sdk/mcp';
export async function getAccessToken(): Promise<string> {
const res = await fetch('https://api.shopify.com/auth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.SHOPIFY_CLIENT_ID,
client_secret: process.env.SHOPIFY_CLIENT_SECRET,
grant_type: 'client_credentials',
}),
});
if (!res.ok) {
throw new Error(`Shopify token request failed with status ${res.status}`);
}
const { access_token } = (await res.json()) as { access_token: string };
return access_token;
}
export async function openShopClient() {
return createMCPClient({
transport: {
type: 'http',
url: `https://${process.env.SHOPIFY_STORE_DOMAIN}/api/ucp/mcp`,
headers: { Authorization: `Bearer ${await getAccessToken()}` },
},
});
}
export function agentMeta() {
const profile = process.env.UCP_AGENT_PROFILE_URL;
if (!profile) throw new Error('UCP_AGENT_PROFILE_URL is not set');
return { 'ucp-agent': { profile } };
}

AI SDK 7 MCP transports reject HTTP redirects by default to prevent server-side request forgery (SSRF). Leave that default in place, because Shopify's UCP endpoint responds without redirecting when the store domain is correct.

Copy link to heading4. Define tools that inject the agent profile

Every UCP request needs a meta object carrying ucp-agent.profile inside its arguments. Exposing that object to the model makes each call depend on the model reproducing a URL correctly, so keep meta out of the schema the model sees and add it in a wrapper around callTool.

Create lib/shop-tools.ts:

import type { MCPClient } from '@ai-sdk/mcp';
import { tool } from 'ai';
import { z } from 'zod';
import { type CartFields, readCart, toUcpLine, withQuantity } from './cart';
import { agentMeta } from './shopify';
const lineItemSchema = z.object({
variantId: z
.string()
.describe('Shopify variant GID, such as gid://shopify/ProductVariant/1234567890'),
quantity: z.number().int().min(1),
});
// cartFields holds the context, buyer, and attribution your session stores for this shopper.
export function createShopTools(shop: MCPClient, cartFields: CartFields = {}) {
const call = (name: string, args: Record<string, unknown> = {}) =>
shop.callTool({ name, arguments: { meta: agentMeta(), ...args } });
return {
searchCatalog: tool({
description:
'Search the store catalog for products that match a shopper query. ' +
'Prices are integers in minor currency units.',
inputSchema: z.object({ query: z.string() }),
execute: ({ query }) => call('search_catalog', { catalog: { query } }),
}),
createCart: tool({
description: 'Create a cart with the variants and quantities the shopper chose.',
inputSchema: z.object({ lineItems: z.array(lineItemSchema).min(1) }),
execute: ({ lineItems }) =>
call('create_cart', { cart: { line_items: lineItems.map(toUcpLine), ...cartFields } }),
}),
setCartQuantity: tool({
description: 'Set the quantity of one variant in an existing cart. Use 0 to remove it.',
inputSchema: z.object({
cartId: z.string(),
variantId: z.string(),
quantity: z.number().int().min(0),
}),
execute: async ({ cartId, variantId, quantity }) => {
const current = await call('get_cart', { id: cartId });
if (current.isError) return current;
const cart = readCart(current.structuredContent);
return call('update_cart', {
id: cartId,
cart: withQuantity(cart, variantId, quantity, cartFields),
});
},
}),
createCheckout: tool({
description:
'Convert a cart into a checkout once the shopper confirms they want to buy. ' +
'The response includes a continue_url to share with the shopper.',
inputSchema: z.object({ cartId: z.string() }),
execute: ({ cartId }) => call('create_checkout', { cart_id: cartId }),
}),
};
}

Each tool returns Shopify's result to the model, including results that carry isError: true, so the model can read the error and correct its input. Shopify also requires a meta["idempotency-key"] universally unique identifier (UUID) on cancel_cart, complete_checkout, and cancel_checkout. If you add those tools, generate the key with crypto.randomUUID() in the same wrapper and reuse it when you retry the same operation.

Copy link to heading5. Read the cart before every write

update_cart uses PUT semantics. Each request replaces the cart's full state with the payload you send, and Shopify removes every field you omit, including context and attribution as well as line items. Sending a partial update drops lines the shopper already added and resets the localization context Shopify uses for price estimates.

The current line items come from get_cart, but the cart fields you set yourself don't come back. Each get_cart response carries line items, totals, and the continue_url, and omits context, buyer, and attribution. Keep those values in your own session state and send them with every write.

Create lib/cart.ts, which the tools from step 4 use to build complete payloads:

export type UcpLine = { quantity: number; item: { id: string } };
// Fields Shopify accepts on a cart write but doesn't return from get_cart.
export type CartFields = {
context?: Record<string, unknown>;
buyer?: Record<string, unknown>;
attribution?: Record<string, unknown>;
};
export type UcpCart = { id: string; line_items: UcpLine[]; continue_url?: string };
export function toUcpLine({ variantId, quantity }: { variantId: string; quantity: number }): UcpLine {
return { quantity, item: { id: variantId } };
}
// Shopify's examples wrap the cart in a `cart` key; live responses return it directly.
export function readCart(structuredContent: unknown): UcpCart {
const content = structuredContent as { cart?: UcpCart } & UcpCart;
return content.cart ?? content;
}
// Returns a complete update_cart payload with one variant's quantity changed.
export function withQuantity(
cart: UcpCart,
variantId: string,
quantity: number,
fields: CartFields = {},
) {
const lineItems = cart.line_items
.filter((line) => line.item.id !== variantId)
.map(({ quantity, item }) => ({ quantity, item: { id: item.id } }));
if (quantity > 0) lineItems.push(toUcpLine({ variantId, quantity }));
return { line_items: lineItems, ...fields };
}

The helper reduces each line to quantity and item.id, because get_cart also returns read-only fields such as the cart line id, the product title, and the price. Shopify's UCP endpoint accepts this payload shape on a full read-and-write round trip.

Shopify's Cart MCP reference documents no version field or concurrency check for carts. Reading the cart immediately before each write narrows the window for lost updates, so keep each cart tied to one agent session.

Copy link to heading6. Run a capped, staged loop in a route handler

generateText runs a single step unless you set a stop condition, so a multi-step purchase flow needs stopWhen. The isStepCount helper sets the ceiling, and prepareStep returns activeTools to narrow what each stage can reach. With staging, the model chooses among a few tools per step instead of every commerce tool at once. Step numbers start at 0.

Create app/api/agent/route.ts:

import { generateText, isStepCount } from 'ai';
import { createShopTools } from '@/lib/shop-tools';
import { openShopClient } from '@/lib/shopify';
export const maxDuration = 300;
export async function POST(request: Request) {
const { prompt } = (await request.json()) as { prompt: string };
const shop = await openShopClient();
try {
const result = await generateText({
model: 'anthropic/claude-opus-5',
instructions:
'You are a shopping assistant for one Shopify store. Search the catalog before ' +
'adding items. Prices are integers in minor currency units, so 2500 USD is $25.00. ' +
'Create a checkout only after the shopper confirms the cart, then share its continue_url.',
tools: createShopTools(shop),
stopWhen: isStepCount(8),
prepareStep: ({ stepNumber }) =>
stepNumber < 2
? { activeTools: ['searchCatalog'] }
: { activeTools: ['createCart', 'setCartQuantity', 'createCheckout'] },
prompt,
});
return Response.json({ text: result.text });
} finally {
await shop.close();
}
}

The finally block closes the MCP client on the error path as well as the success path, so a failed run doesn't leak the connection. When you stream with streamText, close the client in the onEnd callback instead.

AI SDK 7 renamed several options from AI SDK 6. stepCountIs is now isStepCount, system is now instructions, and onFinish is now onEnd, and the AI SDK 7 codemods apply these renames for you.

Copy link to heading7. Hand the buyer the checkout URL

create_checkout returns a continue_url while the checkout is incomplete, requires_escalation, or ready_for_complete, and sending the buyer to that URL is the default completion path. Calling create_checkout twice with the same cart_id returns the same incomplete checkout, so a retried step doesn't open a second checkout.

Direct completion with complete_checkout requires a token that Shopify has granted permission to complete purchases, and the signed and anonymous tiers can't call it at all. Build the agent around the continue_url handoff unless your token carries that permission.

Store the cart ID between requests in a session record, a database row, or an encrypted cookie, and pass it back to the agent on the shopper's next turn. Store the cart's context and attribution in the same record and pass them to createShopTools, because Shopify doesn't return them from get_cart. Losing the cart ID strands a cart the buyer can no longer reach through your agent. Payment stays on the merchant's checkout on every path.

Copy link to headingHow to test a Shopify MCP integration before you deploy

Check Shopify's tool list before any application code runs. The UCP endpoint answers tools/list without credentials or an agent profile, so a curl request confirms that every tool your agent calls is still advertised.

Save this script as scripts/check-shopify-tools.sh and run it in CI with SHOPIFY_STORE_DOMAIN set to your staging store. It requires curl and jq, and it exits with status 1 and an error message when the endpoint redirects or a tool is missing:

#!/usr/bin/env bash
# Fails if Shopify's UCP endpoint stops advertising a tool the agent calls.
set -euo pipefail
required=(search_catalog create_cart get_cart update_cart create_checkout)
response=$(curl -s -w '\n%{http_code}' -X POST "https://${SHOPIFY_STORE_DOMAIN}/api/ucp/mcp" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}')
status=${response##*$'\n'}
if [[ "$status" != "200" ]]; then
echo "Shopify UCP endpoint returned HTTP $status (302 means the storefront is password-protected)" >&2
exit 1
fi
tools=$(jq -r '.result.tools[].name' <<< "${response%$'\n'*}")
for name in "${required[@]}"; do
if ! grep -qx "$name" <<< "$tools"; then
echo "Missing Shopify MCP tool: $name" >&2
exit 1
fi
done
echo "All ${#required[@]} Shopify MCP tools present"

The MCP Inspector command-line interface (CLI) can't run this check against Shopify. Inspector v2 sends follow-up requests after connecting, and Shopify rejects them with error -32001 because they carry no agent profile.

Copy link to headingCatch changed tool schemas with tool fingerprints

The script above catches a renamed tool but passes a changed schema under the same name. Shopify's UCP migration changed both names and schemas, wrapping catalog arguments in a catalog object and returning prices in minor currency units. AI SDK 7's fingerprintTools digests each tool's description, input schema, and title, and detectToolDrift compares the current digests with a baseline you stored after reviewing the definitions.

Save this as scripts/check-shopify-drift.ts, with a baseline written earlier from fingerprintTools:

import { readFile } from 'node:fs/promises';
import { detectToolDrift, fingerprintTools } from 'ai';
import { openShopClient } from '../lib/shopify';
const baseline = JSON.parse(await readFile('shopify-tools.baseline.json', 'utf8'));
const shop = await openShopClient();
try {
const drift = detectToolDrift(await fingerprintTools(await shop.tools()), baseline);
if (drift.changed.length > 0 || drift.added.length > 0) {
console.error('Shopify MCP tool definitions changed:', drift);
process.exitCode = 1;
}
} finally {
await shop.close();
}

Copy link to headingRun the full flow on a preview deployment

Every push to a non-production branch gets its own preview deployment with variables scoped to the Preview environment. Point SHOPIFY_STORE_DOMAIN at your staging store in Preview, and keep UCP_AGENT_PROFILE_URL on the production profile from step 1.

Copy link to headingTrace tool calls with OpenTelemetry

Install @ai-sdk/otel and register the telemetry integration once in instrumentation.ts, alongside your OpenTelemetry provider setup:

import { registerTelemetry } from 'ai';
import { OpenTelemetry } from '@ai-sdk/otel';
export function register() {
registerTelemetry(new OpenTelemetry());
}

Once an integration is registered, every AI SDK 7 call emits telemetry by default. The OpenTelemetry integration follows the OpenTelemetry GenAI semantic conventions, so tool spans carry gen_ai.tool.name and gen_ai.tool.call.id. Use LegacyOpenTelemetry instead if your dashboards query the older ai.* attributes.

AI Gateway request logs show each request's model, provider attempts, latency, token usage, status, and cost. AI Gateway keeps routing attempt details for 30 days.

Copy link to headingHow to deploy and protect the Shopify MCP agent

Set maxDuration in each route at the ceiling that route needs, as the agent route does in step 6. Routes that return in milliseconds then don't inherit the agent loop's budget. On Pro and Enterprise, raise the agent route up to 800 if your loop runs longer than 300s.

Copy link to headingProtect the endpoint with BotID

BotID is an invisible CAPTCHA, so buyers never see a challenge, and you can enable its Deep Analysis tier on high-value routes. With botid@1.5.0 or later, checkBotId() also reports isVerifiedBot and verifiedBotName from Vercel's verified bot directory. Your handler can then admit verified agents that shop on behalf of real people while blocking scrapers.

After you add BotID's client-side setup, check each request at the top of the agent route's POST handler:

import { checkBotId } from 'botid/server';
const verification = await checkBotId();
if (verification.isBot && !verification.isVerifiedBot) {
return new Response('Access denied', { status: 403 });
}

Deep Analysis bills per checkBotId() call, at $1 per 1,000 calls on Pro and custom pricing on Enterprise.

Copy link to headingUse Vercel Workflows for carts that outlast a function

Vercel Workflows run durable steps that pause and resume without holding compute, with per-step retries and no limit on run duration. When a shopper leaves a cart on Tuesday and returns on Friday, the cart outlives any function duration, so run abandoned-cart follow-ups as a workflow and keep the Vercel Function for live conversation.

Copy link to headingAdd model fallbacks through AI Gateway

AI Gateway fails over automatically across providers that serve the same model. To fall back to different models, add a models array to providerOptions.gateway:

const result = await generateText({
model: 'anthropic/claude-opus-5',
providerOptions: {
gateway: {
models: ['google/gemini-3.1-pro-preview'],
},
},
// ...tools, stopWhen, prepareStep, and prompt from step 6
});

Through April 2026, roughly 3.5% of AI Gateway requests completed only after a fallback, which rescued 5.1% of token volume from errors, rate limits, and timeouts. For an agent holding a cart, a rescued request keeps the conversation going instead of dropping the buyer.

Copy link to headingHow to troubleshoot a Shopify MCP integration

These failures recur against Shopify's MCP surface.

Copy link to headingTool not found on the legacy endpoint

Calling search_shop_catalog, get_product_details, get_cart, or update_cart on https://{store-domain}/api/mcp returns JSON-RPC error -32602 with the message Tool not found, because Shopify removed those tools. Move each call to /api/ucp/mcp:

Legacy tool on /api/mcpReplacement
search_shop_catalogsearch_catalog on /api/ucp/mcp
get_product_detailslookup_catalog or get_product on /api/ucp/mcp
get_cart, update_cartcreate_cart, get_cart, update_cart, and cancel_cart on /api/ucp/mcp
search_shop_policies_and_faqsUnchanged on /api/mcp

Update input and output handling at the same time, because UCP wraps catalog arguments in a catalog object and returns prices in minor currency units with Shopify global ID (GID) identifiers. Calling a legacy tool name on the UCP endpoint without a token returns error -32000 with AuthenticationRequired instead of Tool not found, so check the tool name before you debug credentials. On /api/ucp/mcp, Tool not found for a current tool name means your agent profile doesn't declare that tool's capability, as described in the next section.

Copy link to headingUCP discovery failed

Error -32001 means Shopify couldn't load your agent profile, and the code field inside the error's data object tells you why. The code profile_unreachable means Shopify couldn't fetch the URL, which happens when the profile sits behind Deployment Protection or the path is wrong. The code invalid_profile_url means the request carried no profile URL, usually because the meta object is missing from the tool arguments or UCP_AGENT_PROFILE_URL is unset. Open the profile URL in a private browser window to confirm it returns JSON without a login, then run the profile check from step 1.

When the profile loads but doesn't declare a capability, Shopify drops that capability's tools from the negotiated set. Calling one of those tools returns error -32602 with Tool not found instead of -32001.

Copy link to headingCart items or localization disappear after an edit

When a cart loses items or localization after an edit, the update_cart call sent a partial cart object. Read the line items with get_cart, change the one value, and send the complete payload, as withQuantity does in step 5. Localization lost while line items survive means the write left out context, which get_cart doesn't return, so resend it from your session state.

Copy link to headingThrottled checkout calls

Shopify publishes no numeric rate limits, and Checkout MCP throttles sooner than Cart MCP at every tier. Keep browsing and line-item edits on Cart MCP, and call checkout tools only after the buyer confirms intent. When Shopify throttles a request, retry after the delay in the HTTP Retry-After header, and apply exponential backoff with jitter when the header is absent.

Copy link to headingRedirect errors when the client connects

AI SDK 7 rejects HTTP redirects by default, so a redirect from the store endpoint surfaces as a transport error. Redirects to /password mean the store's storefront password is on, which gates /api/ucp/mcp the same way it gates the storefront. Shopify supports no way around that gate, so turn the password off or test against a different store, as described in the prerequisites. Setting redirect: 'follow' doesn't help, because the redirect target is an HTML page. For any other redirect, check that SHOPIFY_STORE_DOMAIN holds the store's domain with no path or protocol.

Copy link to headingProtocol errors read differently from tool errors

The MCP specification separates two failure channels. When a response carries a JSON-RPC error field, the request couldn't be processed, and Shopify uses code -32000 for those, with -32001 for discovery errors. Results with isError: true mean the tool ran and rejected its input.

The tools in step 4 return isError results to the model so it can correct its input on the next step. JSON-RPC errors make callTool throw, and AI SDK records the failure as a tool error for that step.

Copy link to headingHow to keep up with Shopify MCP changes

Shopify changed this surface several times in 2026. The legacy catalog tools stayed available until June 15, 2026, after Shopify moved the catalog to UCP. Shopify announced the deprecation of the Storefront MCP cart tools on June 24, 2026, and has since removed them. Shopify storefronts added support for UCP 2026-08-25 on September 4, 2026.

Three habits shorten your recovery when the next change ships:

  • Subscribe to the Shopify developer changelog, which posts deprecations while the old tools still work.
  • Keep Shopify tool names in one module, as lib/shop-tools.ts does, so a rename is one edit.
  • Run the tool check and the drift check on every preview deployment, so a change fails CI before a shopper reaches it.

Copy link to headingNext steps

Once the purchase flow passes on a preview deployment, promote the same project settings to production. These resources cover each layer in more depth:

Copy link to headingFrequently asked questions

Copy link to headingWhy does search_shop_catalog return "Tool not found"?

Shopify removed search_shop_catalog and the other Storefront MCP catalog and cart tools from /api/mcp when it moved them to UCP. Developers in Shopify's community forums reported the change breaking production apps before they found an announcement, so a tool check in CI is the most reliable early warning.

Copy link to headingWhat is the difference between Shopify's Storefront MCP and its UCP MCP servers?

Storefront MCP is the older endpoint at /api/mcp, and it now hosts only search_shop_policies_and_faqs, which Shopify documents as the Policy and FAQs tool. The UCP servers at /api/ucp/mcp host catalog, cart, checkout, and order tools, and UCP defines no policies capability. Agents that answer shipping, returns, or warranty questions call both endpoints.

Copy link to headingWhat happens if my agent profile is missing or malformed?

Shopify returns an error before any tool runs when it can't load the profile or finds it invalid. For an unreachable profile or a missing profile URL, that error is -32001. When the profile loads, Shopify keeps only the capabilities both sides support and prunes any extension whose parent capability is missing. Tools for a dropped capability then return -32602 with Tool not found, even though the profile itself loaded. Run the profile check from step 1 after every change to the profile.

Copy link to headingWhen should I call the Global Catalog MCP?

Call the Global Catalog MCP for searches that span more than one merchant. It runs on its own host at https://catalog.shopify.com/api/ucp/mcp and supports filters such as shipping country, price, and availability. Cart permalinks from global results are scoped to one merchant, so group selected variants by shop domain and create one checkout URL per merchant.

Copy link to headingCan I authenticate a Shopify MCP agent without a Bearer token?

Yes, at two lower tiers. The signed tier uses HTTP Message Signatures (RFC 9421) with ECDSA P-256 keys published in your agent profile, and the anonymous tier sends no credentials. Both tiers reach catalog, cart, and checkout tools at lower rate limits, and neither can call complete_checkout or get_order.