はじめに

近年、LLMを活用したエージェント開発において、単一のテキスト対話を超えた「状態の維持」が重要な課題となっています。特に画像生成や動画編集などのマルチモーダルなタスクでは、ユーザーが「もう少し明るくして」「背景を切り替えて」といった微調整を繰り返す際、従来のステートレス(無状態)なAPI呼び出しでは文脈が失われ、一貫した結果を得ることが困難です。

本稿では、Model Context Protocol (MCP) を基盤とし、ステートフルなインタラクションAPIを活用して、LLMエージェント(例: Claude CodeやCodexなど)から直接画像を生成・編集できるツールサーバーを実装する手法について解説します。単なるコードの羅列ではなく、なぜこのアーキテクチャが必要なのか、どのようにセッション状態を管理すべきか、そして実務で直面する課題とその解決策に焦点を当てます。

ステートレスとステートフルの境界:なぜ「記憶」が必要か

従来の画像生成APIは、プロンプトを入力として受け取り、画像URLを返すだけのステートレスな設計が一般的でした。これは処理の独立性を保つ上で有効ですが、クリエイティブな作業においては大きな制限となります。

例えば、サイバーパンクなキッチンの画像を作成した後、「ネオンサインを追加して」と指示した場合、ステートレスなAPIでは、元の画像のコンポジションや照明、キャラクター配置を完全に再記述する必要があります。しかし、モデルは前回生成した画像の詳細を「記憶」していません。そのため、変更前の要素が意図せず消滅したり、一貫性が崩れたりするリスクが高まります。

一方、ステートフルなインタラクションAPIは、サーバー側で視覚的な文脈(コンテキスト)を保持し、複数回のターンにわたって画像を更新することを可能にします。これにより、エージェントは「全体を再生成する」のではなく、「差分や特定の要素に対して指示を出す」という効率的なワークフローを実現できます。このアプローチは、単なる生成ツールを超えて、対話型デザインパートナーとしての可能性を開きます。

MCPプロトコルを用いたツールサーバーの設計

LLMエージェントと外部サービス(今回の場合、画像生成モデル)を連携させるための標準的な方法として、MCPが注目されています。MCPは、エージェントが外部データやツールに安全かつ構造化された方法でアクセスするためのオープンなプロトコルです。

アーキテクチャの概要

実装するシステムは、主に以下の2つのコンポーネントで構成されます。

  • MCP Server: 画像生成モデルとの通信を担当するサーバー。FastMCPなどのフレームワークを使用して、エージェントから呼び出される「ツール(関数)」を公開します。
  • State Manager: 各ユーザーセッションごとの画像IDやコンテキスト情報を保持する状態管理層。これにより、後続の編集リクエストが正しい画像に対して適用されます。

エージェント(クライアント)は、MCPプロトコルを通じて「画像生成」や「画像編集」といったツールを呼び出します。サーバー側では、受信したリクエストに基づいてステートフルなAPIを呼び出し、結果をエージェントに戻します。

コード例:基本的なMCPツールの定義

以下は、PythonとFastMCPを使用して画像生成ツールを定義する最小限の実装例です。ここでは具体的なAPIキーのハードコーディングは避け、環境変数から取得することを想定しています。また、外部APIのエンドポイントやモデル名は実装に応じて変更されるため、仮のものとして記載しています。

import os
from fastmcp import FastMCP
import httpx

# MCPサーバーの初期化
app = FastMCP("image-generation-service")

# 画像生成モデルとの通信用クライアント(簡易版)
class ImageModelClient:
    def __init__(self, api_key: str):
        self.api_key = api_key
        # 実際のAPIプロバイダのベースURLに置き換えてください
        self.base_url = "https://api.example-provider.com/v1" 
    
    async def generate_image(self, prompt: str) -> dict:
        headers = {"Authorization": f"Bearer {self.api_key}"}
        # モデル名やパラメータはAPI仕様に応じて調整してください
        payload = {"prompt": prompt, "model": "gemini-flash-lite-image"}
        
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self.base_url}/images/generate",
                headers=headers,
                json=payload
            )
            response.raise_for_status()
            return response.json()

# 環境変数からAPIキーを取得
API_KEY = os.getenv("IMAGE_MODEL_API_KEY")
client = ImageModelClient(API_KEY)

@app.tool
async def generate_image(prompt: str) -> str:
    """テキストプロンプトから画像を生成し、その結果のURLまたはIDを返します。"""
    result = await client.generate_image(prompt)
    # 実際のAPIレスポンス形式に合わせて処理
    return f"Image generated successfully. ID: {result.get('image_id')}"

# サーバー起動時の設定
if __name__ == "__main__":
    app.run()

ステートフルな編集:セッション管理の実装

単なる生成だけでなく、既存の画像に対する「編集」機能を提供するためには、セッションIDと画像IDのマッピングを維持する必要があります。これがステートフルAPIの真価が発揮される部分です。

状態管理の戦略

