Communitygithub.com

xiaowu89/skill-compress

서버 측 이미지 압축 스킬로 CDN 출력 지원 — 배치 처리 및 단일 파일 모드 제공

skill-compress란 무엇인가요?

이 스킬은 nx-mcp-server 원격 압축 서비스를 사용하여 이미지를 스마트하게 압축하는 전문 도구입니다. 사용자는 프로젝트 루트나 홈 디렉토리의 `.mcp.json` 설정 파일만 준비하면 되며, 별도의 로컬 sharp 라이브러리 설치 없이 서버에서 압축이 이루어집니다. 배치 모드에서는 지정된 디렉토리의 모든 이미지를 자동으로 스캔하고, 단일 파일 모드에서는 특정 이미지 하나만 압축합니다. 5MB 미만의 파일만 처리 가능하며, 90 기본 품질로 압축 결과는 CDN URL 형태로 반환됩니다. 전체 과정은 Node.js heredoc으로 한 번의 Bash 호출 안에서 메모리 기반으로 실행되어 임시 파일을 생성하지 않으며, 최대 5개의 동시 요청을 통한 효율적인 처리를 지원합니다. Claude Code, Cursor, Codex 등 MCP 프로토콜을 지원하는 모든 AI 에이전트 플랫폼에서 사용할 수 있습니다.

지원 대상✓Claude Code~Codex CLI✓Cursor
npx skills add xiaowu89/skill-compress

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

이미지 압축

nx-mcp-server 원격 압축 서비스를 통해 이미지를 스마트하게 압축합니다.

설정

프로젝트 루트 디렉토리에 .mcp.json 생성:

{
  "mcpServers": {
    "nx-mcp-compress": {
      "type": "url",
      "url": "https://mcp.api-inference.modelscope.net/4378d43d3e7d4c/mcp",
      "env": {
        "NX_API_KEY": "your 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 보유 여부 질문:
    • Key 있음: 사용자를 위해 ~/.mcp.json 생성 (전역 및 프로젝트 설치 모두 호환)
    • Key 없음: WeChat zhjian_2026으로 문의하도록 안내

⚠️ 설정이 없으면 이후 단계를 진행하지 마세요.

2단계: 압축 실행 (한 번의 Bash 호출, 순수 메모리, 파일 생성 없음)

PIC_DIR, MCP_URL, API_KEY, QUALITY를 교체한 후, heredoc을 통해 직접 stdin으로 node에 전달:

NODE_PATH=$(npm root -g) node << 'COMPRESSEOF'
const fs=require('fs'),path=require('path');
let PIC_DIR='<target image directory absolute path>';
const SINGLE_FILE='<single image path, leave empty to compress entire directory>';
const MCP_URL='<url from .mcp.json>';
const API_KEY='<NX_API_KEY from .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(`총 ${imgs.length}장, ${(origTotal/1024).toFixed(0)}KB`);
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:'5MB 초과'});skip++;console.log(`  [${i+1}/${imgs.length}] ${f} ${(osz/1024).toFixed(0)}KB ⚠️ 5MB 초과로 건너뜀`);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(`압축 가능: ${records.length-skip}장, 건너뜀: ${skip}장`);
console.time('압축');
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: notified');
// 동시성 풀: 항상 LIMIT개의 요청이 실행 중, 완료 시 하나 추가
	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)}KB→${(r.compKb).toFixed(0)}KB (${r.ratio})`)}
	      }
	    }
	  }catch(e){r.error=e.message;fail++}
	  done++;
	}
	
	// 슬라이딩 윈도우: 항상 LIMIT개의 동시 요청 유지
	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('압축');
console.log('\n'+'='.repeat(100));
console.log(`${'파일'.padEnd(40)} ${'원본'.padStart(8)} ${'압축 후'.padStart(8)} ${'압축률'.padStart(8)} ${'결과'.padStart(6)}`);
console.log('-'.repeat(100));
for(const r of records){const oszS=(r.origKb/1024).toFixed(1)+'KB';
if(r.compUrl){const cszS=(r.compKb).toFixed(1)+'KB';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}장 | ✅ ${pass} 성공 | ❌ ${fail} 실패 | 총 ${(totalSaved/1024).toFixed(0)}KB 절약`);

})();
COMPRESSEOF

압축 매개변수: quality 기본 90 (1-100), 로컬 sharp 불필요, 서버 측 압축으로 직접 전송.

3단계: 로컬에 다운로드 (사용자 요청 시)

압축을 다시 실행하지 마세요! 압축 결과의 CDN URL이 콘솔에 출력되었습니다. curl로 직접 다운로드:

curl -o <output directory>/<filename> "<CDN URL>"
  • ✅ 성공: 압축된 CDN URL을 바로 사용 가능
  • ⚠️ 5MB 초과: 서버 제한을 초과한 파일, 수동 처리 필요
  • ❌ 실패: 한 번 재시도

반환 필드 참조

필드타입설명
originalSizenumber원본 크기 (bytes)
compressedSizenumber압축 후 크기 (bytes)
ratiostring압축률, 예: "71.5%"
compressedUrlstringCDN 주소

일반 오류 참조

오류 현상원인올바른 조치
MISSING_API_KEYapiKey 누락반드시 전달
API_AUTH_FAILEDAPI Key 무효.mcp.json의 Key 확인
-32602 Invalid request parametersnotifications/initialized 미전송반드시 세 단계: init → notified → call
406 Not AcceptableAccept 헤더 누락application/json과 text/event-stream 모두 명시
5MB 초과 파일 건너뜀서버 제한사용자에게 수동 처리 안내

금지 사항

  • ❌ 로컬 압축 하지 말 것 (서버 스마트 압축, 로컬 sharp 불필요)
  • ❌ output 매개변수 전달하지 말 것 (존재하지 않음)
  • ❌ apiKey 매개변수 생략하지 말 것
  • ❌ notifications/initialized 단계 생략하지 말 것
  • ❌ 임시 파일 쓰지 말 것 (heredoc 직접 stdin, 순수 메모리 실행)
  • ❌ 과정을 여러 번의 Bash 호출로 나누지 말 것 (한 번의 node << 'COMPRESSEOF'로 처리)
  • ❌ 백슬래시 경로 사용하지 말 것, 항상 슬래시 사용 (d:/path)

관련 스킬