
Building an AI Shopping Agent with UCP
In this tutorial, we'll build a shopping agent that uses UCP to discover products across multiple stores, compare options, and complete purchases — all through the Model Context Protocol (MCP).
What We're Building
By the end of this guide, you'll have a working agent that can:
- Accept natural language shopping requests
- Discover UCP merchants via the Registry API
- Search products across multiple stores simultaneously via MCP
- Rank results by price, availability, and shipping speed
- Complete checkout through UCP with consumer confirmation
This is a production architecture — not a demo. We'll use TypeScript, the MCP SDK, and Claude for natural language understanding.
Architecture
Consumer <-> Agent Core <-> MCP Client <-> UCP Merchants
| (via /.well-known/ucp)
v
LLM Service
|
v
UCP Registry API
(merchant & product discovery)
Agent Core handles conversation, preferences, and orchestration. MCP Client connects to merchant endpoints and calls tools like search_products and create_checkout. UCP Registry API provides the merchant index and product discovery — so your agent doesn't need to know merchant URLs upfront. For a deeper explanation of the discovery layer, see how AI agents discover merchants through UCP.
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: Discover Merchants via the Registry
Instead of hardcoding merchant URLs, use the UCP Registry to discover stores that carry relevant products:
// src/registry.ts
interface RegistryProduct {
title: string;
price: number;
currency: string;
url: string | null;
image_url: string | null;
product_type: string | null;
merchant: string;
store_category: string | null;
platform: string | null;
}
interface RegistryResponse {
data: RegistryProduct[];
meta: {
total: number;
returned: number;
merchants: number;
query: string;
};
}
// Find which stores carry what you're looking for
export async function discoverProducts(
query: string,
options?: { category?: string; maxPrice?: number; limit?: number }
): Promise<RegistryResponse> {
const params = new URLSearchParams({ q: query });
if (options?.category) params.set('category', options.category);
if (options?.maxPrice) params.set('max_price', String(options.maxPrice));
if (options?.limit) params.set('limit', String(options.limit));
const resp = await fetch(
`https://ucpregistry.com/api/v1/products/search?${params}`
);
return resp.json();
}
// Get full merchant details
export async function getMerchantDetails(domain: string) {
const resp = await fetch(
`https://ucpregistry.com/api/v1/merchants/${domain}`,
{ headers: { Authorization: `Bearer ${process.env.REGISTRY_API_KEY}` } }
);
return resp.json();
}
The Registry's Product Discovery API returns sample products across all UCP stores — use it to route your agent to the right merchants before opening MCP connections.
Step 3: Connect to Merchant MCP Endpoints
Once you've identified relevant merchants, connect to their MCP endpoints for the full catalog and live checkout:
// src/merchant.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport }
from '@modelcontextprotocol/sdk/client/streamableHttp.js';
interface UCPManifest {
ucp: {
version: string;
services: {
'dev.ucp.shopping': Array<{
transport: string;
endpoint: string;
}>;
};
};
}
// Discover a merchant's MCP endpoint from their manifest
async function discoverMcpEndpoint(domain: string): Promise<string | null> {
const resp = await fetch(`https://${domain}/.well-known/ucp`);
const manifest: UCPManifest = await resp.json();
const mcpService = manifest.ucp.services['dev.ucp.shopping']
?.find(s => s.transport === 'mcp');
return mcpService?.endpoint ?? null;
}
// Connect to a merchant's MCP server
export async function connectToMerchant(domain: string): Promise<Client | null> {
const endpoint = await discoverMcpEndpoint(domain);
if (!endpoint) return null;
const client = new Client({
name: 'shopping-agent',
version: '1.0.0',
});
await client.connect(
new StreamableHTTPClientTransport(new URL(endpoint))
);
return client;
}
Step 4: Search Products via MCP
With an MCP connection open, search the merchant's full catalog:
// src/search.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
export async function searchMerchant(
client: Client,
domain: string,
query: string,
limit = 10,
) {
const result = await client.callTool({
name: 'search_products',
arguments: { query, limit },
});
const text = result.content?.[0];
if (text && 'text' in text) {
const data = JSON.parse(text.text);
return (data.products ?? []).map((p: any) => ({
...p,
merchant: domain,
}));
}
return [];
}
// Search multiple merchants in parallel
export async function searchAllMerchants(
merchants: Array<{ domain: string; client: Client }>,
query: string,
) {
const results = await Promise.allSettled(
merchants.map(m => searchMerchant(m.client, m.domain, query))
);
return results
.filter(r => r.status === 'fulfilled')
.flatMap(r => (r as PromiseFulfilledResult<any[]>).value);
}
Step 5: Natural Language Understanding
Use an LLM to translate shopping requests into structured queries:
// src/nlp.ts
import Anthropic from '@anthropic-ai/sdk';
import { z } from 'zod';
const anthropic = new Anthropic();
const ShoppingIntent = 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 ShoppingIntent>;
export async function parseShoppingRequest(
message: string
): Promise<ShoppingIntent> {
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 500,
system: `Extract shopping intent as JSON:
{ searchQuery, category?, maxPrice?, constraints[], urgency? }`,
messages: [{ role: 'user', content: message }],
});
const text = response.content[0];
const json = text.type === 'text' ? text.text : '{}';
return ShoppingIntent.parse(JSON.parse(json));
}
"Find me wireless earbuds under $80 with noise cancellation, need them by Thursday" becomes:
{
"searchQuery": "wireless earbuds noise cancellation",
"category": "electronics",
"maxPrice": 80,
"constraints": ["noise cancellation"],
"urgency": "fast"
}
Step 6: Ranking and Comparison
Rank results from multiple merchants:
// src/ranking.ts
interface RankedProduct {
id: string;
title: string;
price: { amount: number; currency: string };
available: boolean;
merchant: string;
}
export function rankProducts(
products: RankedProduct[],
maxPrice?: number,
): RankedProduct[] {
return products
.filter(p => {
if (!p.available) return false;
if (maxPrice && p.price.amount > maxPrice * 100) return false;
return true;
})
.sort((a, b) => a.price.amount - b.price.amount);
}
Step 7: Checkout Flow
UCP requires explicit consumer confirmation before processing payment. Create a checkout session via MCP, then present it for approval:
// src/checkout.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
export async function createCheckout(
client: Client,
variantId: string,
quantity = 1,
) {
const result = await client.callTool({
name: 'create_checkout',
arguments: {
items: [{ variant_id: variantId, quantity }],
},
});
const text = result.content?.[0];
if (text && 'text' in text) {
const session = JSON.parse(text.text);
return {
checkoutUrl: session.checkout_url,
total: session.total,
expiresAt: session.expires_at,
};
}
throw new Error('Failed to create checkout');
}
The checkout_url is what you present to the consumer. They confirm and complete payment on the merchant's site — your agent never handles payment credentials.
Step 8: Putting It All Together
// src/index.ts
import { discoverProducts } from './registry';
import { connectToMerchant } from './merchant';
import { searchAllMerchants } from './search';
import { rankProducts } from './ranking';
import { parseShoppingRequest } from './nlp';
import { createCheckout } from './checkout';
export async function handleShoppingRequest(message: string) {
// 1. Understand intent
const intent = await parseShoppingRequest(message);
// 2. Discover relevant merchants via Registry
const discovery = await discoverProducts(intent.searchQuery, {
category: intent.category,
maxPrice: intent.maxPrice,
limit: 20,
});
// 3. Get unique merchant domains
const domains = [...new Set(discovery.data.map(p => p.merchant))];
// 4. Connect to each merchant via MCP
const merchants = (await Promise.all(
domains.map(async domain => {
const client = await connectToMerchant(domain);
return client ? { domain, client } : null;
})
)).filter(Boolean) as Array<{ domain: string; client: any }>;
// 5. Search full catalogs via MCP (live data)
const products = await searchAllMerchants(merchants, intent.searchQuery);
// 6. Rank and return top results
const ranked = rankProducts(products, intent.maxPrice);
return {
products: ranked.slice(0, 5),
merchants: domains.length,
// When consumer picks a product, call createCheckout()
};
}
The flow is: Registry discovers which stores are relevant, MCP connects for live catalog data and checkout. The Registry is the routing layer; MCP is the transaction layer.
Production Considerations
Error recovery. Merchant inventory can change between search and checkout. Handle MCP error responses explicitly and offer alternatives.
Preference learning. Track which merchants the consumer buys from, preferred price ranges, and brand affinities. Improve ranking over time.
Caching. Cache product discovery results from the Registry API (they update on a probing schedule, not real-time). Don't cache MCP results — those should always be live.
Rate limiting. Some merchants rate-limit MCP queries. Implement exponential backoff and use the Registry's sample products for initial discovery to reduce unnecessary MCP connections.
Monitoring. Track checkout success rates and merchant response times. Deprioritize merchants with high failure rates automatically.
Next Steps
- Browse verified UCP merchants in the Registry
- Test your agent against stores in the Product Discovery index
- Run benchmarks against merchants using the UCP Playground
- Read the API documentation for the full Registry API reference
For more project ideas beyond shopping agents, check out 5 things you can build with UCP today. To understand the payment side, read our payment handlers overview. And for the big picture on where agentic commerce is heading, see the future of agentic commerce: UCP, A2A, and MCP.
Building a shopping agent? Contact us for API access and integration support.


