#
tokens: 30011/50000 18/18 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── .python-version
├── docs
│   └── mcp_doubao_lld.md
├── INTEGRATION_GUIDE.md
├── main.py
├── pyproject.toml
├── pytest.ini
├── README.md
├── requirements.txt
├── rules
│   ├── app.md
│   ├── coding.md
│   ├── codingplan.md
│   ├── LLDesign.md
│   └── pages.md
├── src
│   └── mcp_doubao
│       ├── __init__.py
│       ├── __main__.py
│       ├── config.py
│       ├── doubao_client.py
│       ├── downloader.py
│       ├── server.py
│       ├── tools.py
│       └── types.py
├── start_mcp_server.sh
├── tests
│   ├── test_compress_images.py
│   └── test_generate_images.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
3.11

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv
.DS_Store
.claude
.pytest_cache
```

--------------------------------------------------------------------------------
/tests/test_generate_images.py:
--------------------------------------------------------------------------------

```python

```

--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------

```python
def main():
    print("Hello from mcp-doubao!")


if __name__ == "__main__":
    main()

```

--------------------------------------------------------------------------------
/src/mcp_doubao/__main__.py:
--------------------------------------------------------------------------------

```python
"""Entry point for running MCP Doubao as a module."""

from .server import run_stdio

if __name__ == "__main__":
    run_stdio()
```

--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------

```
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
asyncio_mode = auto
```

--------------------------------------------------------------------------------
/start_mcp_server.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# MCP Doubao Server Startup Script
# This script ensures proper environment setup for Claude Code

# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$SCRIPT_DIR"

# Change to project directory
cd "$PROJECT_DIR"

# Export environment variables
export PYTHONPATH="$PROJECT_DIR/src"

# Start the server using the project's virtual environment
exec "$PROJECT_DIR/.venv/bin/python" -m mcp_doubao.server
```

--------------------------------------------------------------------------------
/src/mcp_doubao/config.py:
--------------------------------------------------------------------------------

```python
"""Configuration constants for MCP Doubao integration."""

import os

# Doubao Ark API Configuration
BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
MODEL_ID = "doubao-seedream-4-0-250828"

# API Key for Doubao Ark - loaded from environment variable
ARK_API_KEY = os.getenv("ARK_API_KEY", "")

# Default parameters
DEFAULT_SIZE = "2K"
MAX_IMAGES = 3

# Available image sizes (for reference)
AVAILABLE_SIZES = [
    "1K",      # 1024x1024
    "2K",      # 2048x2048
    "4K",      # 4096x4096
]
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "mcp-doubao"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "httpx>=0.28.1",
    "mcp>=1.14.1",
    "volcengine-python-sdk[ark]>=4.0.21",
    "pillow>=10.0.0",
]

[project.optional-dependencies]
test = [
    "pytest>=7.0.0",
    "pytest-asyncio>=0.21.0",
    "pytest-mock>=3.10.0",
]

[[tool.uv.index]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true

[dependency-groups]
test = [
    "pytest>=8.4.2",
    "pytest-asyncio>=1.2.0",
    "pytest-mock>=3.15.1",
]

```

--------------------------------------------------------------------------------
/src/mcp_doubao/__init__.py:
--------------------------------------------------------------------------------

```python
"""MCP Doubao: A Model Context Protocol server for Doubao image generation."""

__version__ = "0.1.0"
__author__ = "MCP Doubao Team"
__description__ = "MCP server for Doubao AI image generation using Ark SDK"

from .server import app, run_stdio
from .tools import GENERATE_IMAGES_TOOL, handle_generate_images, COMPRESS_IMAGES_TOOL, handle_compress_images
from .doubao_client import DoubaoClient
from .downloader import ImageDownloader
from .types import GenerateImagesRequest, ImageItem, GenerateImagesResponse

__all__ = [
    # Server components
    "app",
    "run_stdio",

    # Tool components
    "GENERATE_IMAGES_TOOL",
    "handle_generate_images",
    "COMPRESS_IMAGES_TOOL",
    "handle_compress_images",

    # Client and downloader
    "DoubaoClient",
    "ImageDownloader",

    # Types
    "GenerateImagesRequest",
    "ImageItem",
    "GenerateImagesResponse",
]
```

--------------------------------------------------------------------------------
/src/mcp_doubao/server.py:
--------------------------------------------------------------------------------

```python
"""MCP server for Doubao image generation."""

import logging
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Resource, Tool

from .tools import GENERATE_IMAGES_TOOL, handle_generate_images, COMPRESS_IMAGES_TOOL, handle_compress_images


# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


# Create MCP server instance
app = Server("mcp-doubao")


@app.list_tools()
async def list_tools() -> list[Tool]:
    """List available tools."""
    return [GENERATE_IMAGES_TOOL, COMPRESS_IMAGES_TOOL]


@app.call_tool()
async def call_tool(name: str, arguments: dict):
    """Handle tool calls."""
    if name == "generate_images":
        return await handle_generate_images(arguments)
    elif name == "compress_images":
        return await handle_compress_images(arguments)
    else:
        raise ValueError(f"Unknown tool: {name}")


async def main():
    """Main entry point for the server."""
    logger.info("Starting MCP Doubao server...")

    # Run the server with stdio transport
    async with stdio_server() as (read_stream, write_stream):
        await app.run(
            read_stream,
            write_stream,
            app.create_initialization_options()
        )


def run_stdio():
    """Run the server with stdio transport (synchronous entry point)."""
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        logger.info("Server stopped by user")
    except Exception as e:
        logger.error(f"Server error: {str(e)}")
        raise


if __name__ == "__main__":
    run_stdio()
```

--------------------------------------------------------------------------------
/src/mcp_doubao/types.py:
--------------------------------------------------------------------------------

```python
"""Data types for MCP Doubao image generation."""

from typing import List
from dataclasses import dataclass


@dataclass
class GenerateImagesRequest:
    """Request for generating images."""
    prompt: str
    num_images: int = 1
    size: str = "2K"
    watermark: bool = True

    def __post_init__(self):
        """Validate request parameters."""
        if not self.prompt or not self.prompt.strip():
            raise ValueError("Prompt cannot be empty")

        if not isinstance(self.num_images, int) or self.num_images < 1 or self.num_images > 3:
            raise ValueError("num_images must be an integer between 1 and 3")

        if not isinstance(self.size, str):
            raise ValueError("size must be a string")

        if not isinstance(self.watermark, bool):
            raise ValueError("watermark must be a boolean")


@dataclass
class ImageItem:
    """Single generated image item."""
    url: str
    size: str

    def __post_init__(self):
        """Validate image item."""
        if not self.url or not isinstance(self.url, str):
            raise ValueError("url must be a non-empty string")

        if not self.size or not isinstance(self.size, str):
            raise ValueError("size must be a non-empty string")


@dataclass
class GenerateImagesResponse:
    """Response containing generated images."""
    images: List[ImageItem]
    count: int

    def __post_init__(self):
        """Validate response."""
        if not isinstance(self.images, list):
            raise ValueError("images must be a list")

        if not isinstance(self.count, int) or self.count < 0:
            raise ValueError("count must be a non-negative integer")

        if self.count != len(self.images):
            raise ValueError("count must match the number of images")
```

