Moderación de Contenido de Imágenes
Audita imágenes en busca de contenido adulto, político y violento utilizando el servicio MCP nx-mcp-audit.
Configuración
Crea el archivo .mcp.json en la raíz del proyecto:
{
"mcpServers": {
"nx-mcp-audit": {
"type": "url",
"url": "https://mcp.api-inference.modelscope.net/da16b3f65bdb4e/mcp",
"env": {
"NX_API_KEY": "Tu Clave API"
}
}
}
}
Orden de búsqueda: raíz del proyecto → directorio personal del usuario. Extrae url → MCP_URL, env.NX_API_KEY → API_KEY.
La habilidad se conecta directamente al punto MCP, no es necesario reiniciar Claude Code.
¿No tienes clave API? Contacta al WeChat
zhjian_2026para obtener una.
Flujo de Auditoría
Paso 1: Verificar configuración (detener si falta)
cat .mcp.json 2>/dev/null || cat ~/.mcp.json 2>/dev/null
- Encontrado → registrar
urlyNX_API_KEY, continuar al paso 2 - No encontrado → preguntar al usuario si ya tiene una clave API:
- Tiene clave: ayudar al usuario a crear
~/.mcp.json(directorio personal), válido tanto para instalación global como local - No tiene clave: indicar que contacte al WeChat
zhjian_2026para obtenerla, esperar a que el usuario la consiga y vuelva a configurar
- Tiene clave: ayudar al usuario a crear
⚠️ Si falta la configuración, no instalar sharp ni continuar con los pasos siguientes; primero resolver la configuración.
Paso 2: Instalar sharp + ejecutar auditoría (una sola llamada Bash, en memoria, sin archivos)
Reemplaza PIC_DIR, MCP_URL, API_KEY y luego el heredoc se alimenta directamente a node mediante stdin, sin escribir ningún archivo:
NODE_PATH=$(npm root -g) node -e "require('sharp')" 2>/dev/null || npm install -g sharp
NODE_PATH=$(npm root -g) node << 'AUDITEOF'
const fs=require('fs'),path=require('path'),sharp=require('sharp');
let PIC_DIR='<ruta absoluta del directorio de imágenes>';
const SINGLE_FILE='<ruta de una sola imagen, vacío para auditar todo el directorio>';
const MCP_URL='<url leída de .mcp.json>';
const API_KEY='<NX_API_KEY leída de .mcp.json>';
(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} imágenes, ${(origTotal/1024).toFixed(0)}KB`);
const records=[],data_urls=[];let compTotal=0;console.time('Compresión');
for(let i=0;i<imgs.length;i++){const f=imgs[i],fp=path.join(PIC_DIR,f),osz=fs.statSync(fp).size;
try{const buf=await sharp(fp,{limitInputPixels:false}).resize({width:500,height:500,fit:'inside',withoutEnlargement:true}).jpeg({quality:40}).toBuffer();
const url='data:image/jpeg;base64,'+buf.toString('base64');records.push({name:f,origKb:osz,compKb:buf.length,dataUrl:url});
data_urls.push(url);compTotal+=buf.length;console.log(` [${i+1}/${imgs.length}] ${f} ${(osz/1024).toFixed(0)}KB→${(buf.length/1024).toFixed(0)}KB`)}
catch(e){records.push({name:f,origKb:osz,compKb:0,dataUrl:null,error:e.message});console.log(` [${i+1}/${imgs.length}] ${f} ❌ ${e.message}`)}}
console.timeEnd('Compresión');
console.log(`payload: ${(compTotal/1024).toFixed(0)}KB`);
console.time('Auditoría MCP');
const H={'Content-Type':'application/json','Accept':'application/json, text/event-stream','Authorization':`Bearer ${API_KEY}`};
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'}}})});
const sid=r1.headers.get('Mcp-Session-Id');H['Mcp-Session-Id']=sid;console.log(`MCP: init→${sid}`);
await fetch(MCP_URL,{method:'POST',headers:H,body:JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'})});console.log('MCP: notified→202');
let items=[];
if(data_urls.length>0){const r3=await fetch(MCP_URL,{method:'POST',headers:H,body:JSON.stringify({jsonrpc:'2.0',id:'3',method:'tools/call',params:{name:'nx_img_audit',arguments:{files:data_urls,apiKey:API_KEY}}})});
const raw=await r3.json();const inner=JSON.parse(raw.result.content[0].text);items=inner.items}
console.timeEnd('Auditoría MCP');
let itemIdx=0,pass=0,block=0,fail=0;
console.log('\n'+'='.repeat(85));console.log(`${'Archivo'.padEnd(50)} ${'Original'.padStart(6)} ${'Resultado'.padStart(6)} ${'Motor'.padStart(6)} Descripción`);console.log('-'.repeat(85));
for(const r of records){const oszS=(r.origKb/1024).toFixed(0)+'KB';
if(r.dataUrl){const item=items[itemIdx++],safe=item.safe,src=item.source||'-';const ec=item.errcode,em=item.errmsg||item.error||'';let st;
if(em==='invalid api key'||em==='未配置 API Key'){st='⚠️ Error';fail++}else if(safe===true){st='✅ Aprobado';pass++}else if(safe===false){st='⛔ Bloqueado';block++}else{st='❌ Fallo';fail++}
console.log(`${r.name.padEnd(50)} ${oszS.padStart(6)} ${st.padStart(6)} ${src.padStart(6)} ${em.padStart(8)}`)}
else{console.log(`${r.name.padEnd(50)} ${oszS.padStart(6)} ${'❌ Fallo'.padStart(6)} ${'—'.padStart(6)} Error de compresión`);fail++}}
const total=records.length;console.log(`\n📊 ${total} imágenes | ✅ ${pass} aprobadas | ⛔ ${block} bloqueadas | ⚠️ ${fail} errores/fallos | v${items[0]?.auditVersion||'?'}`);
})();
AUDITEOF
Sugerencias
- ✅ Aprobado: se puede usar normalmente
- ⛔ Bloqueado: se recomienda eliminar o revisar manualmente
- ❌ Fallo: reintentar una vez
Referencia rápida de campos
| Campo | Tipo | Descripción |
|---|---|---|
safe | boolean | true=aprobado, false=bloqueado |
source | string | Motor de auditoría (wechat / api) |
errcode | number | Código de error, 0=normal |
errmsg | string | Mensaje de error, "ok"=normal |
message | string | Descripción del resultado de la auditoría |
auditVersion | string | Versión del servicio |
summary | object | {total, pass, block, error} |
Consulta rápida de errores comunes
| Error | Causa | Solución correcta |
|---|---|---|
-32602 Invalid request parameters | No se envió notifications/initialized | Deben ser tres pasos: init → notified → call |
406 Not Acceptable | Falta la cabecera Accept | Declarar tanto application/json como text/event-stream |
"请提供 urls 或 files 参数" | Se usó imagePath que no existe | Usar files (array de base64 dataUrl) |
"invalid api key" | Clave API incorrecta o caducada (errcode=-1) | Verificar la clave en .mcp.json |
"未配置 API Key" | No se envió apiKey | Obligatorio, la definición de la herramienta indica que es opcional, pero es engañoso |
413 Payload Too Large | Payload excede el límite | Tras comprimir suele ser < 200KB, no se activa; imágenes grandes sin comprimir requieren procesamiento por lotes |
Cannot find module 'sharp' | No está instalado globalmente o falta NODE_PATH | NODE_PATH=$(npm root -g) node ... |
Prohibiciones
- ❌ No omitir la compresión (incluso si las imágenes son pequeñas)
- ❌ No usar el parámetro
imagePath(no existe) - ❌ No omitir el parámetro
apiKey - ❌ No omitir el paso
notifications/initialized - ❌ No escribir archivos temporales (heredoc se alimenta directamente a stdin, ejecución en memoria pura)
- ❌ No dividir el flujo en múltiples llamadas Bash (hacerlo todo en una sola
node << 'AUDITEOF') - ❌ No usar rutas con barra invertida (
d:\ruta), el heredoc de Bash las escaparía; usar siempre barras normales (d:/ruta)