Company Logo
MCIP
Architecture

Three-Layer Architecture Design

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.

Architectural Philosophy

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.

Three-Layer Architecture Diagram

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: Presentation & Protocol

The Gateway for AI Agents and HTTP Clients

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.

Core Components

ComponentTechnologyPurpose
Search ControllerNestJS HTTP ControllerSimple vector search endpoint (GET /search)
Hard Filtering ControllerNestJS HTTP ControllerAgentic search endpoint (GET /hard-filtering/search)
MCP Tools@rekog/mcp-nest 1.8.4 + @modelcontextprotocol/sdk 1.25.2AI agent protocol integration
Admin ControllerNestJS HTTP ControllerManagement endpoints (sync, index rebuild)
Session ManagerUUID + Redis (24hr TTL)Agent session isolation
Schema ValidationZod 3.25.76Type-safe request validation

Two Entry Points for Search

MCIP exposes two complementary search paths through Layer 1, each routing to a different Application Service in Layer 2:

1. Simple Vector SearchGET /search

  • Direct vector similarity search in Qdrant
  • Fast, low-latency for straightforward queries
  • No LLM calls — pure embedding + vector search
  • Best for: simple keyword-like queries, high-throughput scenarios

2. Agentic Hard-Filtered SearchGET /hard-filtering/search

  • Full LangGraph 4-stage workflow (see Layer 2)
  • Intelligent filter extraction, brand validation, hybrid search, LLM verification
  • Best for: complex natural language queries with implicit filters like "Nike shoes under $100 but not running"

Both endpoints share the same query parameters: q (query), take (limit), skip (offset).

MCP Tool Implementation

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);
  }
}

MCP Tools (Current & Planned)

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:

Currently Available (Product Discovery Module)

{
  "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"
      }
    }
  ]
}

Design Patterns in Layer 1

  • Command Pattern: Each tool is a self-contained command with Zod validation
  • Facade Pattern: Simple interface hiding system complexity
  • Strategy Pattern: Two search controllers route to different services
  • Dependency Injection: NestJS DI for loose coupling

Layer 2: Application Services

The Intelligence and Orchestration Layer

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.

Core Components

ComponentTechnologyPurpose
Search ServiceNestJS ServiceSimple vector search (fast, no LLM calls)
Hard Filtering ServiceLangGraph 1.0.15Agentic 4-stage search workflow
Feature ExtractionLangChain 1.1.15 + GPT-4o-miniAI filter extraction with Zod structured output
Ingestion ServiceNestJS ServiceProduct sync coordination (triggers BullMQ jobs)
Admin ServiceNestJS ServiceSystem management (sync, index rebuild)

Two Search Modes

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) → Results
  • No LLM calls — fastest possible path
  • Returns results ranked by cosine similarity
  • Response includes filteringStatus: "VECTOR_ONLY"

Mode 2: Agentic Hard-Filtered Search (HardFilteringService + LangGraph)

Query → LangGraph 4-Stage Pipeline → Verified Results
  • Full LangGraph state machine with 4 stages
  • Parallel LLM calls for filter extraction
  • Brand validation against actual catalog
  • Hybrid vector + payload filtering in Qdrant
  • LLM verification of final results
  • Response includes filteringStatus: "AI_FILTERED"

The LangGraph Agentic Search Pipeline (4 Stages)

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:

  • Extract categories from the query
  • Extract intended brands from the query
  • Extract price constraints from the query

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.

Feature Extraction Service

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 },
    ]);
  }
}

Filter Extraction Examples

Natural Language QueryExtracted FiltersSearch Mode
"Nike shoes under $100"brand: ["Nike"], priceMax: 100Agentic (AI_FILTERED)
"laptops except Apple"category: "laptops", excludedBrand: ["Apple"]Agentic (AI_FILTERED)
"gaming headphones $50-150"category: "gaming headphones", priceMin: 50, priceMax: 150Agentic (AI_FILTERED)
"organic coffee beans"cleanedQuery: "organic coffee beans"Either (VECTOR_ONLY or AI_FILTERED)

Embedding Service

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);
  }
}

Simple Search Service (Vector Only)

@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,
    };
  }
}

Performance Benchmarks (100 Concurrent Users)

MetricP50P95P99
Embedding Generation145ms189ms212ms
Vector Search (Qdrant)238ms287ms342ms
Total Response Time421ms498ms587ms

Throughput: 1,247 requests/second

Why LangGraph + LangChain?

  • LangGraph: Declarative agentic workflows with parallel execution, conditional routing, and state management — powers the 4-stage search pipeline
  • LangChain: Structured output with Zod schemas ensures type-safe AI responses
  • Parallel Execution: Stage 1 runs three filter extraction calls concurrently
  • Conditional Routing: Brand validation in Stage 2 can short-circuit the pipeline
  • Observability: Built-in tracing for debugging AI behavior

Layer 3: Domain & Infrastructure

Data Persistence, Adapters, and Async Processing

Layer 3 handles everything below the business logic: product ingestion from e-commerce platforms, vector storage and retrieval, embedding generation, and async queue processing.

Core Components

ComponentTechnologyPurpose
Product RepositoryQdrant (@qdrant/js-client-rest 1.16.0)Vector storage, hybrid search, facet queries
Vectorization ServiceOpenAI (text-embedding-3-small, 1536 dims)Embedding generation for products and queries
VendureMapperGraphQL AdapterVendure platform support
CustomAiMapperGPT-4o poweredAI fallback for any data format
BullMQ ProcessorBullMQ 5.63.2 + RedisAsync product ingestion queue

Product Mapper Interface

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>;
}

Unified Product Schema (Zod)

// 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>;

VendureMapper Implementation

// 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),
    });
  }
}

Qdrant Collection Configuration

SettingValuePurpose
Vector Size1536Full precision from text-embedding-3-small
Distance MetricCosineSemantic similarity matching
Payload Index: price.amountFloatPrice range filtering
Payload Index: brandKeywordBrand filtering + facet search
Payload Index: categoryKeywordCategory filtering

Triggering Product Sync

# 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
}

Extension Points

Adding a New Commerce Module (e.g., Cart)

The three-layer architecture makes it straightforward to add new commerce capabilities:

  1. Layer 1: Define new MCP tools (add_to_cart, view_cart) with Zod schemas
  2. Layer 2: Implement CartService with business logic
  3. Layer 3: Add CartRepository (Redis-backed) for persistence

The existing session infrastructure in Layer 1 and Redis in Layer 3 are already in place.

Adding a New Adapter

  1. Create mapper in src/modules/ingestion/mapper/strategies/
  2. Implement ProductMapper interface
  3. Register in IngestionModule factory
  4. Set SOURCE_STRATEGY environment variable

Adding a New MCP Tool

  1. Define Zod schema for parameters
  2. Add method with @Tool() decorator
  3. Implement business logic in Layer 2 service
  4. Tool auto-registered via @rekog/mcp-nest
  1. Add new filter types to ExtractedFiltersSchema
  2. Add new stages to the LangGraph workflow
  3. Create additional Qdrant payload indexes if needed