--------------------------------------------------------------------------------
/rules/pages.md:
--------------------------------------------------------------------------------

```markdown
---
trigger: model_decision
description: 生成页面README或者理解页面结构
globs: lib/pages/*
---

# Page Documentation Standard

## Directory Structure
- The `lib/pages` directory contains all page implementations of the application
- Each page has its own subdirectory at `lib/pages/{page_name}`
- Each page directory contains a [README.md] file that provides complete documentation for that page

## README.md Content Guidelines
Each page's README.md file should include the following core sections:

### 1. Directory Structure
Clearly list all files and subdirectories within the page directory, with brief descriptions of each file's purpose.

### 2. Page Entry Points
Provide detailed information on how users can access this page, including:
- Navigation paths (which pages can lead to this page)
- Trigger methods (which buttons, menu items to click, etc.)
- Invocation methods (routing methods used, such as `context.pushAppRoute()`)

### 3. Page Components
Describe the main functional areas and components of the page:
- Purpose and content of each area
- Main components and widgets used
- Relationships and interactions between different parts

### 4. Special Considerations
Explain items that require special attention during development and maintenance:
- Design style and standards
- Special requirements for functionality implementation
- Known limitations or areas for improvement
- Dependencies and data sources

## Usage Guidelines
1. **Pre-modification Reading**: Developers must read the README.md file before making any changes to a page
2. **Timely Updates**: After any modification to a page, the README.md file must be updated accordingly
3. **Maintain Consistency**: Ensure documentation descriptions remain consistent with actual code implementation
4. **Use Chinese**: README.md content should be written in Chinese to ensure smooth team communication

By strictly following these standards, we can ensure synchronization between code and documentation, improving team collaboration efficiency and code maintainability.
```

--------------------------------------------------------------------------------
/src/mcp_doubao/doubao_client.py:
--------------------------------------------------------------------------------

```python
"""Doubao Ark SDK client for image generation."""

from typing import List
import logging
from volcenginesdkarkruntime import Ark
from volcenginesdkarkruntime.types.images import SequentialImageGenerationOptions

from .config import BASE_URL, MODEL_ID, ARK_API_KEY
from .types import ImageItem


logger = logging.getLogger(__name__)


class DoubaoClient:
    """Client for interacting with Doubao Ark image generation API."""

    def __init__(self):
        """Initialize the Doubao client."""
        if not ARK_API_KEY or ARK_API_KEY == "REPLACE_WITH_YOUR_KEY":
            raise ValueError(
                "Please set the ARK_API_KEY environment variable. "
                "You can get your API key from https://console.volcengine.com/ark"
            )

        self.client = Ark(
            base_url=BASE_URL,
            api_key=ARK_API_KEY
        )

    def generate_images(
        self,
        prompt: str,
        count: int,
        size: str,
        watermark: bool,
        images: List[str] = None,
        sequential_mode: str = "auto",
        max_images: int = None
    ) -> List[ImageItem]:
        """
        Generate images using Doubao Ark API.

        Args:
            prompt: Text description for image generation
            count: Number of images to generate (1-3)
            size: Image size specification
            watermark: Whether to add watermark
            images: List of base64 encoded reference images (optional)
            sequential_mode: Sequential generation mode ("auto", "true", "false")
            max_images: Max images for sequential generation (overrides count if provided)

        Returns:
            List of ImageItem objects containing URLs and sizes

        Raises:
            Exception: If API call fails or returns unexpected format
        """
        try:
            # Use max_images if provided, otherwise use count
            target_count = max_images if max_images is not None else count

            logger.info(f"Generating {target_count} images with prompt: {prompt[:50]}...")
            if images:
                logger.info(f"Using {len(images)} reference images")

            # Prepare request parameters
            request_params = {
                "model": MODEL_ID,
                "prompt": prompt,
                "size": size,
                "sequential_image_generation": sequential_mode,
                "sequential_image_generation_options": SequentialImageGenerationOptions(
                    max_images=target_count
                ),
                "response_format": "url",
                "watermark": watermark
            }

            # Add reference images if provided
            if images:
                request_params["image"] = images

            logger.debug(f"Request parameters: {request_params}")

            # Call Doubao Ark API
            response = self.client.images.generate(**request_params)

            logger.debug(f"API response: {response}")

            # Parse response
            if not hasattr(response, 'data') or not response.data:
                raise Exception("API response missing data field or data is empty")

            images = []
            for item in response.data:
                if not hasattr(item, 'url'):
                    raise Exception(f"Response item missing url field: {item}")

                # Extract size from response item or use requested size as fallback
                item_size = getattr(item, 'size', size)

                images.append(ImageItem(
                    url=item.url,
                    size=item_size
                ))

            logger.info(f"Successfully generated {len(images)} images")
            return images

        except Exception as e:
            logger.error(f"Error generating images: {str(e)}")
            raise Exception(f"Failed to generate images: {str(e)}")
```

--------------------------------------------------------------------------------
/src/mcp_doubao/downloader.py:
--------------------------------------------------------------------------------

