Compression d'images
Compression intelligente d'images via le service distant nx-mcp-server.
Configuration
Créez un fichier .mcp.json à la racine du projet :
{
"mcpServers": {
"nx-mcp-compress": {
"type": "url",
"url": "https://mcp.api-inference.modelscope.net/4378d43d3e7d4c/mcp",
"env": {
"NX_API_KEY": "Votre clé API"
}
}
}
}
Ordre de recherche : racine du projet → répertoire personnel. Extrait url → MCP_URL, env.NX_API_KEY → API_KEY.
La compétence se connecte directement au point de terminaison MCP, sans nécessiter de redémarrage de Claude Code.
Pas de clé API ? Contactez WeChat
zhjian_2026pour en obtenir une.
Processus de compression
Étape 1 : Vérifier la configuration
cat .mcp.json 2>/dev/null || cat ~/.mcp.json 2>/dev/null
- Trouvé → enregistrer
urletNX_API_KEY, passer à l'étape 2 - Introuvable → demander à l'utilisateur s'il possède déjà une clé API :
- A une clé : aider à créer
~/.mcp.json(valable pour une installation globale ou par projet) - Pas de clé : informer de contacter WeChat
zhjian_2026pour en obtenir une
- A une clé : aider à créer
⚠️ Ne pas poursuivre les étapes suivantes si la configuration est manquante.
Étape 2 : Exécuter la compression (un seul appel Bash, 100% mémoire, zéro fichier)
Remplacez PIC_DIR, MCP_URL, API_KEY, QUALITY, puis le heredoc alimente node directement via stdin :
NODE_PATH=$(npm root -g) node << 'COMPRESSEOF'
const fs=require('fs'),path=require('path');
let PIC_DIR='<chemin absolu du répertoire d\'images>';
const SINGLE_FILE='<chemin de l\'image unique, vide pour compresser tout le répertoire>';
const MCP_URL='<url lue depuis .mcp.json>';
const API_KEY='<NX_API_KEY lue depuis .mcp.json>';
const QUALITY=90;
(async()=>{
const exts=['.png','.jpg','.jpeg','.webp','.bmp','.tga'];
let imgs;
if(SINGLE_FILE){imgs=[path.basename(SINGLE_FILE)];PIC_DIR=path.dirname(SINGLE_FILE)}
else{imgs=fs.readdirSync(PIC_DIR).filter(f=>exts.includes(path.extname(f).toLowerCase())).sort()}
const origTotal=imgs.reduce((s,f)=>s+fs.statSync(path.join(PIC_DIR,f)).size,0);
console.log(`Total ${imgs.length} images, ${(origTotal/1024).toFixed(0)} Ko`);
const maxSize=5*1024*1024;
const records=[];let skip=0;
for(let i=0;i<imgs.length;i++){const f=imgs[i],fp=path.join(PIC_DIR,f),osz=fs.statSync(fp).size;
if(osz>maxSize){records.push({name:f,origKb:osz,compKb:0,compUrl:null,ratio:null,error:'Dépasse la limite de 5 Mo'});skip++;console.log(` [${i+1}/${imgs.length}] ${f} ${(osz/1024).toFixed(0)} Ko ⚠️ Dépasse 5 Mo, ignoré`);continue}
const buf=fs.readFileSync(fp);const ext=path.extname(f).toLowerCase().slice(1);
const mime=ext==='png'?'image/png':ext==='webp'?'image/webp':'image/jpeg';
records.push({name:f,origKb:osz,compKb:0,compUrl:null,ratio:null,dataUrl:'data:'+mime+';base64,'+buf.toString('base64')});}
console.log(`Compressibles : ${records.length-skip}, Ignorés : ${skip}`);
console.time('Compression');
const H={'Content-Type':'application/json','Accept':'application/json, text/event-stream'};
const r1=await fetch(MCP_URL,{method:'POST',headers:H,body:JSON.stringify({jsonrpc:'2.0',id:'1',method:'initialize',params:{protocolVersion:'2025-06-18',capabilities:{},clientInfo:{name:'cc',version:'1'}}})});
H['Mcp-Session-Id']=r1.headers.get('Mcp-Session-Id');console.log('MCP : init');
await fetch(MCP_URL,{method:'POST',headers:H,body:JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'})});console.log('MCP : notifié');
// Pool de concurrence : toujours LIMIT requêtes en vol, une terminée on en lance une autre
const LIMIT=5;
let totalSaved=0,pass=0,fail=0,done=0;
const pending=records.filter(r=>!r.error);
async function compressOne(r,idx){
try{
const r3=await fetch(MCP_URL,{method:'POST',headers:H,body:JSON.stringify({jsonrpc:'2.0',id:'3',method:'tools/call',params:{name:'nx_compress',arguments:{files:[r.dataUrl],quality:QUALITY,apiKey:API_KEY}}})});
const raw=await r3.json();
if(!raw.result){r.error='MCP : '+(raw.error?.message||'unknown');fail++}
else{
const inner=JSON.parse(raw.result.content[0].text);
if(inner.error){r.error=inner.code+': '+inner.error;fail++}
else{const it=inner.items[0];
if(it.error){r.error=it.error;fail++}
else{r.compKb=it.compressedSize/1024;r.ratio=it.ratio;r.compUrl=it.compressedUrl;totalSaved+=it.originalSize-it.compressedSize;pass++;console.log(` [${idx+1}/${imgs.length}] ${r.name} ${(r.origKb/1024).toFixed(0)} Ko → ${(r.compKb).toFixed(0)} Ko (${r.ratio})`)}
}
}
}catch(e){r.error=e.message;fail++}
done++;
}
// Fenêtre glissante : toujours LIMIT opérations concurrentes
let i=0;
const running=new Set();
while(i<pending.length||running.size>0){
while(running.size<LIMIT&&i<pending.length){
const p=compressOne(pending[i],i).then(()=>running.delete(p));
running.add(p);i++;
}
if(running.size>0) await Promise.race(running);
}console.timeEnd('Compression');
console.log('\n'+'='.repeat(100));
console.log(`${'Fichier'.padEnd(40)} ${'Original'.padStart(8)} ${'Compressé'.padStart(8)} ${'Taux'.padStart(8)} ${'Résultat'.padStart(6)}`);
console.log('-'.repeat(100));
for(const r of records){const oszS=(r.origKb/1024).toFixed(1)+' Ko';
if(r.compUrl){const cszS=(r.compKb).toFixed(1)+' Ko';console.log(`${r.name.padEnd(40)} ${oszS.padStart(8)} ${cszS.padStart(8)} ${(r.ratio||'?').padStart(8)} ${'✅'.padStart(6)} ${r.compUrl}`)}
else{console.log(`${r.name.padEnd(40)} ${oszS.padStart(8)} ${'—'.padStart(8)} ${'—'.padStart(8)} ${'❌'.padStart(6)}`)}}
console.log(`\n📊 ${records.length} images | ✅ ${pass} réussies | ❌ ${fail} échecs | Économisé ${(totalSaved/1024).toFixed(0)} Ko`);
})();
COMPRESSEOF
Paramètre de compression : quality par défaut 90 (1-100). Pas besoin de sharp en local, l'envoi est direct au serveur pour compression.
Étape 3 : Télécharger en local (si l'utilisateur le souhaite)
Ne relancez pas la compression ! Les URLs CDN des résultats sont déjà affichées dans la console. Utilisez curl pour télécharger :
curl -o <répertoire de sortie>/<nom du fichier> "<URL CDN>"
- ✅ Réussi : L'URL CDN compressée peut être utilisée directement
- ⚠️ Plus de 5 Mo : Le fichier dépasse la limite serveur, doit être traité manuellement
- ❌ Échec : Réessayez une fois
Référence rapide des champs
| Champ | Type | Description |
|---|---|---|
originalSize | number | Taille originale (octets) |
compressedSize | number | Taille après compression (octets) |
ratio | string | Taux de compression, ex. "71,5%" |
compressedUrl | string | Adresse CDN |
Référence rapide des erreurs courantes
| Erreur observée | Cause | Action correcte |
|---|---|---|
MISSING_API_KEY | apiKey non fourni | Obligatoire |
API_AUTH_FAILED | Clé API invalide | Vérifier la clé dans .mcp.json |
-32602 Invalid request parameters | notifications/initialized non envoyé | Suivre les trois étapes : init → notified → call |
406 Not Acceptable | En-tête Accept manquant | Déclarer à la fois application/json et text/event-stream |
| Fichier > 5 Mo ignoré | Limite serveur | Informer l'utilisateur de traiter manuellement |
Interdictions
- ❌ Ne pas compresser localement (compression intelligente côté serveur, sharp local est superflu)
- ❌ Ne pas envoyer le paramètre
output(il n'existe pas) - ❌ Ne pas omettre le paramètre
apiKey - ❌ Ne pas sauter l'étape
notifications/initialized - ❌ Ne pas écrire de fichiers temporaires (heredoc direct sur stdin, exécution en mémoire)
- ❌ Ne pas diviser le flux en plusieurs appels Bash (un seul
node << 'COMPRESSEOF'suffit) - ❌ Ne pas utiliser de chemins avec antislash ; utilisez toujours des slashes (
d:/path)