All Posts

Building an AI Shopping Agent with UCP

A practical tutorial on building an AI shopping agent that uses UCP to discover products, compare options, and complete purchases on behalf of consumers.

March 4, 2026UCPList Team
AI shopping agentUCP AI agentbuild shopping agent

What We're Building

In this tutorial, we'll build a shopping agent that can:

  1. Accept natural language shopping requests from a consumer
  2. Search UCP-enabled merchants for matching products
  3. Compare options across merchants on price, availability, and shipping
  4. Present recommendations to the consumer
  5. Complete the purchase through UCP checkout upon consumer confirmation

This isn't a toy demo; it's a production architecture that handles real transactions. We'll use TypeScript, the MCP SDK (for connecting to merchant UCP endpoints), and an LLM for natural language understanding.

Architecture Overview

A UCP shopping agent has four main components:

Consumer <-> Agent Core <-> MCP Client <-> UCP Merchants
                |                              (via /.well-known/ucp)
                v
           LLM Service

Agent Core handles conversation management, consumer preferences, and orchestration logic. It's your application code.

MCP Client connects to merchants' MCP endpoints (discovered via their /.well-known/ucp manifest) and calls tools like search_products and create_checkout. The @modelcontextprotocol/sdk npm package provides this.

LLM Service processes natural language inputs and generates structured queries. You can use any LLM provider.

Step 1: Project Setup

mkdir shopping-agent && cd shopping-agent
npm init -y
npm install @modelcontextprotocol/sdk @anthropic-ai/sdk zod
npm install -D typescript @types/node tsx
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "outDir": "dist"
  }
}

Step 2: Connect to Merchant MCP Endpoints

The core of a UCP shopping agent is connecting to merchants via their MCP endpoints. First, discover a merchant's capabilities by fetching their /.well-known/ucp manifest, then connect using the MCP client:

// src/merchant.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

interface UCPManifest {
  name: string;
  mcp_endpoint: string;
  capabilities: string[];
  payment_handlers: string[];
}

// Discover a merchant's UCP capabilities
async function discoverMerchant(storeUrl: string): Promise<UCPManifest> {
  const res = await fetch(new URL('/.well-known/ucp', storeUrl));
  return res.json();
}

// Connect to a merchant's MCP server
async function connectToMerchant(manifest: UCPManifest): Promise<Client> {
  const client = new Client({ name: 'Shopping Agent', version: '1.0.0' });
  const transport = new StreamableHTTPClientTransport(
    new URL(manifest.mcp_endpoint),
  );
  await client.connect(transport);
  return client;
}

The MCP client lets you call tools exposed by the merchant, like search_products, get_product, and create_checkout.

Step 3: Implement Product Search

The core of a shopping agent is searching across multiple merchants simultaneously. With MCP, you call each merchant's search_products tool in parallel:

// src/search.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';

interface SearchParams {
  query: string;
  maxPrice?: number;
  category?: string;
}

export async function searchMerchant(
  client: Client,
  merchantName: string,
  params: SearchParams,
) {
  const result = await client.callTool({
    name: 'search_products',
    arguments: {
      query: params.query,
      category: params.category,
      max_price: params.maxPrice ? params.maxPrice * 100 : undefined,
      limit: 10,
    },
  });

  const text = result.content?.[0];
  if (text && 'text' in text) {
    const products = JSON.parse(text.text);
    return products.map((p: any) => ({ ...p, merchant: merchantName }));
  }
  return [];
}

// Search multiple merchants in parallel
export async function searchAllMerchants(
  merchants: Array<{ name: string; client: Client }>,
  params: SearchParams,
) {
  const results = await Promise.allSettled(
    merchants.map((m) => searchMerchant(m.client, m.name, params)),
  );

  return results
    .filter((r) => r.status === 'fulfilled')
    .flatMap((r) => (r as PromiseFulfilledResult<any[]>).value);
}

You're making parallel MCP tool calls to each merchant's server and combining the results. Error handling is important; some merchants may be slow or unavailable.

