Communitygithub.com

xiaowu89/skill-compress

サーバーサイド画像圧縮スキル。CDN出力に対応し、バッチ処理と単一ファイルモードをサポートします。

skill-compress とは?

このスキルは、`nx-mcp-server` を介してリモートの画像圧縮サービスを呼び出し、画像をサーバーサイドで圧縮します。Node.jsベースで、圧縮処理はすべてメモリ上で行われ、一時ファイルを作成しません。バッチモード(フォルダ内の全画像を圧縮)と単一ファイルモードの両方をサポートしており、品質パラメータ(1〜100、デフォルトは90)を指定可能です。圧縮結果はCDN経由でURLとして返されるため、ダウンロードやホスティングに便利です。また、ファイルサイズが5MBを超える画像は自動的にスキップされるため、サーバーの制限に注意が必要です。設定は `.mcp.json` ファイルに `url` と `apiKey` を記述するだけで、Claude Codeを再起動せずにすぐに使用できます。

対応✓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": "あなたの API Key"
      }
    }
  }
}

検索順序:プロジェクトルート → ユーザーホームディレクトリ。url を MCP_URL、env.NX_API_KEY を API_KEY として抽出します。

このスキルは MCP エンドポイントに直接接続するため、Claude Codeを再起動する必要はありません。

API Key をお持ちでないですか? 微信 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 を持っていない場合:微信 zhjian_2026 まで連絡するよう伝える

⚠️ 設定がない場合は以降の手順に進まないでください。

手順 2:圧縮の実行(1回の Bash 呼び出し、メモリのみ、ファイルは残さない)

PIC_DIR、MCP_URL、API_KEY、QUALITY を適宜置き換えた上で、heredoc を使って標準入力から node に直接渡します:

NODE_PATH=$(npm root -g) node << 'COMPRESSEOF'
const fs=require('fs'),path=require('path');
let PIC_DIR='<対象画像ディレクトリの絶対パス>';
const SINGLE_FILE='<単一画像ファイルのパス。空の場合はディレクトリ全体を圧縮>';
const MCP_URL='<.mcp.json から読み取った url>';
const API_KEY='<.mcp.json から読み取った NX_API_KEY>';
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 <出力ディレクトリ>/<ファイル名> "<CDNアドレス>"
  • ✅ 成功:圧縮された CDN URL をそのまま使用できます
  • ⚠️ 5MB超え:ファイルがサーバー制限を超えているため、手動で処理する必要があります
  • ❌ 失敗:1回だけ再試行してください

戻り値一覧

フィールド型説明
originalSizenumber元のサイズ(バイト)
compressedSizenumber圧縮後のサイズ(バイト)
ratiostring圧縮率(例:"71.5%")
compressedUrlstringCDNアドレス

よくあるエラー一覧

エラー内容原因正しい対処法
MISSING_API_KEYapiKey が渡されていない必ず渡す
API_AUTH_FAILEDAPI Key が無効.mcp.json 内の Key を確認
-32602 Invalid request parametersnotifications/initialized が送信されていないinit → notified → call の3ステップを必ず実行
406 Not AcceptableAccept ヘッダーが不足application/json と text/event-stream の両方を宣言
ファイルが5MB超えでスキップサーバー制限ユーザーに手動処理を促す

禁止事項

  • ❌ ローカルで圧縮しない(サーバーサイドでスマートに圧縮。ローカルの sharp は不要)
  • ❌ output パラメータを渡さない(存在しない)
  • ❌ apiKey パラメータを省略しない
  • ❌ notifications/initialized ステップを省略しない
  • ❌ 一時ファイルを作成しない(heredoc で標準入力に直接渡し、メモリのみで実行)
  • ❌ 処理を複数の Bash 呼び出しに分割しない(1回の node << 'COMPRESSEOF' で完結)
  • ❌ バックスラッシュのパスを使用せず、必ずスラッシュを使用する(d:/path)

関連スキル