From dc1d54a0fc42bb1011d8ab02d985d387aab379ec Mon Sep 17 00:00:00 2001 From: ened Date: Tue, 26 Aug 2025 02:10:26 +0900 Subject: [PATCH] clean up unused tools --- TOOL_USAGE_GUIDE.md | 213 +++++++++++++++++++++ src/server/handlers.py | 399 ++------------------------------------- src/server/mcp_server.py | 74 ++------ src/server/models.py | 136 +++---------- 4 files changed, 264 insertions(+), 558 deletions(-) create mode 100644 TOOL_USAGE_GUIDE.md diff --git a/TOOL_USAGE_GUIDE.md b/TOOL_USAGE_GUIDE.md new file mode 100644 index 0000000..77c6361 --- /dev/null +++ b/TOOL_USAGE_GUIDE.md @@ -0,0 +1,213 @@ +# πŸ“‹ GPT-Edit 도ꡬ μ‚¬μš© κ°€μ΄λ“œ + +gpt-edit ν”„λ‘œμ νŠΈλŠ” OpenAI의 GPT-Image-1 λͺ¨λΈμ„ μ‚¬μš©ν•œ AI 이미지 νŽΈμ§‘ 도ꡬ **총 5개**λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€. + +## 🎯 도ꡬ μš°μ„ μˆœμœ„ 및 μ‚¬μš© ꢌμž₯사항 + +### ⭐⭐⭐ **μ΅œμš°μ„  도ꡬ** (일반 μ‚¬μš©μž μΆ”μ²œ) + +#### 1. `gpt_edit_image` +**κ°€μž₯ κ°„λ‹¨ν•˜κ³  직관적인 이미지 νŽΈμ§‘** +- **μ‚¬μš©λ²•**: 이미지 νŒŒμΌμ„ `input_images/` 폴더에 μ—…λ‘œλ“œ β†’ 파일λͺ…κ³Ό νŽΈμ§‘ μ§€μ‹œμ‚¬ν•­ μž…λ ₯ +- **νŠΉμ§•**: μžλ™ μ΅œμ ν™”, 크기 관리, μ™„μ „ μžλ™ν™” +- **ꢌμž₯ λŒ€μƒ**: λͺ¨λ“  μ‚¬μš©μž (μ΄ˆλ³΄μžλΆ€ν„° μ „λ¬Έκ°€κΉŒμ§€) + +```json +{ + "input_image_name": "my_photo.jpg", + "prompt": "make the sky more dramatic with sunset colors", + "background": "transparent" +} +``` + +#### 2. `gpt_edit_image_with_mask` +**μ •λ°€ν•œ 선택적 μ˜μ—­ νŽΈμ§‘** +- **μ‚¬μš©λ²•**: 이미지 + 마슀크 νŒŒμΌμ„ `input_images/`에 μ—…λ‘œλ“œ β†’ νŽΈμ§‘ μ§€μ‹œ +- **νŠΉμ§•**: 마슀크의 흰색 μ˜μ—­λ§Œ νŽΈμ§‘, μ •κ΅ν•œ μ œμ–΄ κ°€λŠ₯ +- **ꢌμž₯ λŒ€μƒ**: μ •λ°€ν•œ νŽΈμ§‘μ΄ ν•„μš”ν•œ μ‚¬μš©μž + +```json +{ + "input_image_name": "portrait.jpg", + "mask_image_name": "background_mask.png", + "prompt": "change background to mountain landscape" +} +``` + +### ⭐⭐ **μ€‘μš” μœ ν‹Έλ¦¬ν‹° 도ꡬ** + +#### 3. `validate_image` +**이미지 파일 검증 및 정보 확인** +- **μš©λ„**: νŽΈμ§‘ μ „ 이미지 μƒνƒœ 확인, 졜적 크기 μ œμ•ˆ +- **ꢌμž₯ μ‹œμ **: νŽΈμ§‘ μ „ 사전 검사 + +#### 4. `create_mask_from_alpha` +**PNG 투λͺ…λ„λ‘œ 마슀크 생성** +- **μš©λ„**: PNG μ΄λ―Έμ§€μ˜ μ•ŒνŒŒ 채널을 마슀크둜 λ³€ν™˜ +- **ꢌμž₯ μ‹œμ **: 투λͺ… λ°°κ²½ 이미지λ₯Ό 마슀크둜 ν™œμš©ν•  λ•Œ + +### ⭐ **κ³ κΈ‰/μžλ™ν™” 도ꡬ** (특수 λͺ©μ ) + +#### 5. `gpt_batch_edit` +**λŒ€λŸ‰ 이미지 일괄 νŽΈμ§‘ (Claude Code μ „μš© ꢌμž₯)** + +## πŸš€ `gpt_batch_edit` 상세 μ‚¬μš© κ°€μ΄λ“œ + +### βœ… **이런 κ²½μš°μ— μ‚¬μš©ν•˜μ„Έμš”:** + +#### πŸ“Έ **μ „μžμƒκ±°λž˜ λŒ€λŸ‰ 처리** +```python +# Claude Codeμ—μ„œ μ‚¬μš© μ˜ˆμ‹œ +batch_config = { + "edits": [ + { + "input_image_name": "product_001.jpg", + "prompt": "remove background, make it pure white", + "background": "opaque" + }, + { + "input_image_name": "product_002.jpg", + "prompt": "remove background, make it pure white", + "background": "opaque" + } + # ... μ΅œλŒ€ 16κ°œκΉŒμ§€ + ] +} +``` + +#### 🎨 **μ†Œμ…œλ―Έλ””μ–΄ ν…œν”Œλ¦Ώ μžλ™ 생성** +```python +# 같은 νŽΈμ§‘μ„ μ—¬λŸ¬ 이미지에 적용 +social_templates = [ + {"file": "template_mon.jpg", "text": "Monday Motivation"}, + {"file": "template_tue.jpg", "text": "Tuesday Tips"}, + {"file": "template_wed.jpg", "text": "Wednesday Wisdom"} +] + +batch_edits = [ + { + "input_image_name": template["file"], + "prompt": f"add bold text '{template['text']}' at center top" + } + for template in social_templates +] +``` + +#### 🏭 **λ§ˆμΌ€νŒ… 자료 λŒ€λŸ‰ 생성** +- λΈŒλžœλ“œ 둜고 일괄 μΆ”κ°€ +- μ›Œν„°λ§ˆν¬ 적용 +- 크기/λΉ„μœ¨ ν‘œμ€€ν™” + +### ❌ **이런 κ²½μš°μ—λŠ” κ°œλ³„ 도ꡬλ₯Ό μ‚¬μš©ν•˜μ„Έμš”:** + +#### 🎭 **Claude Desktop λŒ€ν™”ν˜• νŽΈμ§‘** +- 각 μ΄λ―Έμ§€λ§ˆλ‹€ λ‹€λ₯Έ νŽΈμ§‘μ΄ ν•„μš”ν•œ 경우 +- μ‹€ν—˜μ μ΄κ±°λ‚˜ μ°½μž‘μ μΈ νŽΈμ§‘ +- 단일 이미지 μ •λ°€ μž‘μ—… + +#### πŸ”¬ **μ •λ°€ν•˜κ±°λ‚˜ μ‹€ν—˜μ  νŽΈμ§‘** +- μ—¬λŸ¬ 번 μ‹œν–‰μ°©μ˜€κ°€ ν•„μš”ν•œ μž‘μ—… +- κ²°κ³Όλ₯Ό 보고 λ‹€μŒ νŽΈμ§‘ λ°©ν–₯을 μ •ν•΄μ•Ό ν•˜λŠ” 경우 + +### 🚧 **gpt_batch_edit μ œμ•½μ‚¬ν•­** + +1. **μ΅œλŒ€ 16개 μ œν•œ**: ν•œ λ²ˆμ— μ΅œλŒ€ 16개 μ΄λ―Έμ§€λ§Œ 처리 κ°€λŠ₯ +2. **순차 처리**: 병렬 처리 μ—†μŒ, 총 μ‹œκ°„ = κ°œλ³„ μ‹œκ°„ Γ— 이미지 수 +3. **파일 사전 μ€€λΉ„**: λͺ¨λ“  이미지λ₯Ό `input_images/` 폴더에 미리 μ—…λ‘œλ“œ ν•„μš” +4. **μ‹€νŒ¨ μ‹œ 영ν–₯**: 쀑간에 μ‹€νŒ¨ν•˜λ©΄ 전체 λ°°μΉ˜μ— 영ν–₯ + +### πŸ’‘ **Claude Code ν™œμš© νŒ¨ν„΄** + +#### 파일 μ€€λΉ„ μžλ™ν™” +```python +import os +import shutil + +# 원본 ν΄λ”μ—μ„œ input_images둜 μžλ™ 볡사 +def prepare_batch_files(source_dir, target_dir="input_images"): + os.makedirs(target_dir, exist_ok=True) + + image_files = [] + for filename in os.listdir(source_dir): + if filename.lower().endswith(('.jpg', '.jpeg', '.png', '.webp')): + shutil.copy2( + os.path.join(source_dir, filename), + os.path.join(target_dir, filename) + ) + image_files.append(filename) + + return image_files +``` + +#### λŒ€λŸ‰ 처리 λΆ„ν•  +```python +def create_batch_chunks(file_list, prompt, chunk_size=16): + \"\"\"파일 리슀트λ₯Ό 16κ°œμ”© λ‚˜λˆ„μ–΄ 배치 생성\"\"\" + batches = [] + + for i in range(0, len(file_list), chunk_size): + chunk = file_list[i:i+chunk_size] + batch = { + "edits": [ + { + "input_image_name": filename, + "prompt": prompt, + "background": "transparent" + } + for filename in chunk + ] + } + batches.append(batch) + + return batches +``` + +## πŸ“Š **도ꡬ 선택 ν”Œλ‘œμš°μ°¨νŠΈ** + +``` +이미지 νŽΈμ§‘μ΄ ν•„μš”ν•œκ°€μš”? + ↓ +단일 μ΄λ―Έμ§€μΈκ°€μš”? β†’ YES β†’ λ§ˆμŠ€ν¬κ°€ ν•„μš”ν•œκ°€μš”? + ↓ ↓ YES β†’ gpt_edit_image_with_mask + NO ↓ NO β†’ gpt_edit_image + ↓ +μ—¬λŸ¬ 이미지 (2개 이상)μΈκ°€μš”? + ↓ YES +Claude Codeλ₯Ό μ‚¬μš©ν•˜λ‚˜μš”? & 같은 νŽΈμ§‘μΈκ°€μš”? + ↓ YES β†’ gpt_batch_edit + ↓ NO β†’ gpt_edit_image (κ°œλ³„μ μœΌλ‘œ μ—¬λŸ¬ 번 μ‚¬μš©) +``` + +## 🎯 **졜적 μ‚¬μš© μ „λž΅** + +### πŸ₯‡ **초보자 ꢌμž₯** +1. `gpt_edit_image` μœ„μ£Ό μ‚¬μš© +2. ν•„μš”μ‹œ `validate_image`둜 사전 검사 + +### πŸ₯ˆ **μ€‘κΈ‰μž ꢌμž₯** +1. `gpt_edit_image` + `gpt_edit_image_with_mask` μ‘°ν•© +2. `create_mask_from_alpha`둜 마슀크 생성 ν™œμš© + +### πŸ₯‰ **κ³ κΈ‰ μ‚¬μš©μž (Claude Code)** +1. λͺ¨λ“  도ꡬ ν™œμš© +2. `gpt_batch_edit`둜 μžλ™ν™” μ›Œν¬ν”Œλ‘œμš° ꡬ성 +3. 슀크립트 기반 λŒ€λŸ‰ 처리 + +## πŸ’Ύ **파일 μ €μž₯ κ·œμΉ™** + +λͺ¨λ“  νŽΈμ§‘ κ²°κ³ΌλŠ” λ‹€μŒ ν˜•μ‹μœΌλ‘œ μ €μž₯λ©λ‹ˆλ‹€: +``` +generated_images/ +β”œβ”€β”€ gptimage1_123456_20250826_143022_000.png # μž…λ ₯ 파일 (μΆ”μ μš©) +β”œβ”€β”€ gptimage1_123456_20250826_143022_001.png # νŽΈμ§‘ κ²°κ³Ό +└── gptimage1_123456_20250826_143022_001.json # νŽΈμ§‘ νŒŒλΌλ―Έν„° +``` + +**Base Name ν˜•μ‹**: `gptimage1_{6μžλ¦¬μ‹œλ“œ}_{λ‚ μ§œ}_{μ‹œκ°„}` +- μ‹œλ“œλŠ” μ„Έμ…˜λ³„λ‘œ λ™μΌν•˜κ²Œ μœ μ§€ +- λͺ¨λ“  κ΄€λ ¨ 파일이 같은 base name μ‚¬μš© +- JSON νŒŒμΌμ— νŽΈμ§‘ κ³Όμ • 전체가 기둝됨 + +--- + +이 κ°€μ΄λ“œλ₯Ό 톡해 상황에 λ§žλŠ” 졜적의 도ꡬλ₯Ό μ„ νƒν•˜μ—¬ 효율적인 AI 이미지 νŽΈμ§‘μ„ κ²½ν—˜ν•΄λ³΄μ„Έμš”! 🎨✨ diff --git a/src/server/handlers.py b/src/server/handlers.py index d977ff4..2d28701 100644 --- a/src/server/handlers.py +++ b/src/server/handlers.py @@ -169,300 +169,13 @@ class ToolHandlers: return self._move_temp_to_generated(temp_file_path, base_name, index, extension) - async def handle_edit_image(self, arguments: Dict[str, Any]) -> List[TextContent | ImageContent]: - """ - Handle edit_image tool call - - Args: - arguments: Tool arguments - - Returns: - List of content items - """ - try: - # Check for input_image_b64 parameter - if 'input_image_b64' not in arguments: - return [TextContent( - type="text", - text="❌ input_image_b64 is required" - )] - - # Get or generate image name - image_name = arguments.get('input_image_name', f'temp_image_{random.randint(1000, 9999)}.png') - - # Save base64 image to temp directory with specified name - image_path = self._save_b64_to_temp_file( - arguments['input_image_b64'], - image_name - ) - logger.info(f"Input image saved to temp: {image_name}") - - # Validate the saved image - is_valid, size_mb, error_msg = validate_image_file(image_path, self.config.max_image_size_mb) - if not is_valid: - return [TextContent( - type="text", - text=f"❌ Image validation failed: {error_msg}" - )] - - # Generate base name and copy temp file to generated_images - base_name = self.config.generate_base_name_simple() - - # Copy temp file to generated_images as {base_name}_000.ext - input_generated_path = self._copy_temp_to_generated(image_path, base_name, 0) - logger.info(f"Input file copied to generated_images: {Path(input_generated_path).name}") - - # Sanitize prompt - prompt = sanitize_prompt(arguments['prompt']) - - # Create edit request (use generated path for processing) - request = ImageEditRequest( - image_path=input_generated_path, # Use generated path instead of temp - prompt=prompt, - background=arguments.get('background', 'transparent') - ) - - # Process edit - response = await self.client.edit_image(request) - - if not response.success: - return [TextContent( - type="text", - text=f"❌ Edit failed: {response.error_message}" - )] - - # Save output image as {base_name}_001.png - saved_path = None - json_path = None - if arguments.get('save_to_file', True): - output_path = self.config.get_output_path(base_name, 1, 'png') - - if save_image(response.edited_image_data, str(output_path)): - saved_path = str(output_path) - - # Save parameters as {base_name}_001.json - if self.config.save_parameters: - params_dict = { - "base_name": base_name, - "timestamp": datetime.now().isoformat(), - "prompt": request.prompt, - "background": request.background, - "input_image_name": image_name, - "input_temp_path": image_path, - "input_generated_path": input_generated_path, - "input_size": get_image_dimensions(input_generated_path), - "output_size": response.image_size, - "execution_time": response.execution_time, - "optimization": response.optimization_info if response.optimization_info else None, - "token_stats": response.token_stats if response.token_stats else None, - "config": { - "model": Config.MODEL, - "quality": Config.QUALITY, - "api_version": "gpt-image-1" - } - } - - json_path = self.config.get_output_path(base_name, 1, 'json') - with open(json_path, 'w', encoding='utf-8') as f: - json.dump(params_dict, f, indent=2, ensure_ascii=False) - logger.info(f"Parameters saved to: {json_path}") - - # Prepare response - contents = [] - - # Add text description - text = f"βœ… Image edited successfully!\n" - text += f"πŸ“ Input: {image_name}\n" - text += f"πŸ“ Base name: {base_name}\n" - text += f"πŸ“ Size: {response.image_size[0]}x{response.image_size[1]}\n" - text += f"⏱️ Processing time: {response.execution_time:.1f}s\n" - - # Add optimization info if image was optimized - if response.optimization_info and response.optimization_info["optimized"]: - opt_info = response.optimization_info - text += f"\nπŸ”„ Image was auto-optimized:\n" - text += f" Original: {opt_info['original_size_mb']:.2f}MB\n" - text += f" Optimized: {opt_info['final_size_mb']:.2f}MB\n" - text += f" Format: {opt_info['format_used']}\n" - - if saved_path: - text += f"\nπŸ’Ύ Output: {Path(saved_path).name}" - text += f"\nπŸ“ Input: {Path(input_generated_path).name}" # Show generated file - if json_path: - text += f"\nπŸ“‹ Parameters: {Path(json_path).name}" - - contents.append(TextContent(type="text", text=text)) - - # Add image preview - image_b64 = encode_image_base64(response.edited_image_data) - contents.append(ImageContent( - type="image", - data=image_b64, - mimeType="image/png" - )) - - # Reset seed for next session - self._reset_seed() - - return contents - - except Exception as e: - logger.error(f"Error in handle_edit_image: {e}", exc_info=True) - return [TextContent( - type="text", - text=f"❌ Unexpected error: {str(e)}" - )] + - async def handle_edit_with_mask(self, arguments: Dict[str, Any]) -> List[TextContent | ImageContent]: - """ - Handle edit_with_mask tool call - - Args: - arguments: Tool arguments - - Returns: - List of content items - """ - try: - # Validate required parameters - if 'input_image_b64' not in arguments: - return [TextContent( - type="text", - text="❌ input_image_b64 is required" - )] - if 'input_mask_b64' not in arguments: - return [TextContent( - type="text", - text="❌ input_mask_b64 is required for masked editing" - )] - - # Get or generate image names - image_name = arguments.get('input_image_name', f'temp_image_{random.randint(1000, 9999)}.png') - mask_name = arguments.get('mask_image_name', f'temp_mask_{random.randint(1000, 9999)}.png') - - # Save base64 images to temp directory with specified names - image_path = self._save_b64_to_temp_file( - arguments['input_image_b64'], - image_name - ) - logger.info(f"Input image saved to temp: {image_name}") - - # Save base64 mask to temp directory - mask_path = self._save_b64_to_temp_file( - arguments['input_mask_b64'], - mask_name - ) - logger.info(f"Mask image saved to temp: {mask_name}") - - # Validate the saved images - is_valid, size_mb, error_msg = validate_image_file(image_path, self.config.max_image_size_mb) - if not is_valid: - return [TextContent( - type="text", - text=f"❌ Image validation failed: {error_msg}" - )] - - # Generate base name and move temp files to generated_images - base_name = self.config.generate_base_name_simple() - - # Copy temp files to generated_images - input_generated_path = self._copy_temp_to_generated(image_path, base_name, 0) - mask_generated_path = self._copy_temp_to_generated(mask_path, f"{base_name}_mask", 0, 'png') # Force PNG for mask - - logger.info(f"Input file copied to generated_images: {Path(input_generated_path).name}") - logger.info(f"Mask file copied to generated_images: {Path(mask_generated_path).name}") - - # Sanitize prompt - prompt = sanitize_prompt(arguments['prompt']) - - # Create edit request with mask (use generated paths) - request = ImageEditRequest( - image_path=input_generated_path, - prompt=prompt, - mask_path=mask_generated_path, - background=arguments.get('background', 'transparent') - ) - - # Process edit - response = await self.client.edit_image(request) - - if not response.success: - return [TextContent( - type="text", - text=f"❌ Masked edit failed: {response.error_message}" - )] - - # Save output image - saved_path = None - json_path = None - if arguments.get('save_to_file', True): - output_path = self.config.get_output_path(base_name, 1, 'png') - - if save_image(response.edited_image_data, str(output_path)): - saved_path = str(output_path) - - # Save parameters - if self.config.save_parameters: - params_dict = { - "base_name": base_name, - "timestamp": datetime.now().isoformat(), - "prompt": request.prompt, - "background": request.background, - "input_image_name": image_name, - "mask_image_name": mask_name, - "input_temp_path": image_path, - "mask_temp_path": mask_path, - "input_generated_path": input_generated_path, - "mask_generated_path": mask_generated_path, - "input_size": get_image_dimensions(input_generated_path), - "output_size": response.image_size, - "execution_time": response.execution_time - } - - json_path = self.config.get_output_path(base_name, 1, 'json') - with open(json_path, 'w', encoding='utf-8') as f: - json.dump(params_dict, f, indent=2, ensure_ascii=False) - - # Prepare response - contents = [] - - text = f"βœ… Image edited with mask successfully!\n" - text += f"πŸ“ Input: {image_name}\n" - text += f"🎭 Mask: {mask_name}\n" - text += f"πŸ“ Base name: {base_name}\n" - text += f"πŸ“ Size: {response.image_size[0]}x{response.image_size[1]}\n" - text += f"⏱️ Processing time: {response.execution_time:.1f}s\n" - - if saved_path: - text += f"\nπŸ’Ύ Output: {Path(saved_path).name}" - if json_path: - text += f"\nπŸ“‹ Parameters: {Path(json_path).name}" - - contents.append(TextContent(type="text", text=text)) - - # Add image preview - image_b64 = encode_image_base64(response.edited_image_data) - contents.append(ImageContent( - type="image", - data=image_b64, - mimeType="image/png" - )) - - # Reset seed for next session - self._reset_seed() - - return contents - - except Exception as e: - logger.error(f"Error in handle_edit_with_mask: {e}", exc_info=True) - return [TextContent( - type="text", - text=f"❌ Unexpected error: {str(e)}" - )] + - async def handle_batch_edit(self, arguments: Dict[str, Any]) -> List[TextContent]: + async def handle_gpt_batch_edit(self, arguments: Dict[str, Any]) -> List[TextContent]: """ - Handle batch_edit tool call + Handle gpt_batch_edit tool call Args: arguments: Tool arguments @@ -610,7 +323,7 @@ class ToolHandlers: successful = sum(1 for r in results if r.get("success")) failed = len(results) - successful - text = f"πŸ“¦ Batch Edit Complete\n" + text = f"πŸ“¦ GPT Batch Edit Complete\n" text += f"🎲 Seed: {seed}\n" text += f"πŸ“ Base name: {base_name}\n" text += f"βœ… Successful: {successful}/{len(results)}\n" @@ -627,7 +340,7 @@ class ToolHandlers: return [TextContent(type="text", text=text)] except Exception as e: - logger.error(f"Error in handle_batch_edit: {e}", exc_info=True) + logger.error(f"Error in handle_gpt_batch_edit: {e}", exc_info=True) self._reset_seed() return [TextContent( type="text", @@ -734,9 +447,9 @@ class ToolHandlers: text=f"❌ Mask creation error: {str(e)}" )] - async def handle_edit_image_from_file(self, arguments: Dict[str, Any]) -> List[TextContent | ImageContent]: + async def handle_gpt_edit_image(self, arguments: Dict[str, Any]) -> List[TextContent | ImageContent]: """ - Handle edit_image_from_file tool call + Handle gpt_edit_image tool call - Read file from INPUT_PATH directory specified by input_image_name - If file size > MAX_IMAGE_SIZE_MB, convert to WebP lossy format - Save converted file to generated_images as {base_name}_000.png @@ -972,15 +685,15 @@ class ToolHandlers: return contents except Exception as e: - logger.error(f"Error in handle_edit_image_from_file: {e}", exc_info=True) + logger.error(f"Error in handle_gpt_edit_image: {e}", exc_info=True) return [TextContent( type="text", text=f"❌ File-based image edit error: {str(e)}" )] - async def handle_edit_with_mask_from_file(self, arguments: Dict[str, Any]) -> List[TextContent | ImageContent]: + async def handle_gpt_edit_image_with_mask(self, arguments: Dict[str, Any]) -> List[TextContent | ImageContent]: """ - Handle edit_with_mask_from_file tool call + Handle gpt_edit_image_with_mask tool call - Read files from INPUT_PATH directory specified by input_image_name and mask_image_name - If file size > MAX_IMAGE_SIZE_MB, convert to WebP lossy format - Save converted files to generated_images as {base_name}_000.png and {base_name}_mask_000.png @@ -1280,96 +993,10 @@ class ToolHandlers: return contents except Exception as e: - logger.error(f"Error in handle_edit_with_mask_from_file: {e}", exc_info=True) + logger.error(f"Error in handle_gpt_edit_image_with_mask: {e}", exc_info=True) return [TextContent( type="text", text=f"❌ File-based masked edit error: {str(e)}" )] - async def handle_move_temp_to_output(self, arguments: Dict[str, Any]) -> List[TextContent]: - """Handle move_temp_to_output tool call - - Args: - arguments: Tool arguments containing temp_file_name - - Returns: - List of content items - """ - try: - temp_file_name = arguments.get('temp_file_name') - output_file_name = arguments.get('output_file_name') - copy_only = arguments.get('copy_only', False) - - if not temp_file_name: - return [TextContent( - type="text", - text="❌ temp_file_name is required" - )] - - # Get temp file path - temp_file_path = self.config.base_path / 'temp' / temp_file_name - - # Check if temp file exists - if not Path(temp_file_path).exists(): - return [TextContent( - type="text", - text=f"❌ Temp file not found: {temp_file_name}" - )] - - # Generate output file name if not provided - if not output_file_name: - base_name = self.config.generate_base_name_simple() - file_ext = Path(temp_file_name).suffix[1:] or 'png' - output_file_name = f"{base_name}_001.{file_ext}" - - # Ensure output directory exists - self.config.ensure_output_directory() - - # Get output path - output_path = self.config.generated_images_path / output_file_name - - # Move or copy file - try: - if copy_only: - import shutil - shutil.copy2(temp_file_path, output_path) - operation = "copied" - else: - # Move file - import shutil - shutil.move(temp_file_path, output_path) - operation = "moved" - - # Verify operation was successful - if not output_path.exists(): - raise RuntimeError(f"File {operation} verification failed") - - logger.info(f"πŸ“ File {operation}: {temp_file_name} -> {output_file_name}") - - # Get file size for reporting - file_size_mb = output_path.stat().st_size / (1024 * 1024) - - text = f"βœ… File {operation} successfully!\n" - text += f"πŸ“ From temp: {temp_file_name}\n" - text += f"πŸ“ To output: {output_file_name}\n" - text += f"πŸ’Ύ Size: {file_size_mb:.2f}MB" - - return [TextContent(type="text", text=text)] - - except PermissionError as e: - return [TextContent( - type="text", - text=f"❌ Permission denied: {str(e)}" - )] - except shutil.Error as e: - return [TextContent( - type="text", - text=f"❌ File operation failed: {str(e)}" - )] - - except Exception as e: - logger.error(f"Error in handle_move_temp_to_output: {e}", exc_info=True) - return [TextContent( - type="text", - text=f"❌ File move error: {str(e)}" - )] + diff --git a/src/server/mcp_server.py b/src/server/mcp_server.py index 1485ef3..f59ceda 100644 --- a/src/server/mcp_server.py +++ b/src/server/mcp_server.py @@ -58,22 +58,16 @@ class GPTEditMCPServer: logger.info(f"Tool called: {name} with arguments: {safe_args}") try: - if name == "edit_image": - return await self.handlers.handle_edit_image(arguments) - elif name == "edit_with_mask": - return await self.handlers.handle_edit_with_mask(arguments) - elif name == "batch_edit": - return await self.handlers.handle_batch_edit(arguments) + if name == "gpt_batch_edit": + return await self.handlers.handle_gpt_batch_edit(arguments) elif name == "validate_image": return await self.handlers.handle_validate_image(arguments) elif name == "create_mask_from_alpha": return await self.handlers.handle_create_mask_from_alpha(arguments) - elif name == "edit_image_from_file": - return await self.handlers.handle_edit_image_from_file(arguments) - elif name == "edit_with_mask_from_file": - return await self.handlers.handle_edit_with_mask_from_file(arguments) - elif name == "move_temp_to_output": - return await self.handlers.handle_move_temp_to_output(arguments) + elif name == "gpt_edit_image": + return await self.handlers.handle_gpt_edit_image(arguments) + elif name == "gpt_edit_image_with_mask": + return await self.handlers.handle_gpt_edit_image_with_mask(arguments) else: error_msg = f"Unknown tool: {name}" logger.error(error_msg) @@ -92,23 +86,7 @@ class GPTEditMCPServer: logger.debug("list_prompts called") prompts = [ Prompt( - name="edit_image", - description="Edit an image with AI-powered instructions", - arguments=[ - { - "name": "image_path", - "description": "Path to the image to edit", - "required": True - }, - { - "name": "edit_description", - "description": "Description of how to edit the image", - "required": True - } - ] - ), - Prompt( - name="edit_with_mask_prompt", + name="gpt_edit_image_with_mask", description="Edit an image with a mask", arguments=[ { @@ -129,17 +107,17 @@ class GPTEditMCPServer: ] ), Prompt( - name="optimize_and_edit", - description="Automatically optimize and edit an image", + name="gpt_batch_edit", + description="Edit multiple images with batch processing (for automation/Claude Code)", arguments=[ { - "name": "image_path", - "description": "Path to the image to edit", + "name": "image_paths", + "description": "Comma-separated list of image paths to edit", "required": True }, { "name": "edit_description", - "description": "Description of how to edit the image", + "description": "Description of how to edit the images", "required": True } ] @@ -153,25 +131,7 @@ class GPTEditMCPServer: """Get a specific prompt""" logger.debug(f"get_prompt called for: {name}") - if name == "edit_image": - if not arguments or "image_path" not in arguments or "edit_description" not in arguments: - return [PromptMessage( - role="user", - content=TextContent( - type="text", - text="Please provide an image_path and edit_description" - ) - )] - - return [PromptMessage( - role="user", - content=TextContent( - type="text", - text=f"Edit the image at '{arguments['image_path']}' with the following instructions: {arguments['edit_description']}" - ) - )] - - elif name == "edit_with_mask_prompt": + if name == "gpt_edit_image_with_mask": if not arguments or "image_path" not in arguments or "mask_path" not in arguments or "edit_description" not in arguments: return [PromptMessage( role="user", @@ -189,13 +149,13 @@ class GPTEditMCPServer: ) )] - elif name == "optimize_and_edit": - if not arguments or "image_path" not in arguments or "edit_description" not in arguments: + elif name == "gpt_batch_edit": + if not arguments or "image_paths" not in arguments or "edit_description" not in arguments: return [PromptMessage( role="user", content=TextContent( type="text", - text="Please provide an image_path and edit_description" + text="Please provide image_paths and edit_description" ) )] @@ -203,7 +163,7 @@ class GPTEditMCPServer: role="user", content=TextContent( type="text", - text=f"Optimize and edit the image at '{arguments['image_path']}' with: {arguments['edit_description']}" + text=f"Batch edit the images at '{arguments['image_paths']}' with: {arguments['edit_description']}" ) )] diff --git a/src/server/models.py b/src/server/models.py index dbe1a6f..4e07751 100644 --- a/src/server/models.py +++ b/src/server/models.py @@ -7,81 +7,15 @@ from mcp.types import Tool class MCPToolDefinitions: """MCP tool definitions for GPTEdit""" - @staticmethod - def get_edit_image_tool() -> Tool: - """Get edit_image tool definition""" - return Tool( - name="edit_image", - description="Edit an existing image using AI-powered editing with OpenAI GPT-Image-1", - inputSchema={ - "type": "object", - "properties": { - "input_image_b64": { - "type": "string", - "description": "Base64 encoded input image data to edit (supports PNG, JPEG, WebP, etc.)" - }, - "prompt": { - "type": "string", - "description": "Description of how to edit the image" - }, - "background": { - "type": "string", - "enum": ["transparent", "opaque"], - "default": "transparent", - "description": "Background type for the edited image" - }, - "save_to_file": { - "type": "boolean", - "default": True, - "description": "Whether to save the edited image to a file" - } - }, - "required": ["input_image_b64", "prompt"] - } - ) + + + @staticmethod - def get_edit_with_mask_tool() -> Tool: - """Get edit_with_mask tool definition""" + def get_gpt_batch_edit_tool() -> Tool: + """Get gpt_batch_edit tool definition""" return Tool( - name="edit_with_mask", - description="Edit an image with a mask to specify which areas to modify", - inputSchema={ - "type": "object", - "properties": { - "input_image_b64": { - "type": "string", - "description": "Base64 encoded input image data to edit (supports PNG, JPEG, WebP, etc.)" - }, - "input_mask_b64": { - "type": "string", - "description": "Base64 encoded mask image (white areas will be edited)" - }, - "prompt": { - "type": "string", - "description": "Description of how to edit the masked areas" - }, - "background": { - "type": "string", - "enum": ["transparent", "opaque"], - "default": "transparent", - "description": "Background type for the edited image" - }, - "save_to_file": { - "type": "boolean", - "default": True, - "description": "Whether to save the edited image to a file" - } - }, - "required": ["input_image_b64", "input_mask_b64", "prompt"] - } - ) - - @staticmethod - def get_batch_edit_tool() -> Tool: - """Get batch_edit tool definition""" - return Tool( - name="batch_edit", + name="gpt_batch_edit", description="Edit multiple images with the same or different prompts (max 16 images)", inputSchema={ "type": "object", @@ -165,10 +99,10 @@ class MCPToolDefinitions: ) @staticmethod - def get_edit_image_from_file_tool() -> Tool: - """Get edit_image_from_file tool definition (for file paths only)""" + def get_gpt_edit_image_tool() -> Tool: + """Get gpt_edit_image tool definition (for file paths only)""" return Tool( - name="edit_image_from_file", + name="gpt_edit_image", description="Edit an image file by providing the file path (alternative to base64 input)", inputSchema={ "type": "object", @@ -203,10 +137,10 @@ class MCPToolDefinitions: ) @staticmethod - def get_edit_with_mask_from_file_tool() -> Tool: - """Get edit_with_mask_from_file tool definition (for file paths only)""" + def get_gpt_edit_image_with_mask_tool() -> Tool: + """Get gpt_edit_image_with_mask tool definition (for file paths only)""" return Tool( - name="edit_with_mask_from_file", + name="gpt_edit_image_with_mask", description="Edit an image with mask by providing file paths (alternative to base64 input)", inputSchema={ "type": "object", @@ -245,46 +179,18 @@ class MCPToolDefinitions: ) - @staticmethod - def get_move_temp_to_output_tool() -> Tool: - """Get move_temp_to_output tool definition""" - return Tool( - name="move_temp_to_output", - description="Move file from temp directory to output (generated_images) directory", - inputSchema={ - "type": "object", - "properties": { - "temp_file_name": { - "type": "string", - "description": "Name of the file in temp directory to move" - }, - "output_file_name": { - "type": "string", - "description": "Optional: Desired name for the output file (will auto-generate if not provided)" - }, - "copy_only": { - "type": "boolean", - "default": False, - "description": "If true, copy file instead of moving (keep original in temp)" - } - }, - "required": ["temp_file_name"] - } - ) + @staticmethod def get_all_tools() -> List[Tool]: """Get all available tools""" return [ - # Main editing tools - MCPToolDefinitions.get_edit_image_tool(), - MCPToolDefinitions.get_edit_with_mask_tool(), - MCPToolDefinitions.get_batch_edit_tool(), - # File-based tools (recommended) - MCPToolDefinitions.get_edit_image_from_file_tool(), - MCPToolDefinitions.get_edit_with_mask_from_file_tool(), - # Utility tools - MCPToolDefinitions.get_validate_image_tool(), - MCPToolDefinitions.get_create_mask_from_alpha_tool(), - MCPToolDefinitions.get_move_temp_to_output_tool() + # File-based tools (recommended - highest priority) + MCPToolDefinitions.get_gpt_edit_image_tool(), # ⭐⭐⭐ μ΅œμš°μ„  + MCPToolDefinitions.get_gpt_edit_image_with_mask_tool(), # ⭐⭐⭐ μ΅œμš°μ„  + # Utility tools (important - medium priority) + MCPToolDefinitions.get_validate_image_tool(), # ⭐⭐ μ€‘μš” + MCPToolDefinitions.get_create_mask_from_alpha_tool(), # ⭐⭐ μ€‘μš” + # Advanced/automation tool (special purpose - low priority) + MCPToolDefinitions.get_gpt_batch_edit_tool() # ⭐ 특수 λͺ©μ μš© (Claude Code λ“±) ]