```python
"""Image download functionality for MCP Doubao."""

import os
import logging
from typing import List, Tuple
from urllib.parse import urlparse
import httpx
from pathlib import Path

from .types import ImageItem


logger = logging.getLogger(__name__)


class ImageDownloader:
    """Handles downloading images from URLs to local filesystem."""

    def __init__(self):
        """Initialize the image downloader."""
        self.client = httpx.Client(timeout=30.0)

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        self.client.close()

    def _get_filename_from_url(self, url: str, index: int, output_dir: Path) -> str:
        """
        Generate a unique filename from URL and index, avoiding overwriting existing files.

        Args:
            url: Image URL
            index: Image index for unique naming
            output_dir: Output directory to check for existing files

        Returns:
            Generated unique filename
        """
        parsed_url = urlparse(url)
        path = parsed_url.path

        # Try to extract extension from URL path
        extension = "jpeg"  # default
        if path and '.' in path:
            ext = path.split('.')[-1].lower()
            # Validate common image extensions
            if ext in ['jpg', 'jpeg', 'png', 'gif', 'webp']:
                extension = ext

        # Generate base filename
        base_filename = f"image_{index + 1:03d}"
        filename = f"{base_filename}.{extension}"

        # Check if file exists and generate unique name
        counter = 1
        while (output_dir / filename).exists():
            filename = f"{base_filename}_{counter}.{extension}"
            counter += 1

        return filename

    def _ensure_directory_exists(self, output_dir: str) -> Path:
        """
        Ensure the output directory exists.

        Args:
            output_dir: Directory path

        Returns:
            Path object for the directory

        Raises:
            OSError: If directory cannot be created
        """
        path = Path(output_dir).resolve()

        try:
            path.mkdir(parents=True, exist_ok=True)
            logger.debug(f"Output directory ready: {path}")
            return path
        except OSError as e:
            logger.error(f"Failed to create directory {path}: {e}")
            raise OSError(f"Cannot create output directory {path}: {e}")

    def download_image(self, url: str, filepath: Path) -> bool:
        """
        Download a single image from URL to filepath.

        Args:
            url: Image URL to download
            filepath: Local file path to save to

        Returns:
            True if download successful, False otherwise
        """
        try:
            logger.info(f"Downloading image from: {url}")

            response = self.client.get(url)
            response.raise_for_status()

            # Check if response contains image data
            content_type = response.headers.get('content-type', '')
            if not content_type.startswith('image/'):
                logger.warning(f"URL may not be an image: content-type={content_type}")

            # Write image data to file
            with open(filepath, 'wb') as f:
                f.write(response.content)

            logger.info(f"Successfully downloaded image to: {filepath}")
            return True

        except httpx.HTTPError as e:
            logger.error(f"HTTP error downloading {url}: {e}")
            return False
        except OSError as e:
            logger.error(f"File system error saving to {filepath}: {e}")
            return False
        except Exception as e:
            logger.error(f"Unexpected error downloading {url}: {e}")
            return False

    def download_images(
        self,
        images: List[ImageItem],
        output_dir: str = "."
    ) -> List[Tuple[ImageItem, str, bool]]:
        """
        Download multiple images to specified directory.

        Args:
            images: List of ImageItem objects to download
            output_dir: Directory to save images (default: current directory)

        Returns:
            List of tuples (ImageItem, local_filepath, success_status)
        """
        results = []

        try:
            # Ensure output directory exists
            dir_path = self._ensure_directory_exists(output_dir)

            logger.info(f"Downloading {len(images)} images to: {dir_path}")

            for index, image in enumerate(images):
                # Generate unique filename
                filename = self._get_filename_from_url(image.url, index, dir_path)
                filepath = dir_path / filename

                # Download image
                success = self.download_image(image.url, filepath)

                # Record result
                results.append((image, str(filepath), success))

                if success:
                    logger.info(f"Image {index + 1}/{len(images)} downloaded successfully")
                else:
                    logger.error(f"Image {index + 1}/{len(images)} download failed")

            successful_count = sum(1 for _, _, success in results if success)
            logger.info(f"Download complete: {successful_count}/{len(images)} successful")

            return results

        except Exception as e:
            logger.error(f"Error during batch download: {e}")
            # Return partial results if any downloads were attempted
            return results
```

--------------------------------------------------------------------------------
/tests/test_compress_images.py:
--------------------------------------------------------------------------------

