@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).
311 lines • 12.3 kB
JavaScript
/**
* YAML設定ファイルの解析ユーティリティ
*/
import fs from 'fs/promises';
import path from 'path';
import yaml from 'js-yaml';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { normalizeAndValidatePath, validatePathWithinBase } from './path.js';
import { getTemplateDirectory } from './templateManager.js';
/**
* YAML文字列をパースする
*/
export function parseYamlString(yamlContent) {
try {
// YAMLパース
const config = yaml.load(yamlContent);
if (!config || typeof config !== 'object') {
throw new Error('Invalid YAML structure: root must be an object');
}
return config;
}
catch (error) {
if (error instanceof yaml.YAMLException) {
throw new McpError(ErrorCode.InvalidParams, `YAML syntax error: ${error.message}`);
}
throw new McpError(ErrorCode.InvalidParams, `Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* YAMLファイルを読み込んでパースする
* デフォルトの読込フォルダはJSONテンプレートフォルダ
* パストラバーサル攻撃を防ぐため、設定されているフォルダ階層より上へはアクセスできない
*/
export async function loadYamlConfig(yamlPath) {
try {
// YAMLファイルのベースディレクトリをテンプレートフォルダとする
const yamlBaseDir = getTemplateDirectory();
// パス解決: 絶対パスまたは相対パス
const absoluteYamlPath = path.isAbsolute(yamlPath)
? yamlPath
: path.join(yamlBaseDir, yamlPath);
// Security: Validate path is within base directory (prevent path traversal)
validatePathWithinBase(absoluteYamlPath, yamlBaseDir);
// YAMLファイル読み込み
const yamlContent = await fs.readFile(absoluteYamlPath, 'utf8');
// YAMLパース(共通関数を使用)
return parseYamlString(yamlContent);
}
catch (error) {
if (error.code === 'ENOENT') {
throw new McpError(ErrorCode.InvalidParams, `YAML file not found: ${yamlPath}\n` +
`Default YAML directory: ${getTemplateDirectory()}`);
}
// McpError はそのまま throw
if (error instanceof McpError) {
throw error;
}
throw error;
}
}
/**
* YAML設定の必須フィールドを検証
*/
export function validateYamlConfig(config) {
const errors = [];
// 必須フィールドのチェック
if (!config.model) {
errors.push('model is required');
}
if (!config.output_path) {
errors.push('output_path is required');
}
if (!config.prompt) {
errors.push('prompt is required');
}
// 参照画像が少なくとも1つ必要
const hasSubjects = config.subjects && config.subjects.length > 0;
const hasControl = config.control !== undefined;
const hasStyle = config.style !== undefined;
if (!hasSubjects && !hasControl && !hasStyle) {
errors.push('At least one reference image type must be provided (subjects, control, or style)');
}
// Subjects の検証
if (config.subjects) {
config.subjects.forEach((subject, index) => {
if (subject.reference_id === undefined || subject.reference_id === null) {
errors.push(`subjects[${index}].reference_id is required`);
}
if (!subject.images || subject.images.length === 0) {
errors.push(`subjects[${index}].images is required and must not be empty`);
}
if (!subject.config) {
errors.push(`subjects[${index}].config is required`);
}
else {
if (!subject.config.type) {
errors.push(`subjects[${index}].config.type is required`);
}
if (!subject.config.description) {
errors.push(`subjects[${index}].config.description is required`);
}
}
});
}
// Control の検証
if (config.control) {
if (config.control.reference_id === undefined || config.control.reference_id === null) {
errors.push('control.reference_id is required');
}
if (!config.control.image_path) {
errors.push('control.image_path is required');
}
if (!config.control.type) {
errors.push('control.type is required');
}
}
// Style の検証
if (config.style) {
if (config.style.reference_id === undefined || config.style.reference_id === null) {
errors.push('style.reference_id is required');
}
if (!config.style.image_path) {
errors.push('style.image_path is required');
}
// style.description は省略可能
}
// reference_id の重複チェック(異なるタイプ間)
const referenceIds = [];
if (config.subjects) {
config.subjects.forEach(subject => {
referenceIds.push({ id: subject.reference_id, type: 'SUBJECT' });
});
}
if (config.control) {
referenceIds.push({ id: config.control.reference_id, type: 'CONTROL' });
}
if (config.style) {
referenceIds.push({ id: config.style.reference_id, type: 'STYLE' });
}
// 重複チェック
const idMap = new Map();
referenceIds.forEach(({ id, type }) => {
if (!idMap.has(id)) {
idMap.set(id, []);
}
idMap.get(id).push(type);
});
idMap.forEach((types, id) => {
if (types.length > 1) {
// 同じSUBJECT同士は許可
const uniqueTypes = Array.from(new Set(types));
if (uniqueTypes.length > 1 || uniqueTypes[0] !== 'SUBJECT') {
errors.push(`reference_id ${id} is used by multiple types: ${types.join(', ')}. ` +
`Each reference_id must be unique across different types.`);
}
}
});
if (errors.length > 0) {
throw new McpError(ErrorCode.InvalidParams, `YAML validation errors:\n${errors.map(e => ` - ${e}`).join('\n')}`);
}
}
/**
* subject_type の正規化(省略形 → API形式)
*/
function normalizeSubjectType(type) {
const typeMap = {
'person': 'person',
'animal': 'animal',
'product': 'product',
'default': 'default',
'SUBJECT_TYPE_PERSON': 'person',
'SUBJECT_TYPE_ANIMAL': 'animal',
'SUBJECT_TYPE_PRODUCT': 'product',
'SUBJECT_TYPE_DEFAULT': 'default',
};
return typeMap[type] || type;
}
/**
* control_type の正規化
*/
function normalizeControlType(type) {
const typeMap = {
'face_mesh': 'face_mesh',
'canny': 'canny',
'scribble': 'scribble',
'CONTROL_TYPE_FACE_MESH': 'face_mesh',
'CONTROL_TYPE_CANNY': 'canny',
'CONTROL_TYPE_SCRIBBLE': 'scribble',
};
return typeMap[type] || type;
}
/**
* YAML設定を CustomizeImageArgs に変換
*/
export async function convertYamlToArgs(config) {
const args = {
prompt: config.prompt,
output_path: config.output_path,
model: config.model,
};
// Optional basic parameters
if (config.aspect_ratio)
args.aspect_ratio = config.aspect_ratio;
if (config.sample_image_size)
args.sample_image_size = config.sample_image_size;
if (config.sample_count !== undefined)
args.sample_count = config.sample_count;
if (config.safety_level)
args.safety_level = config.safety_level;
if (config.person_generation)
args.person_generation = config.person_generation;
if (config.language)
args.language = config.language;
if (config.negative_prompt)
args.negative_prompt = config.negative_prompt;
if (config.region)
args.region = config.region;
if (config.return_base64 !== undefined)
args.return_base64 = config.return_base64;
if (config.include_thumbnail !== undefined)
args.include_thumbnail = config.include_thumbnail;
// Subject images の変換
if (config.subjects && config.subjects.length > 0) {
// すべての subject 画像を統合(reference_id ごとにグループ化)
const subjectGroups = new Map();
config.subjects.forEach(subject => {
if (!subjectGroups.has(subject.reference_id)) {
subjectGroups.set(subject.reference_id, subject);
}
else {
// 同じ reference_id の場合、画像を追加
const existing = subjectGroups.get(subject.reference_id);
existing.images.push(...subject.images);
}
});
// 最初の subject グループを使用(現在の customizeImage API は1グループのみサポート)
const firstSubject = Array.from(subjectGroups.values())[0];
// 画像パスの解決と配列構築
args.subject_images = await Promise.all(firstSubject.images.map(async (imagePath) => {
// 相対パスの場合は正規化(入力パスなので autoNumbering: false)
const normalizedPath = await normalizeAndValidatePath(imagePath, false);
return { image_path: normalizedPath };
}));
// description は必須
args.subject_description = firstSubject.config.description;
args.subject_type = normalizeSubjectType(firstSubject.config.type);
// 警告: 複数の subject グループがある場合
if (subjectGroups.size > 1 && process.env.DEBUG) {
console.error(`[WARNING] Multiple subject groups detected (${subjectGroups.size}). ` +
`Only the first group (reference_id: ${firstSubject.reference_id}) will be used.`);
}
}
// Control image の変換
if (config.control) {
// 入力パスなので autoNumbering: false
const normalizedPath = await normalizeAndValidatePath(config.control.image_path, false);
args.control_image_path = normalizedPath;
args.control_type = normalizeControlType(config.control.type);
if (config.control.enable_computation !== undefined) {
args.enable_control_computation = config.control.enable_computation;
}
}
// Style image の変換
if (config.style) {
// 入力パスなので autoNumbering: false
const normalizedPath = await normalizeAndValidatePath(config.style.image_path, false);
args.style_image_path = normalizedPath;
// description は省略可能
if (config.style.description) {
args.style_description = config.style.description;
}
}
return args;
}
/**
* YAML文字列から CustomizeImageArgs を生成
*/
export async function parseYamlStringToCustomizeArgs(yamlContent) {
// 1. YAML文字列をパース
const config = parseYamlString(yamlContent);
// 2. バリデーション
validateYamlConfig(config);
// 3. CustomizeImageArgs に変換
const args = await convertYamlToArgs(config);
if (process.env.DEBUG) {
console.error('[DEBUG] YAML string parsed successfully');
console.error('[DEBUG] Subjects:', args.subject_images?.length || 0);
console.error('[DEBUG] Control:', !!args.control_image_path);
console.error('[DEBUG] Style:', !!args.style_image_path);
}
return args;
}
/**
* YAMLファイルから CustomizeImageArgs を生成(メイン関数)
*/
export async function parseYamlToCustomizeArgs(yamlPath) {
// 1. YAMLファイル読み込み
const config = await loadYamlConfig(yamlPath);
// 2. バリデーション
validateYamlConfig(config);
// 3. CustomizeImageArgs に変換
const args = await convertYamlToArgs(config);
if (process.env.DEBUG) {
console.error('[DEBUG] YAML parsed successfully');
console.error('[DEBUG] Subjects:', args.subject_images?.length || 0);
console.error('[DEBUG] Control:', !!args.control_image_path);
console.error('[DEBUG] Style:', !!args.style_image_path);
}
return args;
}
//# sourceMappingURL=yamlParser.js.map