エフェメラル(一時的)なストレージ(RedisやMemoryStoreなど)を使用して、各ユーザーのセッション状態を管理します。エージェントからのリクエストには必ずセッションコンテキストが含まれることを前提とします。

import uuid
from typing import Dict, Optional

# 簡易的なメモリ上でのステート管理
class SessionStore:
    def __init__(self):
        self.sessions: Dict[str, str] = {} # session_id -> current_image_id

    def create_session(self) -> str:
        session_id = uuid.uuid4().hex
        self.sessions[session_id] = None
        return session_id

    def update_image(self, session_id: str, image_id: str):
        if session_id in self.sessions:
            self.sessions[session_id] = image_id

    def get_current_image(self, session_id: str) -> Optional[str]:
        return self.sessions.get(session_id)

store = SessionStore()

次に、このステート情報を用いて画像を編集するツールを実装します。モデルのAPIが「image_id」と「modification_prompt」を受け付けることを想定しています。

@app.tool
async def edit_image(session_id: str, modification_prompt: str) -> str:
    """既存の画像に対して変更を加えます。"""
    current_image_id = store.get_current_image(session_id)
    
    if not current_image_id:
        return "Error: No active image in this session. Please generate an image first."

    # ステートフルAPIへの呼び出し
    headers = {"Authorization": f"Bearer {API_KEY}"}
    payload = {
        "image_id": current_image_id,
        "modification": modification_prompt,
        "session_id": session_id
    }
    
    async with httpx.AsyncClient() as client:
        # クライアントのbase_url属性を参照するため、clientインスタンスがImageModelClientと同様の構造を持っている必要があります
        # ここでは簡略化のため、仮のエンドポイントURLを使用しています
        target_url = f"{client.base_url}/images/edit" 
        
        response = await client.post(
            target_url,
            headers=headers,
            json=payload
        )
        response.raise_for_status()
        
        new_image_id = response.json().get('image_id')
        store.update_image(session_id, new_image_id)
        
        return f"Image edited. New ID: {new_image_id}"

実務で直面する課題と対策

このアーキテクチャを実運用レベルに持っていく際、いくつかの重要な設計判断が求められます。

1. セッションの有効期限管理

画像データやセッション情報は永続化すべきではありません。コスト増とプライバシーリスクを避けるため、TTL(Time To Live)を設定したキャッシュストアを使用してください。例えば、Redisを使用して各セッションに24時間の有効期限を設定し、自動的にクリーンアップされるようにします。

2. エラーハンドリングとフォールバック

ステートフルAPIは、中間状態のエラー(例: 編集中に画像が破損した)を起こす可能性があります。この場合、セッションの状態をロールバックするか、あるいは「生成し直し」を促すエラーメッセージを返す必要があります。エージェント側でこれを検知し、適切なアクション(再試行や新しいセッション開始)を取れるよう、エラーコードを明確に定義しましょう。

3. 大規模な画像ファイルの取り扱い

生成された画像自体をセッションストアに保存するのは非効率です。画像はオブジェクトストレージ(S3など)に保存し、ストアにはその参照IDのみを持たせるようにします。また、MCP経由で返すのはURLやメタデータとし、実際のバイナリデータ転送は別のチャネルまたは直接ダウンロードリンク経由で行う設計が一般的です。

エージェント統合とDogfoodingの価値

このツールサーバーを構築した最大の利点は、開発者自身がその恩恵を受けられる点にあります。Claude CodeやCodexのようなコードアシスタントにこのMCPサーバーを接続すると、エディタ内で自然言語による指示だけで画像のプロトタイプを作成・修正できます。

例えば、「左側のメニューの色を青から赤に変更して」という指示に対し、エージェントは内部で現在の状態を把握し、適切なAPI呼び出しを生成します。この「Dogfooding(製品自体を開発者が使うこと)」は、UI/UXの改善やバグ発見において極めて有効です。ステートレスな世界では不可能だった「反復的なデザイン作業」が、エージェントによって自動化・補助されることで、開発速度が飛躍的に向上します。

まとめ

MCPとステートフルなAI APIの組み合わせは、LLMエージェントにおけるマルチモーダルワークフローの未来を切り開く重要な技術です。ステートレスなAPIの限界を超え、文脈を保持した反復的な作業を可能にすることで、より高度で対話型のクリエイティブ支援が可能になります。

実装にあたっては、セッション管理の健全性、エラーハンドリングの堅牢性、そしてリソース効率を優先してください。技術的な詳細よりも、いかにエージェントが「意図」を正確に反映できる状態を維持するかという設計思想が成功の鍵となります。今すぐ既存のプロジェクトにMCPサーバーを追加し、ステートフルなAI体験を実装することから始めてみてはいかがでしょうか。

参考

本記事は海外の技術トレンド「Teaching Claude Code to Paint: A Stateful Image-Editing Skill Built on Gemini's Interactions API and MCP」(DEV)で話題のテーマをもとに、両儀システムソリューションズが独自に解説したものです。