```python
"""Tests for image compression functionality."""

import asyncio
import os
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock

import pytest
from PIL import Image

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from mcp_doubao.tools import handle_compress_images, _compress_single_image


class TestCompressImages:
    """Test cases for compress_images tool."""

    def setup_method(self):
        """Set up test fixtures."""
        self.temp_dir = tempfile.mkdtemp()
        self.temp_path = Path(self.temp_dir)

    def teardown_method(self):
        """Clean up test fixtures."""
        import shutil
        shutil.rmtree(self.temp_dir, ignore_errors=True)

    def create_test_image(self, filename: str, size: tuple = (2048, 2048), format: str = "JPEG") -> Path:
        """Create a test image file."""
        image_path = self.temp_path / filename
        img = Image.new("RGB", size, color="red")
        img.save(image_path, format=format)
        return image_path

    @pytest.mark.asyncio
    async def test_compress_single_image_success(self):
        """Test successful compression of a single image."""
        # Create test image
        input_path = self.create_test_image("test_input.jpg", (2048, 2048))
        output_path = self.temp_path / "test_output.jpg"

        arguments = {
            "input_path": str(input_path),
            "output_path": str(output_path),
            "max_width": 1024,
            "max_height": 1024,
            "quality": 80,
            "format": "JPEG",
            "optimize": True
        }

        result = await handle_compress_images(arguments)

        assert len(result) == 1
        assert "Image compression completed: 1/1 images processed successfully" in result[0].text
        assert output_path.exists()

        # Check that image was resized
        with Image.open(output_path) as img:
            assert img.size == (1024, 1024)

    @pytest.mark.asyncio
    async def test_compress_directory_batch(self):
        """Test batch compression of directory."""
        # Create multiple test images
        self.create_test_image("image1.jpg", (1500, 1500))
        self.create_test_image("image2.png", (2000, 1000))
        self.create_test_image("image3.jpg", (800, 600))

        arguments = {
            "input_path": str(self.temp_path),
            "max_width": 1200,
            "max_height": 800,
            "quality": 85,
            "format": "JPEG"
        }

        result = await handle_compress_images(arguments)

        assert len(result) == 1
        assert "Image compression completed: 3/3 images processed successfully" in result[0].text

        # Check compressed directory exists
        compressed_dir = self.temp_path / "compressed"
        assert compressed_dir.exists()

        # Check all images were compressed (JPEG format saves as .jpeg)
        compressed_files = list(compressed_dir.glob("*_compressed.jpeg"))
        assert len(compressed_files) == 3

    @pytest.mark.asyncio
    async def test_compress_with_aspect_ratio_preservation(self):
        """Test that aspect ratio is preserved during compression."""
        # Create rectangular image
        input_path = self.create_test_image("rect_image.jpg", (3000, 1500))
        output_path = self.temp_path / "rect_output.jpg"

        arguments = {
            "input_path": str(input_path),
            "output_path": str(output_path),
            "max_width": 1200,
            "max_height": 800
        }

        result = await handle_compress_images(arguments)

        assert len(result) == 1
        assert "successfully" in result[0].text

        # Check aspect ratio preserved (should be 1200x600, not 1200x800)
        with Image.open(output_path) as img:
            assert img.size == (1200, 600)  # Maintains 2:1 aspect ratio

    @pytest.mark.asyncio
    async def test_compress_rgba_to_jpeg(self):
        """Test conversion of RGBA image to JPEG."""
        # Create RGBA image with transparency
        input_path = self.temp_path / "rgba_image.png"
        img = Image.new("RGBA", (1000, 1000), color=(255, 0, 0, 128))  # Semi-transparent red
        img.save(input_path, "PNG")

        output_path = self.temp_path / "rgb_output.jpg"

        arguments = {
            "input_path": str(input_path),
            "output_path": str(output_path),
            "format": "JPEG"
        }

        result = await handle_compress_images(arguments)

        assert len(result) == 1
        assert "successfully" in result[0].text

        # Check that output is RGB JPEG
        with Image.open(output_path) as img:
            assert img.mode == "RGB"

    @pytest.mark.asyncio
    async def test_compress_invalid_input_path(self):
        """Test handling of invalid input path."""
        arguments = {
            "input_path": "/nonexistent/path/image.jpg"
        }

        result = await handle_compress_images(arguments)

        assert len(result) == 1
        assert "Error: Invalid parameters" in result[0].text
        assert "Input path does not exist" in result[0].text

    @pytest.mark.asyncio
    async def test_compress_no_output_path_auto_generation(self):
        """Test automatic output path generation."""
        input_path = self.create_test_image("auto_test.jpg")

        arguments = {
            "input_path": str(input_path),
            "max_width": 800,
            "max_height": 600
        }

        result = await handle_compress_images(arguments)

        assert len(result) == 1
        assert "successfully" in result[0].text

        # Check auto-generated output file exists
        expected_output = self.temp_path / "auto_test_compressed.jpg"
        assert expected_output.exists()

    def test_compress_single_image_function(self):
        """Test the _compress_single_image helper function."""
        input_path = self.create_test_image("helper_test.jpg", (1600, 1200))
        output_path = self.temp_path / "helper_output.jpg"

        result = _compress_single_image(
            str(input_path), str(output_path), 800, 600, 90, "JPEG", True
        )

        assert result["success"] is True
        assert result["input"] == str(input_path)
        assert result["output"] == str(output_path)
        assert "KB" in result["original_size"]
        assert "KB" in result["new_size"]
        assert isinstance(result["size_reduction"], float)

        # Check actual compression occurred
        with Image.open(output_path) as img:
            assert img.size == (800, 600)

    @pytest.mark.asyncio
    async def test_compress_different_formats(self):
        """Test compression with different output formats."""
        input_path = self.create_test_image("format_test.jpg")

        for format_type in ["JPEG", "PNG", "WebP"]:
            output_path = self.temp_path / f"format_test.{format_type.lower()}"

            arguments = {
                "input_path": str(input_path),
                "output_path": str(output_path),
                "format": format_type,
                "quality": 80
            }

            result = await handle_compress_images(arguments)

            assert len(result) == 1
            assert "successfully" in result[0].text
            assert output_path.exists()

    @pytest.mark.asyncio
    async def test_compress_size_limits(self):
        """Test compression with various size limits."""
        input_path = self.create_test_image("size_test.jpg", (4000, 3000))

        # Test with small limits
        arguments = {
            "input_path": str(input_path),
            "max_width": 200,
            "max_height": 150,
            "output_path": str(self.temp_path / "small_output.jpg")
        }

        result = await handle_compress_images(arguments)

        assert len(result) == 1
        assert "successfully" in result[0].text

        with Image.open(self.temp_path / "small_output.jpg") as img:
            assert img.size == (200, 150)

    @pytest.mark.asyncio
    async def test_compress_quality_settings(self):
        """Test different quality settings affect file size."""
        input_path = self.create_test_image("quality_test.jpg", (1000, 1000))

        high_quality_path = self.temp_path / "high_quality.jpg"
        low_quality_path = self.temp_path / "low_quality.jpg"

        # High quality
        arguments_high = {
            "input_path": str(input_path),
            "output_path": str(high_quality_path),
            "quality": 95
        }

        # Low quality
        arguments_low = {
            "input_path": str(input_path),
            "output_path": str(low_quality_path),
            "quality": 30
        }

        await handle_compress_images(arguments_high)
        await handle_compress_images(arguments_low)

        # Low quality should produce smaller file
        high_size = high_quality_path.stat().st_size
        low_size = low_quality_path.stat().st_size
        assert low_size < high_size
```

--------------------------------------------------------------------------------
/src/mcp_doubao/tools.py:
--------------------------------------------------------------------------------