Step 4: Natural Language Processing

Here's where the LLM comes in. We need to translate natural language shopping requests into structured search parameters:

// src/nlp.ts
import Anthropic from '@anthropic-ai/sdk';
import { z } from 'zod';

const client = new Anthropic();

const ShoppingIntentSchema = z.object({
  searchQuery: z.string(),
  category: z.string().optional(),
  maxPrice: z.number().optional(),
  constraints: z.array(z.string()),
  urgency: z.enum(['standard', 'fast', 'overnight']).optional(),
});

type ShoppingIntent = z.infer<typeof ShoppingIntentSchema>;

export async function parseShoppingRequest(
  userMessage: string,
): Promise<ShoppingIntent> {
  const response = await client.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 500,
    system: `Extract structured shopping intent from user messages.
Return JSON matching this schema:
{
  searchQuery: string (product search terms),
  category: string | null (product category),
  maxPrice: number | null (maximum price in dollars),
  constraints: string[] (specific requirements),
  urgency: "standard" | "fast" | "overnight" | null
}`,
    messages: [{ role: 'user', content: userMessage }],
  });

  const text =
    response.content[0].type === 'text' ? response.content[0].text : '';
  return ShoppingIntentSchema.parse(JSON.parse(text));
}

This gives us structured data from natural language. "Find me wireless earbuds under $80 with good noise cancellation, need them by Thursday" becomes:

{
  "searchQuery": "wireless earbuds noise cancellation",
  "category": "electronics",
  "maxPrice": 80,
  "constraints": ["noise cancellation"],
  "urgency": "fast"
}

Step 5: Product Comparison and Ranking

Once we have search results from multiple merchants, we need to rank them intelligently:

// src/ranking.ts
interface ProductSearchResult {
  id: string;
  title: string;
  price: { amount: number; currency: string };
  shipping: { estimatedDays: number; cost: number };
  available: boolean;
  merchant: string;
}

interface RankingCriteria {
  maxPrice?: number;
  constraints: string[];
  urgency?: 'standard' | 'fast' | 'overnight';
}

export function rankProducts(
  products: ProductSearchResult[],
  criteria: RankingCriteria,
): ProductSearchResult[] {
  return products
    .filter((p) => {
      // Hard filters
      if (criteria.maxPrice && p.price.amount > criteria.maxPrice * 100) {
        return false;
      }
      if (
        criteria.urgency === 'overnight' &&
        p.shipping.estimatedDays > 1
      ) {
        return false;
      }
      if (criteria.urgency === 'fast' && p.shipping.estimatedDays > 3) {
        return false;
      }
      return p.available;
    })
    .sort((a, b) => {
      // Score based on multiple factors
      const scoreA = computeScore(a, criteria);
      const scoreB = computeScore(b, criteria);
      return scoreB - scoreA;
    });
}

function computeScore(
  product: ProductSearchResult,
  criteria: RankingCriteria,
): number {
  let score = 0;

  // Price efficiency (lower is better, normalized 0-100)
  const priceRatio = criteria.maxPrice
    ? 1 - product.price.amount / (criteria.maxPrice * 100)
    : 0.5;
  score += priceRatio * 30;

  // Shipping speed (faster is better)
  score += Math.max(0, 10 - product.shipping.estimatedDays) * 5;

  // Free shipping bonus
  if (product.shipping.cost === 0) score += 10;

  // Availability bonus
  if (product.available) score += 20;

  return score;
}

Step 6: Checkout Flow

The purchase flow requires consumer confirmation; this is a UCP protocol requirement, not an optional feature. Here's how to implement it using MCP tool calls:

// src/checkout.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';

