Files
gpt-edit/CLAUDE.md
2025-08-26 01:31:42 +09:00

18 KiB

GPT-Edit MCP Server - 설계 및 개발 가이드

📋 프로젝트 개요

GPT-Edit는 OpenAI의 이미지 편집 API를 MCP(Model Context Protocol) 서버로 구현한 프로젝트입니다. 이 문서는 향후 유사한 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. 세션 기반 시드 관리

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)

    # 예: edit_simple → edit_image
    def get_edit_image_tool() -> Tool:
        return Tool(
            name="edit_image",  # 이름 변경
            inputSchema={
                "properties": {
                    "input_image_b64": {...}  # 파라미터 변경
                }
            }
        )
    
  2. 핸들러 수정 (handlers.py)

    async def handle_edit_image(self, arguments):
        # 새 파라미터 처리 로직
        if 'input_image_b64' not in arguments:  # 변경된 파라미터
    
  3. 서버 라우팅 수정 (mcp_server.py)

    if name == "edit_image":  # 변경된 이름
        return await self.handlers.handle_edit_image(arguments)
    
  4. 테스트 실행

    # 단독 테스트
    python tests/test_server.py
    
    # Claude 연동 테스트
    python main.py
    # Claude Desktop에서 도구 호출 테스트
    

자주 발생하는 실수

하지 말아야 할 것:

  • 한쪽만 수정하고 테스트
  • 프롬프트 이름만 바꾸고 핸들러는 그대로 둠
  • 파라미터 이름 변경 시 validation 로직 미수정

반드시 해야 할 것:

  • 모든 관련 파일 동시 수정
  • 변경 후 즉시 테스트
  • 문서 업데이트

실제 수정 예시: image_path → input_image_b64

변경 이유

Claude가 업로드된 이미지를 바로 처리할 수 있도록 Base64 입력으로 변경

수정한 파일들

  1. src/server/models.py - 도구 정의

    # 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 - 핸들러 로직

    # 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 - 새 함수 추가

    def decode_image_base64(base64_str: str) -> bytes:
        """Decode base64 string to image data"""
        return base64.b64decode(base64_str)
    

디버깅 팁

에러 발생 시 확인 순서

  1. 로그 파일 확인

    tail -f gpt-edit.log
    
  2. Claude Desktop 에러 메시지

    • "Method not found" → 함수명 불일치
    • "Invalid arguments" → 파라미터 문제
    • "Server disconnected" → Python 크래시
  3. 단계별 테스트

    # 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 통신 확인

    # 로깅 추가로 통신 내용 확인
    logger.debug(f"Received: {json.dumps(request, indent=2)}")
    logger.debug(f"Sending: {json.dumps(response, indent=2)}")
    

API 변경 테스트 체크리스트

☑️ 1단계: 단독 테스트

# 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단계: 서버 실행 테스트

python main.py
# 로그에 "Tool called: edit_image" 확인

☑️ 3단계: Claude Desktop 테스트

  1. Claude Desktop 재시작
  2. "사용 가능한 도구가 무엇인가요?" 질문
  3. 변경된 도구명 확인
  4. 실제 호출 테스트

☑️ 4단계: 파라미터 테스트

# 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() - 리소스 목록 반환 (없으면 빈 리스트)

로깅 설정

# stdout과 충돌 방지를 위해 stderr 사용
logging.basicConfig(
    level=logging.INFO,
    handlers=[
        logging.FileHandler('gpt-edit.log', encoding='utf-8'),
        logging.StreamHandler(sys.stderr)  # stderr 사용!
    ]
)

📁 프로젝트 구조

gpt-edit/
├── 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 파일 예시:

# 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 사용)

{
    "image_path": "/path/to/image.png",
    "prompt": "edit the image"
}

현재 방식 (input_image_b64 사용)

{
    "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의 이미지 인식과 직접 연동

구현 패턴

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 생성

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. 파일 저장 패턴

# 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. 작업 플로우

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()

📝 저장되는 파라미터 구조

{
  "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"
  }
}

🚀 실행 및 테스트

서버 실행

python main.py

Claude Desktop 설정

claude_desktop_config.json:

{
  "mcpServers": {
    "gpt-edit": {
      "command": "python",
      "args": ["D:/Project/gpt-edit/main.py"]
    }
  }
}

⚠️ 일반적인 문제 해결

1. "Method not found" 에러

  • list_prompts(), list_resources() 메서드 구현 확인
  • MCP 서버 이름이 단순한지 확인 (예: "gpt-edit")

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의 프롬프트 정의도 함께 수정해야 합니다:

@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. 에러 메시지

에러 메시지에서도 변경된 파라미터명 사용:

# Before
return [TextContent(text="image_path is required")]

# After  
return [TextContent(text="input_image_b64 is required")]

6. 로깅 메시지

로깅에서도 일관성 유지:

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 서버는 양방향 통신입니다. 한쪽만 수정하면 작동하지 않습니다!