이미지 콘텐츠 심사
nx-mcp-audit MCP 서비스를 사용하여 이미지의 성인, 정치, 폭력 콘텐츠를 감사합니다.
설정
프로젝트 루트 디렉토리에 .mcp.json 파일을 생성합니다:
{
"mcpServers": {
"nx-mcp-audit": {
"type": "url",
"url": "https://mcp.api-inference.modelscope.net/da16b3f65bdb4e/mcp",
"env": {
"NX_API_KEY": "여기에 API Key를 입력"
}
}
}
}
검색 순서: 프로젝트 루트 → 사용자 홈 디렉토리. url은 MCP_URL로, env.NX_API_KEY는 API_KEY로 추출됩니다.
스킬이 MCP 엔드포인트에 직접 연결되므로 Claude Code를 재시작할 필요가 없습니다.
API Key가 없으신가요? WeChat
zhjian_2026으로 연락하여 키를 받으세요.
심사流程
단계 1: 설정 확인 (설정이 없으면 즉시 중단)
cat .mcp.json 2>/dev/null || cat ~/.mcp.json 2>/dev/null
- 찾음 →
url과NX_API_KEY를 기록하고 단계 2로 진행 - 못 찾음 → 사용자에게 API Key가 있는지 질문:
- 키 있음: 사용자의
~/.mcp.json(홈 디렉토리) 생성 도움, 전역 및 프로젝트 설치에 모두 적용 - 키 없음: WeChat
zhjian_2026으로 연락하여 키를 받도록 안내하고, 사용자가 키를 받은 후 설정하도록 기다림
- 키 있음: 사용자의
⚠️ 설정이 없으면 sharp를 설치하거나 이후 단계를 진행하지 말고, 먼저 설정을 해결한 후에 진행하세요.
단계 2: sharp 설치 + 심사 실행 (한 번의 Bash 호출, 순수 메모리, 파일 생성 없음)
PIC_DIR, MCP_URL, API_KEY를 교체한 후, heredoc을 통해 node에 stdin으로 전달하며 파일을 전혀 쓰지 않습니다:
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='<대상 이미지 디렉토리 절대 경로>';
const SINGLE_FILE='<단일 이미지 경로, 비워두면 전체 디렉토리 심사>';
const MCP_URL='<.mcp.json에서 읽어온 url>';
const API_KEY='<.mcp.json에서 읽어온 NX_API_KEY>';
(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(`총 ${imgs.length}장, 합계 ${(origTotal/1024).toFixed(0)}KB`);
const records=[],data_urls=[];let compTotal=0;console.time('압축');
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('압축');
console.log(`payload: ${(compTotal/1024).toFixed(0)}KB`);
console.time('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('MCP 심사');
let itemIdx=0,pass=0,block=0,fail=0;
console.log('\n'+'='.repeat(85));console.log(`${'파일명'.padEnd(50)} ${'원본'.padStart(6)} ${'결과'.padStart(6)} ${'엔진'.padStart(6)} 설명`);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='⚠️ 오류';fail++}else if(safe===true){st='✅ 통과';pass++}else if(safe===false){st='⛔ 위반';block++}else{st='❌ 실패';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)} ${'❌ 실패'.padStart(6)} ${'—'.padStart(6)} 압축 실패`);fail++}}
const total=records.length;console.log(`\n📊 ${total}장 | ✅ ${pass} 통과 | ⛔ ${block} 위반 | ⚠️ ${fail} 오류/실패 | v${items[0]?.auditVersion||'?'}`);
})();
AUDITEOF
권장사항
- ✅ 통과: 정상 사용 가능
- ⛔ 위반: 삭제 또는 수동 재심사 권장
- ❌ 실패: 한 번 재시도
반환 필드 참고
| 필드 | 타입 | 설명 |
|---|---|---|
safe | boolean | true=통과, false=위반 |
source | string | 심사 엔진 (wechat / api) |
errcode | number | 에러 코드, 0=정상 |
errmsg | string | 에러 메시지, "ok"=정상 |
message | string | 심사 결과 설명 |
auditVersion | string | 서비스 버전 번호 |
summary | object | {total, pass, block, error} |
일반 오류 참고
| 오류 현상 | 원인 | 올바른 조치 |
|---|---|---|
-32602 Invalid request parameters | notifications/initialized를 보내지 않음 | 반드시 3단계: init → notified → call |
406 Not Acceptable | Accept 헤더 누락 | application/json과 text/event-stream 함께 선언 |
"urls 또는 files 매개변수를 제공하세요" | 존재하지 않는 imagePath 사용 | files(base64 dataUrl 배열)로 변경 |
"invalid api key" | API Key 오류 또는 만료 (errcode=-1) | .mcp.json의 Key 확인 |
"API Key가 설정되지 않았습니다" | apiKey를 전달하지 않음 | 반드시 전달해야 함, 도구 정의에서 선택 사항이라고 명시되어 있어도 필수 |
413 Payload Too Large | 페이로드 크기 초과 | 압축 후 보통 200KB 미만이므로 트리거되지 않음; 압축하지 않은 대용량 이미지는 분할 전송 |
Cannot find module 'sharp' | 전역 설치되지 않았거나 NODE_PATH 누락 | NODE_PATH=$(npm root -g) node ... |
금지사항
- ❌ 압축을 건너뛰지 마십시오 (이미지가 매우 작더라도)
- ❌
imagePath매개변수를 사용하지 마십시오 (존재하지 않음) - ❌
apiKey매개변수를 생략하지 마십시오 - ❌
notifications/initialized단계를 생략하지 마십시오 - ❌ 임시 파일을 쓰지 마십시오 (heredoc으로 직접 stdin에 전달, 순수 메모리 실행)
- ❌ 프로세스를 여러 번의 Bash 호출로 나누지 마십시오 (한 번의
node << 'AUDITEOF'로 처리) - ❌ 백슬래시 경로(
d:\path)를 사용하지 마십시오. bash heredoc에서 이스케이프 처리되므로 항상 슬래시(d:/path)를 사용하십시오.