@dondonudonjp/vertexai-imagen-mcp-server
Version:
[DEPRECATED] MCP Server for Vertex AI Imagen image generation. Imagen endpoints shut down 2026-06-30; migrate to the nanoBanana MCP Server (Gemini image models).
277 lines • 11.5 kB
JavaScript
/**
* 画像処理ユーティリティ
*/
import * as path from 'path';
import * as fs from 'fs/promises';
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
import { resolveInputPath } from './path.js';
/**
* Base64文字列を正規化
* Data URI形式の場合はBase64部分のみを抽出
*
* @param input 入力文字列
* @returns 正規化されたBase64文字列
*/
export function normalizeBase64String(input) {
const trimmed = input.trim();
const commaIndex = trimmed.indexOf(',');
if (trimmed.startsWith('data:') && commaIndex !== -1) {
return trimmed.slice(commaIndex + 1).replace(/\s/g, '');
}
return trimmed.replace(/\s/g, '');
}
/**
* ファイルパスからMIMEタイプを検出
*
* @param filePath ファイルパス
* @returns MIMEタイプ(不明な場合はundefined)
*/
export function detectMimeTypeFromPath(filePath) {
const ext = path.extname(filePath).toLowerCase();
switch (ext) {
case ".png":
return "image/png";
case ".jpg":
case ".jpeg":
return "image/jpeg";
case ".webp":
return "image/webp";
case ".bmp":
return "image/bmp";
case ".gif":
return "image/gif";
case ".tif":
case ".tiff":
return "image/tiff";
default:
return undefined;
}
}
/**
* 画像ソースを解決(Base64文字列またはファイルパスから)
*
* @param options 解決オプション
* @returns 解決された画像ソース
* @throws 必須パラメータが不足している場合、またはファイルの読み込みに失敗した場合
*/
export async function resolveImageSource({ base64Value, pathValue, label, required, }) {
if (base64Value && typeof base64Value === 'string' && base64Value.trim().length > 0) {
const trimmed = base64Value.trim();
if (trimmed.startsWith('data:')) {
const commaIndex = trimmed.indexOf(',');
if (commaIndex === -1) {
throw new McpError(ErrorCode.InvalidParams, `${label} data URI is malformed`);
}
const header = trimmed.slice(5, commaIndex); // drop 'data:'
const normalized = trimmed.slice(commaIndex + 1).replace(/\s/g, '');
if (!normalized) {
throw new McpError(ErrorCode.InvalidParams, `${label} data URI contains no base64 data`);
}
const mimeType = header.split(';')[0] || undefined;
return {
base64: normalized,
mimeType,
source: "data-uri",
};
}
const normalized = normalizeBase64String(base64Value);
if (!normalized) {
throw new McpError(ErrorCode.InvalidParams, `${label} base64 string is empty`);
}
return {
base64: normalized,
source: "base64",
};
}
if (pathValue && typeof pathValue === 'string' && pathValue.trim().length > 0) {
const fullPath = resolveInputPath(pathValue);
try {
const buffer = await fs.readFile(fullPath);
return {
base64: buffer.toString('base64'),
mimeType: detectMimeTypeFromPath(fullPath),
source: "file",
filePath: fullPath,
};
}
catch (error) {
throw new McpError(ErrorCode.InvalidParams, `${label} file could not be read from path: ${fullPath} (${error instanceof Error ? error.message : String(error)})`);
}
}
if (required) {
throw new McpError(ErrorCode.InvalidParams, `${label} is required as base64 string or file path`);
}
return undefined;
}
/**
* 画像レスポンスを作成(MCPクライアント用)
*
* @param imageBuffer 画像バッファ
* @param mimeType MIMEタイプ
* @param filePath 保存先ファイルパス(省略可)
* @param additionalInfo 追加情報テキスト(省略可)
* @param options レスポンスオプション(省略可)
* @returns MCPレスポンス形式
*/
export function createImageResponse(imageBuffer, mimeType, filePath, additionalInfo, options) {
const base64Data = imageBuffer.toString('base64');
const dataUrl = `data:${mimeType};base64,${base64Data}`;
let responseText = additionalInfo || '';
if (filePath) {
responseText += `\nSaved to: ${filePath}`;
}
responseText += `\nFile size: ${imageBuffer.length} bytes\nMIME type: ${mimeType}`;
// デフォルトでは画像をLLMコンテキストから除外
const excludeFromContext = options?.excludeFromContext ?? true;
// アノテーションの構築
const annotations = options?.annotations || {
audience: excludeFromContext ? ["user"] : ["user", "assistant"],
};
return {
content: [
{
type: "text",
text: responseText
},
{
type: "image",
data: dataUrl,
mimeType: mimeType,
annotations: annotations
}
],
};
}
/**
* URIベースの画像レスポンスを作成(Resources API用)
*
* @param uri file:// URI
* @param mimeType MIMEタイプ
* @param fileSize ファイルサイズ(バイト)
* @param filePath 保存先の表示パス
* @param absoluteFilePath 実際のファイルの絶対パス(サムネイル生成用)
* @param additionalInfo 追加情報テキスト(省略可)
* @param includeThumbnail サムネイルを含めるか(デフォルト: true)
* @returns MCPレスポンス形式
*/
export async function createUriImageResponse(uri, mimeType, fileSize, filePath, absoluteFilePath, additionalInfo, includeThumbnail = true) {
let responseText = additionalInfo || '';
responseText += `\nSaved to: ${filePath}`;
responseText += `\nFile size: ${fileSize} bytes`;
responseText += `\nMIME type: ${mimeType}`;
// Markdownリンク形式でURIを追加
const fileName = path.basename(absoluteFilePath);
responseText += `\n\n📎 **Full-size image:** [${fileName}](${uri})`;
responseText += `\n💡 *You can access the full-resolution image via MCP Resources API*`;
// プレーンテキストでもURIを表示(コピー用)
responseText += `\n\n🔗 Resource URI:\n\`\`\`\n${uri}\n\`\`\``;
const content = [
{
type: "text",
text: responseText
}
];
// サムネイル生成(オプション)
// includeThumbnail パラメータで制御(各ツールで環境変数を考慮済み)
const thumbnailEnabled = includeThumbnail;
if (thumbnailEnabled) {
try {
if (process.env.DEBUG) {
console.error(`[DEBUG] Thumbnail generation enabled, processing: ${absoluteFilePath}`);
}
const { generateThumbnailDataFromFile } = await import('./thumbnail.js');
const thumbnailData = await generateThumbnailDataFromFile(absoluteFilePath);
content.push({
type: "image",
data: thumbnailData.base64,
mimeType: thumbnailData.mimeType,
annotations: {
audience: ["user", "assistant"], // LLMとユーザー両方に表示
priority: 0.8
}
});
if (process.env.DEBUG) {
const thumbnailSize = Math.round(thumbnailData.base64.length * 0.75); // Base64デコード後のサイズ概算
console.error(`[DEBUG] Thumbnail generated successfully: ~${thumbnailSize} bytes`);
}
}
catch (error) {
// サムネイル生成失敗はエラーとせず、警告のみ
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[WARNING] Failed to generate thumbnail for ${absoluteFilePath}: ${errorMessage}`);
if (process.env.DEBUG && error instanceof Error) {
console.error(`[DEBUG] Thumbnail error stack: ${error.stack}`);
}
}
}
else if (process.env.DEBUG) {
console.error(`[DEBUG] Thumbnail generation disabled (VERTEXAI_IMAGEN_THUMBNAIL=${process.env.VERTEXAI_IMAGEN_THUMBNAIL})`);
}
return { content };
}
/**
* 複数画像のURIベースレスポンスを作成(Resources API用、マルチサンプル生成)
*
* @param imageInfos 複数画像の情報配列
* @param additionalInfo 追加情報テキスト(省略可)
* @param includeThumbnail サムネイルを含めるか(デフォルト: true)
* @returns MCPレスポンス形式
*/
export async function createMultiUriImageResponse(imageInfos, additionalInfo, includeThumbnail = true) {
let responseText = additionalInfo || '';
responseText += `\n\nGenerated ${imageInfos.length} image(s):`;
for (let i = 0; i < imageInfos.length; i++) {
const info = imageInfos[i];
const fileName = path.basename(info.absoluteFilePath);
responseText += `\n ${i + 1}. ${info.filePath} (${info.fileSize} bytes)`;
// Markdownリンクを追加
responseText += `\n 📎 [${fileName}](${info.uri})`;
}
responseText += `\n\n💡 *Full-size images available via MCP Resources API*`;
const content = [
{
type: "text",
text: responseText
}
];
// サムネイル生成(オプション)
const thumbnailEnabled = includeThumbnail;
for (const info of imageInfos) {
// サムネイルを生成
if (thumbnailEnabled) {
try {
if (process.env.DEBUG) {
console.error(`[DEBUG] Generating thumbnail for: ${info.absoluteFilePath}`);
}
const { generateThumbnailDataFromFile } = await import('./thumbnail.js');
const thumbnailData = await generateThumbnailDataFromFile(info.absoluteFilePath);
content.push({
type: "image",
data: thumbnailData.base64,
mimeType: thumbnailData.mimeType,
annotations: {
audience: ["user", "assistant"], // LLMとユーザー両方に表示
priority: 0.8
}
});
if (process.env.DEBUG) {
const thumbnailSize = Math.round(thumbnailData.base64.length * 0.75);
console.error(`[DEBUG] Thumbnail generated: ~${thumbnailSize} bytes`);
}
}
catch (error) {
// サムネイル生成失敗はエラーとせず、警告のみ
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[WARNING] Failed to generate thumbnail for ${info.absoluteFilePath}: ${errorMessage}`);
if (process.env.DEBUG && error instanceof Error) {
console.error(`[DEBUG] Thumbnail error stack: ${error.stack}`);
}
}
}
}
if (!thumbnailEnabled && process.env.DEBUG) {
console.error(`[DEBUG] Thumbnail generation disabled (VERTEXAI_IMAGEN_THUMBNAIL=${process.env.VERTEXAI_IMAGEN_THUMBNAIL})`);
}
return { content };
}
//# sourceMappingURL=image.js.map