asmy-mcp-server
Officialby asmydev
README.md
# asmy-mcp-server
Serveur MCP pour Asmy Digital Store. Expose le catalogue produits, les codes promo
et les stats de la boutique sous forme d'outils MCP, consommables par un agent
(Junior.so, Claude Desktop, ou n'importe quel client MCP).
Backend de données : Firestore. Deux transports : stdio et HTTP (SSE + Streamable HTTP).
## Prérequis
- Node >= 18 (Bun recommandé, les scripts npm l'utilisent)
- Un projet Firebase avec Firestore activé
- Une clé de compte de service Firebase
## Installation
```bash
bun install # ou npm install
cp .env.example .env
```
## Configuration
### Firebase
Le serveur résout les credentials dans cet ordre (`firebase-config.js`) :
1. `FIREBASE_SERVICE_ACCOUNT` — le JSON complet, sur une ligne ou en base64
2. `FIREBASE_SERVICE_ACCOUNT_FILE` — chemin vers le fichier
3. `./firebase-service-account.json` — fallback, à côté de `index.js`
Si `FIREBASE_SERVICE_ACCOUNT` est présent mais invalide, il y a fallback sur le
fichier avec un warning sur stderr, pas un crash.
En local : le fichier. En prod : la variable d'env (base64 évite les galères
d'échappement sur `private_key`).
Le fichier `firebase-service-account.json` est déjà dans `.gitignore`. Il n'y a
aucune raison de l'en sortir.
### Variables d'environnement
| Variable | Défaut | Rôle |
| --- | --- | --- |
| `PORT` | `3000` | Port HTTP |
| `HOST` | `0.0.0.0` | Interface d'écoute |
| `ALLOWED_HOSTS` | — | Hôtes autorisés, séparés par des virgules. Requis quand on bind sur `0.0.0.0` |
| `FIREBASE_SERVICE_ACCOUNT` | — | JSON du service account (une ligne ou base64) |
| `FIREBASE_SERVICE_ACCOUNT_FILE` | — | Chemin alternatif vers le JSON |
| `MCP_STDIO` | — | `1` force le transport stdio |
| `OPENAI_API_KEY` | — | Clients de test uniquement |
| `OPENAI_MODEL` | `gpt-4o-mini` | Clients de test uniquement |
| `MCP_SERVER_URL` | — | `test-mcp-url.js` uniquement |
`localhost`, `127.0.0.1`, `[::1]` et `RAILWAY_PUBLIC_DOMAIN` (injecté par Railway)
sont ajoutés automatiquement à la liste des hôtes autorisés.
Note : `env.js` est un parseur `.env` maison, sans dépendance. Il gère les valeurs
JSON multi-lignes (accolades) — utile pour coller un service account brut dans le
fichier. Il ne gère pas grand-chose d'autre : pas d'interpolation, pas
d'échappements exotiques.
## Lancer
```bash
bun start # HTTP sur $PORT
bun run dev # HTTP + watch
bun run start:stdio # transport stdio
```
Le serveur choisit stdio si `--stdio` est passé en argument ou si `MCP_STDIO=1`.
Sinon, HTTP.
## Endpoints HTTP
| Route | Méthode | Description |
| --- | --- | --- |
| `/` | GET | Health check, renvoie le nom du serveur et la liste des endpoints |
| `/mcp` | ALL | Streamable HTTP (spec 2025-11-25), sessions via header `mcp-session-id` |
| `/sse` | GET | SSE legacy (spec 2024-11-05) — c'est ce que consomme Junior.so |
| `/messages` | POST | Canal retour du transport SSE, session via `?sessionId=` |
Les deux transports coexistent. Une session ouverte en SSE ne peut pas être
reprise sur `/mcp` (retour `-32000`).
Les sessions sont stockées en mémoire dans un objet local. Conséquence : **une
seule instance**. Pas de scaling horizontal sans sticky sessions ou store partagé.
## Outils exposés
16 outils, tous définis dans `index.js`.
**Produits** (collection `products`)
| Outil | Description |
| --- | --- |
| `list_products` | Catalogue avec statut, prix, stock |
| `get_product` | Détail par slug ou id Firestore |
| `create_product` | Création. `autoFindImage=true` déclenche la recherche d'icône |
| `update_product` | Mise à jour partielle |
| `set_product_status` | `active` / `draft` / `featured` |
| `duplicate_product` | Copie avec nouveau nom optionnel |
| `delete_product` | Suppression définitive |
| `find_product_image` | Recherche d'icône seule, sans écrire en base |
**Promotions** (collection `promotions`)
| Outil | Description |
| --- | --- |
| `list_promos` | Tous les codes |
| `get_promo` | Recherche par code |
| `create_promo` | Refuse les doublons de code |
| `update_promo` | Code, réduction ou expiration |
| `delete_promo` | Suppression |
**Divers**
| Outil | Description |
| --- | --- |
| `get_stats` | Lit `stats/overview` |
| `update_stats` | Ajoute une entrée et recalcule les totaux du mois |
| `get_current_date` | Date courante en `Africa/Douala` (UTC+1) |
`get_current_date` existe parce que les LLM se trompent de date et génèrent des
`expiresAt` dans le passé. Un agent qui crée un code promo doit l'appeler d'abord.
### Conventions
- Les codes promo sont normalisés en majuscules, `expiresAt` au format `YYYY-MM-DD`,
`discount` entre 1 et 100.
- Le slug produit est dérivé du nom (NFD, accents retirés, non-alphanumériques
remplacés par des tirets).
- Chaque produit renvoyé porte une `url` construite sur
`https://asmystore.shop/abonnements/{slug}`.
- `update_stats` push dans `history`, puis recalcule `monthTotal` et `monthOrders`
en filtrant sur le préfixe `YYYY-MM`. Écrire deux fois la même entrée la compte
deux fois — il n'y a pas de déduplication.
## Recherche d'images
`product-image.js` résout une icône produit en deux passes :
1. Clearbit (`logo.clearbit.com/{domaine}`), avec une table de correspondance
marque → domaine et un fallback `{slug}.com` / `{slug}.io`
2. DuckDuckGo Images
Chaque candidat est validé par un `HEAD` (puis un `GET` avec `Range` si le HEAD
ne renvoie rien d'exploitable) pour vérifier que le `content-type` est bien une
image.
Deux réserves à garder en tête : l'endpoint `i.js` de DuckDuckGo n'est pas une API
publique — il faut d'abord scraper un token `vqd`, et ça peut casser sans préavis.
Et les logos récupérés appartiennent aux marques concernées ; à toi de vérifier ce
que tu as le droit d'afficher sur la boutique.
En cas d'échec, l'outil renvoie une erreur explicite et le produit est créé sans
icône.
## Tests
Deux clients de test, tous les deux branchés sur OpenAI pour faire du tool calling
en langage naturel. `OPENAI_API_KEY` requise.
```bash
bun run test:mcp # spawn le serveur en stdio
bun run test:mcp "Liste les produits actifs"
MCP_SERVER_URL=https://xxx.up.railway.app bun run test:mcp:url
bun test-mcp-url.js https://xxx.up.railway.app "Quelle est la date ?"
```
Sans argument, les deux entrent en mode REPL. Max 10 tours d'appels d'outils.
## Déploiement (Railway)
1. Push le repo (privé) sur GitHub
2. Railway → New Project → Deploy from GitHub
3. Variables : `FIREBASE_SERVICE_ACCOUNT` (base64), `PORT` si besoin
4. `RAILWAY_PUBLIC_DOMAIN` est injecté automatiquement et ajouté aux hôtes
autorisés — sinon, renseigner `ALLOWED_HOSTS` à la main
Le serveur gère `SIGINT` / `SIGTERM` : les transports ouverts sont fermés avant
l'exit.
### Connexion depuis Junior.so
Dashboard → agent → Integrations → Create → Custom → MCP, puis l'URL SSE :
```
https://<ton-domaine>.up.railway.app/sse
```
Junior parle la spec 2024-11-05, donc `/sse` et pas `/mcp`. Les 16 outils sont
listés automatiquement après connexion.
## Migration des promos
`migrate-promos.js` est un script one-shot pour rapatrier les codes promo du
`localStorage` du front vers Firestore.
1. Sur asmystore.shop, console dev : `localStorage.getItem("asmy-admin-promos")`
2. Coller le JSON dans la constante `PROMOS_JSON` du script
3. `bun migrate-promos.js`
Attention : ce script lit `firebase-service-account.json` en dur, il ne passe pas
par `firebase-config.js`. Les variables d'env ne fonctionnent pas ici, il faut le
fichier. Le script est destiné à être lancé une fois puis oublié.
Côté front, penser à basculer les lectures/écritures promo de `localStorage` vers
la collection `promotions`, sinon les deux sources divergent.
## Dépannage
**`Firebase non configuré`** au démarrage — ni `FIREBASE_SERVICE_ACCOUNT`, ni
`FIREBASE_SERVICE_ACCOUNT_FILE`, ni le fichier par défaut n'ont été trouvés.
**`JSON Firebase incomplet`** — le JSON est parsé mais il manque `type:
"service_account"` ou `private_key`. Typiquement un copier-coller tronqué ou un
base64 mal terminé.
**Requêtes rejetées en prod alors que ça marche en local** — protection contre le
DNS rebinding. Le host n'est pas dans `allowedHosts`. Renseigner `ALLOWED_HOSTS`.
**`Session not found` sur `/messages`** — la session SSE a expiré ou le serveur a
redémarré. Les sessions sont en mémoire, un redéploiement les efface toutes.
**Le client ne voit aucun outil** — vérifier qu'on tape bien `/sse` (spec legacy)
et pas `/mcp`, et que `GET /` répond.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues