---
title: How to build a Shopify MCP agent on Vercel
description: 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.
url: "https://vercel.com/kb/guide/build-a-shopify-mcp-agent-on-vercel"
published: 2026-10-01
last_updated: 2026-10-01
authors: Vercel
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

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`.

## Overview

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
  

## What 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.
  

### Choose 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.
  

## What 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.

| Server                 | Endpoint                                  | Tools                                                                                        |
| ---------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------- |
| Storefront Catalog MCP | `https://{store-domain}/api/ucp/mcp`      | `search_catalog`, `lookup_catalog`, `get_product`                                            |
| Global Catalog MCP     | `https://catalog.shopify.com/api/ucp/mcp` | `search_catalog`, `lookup_catalog`, `get_product` across stores                              |
| Cart MCP               | `https://{store-domain}/api/ucp/mcp`      | `create_cart`, `get_cart`, `update_cart`, `cancel_cart`                                      |
| Checkout MCP           | `https://{store-domain}/api/ucp/mcp`      | `create_checkout`, `get_checkout`, `update_checkout`, `complete_checkout`, `cancel_checkout` |
| Order MCP              | `https://{store-domain}/api/ucp/mcp`      | `get_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.

### What 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.

| Tier      | How the agent identifies itself                                   | Catalog, cart, and checkout tools | `complete_checkout`                                                 | `get_order`                                  |
| --------- | ----------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------- | -------------------------------------------- |
| Token     | Bearer JSON Web Token (JWT) minted from Dev Dashboard credentials | Yes, at the highest rate limits   | Only when Shopify grants the token permission to complete purchases | Only with the `read_global_api_orders` scope |
| Signed    | HTTP Message Signatures (RFC 9421) with ECDSA P-256 keys          | Yes, at lower rate limits         | No                                                                  | No                                           |
| Anonymous | No credentials                                                    | Yes, at the lowest rate limits    | No                                                                  | No                                           |

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.

### How 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.

## Where 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.

## How to wire Shopify MCP tools into your agent loop

The build takes seven steps across five files:

| File                           | Purpose                                        |
| ------------------------------ | ---------------------------------------------- |
| `app/.well-known/ucp/route.ts` | Serves your UCP agent profile                  |
| `lib/shopify.ts`               | Mints a Shopify token and opens the MCP client |
| `lib/cart.ts`                  | Builds complete `update_cart` payloads         |
| `lib/shop-tools.ts`            | Defines the tools the model can call           |
| `app/api/agent/route.ts`       | Runs the agent loop                            |

### 1\. 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`:

```typescript
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:

```bash
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:

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

### 2\. 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.

| Variable                | Type   | Value                                                 |
| ----------------------- | ------ | ----------------------------------------------------- |
| `SHOPIFY_STORE_DOMAIN`  | Config | Your store domain, such as `your-store.myshopify.com` |
| `SHOPIFY_CLIENT_ID`     | Secret | The client ID from the Dev Dashboard                  |
| `SHOPIFY_CLIENT_SECRET` | Secret | The client secret from the Dev Dashboard              |
| `UCP_AGENT_PROFILE_URL` | Config | The profile URL from step 1                           |

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

```bash
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.

### 3\. 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`:

```typescript
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.

### 4\. 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`:

```typescript
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.

### 5\. 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:

```typescript
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.

### 6\. 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`:

```typescript
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.

### 7\. 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.

## How 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:

```bash
#!/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.

### Catch 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`:

```typescript
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();
}
```

### Run 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.

### Trace tool calls with OpenTelemetry

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

```typescript
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.

## How 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.

### Protect 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:

```typescript
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.

### Use 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.

### Add 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`:

```typescript
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.

## How to troubleshoot a Shopify MCP integration

These failures recur against Shopify's MCP surface.

### Tool 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/mcp`       | Replacement                                                                   |
| ------------------------------- | ----------------------------------------------------------------------------- |
| `search_shop_catalog`           | `search_catalog` on `/api/ucp/mcp`                                            |
| `get_product_details`           | `lookup_catalog` or `get_product` on `/api/ucp/mcp`                           |
| `get_cart`, `update_cart`       | `create_cart`, `get_cart`, `update_cart`, and `cancel_cart` on `/api/ucp/mcp` |
| `search_shop_policies_and_faqs` | Unchanged 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.

### UCP 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`.

### Cart 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.

### Throttled 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.

### Redirect 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.

### Protocol 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.

## How 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.
  

## Next 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:

- [Build commerce agents with UCP](https://shopify.dev/docs/agents) in Shopify's agent documentation
  
- [Cart MCP reference](https://shopify.dev/docs/agents/carts-and-checkout/cart-mcp) and [Checkout MCP reference](https://shopify.dev/docs/agents/carts-and-checkout/checkout-mcp)
  
- [MCP tools in AI SDK](https://ai-sdk.dev/docs/ai-sdk-core/mcp-tools), including tool drift detection
  
- [Migrate AI SDK 6.x to 7.0](https://ai-sdk.dev/docs/migration-guides/migration-guide-7-0)
  
- [Configure Vercel Function duration](https://vercel.com/docs/functions/configuring-functions/duration)
  
- [AI Gateway model fallbacks](https://vercel.com/docs/ai-gateway/models-and-providers/model-fallbacks)
  
- [Handle verified bots with BotID](https://vercel.com/docs/botid/verified-bots)
  
- [Environment variables on Vercel](https://vercel.com/docs/environment-variables)
  
- [How to build and deploy conversational commerce](https://vercel.com/i/conversational-commerce)
  

## Frequently asked questions

### Why 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.

### What 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.

### What 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.

### When 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.

### Can 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`.