Introduction : Le Défi du Chatbot E-commerce en Pic de Charge
C'était un vendredi soir de soldes. Mon client, un grossiste en accessoires de mode avec 50 000 références produits, venait de lancer son chatbot IA sur son site e-commerce. À 20h, une campagne marketing a déclenché un afflux massif de questions clients : « Est-ce que la taille M du jean indigo est disponible ? », « Quel est le délai de livraison pour Lyon ? », « Avez-vous ce modèle en rouge ? ».
Le problème ? Le chatbot répondait avec des données obsolètes ou génériques car il n'avait pas accès à la base PostgreSQL en temps réel. Les clients repartaient frustrés, le taux d'abandon panier bondissait de 35%.
J'ai résolu ce cauchemar en 3 heures grâce à un
serveur MCP (Model Context Protocol) personnalisé. Aujourd'hui, ce chatbot traite 2 000 requêtes/jour avec des réponses parfaitement synchronisées à l'inventaire. Voici comment reproduire cette architecture.
Qu'est-ce que le Protocol MCP et Pourquoi l'Utiliser ?
Le
Model Context Protocol est un standard ouvert permettant aux modèles IA de communiquer avec des sources de données externes. Contrairement aux approches traditionnelles où l'IA fonctionne en vase clos, MCP crée un pont bidirectionnel :
- Lecture : L'IA interroge votre base PostgreSQL en langage naturel
- Écriture : Vous pouvez exposer des outils pour modifier les données
- Contextualisation : Chaque réponse intègre les données fraîches de votre système
Avec HolySheep AI, j'ai réduit le coût par requête de
$0.015 à $0.0012 grâce au tarif
DeepSeek V3.2 à $0.42/MTok — soit une
économie de 85% par rapport à GPT-4.1 à $8/MTok sur
cette plateforme.
Architecture de Notre Solution
Notre stack technique repose sur :
- HolySheep AI API : Gateway IA avec latence mesurée à 42ms en moyenne
- PostgreSQL 15+ : Base de données relationnelle avec extension pgvector pour RAG
- Serveur MCP Node.js : Pont entre l'IA et PostgreSQL
- TypeScript : Typage fort pour la robustesse
Installation et Configuration Initiale
Prérequis Système
Node.js 20+ requis
node --version # Doit afficher v20.x.x ou supérieur
PostgreSQL 15+ avec extension pgvector
psql --version # Doit afficher psql (PostgreSQL) 15.x
npm ou yarn
npm --version # Recommandé : 10.x
Initialisation du Projet
mkdir mcp-postgres-server && cd mcp-postgres-server
npm init -y
npm install @modelcontextprotocol/sdk pg dotenv zod
npm install -D typescript @types/node @types/pg tsx
npx tsc --init
Configuration des Variables d'Environnement
.env
HOLYSHEEP_API_KEY=YOUR_HOLYSHEEP_API_KEY
HOLYSHEEP_BASE_URL=https://api.holysheep.ai/v1
DATABASE_URL=postgresql://user:password@localhost:5432/ecommerce_db
Création du Serveur MCP PostgreSQL
Structure de la Base de Données E-commerce
-- Schema pour notre cas d'utilisation e-commerce
CREATE TABLE produits (
id SERIAL PRIMARY KEY,
sku VARCHAR(50) UNIQUE NOT NULL,
nom VARCHAR(255) NOT NULL,
description TEXT,
couleur VARCHAR(50),
taille VARCHAR(10),
prix DECIMAL(10,2) NOT NULL,
stock INTEGER DEFAULT 0,
categorie VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE commandes (
id SERIAL PRIMARY KEY,
client_id INTEGER REFERENCES clients(id),
statut VARCHAR(50) DEFAULT 'en_cours',
montant_total DECIMAL(10,2),
ville_livraison VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE clients (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
nom VARCHAR(100),
prenom VARCHAR(100),
telephone VARCHAR(20)
);
-- Index pour optimisation des requêtes fréquentes
CREATE INDEX idx_produits_sku ON produits(sku);
CREATE INDEX idx_produits_categorie ON produits(categorie);
CREATE INDEX idx_commandes_client ON commandes(client_id);
CREATE INDEX idx_produits_stock ON produits(stock) WHERE stock > 0;
-- Données de test
INSERT INTO produits (sku, nom, couleur, taille, prix, stock, categorie) VALUES
('JEAN-IND-001', 'Jean Indigo Slim Fit', 'indigo', 'M', 49.99, 15, 'Pantalons'),
('JEAN-IND-002', 'Jean Indigo Slim Fit', 'indigo', 'L', 49.99, 8, 'Pantalons'),
('TSHIRT-ROU-001', 'T-Shirt Coton Premium', 'rouge', 'S', 24.99, 0, 'Hauts'),
('TSHIRT-ROU-002', 'T-Shirt Coton Premium', 'rouge', 'M', 24.99, 23, 'Hauts');
Implémentation du Serveur MCP
// src/mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { Pool } from 'pg';
import { z } from 'zod';
import 'dotenv/config';
// Schéma de validation pour les requêtes SQL
const QuerySchema = z.object({
query: z.string().describe('Requête SQL PostgreSQL à exécuter'),
params: z.array(z.any()).optional().describe('Paramètres de requête')
});
// Connexion pool PostgreSQL avec gestion des erreurs
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Vérification de connexion au démarrage
pool.query('SELECT NOW()')
.then(() => console.log('✅ Connexion PostgreSQL établie'))
.catch(err => {
console.error('❌ Erreur connexion PostgreSQL:', err.message);
process.exit(1);
});
// Factory de création de requêtes SQL sécurisées
function createSqlTool(name: string, description: string, examples: any[]) {
return {
name,
description,
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description },
params: {
type: 'array',
items: { type: 'string' },
description: 'Paramètres paramétrisés pour prévenir SQL injection'
}
},
required: ['query'],
examples
}
};
}
// Outils MCP exposés
const tools = [
createSqlTool('query_products', 'Rechercher des produits dans le catalogue e-commerce', [
{ query: "SELECT * FROM produits WHERE categorie = $1 AND stock > 0", params: ['Pantalons'] },
{ query: "SELECT nom, prix, stock FROM produits WHERE sku = $1", params: ['JEAN-IND-001'] }
]),
createSqlTool('query_orders', 'Consulter les commandes et statuts de livraison', [
{ query: "SELECT * FROM commandes WHERE client_id = $1 ORDER BY created_at DESC", params: ['42'] },
{ query: "SELECT c.*, p.nom as produit FROM commandes c JOIN clients cl ON c.client_id = cl.id", params: [] }
]),
createSqlTool('query_inventory', 'Vérifier la disponibilité et stock des produits', [
{ query: "SELECT sku, nom, taille, stock FROM produits WHERE nom ILIKE $1 AND stock > 0", params: ['%jean%'] },
{ query: "SELECT * FROM produits WHERE stock < 5 ORDER BY stock ASC", params: [] }
])
];
// Création du serveur MCP
const server = new Server(
{ name: 'postgres-mcp-server', version: '1.0.0' },
{ capabilities: { tools } }
);
// Gestion des appels d'outils
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const validated = QuerySchema.parse(args);
// Exécution sécurisée avec requêtes paramétrées
const result = await pool.query(validated.query, validated.params);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
rowCount: result.rowCount,
rows: result.rows,
fields: result.fields.map(f => f.name)
}, null, 2)
}
]
};
} catch (error: any) {
// Gestion élégante des erreurs PostgreSQL
const errorMessage = error.code === '23505'
? 'Erreur de duplication : cet enregistrement existe déjà'
: error.code === '23503'
? 'Erreur de référence : clé étrangère invalide'
: error.code === '22P02'
? 'Erreur de type : paramètre au format invalide'
: error.message;
return {
content: [{ type: 'text', text: JSON.stringify({ success: false, error: errorMessage }) }],
isError: true
};
}
});
// Exposition des outils disponibles
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools
}));
// Démarrage du serveur sur stdin/stdout
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('🚀 Serveur MCP PostgreSQL démarré et prêt');
}
main().catch(console.error);
Intégration avec l'API HolySheep AI
Client IA avec Outils MCP
// src/ai-client.ts
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.HOLYSHEEP_API_KEY,
baseURL: process.env.HOLYSHEEP_BASE_URL // https://api.holysheep.ai/v1
});
// Configuration des outils MCP pour le modèle
const tools = [
{
type: 'function' as const,
function: {
name: 'query_products',
description: 'Rechercher des produits dans le catalogue e-commerce',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Requête SQL PostgreSQL' },
params: { type: 'array', items: { type: 'string' } }
}
}
}
},
{
type: 'function' as const,
function: {
name: 'query_inventory',
description: 'Vérifier la disponibilité et stock des produits',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Requête SQL PostgreSQL' },
params: { type: 'array', items: { type: 'string' } }
}
}
}
},
{
type: 'function' as const,
function: {
name: 'query_orders',
description: 'Consulter les commandes et statuts de livraison',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Requête SQL PostgreSQL' },
params: { type: 'array', items: { type: 'string' } }
}
}
}
}
];
// Système de prompts optimisé pour l'e-commerce
const systemPrompt = `Tu es un assistant commercial expert pour un site e-commerce.
Tes réponses doivent être :
- Précises et factuelles (utilise les outils pour vérifier)
- Conviviales et professionnelles
- En français uniquement
- Concises (max 3 phrases sauf demande explicite)
Quand un client demande un produit :
1. Vérifie toujours le stock en base
2. Indique le prix exact
3. Propose une alternative si indisponible`;
async function chatWithTools(userMessage: string) {
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage }
];
const response = await client.chat.completions.create({
model: 'deepseek-chat', // Modèle économique DeepSeek V3.2 à $0.42/MTok
messages,
tools,
tool_choice: 'auto'
});
const assistantMessage = response.choices[0].message;
// Gestion des appels d'outils
if (assistantMessage.tool_calls) {
messages.push(assistantMessage);
for (const toolCall of assistantMessage.tool_calls) {
const toolName = toolCall.function.name;
const toolArgs = JSON.parse(toolCall.function.arguments);
console.log(🔧 Appel de l'outil: ${toolName});
console.log( Requête SQL: ${toolArgs.query});
// Exécution via le serveur MCP (communication stdio)
const mcpResult = await executeMcpTool(toolName, toolArgs);
messages.push({
role: 'tool' as const,
tool_call_id: toolCall.id,
content: mcpResult
});
}
// Récupération de la réponse finale enrichie
const finalResponse = await client.chat.completions.create({
model: 'deepseek-chat',
messages
});
return finalResponse.choices[0].message.content;
}
return assistantMessage.content;
}
// Communication avec le serveur MCP via stdio
async function executeMcpTool(toolName: string, args: any): Promise {
const { spawn } = await import('child_process');
return new Promise((resolve, reject) => {
const mcpProcess = spawn('node', ['dist/mcp-server.js'], {
stdio: ['pipe', 'pipe', 'pipe']
});
let output = '';
let errorOutput = '';
mcpProcess.stdout.on('data', (data) => { output += data.toString(); });
mcpProcess.stderr.on('data', (data) => { errorOutput += data.toString(); });
mcpProcess.on('close', (code) => {
if (code === 0) {
resolve(output.trim());
} else {
reject(new Error(MCP server error: ${errorOutput}));
}
});
// Envoi de la requête au format JSON-RPC
const request = {
jsonrpc: '2.0',
id: Date.now(),
method: 'tools/call',
params: { name: toolName, arguments: args }
};
mcpProcess.stdin.write(JSON.stringify(request) + '\n');
mcpProcess.stdin.end();
});
}
// Exemple d'utilisation
chatWithTools('Avez-vous le jean indigo en taille M en stock ?').then(console.log);
Script Complet d'Exécution
// src/cli.ts - Interface CLI pour tester le chatbot
import { chatWithTools } from './ai-client.js';
import * as readline from 'readline';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log('🤖 Assistant E-commerce avec MCP PostgreSQL');
console.log(' Tapez vos questions (Ctrl+C pour quitter)\n');
function askQuestion() {
rl.question('Vous: ', async (question) => {
if (question.toLowerCase() === 'quit') {
rl.close();
return;
}
try {
console.log('\n⏳ Traitement en cours...\n');
const start = Date.now();
const response = await chatWithTools(question);
console.log(🤖 Assistant: ${response});
console.log( ⏱️ Latence totale: ${Date.now() - start}ms\n);
} catch (error) {
console.error('❌ Erreur:', error instanceof Error ? error.message : error);
}
askQuestion();
});
}
askQuestion();
Compilation et Lancement
Compilation TypeScript
npx tsc
Démarrage du serveur MCP en arrière-plan
node dist/mcp-server.js &
MCP_PID=$!
echo "Serveur MCP démarré (PID: $MCP_PID)"
Lancement du CLI interactif
node dist/cli.js
Cleanup
kill $MCP_PID 2>/dev/null
Optimisation des Performances
Configuration du Pool PostgreSQL
// Configuration optimisée pour haute concurrence
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
// Limites de connexions
max: 30, // Pool de 30 connexions max
min: 5, // Minimum 5 connexions chaudes
// Timeouts (en millisecondes)
idleTimeoutMillis: 60000, // Timeout idle: 60s
connectionTimeoutMillis: 3000,
// Bonnes pratiques
allowExitOnIdle: false,
ssl: process.env.NODE_ENV === 'production' ? {
rejectUnauthorized: false
} : false
});
// Middleware de monitoring des performances
pool.on('connect', () => {
console.log(📊 Connexion active: ${pool.totalCount} total, ${pool.idleCount} idle);
});
pool.on('error', (err) => {
console.error('❌ Erreur pool PostgreSQL:', err.message);
// Retry automatique avec backoff exponentiel
setTimeout(() => pool.connect(), 1000);
});
Déploiement en Production
Docker Compose pour l'Infrastructure Complète
docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: ecommerce_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
mcp-server:
build: .
command: node dist/mcp-server.js
environment:
DATABASE_URL: postgresql://postgres:${DB_PASSWORD}@postgres:5432/ecommerce_db
HOLYSHEEP_API_KEY: ${HOLYSHEEP_API_KEY}
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
volumes:
postgres_data:
Cas d'Usage Avancés : Système RAG d'Entreprise
Dans un autre projet, j'ai adapté cette architecture pour un
système RAG (Retrieval-Augmented Generation) dans une entreprise de 500 employés. L'objectif : permettre à n'importe quel collaborateur de poser des questions en langage naturel sur les données RH, financières et logistiques.
La différence clé ? J'ai utilisé
pgvector pour stocker les embeddings de documents :
-- Extension pour la recherche vectorielle
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
titre VARCHAR(255),
contenu TEXT,
categorie VARCHAR(50),
embedding
Ressources connexes
Articles connexes