```python
"""MCP tool definitions for image generation."""

import base64
import logging
import os
from pathlib import Path
from typing import Any, Dict, List, Optional
from mcp.types import Tool, TextContent
from PIL import Image

from .doubao_client import DoubaoClient
from .types import GenerateImagesRequest, GenerateImagesResponse
from .config import DEFAULT_SIZE, MAX_IMAGES
from .downloader import ImageDownloader


logger = logging.getLogger(__name__)


def _convert_image_to_base64(image_path: str) -> str:
    """
    Convert a local image file to base64 format for API.

    Image requirements:
    - Format: JPEG, PNG only
    - Aspect ratio: [1/3, 3] (width/height)
    - Min size: > 14px (width and height)
    - Max size: 10MB
    - Max pixels: 6000×6000

    Args:
        image_path: Path to the local image file

    Returns:
        Base64 encoded string in format: data:image/<format>;base64,<base64_data>

    Raises:
        FileNotFoundError: If image file doesn't exist
        ValueError: If file is not a valid image or doesn't meet requirements
    """
    image_path_obj = Path(image_path)

    if not image_path_obj.exists():
        raise FileNotFoundError(f"Image file not found: {image_path}")

    if not image_path_obj.is_file():
        raise ValueError(f"Path is not a file: {image_path}")

    # Check file size (max 10MB)
    file_size = image_path_obj.stat().st_size
    max_size_mb = 10
    if file_size > max_size_mb * 1024 * 1024:
        raise ValueError(f"Image file too large: {file_size / (1024 * 1024):.1f}MB (max {max_size_mb}MB)")

    # Get image format from file extension - only JPEG and PNG supported
    suffix = image_path_obj.suffix.lower()
    format_map = {
        '.jpg': 'jpeg',
        '.jpeg': 'jpeg',
        '.png': 'png'
    }

    if suffix not in format_map:
        raise ValueError(f"Unsupported image format: {suffix}. Only JPEG and PNG are supported.")

    image_format = format_map[suffix]

    # Validate image dimensions using PIL
    try:
        with Image.open(image_path) as img:
            width, height = img.size

            # Check minimum size (> 14px for both width and height)
            if width <= 14 or height <= 14:
                raise ValueError(f"Image too small: {width}x{height}px (minimum: 15x15px)")

            # Check maximum pixels (6000x6000)
            if width > 6000 or height > 6000:
                raise ValueError(f"Image too large: {width}x{height}px (maximum: 6000x6000px)")

            # Check aspect ratio [1/3, 3]
            aspect_ratio = width / height
            if aspect_ratio < 1/3 or aspect_ratio > 3:
                raise ValueError(f"Invalid aspect ratio: {aspect_ratio:.2f} (must be between 0.33 and 3.0)")

            logger.info(f"Image validation passed: {width}x{height}px, aspect ratio: {aspect_ratio:.2f}")

    except Exception as e:
        if isinstance(e, ValueError):
            raise  # Re-raise validation errors
        raise ValueError(f"Failed to validate image {image_path}: {str(e)}")

    try:
        # Read and encode the image
        with open(image_path, 'rb') as image_file:
            base64_data = base64.b64encode(image_file.read()).decode('utf-8')
            return f"data:image/{image_format};base64,{base64_data}"
    except Exception as e:
        raise ValueError(f"Failed to read image file {image_path}: {str(e)}")


# Initialize the Doubao client
_doubao_client = None


def get_doubao_client() -> DoubaoClient:
    """Get or create Doubao client instance."""
    global _doubao_client
    if _doubao_client is None:
        _doubao_client = DoubaoClient()
    return _doubao_client


# MCP Tool definition
GENERATE_IMAGES_TOOL = Tool(
    name="generate_images",
    description="Generate images from text prompts using Doubao AI. Supports multi-image generation: text-to-images (up to 15), single image + text (up to 14), or multi-image + text (2-10 input images, total ≤15).",
    inputSchema={
        "type": "object",
        "properties": {
            "prompt": {
                "type": "string",
                "description": "Text description for image generation."
            },
            "num_images": {
                "type": "integer",
                "description": "Number of images to generate (1-3)",
                "minimum": 1,
                "maximum": MAX_IMAGES,
                "default": 1
            },
            "size": {
                "type": "string",
                "description": "Image size specification. Supports two methods (cannot be mixed):\nMethod 1: Resolution preset (1K, 2K, 4K) - specify resolution and describe aspect ratio in prompt, model determines final size.\nMethod 2: Exact pixel dimensions (e.g., 2048x2048) - specify width×height directly.\nDefault: 2048x2048\nPixel range: [1280x720, 4096x4096]\nAspect ratio range: [1/16, 16]\nRecommended sizes:\n1:1 → 2048x2048\n4:3 → 2304x1728\n3:4 → 1728x2304\n16:9 → 2560x1440\n9:16 → 1440x2560\n3:2 → 2496x1664\n2:3 → 1664x2496\n21:9 → 3024x1296",
                "default": DEFAULT_SIZE
            },
            "watermark": {
                "type": "boolean",
                "description": "Whether to add watermark to generated images",
                "default": False
            },
            "output_dir": {
                "type": "string",
                "description": "Absolute path to directory for saving downloaded images. Must be an absolute path (e.g., /Users/username/images or C:\\Users\\username\\images)"
            },
            "image_paths": {
                "type": "array",
                "description": "Optional local file paths to reference images for multi-image generation (1-10 images). Requirements: JPEG/PNG only, size >14px, max 10MB, max 6000×6000px, aspect ratio 1/3-3. Tool auto-converts to base64.",
                "items": {
                    "type": "string",
                    "description": "Absolute path to local image file (JPEG or PNG only)"
                },
                "minItems": 0,
                "maxItems": 10
            },
            "sequential_image_generation": {
                "type": "string",
                "description": "控制是否关闭组图功能。组图:基于您输入的内容,生成的一组内容关联的图片。\n取值范围:\n- auto:自动判断模式,模型会根据用户提供的提示词自主判断是否返回组图以及组图包含的图片数量\n- disabled:关闭组图功能,模型只会生成一张图\n默认值:disabled(默认关闭组图功能)",
                "enum": ["auto", "disabled"],
                "default": "disabled"
            },
            "max_images": {
                "type": "integer",
                "description": "Maximum number of images to generate in sequential mode (1-15). Only effective when sequential_image_generation is set to 'auto'. Total input + output images ≤ 15",
                "minimum": 1,
                "maximum": 15,
                "default": 3
            }
        },
        "required": ["prompt", "output_dir"]
    }
)


# Image compression/resize tool
COMPRESS_IMAGES_TOOL = Tool(
    name="compress_images",
    description="Compress and resize images to optimize for web usage",
    inputSchema={
        "type": "object",
        "properties": {
            "input_path": {
                "type": "string",
                "description": "Path to the input image file or directory containing images"
            },
            "output_path": {
                "type": "string",
                "description": "Path for the output file or directory (optional, defaults to adding '_compressed' suffix)",
                "default": ""
            },
            "max_width": {
                "type": "integer",
                "description": "Maximum width in pixels (maintains aspect ratio)",
                "minimum": 100,
                "maximum": 4096,
                "default": 1920
            },
            "max_height": {
                "type": "integer",
                "description": "Maximum height in pixels (maintains aspect ratio)",
                "minimum": 100,
                "maximum": 4096,
                "default": 1080
            },
            "quality": {
                "type": "integer",
                "description": "JPEG compression quality (1-100, higher = better quality but larger file)",
                "minimum": 1,
                "maximum": 100,
                "default": 100
            },
            "format": {
                "type": "string",
                "description": "Output image format",
                "enum": ["JPEG", "PNG", "WebP"],
                "default": "JPEG"
            },
            "optimize": {
                "type": "boolean",
                "description": "Enable optimization for smaller file size",
                "default": True
            }
        },
        "required": ["input_path"]
    }
)


async def handle_compress_images(arguments: Dict[str, Any]) -> list[TextContent]:
    """
    Handle the compress_images tool call.

    Args:
        arguments: Tool call arguments from MCP client

    Returns:
        List of TextContent containing the compression results

    Raises:
        ValueError: If arguments are invalid
        Exception: If image compression fails
    """
    try:
        input_path = arguments.get("input_path")
        output_path = arguments.get("output_path", "")
        max_width = arguments.get("max_width", 1920)
        max_height = arguments.get("max_height", 1080)
        quality = arguments.get("quality", 85)
        format_type = arguments.get("format", "JPEG")
        optimize = arguments.get("optimize", True)

        logger.info(f"Compressing images: input={input_path}, max_size={max_width}x{max_height}, "
                   f"quality={quality}, format={format_type}")

        input_path_obj = Path(input_path)

        if not input_path_obj.exists():
            raise ValueError(f"Input path does not exist: {input_path}")

        results = []
        processed_count = 0

        if input_path_obj.is_file():
            # Single file processing
            if not output_path:
                output_path = str(input_path_obj.parent / f"{input_path_obj.stem}_compressed{input_path_obj.suffix}")

            result = _compress_single_image(
                str(input_path_obj), output_path, max_width, max_height, quality, format_type, optimize
            )
            results.append(result)
            if result["success"]:
                processed_count += 1

        elif input_path_obj.is_dir():
            # Directory processing
            output_dir = Path(output_path) if output_path else input_path_obj / "compressed"
            output_dir.mkdir(exist_ok=True)

            # Process all image files in directory
            image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp'}
            for file_path in input_path_obj.iterdir():
                if file_path.suffix.lower() in image_extensions:
                    output_file = output_dir / f"{file_path.stem}_compressed.{format_type.lower()}"
                    result = _compress_single_image(
                        str(file_path), str(output_file), max_width, max_height, quality, format_type, optimize
                    )
                    results.append(result)
                    if result["success"]:
                        processed_count += 1
        else:
            raise ValueError(f"Input path is neither a file nor a directory: {input_path}")

        # Format response
        response_lines = [f"Image compression completed: {processed_count}/{len(results)} images processed successfully"]

        for result in results:
            if result["success"]:
                response_lines.append(
                    f"✓ {result['input']} → {result['output']}\n"
                    f"  Size: {result['original_size']} → {result['new_size']} "
                    f"({result['size_reduction']:.1f}% reduction)"
                )
            else:
                response_lines.append(f"✗ {result['input']}: {result['error']}")

        logger.info(f"Image compression completed: {processed_count}/{len(results)} images processed")

        return [TextContent(
            type="text",
            text="\n".join(response_lines)
        )]

    except ValueError as e:
        logger.error(f"Compression validation error: {str(e)}")
        return [TextContent(
            type="text",
            text=f"Error: Invalid parameters - {str(e)}"
        )]

    except Exception as e:
        logger.error(f"Image compression error: {str(e)}")
        return [TextContent(
            type="text",
            text=f"Error: Failed to compress images - {str(e)}"
        )]


def _compress_single_image(input_path: str, output_path: str, max_width: int, max_height: int,
                          quality: int, format_type: str, optimize: bool) -> Dict[str, Any]:
    """
    Compress a single image file.

    Returns:
        Dict containing compression result information
    """
    try:
        # Get original file size
        original_size = os.path.getsize(input_path)

        # Open and process image
        with Image.open(input_path) as img:
            # Convert RGBA to RGB for JPEG format
            if format_type == "JPEG" and img.mode in ("RGBA", "LA", "P"):
                # Create white background
                background = Image.new("RGB", img.size, (255, 255, 255))
                if img.mode == "P":
                    img = img.convert("RGBA")
                background.paste(img, mask=img.split()[-1] if img.mode == "RGBA" else None)
                img = background

            # Calculate new size maintaining aspect ratio
            width, height = img.size
            if width > max_width or height > max_height:
                ratio = min(max_width / width, max_height / height)
                new_width = int(width * ratio)
                new_height = int(height * ratio)
                img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)

            # Save with compression
            save_kwargs = {"optimize": optimize}
            if format_type == "JPEG":
                save_kwargs["quality"] = quality
            elif format_type == "WebP":
                save_kwargs["quality"] = quality

            img.save(output_path, format=format_type, **save_kwargs)

        # Get new file size
        new_size = os.path.getsize(output_path)
        size_reduction = ((original_size - new_size) / original_size) * 100

        return {
            "success": True,
            "input": input_path,
            "output": output_path,
            "original_size": f"{original_size / 1024:.1f} KB",
            "new_size": f"{new_size / 1024:.1f} KB",
            "size_reduction": size_reduction
        }

    except Exception as e:
        return {
            "success": False,
            "input": input_path,
            "output": output_path,
            "error": str(e)
        }


async def handle_generate_images(arguments: Dict[str, Any]) -> list[TextContent]:
    """
    Handle the generate_images tool call.

    Args:
        arguments: Tool call arguments from MCP client

    Returns:
        List of TextContent containing the generation results

    Raises:
        ValueError: If arguments are invalid
        Exception: If image generation fails
    """
    try:
        # Extract and validate arguments
        prompt = arguments.get("prompt")
        num_images = arguments.get("num_images", 1)
        size = arguments.get("size", DEFAULT_SIZE)
        watermark = arguments.get("watermark", False)
        output_dir = arguments.get("output_dir")

        # Validate output_dir is provided and is absolute path
        if not output_dir:
            raise ValueError("output_dir is required")

        output_path = Path(output_dir)
        if not output_path.is_absolute():
            raise ValueError(f"output_dir must be an absolute path, got: {output_dir}")
        image_paths = arguments.get("image_paths", [])
        sequential_mode = arguments.get("sequential_image_generation", "disabled")
        max_images = arguments.get("max_images", 3)

        # Convert local image paths to base64
        base64_images = []
        if image_paths:
            logger.info(f"Converting {len(image_paths)} local images to base64")
            for image_path in image_paths:
                try:
                    base64_image = _convert_image_to_base64(image_path)
                    base64_images.append(base64_image)
                    logger.info(f"Converted image: {image_path}")
                except Exception as e:
                    raise ValueError(f"Failed to convert image {image_path}: {str(e)}")

        logger.info(f"Received generate_images request: prompt='{prompt[:50]}...', "
                   f"num_images={num_images}, size={size}, watermark={watermark}, "
                   f"output_dir='{output_dir}', ref_images={len(base64_images)}, "
                   f"sequential_mode={sequential_mode}, max_images={max_images}")

        # Create and validate request
        request = GenerateImagesRequest(
            prompt=prompt,
            num_images=num_images,
            size=size,
            watermark=watermark
        )

        # Get client and generate images
        client = get_doubao_client()
        images = client.generate_images(
            prompt=request.prompt,
            count=request.num_images,
            size=request.size,
            watermark=request.watermark,
            images=base64_images if base64_images else None,
            sequential_mode=sequential_mode,
            max_images=max_images
        )

        # Create response
        response = GenerateImagesResponse(
            images=images,
            count=len(images)
        )

        # Download images to output directory
        logger.info(f"Downloading {response.count} images to: {output_dir}")

        with ImageDownloader() as downloader:
            download_results = downloader.download_images(response.images, output_dir)

        # Format response for MCP
        response_text_lines = [f"Generated {response.count} images:"]

        for i, (image_item, local_path, success) in enumerate(download_results):
            if success:
                response_text_lines.append(
                    f"Image {i+1}: {image_item.url} (size: {image_item.size})\n"
                    f"  → Downloaded to: {local_path}"
                )
            else:
                response_text_lines.append(
                    f"Image {i+1}: {image_item.url} (size: {image_item.size})\n"
                    f"  → Download failed"
                )

        successful_downloads = sum(1 for _, _, success in download_results if success)
        response_text_lines.append(f"\nDownload summary: {successful_downloads}/{response.count} images saved successfully")

        logger.info(f"Successfully generated and downloaded {successful_downloads}/{response.count} images")

        return [TextContent(
            type="text",
            text="\n".join(response_text_lines)
        )]

    except ValueError as e:
        logger.error(f"Validation error: {str(e)}")
        return [TextContent(
            type="text",
            text=f"Error: Invalid parameters - {str(e)}"
        )]

    except Exception as e:
        logger.error(f"Image generation error: {str(e)}")
        return [TextContent(
            type="text",
            text=f"Error: Failed to generate images - {str(e)}"
        )]
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
# This file was autogenerated by uv via the following command:
#    uv export --format requirements.txt --output-file requirements.txt
annotated-types==0.7.0 \
    --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
    --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
    # via pydantic
anyio==4.10.0 \
    --hash=sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6 \
    --hash=sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1
    # via
    #   httpx
    #   mcp
    #   sse-starlette
    #   starlette
    #   volcengine-python-sdk
attrs==25.3.0 \
    --hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \
    --hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b
    # via
    #   jsonschema
    #   referencing
certifi==2025.8.3 \
    --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \
    --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5
    # via
    #   httpcore
    #   httpx
    #   volcengine-python-sdk
cffi==2.0.0 ; platform_python_implementation != 'PyPy' \
    --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
    --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
    --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \
    --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \
    --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \
    --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \
    --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
    --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \
    --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \
    --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
    --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \
    --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \
    --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \
    --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
    --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \
    --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \
    --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
    --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
    --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
    --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \
    --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
    --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \
    --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \
    --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \
    --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \
    --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
    --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \
    --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \
    --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \
    --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \
    --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \
    --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \
    --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \
    --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
    --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \
    --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
    --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \
    --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \
    --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \
    --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \
    --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \
    --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \
    --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \
    --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
    --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \
    --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \
    --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \
    --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \
    --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \
    --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \
    --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \
    --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \
    --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \
    --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \
    --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
    --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \
    --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \
    --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \
    --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \
    --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5
    # via cryptography
click==8.3.0 ; sys_platform != 'emscripten' \
    --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \
    --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4
    # via uvicorn
colorama==0.4.6 ; sys_platform == 'win32' \
    --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
    --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
    # via click
cryptography==46.0.1 \
    --hash=sha256:0a17377fa52563d730248ba1f68185461fff36e8bc75d8787a7dd2e20a802b7a \
    --hash=sha256:0ca4be2af48c24df689a150d9cd37404f689e2968e247b6b8ff09bff5bcd786f \
    --hash=sha256:0d1922d9280e08cde90b518a10cd66831f632960a8d08cb3418922d83fce6f12 \
    --hash=sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6 \
    --hash=sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080 \
    --hash=sha256:13e67c4d3fb8b6bc4ef778a7ccdd8df4cd15b4bcc18f4239c8440891a11245cc \
    --hash=sha256:15b5fd9358803b0d1cc42505a18d8bca81dabb35b5cfbfea1505092e13a9d96d \
    --hash=sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475 \
    --hash=sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75 \
    --hash=sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead \
    --hash=sha256:34f04b7311174469ab3ac2647469743720f8b6c8b046f238e5cb27905695eb2a \
    --hash=sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a \
    --hash=sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab \
    --hash=sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1 \
    --hash=sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee \
    --hash=sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277 \
    --hash=sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657 \
    --hash=sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2 \
    --hash=sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3 \
    --hash=sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5 \
    --hash=sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28 \
    --hash=sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32 \
    --hash=sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a \
    --hash=sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128 \
    --hash=sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7 \
    --hash=sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca \
    --hash=sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784 \
    --hash=sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e \
    --hash=sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd \
    --hash=sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736 \
    --hash=sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8 \
    --hash=sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a \
    --hash=sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b \
    --hash=sha256:af84e8e99f1a82cea149e253014ea9dc89f75b82c87bb6c7242203186f465129 \
    --hash=sha256:b9c79af2c3058430d911ff1a5b2b96bbfe8da47d5ed961639ce4681886614e70 \
    --hash=sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05 \
    --hash=sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0 \
    --hash=sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b \
    --hash=sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0 \
    --hash=sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8 \
    --hash=sha256:e34da95e29daf8a71cb2841fd55df0511539a6cdf33e6f77c1e95e44006b9b46 \
    --hash=sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0 \
    --hash=sha256:e94eb5fa32a8a9f9bf991f424f002913e3dd7c699ef552db9b14ba6a76a6313b \
    --hash=sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0 \
    --hash=sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7 \
    --hash=sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc \
    --hash=sha256:ef648d2c690703501714588b2ba640facd50fd16548133b11b2859e8655a69da \
    --hash=sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9 \
    --hash=sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef \
    --hash=sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9 \
    --hash=sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7 \
    --hash=sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0
    # via volcengine-python-sdk
h11==0.16.0 \
    --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
    --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
    # via
    #   httpcore
    #   uvicorn
httpcore==1.0.9 \
    --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
    --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
    # via httpx
httpx==0.28.1 \
    --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
    --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
    # via
    #   mcp
    #   mcp-doubao
    #   volcengine-python-sdk
httpx-sse==0.4.1 \
    --hash=sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e \
    --hash=sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37
    # via mcp
idna==3.10 \
    --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
    --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
    # via
    #   anyio
    #   httpx
jsonschema==4.25.1 \
    --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \
    --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85
    # via mcp
jsonschema-specifications==2025.9.1 \
    --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \
    --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d
    # via jsonschema
mcp==1.14.1 \
    --hash=sha256:31c4406182ba15e8f30a513042719c3f0a38c615e76188ee5a736aaa89e20134 \
    --hash=sha256:3b7a479e8e5cbf5361bdc1da8bc6d500d795dc3aff44b44077a363a7f7e945a4
    # via mcp-doubao
pillow==11.3.0 \
    --hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \
    --hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \
    --hash=sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59 \
    --hash=sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50 \
    --hash=sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632 \
    --hash=sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a \
    --hash=sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51 \
    --hash=sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced \
    --hash=sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f \
    --hash=sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12 \
    --hash=sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 \
    --hash=sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6 \
    --hash=sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580 \
    --hash=sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac \
    --hash=sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd \
    --hash=sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722 \
    --hash=sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8 \
    --hash=sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673 \
    --hash=sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788 \
    --hash=sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542 \
    --hash=sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e \
    --hash=sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd \
    --hash=sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8 \
    --hash=sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523 \
    --hash=sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809 \
    --hash=sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477 \
    --hash=sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027 \
    --hash=sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b \
    --hash=sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c \
    --hash=sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e \
    --hash=sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b \
    --hash=sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7 \
    --hash=sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361 \
    --hash=sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae \
    --hash=sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d \
    --hash=sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58 \
    --hash=sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6 \
    --hash=sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024 \
    --hash=sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d \
    --hash=sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f \
    --hash=sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874 \
    --hash=sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa \
    --hash=sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149 \
    --hash=sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6 \
    --hash=sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d \
    --hash=sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd \
    --hash=sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c \
    --hash=sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31 \
    --hash=sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e \
    --hash=sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db \
    --hash=sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494 \
    --hash=sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69 \
    --hash=sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94 \
    --hash=sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77 \
    --hash=sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7 \
    --hash=sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a \
    --hash=sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438 \
    --hash=sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288 \
    --hash=sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b \
    --hash=sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635 \
    --hash=sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3 \
    --hash=sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d \
    --hash=sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe \
    --hash=sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0 \
    --hash=sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805 \
    --hash=sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8 \
    --hash=sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36 \
    --hash=sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e \
    --hash=sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12 \
    --hash=sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d \
    --hash=sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c \
    --hash=sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6 \
    --hash=sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1 \
    --hash=sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653 \
    --hash=sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c \
    --hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \
    --hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3
    # via mcp-doubao
pycparser==2.23 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \
    --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
    --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
    # via cffi
pydantic==2.11.9 \
    --hash=sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2 \
    --hash=sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2
    # via
    #   mcp
    #   pydantic-settings
    #   volcengine-python-sdk
pydantic-core==2.33.2 \
    --hash=sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56 \
    --hash=sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef \
    --hash=sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a \
    --hash=sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f \
    --hash=sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab \
    --hash=sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916 \
    --hash=sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf \
    --hash=sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a \
    --hash=sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7 \
    --hash=sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612 \
    --hash=sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1 \
    --hash=sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7 \
    --hash=sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a \
    --hash=sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7 \
    --hash=sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025 \
    --hash=sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849 \
    --hash=sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b \
    --hash=sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e \
    --hash=sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea \
    --hash=sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac \
    --hash=sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51 \
    --hash=sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e \
    --hash=sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162 \
    --hash=sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65 \
    --hash=sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de \
    --hash=sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc \
    --hash=sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb \
    --hash=sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef \
    --hash=sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1 \
    --hash=sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5 \
    --hash=sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88 \
    --hash=sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290 \
    --hash=sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d \
    --hash=sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc \
    --hash=sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc \
    --hash=sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30 \
    --hash=sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e \
    --hash=sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9 \
    --hash=sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9 \
    --hash=sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f \
    --hash=sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5 \
    --hash=sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab \
    --hash=sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593 \
    --hash=sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1 \
    --hash=sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f \
    --hash=sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8 \
    --hash=sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf \
    --hash=sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246 \
    --hash=sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9 \
    --hash=sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011 \
    --hash=sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6 \
    --hash=sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8 \
    --hash=sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2 \
    --hash=sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6 \
    --hash=sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d
    # via pydantic
pydantic-settings==2.10.1 \
    --hash=sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee \
    --hash=sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796
    # via mcp
python-dateutil==2.9.0.post0 \
    --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
    --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
    # via volcengine-python-sdk
python-dotenv==1.1.1 \
    --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \
    --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab
    # via pydantic-settings
python-multipart==0.0.20 \
    --hash=sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 \
    --hash=sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13
    # via mcp
pywin32==311 ; sys_platform == 'win32' \
    --hash=sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151 \
    --hash=sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87 \
    --hash=sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503 \
    --hash=sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d \
    --hash=sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31 \
    --hash=sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a \
    --hash=sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42 \
    --hash=sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2 \
    --hash=sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee \
    --hash=sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067 \
    --hash=sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852 \
    --hash=sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d
    # via mcp
referencing==0.36.2 \
    --hash=sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa \
    --hash=sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0
    # via
    #   jsonschema
    #   jsonschema-specifications
rpds-py==0.27.1 \
    --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \
    --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \
    --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \
    --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \
    --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \
    --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \
    --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \
    --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \
    --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \
    --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \
    --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \
    --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \
    --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \
    --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \
    --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \
    --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \
    --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \
    --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \
    --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \
    --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \
    --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \
    --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \
    --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \
    --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \
    --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \
    --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \
    --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \
    --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \
    --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \
    --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \
    --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \
    --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \
    --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \
    --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \
    --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \
    --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \
    --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \
    --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \
    --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \
    --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \
    --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \
    --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \
    --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \
    --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \
    --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \
    --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \
    --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \
    --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \
    --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \
    --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \
    --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \
    --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \
    --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \
    --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \
    --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \
    --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \
    --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \
    --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \
    --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \
    --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \
    --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \
    --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \
    --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \
    --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \
    --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \
    --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \
    --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \
    --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \
    --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \
    --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \
    --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \
    --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \
    --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \
    --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \
    --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \
    --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \
    --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \
    --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \
    --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \
    --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \
    --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \
    --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \
    --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \
    --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \
    --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \
    --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \
    --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \
    --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \
    --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \
    --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \
    --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \
    --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \
    --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \
    --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \
    --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \
    --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \
    --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \
    --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \
    --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \
    --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \
    --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21
    # via
    #   jsonschema
    #   referencing
six==1.17.0 \
    --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
    --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
    # via
    #   python-dateutil
    #   volcengine-python-sdk
sniffio==1.3.1 \
    --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
    --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
    # via anyio
sse-starlette==3.0.2 \
    --hash=sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a \
    --hash=sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a
    # via mcp
starlette==0.48.0 \
    --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \
    --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46
    # via mcp
typing-extensions==4.15.0 \
    --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
    --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
    # via
    #   anyio
    #   pydantic
    #   pydantic-core
    #   referencing
    #   starlette
    #   typing-inspection
typing-inspection==0.4.1 \
    --hash=sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51 \
    --hash=sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28
    # via
    #   pydantic
    #   pydantic-settings
urllib3==2.5.0 \
    --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
    --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
    # via volcengine-python-sdk
uvicorn==0.36.0 ; sys_platform != 'emscripten' \
    --hash=sha256:527dc68d77819919d90a6b267be55f0e76704dca829d34aea9480be831a9b9d9 \
    --hash=sha256:6bb4ba67f16024883af8adf13aba3a9919e415358604ce46780d3f9bdc36d731
    # via mcp
volcengine-python-sdk==4.0.21 \
    --hash=sha256:703a42806780b943342c7dd9a5d334d2f423fa16e11387e78859f52a018145c6 \
    --hash=sha256:73256033b2b2c525280e7bf9c829c28b9c8ade527f7c706154c3922663bd77aa
    # via mcp-doubao

```