MCIP's three-layer architecture cleanly separates concerns: Layer 1 (Presentation & Protocol) handles MCP tools and HTTP controllers, Layer 2 (Application Services) provides two search modes — simple vector search and LangGraph-powered agentic search with 4-stage intelligent filtering, Layer 3 (Domain & Infrastructure) manages Qdrant vector storage, OpenAI embeddings, platform adapters, and BullMQ async ingestion.
MCIP's three-layer architecture isn't just a technical choice — it's a strategic design that enables machine customers to interact with commerce platforms without knowing their specifics. Like the OSI model revolutionized networking, our architecture revolutionizes machine commerce by cleanly separating protocol, business logic, and platform integration.
The architecture is designed around a modular protocol vision: each commerce capability (product search, cart management, checkout, order tracking) is implemented as a separate module that plugs into the same three layers. Product discovery is the first module — but the layers themselves are protocol-agnostic and ready for expansion.
The beauty of this separation is evolution without disruption. We can add new commerce modules (cart, checkout) without touching adapters, improve search intelligence without changing the protocol layer, or connect new e-commerce platforms without modifying core logic.
Layer 1 is MCIP's front door — where AI agents connect via MCP and HTTP clients interact via REST endpoints. This layer handles request routing, validation, and session management.
| Component | Technology | Purpose |
|---|---|---|
| Search Controller | NestJS HTTP Controller | Simple vector search endpoint (GET /search) |
| Hard Filtering Controller | NestJS HTTP Controller | Agentic search endpoint (GET /hard-filtering/search) |
| MCP Tools | @rekog/mcp-nest 1.8.4 + @modelcontextprotocol/sdk 1.25.2 | AI agent protocol integration |
| Admin Controller | NestJS HTTP Controller | Management endpoints (sync, index rebuild) |
| Session Manager | UUID + Redis (24hr TTL) | Agent session isolation |
| Schema Validation | Zod 3.25.76 | Type-safe request validation |
MCIP exposes two complementary search paths through Layer 1, each routing to a different Application Service in Layer 2:
1. Simple Vector Search — GET /search
2. Agentic Hard-Filtered Search — GET /hard-filtering/search
Both endpoints share the same query parameters: q (query), take (limit), skip (offset).
Here's how tools are defined using the actual codebase pattern:
// Using @rekog/mcp-nest for MCP tool definition
import { Tool } from '@rekog/mcp-nest';
import { z } from 'zod';
// Zod schema for type-safe validation
const SearchProductSchema = z.object({
query: z.string().min(1).describe('Natural language search query'),
take: z.number().int().positive().optional().default(10),
skip: z.number().int().nonnegative().optional().default(0),
});
@Injectable()
export class McpToolsService {
constructor(
@Inject(SEARCH_SERVICE)
private readonly searchService: SearchService,
) {}
@Tool({
name: 'search_product',
description: 'Search products using natural language with AI-powered filter extraction',
parameters: SearchProductSchema,
})
async searchProduct(params: z.infer<typeof SearchProductSchema>) {
return this.searchService.search(params);
}
}Layer 1 currently exposes one production-ready tool for product discovery. As MCIP evolves into a full commerce protocol, additional tools will be added as separate modules:
{
"tools": [
{
"name": "search_product",
"description": "Search products using natural language with AI-powered filter extraction",
"parameters": {
"query": "string (required) - Natural language search",
"take": "number (optional, default: 10) - Results limit",
"skip": "number (optional, default: 0) - Pagination offset"
}
}
]
}Layer 2 is where MCIP's business logic lives. It provides two search modes and orchestrates all service coordination. The key innovation is the LangGraph-powered agentic search — a 4-stage state machine that transforms natural language into precise, filtered results.
| Component | Technology | Purpose |
|---|---|---|
| Search Service | NestJS Service | Simple vector search (fast, no LLM calls) |
| Hard Filtering Service | LangGraph 1.0.15 | Agentic 4-stage search workflow |
| Feature Extraction | LangChain 1.1.15 + GPT-4o-mini | AI filter extraction with Zod structured output |
| Ingestion Service | NestJS Service | Product sync coordination (triggers BullMQ jobs) |
| Admin Service | NestJS Service | System management (sync, index rebuild) |
MCIP provides two complementary search approaches. Choosing between them depends on query complexity and latency requirements:
Mode 1: Simple Vector Search (SearchService)
Query → Embed (OpenAI) → Vector Search (Qdrant) → ResultsfilteringStatus: "VECTOR_ONLY"Mode 2: Agentic Hard-Filtered Search (HardFilteringService + LangGraph)
Query → LangGraph 4-Stage Pipeline → Verified ResultsfilteringStatus: "AI_FILTERED"This is MCIP's core differentiator for product discovery — a 4-stage LangGraph state machine:
Stage 1 — Parallel Filter Extraction (via GPT-4o-mini)
Runs three LLM calls in parallel using LangGraph:
Uses Zod schemas for type-safe structured output parsing.
Stage 2 — Brand Validation
Queries Qdrant facet search for available brands in the catalog. Validates extracted brands against actual store inventory. If the brand doesn't exist in the store, returns empty results immediately.
Stage 3 — Hybrid Search (Vector + Payload Filtering)
Generates a 1536-dimensional query embedding via OpenAI text-embedding-3-small, then executes hybrid search in Qdrant combining vector similarity (cosine distance) with exact payload filtering (brand, category, price range).
Stage 4 — LLM Verification
Passes search results through GPT-4o-mini for semantic verification — confirming results actually match the user's intent. Returns top 5 verified products with metadata.
The Feature Extraction service uses LangChain with Zod for structured AI output:
// Feature extraction using LangChain structured output
import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';
const ExtractedFiltersSchema = z.object({
brand: z.array(z.string()).optional().describe('Brand names mentioned'),
excludedBrand: z.array(z.string()).optional().describe('Brands to exclude'),
priceRange: z.object({
min: z.number().optional(),
max: z.number().optional(),
currency: z.string().default('USD'),
}).optional(),
category: z.string().optional(),
cleanedQuery: z.string().describe('Query without filter terms'),
});
@Injectable()
export class FeatureExtractionService {
private model: ChatOpenAI;
constructor() {
this.model = new ChatOpenAI({
modelName: 'gpt-4o-mini',
temperature: 0,
});
}
async extractFilters(query: string) {
const structuredModel = this.model.withStructuredOutput(ExtractedFiltersSchema);
return structuredModel.invoke([
{
role: 'system',
content: 'Extract shopping filters from the query. Identify brands, price limits, and categories.',
},
{ role: 'user', content: query },
]);
}
}| Natural Language Query | Extracted Filters | Search Mode |
|---|---|---|
| "Nike shoes under $100" | brand: ["Nike"], priceMax: 100 | Agentic (AI_FILTERED) |
| "laptops except Apple" | category: "laptops", excludedBrand: ["Apple"] | Agentic (AI_FILTERED) |
| "gaming headphones $50-150" | category: "gaming headphones", priceMin: 50, priceMax: 150 | Agentic (AI_FILTERED) |
| "organic coffee beans" | cleanedQuery: "organic coffee beans" | Either (VECTOR_ONLY or AI_FILTERED) |
Converts queries to 1536-dimensional vectors using OpenAI:
@Injectable()
export class VectorizationService {
private openai: OpenAI;
constructor(private config: ConfigService) {
this.openai = new OpenAI({
apiKey: config.get('OPENAI_API_KEY'),
});
}
async embedString(text: string): Promise<number[]> {
const response = await this.openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
});
return response.data[0].embedding; // 1536 dimensions
}
async embedProduct(product: UnifiedProduct): Promise<number[]> {
const textToEmbed = [
`Title: ${product.title}`,
`Description: ${product.description}`,
`Keywords: ${product.keywords.join(', ')}`,
product.brand ? `Brand: ${product.brand}` : '',
product.category ? `Category: ${product.category}` : '',
].filter(Boolean).join('\n');
return this.embedString(textToEmbed);
}
}@Injectable()
export class SearchService {
constructor(
@Inject(VECTORIZATION_SERVICE)
private vectorization: VectorizationService,
@Inject(FEATURE_EXTRACTION_SERVICE)
private featureExtraction: FeatureExtractionService,
@Inject(PRODUCT_REPOSITORY)
private repository: ProductRepository,
) {}
async search(params: { query: string; take?: number; skip?: number }) {
// Step 1: Extract filters from natural language
const filters = await this.featureExtraction.extractFilters(params.query);
// Step 2: Generate embedding for cleaned query
const vector = await this.vectorization.embedString(filters.cleanedQuery);
// Step 3: Hybrid search in Qdrant
const results = await this.repository.hybridSearch(
vector,
filters,
params.take ?? 10,
params.skip ?? 0,
);
return {
meta: {
count: results.length,
query: params.query,
filteringStatus: filters.brand || filters.priceRange ? 'AI_FILTERED' : 'RAG_ONLY',
appliedFilters: filters,
},
items: results,
};
}
}| Metric | P50 | P95 | P99 |
|---|---|---|---|
| Embedding Generation | 145ms | 189ms | 212ms |
| Vector Search (Qdrant) | 238ms | 287ms | 342ms |
| Total Response Time | 421ms | 498ms | 587ms |
Throughput: 1,247 requests/second
Layer 3 handles everything below the business logic: product ingestion from e-commerce platforms, vector storage and retrieval, embedding generation, and async queue processing.
| Component | Technology | Purpose |
|---|---|---|
| Product Repository | Qdrant (@qdrant/js-client-rest 1.16.0) | Vector storage, hybrid search, facet queries |
| Vectorization Service | OpenAI (text-embedding-3-small, 1536 dims) | Embedding generation for products and queries |
| VendureMapper | GraphQL Adapter | Vendure platform support |
| CustomAiMapper | GPT-4o powered | AI fallback for any data format |
| BullMQ Processor | BullMQ 5.63.2 + Redis | Async product ingestion queue |
All adapters implement this interface:
// src/modules/ingestion/mapper/product-mapper.interface.ts
import { UnifiedProduct } from '../../../domain/product.schema';
export interface ProductMapper {
map(raw: any): Promise<UnifiedProduct>;
}For future multi-store platform support, a higher-level adapter interface is planned:
// Future: Platform adapter interface for live search
interface IProductService {
searchProducts(query: string, options?: SearchOptions): Promise<Product[]>;
getProduct(id: string): Promise<Product>;
getProductBySlug(slug: string): Promise<Product>;
}// src/domain/product.schema.ts
import { z } from 'zod';
export const PriceSchema = z.object({
amount: z.number().nonnegative(),
currency: z.enum(['UAH', 'USD', 'EUR']),
});
export const AttributeSchema = z.object({
name: z.string(),
value: z.union([z.string(), z.number(), z.boolean()]),
});
export const VariantSchema = z.object({
sku: z.string(),
title: z.string(),
price: PriceSchema.nullable(),
available: z.boolean(),
});
export 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: PriceSchema,
mainImage: z.string(),
attributes: z.array(AttributeSchema),
variants: z.array(VariantSchema),
keywords: z.array(z.string()),
});
export type UnifiedProduct = z.infer<typeof UnifiedProductSchema>;// src/modules/ingestion/mapper/strategies/vendure/vendure.mapper.ts
@Injectable()
export class VendureMapper implements ProductMapper {
constructor(private config: ConfigService) {}
async map(raw: VendureProduct): Promise<UnifiedProduct> {
const basePrice = raw.variants[0]?.priceWithTax ?? 0;
return UnifiedProductSchema.parse({
externalId: raw.id,
url: this.buildProductUrl(raw.slug),
title: raw.name,
description: this.stripHtml(raw.description || ''),
brand: this.extractBrand(raw.facetValues),
category: this.extractCategory(raw.collections),
price: {
amount: basePrice / 100, // Vendure stores cents
currency: raw.variants[0]?.currencyCode || 'USD',
},
mainImage: this.transformImageUrl(raw.featuredAsset?.preview || ''),
attributes: this.mapFacetsToAttributes(raw.facetValues),
variants: this.mapVariants(raw.variants, basePrice),
keywords: this.generateKeywords(raw),
});
}
}| Setting | Value | Purpose |
|---|---|---|
| Vector Size | 1536 | Full precision from text-embedding-3-small |
| Distance Metric | Cosine | Semantic similarity matching |
| Payload Index: price.amount | Float | Price range filtering |
| Payload Index: brand | Keyword | Brand filtering + facet search |
| Payload Index: category | Keyword | Category filtering |
# Sync products from configured SOURCE_URL
curl -X POST http://localhost:8080/admin/sync \
-H "x-admin-api-key: your-secret-key"
# Response
{
"status": "success",
"message": "Queued 150 products from URL",
"count": 150
}The three-layer architecture makes it straightforward to add new commerce capabilities:
add_to_cart, view_cart) with Zod schemasThe existing session infrastructure in Layer 1 and Redis in Layer 3 are already in place.
src/modules/ingestion/mapper/strategies/ProductMapper interfaceIngestionModule factorySOURCE_STRATEGY environment variable@Tool() decoratorExtractedFiltersSchema