MCIP is the Machine Customer Interaction Protocol — a universal commerce protocol that gives AI agents shopping superpowers. Product discovery is the first implemented capability. Connect your agent to MCIP's search endpoints via simple HTTP calls, or explore the Playground Agent — our reference implementation that adds query refinement, structured comparison, and Multi-Criteria Optimization (MCOP) for mathematically-grounded product ranking.
MCIP is the Machine Customer Interaction Protocol — a universal commerce enablement system that provides AI agents with semantic understanding capabilities. Think of it as giving your AI agent the shopping expertise of a seasoned buyer.
Product discovery is the first step. MCIP provides intelligent search through two modes:
| Mode | Endpoint | Best For | Latency |
|---|---|---|---|
| Simple Vector Search | GET /search | Fast queries, straightforward searches | ~300ms |
| Agentic Search | GET /hard-filtering/search | Natural language with implicit filters | ~500ms |
Your agent provides conversational intelligence. MCIP provides commerce expertise. Together, they create something neither could achieve alone.
Direct vector similarity search in Qdrant. Fast, low-latency, no LLM calls:
# Simple search
curl "http://localhost:8080/search?q=gaming+laptop&take=10"When to use: Straightforward queries where the user's intent is clear and no filter extraction is needed.
Full 4-stage LangGraph workflow with intelligent filter extraction:
# Agentic search with automatic filter extraction
curl "http://localhost:8080/hard-filtering/search?q=nike+shoes+under+100"The 4-Stage Pipeline:
User query: "Nike shoes under $100"
↓
┌─────────────────────────────────────────┐
│ Stage 1: Parallel Filter Extraction │
│ (GPT-4o-mini extracts in parallel) │
│ • brand: ["Nike"] │
│ • category: ["Shoes"] │
│ • priceMax: 100 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Stage 2: Brand Validation │
│ (Qdrant facet search) │
│ • Validates "Nike" exists in catalog │
│ • If not found → returns empty │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Stage 3: Hybrid Search │
│ (Vector similarity + payload filtering) │
│ • 1536-dim embedding generation │
│ • Qdrant hybrid search │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Stage 4: LLM Verification │
│ (GPT-4o-mini semantic filtering) │
│ • Verifies results match intent │
│ • Returns top 5 verified products │
└─────────────────────────────────────────┘When to use: Natural language queries with implicit filters like "laptops except Apple" or "running shoes between $50 and $150".
Both search modes return a unified response structure:
{
"meta": {
"count": 5,
"take": 10,
"skip": 0,
"q": "nike shoes under 100",
"filteringStatus": "AI_FILTERED",
"appliedFilters": {
"brand": ["Nike"],
"priceRange": {
"min": null,
"max": 100,
"currency": "UAH"
}
}
},
"items": [
{
"externalId": "prod_123",
"url": "https://store.com/products/nike-air-max",
"title": "Nike Air Max 90",
"description": "Classic sneakers with Air cushioning",
"brand": "Nike",
"category": "Shoes",
"price": {
"amount": 89.99,
"currency": "USD"
},
"mainImage": "https://cdn.store.com/images/nike-air-max.jpg",
"attributes": [
{ "name": "Color", "value": "White" },
{ "name": "Material", "value": "Leather" }
],
"variants": [
{ "sku": "NAM90-W-42", "title": "White / 42", "price": null, "available": true }
],
"keywords": ["nike", "sneakers", "running", "air max"],
"score": 0.892
}
]
}| Status | Meaning | Search Mode |
|---|---|---|
AI_FILTERED | Agentic workflow extracted and applied filters | Agentic |
VECTOR_ONLY | Pure vector similarity search | Simple |
FALLBACK | Degraded mode (e.g., embedding API failure) | Either |
MCIP exposes tools through the Model Context Protocol (MCP). Your agent discovers available tools, then calls them as needed:
// 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;
private baseUrl: string;
constructor(baseUrl: string = "http://localhost:8080") {
this.client = new Client(
{ name: "shopping-agent", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
this.baseUrl = baseUrl;
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)}`;
}
}Add methods for both search modes:
// Continuing MCIPClient class
// Mode 1: Simple Vector Search (fast, ~300ms)
async searchSimple(
query: string,
options?: { take?: number; skip?: number }
): Promise<SearchResponse> {
const params = new URLSearchParams({
q: query,
take: String(options?.take ?? 10),
skip: String(options?.skip ?? 0),
});
const response = await fetch(`${this.baseUrl}/search?${params}`);
return response.json();
}
// Mode 2: Agentic Search (intelligent, ~500ms)
async searchAgentic(
query: string,
options?: { take?: number; skip?: number }
): Promise<SearchResponse> {
const params = new URLSearchParams({
q: query,
take: String(options?.take ?? 10),
skip: String(options?.skip ?? 0),
});
const response = await fetch(`${this.baseUrl}/hard-filtering/search?${params}`);
return response.json();
}For production-grade implementations, we've built the MCIP Playground Agent — a real product search and recommendation engine that demonstrates how to build sophisticated AI agents on top of MCIP. It bridges the gap between unstructured web data and precise, mathematically-grounded decision making by combining LLMs with Multi-Criteria Optimization.
This is a working NestJS application that you can clone, deploy, and extend. It's intentionally straightforward — a practical example of how any team can implement an intelligent commerce agent using MCIP as the product discovery backbone.
Source: The Playground Agent ships with its own Technical Documentation covering architecture, modules, and deployment.
The Playground Agent was designed with five core objectives:
The Playground Agent follows a modular NestJS architecture, emphasizing separation of concerns and dependency injection. It has seven core modules:
| Module | Responsibility |
|---|---|
| Decision Making Layer | Central orchestrator managing the end-to-end search flow |
| Query Summary | Analyzes current and previous queries to refine search intent |
| Search Engine | Queries multiple registered stores (via MCIP's Store Directory) in parallel and aggregates results into a unified format |
| Data Transformer | Uses AI to parse raw product data into structured feature matrices |
| Data Preprocessing | Filters irrelevant parameters and prepares numerical data for analysis |
| MCOP Service | Implements Multi-Criteria Optimization algorithms |
| AI Client | Wrapper around LLM services for reasoning and transformation tasks |
The Playground Agent follows a modular NestJS architecture, emphasizing separation of concerns and dependency injection:
┌─────────────────────────────────────────────────────────────────┐
│ MCIP Playground Agent │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ Query Summary │ │ Search Engine │ │ Data Transformer│ │
│ │ (Refines │───▶│ (Calls MCIP │───▶│ (Structures │ │
│ │ intent) │ │ via Store │ │ features) │ │
│ │ │ │ Directory) │ │ │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ Session History│ │ MCOP Service │◀───│ Data │ │
│ │ │ │ (Pareto set, │ │ Preprocessing │ │
│ │ │ │ normalize, │ │ (Numerical │ │
│ │ │ │ score) │ │ conversion) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ AI Client │ │
│ │ (Generates │ │
│ │ reasoning) │ │
│ └────────────────┘ │
│ │
│ ┌────────────────────────────────┐ │
│ │ Decision Making Layer │ │
│ │ (Orchestrates entire flow) │ │
│ └────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ Axios (HTTP)
▼
┌──────────────────┐
│ MCIP Server │
│ (Vector Search │
│ + Agentic) │
└──────────────────┘When a user submits a query, the Query Summary module examines it alongside previous queries in the session. It uses an LLM to generate a "refined query" that better captures the user's ultimate goal:
// Query refinement example
interface QueryContext {
currentQuery: string;
previousQueries: string[];
sessionPreferences: Record<string, any>;
}
async function refineQuery(context: QueryContext): Promise<string> {
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "system",
content: `Analyze the user's current query in context of their session history.
Generate a refined search query that captures their true intent.
Previous queries: ${context.previousQueries.join(", ")}
Current query: ${context.currentQuery}`
}
],
response_format: { type: "json_object" },
});
return JSON.parse(response.choices[0].message.content!).refinedQuery;
}
// Example:
// "Find a lightweight laptop" + "under $1000"
// → "High-performance lightweight laptop under $1000"The Search Engine queries multiple registered stores defined in the Store Directory through MCIP's endpoints in parallel. It fetches raw product data and validates it against the UnifiedProduct schema using Zod, ensuring consistency across different sources:
import { z } from "zod";
const UnifiedProductSchema = z.object({
externalId: z.string(),
url: z.string(),
title: z.string().min(3),
description: z.string(),
brand: z.string().optional(),
category: z.string().optional(),
price: z.object({
amount: z.number(),
currency: z.enum(["UAH", "USD", "EUR"]),
}),
mainImage: z.string(),
attributes: z.array(z.object({
name: z.string(),
value: z.union([z.string(), z.number(), z.boolean()]),
})),
variants: z.array(z.object({
sku: z.string(),
title: z.string(),
price: z.object({ amount: z.number(), currency: z.string() }).nullable(),
available: z.boolean(),
})),
keywords: z.array(z.string()),
score: z.number().optional(),
});
async function aggregateProducts(refinedQuery: string): Promise<UnifiedProduct[]> {
const response = await mcip.searchAgentic(refinedQuery, { take: 20 });
// Validate each product against schema
return response.items
.map(item => {
const result = UnifiedProductSchema.safeParse(item);
return result.success ? result.data : null;
})
.filter(Boolean);
}Raw product descriptions are often messy and unstructured. This stage converts them into a structured, numerical representation that can be mathematically analyzed. It has two parts:
Part A: Data Transformer — The Data Transformer sends product data to an LLM to extract a structured feature matrix:
interface FeatureMatrix {
products: string[]; // Product IDs
features: string[]; // Feature names
values: (number | null)[][]; // Feature values per product
weights: number[]; // Feature importance (0-1)
directions: ("max" | "min")[]; // Higher or lower is better
}
async function transformToFeatureMatrix(
products: UnifiedProduct[],
userIntent: string
): Promise<FeatureMatrix> {
const extractionResponse = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "system",
content: `Extract comparable features from these products.
User is looking for: ${userIntent}
For each feature, determine:
- The numerical value for each product
- Whether higher ("max") or lower ("min") is better
- The weight (0-1) based on relevance to user intent`
},
{
role: "user",
content: JSON.stringify(products.map(p => ({
id: p.externalId,
title: p.title,
description: p.description,
price: p.price.amount,
attributes: p.attributes,
})))
}
],
response_format: { type: "json_object" },
});
return JSON.parse(extractionResponse.choices[0].message.content!);
}Part B: Data Preprocessing — The Data Preprocessing stage then:
interface PreprocessedData {
matrix: number[][]; // Numerical feature matrix
featureNames: string[]; // Feature names (filtered)
directions: ("max" | "min")[];// Benefit directions
weights: number[]; // Importance weights (sum to 1)
}
function preprocessFeatureMatrix(
rawMatrix: FeatureMatrix,
userIntent: string
): PreprocessedData {
// Filter irrelevant features based on user intent
const relevantIndices = rawMatrix.features
.map((f, i) => ({ feature: f, index: i, weight: rawMatrix.weights[i] }))
.filter(f => f.weight > 0.1) // Threshold for relevance
.map(f => f.index);
// Extract only relevant columns
const filteredMatrix = rawMatrix.values.map(row =>
relevantIndices.map(i => row[i] ?? 0) // Replace null with 0
);
// Normalize weights to sum to 1
const relevantWeights = relevantIndices.map(i => rawMatrix.weights[i]);
const weightSum = relevantWeights.reduce((a, b) => a + b, 0);
const normalizedWeights = relevantWeights.map(w => w / weightSum);
return {
matrix: filteredMatrix,
featureNames: relevantIndices.map(i => rawMatrix.features[i]),
directions: relevantIndices.map(i => rawMatrix.directions[i]),
weights: normalizedWeights,
};
}This is the mathematical heart of the Playground Agent. Instead of a "black box" AI ranking, MCOP uses a three-step mathematical process:
// MCOP Service Implementation
interface MCOPResult {
rankedProducts: string[]; // Product IDs in order
scores: number[]; // Objective scores
paretoSet: string[]; // Non-dominated products
}
class MCOPService {
/**
* Step 1: Pareto Set Computation
* Filters out "dominated" products — those worse than another in ALL criteria
*/
computeParetoSet(matrix: FeatureMatrix): string[] {
const dominated = new Set<number>();
for (let i = 0; i < matrix.products.length; i++) {
for (let j = 0; j < matrix.products.length; j++) {
if (i === j) continue;
if (this.dominates(matrix, j, i)) {
dominated.add(i);
break;
}
}
}
return matrix.products.filter((_, i) => !dominated.has(i));
}
private dominates(matrix: FeatureMatrix, a: number, b: number): boolean {
let dominated = true;
let strictlyBetter = false;
for (let f = 0; f < matrix.features.length; f++) {
const valA = matrix.values[a][f];
const valB = matrix.values[b][f];
if (valA === null || valB === null) continue;
const direction = matrix.directions[f];
const aIsBetter = direction === "max" ? valA > valB : valA < valB;
const aIsEqual = valA === valB;
if (!aIsBetter && !aIsEqual) dominated = false;
if (aIsBetter) strictlyBetter = true;
}
return dominated && strictlyBetter;
}
/**
* Step 2: Normalization
* Scales different units (price in $, weight in kg) to common range (0-1)
*/
normalize(matrix: FeatureMatrix): number[][] {
const normalized: number[][] = [];
for (let f = 0; f < matrix.features.length; f++) {
const values = matrix.values.map(row => row[f]).filter(v => v !== null) as number[];
const min = Math.min(...values);
const max = Math.max(...values);
const range = max - min || 1;
const normalizedColumn = matrix.values.map(row => {
const val = row[f];
if (val === null) return 0;
const norm = (val - min) / range;
// Invert if lower is better
return matrix.directions[f] === "min" ? 1 - norm : norm;
});
normalized.push(normalizedColumn);
}
return normalized;
}
/**
* Step 3: Weighted Scoring
* Applies user-specific weights to calculate final objective score
*/
calculateScores(normalizedMatrix: number[][], weights: number[]): number[] {
const productCount = normalizedMatrix[0]?.length || 0;
const scores: number[] = [];
for (let p = 0; p < productCount; p++) {
let score = 0;
for (let f = 0; f < normalizedMatrix.length; f++) {
score += normalizedMatrix[f][p] * weights[f];
}
scores.push(score);
}
return scores;
}
/**
* Main ranking function
*/
rank(matrix: FeatureMatrix): MCOPResult {
const paretoSet = this.computeParetoSet(matrix);
const normalized = this.normalize(matrix);
const scores = this.calculateScores(normalized, matrix.weights);
const ranked = matrix.products
.map((id, i) => ({ id, score: scores[i] }))
.sort((a, b) => b.score - a.score);
return {
rankedProducts: ranked.map(r => r.id),
scores: ranked.map(r => r.score),
paretoSet,
};
}
}Finally, the top-ranked products are sent back to the LLM. The AI Client generates a natural language explanation (Reasoning) that justifies the ranking, highlighting the pros and cons of the top choices in relation to the user's query. The result is returned as a structured JSON object or as a real-time stream of status updates and results via RxJS observables:
interface RecommendationResult {
products: UnifiedProduct[];
reasoning: string;
paretoSet: string[];
scores: Record<string, number>;
}
async function generateReasoning(
rankedProducts: UnifiedProduct[],
userQuery: string,
mcopResult: MCOPResult
): Promise<RecommendationResult> {
const top5 = rankedProducts.slice(0, 5);
const reasoningResponse = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "system",
content: `Generate a clear, helpful explanation for why these products
are recommended for the user's query: "${userQuery}"
Highlight:
- Why the #1 pick is best
- Trade-offs between top options
- Which products are in the Pareto-optimal set (non-dominated)
Be concise but informative.`
},
{
role: "user",
content: JSON.stringify({
products: top5.map(p => ({
id: p.externalId,
title: p.title,
price: p.price,
score: mcopResult.scores[mcopResult.rankedProducts.indexOf(p.externalId)],
})),
paretoSet: mcopResult.paretoSet,
})
}
],
});
return {
products: top5,
reasoning: reasoningResponse.choices[0].message.content!,
paretoSet: mcopResult.paretoSet,
scores: Object.fromEntries(
mcopResult.rankedProducts.map((id, i) => [id, mcopResult.scores[i]])
),
};
}Traditional AI-only ranking is a "black box" — you can't explain why Product A ranked higher than Product B. MCOP provides:
| Aspect | AI-Only Ranking | MCOP Ranking |
|---|---|---|
| Transparency | Opaque | Clear mathematical formula |
| Reproducibility | May vary | Deterministic given same inputs |
| Bias | LLM biases affect ranking | Pure mathematical optimization |
| Explainability | "AI thinks this is better" | "Score: 0.87 based on price (0.3), weight (0.2)..." |
| User Control | Limited | Adjustable weights per criterion |
// src/decision-making.service.ts
class DecisionMakingService {
constructor(
private querySummary: QuerySummaryService,
private searchEngine: SearchEngineService,
private dataTransformer: DataTransformerService,
private mcopService: MCOPService,
private aiClient: AIClientService,
) {}
async processSearch(
query: string,
sessionHistory: string[]
): Promise<RecommendationResult> {
// Stage 1: Refine query based on session context
const refinedQuery = await this.querySummary.refine({
currentQuery: query,
previousQueries: sessionHistory,
});
// Stage 2: Aggregate products from MCIP
const products = await this.searchEngine.search(refinedQuery);
// Stage 3: Transform to feature matrix
const featureMatrix = await this.dataTransformer.transform(
products,
refinedQuery
);
// Stage 4: MCOP ranking
const mcopResult = this.mcopService.rank(featureMatrix);
// Stage 5: Generate reasoning
const rankedProducts = mcopResult.rankedProducts
.map(id => products.find(p => p.externalId === id)!)
.filter(Boolean);
return this.aiClient.generateReasoning(
rankedProducts,
refinedQuery,
mcopResult
);
}
}The Playground Agent is containerized using Docker and can be deployed using the provided docker-compose.yml or the build-and-push.sh script for cloud environments. It connects to your MCIP server instance via HTTP (Axios), so make sure MCIP is running and accessible before starting the agent.
Without sessions, every interaction starts fresh. Sessions maintain state across conversations — search history, viewed products, and user preferences. The Playground Agent uses session history for query refinement.
// 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: [],
lastSearchMode: null,
preferences: {},
},
});
return sessionId;
}
recordSearch(
sessionId: string,
query: string,
mode: "VECTOR_ONLY" | "AI_FILTERED",
results: UnifiedProduct[]
): void {
const session = this.getSession(sessionId);
if (session) {
session.context.searchHistory.push({
query,
mode,
resultCount: results.length,
timestamp: new Date(),
});
session.context.viewedProducts = results.map(p => p.externalId);
session.context.lastSearchMode = mode;
}
}
// Get previous queries for refinement
getPreviousQueries(sessionId: string, limit: number = 5): string[] {
const session = this.getSession(sessionId);
if (!session) return [];
return session.context.searchHistory
.slice(-limit)
.map(s => s.query);
}
}MCIP delivers consistent performance at scale:
| Metric | P50 | P95 | P99 |
|---|---|---|---|
| Embedding Generation | 145ms | 189ms | 212ms |
| Vector Search | 238ms | 287ms | 342ms |
| Total (Simple) | ~300ms | ~400ms | ~450ms |
| Total (Agentic) | 421ms | 498ms | 587ms |
Throughput: 1,247 requests/second
Graceful degradation keeps conversations flowing:
async function safeSearch(
mcip: MCIPClient,
query: string,
useAgentic: boolean
): Promise<{ results: SearchResponse | null; hadError: boolean; fallbackUsed: boolean }> {
try {
const results = useAgentic
? await mcip.searchAgentic(query)
: await mcip.searchSimple(query);
const fallbackUsed = results.meta.filteringStatus === "FALLBACK";
return { results, hadError: false, fallbackUsed };
} catch (error) {
console.error(`Search error:`, error);
// Try fallback to simple search if agentic failed
if (useAgentic) {
try {
const fallbackResults = await mcip.searchSimple(query);
return { results: fallbackResults, hadError: true, fallbackUsed: true };
} catch {
// Both failed
}
}
return { results: null, hadError: true, fallbackUsed: false };
}
}| 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 |
# openapi.yaml for GPT Action
openapi: 3.0.0
info:
title: MCIP Shopping Assistant API
version: 1.0.0
servers:
- url: https://your-mcip-server.com
paths:
/search:
get:
operationId: simpleSearch
summary: Fast vector search for products
parameters:
- name: q
in: query
required: true
schema:
type: string
/hard-filtering/search:
get:
operationId: agenticSearch
summary: Intelligent search with automatic filter extraction
parameters:
- name: q
in: query
required: true
schema:
type: stringconst tools = [
{
name: "simple_search",
description: "Fast vector search for straightforward product queries.",
input_schema: {
type: "object",
properties: {
query: { type: "string", description: "Product search query" },
limit: { type: "number", default: 10 },
},
required: ["query"],
},
},
{
name: "agentic_search",
description: "Intelligent search with automatic brand, category, and price filter extraction.",
input_schema: {
type: "object",
properties: {
query: { type: "string", description: "Natural language search query with filters" },
limit: { type: "number", default: 10 },
},
required: ["query"],
},
},
];Cause: Brand validation failed — the extracted brand doesn't exist in the catalog
Solution: Check meta.appliedFilters to see what was extracted.
Cause: Feature weights not properly calibrated to user intent
Solution: Review the weight extraction in Data Transformer stage.
Cause: Multiple LLM calls in Playground Agent pipeline
Solution: Cache query refinements, parallelize where possible.
Cause: MCIP encountered an error and degraded gracefully
Solution: Check MCIP server logs. Results are still valid but may be less precise.