export async function initiateCheckout(
  client: Client,
  product: { id: string; title: string; merchant: string; price: { amount: number; currency: string } },
  variantId?: string,
) {
  // Call the merchant's create_checkout tool via MCP
  const result = await client.callTool({
    name: 'create_checkout',
    arguments: {
      product_id: product.id,
      variant_id: variantId,
      quantity: 1,
    },
  });

  const text = result.content?.[0];
  if (text && 'text' in text) {
    const session = JSON.parse(text.text);
    return {
      sessionId: session.session_id,
      summary: {
        product: product.title,
        merchant: product.merchant,
        price: formatPrice(product.price),
        total: formatPrice(session.total),
        checkoutUrl: session.checkout_url,
      },
      expiresAt: session.expires_at,
    };
  }

  throw new Error('Failed to create checkout session');
}

function formatPrice(price: { amount: number; currency: string }) {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: price.currency,
  }).format(price.amount / 100);
}

Step 7: Consumer Confirmation Patterns

How you present the confirmation to the consumer depends on your agent's interface. Here are common patterns:

Chat-based confirmation. Present the order summary as a formatted message with "Confirm" and "Cancel" buttons. The consumer reviews and taps to confirm.

Voice-based confirmation. Read out the key details (item, price, delivery estimate) and ask for verbal confirmation. Consider requiring a PIN or biometric for purchases above a threshold.

Notification-based confirmation. Push a rich notification to the consumer's phone with order details and a confirm button. This works well for agents that operate in the background.

The UCP specification requires that consumers explicitly confirm before payment is processed. Your agent must not auto-confirm purchases, even if the consumer has previously said "buy whatever you think is best." Each transaction needs its own confirmation.

Step 8: Putting It All Together

// src/index.ts
import { discoverMerchant, connectToMerchant } from './merchant';
import { parseShoppingRequest } from './nlp';
import { searchAllMerchants } from './search';
import { rankProducts } from './ranking';
import { initiateCheckout } from './checkout';

// List of known UCP merchant URLs (could also be loaded from a directory)
const MERCHANT_URLS = [
  'https://store-a.example.com',
  'https://store-b.example.com',
];

export async function handleShoppingRequest(message: string) {
  // 1. Understand what the consumer wants
  const intent = await parseShoppingRequest(message);

  // 2. Connect to UCP merchants via MCP
  const merchants = await Promise.all(
    MERCHANT_URLS.map(async (url) => {
      const manifest = await discoverMerchant(url);
      const client = await connectToMerchant(manifest);
      return { name: manifest.name, client };
    }),
  );

  // 3. Search across all merchants in parallel
  const results = await searchAllMerchants(merchants, {
    query: intent.searchQuery,
    maxPrice: intent.maxPrice,
    category: intent.category,
  });

  // 4. Rank and filter results
  const ranked = rankProducts(results, {
    maxPrice: intent.maxPrice,
    constraints: intent.constraints,
    urgency: intent.urgency,
  });

  if (ranked.length === 0) {
    return { type: 'no-results' as const, message: 'No products found.' };
  }

  // 5. Present top recommendations
  return {
    type: 'recommendations' as const,
    products: ranked.slice(0, 3),
    // When consumer selects a product, call initiateCheckout
    // with the appropriate merchant's MCP client
  };
}

Production Considerations

Building a production shopping agent requires attention to several additional concerns that go beyond the basic tutorial.

You'll need to handle error recovery gracefully. What happens when a merchant's inventory changes between search and checkout? MCP tool calls return structured error responses; handle them explicitly and offer alternatives.

Implement consumer preference learning. Store which merchants the consumer has bought from, preferred price ranges, sizing preferences, and brand affinities. Use this data to improve ranking over time.

Set up monitoring and alerting. Track checkout success rates, merchant response times, and consumer satisfaction. If a merchant's UCP endpoints start failing, your agent should deprioritize them automatically.

Consider rate limiting and caching. Some merchants may rate-limit UCP queries. Implement exponential backoff in your MCP client calls and cache product data for short periods to reduce redundant queries.

The UCP samples repository provides reference implementations that can serve as a starting point for your own agent. The MCP specification covers the client protocol details.

Find Merchants to Test Against

Building and testing an agent requires live UCP endpoints. The UCPList directory lists 57+ UCP-enabled merchants with confirmed live endpoints. Start with any of the verified listings to get real product data flowing through your agent.

Read next