import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import sharp from "sharp";
import fs from "fs/promises";
import path from "path";
// --- サーバーの初期設定 ---
// 1. コマンドライン引数から画像フォルダのパスを取得
const imageFolderPath = process.argv[2];
if (!imageFolderPath) {
console.error("エラー: 画像フォルダのパスを引数で指定してください。");
process.exit(1);
}
// 絶対パスに変換
const absoluteImageFolderPath = path.resolve(imageFolderPath);
// フォルダの存在を確認
try {
const stats = await fs.stat(absoluteImageFolderPath);
if (!stats.isDirectory()) {
console.error(`エラー: 指定されたパスはフォルダではありません: ${absoluteImageFolderPath}`);
process.exit(1);
}
} catch (error) {
console.error(`エラー: 指定されたパスが見つかりません: ${absoluteImageFolderPath}`);
process.exit(1);
}
// 2. McpServerのインスタンスを作成
const server = new McpServer({
name: "image-editor-mcp",
version: "1.0.0"
});
// --- ヘルパー関数 ---
// ファイルパスを検証し、安全なフルパスを返す
const resolveAndValidatePath = (fileName: string): string => {
const fullPath = path.resolve(absoluteImageFolderPath, fileName);
// ディレクトリトラバーサル攻撃を防ぐため、パスが許可されたフォルダ内にあるか確認
if (!fullPath.startsWith(absoluteImageFolderPath)) {
throw new Error("許可されていないファイルパスへのアクセスです。");
}
return fullPath;
};
// --- ツールの登録 ---
// 1. 画像の明るさを調整するツール
server.registerTool(
"adjustBrightness",
{
title: "画像の明るさ調整",
description: "画像の明るさを調整します。",
inputSchema: {
fileName: z.string().describe("編集する画像ファイル名 (例: photo.jpg)"),
brightness: z.number().min(0.1).max(2.0).describe("明るさのレベル (1.0が通常。1.0より大きいと明るく、小さいと暗くなる)")
}
},
async ({ fileName, brightness }) => {
try {
const inputPath = resolveAndValidatePath(fileName);
const parsedPath = path.parse(inputPath);
const outputPath = path.join(parsedPath.dir, `${parsedPath.name}-brightened${parsedPath.ext}`);
await sharp(inputPath)
.modulate({ brightness })
.toFile(outputPath);
return { content: [{ type: "text", text: `画像の明るさを調整し、${outputPath} に保存しました。` }] };
} catch (error: any) {
return { content: [{ type: "text", text: `エラーが発生しました: ${error.message}` }], isError: true };
}
}
);
// 2. 画像をトリミングするツール
server.registerTool(
"cropImage",
{
title: "画像のトリミング",
description: "指定された範囲で画像をトリミングします。",
inputSchema: {
fileName: z.string().describe("編集する画像ファイル名"),
left: z.number().int().min(0).describe("トリミング領域の左上のX座標"),
top: z.number().int().min(0).describe("トリミング領域の左上のY座標"),
width: z.number().int().min(1).describe("トリミング領域の幅"),
height: z.number().int().min(1).describe("トリミング領域の高さ")
}
},
async ({ fileName, left, top, width, height }) => {
try {
const inputPath = resolveAndValidatePath(fileName);
const parsedPath = path.parse(inputPath);
const outputPath = path.join(parsedPath.dir, `${parsedPath.name}-cropped${parsedPath.ext}`);
await sharp(inputPath)
.extract({ left, top, width, height })
.toFile(outputPath);
return { content: [{ type: "text", text: `画像をトリミングし、${outputPath} に保存しました。` }] };
} catch (error: any) {
return { content: [{ type: "text", text: `エラーが発生しました: ${error.message}` }], isError: true };
}
}
);
// 3. 画像を軽量化(圧縮)するツール
server.registerTool(
"compressImage",
{
title: "画像の軽量化(圧縮)",
description: "画像の品質を調整してファイルサイズを小さくします。",
inputSchema: {
fileName: z.string().describe("編集する画像ファイル名"),
quality: z.number().int().min(1).max(100).describe("画像の品質 (1から100の整数)")
}
},
async ({ fileName, quality }) => {
try {
const inputPath = resolveAndValidatePath(fileName);
const parsedPath = path.parse(inputPath);
const outputPath = path.join(parsedPath.dir, `${parsedPath.name}-compressed${parsedPath.ext}`);
const ext = parsedPath.ext.toLowerCase();
let sharpInstance = sharp(inputPath);
// ファイル形式に応じて圧縮オプションを設定
if (ext === '.jpeg' || ext === '.jpg') {
sharpInstance = sharpInstance.jpeg({ quality });
} else if (ext === '.png') {
sharpInstance = sharpInstance.png({ quality });
} else if (ext === '.webp') {
sharpInstance = sharpInstance.webp({ quality });
} else {
return { content: [{ type: "text", text: `サポートされていない画像形式です: ${ext}` }], isError: true };
}
await sharpInstance.toFile(outputPath);
return { content: [{ type: "text", text: `画像を圧縮し、${outputPath} に保存しました。` }] };
} catch (error: any) {
return { content: [{ type: "text", text: `エラーが発生しました: ${error.message}` }], isError: true };
}
}
);
// --- サーバーの起動 ---
async function main() {
console.error(`画像編集サーバーを起動中... 対象フォルダ: ${absoluteImageFolderPath}`);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("サーバーが接続を待機しています...");
}
main().catch(err => {
console.error("サーバーの起動に失敗しました:", err);
process.exit(1);
});