Files
gpt-edit/CLAUDE.md

616 lines
18 KiB
Markdown
Raw Normal View History

2025-08-26 01:31:42 +09:00
# GPT-Edit MCP Server - 설계 및 개발 가이드
2025-08-25 00:39:39 +09:00
## 📋 프로젝트 개요
2025-08-26 01:31:42 +09:00
GPT-Edit는 OpenAI의 이미지 편집 API를 MCP(Model Context Protocol) 서버로 구현한 프로젝트입니다.
2025-08-25 00:39:39 +09:00
이 문서는 향후 유사한 MCP 서버 개발 시 참고할 수 있는 설계 원칙과 구현 가이드를 제공합니다.
## 🏗️ 핵심 설계 원칙
### 1. 파일 구조 단순화
**✅ 단일 디렉토리 구조 채택:**
```
generated_images/
├── gptimage1_123456_20250824_143022_000.png # 입력 파일
├── gptimage1_123456_20250824_143022_001.png # 첫 번째 출력
├── gptimage1_123456_20250824_143022_001.json # 첫 번째 출력 파라미터
├── gptimage1_123456_20250824_143022_mask.png # 마스크 파일 (필요시)
└── ...
```
### 2. 파일 명명 규칙
#### Base Name 형식
`gptimage1_{seed}_{yyyymmdd}_{hhmmss}`
- `gptimage1`: 고정 prefix (환경변수로 변경 가능)
- `{seed}`: 6자리 랜덤 시드 (세션 단위로 유지)
- `{yyyymmdd}`: 날짜 (예: 20250824)
- `{hhmmss}`: 시간 (예: 143022)
#### 파일 타입별 명명
- **입력 파일**: `{base_name}_000.{ext}` (000은 항상 입력을 의미)
- **출력 파일**: `{base_name}_001.png`, `{base_name}_002.png`, ...
- **파라미터 파일**: `{base_name}_001.json`, `{base_name}_002.json`, ...
- **마스크 파일**: `{base_name}_mask.{ext}`
### 3. 세션 기반 시드 관리
```python
class ToolHandlers:
def __init__(self, config):
self.current_seed = None # 세션 시드
def _get_or_create_seed(self):
if self.current_seed is None:
self.current_seed = random.randint(0, 999999)
return self.current_seed
def _reset_seed(self):
self.current_seed = None # 작업 완료 후 리셋
```
## ⚠️ API 수정 시 필수 체크리스트
### 함수명이나 파라미터 변경 시 반드시 확인할 파일들
MCP 서버는 Claude와 통신하는 양방향 시스템입니다. **한쪽만 수정하면 작동하지 않습니다!**
#### 1. 서버 측 (MCP Server)
- `src/server/models.py` - 도구 정의 (Tool definitions)
- `src/server/handlers.py` - 도구 실행 로직 (Handler implementations)
- `src/server/mcp_server.py` - 도구 등록 및 라우팅 (Tool registration)
#### 2. 커넥터 측 (API Connector)
- `src/connector/openai_client.py` - API 호출 로직
- `src/connector/config.py` - 설정 및 파라미터 관리
- `src/connector/models.py` - 데이터 모델 정의 (있는 경우)
#### 3. 유틸리티
- `src/utils/` - 공통 함수들 (파라미터 타입 변경 시)
#### 4. 문서
- `README.md` - 사용 예제
- `TECHNICAL_SPECS.md` - API 스펙
- `MCP_CONNECTOR_GUIDE.md` - 연동 가이드
### 수정 순서 (중요!)
1. **모델 정의 수정** (`models.py`)
```python
# 예: edit_simple → edit_image
def get_edit_image_tool() -> Tool:
return Tool(
name="edit_image", # 이름 변경
inputSchema={
"properties": {
"input_image_b64": {...} # 파라미터 변경
}
}
)
```
2. **핸들러 수정** (`handlers.py`)
```python
async def handle_edit_image(self, arguments):
# 새 파라미터 처리 로직
if 'input_image_b64' not in arguments: # 변경된 파라미터
```
3. **서버 라우팅 수정** (`mcp_server.py`)
```python
if name == "edit_image": # 변경된 이름
return await self.handlers.handle_edit_image(arguments)
```
4. **테스트 실행**
```bash
# 단독 테스트
python tests/test_server.py
# Claude 연동 테스트
python main.py
# Claude Desktop에서 도구 호출 테스트
```
### 자주 발생하는 실수
**하지 말아야 할 것:**
- 한쪽만 수정하고 테스트
- 프롬프트 이름만 바꾸고 핸들러는 그대로 둠
- 파라미터 이름 변경 시 validation 로직 미수정
**반드시 해야 할 것:**
- 모든 관련 파일 동시 수정
- 변경 후 즉시 테스트
- 문서 업데이트
### 실제 수정 예시: image_path → input_image_b64
#### 변경 이유
Claude가 업로드된 이미지를 바로 처리할 수 있도록 Base64 입력으로 변경
#### 수정한 파일들
1. **`src/server/models.py`** - 도구 정의
```python
# Before
"image_path": {
"type": "string",
"description": "Path to the image file"
}
# After
"input_image_b64": {
"type": "string",
"description": "Base64 encoded input image data"
}
```
2. **`src/server/handlers.py`** - 핸들러 로직
```python
# Before
image_path = arguments.get('image_path')
if not image_path or not Path(image_path).exists():
return [TextContent(text="Image not found")]
# After
if 'input_image_b64' not in arguments:
return [TextContent(text="input_image_b64 is required")]
# Save b64 to temp file
image_path = self._save_b64_to_temp_file(
arguments['input_image_b64'],
base_name,
0,
"input"
)
```
3. **`src/utils/image_utils.py`** - 새 함수 추가
```python
def decode_image_base64(base64_str: str) -> bytes:
"""Decode base64 string to image data"""
return base64.b64decode(base64_str)
```
### 디버깅 팁
#### 에러 발생 시 확인 순서
1. **로그 파일 확인**
```bash
2025-08-26 01:31:42 +09:00
tail -f gpt-edit.log
2025-08-25 00:39:39 +09:00
```
2. **Claude Desktop 에러 메시지**
- "Method not found" → 함수명 불일치
- "Invalid arguments" → 파라미터 문제
- "Server disconnected" → Python 크래시
3. **단계별 테스트**
```python
# tests/test_api_change.py
async def test_new_parameter():
handlers = ToolHandlers(config)
result = await handlers.handle_edit_image({
"input_image_b64": test_b64,
"prompt": "test"
})
assert result[0].type == "text"
```
4. **JSON-RPC 통신 확인**
```python
# 로깅 추가로 통신 내용 확인
logger.debug(f"Received: {json.dumps(request, indent=2)}")
logger.debug(f"Sending: {json.dumps(response, indent=2)}")
```
### API 변경 테스트 체크리스트
☑️ **1단계: 단독 테스트**
```bash
# Python 직접 테스트
python -c "from src.server.models import MCPToolDefinitions; print([t.name for t in MCPToolDefinitions.get_all_tools()])"
# 결과: ['edit_image', 'edit_with_mask', ...] 확인
```
☑️ **2단계: 서버 실행 테스트**
```bash
python main.py
# 로그에 "Tool called: edit_image" 확인
```
☑️ **3단계: Claude Desktop 테스트**
1. Claude Desktop 재시작
2. "사용 가능한 도구가 무엇인가요?" 질문
3. 변경된 도구명 확인
4. 실제 호출 테스트
☑️ **4단계: 파라미터 테스트**
```python
# tests/test_parameters.py
import asyncio
from src.server.handlers import ToolHandlers
from src.connector import Config
async def test():
handlers = ToolHandlers(Config())
# 새 파라미터로 테스트
result = await handlers.handle_edit_image({
"input_image_b64": "base64_data_here",
"prompt": "test prompt"
})
print(result)
asyncio.run(test())
```
## 🔧 MCP 서버 구현 체크리스트
### 필수 MCP 메서드
MCP 서버는 다음 메서드들을 반드시 구현해야 합니다:
1. **`list_tools()`** - 사용 가능한 도구 목록 반환
2. **`call_tool()`** - 도구 실행
3. **`list_prompts()`** - 프롬프트 템플릿 목록 반환
4. **`get_prompt()`** - 특정 프롬프트 템플릿 반환
5. **`list_resources()`** - 리소스 목록 반환 (없으면 빈 리스트)
### 로깅 설정
```python
# stdout과 충돌 방지를 위해 stderr 사용
logging.basicConfig(
level=logging.INFO,
handlers=[
2025-08-26 01:31:42 +09:00
logging.FileHandler('gpt-edit.log', encoding='utf-8'),
2025-08-25 00:39:39 +09:00
logging.StreamHandler(sys.stderr) # stderr 사용!
]
)
```
## 📁 프로젝트 구조
```
2025-08-26 01:31:42 +09:00
gpt-edit/
2025-08-25 00:39:39 +09:00
├── src/
│ ├── connector/ # API 연결 모듈
│ │ ├── config.py # 설정 관리 및 파일명 생성
│ │ └── openai_client.py
│ ├── server/ # MCP 서버
│ │ ├── mcp_server.py # MCP 핸들러 등록
│ │ ├── handlers.py # 도구 구현
│ │ └── models.py # 도구 정의
│ └── utils/ # 유틸리티
├── generated_images/ # 모든 이미지 저장 (단일 디렉토리)
├── temp/ # 임시 파일
├── .env # 환경 변수
└── main.py # 진입점
```
## 🔑 환경 변수 설정
`.env` 파일 예시:
```bash
# API 설정
OPENAI_API_KEY=sk-xxxxx
OPENAI_ORGANIZATION=org-xxxxx # 선택사항
# 서버 설정
LOG_LEVEL=INFO
MAX_IMAGE_SIZE_MB=4
DEFAULT_TIMEOUT=30
# 파일 명명 설정
OUTPUT_FILENAME_PREFIX=gptimage1 # 파일명 prefix
# 기능 플래그
ENABLE_AUTO_OPTIMIZE=true
SAVE_ORIGINALS=true
SAVE_PARAMETERS=true
# 경로 설정 (기본값 사용 권장)
# GENERATED_IMAGES_PATH=./generated_images
```
## 🎯 Base64 입력 지원 (2025-01-17 업데이트)
### 변경사항
Claude와의 통합을 개선하기 위해 `image_path` 대신 `image_b64`를 사용하도록 변경했습니다.
#### 이전 방식 (image_path 사용)
```python
{
"image_path": "/path/to/image.png",
"prompt": "edit the image"
}
```
#### 현재 방식 (input_image_b64 사용)
```python
{
"input_image_b64": "base64_encoded_string_here", # PNG, JPEG, WebP 등 지원
"prompt": "edit the image",
"background": "transparent", # 선택사항: "transparent" 또는 "opaque"
"save_to_file": true # 선택사항: 파일로 저장 여부
}
```
### 장점
1. **즉시 사용 가능**: Claude가 업로드된 이미지를 바로 처리
2. **경로 문제 해결**: 파일 시스템 경로 찾기 불필요
3. **직접 통합**: Claude의 이미지 인식과 직접 연동
### 구현 패턴
```python
def _save_b64_to_temp_file(self, b64_data: str, base_name: str, index: int, file_type: str = "input") -> str:
"""Base64 데이터를 파일로 저장 (다양한 포맷 지원)"""
# 1. Base64 디코딩
image_data = decode_image_base64(b64_data)
# 2. 이미지 포맷 자동 감지
with Image.open(io.BytesIO(image_data)) as img:
format_ext = img.format.lower() # PNG, JPEG, WEBP 등 감지
# 마스크는 PNG로 변환 (OpenAI API 요구사항)
if file_type == "mask" and format_ext != 'png':
buffer = io.BytesIO()
img.save(buffer, format='PNG')
image_data = buffer.getvalue()
format_ext = 'png'
# 3. 파일명 생성 (원본 포맷 유지)
if file_type == "mask":
filename = f"{base_name}_mask.{format_ext}"
else:
filename = f"{base_name}_{index:03d}.{format_ext}"
# 4. generated_images에 저장
file_path = self.config.generated_images_path / filename
save_image(image_data, str(file_path))
return str(file_path)
```
### 워크플로우
1. Claude가 이미지를 base64로 전송 (원본 포맷 유지: PNG, JPEG, WebP 등)
2. 포맷을 자동 감지하여 `generated_images`에 저장 (입력 추적용)
3. 저장된 파일 경로로 OpenAI API 호출
4. 결과를 저장하고 base64로 반환
### 지원 이미지 포맷
- **입력 이미지**: PNG, JPEG, WebP, GIF, BMP, TIFF
- **마스크 이미지**: PNG (다른 포맷은 자동 변환)
- **출력 이미지**: PNG (OpenAI API 기본값)
## 🛠️ 구현 패턴
### 1. Base Name 생성
```python
def generate_base_name(seed: Optional[int] = None) -> str:
"""gptimage1_{seed}_{yyyymmdd}_{hhmmss} 형식으로 생성"""
if seed is None:
seed = random.randint(0, 999999)
now = datetime.now()
date_str = now.strftime("%Y%m%d")
time_str = now.strftime("%H%M%S")
return f"gptimage1_{seed}_{date_str}_{time_str}"
```
### 2. 파일 저장 패턴
```python
# Base name 생성
seed = self._get_or_create_seed()
base_name = self.config.generate_base_name(seed)
# 입력 파일 저장
input_path = self.config.get_output_path(base_name, 0, 'png')
shutil.copy2(original_image, input_path)
# 출력 파일 저장
output_path = self.config.get_output_path(base_name, 1, 'png')
save_image(edited_image, output_path)
# 파라미터 저장
json_path = self.config.get_output_path(base_name, 1, 'json')
with open(json_path, 'w') as f:
json.dump(params, f, indent=2)
```
### 3. 작업 플로우
```python
async def handle_edit_image(self, arguments):
try:
# 1. 시드 생성/가져오기
seed = self._get_or_create_seed()
base_name = self.config.generate_base_name(seed)
# 2. 입력 파일 저장 (000)
input_path = self.config.get_output_path(base_name, 0, ext)
# 3. 처리 실행
response = await self.client.edit_image(request)
# 4. 출력 파일 저장 (001)
output_path = self.config.get_output_path(base_name, 1, 'png')
# 5. 파라미터 저장 (001.json)
json_path = self.config.get_output_path(base_name, 1, 'json')
finally:
# 6. 세션 종료 시 시드 리셋
self._reset_seed()
```
## 📝 저장되는 파라미터 구조
```json
{
"base_name": "gptimage1_123456_20250824_143022",
"seed": 123456,
"timestamp": "2025-08-24T14:30:22.123456",
"prompt": "make the image more colorful",
"background": "transparent",
"input_image": "generated_images/gptimage1_123456_20250824_143022_000.png",
"input_size": [1024, 768],
"output_size": [1024, 1024],
"execution_time": 3.45,
"optimization": {
"optimized": true,
"original_size_mb": 5.2,
"final_size_mb": 3.8,
"format_used": "PNG"
},
"token_stats": {
"estimated_tokens": 45,
"token_limit": 1000,
"usage_percentage": 4.5
},
"config": {
"model": "gpt-image-1",
"quality": "high",
"api_version": "gpt-image-1"
}
}
```
## 🚀 실행 및 테스트
### 서버 실행
```bash
python main.py
```
### Claude Desktop 설정
`claude_desktop_config.json`:
```json
{
"mcpServers": {
2025-08-26 01:31:42 +09:00
"gpt-edit": {
2025-08-25 00:39:39 +09:00
"command": "python",
2025-08-26 01:31:42 +09:00
"args": ["D:/Project/gpt-edit/main.py"]
2025-08-25 00:39:39 +09:00
}
}
}
```
## ⚠️ 일반적인 문제 해결
### 1. "Method not found" 에러
- `list_prompts()`, `list_resources()` 메서드 구현 확인
2025-08-26 01:31:42 +09:00
- MCP 서버 이름이 단순한지 확인 (예: "gpt-edit")
2025-08-25 00:39:39 +09:00
### 2. JSON 파싱 에러
- 로그가 stdout으로 출력되지 않도록 stderr 사용
- print() 대신 logger 사용
### 3. 파일 명명 충돌
- 타임스탬프와 시드 조합으로 유니크성 보장
- 같은 세션 내에서는 동일 시드 사용
## 📚 핵심 교훈
1. **일관된 명명**: `prefix_seed_date_time_number` 패턴 고수
2. **세션 관리**: 한 작업 세션 동안 동일 시드 유지
3. **단순한 구조**: 모든 파일을 한 디렉토리에 저장
4. **추적 가능성**: 파일명만으로 언제, 어떤 작업인지 파악 가능
5. **자동화**: Base name 생성과 파일 경로 관리 자동화
## 🔄 다중 편집 시나리오
### 같은 이미지를 여러 번 편집
```
# 첫 번째 편집 (seed: 123456)
gptimage1_123456_20250824_143022_000.png # 원본
gptimage1_123456_20250824_143022_001.png # 첫 편집 결과
# 두 번째 편집 (새 seed: 789012)
gptimage1_789012_20250824_143055_000.png # 원본 (복사)
gptimage1_789012_20250824_143055_001.png # 두 번째 편집 결과
```
### 배치 편집
```
# 배치 작업 (seed: 456789)
gptimage1_456789_20250824_144512_001.png # 첫 번째 이미지 결과
gptimage1_456789_20250824_144512_002.png # 두 번째 이미지 결과
gptimage1_456789_20250824_144512_003.png # 세 번째 이미지 결과
```
## ⚠️ API 변경 시 놓치기 쉬운 부분들
### 1. 프롬프트 정의 (list_prompts)
함수명 변경 시 `mcp_server.py`의 프롬프트 정의도 함께 수정해야 합니다:
```python
@self.server.list_prompts()
async def handle_list_prompts():
prompts = [
Prompt(
name="edit_image", # ← 이것도 변경 필요!
description="Edit an image with AI",
```
### 2. 배치 처리 (batch_edit)
`batch_edit`는 여전히 `image_path`를 사용할 수 있습니다.
두 함수의 일관성을 유지하려면 함께 수정해야 합니다.
### 3. 유틸리티 함수
파라미터 타입 변경 시 관련 유틸리티 함수들도 확인:
- `validation.py`: 파라미터 검증 함수
- `image_utils.py`: 이미지 처리 함수
- `token_utils.py`: 토큰 계산 함수
### 4. 테스트 파일
`tests/` 폴더의 모든 테스트 파일들도 업데이트:
- 예시 데이터
- 함수 호출
- 기대값 검증
### 5. 에러 메시지
에러 메시지에서도 변경된 파라미터명 사용:
```python
# Before
return [TextContent(text="image_path is required")]
# After
return [TextContent(text="input_image_b64 is required")]
```
### 6. 로깅 메시지
로깅에서도 일관성 유지:
```python
logger.info(f"Processing edit_image with prompt: {prompt}")
# 함수명이 로그에도 반영되어야 함
```
### 7. 문서의 코드 예시
문서에 있는 코드 예시들도 모두 업데이트:
- README.md의 사용 예시
- TECHNICAL_SPECS.md의 API 스펙
- CLAUDE.md의 구현 패턴
### 8. 통합 테스트 체크리스트
API 변경 후 반드시 실행할 통합 테스트:
☑️ **MCP 서버 테스트**
- [ ] `python main.py` 실행 확인
- [ ] 로그에 에러 없는지 확인
- [ ] Ctrl+C로 정상 종료 확인
☑️ **Claude Desktop 테스트**
- [ ] Claude Desktop 완전 종료 후 재시작
- [ ] "사용 가능한 도구가 무엇인가요?" 질문
- [ ] 변경된 도구명 표시 확인
- [ ] 실제 이미지 업로드 테스트
- [ ] 결과 파일 생성 확인
☑️ **파일 시스템 테스트**
- [ ] `generated_images/` 폴더에 파일 생성 확인
- [ ] 파일명 형식 확인 (base_name 패턴)
- [ ] JSON 파라미터 파일 저장 확인
---
이 가이드를 따르면 일관되고 추적 가능한 파일 구조를 가진 MCP 서버를 개발할 수 있습니다.
**💡 기억하세요: MCP 서버는 양방향 통신입니다. 한쪽만 수정하면 작동하지 않습니다!**