Transform any AI into a shopping expert. Connect to MCIP, maintain context across conversations, and let your agent search, compare, and manage carts like a seasoned personal shopper.
Think of your AI agent as a personal shopping assistant at a high-end department store. The assistant doesn't just answer questions—they remember your preferences, carry your selections, suggest alternatives, and guide you through checkout. That's exactly what we're building.
MCIP provides the shopping expertise (semantic search, cart management, multi-store access). Your agent provides the conversational intelligence (understanding intent, maintaining context, making recommendations). Together, they create something neither could achieve alone.
MCIP exposes tools through the Model Context Protocol (MCP). Your agent discovers available tools, then calls them as needed during conversations:
User: "Find me a gaming laptop under $1500"
↓
Agent: [Understands intent: product search]
↓
Agent → MCIP: search_product({query: "gaming laptop", maxPrice: 1500})
↓
MCIP → Agent: [Returns 8 products with scores]
↓
Agent: "I found 8 gaming laptops under $1500. The top pick is..."MCIP provides five core tools for commerce operations:
| Tool | Purpose | When to Use |
|---|---|---|
search_product | Semantic product search | User asks to find, browse, or compare products |
add_to_cart | Add item to session cart | User wants to save or buy a product |
view_cart | Show current cart contents | User asks "what's in my cart?" |
update_cart_item | Change quantity or remove | User says "remove that" or "I need 3" |
clear_cart | Empty entire cart | User wants to start over |
First, your agent needs to know what MCIP can do:
// src/mcip-client.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
export class MCIPClient {
private client: Client;
private sessionId: string;
constructor() {
this.client = new Client(
{ name: "shopping-agent", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
this.sessionId = this.generateSessionId();
}
async connect(): Promise<void> {
const transport = new StdioClientTransport({
command: "node",
args: ["path/to/mcip-server"],
});
await this.client.connect(transport);
console.log("Connected to MCIP");
}
async discoverTools(): Promise<Tool[]> {
const response = await this.client.listTools();
return response.tools;
}
private generateSessionId(): string {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}Now let's search for products:
// Continuing MCIPClient class
async searchProducts(query: string, options?: SearchOptions): Promise<Product[]> {
const result = await this.client.callTool({
name: "search_product",
arguments: {
sessionId: this.sessionId,
query: query,
filters: {
maxPrice: options?.maxPrice,
minPrice: options?.minPrice,
categories: options?.categories,
inStock: options?.inStock ?? true,
},
limit: options?.limit ?? 10,
},
});
return this.parseSearchResults(result);
}
private parseSearchResults(result: any): Product[] {
// MCP returns content as an array of content blocks
const content = result.content[0];
if (content.type === "text") {
return JSON.parse(content.text).products;
}
return [];
}Here's a minimal agent that can search and respond:
// src/simple-agent.ts
import { MCIPClient } from "./mcip-client";
import OpenAI from "openai";
const openai = new OpenAI();
const mcip = new MCIPClient();
async function handleUserMessage(message: string): Promise<string> {
// Step 1: Understand the user's intent
const intentResponse = await openai.chat.completions.create({
model: "gpt-4",
messages: [
{
role: "system",
content: `You are a shopping assistant. Analyze the user's message and respond with JSON:
{"intent": "search" | "add_to_cart" | "view_cart" | "general",
"query": "extracted search terms if searching",
"filters": {"maxPrice": number | null, "category": string | null}}`
},
{ role: "user", content: message }
],
response_format: { type: "json_object" },
});
const intent = JSON.parse(intentResponse.choices[0].message.content!);
// Step 2: Execute the appropriate action
let context = "";
if (intent.intent === "search") {
const products = await mcip.searchProducts(intent.query, intent.filters);
context = formatProductsForLLM(products);
} else if (intent.intent === "view_cart") {
const cart = await mcip.viewCart();
context = formatCartForLLM(cart);
}
// Step 3: Generate natural response
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: [
{
role: "system",
content: `You are a helpful shopping assistant. Use the provided product/cart data to answer naturally. Be concise but informative.`
},
{ role: "user", content: message },
{ role: "assistant", content: `[Available data: ${context}]` }
],
});
return response.choices[0].message.content!;
}
function formatProductsForLLM(products: Product[]): string {
return products.map((p, i) =>
`${i + 1}. ${p.name} - $${p.price.amount} (${p.score.toFixed(2)} relevance)`
).join("\n");
}Without sessions, every interaction starts fresh. The agent forgets the laptop you loved, the items in your cart, and the preferences you mentioned. Sessions fix this by maintaining state across the conversation.
// src/session-manager.ts
export class SessionManager {
private sessions: Map<string, SessionData> = new Map();
private readonly TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
createSession(userId: string): string {
const sessionId = `sess_${userId}_${Date.now()}`;
this.sessions.set(sessionId, {
id: sessionId,
userId,
createdAt: new Date(),
lastActivity: new Date(),
context: {
searchHistory: [],
viewedProducts: [],
preferences: {},
},
});
return sessionId;
}
getSession(sessionId: string): SessionData | null {
const session = this.sessions.get(sessionId);
if (!session) return null;
// Check if expired
const age = Date.now() - session.lastActivity.getTime();
if (age > this.TTL_MS) {
this.sessions.delete(sessionId);
return null;
}
// Update last activity
session.lastActivity = new Date();
return session;
}
updateContext(sessionId: string, updates: Partial<SessionContext>): void {
const session = this.getSession(sessionId);
if (session) {
session.context = { ...session.context, ...updates };
}
}
}
interface SessionData {
id: string;
userId: string;
createdAt: Date;
lastActivity: Date;
context: SessionContext;
}
interface SessionContext {
searchHistory: SearchQuery[];
viewedProducts: string[]; // Product IDs
preferences: UserPreferences;
}Track what the user has seen and discussed:
// Enhanced agent with context awareness
class ContextAwareAgent {
private mcip: MCIPClient;
private sessions: SessionManager;
private conversationHistory: Map<string, Message[]> = new Map();
async handleMessage(sessionId: string, message: string): Promise<string> {
const session = this.sessions.getSession(sessionId);
if (!session) {
throw new Error("Session expired. Please start a new conversation.");
}
// Get conversation history
const history = this.conversationHistory.get(sessionId) || [];
// Add user message
history.push({ role: "user", content: message });
// Build context from session
const contextPrompt = this.buildContextPrompt(session);
// Generate response with full context
const response = await this.generateResponse(contextPrompt, history);
// Update session based on what happened
await this.updateSessionFromInteraction(session, message, response);
// Save to history
history.push({ role: "assistant", content: response });
this.conversationHistory.set(sessionId, history.slice(-20)); // Keep last 20
return response;
}
private buildContextPrompt(session: SessionData): string {
const { searchHistory, viewedProducts, preferences } = session.context;
let context = "User context:\n";
if (searchHistory.length > 0) {
const recent = searchHistory.slice(-3);
context += `- Recent searches: ${recent.map(s => s.query).join(", ")}\n`;
}
if (viewedProducts.length > 0) {
context += `- Recently viewed ${viewedProducts.length} products\n`;
}
if (preferences.priceRange) {
context += `- Preferred price range: $${preferences.priceRange.min}-$${preferences.priceRange.max}\n`;
}
if (preferences.brands?.length) {
context += `- Preferred brands: ${preferences.brands.join(", ")}\n`;
}
return context;
}
}Users don't say "execute add_to_cart with product ID xyz". They say "I'll take that one" or "add the second laptop to my cart". Your agent needs to bridge this gap.
// src/cart-handler.ts
export class CartHandler {
private mcip: MCIPClient;
private recentProducts: Map<string, Product[]> = new Map(); // sessionId -> products
async handleCartIntent(
sessionId: string,
intent: CartIntent
): Promise<CartResult> {
switch (intent.action) {
case "add":
return this.handleAdd(sessionId, intent);
case "remove":
return this.handleRemove(sessionId, intent);
case "update_quantity":
return this.handleQuantityUpdate(sessionId, intent);
case "view":
return this.handleView(sessionId);
case "clear":
return this.handleClear(sessionId);
}
}
private async handleAdd(sessionId: string, intent: CartIntent): Promise<CartResult> {
// Resolve "that one", "the first option", "the Nike shoes"
const product = await this.resolveProductReference(
sessionId,
intent.productReference
);
if (!product) {
return {
success: false,
message: "I'm not sure which product you mean. Could you be more specific?",
};
}
// Select variant if needed
const variant = await this.resolveVariant(product, intent.variantHints);
// Add to cart
const result = await this.mcip.addToCart({
sessionId,
productId: product.externalId,
variantId: variant?.sku,
quantity: intent.quantity || 1,
});
return {
success: true,
message: `Added ${product.name} to your cart.`,
cart: result.cart,
};
}
private async resolveProductReference(
sessionId: string,
reference: string
): Promise<Product | null> {
const recentProducts = this.recentProducts.get(sessionId) || [];
// Handle positional references
const positionMatch = reference.match(/^(first|second|third|\d+)/i);
if (positionMatch) {
const positions: Record<string, number> = {
first: 0, second: 1, third: 2,
"1": 0, "2": 1, "3": 2,
};
const index = positions[positionMatch[1].toLowerCase()] ?? parseInt(positionMatch[1]) - 1;
return recentProducts[index] || null;
}
// Handle "that one", "this" - return most recently discussed
if (/^(that|this|it)$/i.test(reference)) {
return recentProducts[0] || null;
}
// Handle name/brand matching
const lowerRef = reference.toLowerCase();
return recentProducts.find(p =>
p.name.toLowerCase().includes(lowerRef) ||
p.brand?.toLowerCase().includes(lowerRef)
) || null;
}
// Track products shown to user
recordShownProducts(sessionId: string, products: Product[]): void {
this.recentProducts.set(sessionId, products);
}
}
interface CartIntent {
action: "add" | "remove" | "update_quantity" | "view" | "clear";
productReference?: string; // "the first one", "Nike Air Max", "that"
variantHints?: string[]; // ["size 10", "blue"]
quantity?: number;
}Real shopping conversations flow naturally:
// Example conversation flow
// Turn 1
User: "Find me running shoes under $150"
Agent: [Searches, shows 5 options]
"I found 5 great options! The top picks are:
1. Nike Air Zoom - $129 (highly rated for cushioning)
2. Adidas Ultraboost - $140 (great for long distances)
3. Brooks Ghost - $119 (excellent stability)..."
// Turn 2
User: "Tell me more about the Nike ones"
Agent: [Retrieves product details from context]
"The Nike Air Zoom features responsive foam cushioning..."
// Turn 3
User: "I'll take them in size 10"
Agent: [Resolves "them" → Nike Air Zoom, variant → size 10]
"Perfect! I've added the Nike Air Zoom in size 10 to your cart.
Your cart total is $129. Would you like to continue shopping?"
// Turn 4
User: "Actually, make that 2 pairs"
Agent: [Updates quantity]
"Done! Updated to 2 pairs. Your new total is $258."Graceful degradation keeps conversations flowing:
// src/resilient-agent.ts
async function safeToolCall<T>(
operation: () => Promise<T>,
fallback: T,
context: string
): Promise<{ result: T; hadError: boolean }> {
try {
const result = await operation();
return { result, hadError: false };
} catch (error) {
console.error(`Error in ${context}:`, error);
// Log for monitoring
await logError({
context,
error: error.message,
timestamp: new Date(),
});
return { result: fallback, hadError: true };
}
}
// Usage in agent
async function handleSearch(query: string): Promise<string> {
const { result: products, hadError } = await safeToolCall(
() => mcip.searchProducts(query),
[], // Empty array as fallback
"product_search"
);
if (hadError) {
return "I'm having trouble searching right now. Could you try again in a moment?";
}
if (products.length === 0) {
return "I couldn't find products matching that description. Could you try different terms?";
}
return formatProductResponse(products);
}Protect both MCIP and external APIs:
// src/rate-limiter.ts
import { RateLimiter } from "limiter";
export class AgentRateLimiter {
private limiters: Map<string, RateLimiter> = new Map();
private getLimiter(sessionId: string): RateLimiter {
if (!this.limiters.has(sessionId)) {
// 30 requests per minute per session
this.limiters.set(sessionId, new RateLimiter({
tokensPerInterval: 30,
interval: "minute",
}));
}
return this.limiters.get(sessionId)!;
}
async checkLimit(sessionId: string): Promise<boolean> {
const limiter = this.getLimiter(sessionId);
const hasToken = await limiter.tryRemoveTokens(1);
if (!hasToken) {
console.warn(`Rate limit exceeded for session ${sessionId}`);
}
return hasToken;
}
}Track what matters in production:
// src/agent-metrics.ts
import { Counter, Histogram, Gauge } from "prom-client";
export const metrics = {
// Conversation metrics
messagesProcessed: new Counter({
name: "agent_messages_total",
help: "Total messages processed",
labelNames: ["intent", "status"],
}),
// Tool usage
toolCalls: new Counter({
name: "agent_tool_calls_total",
help: "MCP tool invocations",
labelNames: ["tool", "status"],
}),
// Response times
responseLatency: new Histogram({
name: "agent_response_seconds",
help: "Time to generate response",
labelNames: ["intent"],
buckets: [0.5, 1, 2, 5, 10],
}),
// Session health
activeSessions: new Gauge({
name: "agent_active_sessions",
help: "Currently active sessions",
}),
// Cart metrics
cartOperations: new Counter({
name: "agent_cart_operations_total",
help: "Cart operations performed",
labelNames: ["operation", "status"],
}),
};
// Usage
metrics.toolCalls.inc({ tool: "search_product", status: "success" });
metrics.responseLatency.observe({ intent: "search" }, responseTime);| Scale | Architecture | Session Storage | Notes |
|---|---|---|---|
| Prototype | Single instance | In-memory | Fine for development |
| Small (< 100 concurrent) | Single instance | Redis | Add persistence |
| Medium (100-1000) | 2-3 instances + LB | Redis cluster | Horizontal scaling |
| Large (1000+) | Auto-scaling group | Redis cluster + sharding | Full production setup |
For ChatGPT plugins and GPT Actions:
# openapi.yaml for GPT Action
openapi: 3.0.0
info:
title: Shopping Assistant API
version: 1.0.0
servers:
- url: https://your-agent.com/api
paths:
/search:
post:
operationId: searchProducts
summary: Search for products
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
query:
type: string
description: Natural language search query
maxPrice:
type: number
responses:
'200':
description: Search resultsFor Anthropic Claude with tool use:
const tools = [
{
name: "search_products",
description: "Search for products using natural language. Use when user wants to find, browse, or compare products.",
input_schema: {
type: "object",
properties: {
query: {
type: "string",
description: "Natural language search query",
},
max_price: {
type: "number",
description: "Maximum price filter",
},
},
required: ["query"],
},
},
// ... other tools
];
const response = await anthropic.messages.create({
model: "claude-3-opus-20240229",
max_tokens: 1024,
tools: tools,
messages: [{ role: "user", content: userMessage }],
});Cause: Product context not being tracked
Solution: Ensure you record shown products after each search.
Cause: TTL too short or activity not being tracked
Solution: Update lastActivity on every interaction and increase TTL if needed.
Cause: Sequential API calls
Solution: Parallelize where possible:
// Instead of sequential
const products = await mcip.searchProducts(query);
const cart = await mcip.viewCart();
// Use parallel
const [products, cart] = await Promise.all([
mcip.searchProducts(query),
mcip.viewCart(),
]);Cause: Session ID mismatch between agent and MCIP
Solution: Verify session ID is passed consistently:
// Always include sessionId in MCIP calls
await mcip.addToCart({
sessionId: this.currentSessionId, // Must match across all calls
productId: product.id,
quantity: 1,
});You've built an intelligent shopping agent! Here's where to go from here: