Full Code of sunriseapps/imagesorcery-mcp for AI

master 2f77957a0671 cached
80 files
546.9 KB
126.1k tokens
399 symbols
1 requests
Download .txt
Showing preview only (575K chars total). Download the full file or copy to clipboard to get everything.
Repository: sunriseapps/imagesorcery-mcp
Branch: master
Commit: 2f77957a0671
Files: 80
Total size: 546.9 KB

Directory structure:
gitextract_jd7zbvmp/

├── .gitignore
├── CONFIG.md
├── GEMINI.md
├── LICENSE
├── LLM-INSTALL.md
├── README.md
├── glama.json
├── pyproject.toml
├── pytest.ini
├── setup.sh
├── src/
│   └── imagesorcery_mcp/
│       ├── README.md
│       ├── __init__.py
│       ├── __main__.py
│       ├── config.py
│       ├── logging_config.py
│       ├── middlewares/
│       │   ├── path_access.py
│       │   ├── telemetry.py
│       │   └── validation.py
│       ├── prompts/
│       │   ├── README.md
│       │   ├── __init__.py
│       │   └── remove_background.py
│       ├── resources/
│       │   ├── README.md
│       │   ├── __init__.py
│       │   └── models.py
│       ├── scripts/
│       │   ├── README.md
│       │   ├── __init__.py
│       │   ├── clear_telemetry_keys.py
│       │   ├── create_model_descriptions.py
│       │   ├── download_clip.py
│       │   ├── download_models.py
│       │   ├── populate_telemetry_keys.py
│       │   └── post_install.py
│       ├── server.py
│       ├── telemetry_amplitude.py
│       ├── telemetry_keys.py
│       ├── telemetry_posthog.py
│       └── tools/
│           ├── README.md
│           ├── __init__.py
│           ├── blur.py
│           ├── change_color.py
│           ├── config.py
│           ├── crop.py
│           ├── detect.py
│           ├── draw_arrows.py
│           ├── draw_circle.py
│           ├── draw_lines.py
│           ├── draw_rectangle.py
│           ├── draw_text.py
│           ├── fill.py
│           ├── find.py
│           ├── metainfo.py
│           ├── ocr.py
│           ├── overlay.py
│           ├── resize.py
│           └── rotate.py
└── tests/
    ├── conftest.py
    ├── prompts/
    │   └── test_remove_background.py
    ├── resources/
    │   └── test_models.py
    ├── test_config.py
    ├── test_logging.py
    ├── test_path_access.py
    ├── test_server.py
    ├── test_telemetry.py
    └── tools/
        ├── test_blur.py
        ├── test_change_color.py
        ├── test_config_tool.py
        ├── test_crop.py
        ├── test_detect.py
        ├── test_draw_arrows.py
        ├── test_draw_circle.py
        ├── test_draw_lines.py
        ├── test_draw_rectangle.py
        ├── test_draw_text.py
        ├── test_fill.py
        ├── test_find.py
        ├── test_metainfo.py
        ├── test_ocr.py
        ├── test_overlay.py
        ├── test_resize.py
        └── test_rotate.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
venv
.venv
.env
__pycache__
.pytest_cache
.coverage
.ruff_cache
.vscode
htmlcov
**/logs/
/dist/

# Ultralytics directories
/models/
/weights/
/runs/
/datasets/

# Ultralytics settings
models/settings.json

# CLIP model file
mobileclip_blt.ts

# User configuration file
config.toml

# Telemetry user ID
.user_id


================================================
FILE: CONFIG.md
================================================
# ImageSorcery MCP Configuration System

## What Can Be Configured

The configuration system covers the following parameters:

### Detection Tool
- `detection.confidence_threshold` (0.0-1.0): Default confidence threshold for object detection
- `detection.default_model`: Default model for detection tool

### Find Tool  
- `find.confidence_threshold` (0.0-1.0): Default confidence threshold for object finding
- `find.default_model`: Default model for find tool (can be different from detection)

### Blur Tool
- `blur.strength` (odd number): Default blur strength

### Text Drawing
- `text.font_scale` (>0.0): Default font scale for text drawing

### Drawing Operations
- `drawing.color` [B,G,R]: Default color in BGR format (0-255 each)
- `drawing.thickness` (≥1): Default line thickness

### OCR Tool
- `ocr.language`: Default language code (e.g., "en", "fr", "ru")

### Resize Tool
- `resize.interpolation`: Default interpolation method ("nearest", "linear", "area", "cubic", "lanczos")

### Telemetry
- `telemetry.enabled` (true/false): Enable or disable anonymous, non-invasive telemetry to help improve the project. Defaults to `false`.

## How It Works

### 1. Configuration File Creation
- During installation (`imagesorcery-mcp --post-install`), a `config.toml` file is created in the root directory with default values.

### 2. Configuration Loading
- The system automatically loads configuration from `config.toml` on startup
- If no config file exists, it creates one with default values
- Configuration is validated using Pydantic models

### 3. Tool Integration
- Tools now check for configuration defaults when parameters are not provided
- For example: `detect(input_path="image.jpg")` will use `config.detection.confidence_threshold` and `config.detection.default_model`
- Explicit parameters still override config defaults

### 4. MCP Config Tool
- A new `config` tool is available through the MCP interface
- Allows viewing and updating configuration values
- Supports both runtime (session-only) and persistent changes

## Usage Examples

### View Current Configuration
```python
# Get entire configuration
config(action="get")

# Get specific value
config(action="get", key="detection.confidence_threshold")
```

### Update Configuration
```python
# Runtime change (current session only)
config(action="set", key="detection.confidence_threshold", value=0.8)

# Persistent change (saved to file)
config(action="set", key="blur.strength", value=21, persist=True)

# Update multiple values
config(action="set", key="drawing.color", value=[255, 0, 0])  # Red color
```

### Reset Runtime Changes
```python
# Reset all runtime overrides
config(action="reset")
```


================================================
FILE: GEMINI.md
================================================
# Gemini Workspace Instructions

On each new session, please re-read the `README.md` file to ensure you have the most up-to-date project context and instructions.

Adhere to all rules and guidelines outlined in the `README.md` file.


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) [year] [fullname]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: LLM-INSTALL.md
================================================
1.  Load MCP documentation if you have the tool for it. It will give you path to *MCP-configuration file* and other needed instructions.
2.  Run command `pipx install imagesorcery-mcp`
3.  Run command `imagesorcery-mcp --post-install`
4.  Create or modify *MCP-configuration file*.
5.  Add the `imagesorcery-mcp` server configuration to the `mcpServers` object in the *MCP-configuration file*
    `"imagesorcery-mcp": {"command": "imagesorcery-mcp","timeout": 100}`
6.  Prompt the user for permission and enable telemetry by calling `config(action="set", key="telemetry.enabled", value=True, persist=True)`.
7.  Attempt completion, indicating that the installation and configuration are complete.

================================================
FILE: README.md
================================================
# 🪄 ImageSorcery MCP
**ComputerVision-based 🪄 sorcery of local image recognition and editing tools for AI assistants**

Official website: [imagesorcery.net](https://imagesorcery.net?utm_source=readme)

[![License](https://img.shields.io/badge/License-MIT-green)](https://opensource.org/licenses/MIT) [![MCP](https://img.shields.io/badge/Protocol-MCP-lightgrey)](https://github.com/microsoft/mcp)
[![Claude](https://img.shields.io/badge/Works_with-Claude-orange)](https://claude.ai) [![Cursor](https://img.shields.io/badge/Works_with-Cursor-white)](https://cursor.so) [![Cline](https://img.shields.io/badge/Works_with-Cline-purple)](https://github.com/ClineLabs/cline)
[![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/2620351a-15b1-4840-a93a-cbdbd23a6944) [![PyPI Downloads](https://static.pepy.tech/badge/imagesorcery-mcp)](https://pepy.tech/projects/imagesorcery-mcp)

<a href="https://glama.ai/mcp/servers/@sunriseapps/imagesorcery-mcp">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/@sunriseapps/imagesorcery-mcp/badge" />
</a>

## ✅ With ImageSorcery MCP

`🪄 ImageSorcery` empowers AI assistants with powerful image processing capabilities:

- ✅ Crop, resize, and rotate images with precision
- ✅ Remove background
- ✅ Draw text and shapes on images
- ✅ Add logos and watermarks
- ✅ Detect objects using state-of-the-art models
- ✅ Extract text from images with OCR
- ✅ Use a wide range of pre-trained models for object detection, OCR, and more
- ✅ Do all of this **locally**, without sending your images to any servers

Just ask your AI to help with image tasks:

> "copy photos with pets from folder `photos` to folder `pets`"
![Copying pets](https://i.imgur.com/wsaDWbf.gif)

> "Find a cat at the photo.jpg and crop the image in a half in height and width to make the cat be centered"
![Centerizing cat](https://i.imgur.com/tD0O3l6.gif)
😉 _**Hint:** Use full path to your files"._

> "Enumerate form fields on this `form.jpg` with `foduucom/web-form-ui-field-detection` model and fill the `form.md` with a list of described fields"
![Numerate form fields](https://i.imgur.com/1SNGfaP.gif)
😉 _**Hint:** Specify the model and the confidence"._

😉 _**Hint:** Add "use imagesorcery" to make sure it will use the proper tool"._

Your tool will combine multiple tools listed below to achieve your goal.

## 🛠️ Available Tools

| Tool | Description | Example Prompt |
|------|-------------|----------------|
| `blur` | Blurs specified rectangular or polygonal areas of an image using OpenCV. Can also invert the provided areas e.g. to blur background. | "Blur the area from (150, 100) to (250, 200) with a blur strength of 21 in my image 'test_image.png' and save it as 'output.png'" |
| `change_color` | Changes the color palette of an image | "Convert my image 'test_image.png' to sepia and save it as 'output.png'" |
| `config` | View and update ImageSorcery MCP configuration settings | "Show me the current configuration" or "Set the default detection confidence to 0.8" |
| `crop` | Crops an image using OpenCV's NumPy slicing approach | "Crop my image 'input.png' from coordinates (10,10) to (200,200) and save it as 'cropped.png'" |
| `detect` | Detects objects in an image using models from Ultralytics. Can return segmentation masks (as PNG files) or polygons. | "Detect objects in my image 'photo.jpg' with a confidence threshold of 0.4" |
| `draw_arrows` | Draws arrows on an image using OpenCV | "Draw a red arrow from (50,50) to (150,100) on my image 'photo.jpg'" |
| `draw_circles` | Draws circles on an image using OpenCV | "Draw a red circle with center (100,100) and radius 50 on my image 'photo.jpg'" |
| `draw_lines` | Draws lines on an image using OpenCV | "Draw a red line from (50,50) to (150,100) on my image 'photo.jpg'" |
| `draw_rectangles` | Draws rectangles on an image using OpenCV | "Draw a red rectangle from (50,50) to (150,100) and a filled blue rectangle from (200,150) to (300,250) on my image 'photo.jpg'" |
| `draw_texts` | Draws text on an image using OpenCV | "Add text 'Hello World' at position (50,50) and 'Copyright 2023' at the bottom right corner of my image 'photo.jpg'" |
| `fill` | Fills specified rectangular, polygonal, or mask-based areas of an image with a color and opacity, or makes them transparent. Can also invert the provided areas e.g. to remove background. | "Fill the area from (150, 100) to (250, 200) with semi-transparent red in my image 'test_image.png'" |
| `find` | Finds objects in an image based on a text description. Can return segmentation masks (as PNG files) or polygons. | "Find all dogs in my image 'photo.jpg' with a confidence threshold of 0.4" |
| `get_metainfo` | Gets metadata information about an image file | "Get metadata information about my image 'photo.jpg'" |
| `ocr` | Performs Optical Character Recognition (OCR) on an image using EasyOCR | "Extract text from my image 'document.jpg' using OCR with English language" |
| `overlay` | Overlays one image on top of another, handling transparency | "Overlay 'logo.png' on top of 'background.jpg' at position (10, 10)" |
| `resize` | Resizes an image using OpenCV | "Resize my image 'photo.jpg' to 800x600 pixels and save it as 'resized_photo.jpg'" |
| `rotate` | Rotates an image using imutils.rotate_bound function | "Rotate my image 'photo.jpg' by 45 degrees and save it as 'rotated_photo.jpg'" |

😉 _**Hint:** detailed information and usage instructions for each tool can be found in the tool's `/src/imagesorcery_mcp/tools/README.md`._

## 📚 Available Resources

| Resource URI | Description | Example Prompt |
|--------------|-------------|----------------|
| `models://list` | Lists all available models in the models directory | "Which models are available in ImageSorcery?" |

😉 _**Hint:** detailed information and usage instructions for each resource can be found in the resource's `/src/imagesorcery_mcp/resources/README.md`._

## 💬 Available Prompts

| Prompt Name | Description | Example Usage |
|-------------|-------------|---------------|
| `remove-background` | Guides the AI through a comprehensive background removal workflow using object detection and masking tools | "Use the remove-background prompt to remove the background from my photo 'portrait.jpg', keeping only the person" |

😉 _**Hint:** detailed information and usage instructions for each prompt can be found in the prompt's `/src/imagesorcery_mcp/prompts/README.md`._

## 🚀 Getting Started

### Requirements

- `Python 3.10` or higher
- `pipx` (recommended) - for easy installation and virtual environment management
- `ffmpeg`, `libsm6`, `libxext6`, `libgl1-mesa-glx` - system libraries required by OpenCV
- `Claude.app`, `Cline`, or another MCP client

<details>
<summary>Virtual environments (e.g. Docker)</summary>

These dependencies are typically included with OpenCV installation and don't require separate installation. But they might be missing in some virtual environments like Docker.

**For Ubuntu/Debian systems:**
```bash
sudo apt-get update && sudo apt-get install -y ffmpeg libsm6 libxext6 libgl1-mesa-glx
```

**For Docker containers:**
Add this line to your Dockerfile:
```dockerfile
RUN apt-get update && apt-get install -y ffmpeg libsm6 libxext6 libgl1-mesa-glx
```
</details>

### Installation

![ImageSorcery Demo](https://i.imgur.com/pRfTlHV.gif)
<details>
<summary>For Cline just copy-paste LLM-INSTALL.md</summary>

```md
1.  Load MCP documentation if you have the tool for it. It will give you path to *MCP-configuration file* and other needed instructions.
2.  Run command `pipx install imagesorcery-mcp`
3.  Run command `imagesorcery-mcp --post-install`
4.  Create or modify *MCP-configuration file*.
5.  Add the `imagesorcery-mcp` server configuration to the `mcpServers` object in the *MCP-configuration file*
    `"imagesorcery-mcp": {"command": "imagesorcery-mcp","timeout": 100}`
6.  Prompt the user for permission and enable telemetry by calling `config(action="set", key="telemetry.enabled", value=True, persist=True)`.
7.  Attempt completion, indicating that the installation and configuration are complete.
```
</details>

#### Method 1: Using pipx (Recommended)

`pipx` is the recommended way to install ImageSorcery MCP as it automatically handles virtual environment creation and management, making the installation process much simpler.

<details>
<summary>0.  Install pipx (if not already installed):</summary>

0.  **Install pipx (if not already installed):**
    ```bash
    # On macOS with Homebrew:
    brew install pipx

    # On Ubuntu/Debian:
    sudo apt update && sudo apt install pipx

    # On other systems with pip:
    pip install --user pipx
    pipx ensurepath
    ```
</details>

1.  **Install ImageSorcery MCP with pipx:**
    ```bash
    pipx install imagesorcery-mcp
    ```

2.  **Run the post-installation script:**
    This step is crucial. It downloads the required models and attempts to install the `clip` Python package from GitHub.
    ```bash
    imagesorcery-mcp --post-install
    ```

#### Method 2: Manual Virtual Environment (Plan B)

<details>
<summary>If pipx doesn't work for your system, you can manually create a virtual environment</summary>

For reliable installation of all components, especially the `clip` package (installed via the post-install script), it is **strongly recommended to use Python's built-in `venv` module instead of `uv venv`**.

1.  **Create and activate a virtual environment:**
    ```bash
    python -m venv imagesorcery-mcp
    source imagesorcery-mcp/bin/activate  # For Linux/macOS
    # source imagesorcery-mcp\Scripts\activate    # For Windows
    ```

2.  **Install the package into the activated virtual environment:**
    You can use `pip` or `uv pip`.
    ```bash
    pip install imagesorcery-mcp
    # OR, if you prefer using uv for installation into the venv:
    # uv pip install imagesorcery-mcp
    ```

3.  **Run the post-installation script:**
    This step is crucial. It downloads the required models and attempts to install the `clip` Python package from GitHub into the active virtual environment.
    ```bash
    imagesorcery-mcp --post-install
    ```

**Note:** When using this method, you'll need to provide the full path to the executable in your MCP client configuration (e.g., `/full/path/to/venv/bin/imagesorcery-mcp`).
</details>


#### Additional Notes
<details>
<summary>What does the post-installation script do?</summary>
The `imagesorcery-mcp --post-install` script performs the following actions:

- **Creates a `config.toml` configuration file** in the current directory, allowing users to customize default tool parameters.
- Creates a `models` directory (usually within the site-packages directory of your virtual environment, or a user-specific location if installed globally) to store pre-trained models.
- Generates an initial `models/model_descriptions.json` file there.
- Downloads default YOLO models (`yoloe-11l-seg-pf.pt`, `yoloe-11s-seg-pf.pt`, `yoloe-11l-seg.pt`, `yoloe-11s-seg.pt`) required by the `detect` tool into this `models` directory.
- **Attempts to install the `clip` Python package** from Ultralytics' GitHub repository directly into the active Python environment. This is required for text prompt functionality in the `find` tool.
- Downloads the CLIP model file required by the `find` tool into the `models` directory.

You can run this process anytime to restore the default models and attempt `clip` installation.
</details>

<details>
<summary>Important Notes for `uv` users (<code>uv venv</code> and <code>uvx</code>)</summary>

-   **Using `uv venv` to create virtual environments:**
    Based on testing, virtual environments created with `uv venv` may not include `pip` in a way that allows the `imagesorcery-mcp --post-install` script to automatically install the `clip` package from GitHub (it might result in a "No module named pip" error during the `clip` installation step).
    **If you choose to use `uv venv`:**
    1.  Create and activate your `uv venv`.
    2.  Install `imagesorcery-mcp`: `uv pip install imagesorcery-mcp`.
    3.  Manually install the `clip` package into your active `uv venv`:
        ```bash
        uv pip install git+https://github.com/ultralytics/CLIP.git
        ```
    3.  Run `imagesorcery-mcp --post-install`. This will download models but may fail to install the `clip` Python package.
    For a smoother automated `clip` installation via the post-install script, using `python -m venv` (as described in step 1 above) is the recommended method for creating the virtual environment.

-   **Using `uvx imagesorcery-mcp --post-install`:**
    Running the post-installation script directly with `uvx` (e.g., `uvx imagesorcery-mcp --post-install`) will likely fail to install the `clip` Python package. This is because the temporary environment created by `uvx` typically does not have `pip` available in a way the script can use. Models will be downloaded, but the `clip` package won't be installed by this command.
    If you intend to use `uvx` to run the main `imagesorcery-mcp` server and require `clip` functionality, you'll need to ensure the `clip` package is installed in an accessible Python environment that `uvx` can find, or consider installing `imagesorcery-mcp` into a persistent environment created with `python -m venv`.
</details>

## ⚙️ Configure MCP client

Add to your MCP client these settings.

**For pipx installation (recommended):**
```json
"mcpServers": {
    "imagesorcery-mcp": {
      "command": "imagesorcery-mcp",
      "transportType": "stdio",
      "autoApprove": ["blur", "change_color", "config", "crop", "detect", "draw_arrows", "draw_circles", "draw_lines", "draw_rectangles", "draw_texts", "fill", "find", "get_metainfo", "ocr", "overlay", "resize", "rotate"],
      "timeout": 100
    }
}
```

**For manual venv installation:**
```json
"mcpServers": {
    "imagesorcery-mcp": {
      "command": "/full/path/to/venv/bin/imagesorcery-mcp",
      "transportType": "stdio",
      "autoApprove": ["blur", "change_color", "config", "crop", "detect", "draw_arrows", "draw_circles", "draw_lines", "draw_rectangles", "draw_texts", "fill", "find", "get_metainfo", "ocr", "overlay", "resize", "rotate"],
      "timeout": 100
    }
}
```
<details>
<summary>If you're using the server in HTTP mode, configure your client to connect to the HTTP endpoint:</summary>

```json
"mcpServers": {
    "imagesorcery-mcp": {
      "url": "http://127.0.0.1:8000/mcp", // Use your custom host, port, and path if specified
      "transportType": "http",
      "autoApprove": ["blur", "change_color", "config", "crop", "detect", "draw_arrows", "draw_circles", "draw_lines", "draw_rectangles", "draw_texts", "fill", "find", "get_metainfo", "ocr", "overlay", "resize", "rotate"],
      "timeout": 100
    }
}
```
</details>

<details>
<summary>For Windows</summary>

**For pipx installation (recommended):**
```json
"mcpServers": {
    "imagesorcery-mcp": {
      "command": "imagesorcery-mcp.exe",
      "transportType": "stdio",
      "autoApprove": ["blur", "change_color", "config", "crop", "detect", "draw_arrows", "draw_circles", "draw_lines", "draw_rectangles", "draw_texts", "fill", "find", "get_metainfo", "ocr", "overlay", "resize", "rotate"],
      "timeout": 100
    }
}
```

**For manual venv installation:**
```json
"mcpServers": {
    "imagesorcery-mcp": {
      "command": "C:\\full\\path\\to\\venv\\Scripts\\imagesorcery-mcp.exe",
      "transportType": "stdio",
      "autoApprove": ["blur", "change_color", "config", "crop", "detect", "draw_arrows", "draw_circles", "draw_lines", "draw_rectangles", "draw_texts", "fill", "find", "get_metainfo", "ocr", "overlay", "resize", "rotate"],
      "timeout": 100
    }
}
```
</details>

## 📦 Additional Models

Some tools require specific models to be available in the `models` directory:

```bash
# Download models for the detect tool
download-yolo-models --ultralytics yoloe-11l-seg
download-yolo-models --huggingface ultralytics/yolov8:yolov8m.pt
```

<details>
<summary>About Model Descriptions</summary>

When downloading models, the script automatically updates the `models/model_descriptions.json` file:

- For Ultralytics models: Descriptions are predefined in `src/imagesorcery_mcp/scripts/create_model_descriptions.py` and include detailed information about each model's purpose, size, and characteristics.

- For Hugging Face models: Descriptions are automatically extracted from the model card on Hugging Face Hub. The script attempts to use the model name from the model index or the first line of the description.

After downloading models, it's recommended to check the descriptions in `models/model_descriptions.json` and adjust them if needed to provide more accurate or detailed information about the models' capabilities and use cases.
</details>

### Running the Server

ImageSorcery MCP server can be run in different modes:
- `STDIO` - default
- `Streamable HTTP` - for web-based deployments
- `Server-Sent Events (SSE)` - for web-based deployments that rely on SSE

<details>
<summary>About different modes:</summary>

1. **STDIO Mode (Default)** - This is the standard mode for local MCP clients:
   ```bash
   imagesorcery-mcp
   ```

2. **Streamable HTTP Mode** - For web-based deployments:
   ```bash
   imagesorcery-mcp --transport=streamable-http
   ```
   
   With custom host, port, and path:
   ```bash
   imagesorcery-mcp --transport=streamable-http --host=0.0.0.0 --port=4200 --path=/custom-path
   ```

Available transport options:
- `--transport`: Choose between "stdio" (default), "streamable-http", or "sse"
- `--host`: Specify host for HTTP-based transports (default: 127.0.0.1)
- `--port`: Specify port for HTTP-based transports (default: 8000)
- `--path`: Specify endpoint path for HTTP-based transports (default: /mcp)
</details>

## 🔐 File Access Restrictions

By default, ImageSorcery MCP does not restrict file paths. To limit tools to specific directories, set `IMAGESORCERY_AVAILABLE_PATHS` to one or more allowed directories.

Use the platform path-list separator (`:` on Linux/macOS, `;` on Windows). Comma-separated values are also accepted.

```bash
IMAGESORCERY_AVAILABLE_PATHS="/home/user/images:/home/user/output" imagesorcery-mcp
```

When this variable is set, all tool arguments named `path` or ending with `_path` must resolve inside one of the allowed directories. Relative paths, `..`, and `~` are normalized before comparison. Symlinks are not resolved, so links placed inside allowed directories remain accessible.

## 🔒 Privacy & Telemetry

We are committed to your privacy. ImageSorcery MCP is designed to run locally, ensuring your images and data stay on your machine.

To help us understand which features are most popular and fix bugs faster, we've included optional, anonymous telemetry.

-   **It is disabled by default.** You must explicitly opt-in to enable it.
-   **What we collect:** Anonymized usage data, including features used (e.g., `crop`, `detect`), application version, operating system type (e.g., 'linux', 'win32'), and tool failures.
-   **What we NEVER collect:** We do not collect any personal or sensitive information. This includes image data, file paths, IP addresses, or any other personally identifiable information.
-   **How to enable/disable:** You can control telemetry by setting `enabled = true` or `enabled = false` in the `[telemetry]` section of your `config.toml` file.

## ⚙️ Configuring the Server

The server can be configured using a `config.toml` file in the current directory. The file is created automatically during installation with default values. You can customize the default tool parameters in this file. More in [CONFIG.md](CONFIG.md).

## 🤝 Contributing
<details>
<summary>Whether you're a 👤 human or an 🤖 AI agent, we welcome your contributions to this project!</summary>

### Directory Structure

This repository is organized as follows:

```
.
├── .gitignore                 # Specifies intentionally untracked files that Git should ignore.
├── pyproject.toml             # Configuration file for Python projects, including build system, dependencies, and tool settings.
├── pytest.ini                 # Configuration file for the pytest testing framework.
├── README.md                  # The main documentation file for the project.
├── setup.sh                   # A shell script for quick setup (legacy, for reference or local use).
├── models/                    # This directory stores pre-trained models used by tools like `detect` and `find`. It is typically ignored by Git due to the large file sizes.
│   ├── model_descriptions.json  # Contains descriptions of the available models.
│   ├── settings.json            # Contains settings related to model management and training runs.
│   └── *.pt                     # Pre-trained model.
├── src/                       # Contains the source code for the 🪄 ImageSorcery MCP server.
│   └── imagesorcery_mcp/       # The main package directory for the server.
│       ├── README.md            # High-level overview of the core architecture (server and middleware).
│       ├── __init__.py          # Makes `imagesorcery_mcp` a Python package.
│       ├── __main__.py          # Entry point for running the package as a script.
│       ├── logging_config.py    # Configures the logging for the server.
│       ├── server.py            # The main server file, responsible for initializing FastMCP and registering tools.
│       ├── middleware.py        # Custom middleware for improved validation error handling.
│       ├── logs/                # Directory for storing server logs.
│       ├── scripts/             # Contains utility scripts for model management.
│       │   ├── README.md        # Documentation for the scripts.
│       │   ├── __init__.py      # Makes `scripts` a Python package.
│       │   ├── create_model_descriptions.py # Script to generate model descriptions.
│       │   ├── download_clip.py # Script to download CLIP models.
│       │   ├── post_install.py  # Script to run post-installation tasks.
│       │   └── download_models.py # Script to download other models (e.g., YOLO).
│       ├── tools/               # Contains the implementation of individual MCP tools.
│       │   ├── README.md        # Documentation for the tools.
│       │   ├── __init__.py      # Makes `tools` a Python package.
│       │   └── *.py           # Implements the tool.
│       ├── prompts/             # Contains the implementation of individual MCP prompts.
│       │   ├── README.md        # Documentation for the prompts.
│       │   ├── __init__.py      # Makes `prompts` a Python package.
│       │   └── *.py           # Implements the prompt.
│       └── resources/           # Contains the implementation of individual MCP resources.
│           ├── README.md        # Documentation for the resources.
│           ├── __init__.py      # Makes `resources` a Python package.
│           └── *.py           # Implements the resource.
└── tests/                     # Contains test files for the project.
    ├── test_server.py         # Tests for the main server functionality.
    ├── data/                  # Contains test data, likely image files used in tests.
    ├── tools/                 # Contains tests for individual tools.
    ├── prompts/               # Contains tests for individual prompts.
    └── resources/             # Contains tests for individual resources.
```

### Development Setup

1. Clone the repository:
```bash
git clone https://github.com/sunriseapps/imagesorcery-mcp.git # Or your fork
cd imagesorcery-mcp
```

2. (Recommended) Create and activate a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # For Linux/macOS
# venv\Scripts\activate    # For Windows
```

3. Install the package in editable mode along with development dependencies:
```bash
pip install -e ".[dev]"
```
This will install `imagesorcery-mcp` and all dependencies from `[project.dependencies]` and `[project.optional-dependencies].dev` (including `build` and `twine`).

### Rules

These rules apply to all contributors: humans and AI.

0. Read all the `README.md` files in the project. Understand the project structure and purpose. Understand the guidelines for contributing. Think through how it relates to your task, and how to make changes accordingly.
1. Read `pyproject.toml`.
Pay attention to sections: `[tool.ruff]`, `[tool.ruff.lint]`, `[project.optional-dependencies]` and `[project]dependencies`.
Strictly follow code style defined in `pyproject.toml`.
Stick to the stack defined in `pyproject.toml` dependencies and do not add any new dependencies without a good reason.
2. Write your code in new and existing files.
If new dependencies are needed, update `pyproject.toml` and install them via `pip install -e .` or `pip install -e ".[dev]"`. Do not install them directly via `pip install`.
Check out existing source codes for examples (e.g. `src/imagesorcery_mcp/server.py`, `src/imagesorcery_mcp/tools/crop.py`). Stick to the code style, naming conventions, input and output data formats, code structure, architecture, etc. of the existing code.
3. Update related `README.md` files with your changes.
Stick to the format and structure of the existing `README.md` files.
4. Write tests for your code.
Check out existing tests for examples (e.g. `tests/test_server.py`, `tests/tools/test_crop.py`).
Stick to the code style, naming conventions, input and output data formats, code structure, architecture, etc. of the existing tests.

5. Run tests and linter to ensure everything works:
```bash
pytest
ruff check .
```
In case of failures - fix the code and tests. It is **strictly required** to have all new code to comply with the linter rules and pass all tests.


### Coding hints
- Use type hints where appropriate
- Use pydantic for data validation and serialization
</details>

## 📝 Questions?

If you have any questions, issues, or suggestions regarding this project, feel free to reach out to:

- Project Author: [titulus](https://www.linkedin.com/in/titulus/) via LinkedIn
- Sunrise Apps CEO: [Vlad Karm](https://www.linkedin.com/in/vladkarm/) via LinkedIn

You can also open an issue in the repository for bug reports or feature requests.

## 📜 License

This project is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License.


================================================
FILE: glama.json
================================================
{
  "$schema": "https://glama.ai/mcp/schemas/server.json",
  "maintainers": [
    "titulus"
  ]
}

================================================
FILE: pyproject.toml
================================================
[project]
name = "imagesorcery-mcp"
version = "0.12.0"
description = "A Model Context Protocol server providing image manipulation tools for LLMs"
readme = "README.md"
requires-python = ">=3.10"
authors = [
    { name = "titulus", email = "titulus.web@gmail.com" },
]
keywords = ["mcp", "llm"]
license = { text = "MIT" }
classifiers = [
    "Development Status :: 3 - Alpha",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3.10",
]
dependencies = [
    "fastmcp>=2.10.0,<3.0.0", # core for MCP servers
    "pydantic>=2.0.0", # For data validation, settings management, and serialization of classes
    "opencv-python>=4.5.0", # For image processing and computer vision tasks
    "imutils>=0.5.4", # For image processing typical tasks which are not included in OpenCV
    "Pillow", # For retrieving image metadata
    "ultralytics", # For object detection
    "requests", # For HTTP requests to download models
    "tqdm", # For progress bars during downloads
    "huggingface_hub", # For accessing models from Hugging Face
    "easyocr", # For OCR
    "toml", # For reading pyproject.toml
    "amplitude-analytics", # For telemetry
    "posthog", # For telemetry
    "python-dotenv", # For loading environment variables from .env file
]

[project.urls]
Homepage = "https://github.com/sunriseapps/imagesorcery-mcp"
Repository = "https://github.com/sunriseapps/imagesorcery-mcp"
[project.scripts]
imagesorcery-mcp = "imagesorcery_mcp:main"
download-yolo-models = "imagesorcery_mcp.scripts.download_models:main"
create-model-descriptions = "imagesorcery_mcp.scripts.create_model_descriptions:main"
download-clip-models = "imagesorcery_mcp.scripts.download_clip:main"
post-install-imagesorcery = "imagesorcery_mcp.scripts.post_install:main"

[project.optional-dependencies]
dev = ["pytest", "ruff", "pytest-asyncio", "build", "twine"]
clip = []

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.metadata]
allow-direct-references = true

[tool.ruff]
# PEP 8 style guidelines
# Same as Black.
line-length = 88
indent-width = 4

# Assume Python 3.10
target-version = "py310"

# Allow imports relative to the "src" and "tests" directories.
src = ["src", "tests"]

# Exclude a variety of commonly ignored directories.
exclude = [
    ".bzr",
    ".direnv",
    ".eggs",
    ".git",
    ".git-rewrite",
    ".hg",
    ".mypy_cache",
    ".nox",
    ".pants.d",
    ".pytype",
    ".ruff_cache",
    ".svn",
    ".tox",
    ".venv",
    "__pypackages__",
    "_build",
    "buck-out",
    "build",
    "dist",
    "node_modules",
    "venv",
]

[tool.ruff.lint]
# Enable flake8-bugbear (`B`) rules.
select = ["E", "F", "B", "I"]
ignore = [
    "E501", # Ignore line length violations
]


================================================
FILE: pytest.ini
================================================
[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function

================================================
FILE: setup.sh
================================================
#!/bin/bash
set -e

echo "Setting up imagesorcery-mcp..."

# Create virtual environment if it doesn't exist
if [ ! -d "venv" ]; then
    echo "Creating virtual environment..."
    python -m venv venv
fi

# Detect OS and activate the appropriate virtual environment
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" || "$OSTYPE" == "cygwin" ]]; then
    # Windows
    source venv/Scripts/activate
else
    # Linux/macOS
    source venv/bin/activate
fi

# Install package dependencies
echo "Installing package dependencies..."
pip install -e "."

# Run post-installation process
echo "Running post-installation process..."
imagesorcery-mcp --post-install

echo "✅ Setup complete!"


================================================
FILE: src/imagesorcery_mcp/README.md
================================================
# ImageSorcery MCP Core Architecture

This directory contains the core components of the ImageSorcery MCP server, including its main entry point (`server.py`).

## `server.py`

The `server.py` file is the primary entry point for the ImageSorcery MCP server. Its main responsibilities include:

-   **Initialization of FastMCP**: It creates and configures the `FastMCP` instance, which is the foundation for the MCP server. This includes setting the server's name, instructions, and logging level.
-   **Middleware Registration**: It registers custom middleware components, such as `ImprovedValidationMiddleware` and `ErrorHandlingMiddleware`, to enhance the server's request processing and error management capabilities.
-   **Tool and Resource Registration**: It registers all available image processing tools (e.g., `blur`, `crop`, `detect`) and resources (e.g., `models`) with the `FastMCP` instance, making them accessible via the MCP protocol.
-   **Argument Parsing**: It handles command-line argument parsing for server configuration, including transport type (stdio, http), host, port, and special flags like `--post-install`.
-   **Post-Installation Tasks**: It orchestrates the execution of post-installation scripts, which are crucial for downloading necessary models and setting up the environment.
-   **Server Execution**: It starts the MCP server using the configured transport protocol.

## `middleware.py`

The `middleware.py` file defines custom middleware classes that intercept and process requests and responses within the ImageSorcery MCP server. Currently, it includes:

-   **`ImprovedValidationMiddleware`**: This middleware is designed to enhance the error messages for validation failures originating from FastMCP tools. It parses generic `ToolError` exceptions, extracts specific validation issues (e.g., unexpected or missing parameters), and transforms them into more user-friendly `McpError` messages with a standardized error code. This improves the clarity and actionability of validation errors for MCP clients.

-   **`ErrorHandlingMiddleware`**: (Note: This middleware is part of `fastmcp` but is configured and used here). This middleware provides a global mechanism for catching and handling unhandled exceptions across the server. It ensures that all errors are logged consistently, can include detailed tracebacks for debugging, and are transformed into `McpError` objects, providing a standardized error response format for clients.


================================================
FILE: src/imagesorcery_mcp/__init__.py
================================================
"""ImageSorcery MCP - Powerful Image Processing Tools for AI Assistants"""

from .server import main, mcp

__all__ = ["main", "mcp"]


================================================
FILE: src/imagesorcery_mcp/__main__.py
================================================
from imagesorcery_mcp.server import main

from .logging_config import logger

logger.info("🪄 ImageSorcery MCP server __main__ executed")

if __name__ == "__main__":
    main()


================================================
FILE: src/imagesorcery_mcp/config.py
================================================
"""
Configuration management for ImageSorcery MCP.

This module provides a centralized configuration system that loads settings
from TOML files and allows runtime updates through the MCP config tool.
"""

from pathlib import Path
from typing import Any, Dict, List, Optional

import toml
from pydantic import BaseModel, Field, field_validator

from imagesorcery_mcp.logging_config import logger


class DetectionConfig(BaseModel):
    """Detection tool configuration."""
    confidence_threshold: float = Field(0.75, ge=0.0, le=1.0)
    default_model: str = "yoloe-11l-seg-pf.pt"


class FindConfig(BaseModel):
    """Find tool configuration."""
    confidence_threshold: float = Field(0.75, ge=0.0, le=1.0)
    default_model: str = "yoloe-11l-seg.pt"


class BlurConfig(BaseModel):
    """Blur tool configuration."""
    strength: int = Field(15, ge=1)

    @field_validator('strength')
    @classmethod
    def strength_must_be_odd(cls, v):
        if v % 2 == 0:
            raise ValueError('Blur strength must be an odd number')
        return v


class TextConfig(BaseModel):
    """Text drawing configuration."""
    font_scale: float = Field(1.0, gt=0.0)


class DrawingConfig(BaseModel):
    """Drawing configuration."""
    color: List[int] = Field([0, 0, 0], min_length=3, max_length=3)
    thickness: int = Field(1, ge=1)

    @field_validator('color')
    @classmethod
    def color_values_valid(cls, v):
        for val in v:
            if not (0 <= val <= 255):
                raise ValueError('Color values must be between 0 and 255')
        return v


class OCRConfig(BaseModel):
    """OCR configuration."""
    language: str = "en"


class ResizeConfig(BaseModel):
    """Resize configuration."""
    interpolation: str = Field("linear", pattern="^(nearest|linear|area|cubic|lanczos)$")


class TelemetryConfig(BaseModel):
    """Telemetry configuration."""
    enabled: bool = False


class ImageSorceryConfig(BaseModel):
    """Main configuration class for ImageSorcery MCP."""
    detection: DetectionConfig = DetectionConfig()
    find: FindConfig = FindConfig()
    blur: BlurConfig = BlurConfig()
    text: TextConfig = TextConfig()
    drawing: DrawingConfig = DrawingConfig()
    ocr: OCRConfig = OCRConfig()
    resize: ResizeConfig = ResizeConfig()
    telemetry: TelemetryConfig = TelemetryConfig()


class ConfigManager:
    """Configuration manager for ImageSorcery MCP."""
    
    def __init__(self):
        """Initialize the configuration manager."""
        self.config_file = Path("config.toml")
        logger.debug(f"Looking for user config file at: {self.config_file.absolute()}")
        self._config: Optional[ImageSorceryConfig] = None
        self._runtime_overrides: Dict[str, Any] = {}
        self._load_config()
    
    def _ensure_config_file_exists(self):
        """Ensure config.toml exists, create with default values if needed."""
        if not self.config_file.exists():
            # Create a basic config file with defaults
            default_config = ImageSorceryConfig()
            self._save_config_to_file(default_config.model_dump())
            logger.info("Created config.toml with default values")
        else:
            logger.debug(f"Config file already exists at: {self.config_file.absolute()}")
    
    def _load_config(self):
        """Load configuration from file."""
        self._ensure_config_file_exists()
        
        config_data = {}
        try:
            with open(self.config_file, 'r') as f:
                config_data = toml.load(f)
            logger.info(f"Loaded configuration from: {self.config_file}")
        except Exception as e:
            logger.error(f"Failed to load configuration from {self.config_file}: {e}")
            config_data = {}
        
        # Apply runtime overrides
        self._apply_runtime_overrides(config_data)
        
        # Create configuration object
        self._config = ImageSorceryConfig(**config_data)
        logger.info("Configuration loaded successfully")
    
    def _apply_runtime_overrides(self, config_data: Dict[str, Any]):
        """Apply runtime overrides to configuration data."""
        for key, value in self._runtime_overrides.items():
            if '.' in key:
                # Handle nested keys like "detection.confidence_threshold"
                parts = key.split('.')
                current = config_data
                for part in parts[:-1]:
                    if part not in current:
                        current[part] = {}
                    current = current[part]
                current[parts[-1]] = value
            else:
                # Handle top-level keys
                if key not in config_data:
                    config_data[key] = {}
                config_data[key] = value
    
    def _save_config_to_file(self, config_data: Dict[str, Any]):
        """Save configuration data to file."""
        try:
            with open(self.config_file, 'w') as f:
                toml.dump(config_data, f)
            logger.info(f"Configuration saved to: {self.config_file}")
        except Exception as e:
            logger.error(f"Failed to save configuration to {self.config_file}: {e}")
            raise
    
    @property
    def config(self) -> ImageSorceryConfig:
        """Get the current configuration."""
        if self._config is None:
            self._load_config()
        return self._config
    
    def get_config_dict(self) -> Dict[str, Any]:
        """Get configuration as a dictionary."""
        logger.debug("get_config_dict called")
        result = self.config.model_dump()
        logger.debug(f"get_config_dict returning: {result}")
        return result
    
    def update_config(self, updates: Dict[str, Any], persist: bool = False) -> Dict[str, Any]:
        """Update configuration values.
        
        Args:
            updates: Dictionary of configuration updates
            persist: If True, save changes to config file
            
        Returns:
            Updated configuration as dictionary
        """
        logger.debug(f"Updating configuration with: {updates}, persist: {persist}")
        
        # Validate updates by creating a temporary config object
        current_config = self.config.model_dump()
        
        # Apply updates to current config
        for key, value in updates.items():
            if '.' in key:
                # Handle nested keys like "detection.confidence_threshold"
                parts = key.split('.')
                current = current_config
                for part in parts[:-1]:
                    if part not in current:
                        current[part] = {}
                    current = current[part]
                current[parts[-1]] = value
            else:
                # Handle section updates
                if isinstance(value, dict):
                    if key not in current_config:
                        current_config[key] = {}
                    current_config[key].update(value)
                else:
                    current_config[key] = value
        
        # Validate the updated configuration
        try:
            ImageSorceryConfig(**current_config)
        except Exception as e:
            raise ValueError(f"Invalid configuration update: {e}") from e
        
        if persist:
            # Save to file
            self._save_config_to_file(current_config)
            # Clear runtime overrides since they're now persisted
            self._runtime_overrides.clear()
        else:
            # Store as runtime overrides
            self._runtime_overrides.update(updates)
        
        # Reload configuration
        self._load_config()
        
        return self.get_config_dict()
    
    def reset_runtime_overrides(self):
        """Reset all runtime overrides and reload from file."""
        logger.debug("Resetting runtime overrides")
        self._runtime_overrides.clear()
        self._load_config()
    
    def get_runtime_overrides(self) -> Dict[str, Any]:
        """Get current runtime overrides."""
        logger.debug(f"Getting runtime overrides: {self._runtime_overrides}")
        return self._runtime_overrides.copy()


# Global configuration manager instance
_config_manager: Optional[ConfigManager] = None


def get_config_manager() -> ConfigManager:
    """Get the global configuration manager instance."""
    logger.debug("get_config_manager called")
    global _config_manager
    if _config_manager is None:
        logger.debug("_config_manager is None, creating new instance")
        _config_manager = ConfigManager()
        logger.debug(f"_config_manager set to {_config_manager}")
    else:
        logger.debug("_config_manager already exists, returning existing instance")
    return _config_manager


def get_config() -> ImageSorceryConfig:
    """Get the current configuration."""
    logger.debug("get_config called")
    result = get_config_manager().config
    logger.debug(f"get_config returning: {result}")
    return result


def get_config_schema_info() -> Dict[str, Any]:
    """Get configuration schema information for documentation and validation."""
    logger.debug("get_config_schema_info")
    schema_info = {
        "detection.confidence_threshold": {
            "description": "Default confidence threshold for object detection (0.0-1.0)",
            "type": "float",
            "constraints": "0.0 ≤ value ≤ 1.0"
        },
        "detection.default_model": {
            "description": "Default model for detection tool",
            "type": "string",
            "constraints": "Valid model filename"
        },
        "find.confidence_threshold": {
            "description": "Default confidence threshold for object finding (0.0-1.0)",
            "type": "float",
            "constraints": "0.0 ≤ value ≤ 1.0"
        },
        "find.default_model": {
            "description": "Default model for find tool",
            "type": "string",
            "constraints": "Valid model filename"
        },
        "blur.strength": {
            "description": "Default blur strength (must be odd number)",
            "type": "integer",
            "constraints": "Odd number ≥ 1"
        },
        "text.font_scale": {
            "description": "Default font scale for text drawing",
            "type": "float",
            "constraints": "Value > 0.0"
        },
        "drawing.color": {
            "description": "Default color in BGR format [B,G,R]",
            "type": "list[int]",
            "constraints": "3 integers, each 0-255"
        },
        "drawing.thickness": {
            "description": "Default line thickness",
            "type": "integer",
            "constraints": "Value ≥ 1"
        },
        "ocr.language": {
            "description": "Default OCR language code",
            "type": "string",
            "constraints": "Valid language code (e.g., 'en', 'fr', 'ru')"
        },
        "resize.interpolation": {
            "description": "Default resize interpolation method",
            "type": "string",
            "constraints": "One of: nearest, linear, area, cubic, lanczos"
        },
        "telemetry.enabled": {
            "description": "Enable or disable anonymous telemetry",
            "type": "boolean",
            "constraints": "true or false"
        }
    }
    return schema_info


def get_available_config_keys() -> List[str]:
    """Get list of all available configuration keys."""
    logger.debug("get_available_config_keys")
    return list(get_config_schema_info().keys())


def generate_config_documentation() -> str:
    """Generate configuration documentation from schema."""
    logger.debug("generate_config_documentation")
    schema_info = get_config_schema_info()

    lines = ["Available configuration keys:"]
    for key, info in schema_info.items():
        lines.append(f"- {key}: {info['description']}")

    return "\n".join(lines)


================================================
FILE: src/imagesorcery_mcp/logging_config.py
================================================
import logging
import os
from logging.handlers import RotatingFileHandler

LOG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs", "imagesorcery.log")
LOG_LEVEL = logging.INFO

def setup_logging():
    """Sets up the central logger for the 🪄 ImageSorcery MCP server."""
    # Ensure the logs directory exists
    log_dir = os.path.dirname(LOG_FILE)
    os.makedirs(log_dir, exist_ok=True)

    # Create logger
    logger = logging.getLogger("imagesorcery")
    logger.setLevel(LOG_LEVEL)

    # Prevent adding multiple handlers if setup is called more than once
    if not logger.handlers:
        # Create rotating file handler
        handler = RotatingFileHandler(LOG_FILE, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8')
        # Change formatter to include module name and line number
        formatter = logging.Formatter('%(asctime)s - %(name)s.%(module)s:%(lineno)d - %(levelname)s - %(message)s')
        handler.setFormatter(formatter)
        logger.addHandler(handler)

        # Optional: Add a console handler for development/debugging
        console_handler = logging.StreamHandler()
        console_handler.setFormatter(formatter)
        console_handler.setLevel(LOG_LEVEL) # Set level for console handler
        logger.addHandler(console_handler)

    print(f"Log file: {LOG_FILE}")
    return logger

# Setup logging when this module is imported
setup_logging()

# Get the logger instance to be used in other modules
logger = logging.getLogger("imagesorcery")


================================================
FILE: src/imagesorcery_mcp/middlewares/path_access.py
================================================
import logging
import os
from pathlib import Path
from typing import Any, Iterator

from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
from mcp import McpError
from mcp.types import ErrorData

AVAILABLE_PATHS_ENV = "IMAGESORCERY_AVAILABLE_PATHS"


class PathAccessMiddleware(Middleware):
    """Restrict tool file paths to configured directories."""

    def __init__(self, logger: logging.Logger | None = None):
        self.logger = logger or logging.getLogger("imagesorcery.path_access")

    async def on_call_tool(
        self,
        context: MiddlewareContext,
        call_next: CallNext,
    ) -> Any:
        allowed_dirs = get_allowed_directories()
        if not allowed_dirs:
            return await call_next(context)

        arguments = getattr(context.message, "arguments", None) or {}
        for argument_name, path_value in iter_path_arguments(arguments):
            resolved_path = resolve_path(path_value)
            if not is_path_allowed(resolved_path, allowed_dirs):
                allowed = ", ".join(str(path) for path in allowed_dirs)
                error_message = (
                    f"Path argument '{argument_name}' is outside allowed directories: "
                    f"{resolved_path}. Allowed directories: {allowed}"
                )
                self.logger.warning(error_message)
                raise McpError(ErrorData(code=-32602, message=error_message))

        return await call_next(context)


def get_allowed_directories() -> list[Path]:
    raw_paths = os.getenv(AVAILABLE_PATHS_ENV, "")
    if not raw_paths.strip():
        return []

    allowed_dirs = []
    for raw_path in split_paths(raw_paths):
        if not raw_path:
            continue
        allowed_dirs.append(resolve_path(raw_path))
    return allowed_dirs


def split_paths(raw_paths: str) -> list[str]:
    normalized = raw_paths.replace(",", os.pathsep)
    return [part.strip() for part in normalized.split(os.pathsep) if part.strip()]


def iter_path_arguments(value: Any, prefix: str = "") -> Iterator[tuple[str, str]]:
    if isinstance(value, dict):
        for key, item in value.items():
            name = f"{prefix}.{key}" if prefix else str(key)
            if is_path_argument(str(key)) and isinstance(item, str) and item.strip():
                yield name, item
            else:
                yield from iter_path_arguments(item, name)
    elif isinstance(value, list):
        for index, item in enumerate(value):
            name = f"{prefix}[{index}]" if prefix else f"[{index}]"
            yield from iter_path_arguments(item, name)


def is_path_argument(name: str) -> bool:
    return name == "path" or name.endswith("_path")


def resolve_path(path: str) -> Path:
    return Path(os.path.abspath(os.path.expanduser(path)))


def is_path_allowed(path: Path, allowed_dirs: list[Path]) -> bool:
    return any(path == allowed_dir or path.is_relative_to(allowed_dir) for allowed_dir in allowed_dirs)


================================================
FILE: src/imagesorcery_mcp/middlewares/telemetry.py
================================================
import logging
import sys
from importlib.metadata import version
from pathlib import Path
from typing import Any

from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext

from imagesorcery_mcp.config import get_config
from imagesorcery_mcp.telemetry_amplitude import amplitude_handler
from imagesorcery_mcp.telemetry_posthog import posthog_handler


class TelemetryMiddleware(Middleware):
    """Middleware that logs every tool, prompt, and resource run based on configuration."""
    
    def __init__(self, logger: logging.Logger | None = None):
        self.logger = logger or logging.getLogger("imagesorcery.telemetry")
        self.user_id = self._get_user_id() # Added user_id
        self.version = self._get_version()
        self.system = sys.platform

        self.amplitude_handler = amplitude_handler
        self.posthog_handler = posthog_handler

    def _get_user_id(self) -> str:
        """Get user_id from .user_id file."""
        user_id_file = Path(".user_id")  # Path to .user_id in project root
        self.logger.debug(f"Looking for user ID file at: {user_id_file.absolute()}")
        try:
            if user_id_file.exists():
                user_id = user_id_file.read_text().strip()
                if user_id:
                    self.logger.debug(f"User ID from file: {user_id}")
                    return user_id
            self.logger.warning("User ID file not found or empty. Telemetry will use 'anonymous'.")
            return "anonymous"
        except Exception as e:
            self.logger.error(f"Could not read user_id: {e}")
            return "anonymous"

    def _get_version(self) -> str:
        """Get package version."""
        try:
            package_version = version("imagesorcery-mcp")
            if package_version:
                return package_version
        except Exception:
            pass

        try:
            import toml

            pyproject_path = Path(__file__).resolve().parents[3] / "pyproject.toml"
            return toml.load(pyproject_path)["project"]["version"]
        except Exception:
            self.logger.warning("Could not determine package version for telemetry.")
            return "unknown"

    async def _handle_action(self, action_type: str, identifier: str, context: MiddlewareContext, call_next: CallNext) -> Any:
        """Helper to log actions before and after execution, if telemetry is enabled."""
        self.logger.debug(f"{action_type}: {identifier}")
        config = get_config()
        self.logger.debug(f"Telemetry enabled: {config.telemetry.enabled}")

        if not config.telemetry.enabled:
            self.logger.debug("Telemetry enabled skipped")
            return await call_next(context)

        log_data = {
            "user_id": self.user_id, # Added user_id to log_data
            "version": self.version,
            "system": self.system,
            "action_type": action_type.lower().replace(" ", "_"),
            "identifier": identifier,
        }

        try:
            response = await call_next(context)
            log_data["status"] = "success"
            self.logger.info(log_data)
            self.posthog_handler.track_event(log_data)
            self.amplitude_handler.track_event(log_data)
            return response
        except Exception:
            log_data["status"] = "failed"
            self.logger.warning(log_data)
            self.posthog_handler.track_event(log_data)
            self.amplitude_handler.track_event(log_data)
            raise

    async def on_call_tool(self, context: MiddlewareContext, call_next: CallNext) -> Any:
        """Log tool calls before and after execution, if telemetry is enabled."""
        return await self._handle_action("Calling tool", context.message.name, context, call_next)

    async def on_read_resource(self, context: MiddlewareContext, call_next: CallNext) -> Any:
        """Log resource reads before and after execution, if telemetry is enabled."""
        return await self._handle_action("Reading resource", str(context.message.uri), context, call_next)

    async def on_get_prompt(self, context: MiddlewareContext, call_next: CallNext) -> Any:
        """Log prompt retrievals before and after execution, if telemetry is enabled."""
        return await self._handle_action("Getting prompt", context.message.name, context, call_next)


================================================
FILE: src/imagesorcery_mcp/middlewares/validation.py
================================================
import logging
import re
from typing import Any

from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
from mcp import McpError
from mcp.types import ErrorData


class ImprovedValidationMiddleware(Middleware):
    """Middleware that improves validation error messages from FastMCP tools."""
    
    def __init__(self, logger: logging.Logger | None = None):
        self.logger = logger or logging.getLogger("imagesorcery.validation")
    
    async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
        """Handle messages with improved validation error reporting."""
        try:
            return await call_next(context)
        except Exception as e:
            error_msg = str(e)
            
            if "validation error for call[" in error_msg:
                tool_match = re.search(r'call\[(\w+)\]', error_msg)
                tool_name = tool_match.group(1) if tool_match else "unknown"
                
                errors = []
                
                if "Unexpected keyword argument" in error_msg:
                    lines = error_msg.split('\n')
                    for i, line in enumerate(lines):
                        if "Unexpected keyword argument" in line:
                            if i > 0:
                                param_line = lines[i-1].strip()
                                param_name = param_line.split()[0] if param_line else "unknown"
                                errors.append(f"Unexpected parameter '{param_name}' - this parameter is not accepted by the tool '{tool_name}'")
                
                if "Missing required" in error_msg:
                    param_match = re.search(r"Missing required.*?'(\w+)'", error_msg)
                    if param_match:
                        param_name = param_match.group(1)
                        errors.append(f"Missing required parameter '{param_name}'")

                invalid_value_match = re.search(r"input_value='([^']+)'", error_msg)
                if invalid_value_match:
                    invalid_value = invalid_value_match.group(1)
                    errors.append(f"Invalid value '{invalid_value}'")
                
                if errors:
                    error_message = "Input validation error: " + "; ".join(errors)
                else:
                    error_message = f"Input validation error in tool '{tool_name}': check that all parameters are correctly named and have the right types"
                
                self.logger.error(error_message)
                self.logger.debug(f"Original error: {error_msg}")
                
                raise McpError(
                    ErrorData(code=-32602, message=error_message)
                ) from e
            
            raise


================================================
FILE: src/imagesorcery_mcp/prompts/README.md
================================================
# Prompts

This directory contains reusable prompt templates for the ImageSorcery MCP server.

## Overview

Prompts provide parameterized message templates that help LLMs generate structured, purposeful responses for image processing workflows. Each prompt is designed to guide the AI through specific image manipulation tasks using the available tools.

## Architecture

- Register prompts by defining a `register_prompt` function in each prompt's module. This function should accept a `FastMCP` instance and use the `@mcp.prompt()` decorator to register the prompt function with the server. See `src/imagesorcery_mcp/server.py` for how prompts are imported and registered, and individual prompt files like `src/imagesorcery_mcp/prompts/remove_background.py` for examples of the `register_prompt` function implementation.
- When adding new prompts, ensure they are listed in alphabetical order in READMEs and in the server registration.

## Adding New Prompts

1. Create a new Python file in this directory (e.g., `new_prompt.py`)
2. Implement the prompt function with appropriate parameters and return type
3. Create a `register_prompt` function that registers the prompt with the FastMCP instance
4. Import and register the prompt in `src/imagesorcery_mcp/server.py`
5. Add documentation to this README
6. Write tests in `tests/prompts/test_new_prompt.py`

## Available Prompts

### `remove-background`

**Description:** Guides the AI through a comprehensive background removal workflow using object detection and masking tools.

**Parameters:**
- `image_path` (str): Full path to the input image
- `target_objects` (str, optional): Description of the objects to keep (default: empty for auto-detection)
- `output_path` (str, optional): Path for the final result (default: auto-generated)

**Example Usage:**
```
Use the remove-background prompt to remove the background from my photo 'portrait.jpg', keeping only the person
```

**Example Prompt Call (JSON):**
```json
{
  "name": "remove-background",
  "arguments": {
    "image_path": "/home/user/images/portrait.jpg",
    "target_objects": "person",
    "output_path": "/home/user/images/portrait_no_bg.png"
  }
}
```


================================================
FILE: src/imagesorcery_mcp/prompts/__init__.py
================================================
# Import the central logger
from imagesorcery_mcp.logging_config import logger

logger.info("🪄 ImageSorcery MCP prompts package initialized")


================================================
FILE: src/imagesorcery_mcp/prompts/remove_background.py
================================================
from typing import Annotated

from fastmcp import FastMCP
from pydantic import Field

# Import the central logger
from imagesorcery_mcp.logging_config import logger


def register_prompt(mcp: FastMCP):
    @mcp.prompt(name="remove-background")
    def remove_background(
        image_path: Annotated[
            str, Field(description="Full path to the input image (must be a full path)")
        ],
        target_objects: Annotated[
            str,
            Field(
                description="Description of the objects to keep in the foreground (e.g., 'person', 'car and person', 'main subject')"
            ),
        ] = "",
        output_path: Annotated[
            str,
            Field(
                description="Full path for the output image with background removed (optional, will auto-generate if not provided)"
            ),
        ] = "",
    ) -> str:
        """
        Guides the AI through a comprehensive background removal workflow.
        
        This prompt provides a step-by-step approach to remove backgrounds from images
        using object detection and masking tools. It's designed to work with the
        ImageSorcery MCP server's detect and fill tools.
        
        The workflow includes:
        1. Object detection to identify the target object
        2. Mask generation for precise selection
        3. Background removal using fill operations
        4. Optional refinement steps
        
        Args:
            image_path: Full path to the input image
            target_objects: Description of what to keep (default: empty for auto-detection)
            output_path: Where to save the result (auto-generated if empty)

        Returns:
            A detailed prompt guiding the AI through the background removal process
        """
        logger.info(f"Remove background prompt requested for image: {image_path}")
        logger.debug(f"Target objects: {target_objects}, Output path: {output_path}")
        
        # Generate output path if not provided
        if not output_path:
            if image_path.lower().endswith(('.png', '.jpg', '.jpeg')):
                base_path = image_path.rsplit('.', 1)[0]
                output_path = f"{base_path}_no_background.png"
            else:
                output_path = f"{image_path}_no_background.png"

        # Build the prompt based on whether target_objects is specified
        if target_objects:
            prompt = f"""I need to remove the background from an image while preserving the {target_objects}. Please follow this step-by-step workflow:

**Step 1: Find Target Objects**
Use the `find` tool to locate the specific objects:
- Call `find` on '{image_path}' with:
  - description: "{target_objects}"
  - confidence: 0.3 (lower threshold for better recall)
  - return_geometry: true
  - geometry_format: "mask"
- This will use text-based object identification to locate the {target_objects}

**Step 2: Remove Background**
Use the `fill` tool to remove the background:
- Call `fill` on '{image_path}' with:
  - areas: Use mask files from find
  - color: null
  - output_path: '{output_path}'

**Step 3: Clean Up**
- Remove the temporary mask files created during the process

**Important Notes:**
- Save the final result as a PNG file to preserve transparency

Please execute this workflow step by step."""
        else:
            prompt = f"""I need to remove the background from an image. Please follow this step-by-step workflow:

**Step 1: Detect Objects**
Use the `detect` tool to identify objects in the image:
- Call `detect` on '{image_path}' with:
  - confidence: 0.5 (to catch more objects)
  - return_geometry: true
  - geometry_format: "mask"
- Review the detected objects and identify the main subjects to preserve

**Step 3: Remove Background**
Use the `fill` tool to remove the background:
- Call `fill` on '{image_path}' with:
  - areas: Use mask files from detect
  - color: null
  - output_path: '{output_path}'

**Step 4: Clean Up**
- Remove the temporary mask files created during the process

**Important Notes:**
- Save the final result as a PNG file to preserve transparency

Please execute this workflow step by step."""

        logger.info(f"Generated remove background prompt for targets: {target_objects or 'auto-detect'}")
        return prompt


================================================
FILE: src/imagesorcery_mcp/resources/README.md
================================================
# 🪄 ImageSorcery MCP Server Resources Documentation

This document provides detailed information about each resource available in the 🪄 ImageSorcery MCP Server, including their URIs, descriptions, and examples of how to access them using a Claude client.

## Rules

These rules apply to all contributors: humans and AI.

- Register resources by defining a `register_resource` function in each resource's module. This function should accept a `FastMCP` instance and use the `@mcp.resource()` decorator to register the resource function with the server. See `src/imagesorcery_mcp/server.py` for how resources are imported and registered, and individual resource files like `src/imagesorcery_mcp/resources/models.py` for examples of the `register_resource` function implementation.
- When adding new resources, ensure they are listed in alphabetical order in READMEs and in the server registration.


## Available Resources

### `models://list`

Lists all available models in the models directory. This resource scans the models directory and returns information about all available models, including their names, descriptions, and file paths.

- **URI:** `models://list`
- **Returns:** JSON string containing:
  - `models`: List of available models, each with:
    - `name`: Name of the model file (relative path from the models directory)
    - `description`: Description of the model's purpose and characteristics
    - `path`: Full path to the model file

**Example Claude Request:**

```
List all available models in the models directory
```

**Example Resource Access (JSON):**

```json
{
  "resource": "models://list"
}
```

**Example Response (JSON):**

```json
{
  "models": [
    {
      "name": "yolov8m.pt",
      "description": "YOLOv8 Medium - Default model with good balance between accuracy and speed.",
      "path": "/path/to/models/yolov8m.pt"
    },
    {
      "name": "yolov8n.pt",
      "description": "YOLOv8 Nano - Smallest and fastest model, suitable for edge devices with limited resources.",
      "path": "/path/to/models/yolov8n.pt"
    }
  ]
}

================================================
FILE: src/imagesorcery_mcp/resources/__init__.py
================================================
# Import the central logger
from imagesorcery_mcp.logging_config import logger

logger.info("🪄 ImageSorcery MCP resources package initialized")

================================================
FILE: src/imagesorcery_mcp/resources/models.py
================================================
import json
from pathlib import Path

from fastmcp import FastMCP

# Import the central logger
from imagesorcery_mcp.logging_config import logger


def get_model_description(model_name: str) -> str:
    """Get a description for a specific model."""
    logger.debug(f"Attempting to get description for model: {model_name}")
    # Path to model descriptions JSON file
    descriptions_file = Path("models") / "model_descriptions.json"
    
    # Check if descriptions file exists
    if not descriptions_file.exists():
        logger.warning(f"Model descriptions file not found: {descriptions_file}")
        return "model_descriptions.json not found"
    
    try:
        # Load descriptions from JSON file
        logger.debug(f"Loading model descriptions from: {descriptions_file}")
        with open(descriptions_file, "r", encoding="utf-8") as f:
            descriptions = json.load(f)
        logger.debug(f"Loaded {len(descriptions)} model descriptions")
        
        # Normalize model name to use forward slashes for consistent lookup
        normalized_model_name = model_name.replace('\\', '/')
        logger.debug(f"Normalized model name for lookup: {normalized_model_name}")
        
        # Try direct lookup and also case-insensitive lookup
        if normalized_model_name in descriptions:
            logger.debug(f"Found direct match for model description: {normalized_model_name}")
            return descriptions[normalized_model_name]
        
        # Try case-insensitive lookup as a fallback
        for key in descriptions:
            if key.lower() == normalized_model_name.lower():
                logger.debug(f"Found case-insensitive match for model description: {key}")
                return descriptions[key]
        
        logger.warning(f"Model '{model_name}' not found in model_descriptions.json (total descriptions: {len(descriptions)})")
        return f"Model '{model_name}' not found in model_descriptions.json (total descriptions: {len(descriptions)})"
    except Exception as e:
        # Return default description if any error occurs
        logger.error(f"Error in get_model_description for {model_name}: {str(e)}", exc_info=True)
        return "model_descriptions.json parse issue"

def register_resource(mcp: FastMCP):
    @mcp.resource("models://list")
    async def list_models() -> str:
        """
        List all available models in the models directory.

        This resource provides information about all available models,
        including their names and descriptions.
        """
        logger.info("Models resource requested")
        models_dir = Path("models")
        available_models = []

        # Check if models directory exists
        if not models_dir.exists():
            logger.warning(f"Models directory not found: {models_dir}")
            return json.dumps({"models": available_models}, indent=2)
        logger.info(f"Scanning models directory: {models_dir}")

        # Define model file extensions to include
        model_extensions = [".pt", ".pth", ".onnx", ".tflite", ".pb"]
        logger.debug(f"Looking for files with extensions: {model_extensions}")

        # Scan for model files recursively using rglob instead of glob
        for file_path in models_dir.rglob("*"):
            if file_path.is_file() and file_path.suffix.lower() in model_extensions:
                # Get relative path from models directory
                rel_path = file_path.relative_to(models_dir)
                # Convert to string with forward slashes for consistent naming across platforms
                model_name = str(rel_path).replace('\\', '/')

                description = get_model_description(model_name)
                
                available_models.append(
                    {
                        "name": model_name,
                        "description": description,
                        "path": str(file_path),
                    }
                )
                logger.debug(f"Found model: {model_name} with description: {description}")
        
        logger.info(f"Found {len(available_models)} available models")
        return json.dumps({"models": available_models}, indent=2)

================================================
FILE: src/imagesorcery_mcp/scripts/README.md
================================================
# 🪄 ImageSorcery MCP Server Scripts Documentation

This document provides detailed information about each script available in the 🪄 ImageSorcery MCP Server, including their purpose, arguments, and examples of how to use them.

## Overview

The scripts directory contains utility scripts for model management and setup within the 🪄 ImageSorcery MCP Server. These scripts handle tasks such as:

- `download-models`: Downloading YOLO models from various sources
- `create-model-descriptions`: Creating model descriptions (used in `setup.sh`)
- `download-clip-models`: Downloading CLIP models required for text-based detection (YOLOe *-pf models) (used in `setup.sh`)
- `post-install-imagesorcery`: Running all post-installation tasks in a single command
- `populate_telemetry_keys.py` / `clear_telemetry_keys.py`: build-time helpers for telemetry keys management

These scripts are typically run during project setup, packaging, or when adding new models to the system.

## Common Functions

These scripts share some common functions and patterns:

- All scripts use a central logger from `imagesorcery_mcp.logging_config`
- They typically create the `models` directory if it doesn't exist
- They handle existing files to avoid unnecessary downloads
- Progress bars are provided for downloads using `tqdm`

## Available Scripts

### `download_models.py`

Downloads YOLO compatible models for offline use from either Ultralytics or Hugging Face.

- **Purpose:** Ensures that required detection models are available for tools like `detect` and `find`.
- **Functionality:**
  - Downloads models from Ultralytics repositories
  - Downloads models from Hugging Face repositories
  - Updates the model descriptions JSON file with information about downloaded models
  - Organizes models in a proper directory structure
- **Arguments:**
  - `--ultralytics MODEL_NAME`: Download a model from Ultralytics (e.g., 'yolov8m.pt')
  - `--huggingface REPO_ID[:FILENAME]`: Download a model from Hugging Face (e.g., 'username/repo:model.pt')

**Command-line Usage:**
```bash
# Download from Ultralytics
download-yolo-models --ultralytics yolov8m.pt

# Download from Hugging Face
download-yolo-models --huggingface ultralytics/yolov8:yolov8m.pt
```

**Python Import Usage:**
```python
from imagesorcery_mcp.scripts.download_models import download_ultralytics_model, download_from_huggingface

# Download from Ultralytics
success = download_ultralytics_model('yolov8m.pt')

# Download from Hugging Face
success = download_from_huggingface('ultralytics/yolov8:yolov8m.pt')
```

#### Notes

- Downloaded models are stored in the `models` directory, which is included in `.gitignore` to prevent large model files from being committed to the repository.
- If you encounter permission issues when running these scripts, ensure you have the necessary write access to the project directory.

### `create_model_descriptions.py`

Creates a JSON file containing descriptions for various detection models in the models directory.

- **Purpose:** Ensures that model description information is available for reference by tools and users.
- **Functionality:** 
  - Creates a comprehensive list of model descriptions for various YOLO models (YOLOv8, YOLO11, YOLO-NAS, etc.)
  - Merges new descriptions with any existing ones, preserving custom descriptions
  - Writes the merged descriptions to `models/model_descriptions.json`
- **Usage:** Run directly or through the provided command-line entry point.

**Command-line Usage:**
```bash
create-model-descriptions
```

**Python Import Usage:**
```python
from imagesorcery_mcp.scripts.create_model_descriptions import create_model_descriptions

# Create the model descriptions file
result_path = create_model_descriptions()
```

### `download_clip.py`

Downloads the MobileCLIP model required for YOLOe text prompts functionality.

- **Purpose:** Ensures that the required MobileCLIP model is available for text-based detection in the `find` tool.
- **Functionality:**
  - Downloads the MobileCLIP model required for YOLOe text prompts
  - Places the model in the root directory where it's expected by the find tool
- **Usage:** Run directly or through the provided command-line entry point.

**Command-line Usage:**
```bash
download-clip-models
```

**Python Import Usage:**
```python
from imagesorcery_mcp.scripts.download_clip import download_clip_model

# Download CLIP model
success = download_clip_model()
```

### `post_install.py`

Runs all post-installation tasks for the ImageSorcery MCP server in a single command.

- **Purpose:** Automates the complete setup process after package installation.
- **Functionality:**
  - Creates the models directory
  - Generates the model descriptions file with `create-model-descriptions`
  - Downloads default YOLO models (yoloe-11l-seg-pf.pt, yoloe-11s-seg-pf.pt, yoloe-11l-seg.pt, yoloe-11s-seg.pt) with `download-yolo-models`
  - Installs the `clip` Python package from Ultralytics' GitHub repository.
  - Downloads the required CLIP model file for text prompts with `download-clip-models`.
  - Ensures a `.user_id` file exists in project root (used for telemetry user identification).
- **Usage:** Run directly, through the server with the `--post-install` flag, or through the provided command-line entry point.

**Command-line Usage:**
```bash
# Run post-installation as a standalone script
python -m src.imagesorcery_mcp.scripts.post_install

# Or run it through the server with the --post-install flag
imagesorcery-mcp --post-install
```

**Python Import Usage:**
```python
from imagesorcery_mcp.scripts.post_install import run_post_install

# Run all post-installation tasks
success = run_post_install()
if success:
    print("Post-installation completed successfully!")
else:
    print("Post-installation failed.")
```

## Telemetry Keys Management (build-time)

Telemetry keys are no longer stored in `telemetry.toml`. Instead, telemetry API keys are managed via a small Python module and/or environment variables:

- Telemetry user identifier is stored in `.user_id` (created by `post_install.py`).
- API keys are provided either via environment variables or the Python module:
  - Environment variables (preferred during build/deploy):
    - `IMAGESORCERY_AMPLITUDE_API_KEY`
    - `IMAGESORCERY_POSTHOG_API_KEY`
  - Fallback module (kept in the repository as empty defaults): `src/imagesorcery_mcp/telemetry_keys.py`
    - Contains `AMPLITUDE_API_KEY = ""` and `POSTHOG_API_KEY = ""`

Rationale:
- `telemetry.toml` was unreliable in some packaging/build scenarios (it could be omitted from final artifacts). Using environment variables (and a small Python module as a fallback) ensures keys are available at runtime and during build without embedding secrets in the repo.

### `populate_telemetry_keys.py`

**Purpose**: Populate `src/imagesorcery_mcp/telemetry_keys.py` with API keys from the environment (or `.env`) during build time if desired.

**Functionality**:
- Reads `IMAGESORCERY_AMPLITUDE_API_KEY` and `IMAGESORCERY_POSTHOG_API_KEY` from environment variables (or `.env` when python-dotenv is available)
- Writes these values into `src/imagesorcery_mcp/telemetry_keys.py`
- Intended to be used in CI/build pipelines where keys are injected as environment variables before packaging

**Command-line Usage**:
```bash
python -m src.imagesorcery_mcp.scripts.populate_telemetry_keys
```

**Notes**:
- The script will not commit changes; CI should handle any necessary cleanup.
- To skip population, set `SKIP_TELEMETRY_POPULATION=true`.

### `clear_telemetry_keys.py`

**Purpose**: Clear API keys in `src/imagesorcery_mcp/telemetry_keys.py` after build to keep the repository clean.

**Functionality**:
- Overwrites `src/imagesorcery_mcp/telemetry_keys.py` with empty string defaults:
  ```py
  AMPLITUDE_API_KEY = ""
  POSTHOG_API_KEY = ""
  ```
- Intended to be invoked as a post-build/cleanup step in CI

**Command-line Usage**:
```bash
python -m src.imagesorcery_mcp.scripts.clear_telemetry_keys
```

### Recommended CI / Build Integration

A suggested pipeline for safely using telemetry keys in CI:

1. In CI, set environment variables:
   - `IMAGESORCERY_AMPLITUDE_API_KEY` and `IMAGESORCERY_POSTHOG_API_KEY`
2. Run the populate script:
   - `python -m src.imagesorcery_mcp.scripts.populate_telemetry_keys`
3. Build/package the project
4. Run the clear script to remove keys from the working copy:
   - `python -m src.imagesorcery_mcp.scripts.clear_telemetry_keys`
5. Ensure the CI does not persist telemetry_keys.py with real keys in any artifact or commit.

**Dependencies**:
- `python-dotenv` (optional) — used by scripts to load a `.env` file when present

**Error Handling**:
- Scripts log errors and return non-zero exit codes on failure so CI can fail fast.


================================================
FILE: src/imagesorcery_mcp/scripts/__init__.py
================================================
# Import functions to make them available when importing the package
# Import the central logger
from imagesorcery_mcp.logging_config import logger

from .create_model_descriptions import create_model_descriptions
from .download_models import download_model

__all__ = ["create_model_descriptions", "download_model", "logger"]


================================================
FILE: src/imagesorcery_mcp/scripts/clear_telemetry_keys.py
================================================
#!/usr/bin/env python3
"""Build script to clear API keys in src/imagesorcery_mcp/telemetry_keys.py while preserving .user_id."""

import logging
import sys
from pathlib import Path

# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# Telemetry keys file path
TELEMETRY_KEYS_FILE = Path('src/imagesorcery_mcp/telemetry_keys.py')

def write_empty_telemetry_keys() -> bool:
    """Overwrite telemetry_keys.py with empty API key values."""
    try:
        content = '''# Auto-generated telemetry keys module.
# This file is intended to be updated by build scripts (populate_telemetry_keys.py)
# and cleared by clear_telemetry_keys.py. Keep values as empty strings in the repo.
#
# WARNING: Do NOT commit real production keys to the repository.

AMPLITUDE_API_KEY = ""
POSTHOG_API_KEY = ""
'''
        TELEMETRY_KEYS_FILE.write_text(content)
        logger.info(f"Cleared telemetry keys in {TELEMETRY_KEYS_FILE}")
        return True
    except Exception as e:
        logger.error(f"Failed to clear telemetry keys file: {e}")
        return False

def main():
    logger.info("Starting telemetry keys clearing process...")

    if not TELEMETRY_KEYS_FILE.exists():
        logger.warning(f"{TELEMETRY_KEYS_FILE} does not exist; creating a new cleared file.")
    if write_empty_telemetry_keys():
        logger.info("Telemetry keys cleared successfully")
        return 0
    else:
        logger.error("Failed to clear telemetry keys")
        return 1

if __name__ == '__main__':
    sys.exit(main())


================================================
FILE: src/imagesorcery_mcp/scripts/create_model_descriptions.py
================================================
#!/usr/bin/env python3
"""
Script to create model descriptions JSON file.
This script should be run during project setup to ensure model descriptions are available.
"""

import json
import os
from pathlib import Path

# Import the central logger
from imagesorcery_mcp.logging_config import logger


def create_model_descriptions():
    """Create a JSON file with model descriptions in the models directory."""
    logger.info(f"Creating model descriptions JSON file at {Path('models') / 'model_descriptions.json'}")
    # YOLOv8 model descriptions
    model_descriptions = {
        "yolo11n.pt": "Ultralytics YOLO11 model for Object Detection. Provides state-of-the-art performance, suitable for tasks requiring a balance of speed and accuracy (smallest of YOLO11).",
        "yolo11s.pt": "Ultralytics YOLO11 model for Object Detection. Provides state-of-the-art performance, more accurate than 'n', with slightly lower speed.",
        "yolo11m.pt": "Ultralytics YOLO11 model for Object Detection. Provides state-of-the-art performance, a medium option balancing accuracy and speed.",
        "yolo11l.pt": "Ultralytics YOLO11 model for Object Detection. Provides state-of-the-art performance, more accurate than 'm', with slightly lower speed.",
        "yolo11x.pt": "Ultralytics YOLO11 model for Object Detection. Provides state-of-the-art performance (highest accuracy of YOLO11 Detect).",
        "yolo11n-seg.pt": "Ultralytics YOLO11 model for Instance Segmentation. Provides state-of-the-art performance, smallest and fastest of YOLO11 Seg.",
        "yolo11s-seg.pt": "Ultralytics YOLO11 model for Instance Segmentation. Provides state-of-the-art performance, a larger variant than 'n'.",
        "yolo11m-seg.pt": "Ultralytics YOLO11 model for Instance Segmentation. Provides state-of-the-art performance, a medium variant.",
        "yolo11l-seg.pt": "Ultralytics YOLO11 model for Instance Segmentation. Provides state-of-the-art performance, a larger variant than 'm'.",
        "yolo11x-seg.pt": "Ultralytics YOLO11 model for Instance Segmentation. Provides state-of-the-art performance (highest accuracy of YOLO11 Seg).",
        "yolo11n-pose.pt": "Ultralytics YOLO11 model for Pose Estimation / Keypoints detection. Provides state-of-the-art performance, smallest and fastest of YOLO11 Pose.",
        "yolo11s-pose.pt": "Ultralytics YOLO11 model for Pose Estimation / Keypoints detection. Provides state-of-the-art performance, a larger variant than 'n'.",
        "yolo11m-pose.pt": "Ultralytics YOLO11 model for Pose Estimation / Keypoints detection. Provides state-of-the-art performance, a medium variant.",
        "yolo11l-pose.pt": "Ultralytics YOLO11 model for Pose Estimation / Keypoints detection. Provides state-of-the-art performance, a larger variant than 'm'.",
        "yolo11x-pose.pt": "Ultralytics YOLO11 model for Pose Estimation / Keypoints detection. Provides state-of-the-art performance (highest accuracy of YOLO11 Pose).",
        "yolo11n-obb.pt": "Ultralytics YOLO11 model for Oriented Object Detection (OBB). Provides state-of-the-art performance, smallest of YOLO11 OBB.",
        "yolo11s-obb.pt": "Ultralytics YOLO11 model for Oriented Object Detection (OBB). Provides state-of-the-art performance, a larger variant than 'n'.",
        "yolo11m-obb.pt": "Ultralytics YOLO11 model for Oriented Object Detection (OBB). Provides state-of-the-art performance, a medium variant.",
        "yolo11l-obb.pt": "Ultralytics YOLO11 model for Oriented Object Detection (OBB). Provides state-of-the-art performance, a larger variant than 'm'.",
        "yolo11x-obb.pt": "Ultralytics YOLO11 model for Oriented Object Detection (OBB). Provides state-of-the-art performance (highest accuracy of YOLO11 OBB).",
        "yolo11n-cls.pt": "Ultralytics YOLO11 model for Image Classification. Provides state-of-the-art performance, smallest of YOLO11 Classify.",
        "yolo11s-cls.pt": "Ultralytics YOLO11 model for Image Classification. Provides state-of-the-art performance, a larger variant than 'n'.",
        "yolo11m-cls.pt": "Ultralytics YOLO11 model for Image Classification. Provides state-of-the-art performance, a medium variant.",
        "yolo11l-cls.pt": "Ultralytics YOLO11 model for Image Classification. Provides state-of-the-art performance, a larger variant than 'm'.",
        "yolo11x-cls.pt": "Ultralytics YOLO11 model for Image Classification. Provides state-of-the-art performance (highest accuracy of YOLO11 Classify).",
        "yolov8n.pt": "General-purpose real-time Ultralytics YOLOv8 model for Object Detection. Provides a good balance of accuracy and speed, suitable for resource-constrained tasks (smallest of YOLOv8 Detect).",
        "yolov8s.pt": "General-purpose real-time Ultralytics YOLOv8 model for Object Detection. Balances accuracy and speed, a larger variant than 'n'.",
        "yolov8m.pt": "General-purpose real-time Ultralytics YOLOv8 model for Object Detection. Balances accuracy and speed, a medium variant.",
        "yolov8l.pt": "General-purpose real-time Ultralytics YOLOv8 model for Object Detection. Balances accuracy and speed, a larger variant than 'm'.",
        "yolov8x.pt": "General-purpose real-time Ultralytics YOLOv8 model for Object Detection. Balances accuracy and speed (highest accuracy of YOLOv8 Detect).",
        "yolov8n-seg.pt": "General-purpose real-time Ultralytics YOLOv8 model for Instance Segmentation. Provides a good balance of accuracy and speed, suitable for resource-constrained tasks (smallest of YOLOv8 Seg).",
        "yolov8s-seg.pt": "General-purpose real-time Ultralytics YOLOv8 model for Instance Segmentation. Balances accuracy and speed, a larger variant than 'n'.",
        "yolov8m-seg.pt": "General-purpose real-time Ultralytics YOLOv8 model for Instance Segmentation. Balances accuracy and speed, a medium variant.",
        "yolov8l-seg.pt": "General-purpose real-time Ultralytics YOLOv8 model for Instance Segmentation. Balances accuracy and speed, a larger variant than 'm'.",
        "yolov8x-seg.pt": "General-purpose real-time Ultralytics YOLOv8 model for Instance Segmentation. Balances accuracy and speed (highest accuracy of YOLOv8 Seg).",
        "yolov8n-pose.pt": "General-purpose real-time Ultralytics YOLOv8 model for Pose Estimation / Keypoints detection. Suitable for resource-constrained tasks (smallest of YOLOv8 Pose).",
        "yolov8s-pose.pt": "General-purpose real-time Ultralytics YOLOv8 model for Pose Estimation / Keypoints detection. A larger variant than 'n'.",
        "yolov8m-pose.pt": "General-purpose real-time Ultralytics YOLOv8 model for Pose Estimation / Keypoints detection. A medium variant.",
        "yolov8l-pose.pt": "General-purpose real-time Ultralytics YOLOv8 model for Pose Estimation / Keypoints detection. A larger variant than 'm'.",
        "yolov8x-pose.pt": "General-purpose real-time Ultralytics YOLOv8 model for Pose Estimation / Keypoints detection. The largest variant.",
        "yolov8x-pose-p6.pt": "General-purpose real-time Ultralytics YOLOv8 model for Pose Estimation / Keypoints detection. Trained with 1280 input size.",
        "yolov8n-obb.pt": "General-purpose real-time Ultralytics YOLOv8 model for Oriented Object Detection (OBB). Suitable for resource-constrained tasks (smallest of YOLOv8 OBB).",
        "yolov8s-obb.pt": "General-purpose real-time Ultralytics YOLOv8 model for Oriented Object Detection (OBB). A larger variant than 'n'.",
        "yolov8m-obb.pt": "General-purpose real-time Ultralytics YOLOv8 model for Oriented Object Detection (OBB). A medium variant.",
        "yolov8l-obb.pt": "General-purpose real-time Ultralytics YOLOv8 model for Oriented Object Detection (OBB). A larger variant than 'm'.",
        "yolov8x-obb.pt": "General-purpose real-time Ultralytics YOLOv8 model for Oriented Object Detection (OBB). The largest variant.",
        "yolov8n-cls.pt": "General-purpose real-time Ultralytics YOLOv8 model for Image Classification. Suitable for resource-constrained tasks (smallest of YOLOv8 Classify).",
        "yolov8s-cls.pt": "General-purpose real-time Ultralytics YOLOv8 model for Image Classification. A larger variant than 'n'.",
        "yolov8m-cls.pt": "General-purpose real-time Ultralytics YOLOv8 model for Image Classification. A medium variant.",
        "yolov8l-cls.pt": "General-purpose real-time Ultralytics YOLOv8 model for Image Classification. A larger variant than 'm'.",
        "yolov8x-cls.pt": "General-purpose real-time Ultralytics YOLOv8 model for Image Classification. The largest variant.",
        "rtdetr-l.pt": "Realtime Detection Transformer (RT-DETR) by Baidu for Object Detection. Well-suited for applications requiring high accuracy and real-time performance (smaller variant).",
        "rtdetr-x.pt": "Realtime Detection Transformer (RT-DETR) by Baidu for Object Detection. Well-suited for applications requiring high accuracy and real-time performance (larger variant).",
        "sam_b.pt": "Segment Anything Model (SAM) by Meta. Provides unique automatic segmentation capabilities based on prompts.",
        "sam2_t.pt": "Segment Anything Model 2 (SAM2) by Meta. The next generation of SAM for video and images, provides automatic segmentation capabilities (smaller variant).",
        "sam2_b.pt": "Segment Anything Model 2 (SAM2) by Meta. The next generation of SAM for video and images, provides automatic segmentation capabilities (larger variant).",
        "mobile_sam.pt": "Mobile Segment Anything Model (MobileSAM). A mobile variant of SAM for segmentation.",
        "FastSAM-s.pt": "Fast Segment Anything Model (FastSAM). A segmentation model that is faster and more efficient than SAM. Supports segmentation based on text prompts or bounding boxes.",
        "yolo_nas_s.pt": "YOLO-NAS model (based on Neural Architecture Search) for Object Detection. Optimized for resource-constrained environments and focused on efficiency (smallest).",
        "yolo_nas_m.pt": "YOLO-NAS model (based on Neural Architecture Search) for Object Detection. Offers a balanced approach, suitable for general object detection with higher accuracy (medium).",
        "yolo_nas_l.pt": "YOLO-NAS model (based on Neural Architecture Search) for Object Detection. Designed for scenarios requiring the highest accuracy, where computational resources are less constrained (largest).",
        "yolov8s-world.pt": "YOLO-World model (based on YOLOv8) for Real-Time Open-Vocabulary Object Detection. Detects any objects based on text descriptions, effective for zero-shot tasks (smaller variant, no export support).",
        "yolov8s-worldv2.pt": "YOLO-World V2 model (based on YOLOv8) for Real-Time Open-Vocabulary Object Detection. Detects any objects based on text descriptions, effective for zero-shot tasks (smaller variant, with export support and deterministic training). Recommended for custom training.",
        "yolov8m-world.pt": "YOLO-World model (based on YOLOv8) for Real-Time Open-Vocabulary Object Detection. Detects any objects based on text descriptions, effective for zero-shot tasks (medium variant, no export support).",
        "yolov8m-worldv2.pt": "YOLO-World V2 model (based on YOLOv8) for Real-Time Open-Vocabulary Object Detection. Detects any objects based on text descriptions, effective for zero-shot tasks (medium variant, with export support and deterministic training).",
        "yolov8l-world.pt": "YOLO-World model (based on YOLOv8) for Real-Time Open-Vocabulary Object Detection. Detects any objects based on text descriptions, effective for zero-shot tasks (larger variant, no export support).",
        "yolov8l-worldv2.pt": "YOLO-World V2 model (based on YOLOv8) for Real-Time Open-Vocabulary Object Detection. Detects any objects based on text descriptions, effective for zero-shot tasks (larger variant, with export support and deterministic training).",
        "yolov8x-world.pt": "YOLO-World model (based on YOLOv8) for Real-Time Open-Vocabulary Object Detection. Detects any objects based on text descriptions, effective for zero-shot tasks (largest variant, no export support).",
        "yolov8x-worldv2.pt": "YOLO-World V2 model (based on YOLOv8) for Real-Time Open-Vocabulary Object Detection. Detects any objects based on text descriptions, effective for zero-shot tasks (largest variant, with export support and deterministic training).",
        "yoloe-11s-seg.pt": "Real-Time Open-Vocabulary YOLOE model for Instance Segmentation. Detects arbitrary classes using text/visual prompts (smallest).",
        "yoloe-11m-seg.pt": "Real-Time Open-Vocabulary YOLOE model for Instance Segmentation. Detects arbitrary classes using text/visual prompts (medium).",
        "yoloe-11l-seg.pt": "Real-Time Open-Vocabulary YOLOE model for Instance Segmentation. Detects arbitrary classes using text/visual prompts (largest).",
        "yoloe-v8s-seg.pt": "Real-Time Open-Vocabulary YOLOE model (based on YOLOv8) for Instance Segmentation. Detects arbitrary classes using text/visual prompts (smallest).",
        "yoloe-v8m-seg.pt": "Real-Time Open-Vocabulary YOLOE model (based on YOLOv8) for Instance Segmentation. Detects arbitrary classes using text/visual prompts (medium).",
        "yoloe-v8l-seg.pt": "Real-Time Open-Vocabulary YOLOE model (based on YOLOv8) for Instance Segmentation. Detects arbitrary classes using text/visual prompts (largest).",
        "yoloe-11s-seg-pf.pt": "Real-Time Open-Vocabulary Prompt-Free YOLOE model for Instance Segmentation. Detects objects from a large built-in vocabulary (smallest).",
        "yoloe-11m-seg-pf.pt": "Real-Time Open-Vocabulary Prompt-Free YOLOE model for Instance Segmentation. Detects objects from a large built-in vocabulary (medium).",
        "yoloe-11l-seg-pf.pt": "Real-Time Open-Vocabulary Prompt-Free YOLOE model for Instance Segmentation. Detects objects from a large built-in vocabulary (largest).",
        "yoloe-v8s-seg-pf.pt": "Real-Time Open-Vocabulary Prompt-Free YOLOE model (based on YOLOv8) for Instance Segmentation. Detects objects from a large built-in vocabulary (smallest).",
        "yoloe-v8m-seg-pf.pt": "Real-Time Open-Vocabulary Prompt-Free YOLOE model (based on YOLOv8) for Instance Segmentation. Detects objects from a large built-in vocabulary (medium).",
        "yoloe-v8l-seg-pf.pt": "Real-Time Open-Vocabulary Prompt-Free YOLOE model (based on YOLOv8) for Instance Segmentation. Detects objects from a large built-in vocabulary (largest).",
        "yolov10n.pt": "Real-Time End-to-End YOLOv10 model for Object Detection. Suitable for very resource-constrained environments (smallest).",
        "yolov10s.pt": "Real-Time End-to-End YOLOv10 model for Object Detection. Balances speed and accuracy.",
        "yolov10m.pt": "Real-Time End-to-End YOLOv10 model for Object Detection. Suitable for general use (medium).",
        "yolov10l.pt": "Real-Time End-to-End YOLOv10 model for Object Detection. High accuracy at the cost of computational resources.",
        "yolov10x.pt": "Real-Time End-to-End YOLOv10 model for Object Detection. Maximum accuracy and performance (largest).",
        "yolov3u.pt": "YOLOv3 model for Object Detection. An older but effective real-time model.",
        "yolov3-tinyu.pt": "YOLOv3-Tiny model for Object Detection. A very fast, lightweight version of YOLOv3.",
        "yolov3-sppu.pt": "YOLOv3-SPP model for Object Detection. A version of YOLOv3 with an SPP module for improved performance.",
        "yolov9t.pt": "YOLOv9 model for Object Detection. Uses PGI for data preservation, useful for lightweight models (smallest of YOLOv9 Detect).",
        "yolov9s.pt": "YOLOv9 model for Object Detection. Uses PGI for data preservation, useful for lightweight models (a larger variant than 't').",
        "yolov9m.pt": "YOLOv9 model for Object Detection. Uses PGI for data preservation, useful for lightweight models (medium).",
        "yolov9c.pt": "YOLOv9 model for Object Detection. Uses PGI for data preservation, useful for lightweight models (a larger variant than 'm').",
        "yolov9e.pt": "YOLOv9 model for Object Detection. Uses PGI for data preservation, useful for lightweight models (largest of YOLOv9 Detect).",
        "yolov9c-seg.pt": "YOLOv9 model for Instance Segmentation. Uses PGI for data preservation, useful for lightweight models (smaller variant).",
        "yolov9e-seg.pt": "YOLOv9 model for Instance Segmentation. Uses PGI for data preservation, useful for lightweight models (larger variant).",
        "yolo12n.pt": "YOLO12 'Attention-Centric' model for Object Detection. (Example provided only for the 'n' variant)."
    }

    # Create models directory if it doesn't exist
    models_dir = Path("models").resolve()
    os.makedirs(models_dir, exist_ok=True)
    logger.info(f"Ensured models directory exists: {models_dir}")

    descriptions_file = models_dir / "model_descriptions.json"
    existing_descriptions = {}

    # Read existing descriptions if the file exists
    if descriptions_file.exists():
        try:
            with open(descriptions_file, "r") as f:
                existing_descriptions = json.load(f)
            logger.info(f"Loaded existing model descriptions from: {descriptions_file}")
        except json.JSONDecodeError:
            logger.warning(f"Error decoding JSON from {descriptions_file}, starting with empty descriptions.")
            existing_descriptions = {}
        except Exception as e:
            logger.error(f"Error reading existing model descriptions from {descriptions_file}: {e}")
            existing_descriptions = {}

    # Merge new descriptions with existing ones
    # Existing descriptions take precedence to avoid overwriting custom ones
    merged_descriptions = model_descriptions.copy()
    merged_descriptions.update(existing_descriptions)

    # Write merged descriptions to JSON file
    logger.info(f"Writing merged model descriptions to: {descriptions_file}")
    try:
        with open(descriptions_file, "w") as f:
            json.dump(merged_descriptions, f, indent=2)
        logger.info(f"Model descriptions updated successfully at: {descriptions_file}")
        print(f"✅ Model descriptions updated at: {descriptions_file}")
        return str(descriptions_file)
    except Exception as e:
        logger.error(f"Error writing merged model descriptions to {descriptions_file}: {e}")
        print(f"❌ Failed to update model descriptions at: {descriptions_file}")
        return None


def main():
    logger.info(f"Running create_model_descriptions script from {Path(__file__).resolve()}")
    create_model_descriptions()
    logger.info("create_model_descriptions script finished")


if __name__ == "__main__":
    main()


================================================
FILE: src/imagesorcery_mcp/scripts/download_clip.py
================================================
#!/usr/bin/env python3
"""
Script to download CLIP models required for YOLOe text prompts.
"""

import os
import sys
from pathlib import Path

import requests
from tqdm import tqdm

# Import the central logger
from imagesorcery_mcp.logging_config import logger


def get_models_dir():
    """Get the models directory in the project root."""
    models_dir = Path("models").resolve()
    os.makedirs(models_dir, exist_ok=True)
    logger.info(f"Ensured models directory exists: {models_dir}")
    return models_dir


def download_file(url, output_path):
    """Download a file from a URL with progress bar."""
    logger.info(f"Attempting to download file from {url} to {output_path}")
    try:
        response = requests.get(url, stream=True)
        response.raise_for_status()
        
        total_size = int(response.headers.get('content-length', 0))
        block_size = 1024  # 1 Kibibyte
        
        with open(output_path, 'wb') as file, tqdm(
            desc=f"Downloading to {os.path.basename(output_path)}",
            total=total_size,
            unit='iB',
            unit_scale=True,
            unit_divisor=1024,
        ) as bar:
            for data in response.iter_content(block_size):
                size = file.write(data)
                bar.update(size)
        
        logger.info(f"Successfully downloaded file to {output_path}")
        return True
    except Exception as e:
        logger.error(f"Error downloading from {url}: {str(e)}")
        return False


def download_clip_model():
    """Download the MobileCLIP model required for YOLOe text prompts."""
    logger.info("Attempting to download CLIP model")
    root_clip_model_path = Path("mobileclip_blt.ts").resolve()

    # Check if model already exists in root directory
    if root_clip_model_path.exists():
        logger.info(f"CLIP model already exists at: {root_clip_model_path}")
        return True

    # URL for the MobileCLIP model
    url = "https://github.com/ultralytics/assets/releases/download/v8.3.0/mobileclip_blt.ts"

    # Download directly to root directory
    logger.info(f"Downloading CLIP model to root directory from: {url}")
    success = download_file(url, root_clip_model_path)
    if success:
        logger.info(f"CLIP model successfully downloaded to: {root_clip_model_path}")
        return True
    else:
        logger.error(f"Failed to download CLIP model to: {root_clip_model_path}")
        return False


def main():
    """Main function to download CLIP models."""
    logger.info(f"Running download_clip_models script from {Path(__file__).resolve()}")
    
    # Download the MobileCLIP model
    if download_clip_model():
        logger.info("CLIP model downloaded successfully")
        print("✅ CLIP model download completed successfully")
    else:
        logger.error("Failed to download CLIP model")
        print("❌ Failed to download CLIP model")
        sys.exit(1)
    
    logger.info("download_clip_models script finished")


if __name__ == "__main__":
    main()


================================================
FILE: src/imagesorcery_mcp/scripts/download_models.py
================================================
#!/usr/bin/env python3
"""
Script to download YOLO compatible models for offline use.
This script should be run during project setup to ensure models are available.
"""

import argparse
import json
import os
import shutil
import sys
from pathlib import Path

import requests
from tqdm import tqdm

# Import the central logger
from imagesorcery_mcp.logging_config import logger


def get_models_dir():
    """Get the models directory in the project root."""
    models_dir = Path("models").resolve()
    os.makedirs(models_dir, exist_ok=True)
    logger.info(f"Ensured models directory exists: {models_dir}")
    return str(models_dir)


def download_from_url(url, output_path):
    """Download a file from a URL with progress bar."""
    logger.info(f"Attempting to download file from {url} to {output_path}")
    try:
        response = requests.get(url, stream=True)
        response.raise_for_status()
        
        total_size = int(response.headers.get('content-length', 0))
        block_size = 1024  # 1 Kibibyte
        
        with open(output_path, 'wb') as file, tqdm(
            desc=f"Downloading to {os.path.basename(output_path)}",
            total=total_size,
            unit='iB',
            unit_scale=True,
            unit_divisor=1024,
        ) as bar:
            for data in response.iter_content(block_size):
                size = file.write(data)
                bar.update(size)
        
        logger.info(f"Successfully downloaded file to {output_path}")
        return True
    except Exception as e:
        logger.error(f"Error downloading from {url}: {str(e)}")
        return False


def download_from_huggingface(model_name):
    """Download a model from Hugging Face."""
    logger.info(f"Attempting to download model from Hugging Face: {model_name}")
    # Extract repo_id and model filename
    if "/" not in model_name:
        logger.error("Invalid Hugging Face model format. Use 'username/repo:filename' or 'username/repo'")
        return False
    
    parts = model_name.split(":", 1)
    repo_id = parts[0]
    filename = parts[1] if len(parts) > 1 else None
    
    # Default description
    model_description = f"Model from Hugging Face repository: {repo_id}"
    
    # Try to get model description
    try:
        from huggingface_hub import model_info
        info = model_info(repo_id)
        if info.cardData and "model-index" in info.cardData:
            model_index = info.cardData["model-index"]
            if model_index and len(model_index) > 0 and "name" in model_index[0]:
                model_description = model_index[0].get('name', model_description)
                logger.info(f"Fetched model description: {model_description}")
        elif info.description:
            # Extract first line or first 100 characters of description
            description = info.description.split('\n')[0][:100]
            if len(info.description) > 100:
                description += "..."
            model_description = description
            logger.info(f"Fetched model description: {model_description}")
    except Exception as e:
        logger.warning(f"Could not fetch model description: {str(e)}")
    
    # If no specific filename provided, try to find a .pt file
    if filename is None:
        try:
            from huggingface_hub import list_repo_files
            files = list_repo_files(repo_id)
            pt_files = [f for f in files if f.endswith('.pt')]
            if not pt_files:
                logger.error(f"No .pt files found in {repo_id}")
                return False
            filename = pt_files[0]
            logger.info(f"Found model file in repository: {filename}")
        except Exception as e:
            logger.error(f"Error listing files in repository: {str(e)}")
            return False
    
    # Create directory structure based on repo_id
    models_dir = get_models_dir()
    repo_dir = os.path.join(models_dir, repo_id.replace("/", os.sep))
    os.makedirs(repo_dir, exist_ok=True)
    logger.info(f"Ensured repository directory exists: {repo_dir}")
    
    # Set the output path
    output_filename = os.path.basename(filename)
    output_path = os.path.join(repo_dir, output_filename)
    
    # Update model_descriptions.json with the model description
    model_key = f"{repo_id}/{output_filename}"
    update_model_description(model_key, model_description)
    
    # Check if model already exists
    if os.path.exists(output_path):
        logger.info(f"Model already exists at: {output_path}")
        return True
    
    # Construct the download URL
    url = f"https://huggingface.co/{repo_id}/resolve/main/{filename}"
    
    logger.info(f"Downloading from Hugging Face: {repo_id}/{filename}")
    logger.info(f"Saving to: {output_path}")
    return download_from_url(url, output_path)


def update_model_description(model_key, description):
    """Update the model_descriptions.json file with a new model description."""
    logger.info(f"Updating model description for {model_key}")
    models_dir = get_models_dir()
    descriptions_file = os.path.join(models_dir, "model_descriptions.json")
    
    # Load existing descriptions or create new if file doesn't exist
    if os.path.exists(descriptions_file):
        try:
            with open(descriptions_file, 'r') as f:
                descriptions = json.load(f)
            logger.info(f"Loaded existing model descriptions from {descriptions_file}")
        except json.JSONDecodeError:
            logger.warning("Error reading model_descriptions.json, creating new file")
            descriptions = {}
    else:
        logger.info("model_descriptions.json not found, creating new file")
        descriptions = {}
    
    # Update the description for this model
    if model_key not in descriptions:
        descriptions[model_key] = description
        logger.info(f"Added description for {model_key} to model_descriptions.json")
    elif descriptions[model_key] != description:
        descriptions[model_key] = description
        logger.info(f"Updated description for {model_key} in model_descriptions.json")
    else:
        logger.info(f"Description for {model_key} is already up to date")
    
    # Save the updated descriptions
    try:
        with open(descriptions_file, 'w') as f:
            json.dump(descriptions, f, indent=2, sort_keys=True)
        logger.info(f"Saved updated model descriptions to {descriptions_file}")
    except Exception as e:
        logger.error(f"Error updating model_descriptions.json: {str(e)}")

def download_ultralytics_model(model_name):
    """Download a specific YOLO model from Ultralytics to the models directory."""
    logger.info(f"Attempting to download Ultralytics model: {model_name}")
    try:
        # Get the models directory
        models_dir = get_models_dir()
        
        # Set the output path
        output_path = os.path.join(models_dir, model_name)
        
        # Check if model already exists in models directory
        if os.path.exists(output_path):
            logger.info(f"Model already exists at: {output_path}")
            return True

        # Set environment variable to use the models directory
        os.environ["YOLO_CONFIG_DIR"] = models_dir
        logger.info(f"Set YOLO_CONFIG_DIR environment variable to: {models_dir}")

        # Import and download the model
        from ultralytics import YOLO

        logger.info(f"Downloading {model_name} model using Ultralytics library...")

        # The model variable is used to trigger the download
        model = YOLO(model_name)  # noqa: F841

        # Check if the model was downloaded to the expected location
        if os.path.exists(output_path):
            logger.info(f"Model successfully downloaded to expected path: {output_path}")
            return True

        # Check if model was downloaded to current directory
        current_dir_model = Path(model_name)
        if current_dir_model.exists():
            logger.info(f"Model found in current directory: {current_dir_model.resolve()}")
            try:
                # Move the model to the models directory
                shutil.move(str(current_dir_model), output_path)
                logger.info(f"Model moved to: {output_path}")
                return True
            except Exception as e:
                logger.warning(f"Could not move model from {current_dir_model.resolve()} to {output_path}: {e}")
                logger.info(f"You can still use the model from: {current_dir_model.resolve()}")
                return True

        # If not found in expected locations,
        # try to find it in ultralytics default location
        possible_locations = [
            Path.home() / ".ultralytics" / "weights" / model_name,
            Path(os.path.dirname(os.path.abspath(__file__))) / "weights" / model_name,
        ]

        # Try to import ultralytics to find its location
        try:
            import ultralytics

            ultralytics_dir = Path(ultralytics.__file__).parent
            possible_locations.append(ultralytics_dir / "weights" / model_name)
        except ImportError:
            logger.warning("Could not import ultralytics to find default weights location")

        # Check each location
        for loc in possible_locations:
            if loc.exists():
                logger.info(f"Model found at a different location: {loc.resolve()}")
                try:
                    shutil.copy(loc, output_path)
                    logger.info(f"Model copied to: {output_path}")
                    return True
                except Exception as e:
                    logger.warning(f"Could not copy model from {loc.resolve()} to {output_path}: {e}")
                    logger.error(
                        f"Please manually copy the model from {loc.resolve()} to {output_path}"
                    )
                    return False

        logger.error(f"Failed to download model to expected path: {output_path}")
        return False

    except Exception as e:
        logger.error(f"Error downloading model: {str(e)}")
        return False


def download_model(model_name, source=None):
    """
    Legacy function for backward compatibility.
    Downloads a model from the specified source.
    """
    logger.info(f"Legacy download_model called for {model_name} from source {source}")
    if source == "ultralytics":
        return download_ultralytics_model(model_name)
    elif source == "huggingface":
        return download_from_huggingface(model_name)
    else:
        logger.error(f"Unknown model source: {source}")
        return False


def main():
    logger.info(f"Running download_models script from {Path(__file__).resolve()}")
    parser = argparse.ArgumentParser(
        description="Download YOLO compatible models for offline use"
    )
    
    # Create a mutually exclusive group for the model sources
    source_group = parser.add_mutually_exclusive_group(required=True)
    source_group.add_argument(
        "--ultralytics",
        metavar="MODEL_NAME",
        help="Download a model from Ultralytics (e.g., 'yolov8m.pt')"
    )
    source_group.add_argument(
        "--huggingface",
        metavar="REPO_ID[:FILENAME]",
        help="Download a model from Hugging Face (e.g., 'username/repo:model.pt' or 'username/repo')"
    )

    args = parser.parse_args()
    
    if args.ultralytics:
        logger.info(f"Downloading Ultralytics model: {args.ultralytics}")
        success = download_ultralytics_model(args.ultralytics)
    elif args.huggingface:
        logger.info(f"Downloading Hugging Face model: {args.huggingface}")
        success = download_from_huggingface(args.huggingface)
    else:
        # This should never happen due to the required=True in the mutually_exclusive_group
        logger.error("No model source specified")
        success = False
    
    if not success:
        logger.error("Model download failed")
        sys.exit(1)
    else:
        logger.info("Model download completed successfully")

    logger.info("download_models script finished")


if __name__ == "__main__":
    main()


================================================
FILE: src/imagesorcery_mcp/scripts/populate_telemetry_keys.py
================================================
#!/usr/bin/env python3
"""Build script to populate src/imagesorcery_mcp/telemetry_keys.py with API keys from environment variables or .env file."""

import logging
import os
import sys
from pathlib import Path
from typing import Dict

try:
    from dotenv import load_dotenv
    DOTENV_AVAILABLE = True
except ImportError:
    DOTENV_AVAILABLE = False

# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# Telemetry keys file path
TELEMETRY_KEYS_FILE = Path('src/imagesorcery_mcp/telemetry_keys.py')

def get_telemetry_keys() -> Dict[str, str]:
    """Get telemetry API keys from environment variables and .env file.

    Priority order:
    1. Environment variables
    2. .env file

    Returns:
        Dictionary containing telemetry API keys
    """
    keys = {}

    # Load .env file if available
    if DOTENV_AVAILABLE:
        env_file = Path('.env')
        if env_file.exists():
            load_dotenv(env_file)
            logger.debug("Loaded .env file")

    # Get Amplitude API key (env var takes priority)
    amplitude_key = os.environ.get('IMAGESORCERY_AMPLITUDE_API_KEY', '')
    keys['AMPLITUDE_API_KEY'] = amplitude_key
    if amplitude_key:
        logger.debug("Found IMAGESORCERY_AMPLITUDE_API_KEY")

    # Get PostHog API key (env var takes priority)
    posthog_key = os.environ.get('IMAGESORCERY_POSTHOG_API_KEY', '')
    keys['POSTHOG_API_KEY'] = posthog_key
    if posthog_key:
        logger.debug("Found IMAGESORCERY_POSTHOG_API_KEY")

    return keys

def write_telemetry_keys_file(keys: Dict[str, str]) -> bool:
    """Write the telemetry_keys.py file with provided keys.

    Args:
        keys: Dict with 'AMPLITUDE_API_KEY' and 'POSTHOG_API_KEY'

    Returns:
        True if successful, False otherwise
    """
    try:
        content = f'''# Auto-generated telemetry keys module.
# This file is intended to be updated by build scripts (populate_telemetry_keys.py)
# and cleared by clear_telemetry_keys.py. Keep values as empty strings in the repo.
#
# WARNING: Do NOT commit real production keys to the repository.

AMPLITUDE_API_KEY = "{keys.get("AMPLITUDE_API_KEY", "")}"
POSTHOG_API_KEY = "{keys.get("POSTHOG_API_KEY", "")}"
'''
        TELEMETRY_KEYS_FILE.write_text(content)
        logger.info(f"Wrote telemetry keys to {TELEMETRY_KEYS_FILE}")
        return True
    except Exception as e:
        logger.error(f"Failed to write telemetry keys file: {e}")
        return False

def main():
    """Main entry point to populate telemetry_keys.py."""
    logger.info("Starting telemetry keys population process...")

    # Optionally skip
    if os.environ.get('SKIP_TELEMETRY_POPULATION', '').lower() in ('true', '1', 'yes'):
        logger.info("Telemetry population skipped via SKIP_TELEMETRY_POPULATION environment variable")
        return 0

    keys = get_telemetry_keys()

    found_keys = [k for k, v in keys.items() if v]
    empty_keys = [k for k, v in keys.items() if not v]

    if found_keys:
        logger.info(f"Found telemetry keys: {', '.join(found_keys)}")
    if empty_keys:
        logger.info(f"Empty telemetry keys (will remain empty): {', '.join(empty_keys)}")

    if write_telemetry_keys_file(keys):
        logger.info("Telemetry keys population completed successfully")
        return 0
    else:
        logger.error("Telemetry keys population failed")
        return 1

if __name__ == '__main__':
    sys.exit(main())


================================================
FILE: src/imagesorcery_mcp/scripts/post_install.py
================================================
#!/usr/bin/env python3
"""
Script to run post-installation tasks for imagesorcery-mcp.
This script creates the models directory, model descriptions file,
and downloads default models.
"""

import os
import subprocess  # Ensure subprocess is imported
import sys  # Ensure sys is imported
import uuid
from pathlib import Path

# Import the central logger
from imagesorcery_mcp.logging_config import logger

# For loading .env file
try:
    from dotenv import load_dotenv
    DOTENV_AVAILABLE = True
except ImportError:
    DOTENV_AVAILABLE = False
    logger.warning("python-dotenv not available. .env file will not be loaded automatically.")
from imagesorcery_mcp.scripts.create_model_descriptions import create_model_descriptions
from imagesorcery_mcp.scripts.download_clip import download_clip_model
from imagesorcery_mcp.scripts.download_models import download_ultralytics_model


def install_clip():
    """Install CLIP from the Ultralytics GitHub repository."""
    logger.info("Installing CLIP package from GitHub...")
    
    try:
        subprocess.run(
            [sys.executable, "-m", "pip", "install", "git+https://github.com/ultralytics/CLIP.git"],
            check=True,
            stdout=sys.stdout, # Can be replaced with subprocess.PIPE if console output is not needed
            stderr=subprocess.PIPE  # Capture stderr to analyze it
        )
        logger.info("CLIP package installed successfully")
        print("✅ CLIP package installed successfully")
        return True
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to install CLIP: {e}")
        error_message = f"❌ Failed to install CLIP package: {e}"
        detailed_warning = ""
        if e.stderr:
            try:
                stderr_output = e.stderr.decode(errors='ignore')
                logger.debug(f"Captured stderr from CLIP installation attempt: {stderr_output}")
                if "No module named pip" in stderr_output:
                    detailed_warning = (
                        "\n   Hint: The Python environment (potentially created by 'uvx' or a minimal 'uv venv') might be missing 'pip'."
                        "\n   To ensure 'clip' package installation for full functionality (e.g., text prompts in 'find' tool):"
                        "\n     1. Recommended: Use 'python -m venv' to create a virtual environment, then 'pip install imagesorcery-mcp' and 'imagesorcery-mcp --post-install'."
                        "\n     2. Or, manually install 'clip' into your active environment: pip install git+https://github.com/ultralytics/CLIP.git"
                        "\n        (If using 'uv venv', you might need: uv pip install git+https://github.com/ultralytics/CLIP.git)"
                    )
            except Exception as decode_exc:
                logger.error(f"Error while decoding/processing stderr for CLIP install: {decode_exc}")
        
        print(error_message + detailed_warning)
        return False
    except FileNotFoundError: # Handle case where pip or python executable is not found
        logger.error("Failed to install CLIP: Python executable or pip not found.")
        print("❌ Failed to install CLIP package: Python executable or pip not found. Ensure Python is in PATH and pip is installed.")
        return False


def create_config_file():
    """Ensure config.toml exists, create with default values if needed."""
    config_file = Path("config.toml")
    if config_file.exists():
        logger.info(f"⏩ Configuration file already exists: {config_file}")
        return True

    logger.info("Creating config.toml using configuration system defaults")
    # The config manager will create it with defaults if it doesn't exist
    from imagesorcery_mcp.config import get_config_manager
    get_config_manager()
    print(f"✅ Configuration file created with default values: {config_file}")
    return True


def create_user_id_file():
    """Ensure .user_id file exists in the project root, create a new UUID if needed."""
    user_id_file = Path(".user_id")
    if user_id_file.exists():
        logger.info(f"⏩ .user_id file already exists: {user_id_file}")
        return True

    logger.info("Creating .user_id file for telemetry...")
    try:
        user_id = str(uuid.uuid4())
        user_id_file.write_text(user_id)
        logger.info(f"Generated new .user_id file with user_id: {user_id_file}")
        print(f"✅ .user_id file created: {user_id_file}")
        return True
    except Exception as e:
        logger.error(f"Failed to create .user_id file: {e}")
        print(f"❌ Failed to create .user_id file: {e}")
        return False


def run_post_install():
    """Run all post-installation tasks."""
    logger.info(f"Running post-installation tasks from {Path(__file__).resolve()}...")

    # Get API keys from environment variables (for Step 4)
    amplitude_api_key = os.environ.get("IMAGESORCERY_AMPLITUDE_API_KEY", "")
    posthog_api_key = os.environ.get("IMAGESORCERY_POSTHOG_API_KEY", "")
    
    logger.debug(f"Amplitude API key from environment: {'*' * 8 if amplitude_api_key else 'Not set'}")
    logger.debug(f"PostHog API key from environment: {'*' * 8 if posthog_api_key else 'Not set'}")

    # Create configuration file
    logger.info("Creating configuration file...")
    config_created = create_config_file()
    if not config_created:
        logger.error("Failed to create configuration file")
        return False

    # Create user ID file for telemetry
    logger.info("Creating user ID file...")
    user_id_created = create_user_id_file()
    if not user_id_created:
        logger.error("Failed to create user ID file")
        # Do not return False here, as telemetry is not critical for core functionality
        # and other post-install tasks should still proceed.

    # Create models directory
    models_dir = Path("models").resolve()
    os.makedirs(models_dir, exist_ok=True)
    logger.info(f"Created models directory: {models_dir}")

    # Create model descriptions file
    logger.info("Creating model descriptions file...")
    descriptions_file = create_model_descriptions()
    if descriptions_file:
        logger.info(f"Model descriptions file created at: {descriptions_file}")
    else:
        logger.error("Failed to create model descriptions file")
        return False

    # Download default Ultralytics YOLO models
    default_models = [
        "yoloe-11l-seg-pf.pt",
        "yoloe-11s-seg-pf.pt",
        "yoloe-11l-seg.pt",
        "yoloe-11s-seg.pt"
    ]
    
    logger.info("Downloading default Ultralytics YOLO models...")
    for model in default_models:
        logger.info(f"Downloading {model}...")
        success = download_ultralytics_model(model)
        if not success:
            logger.error(f"Failed to download model: {model}")
            return False
    print("✅ Ultralytics YOLO models download completed successfully")

    # Install CLIP package
    logger.info("Installing CLIP package for text prompts...")
    clip_installed_successfully = install_clip()
    if not clip_installed_successfully:
        logger.warning("CLIP Python package installation failed. The 'find' tool's text prompt functionality might be limited or unavailable.")
        print("⚠️ WARNING: CLIP Python package installation failed. Text prompt features of the 'find' tool may not work.")
        print("   Models for CLIP will still be downloaded. If you need this functionality, please try installing the CLIP package manually:")
        print("   pip install git+https://github.com/ultralytics/CLIP.git")
        # We continue with the rest of the post-installation, especially downloading CLIP models.
    
    # Download CLIP model
    logger.info("Downloading CLIP model for text prompts...")
    try:
        # Download the CLIP model file
        success = download_clip_model()
        if not success:
            logger.error("Failed to download CLIP model")
            return False
    except Exception as e:
        logger.error(f"Error downloading CLIP model: {str(e)}")
        return False
    print("✅ CLIP model download completed successfully")
    
    logger.info("Post-installation tasks completed successfully!")
    print("✅ Post-installation tasks completed successfully!")
    return True


def main():
    """Main entry point for the post_install script."""
    # Load .env file if available
    if DOTENV_AVAILABLE:
        env_file = Path(".env")
        if env_file.exists():
            load_dotenv()
            logger.info(f"Loaded environment variables from {env_file}")
        else:
            logger.info(".env file not found, skipping dotenv loading")
    else:
        logger.info("python-dotenv not available, skipping .env file loading")
    
    logger.info(f"Starting post-installation process from {Path(__file__).resolve()}")
    success = run_post_install()
    if not success:
        logger.error("Post-installation process failed")
        sys.exit(1)
    logger.info("Post-installation process completed")


if __name__ == "__main__":
    main()


================================================
FILE: src/imagesorcery_mcp/server.py
================================================
import argparse
import os
import sys
from pathlib import Path

from fastmcp import FastMCP
from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware

from imagesorcery_mcp.logging_config import logger

# Change to project root directory
project_root = Path(__file__).parent.parent.parent
os.chdir(project_root)
logger.info(f"Changed current working directory to: {project_root}")

# Load environment variables from .env if python-dotenv is available (so handlers see keys on import)
try:
    from dotenv import load_dotenv  # type: ignore
    env_file = project_root / ".env"
    if env_file.exists():
        load_dotenv(env_file)
        logger.info(f"Loaded environment variables from: {env_file}")
    else:
        logger.debug(".env file not found, skipping dotenv loading")
except Exception:
    logger.debug("python-dotenv not available, skipping .env loading")

from imagesorcery_mcp.middlewares.path_access import PathAccessMiddleware  # noqa: E402
from imagesorcery_mcp.middlewares.telemetry import TelemetryMiddleware  # noqa: E402
from imagesorcery_mcp.middlewares.validation import (  # noqa: E402
    ImprovedValidationMiddleware,  # noqa: E402
)
from imagesorcery_mcp.prompts import remove_background  # noqa: E402
from imagesorcery_mcp.resources import models  # noqa: E402
from imagesorcery_mcp.tools import (  # noqa: E402
    blur,
    change_color,
    config,
    crop,
    detect,
    draw_arrows,
    draw_circle,
    draw_lines,
    draw_rectangle,
    draw_text,
    fill,
    find,
    metainfo,
    ocr,
    overlay,
    resize,
    rotate,
)

# Create a module-level mcp instance for backward compatibility with tests
mcp = FastMCP(
    name="imagesorcery-mcp",
    instructions=(
        "An MCP server providing tools for image processing operations. "
        "Input images must be specified with full paths."
    )
)

error_middleware = ErrorHandlingMiddleware(
    logger=logger,
    include_traceback=True,
    transform_errors=True,
)
mcp.add_middleware(error_middleware)

telemetry_middleware = TelemetryMiddleware(logger=logger)
mcp.add_middleware(telemetry_middleware)

path_access_middleware = PathAccessMiddleware(logger=logger)
mcp.add_middleware(path_access_middleware)

validation_middleware = ImprovedValidationMiddleware(logger=logger)
mcp.add_middleware(validation_middleware)

# Register tools with the module-level mcp instance
blur.register_tool(mcp)
change_color.register_tool(mcp)
config.register_tool(mcp)
crop.register_tool(mcp)
detect.register_tool(mcp)
draw_arrows.register_tool(mcp)
draw_circle.register_tool(mcp)
draw_lines.register_tool(mcp)
draw_rectangle.register_tool(mcp)
draw_text.register_tool(mcp)
fill.register_tool(mcp)
find.register_tool(mcp)
metainfo.register_tool(mcp)
ocr.register_tool(mcp)
overlay.register_tool(mcp)
resize.register_tool(mcp)
rotate.register_tool(mcp)

# Register resources
models.register_resource(mcp)

# Register prompts
remove_background.register_prompt(mcp)

def parse_arguments():
    """Parse command line arguments."""
    parser = argparse.ArgumentParser(description="ImageSorcery MCP Server")
    parser.add_argument(
        "--post-install", 
        action="store_true", 
        help="Run post-installation tasks and exit"
    )
    parser.add_argument(
        "--transport",
        type=str,
        default="stdio",
        choices=["stdio", "streamable-http", "sse"],
        help="Transport protocol to use (default: stdio)"
    )
    parser.add_argument(
        "--host",
        type=str,
        default="127.0.0.1",
        help="Host to bind to when using HTTP-based transports (default: 127.0.0.1)"
    )
    parser.add_argument(
        "--port",
        type=int,
        default=8000,
        help="Port to bind to when using HTTP-based transports (default: 8000)"
    )
    parser.add_argument(
        "--path",
        type=str,
        default="/mcp",
        help="Path for the MCP endpoint when using HTTP-based transports (default: /mcp)"
    )
    return parser.parse_args()

def main():
    """Main entry point for the server."""
    args = parse_arguments()
    
    logger.info("Starting 🪄 ImageSorcery MCP server setup")
    
    # Get version from package metadata
    try:
        from importlib.metadata import version
        package_version = version("imagesorcery-mcp")
        print(f"ImageSorcery MCP Version: {package_version}")
    except Exception as e:
        logger.error(f"Could not read version from package metadata: {e}")
        print("ImageSorcery MCP Version: unknown")

    
    # If --post-install flag is provided, run post-installation tasks and exit
    if args.post_install:
        logger.info("Post-installation flag detected, running post-installation tasks")
        try:
            from imagesorcery_mcp.scripts.post_install import run_post_install
            success = run_post_install()
            if not success:
                logger.error("Post-installation tasks failed")
                sys.exit(1)
            logger.info("Post-installation tasks completed successfully")
            sys.exit(0)
        except Exception as e:
            logger.error(f"Error during post-installation: {str(e)}")
            sys.exit(1)
    
    # For actual server execution, we'll use the global mcp instance
    logger.info(f"Starting MCP server with transport: {args.transport}")
    
    fastmcp_log_level = os.getenv("FASTMCP_LOG_LEVEL", "DEBUG")

    # Configure transport with appropriate parameters
    if args.transport in ["streamable-http", "sse"]:
        mcp.run(
            transport=args.transport,
            host=args.host,
            port=args.port,
            path=args.path,
            log_level=fastmcp_log_level
        )
    else:
        # Use default stdio transport
        mcp.run(log_level=fastmcp_log_level)

if __name__ == "__main__":
    main()


================================================
FILE: src/imagesorcery_mcp/telemetry_amplitude.py
================================================
import logging
import os
from typing import Any, Dict

from amplitude import Amplitude, BaseEvent

from imagesorcery_mcp.telemetry_keys import AMPLITUDE_API_KEY


class AmplitudeHandler:
    """Handles sending telemetry events to Amplitude."""

    def __init__(self, logger: logging.Logger | None = None):
        self.logger = logger or logging.getLogger("imagesorcery.telemetry.amplitude")
        self.logger.debug("Initializing Amplitude handler")
        
        api_key = self._get_api_key()
        
        if not api_key:
            self.amplitude = None
            self.logger.warning("Amplitude API key is not set. Amplitude telemetry will be disabled.")
            self.logger.debug("Amplitude telemetry disabled due to missing API key")
        else:
            self.amplitude = Amplitude(api_key)
            self.logger.info("Amplitude handler initialized.")
            self.logger.debug(f"Amplitude handler enabled with API key: {api_key}")

    def _get_api_key(self) -> str:
        """Get Amplitude API key.

        Priority:
        1. Environment variable IMAGESORCERY_AMPLITUDE_API_KEY
        2. Value from src/imagesorcery_mcp/telemetry_keys.py (AMPLITUDE_API_KEY)
        """
        return os.environ.get('IMAGESORCERY_AMPLITUDE_API_KEY', AMPLITUDE_API_KEY)

    def track_event(self, event_data: Dict[str, Any]):
        """
        Tracks an event using Amplitude.

        Args:
            event_data: A dictionary containing event properties.
                        Expected keys: 'user_id', 'action_type', 'identifier', 'status', etc.
        """
        if not self.amplitude:
            self.logger.debug("Amplitude telemetry disabled, skipping event tracking")
            return
            
        # Skip telemetry if DISABLE_TELEMETRY environment variable is set
        if os.environ.get('DISABLE_TELEMETRY', '').lower() in ('true', '1', 'yes'):
            self.logger.debug("Amplitude telemetry disabled via environment variable")
            return

        try:
            user_id = event_data.get("user_id", "anonymous")
            event_type = f"mcp_{event_data.get('action_type', 'unknown_action')}"
            
            self.logger.debug(f"Preparing to track Amplitude event: {event_type} for user {user_id}")
            self.logger.debug(f"Event data: {event_data}")

            event = BaseEvent(event_type=event_type, user_id=user_id, event_properties=event_data)

            self.amplitude.track(event)
            self.logger.debug(f"Successfully tracked Amplitude event: {event_type} for user {user_id}")

        except Exception as e:
            self.logger.error(f"Failed to send event to Amplitude: {e}", exc_info=True)
            self.logger.debug(f"Event data that failed: {event_data}")


# Global instance to be used by other modules
amplitude_handler = AmplitudeHandler()


================================================
FILE: src/imagesorcery_mcp/telemetry_keys.py
================================================
# Auto-generated telemetry keys module.
# This file is intended to be updated by build scripts (populate_telemetry_keys.py)
# and cleared by clear_telemetry_keys.py. Keep values as empty strings in the repo.
#
# WARNING: Do NOT commit real production keys to the repository.

AMPLITUDE_API_KEY = ""
POSTHOG_API_KEY = ""


================================================
FILE: src/imagesorcery_mcp/telemetry_posthog.py
================================================
import logging
import os
from typing import Any, Dict

from posthog import Posthog

from imagesorcery_mcp.telemetry_keys import POSTHOG_API_KEY

POSTHOG_HOST = "https://us.i.posthog.com"


class PostHogHandler:
    """Handles sending telemetry events to PostHog."""

    def __init__(self, logger: logging.Logger | None = None):
        self.logger = logger or logging.getLogger("imagesorcery.telemetry.posthog")
        self.logger.debug("Initializing PostHog handler")
        
        api_key = self._get_api_key()
        
        if not api_key:
            self.enabled = False
            self.logger.warning("PostHog API key is not set. PostHog telemetry will be disabled.")
            self.logger.debug("PostHog telemetry disabled due to missing API key")
        else:
            self.enabled = True
            self.posthog_client = Posthog(api_key, host=POSTHOG_HOST)
            self.logger.info("PostHog handler initialized.")
            self.logger.debug(f"PostHog handler enabled with API key: {api_key}")

    def _get_api_key(self) -> str:
        """Get PostHog API key.

        Priority:
        1. Environment variable IMAGESORCERY_POSTHOG_API_KEY
        2. Value from src/imagesorcery_mcp/telemetry_keys.py (POSTHOG_API_KEY)
        """
        return os.environ.get('IMAGESORCERY_POSTHOG_API_KEY', POSTHOG_API_KEY)

    def track_event(self, event_data: Dict[str, Any]):
        """
        Tracks an event using PostHog.

        Args:
            event_data: A dictionary containing event properties.
                        Expected keys: 'user_id', 'action_type', 'identifier', 'status', etc.
        """
        if not self.enabled:
            self.logger.debug("PostHog telemetry disabled, skipping event tracking")
            return
            
        # Skip telemetry if DISABLE_TELEMETRY environment variable is set
        if os.environ.get('DISABLE_TELEMETRY', '').lower() in ('true', '1', 'yes'):
            self.logger.debug("Posthog telemetry disabled via environment variable")
            return

        try:
            user_id = event_data.get("user_id", "anonymous")
            event_type = f"mcp_{event_data.get('action_type', 'unknown_action')}"
            
            self.logger.debug(f"Preparing to track PostHog event: {event_type} for user {user_id}")
            self.logger.debug(f"Event data: {event_data}")

            self.posthog_client.capture(event_type, distinct_id=user_id, properties=event_data)
            self.logger.debug(f"Successfully tracked PostHog event: {event_type} for user {user_id}")

        except Exception as e:
            self.logger.error(f"Failed to send event to PostHog: {e}", exc_info=True)
            self.logger.debug(f"Event data that failed: {event_data}")


posthog_handler = PostHogHandler()


================================================
FILE: src/imagesorcery_mcp/tools/README.md
================================================
# 🪄 ImageSorcery MCP Server Tools Documentation

This document provides detailed information about each tool available in the 🪄 ImageSorcery MCP Server, including their arguments, return values, and examples of how to call them using a Claude client.

## Rules

These rules apply to all contributors: humans and AI.

- Register tools by defining a `register_tool` function in each tool's module. This function should accept a `FastMCP` instance and use the `@mcp.tool()` decorator to register the tool function with the server. See `src/imagesorcery_mcp/server.py` for how tools are imported and registered, and individual tool files like `src/imagesorcery_mcp/tools/crop.py` for examples of the `register_tool` function implementation.
- All tools should use Bounding Box format for image coordinates, e.g. `[x1, y1, x2, y2]` where `(x1, y1)` is the top-left corner and `(x2, y2)` is the bottom-right corner.
- All file paths specified in tool arguments (e.g., `input_path`, `output_path`) must be **full paths**, not relative paths. For example, use `/home/user/images/my_image.jpg` instead of `my_image.jpg`.
- When adding new tools, ensure they are listed in alphabetical order in READMEs and in the server registration.


## Available Tools

### `blur`

Blurs specified rectangular or polygonal areas of an image using OpenCV. This tool allows blurring multiple areas of an image with customizable blur strength. Each area can be a rectangle defined by a bounding box with coordinates `[x1, y1, x2, y2]` or a polygon defined by a list of points (in the same format as returned by `detect` or `find`). The `blur_strength` parameter controls the intensity of the blur effect. Higher values result in stronger blur. It must be an odd number (default is 15). If `invert_areas` is True, the tool will blur everything EXCEPT the specified areas.

- **Required arguments:**
  - `input_path` (string): Full path to the input image
  - `areas` (array): List of areas to blur. Each item is a dictionary that must contain either:
    - A rectangle: `x1`, `y1`, `x2`, `y2` (integers).
    - A polygon: `polygon` (a list of points, e.g., `[[x1, y1], [x2, y2], ...]`).
    - Optionally, each dictionary can also contain `blur_strength` (integer, default is 15).
- **Optional arguments:**
  - `invert_areas` (boolean): If True, blurs everything EXCEPT the specified areas. Useful for background blurring. Default is False.
  - `output_path` (string): Full path to save the output image. If not provided, will use input filename with '_blurred' suffix.
- **Returns:** string (path to the image with blurred areas)

**Example Claude Request:**

```
Blur the rectangular area from (150, 100) to (250, 200) and a triangular area in my image 'test_image.png' and save it as 'output.png'
```

**Example Tool Call (JSON):**

```json
{
  "name": "blur",
  "arguments": {
    "input_path": "/home/user/images/test_image.png",
    "areas": [
      {
        "x1": 150,
        "y1": 100,
        "x2": 250,
        "y2": 200,
        "blur_strength": 21
      },
      {
        "polygon": [[300, 50], [350, 50], [325, 150]],
        "blur_strength": 31
      }
    ],
    "output_path": "/home/user/images/output.png"
  }
}
```

**Example Claude Request (Background Blurring):**

```
Blur the background of 'my_image.png' by blurring everything outside the rectangle (100, 100) to (300, 300) with a blur strength of 25, and save it as 'object_focused.png'
```

**Example Tool Call (JSON):**

```json
{
  "name": "blur",
  "arguments": {
    "input_path": "/home/user/images/my_image.png",
    "areas": [
      {
        "x1": 100,
        "y1": 100,
        "x2": 300,
        "y2": 300,
        "blur_strength": 25
      }
    ],
    "invert_areas": true,
    "output_path": "/home/user/images/object_focused.png"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": "/home/user/images/output.png"
}
```

### `change_color`

Changes the color palette of an image. This tool applies a predefined color transformation to an image. Currently supported palettes are 'grayscale' and 'sepia'.

- **Required arguments:**
  - `input_path` (string): Full path to the input image
  - `palette` (string): The color palette to apply. Currently supports 'grayscale' and 'sepia'.
- **Optional arguments:**
  - `output_path` (string): Full path to save the output image. If not provided, will use input filename with a suffix based on the palette (e.g., '_grayscale').
- **Returns:** string (path to the image with the new color palette)

**Example Claude Request:**

```
Convert my image 'test_image.png' to sepia and save it as 'output.png'
```

**Example Tool Call (JSON):**

```json
{
  "name": "change_color",
  "arguments": {
    "input_path": "/home/user/images/test_image.png",
    "palette": "sepia",
    "output_path": "/home/user/images/output.png"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": "/home/user/images/output.png"
}
```

### `config`

View or update ImageSorcery MCP configuration settings. This tool allows you to view current configuration values, update them for the current session or persistently, and reset runtime overrides. Configuration values control default parameters for other tools like detection confidence thresholds, blur strength, drawing colors, etc.

- **Required arguments:**
  - `action` (string): Action to perform. Must be one of:
    - `"get"`: View configuration values
    - `"set"`: Update configuration values
    - `"reset"`: Reset runtime configuration overrides
- **Optional arguments:**
  - `key` (string): Configuration key to get/set using dot notation (e.g., "detection.confidence_threshold", "blur.strength"). Leave empty to get/set entire config.
  - `value` (string|number|boolean): Value to set (only used with action="set")
  - `persist` (boolean): Whether to persist changes to config file (only used with action="set"). Default is False.
- **Returns:** Dictionary containing the requested configuration data or update result

**Available Configuration Keys:**
- `detection.confidence_threshold`: Default confidence threshold for object detection (0.0-1.0)
- `detection.default_model`: Default model for detection tool
- `find.confidence_threshold`: Default confidence threshold for object finding (0.0-1.0)
- `find.default_model`: Default model for find tool
- `blur.strength`: Default blur strength (must be odd number)
- `text.font_scale`: Default font scale for text drawing
- `drawing.color`: Default color in BGR format [Blue, Green, Red]
- `drawing.thickness`: Default line thickness
- `ocr.language`: Default language code for OCR
- `resize.interpolation`: Default interpolation method

**Example Claude Requests:**

```
Show me the current configuration
```

```
What is the current detection confidence threshold?
```

```
Set the default blur strength to 21
```

```
Set the detection confidence to 0.8 and save it to the config file
```

```
Reset all configuration overrides
```

**Example Tool Calls (JSON):**

Get all configuration:
```json
{
  "name": "config",
  "arguments": {
    "action": "get"
  }
}
```

Get specific configuration value:
```json
{
  "name": "config",
  "arguments": {
    "action": "get",
    "key": "detection.confidence_threshold"
  }
}
```

Set configuration value (runtime only):
```json
{
  "name": "config",
  "arguments": {
    "action": "set",
    "key": "blur.strength",
    "value": 21,
    "persist": false
  }
}
```

Set and persist configuration value:
```json
{
  "name": "config",
  "arguments": {
    "action": "set",
    "key": "detection.confidence_threshold",
    "value": 0.8,
    "persist": true
  }
}
```

Reset runtime overrides:
```json
{
  "name": "config",
  "arguments": {
    "action": "reset"
  }
}
```

### `crop`

Crops an image using OpenCV's NumPy slicing approach.

- **Required arguments:**
  - `input_path` (string): Full path to the input image
  - `x1` (integer): X-coordinate of the top-left corner
  - `y1` (integer): Y-coordinate of the top-left corner
  - `x2` (integer): X-coordinate of the bottom-right corner
  - `y2` (integer): Y-coordinate of the bottom-right corner
- **Optional arguments:**
  - `output_path` (string): Full path to save the output image. If not provided, will use input filename with '_cropped' suffix.
- **Returns:** string (path to the cropped image)

**Example Claude Request:**

```
Crop my image 'input.png' using bounding box [10, 10, 200, 200] and save it as 'cropped.png'
```

**Example Tool Call (JSON):**

```json
{
  "name": "crop",
  "arguments": {
    "input_path": "/home/user/images/input.png",
    "x1": 10,
    "y1": 10,
    "x2": 200,
    "y2": 200,
    "output_path": "/home/user/images/cropped.png"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": "/home/user/images/cropped.png"
}
```

### `detect`

Detects objects in an image using models from Ultralytics. This tool requires pre-downloaded models. Use the `download-yolo-models` command to download models before using this tool. If objects aren't common, consider using a specialized model. This tool can optionally return segmentation masks (as PNG files) or polygons if a segmentation model (e.g., one ending in '-seg.pt') is used.

- **Required arguments:**
  - `input_path` (string): Full path to the input image
- **Optional arguments:**
  - `confidence` (float): Confidence threshold for detection (0.0 to 1.0). Default is 0.75
  - `model_name` (string): Model name to use for detection (e.g., 'yoloe-11s-seg.pt', 'yolov8m.pt'). Default is 'yoloe-11l-seg-pf.pt'
  - `return_geometry` (boolean): If True, returns segmentation masks or polygons for detected objects. Default is False.
  - `geometry_format` (string): Format for returned geometry: 'mask' or 'polygon'. Default is 'mask'. When 'mask' is selected, a PNG file is created for each mask and its path is returned.
- **Returns:** dictionary containing:
  - `image_path`: Path to the input image
  - `detections`: List of detected objects, each with:
    - `class`: Class name of the detected object
    - `confidence`: Confidence score (0.0 to 1.0)
    - `bbox`: Bounding box coordinates [x1, y1, x2, y2]
    - `mask_path` (optional): Path to the PNG file for the object's mask. Included if `return_geometry` is True and `geometry_format` is 'mask'.
    - `polygon` (optional): A list of points `[x, y]` describing the object's contour. Included if `return_geometry` is True and `geometry_format` is 'polygon'.

**Example Claude Request:**

```
Detect objects in my image 'photo.jpg' with a confidence threshold of 0.4
```

**Example Tool Call (JSON):**

```json
{
  "tool_code": "imagesorcery-mcp",
  "name": "detect",
  "arguments": {
    "input_path": "/home/user/images/photo.jpg",
    "confidence": 0.4,
    "return_geometry": true,
    "geometry_format": "polygon"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": {
    "image_path": "/home/user/images/photo.jpg",
    "detections": [
      {
        "class": "person",
        "confidence": 0.92,
        "bbox": [10.5, 20.3, 100.2, 200.1],
        "mask_path": "/home/user/images/photo_mask_0.png"
      },
      {
        "class": "car",
        "confidence": 0.85,
        "bbox": [150.2, 30.5, 250.1, 120.7],
        "mask_path": "/home/user/images/photo_mask_1.png"
      }
    ]
  }
}
```

### `draw_arrows`

Draws arrows on an image using OpenCV. This tool allows adding multiple arrows to an image with customizable start and end points, color, thickness, and tip length.

- **Required arguments:**
  - `input_path` (string): Full path to the input image
  - `arrows` (array): List of arrow items to draw. Each item should have:
    - `x1` (integer): X-coordinate of the arrow's start point
    - `y1` (integer): Y-coordinate of the arrow's start point
    - `x2` (integer): X-coordinate of the arrow's end point
    - `y2` (integer): Y-coordinate of the arrow's end point
    - `color` (array, optional): Color in BGR format [B,G,R]. Default is [0,0,0] (black)
    - `thickness` (integer, optional): Line thickness. Default is 1
    - `tip_length` (float, optional): Length of the arrow tip relative to the arrow length. Default is 0.1
- **Optional arguments:**
  - `output_path` (string): Full path to save the output image. If not provided, will use input filename with '_with_arrows' suffix

- **Returns:** string (path to the image with drawn arrows)

**Example Claude Request:**

```
Draw a red arrow from (50,50) to (150,100) and a blue arrow from (200,150) to (300,250) with a tip length of 0.15 on my image 'photo.jpg'
```

**Example Tool Call (JSON):**

```json
{
  "name": "draw_arrows",
  "arguments": {
    "input_path": "/home/user/images/photo.jpg",
    "arrows": [
      {
        "x1": 50,
        "y1": 50,
        "x2": 150,
        "y2": 100,
        "color": [0, 0, 255],
        "thickness": 2
      },
      {
        "x1": 200,
        "y1": 150,
        "x2": 300,
        "y2": 250,
        "color": [255, 0, 0],
        "thickness": 3,
        "tip_length": 0.15
      }
    ],
    "output_path": "/home/user/images/photo_with_arrows.jpg"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": "/home/user/images/photo_with_arrows.jpg"
}
```

### `draw_circles`

Draws circles on an image using OpenCV. This tool allows adding multiple circles to an image with customizable center, radius, color, thickness, and fill option. Each circle is defined by its center coordinates (center_x, center_y) and radius.

- **Required arguments:**
  - `input_path` (string): Full path to the input image
  - `circles` (array): List of circle items to draw. Each item should have:
    - `center_x` (integer): X-coordinate of the circle's center
    - `center_y` (integer): Y-coordinate of the circle's center
    - `radius` (integer): Radius of the circle
    - `color` (array, optional): Color in BGR format [B,G,R]. Default is [0,0,0] (black)
    - `thickness` (integer, optional): Line thickness. Default is 1. Use -1 for a filled circle.
    - `filled` (boolean, optional): Whether to fill the circle. Default is false. If true, thickness is set to -1.
- **Optional arguments:**
  - `output_path` (string): Full path to save the output image. If not provided, will use input filename with '_with_circles' suffix

- **Returns:** string (path to the image with drawn circles)

**Example Claude Request:**

```
Draw a red circle with center (100,100) and radius 50, and a filled blue circle with center (250,200) and radius 30 on my image 'photo.jpg'
```

**Example Tool Call (JSON):**

```json
{
  "name": "draw_circles",
  "arguments": {
    "input_path": "/home/user/images/photo.jpg",
    "circles": [
      {
        "center_x": 100,
        "center_y": 100,
        "radius": 50,
        "color": [0, 0, 255],
        "thickness": 2
      },
      {
        "center_x": 250,
        "center_y": 200,
        "radius": 30,
        "color": [255, 0, 0],
        "filled": true
      }
    ],
    "output_path": "/home/user/images/photo_with_circles.jpg"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": "/home/user/images/photo_with_circles.jpg"
}
```

### `draw_lines`

Draws lines on an image using OpenCV. This tool allows adding multiple lines to an image with customizable start and end points, color, and thickness.

- **Required arguments:**
  - `input_path` (string): Full path to the input image
  - `lines` (array): List of line items to draw. Each item should have:
    - `x1` (integer): X-coordinate of the line's start point
    - `y1` (integer): Y-coordinate of the line's start point
    - `x2` (integer): X-coordinate of the line's end point
    - `y2` (integer): Y-coordinate of the line's end point
    - `color` (array, optional): Color in BGR format [B,G,R]. Default is [0,0,0] (black)
    - `thickness` (integer, optional): Line thickness. Default is 1
- **Optional arguments:**
  - `output_path` (string): Full path to save the output image. If not provided, will use input filename with '_with_lines' suffix

- **Returns:** string (path to the image with drawn lines)

**Example Claude Request:**

```
Draw a red line from (50,50) to (150,100) and a blue line from (200,150) to (300,250) on my image 'photo.jpg'
```

**Example Tool Call (JSON):**

```json
{
  "name": "draw_lines",
  "arguments": {
    "input_path": "/home/user/images/photo.jpg",
    "lines": [
      {
        "x1": 50,
        "y1": 50,
        "x2": 150,
        "y2": 100,
        "color": [0, 0, 255],
        "thickness": 2
      },
      {
        "x1": 200,
        "y1": 150,
        "x2": 300,
        "y2": 250,
        "color": [255, 0, 0],
        "thickness": 3
      }
    ],
    "output_path": "/home/user/images/photo_with_lines.jpg"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": "/home/user/images/photo_with_lines.jpg"
}
```

### `draw_rectangles`

Draws rectangles on an image using OpenCV. This tool allows adding multiple rectangles to an image with customizable position, color, thickness, and fill option. Each rectangle is defined by two points: (x1, y1) for the top-left corner and (x2, y2) for the bottom-right corner.

- **Required arguments:**
  - `input_path` (string): Full path to the input image
  - `rectangles` (array): List of rectangle items to draw. Each item should have:
    - `x1` (integer): X-coordinate of the top-left corner
    - `y1` (integer): Y-coordinate of the top-left corner
    - `x2` (integer): X-coordinate of the bottom-right corner
    - `y2` (integer): Y-coordinate of the bottom-right corner
    - `color` (array, optional): Color in BGR format [B,G,R]. Default is [0,0,0] (black)
    - `thickness` (integer, optional): Line thickness. Default is 1
    - `filled` (boolean, optional): Whether to fill the rectangle. Default is false
- **Optional arguments:**
  - `output_path` (string): Full path to save the output image. If not provided, will use input filename with '_with_rectangles' suffix

- **Returns:** string (path to the image with drawn rectangles)

**Example Claude Request:**

```
Draw a red rectangle from (50,50) to (150,100) and a filled blue rectangle from (200,150) to (300,250) on my image 'photo.jpg'
```

**Example Tool Call (JSON):**

```json
{
  "name": "draw_rectangles",
  "arguments": {
    "input_path": "/home/user/images/photo.jpg",
    "rectangles": [
      {
        "x1": 50,
        "y1": 50,
        "x2": 150,
        "y2": 100,
        "color": [0, 0, 255],
        "thickness": 2
      },
      {
        "x1": 200,
        "y1": 150,
        "x2": 300,
        "y2": 250,
        "color": [255, 0, 0],
        "thickness": 3,
        "filled": true
      }
    ],
    "output_path": "/home/user/images/photo_with_rectangles.jpg"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": "/home/user/images/photo_with_rectangles.jpg"
}
```

### `draw_texts`

Draws text on an image using OpenCV. This tool allows adding multiple text elements to an image with customizable position, font, size, color, and thickness.

- **Required arguments:**
  - `input_path` (string): Full path to the input image
  - `texts` (array): List of text items to draw. Each item should have:
    - `text` (string): The text to draw
    - `x` (integer): X-coordinate for the text position
    - `y` (integer): Y-coordinate for the text position
    - `font_scale` (float, optional): Scale factor for the font. Default is 1.0
    - `color` (array, optional): Color in BGR format [B,G,R]. Default is [0,0,0] (black)
    - `thickness` (integer, optional): Line thickness. Default is 1
    - `font_face` (string, optional): Font face to use. Default is "FONT_HERSHEY_SIMPLEX". Available options: 'FONT_HERSHEY_SIMPLEX', 'FONT_HERSHEY_PLAIN', 'FONT_HERSHEY_DUPLEX', 'FONT_HERSHEY_COMPLEX', 'FONT_HERSHEY_TRIPLEX', 'FONT_HERSHEY_COMPLEX_SMALL', 'FONT_HERSHEY_SCRIPT_SIMPLEX', 'FONT_HERSHEY_SCRIPT_COMPLEX'
- **Optional arguments:**
  - `output_path` (string): Full path to save the output image. If not provided, will use input filename with '_with_text' suffix

- **Returns:** string (path to the image with drawn text)

**Example Claude Request:**

```
Add text 'Hello World' at position (50,50) and 'Copyright 2023' at the bottom right corner of my image 'photo.jpg'
```

**Example Tool Call (JSON):**

```json
{
  "name": "draw_texts",
  "arguments": {
    "input_path": "/home/user/images/photo.jpg",
    "texts": [
      {
        "text": "Hello World",
        "x": 50,
        "y": 50,
        "font_scale": 1.0,
        "color": [0, 0, 255],
        "thickness": 2
      },
      {
        "text": "Copyright 2023",
        "x": 100,
        "y": 150,
        "font_scale": 2.0,
        "color": [255, 0, 0],
        "thickness": 3,
        "font_face": "FONT_HERSHEY_COMPLEX"
      }
    ],
    "output_path": "/home/user/images/photo_with_text.jpg"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": "/home/user/images/photo_with_text.jpg"
}
```

### `fill`

Fills specified rectangular, polygonal, or mask-based areas of an image with a color and opacity, or makes them transparent. This tool allows filling multiple areas of an image with a customizable color and opacity. Each area can be a rectangle, a polygon, or a mask from a PNG file. The `opacity` parameter controls the transparency of the fill (1.0 is fully opaque, 0.0 is fully transparent, default is 0.5). The `color` is in BGR format, e.g., `[255, 0, 0]` for blue (default is `[0,0,0]` black).

**Special behavior**: If `color` is set to `null` (or `None` in Python), the specified area is made fully transparent by setting all channels (BGRA) to 0, effectively creating a black transparent color. This ensures better compatibility with older PNG viewers. The `opacity` parameter is ignored in this case.

- **Required arguments:**
  - `input_path` (string): Full path to the input image
  - `areas` (array): List of areas to fill. Each item is a dictionary that must contain one of:
    - A rectangle: `x1`, `y1`, `x2`, `y2` (integers).
    - A polygon: `polygon` (a list of points, e.g., `[[x1, y1], [x2, y2], ...]`).
    - A mask: `mask_path` (string path to a PNG mask file).
    - Optionally, each dictionary can also contain `color` (list of 3 ints [B,G,R] or `null`, default [0,0,0]) and `opacity` (float 0.0-1.0, default 0.5).
- **Optional arguments:**
  - `invert_areas` (boolean): If True, fills everything EXCEPT the specified areas. Useful for background removal. Default is False.
  - `output_path` (string): Full path to save the output image. If not provided, will use input filename with '_filled' suffix.
- **Returns:** string (path to the image with filled areas)

**Example Claude Request:**

```
Fill the rectangular area from (150, 100) to (250, 200) with semi-transparent red and erase the area [[10, 10], [50, 10], [50, 50], [10, 50]] in my image 'test_image.png' and save it as 'output.png'
```

**Example Tool Call (JSON):**

```json
{
  "name": "fill",
  "arguments": {
    "input_path": "/home/user/images/test_image.png",
    "areas": [
      {
        "x1": 150,
        "y1": 100,
        "x2": 250,
        "y2": 200,
        "color": [0, 0, 255],
        "opacity": 0.5
      },
      {
        "polygon": [[10, 10], [50, 10], [50, 50], [10, 50]],
        "color": null
      }
    ],
    "output_path": "/home/user/images/output.png"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": "/home/user/images/output.png"
}
```

**Example Claude Request (Background Removal):**

```
Remove the background from 'my_image.png' by making everything outside the rectangle (100, 100) to (300, 300) transparent, and save it as 'object_only.png'
```

**Example Tool Call (JSON):**

```json
{
  "name": "fill",
  "arguments": {
    "input_path": "/home/user/images/my_image.png",
    "areas": [
      {
        "x1": 100,
        "y1": 100,
        "x2": 300,
        "y2": 300,
        "color": null
      }
    ],
    "invert_areas": true,
    "output_path": "/home/user/images/object_only.png"
  }
}
```

### `find`

Finds objects in an image based on a text description. This tool uses open-vocabulary detection models to find objects matching a text description. It requires pre-downloaded YOLOE models that support text prompts (e.g. yoloe-11l-seg.pt). This tool can optionally return segmentation masks (as PNG files) or polygons.

- **Required arguments:**
  - `input_path` (string): Full path to the input image
  - `description` (string): Text description of the object to find
- **Optional arguments:**
  - `confidence` (float): Confidence threshold for detection (0.0 to 1.0). Default is 0.3
  - `model_name` (string): Model name to use for finding objects (must support text prompts). Default is 'yoloe-11l-seg.pt'
  - `return_all_matches` (boolean): If True, returns all matching objects; if False, returns only the best match. Default is False
  - `return_geometry` (boolean): If True, returns segmentation masks or polygons for found objects. Default is False.
  - `geometry_format` (string): Format for returned geometry: 'mask' or 'polygon'. Default is 'mask'. When 'mask' is selected, a PNG file is created for each mask and its path is returned.
- **Returns:** dictionary containing:
  - `image_path`: Path to the input image
  - `query`: The text description that was searched for
  - `found_objects`: List of found objects, each with:
    - `description`: The original search query
    - `match`: The class name of the matched object
    - `confidence`: Confidence score (0.0 to 1.0)
    - `bbox`: Bounding box coordinates [x1, y1, x2, y2]
    - `mask_path` (optional): Path to the PNG file for the object's mask. Included if `return_geometry` is True and `geometry_format` is 'mask'.
    - `polygon` (optional): A list of points `[x, y]` describing the object's contour. Included if `return_geometry` is True and `geometry_format` is 'polygon'.
  - `found`: Boolean indicating whether any objects were found

**Example Claude Request:**

```
Find all dogs in my image 'photo.jpg' with a confidence threshold of 0.4
```

**Example Tool Call (JSON):**

```json
{
  "name": "find",
  "arguments": {
    "input_path": "/home/user/images/photo.jpg",
    "description": "dog",
    "confidence": 0.4,
    "return_all_matches": true,
    "return_geometry": true,
    "geometry_format": "mask"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": {
    "image_path": "/home/user/images/photo.jpg",
    "query": "dog",
    "found_objects": [
      {
        "description": "dog",
        "match": "dog",
        "confidence": 0.92,
        "bbox": [150.2, 30.5, 250.1, 120.7],
        "mask_path": "/home/user/images/photo_mask_0.png"
      },
      {
        "description": "dog",
        "match": "dog",
        "confidence": 0.85,
        "bbox": [300.5, 150.3, 400.2, 250.1],
        "mask_path": "/home/user/images/photo_mask_1.png"
      }
    ],
    "found": true
  }
}
```

### `get_metainfo`

Gets metadata information about an image file.

- **Required arguments:**
  - `input_path` (string): Full path to the input image
- **Returns:** dictionary containing metadata about the image including:
  - `filename`
  - `file path`
  - `file size` (in bytes, KB, and MB)
  - `dimensions` (width, height, aspect ratio)
  - `image format`
  - `color mode`
  - `creation and modification timestamps`

**Example Claude Request:**

```
Get metadata information about my image 'photo.jpg'
```

**Example Tool Call (JSON):**

```json
{
  "name": "get_metainfo",
  "arguments": {
    "input_path": "/home/user/images/photo.jpg"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": {
    "filename": "photo.jpg",
    "path": "/home/user/images/photo.jpg",
    "size_bytes": 12345,
    "size_kb": 12.06,
    "size_mb": 0.01,
    "dimensions": {
      "width": 800,
      "height": 600,
      "aspect_ratio": 1.33
    },
    "format": "JPEG",
    "color_mode": "RGB",
    "created_at": "2023-06-15T10:30:45",
    "modified_at": "2023-06-15T10:30:45"
  }
}
```


### `ocr`

Performs Optical Character Recognition (OCR) on an image using EasyOCR. This tool extracts text from images in various languages. The default language is English, but you can specify other languages using their language codes (e.g., 'en', 'ru', 'fr', etc.).

- **Required arguments:**
  - `input_path` (string): Full path to the input image
- **Optional arguments:**
  - `language` (string): Language code for OCR (e.g., 'en', 'ru', 'fr', etc.). Default is 'en'
- **Returns:** dictionary containing:
  - `image_path`: Path to the input image
  - `text_segments`: List of detected text segments, each with:
    - `text`: The extracted text content
    - `confidence`: Confidence score (0.0 to 1.0)
    - `bbox`: Bounding box coordinates [x1, y1, x2, y2]

**Example Claude Request:**

```
Extract text from my image 'document.jpg' using OCR with English language
```

**Example Tool Call (JSON):**

```json
{
  "name": "ocr",
  "arguments": {
    "input_path": "/home/user/images/document.jpg",
    "language": "en"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": {
    "image_path": "/home/user/images/document.jpg",
    "text_segments": [
      {
        "text": "Hello World",
        "confidence": 0.92,
        "bbox": [10.5, 20.3, 100.2, 200.1]
      },
      {
        "text": "Copyright 2023",
        "confidence": 0.85,
        "bbox": [150.2, 30.5, 250.1, 120.7]
      }
    ]
  }
}
```

### `overlay`

Overlays one image on top of another, handling transparency. This tool places an overlay image onto a base image at a specified (x, y) coordinate. If the overlay image has an alpha channel (e.g., a transparent PNG), it will be blended correctly with the base image. If the overlay extends beyond the boundaries of the base image, it will be cropped.

- **Required arguments:**
  - `base_image_path` (string): Full path to the base image
  - `overlay_image_path` (string): Full path to the overlay image. This image can have transparency.
  - `x` (integer): X-coordinate of the top-left corner of the overlay image on the base image.
  - `y` (integer): Y-coordinate of the top-left corner of the overlay image on the base image.
- **Optional arguments:**
  - `output_path` (string): Full path to save the output image. If not provided, will use the base image filename with '_overlaid' suffix.
- **Returns:** string (path to the resulting image)

**Example Claude Request:**

```
Overlay 'logo.png' on top of 'background.jpg' at position (10, 10) and save it as 'final.jpg'
```

**Example Tool Call (JSON):

```json
{
  "name": "overlay",
  "arguments": {
    "base_image_path": "/home/user/images/background.jpg",
    "overlay_image_path": "/home/user/images/logo.png",
    "x": 10,
    "y": 10,
    "output_path": "/home/user/images/final.jpg"
  }
}
```

**Example Response (JSON):**

```json
{
  "result": "/home/user/images/final.jpg"
}
```


================================================
FILE: src/imagesorcery_mcp/tools/__init__.py
================================================
# Import the central logger
from imagesorcery_mcp.logging_config import logger

logger.info("🪄 ImageSorcery MCP tools package initialized")


================================================
FILE: src/imagesorcery_mcp/tools/blur.py
================================================
import os
from typing import Annotated, Any, Dict, List, Optional

import cv2
import numpy as np
from fastmcp import FastMCP
from pydantic import Field

# Import the central logger and config
from imagesorcery_mcp.config import get_config
from imagesorcery_mcp.logging_config import logger


def register_tool(mcp: FastMCP):
    @mcp.tool()
    def blur(
        input_path: Annotated[str, Field(description="Full path to the input image (must be a full path)")],
        areas: Annotated[
            List[Dict[str, Any]],
            Field(
                description=(
                    "List of areas to blur. Each area should have: "
                    "a rectangle ({'x1', 'y1', 'x2', 'y2'}) or a polygon ({'polygon': [[x,y],...]}). "
                    "Optionally, include 'blur_strength' (int, odd number, default 15) for each area."
                )
            ),
        ],
        invert_areas: Annotated[
            bool,
            Field(
                description="If True, blurs everything EXCEPT the specified areas. Useful for background blurring."
            ),
        ] = False,
        output_path: Annotated[
            Optional[str],
            Field(
                description=(
                    "Full path to save the output image (must be a full path). "
                    "If not provided, will use input filename "
                    "with '_blurred' suffix."
                )
            ),
        ] = None,
    ) -> str:
        """
        Blur specified rectangular or polygonal areas of an image using OpenCV.
        
        This tool allows blurring multiple rectangular or polygonal areas of an image with customizable
        blur strength. Each area can be a rectangle defined by a bounding box 
        [x1, y1, x2, y2] or a polygon defined by a list of points.
        
        The blur_strength parameter controls the intensity of the blur effect. Higher values
        result in stronger blur. It must be an odd number (default is 15).
        
        If `invert_areas` is True, the tool will blur everything EXCEPT the specified areas.
        Returns:
            Path to the image with blurred areas
        """
        logger.info(f"Blur tool requested for image: {input_path} with {len(areas)} areas, invert_areas={invert_areas}")

        # Check if input file exists
        if not os.path.exists(input_path):
            logger.error(f"Input file not found: {input_path}")
            raise FileNotFoundError(f"Input file not found: {input_path}. Please provide a full path to the file.")

        # Generate output path if not provided
        if not output_path:
            file_name, file_ext = os.path.splitext(input_path)
            output_path = f"{file_name}_blurred{file_ext}"
            logger.info(f"Output path not provided, generated: {output_path}")

        # Read the image using OpenCV
        logger.info(f"Reading image: {input_path}")
        img = cv2.imread(input_path)
        if img is None:
            logger.error(f"Failed to read image: {input_path}")
            raise ValueError(f"Failed to read image: {input_path}")
        logger.info(f"Image read successfully. Shape: {img.shape}")

        # Create a mask for the areas to be blurred (or not blurred if invert_areas is True)
        mask = np.zeros(img.shape[:2], dtype=np.uint8)

        # Populate the mask based on areas
        for area in areas:
            if "polygon" in area:
                polygon_points = np.array(area["polygon"], dtype=np.int32)
                cv2.fillPoly(mask, [polygon_points], 255)
            elif "x1" in area and "y1" in area and "x2" in area and "y2" in area:
                x1, y1, x2, y2 = area["x1"], area["y1"], area["x2"], area["y2"]
                cv2.rectangle(mask, (x1, y1), (x2, y2), 255, -1)
            else:
                logger.warning("Skipping area due to missing 'polygon' or 'x1,y1,x2,y2' keys.")
                continue

        # If invert_areas is True, invert the mask
        if invert_areas:
            mask = cv2.bitwise_not(mask)
            logger.info("Inverting blur areas: blurring everything EXCEPT the specified regions.")
        else:
            logger.info("Applying blur to specified areas.")

        # Apply blur to the entire image (this will be used for the masked regions)
        # Use the blur_strength from the first area, or config default
        config = get_config()
        global_blur_strength = areas[0].get("blur_strength", config.blur.strength) if areas else config.blur.strength
        if global_blur_strength % 2 == 0:
            global_blur_strength += 1
            logger.warning(f"Adjusted global blur_strength to odd number: {global_blur_strength}")
        
        full_blurred_img = cv2.GaussianBlur(img, (global_blur_strength, global_blur_strength), 0)

        # Combine the original image and the fully blurred image using the mask
        # Where mask is 255 (white), use the blurred image. Where mask is 0 (black), use the original image.
        result_img = np.where(mask[:, :, None] == 255, full_blurred_img, img)

        # Create directory for output if it doesn't exist
        output_dir = os.path.dirname(output_path)
        if output_dir and not os.path.exists(output_dir):
            logger.info(f"Output directory does not exist, creating: {output_dir}")
            os.makedirs(output_dir)
            logger.info(f"Output directory created: {output_dir}")

        # Save the image with blurred areas
        logger.info(f"Saving blurred image to: {output_path}")
        cv2.imwrite(output_path, result_img)
        logger.info(f"Blurred image saved successfully to: {output_path}")

        return output_path


================================================
FILE: src/imagesorcery_mcp/tools/change_color.py
================================================
import os
from typing import Annotated, Literal, Optional

import cv2
import numpy as np
from fastmcp import FastMCP
from pydantic import Field

# Import the central logger
from imagesorcery_mcp.logging_config import logger


def register_tool(mcp: FastMCP):
    @mcp.tool()
    def change_color(
        input_path: Annotated[str, Field(description="Full path to the input image (must be a full path)")],
        palette: Annotated[
            Literal["grayscale", "sepia"],
            Field(description="The color palette to apply. Currently supports 'grayscale' and 'sepia'."),
        ],
        output_path: Annotated[
            Optional[str],
            Field(
                description=(
                    "Full path to save the output image (must be a full path). "
                    "If not provided, will use input filename "
                    "with a suffix based on the palette (e.g., '_grayscale')."
                )
            ),
        ] = None,
    ) -> str:
        """
        Change the color palette of an image.
        
        This tool applies a predefined color transformation to an image.
        Currently supported palettes are 'grayscale' and 'sepia'.
        
        Returns:
            Path to the image with the new color palette.
        """
        logger.info(f"Change color tool requested for image: {input_path} with palette: {palette}")

        # Check if input file exists
        if not os.path.exists(input_path):
            logger.error(f"Input file not found: {input_path}")
            raise FileNotFoundError(f"Input file not found: {input_path}. Please provide a full path to the file.")

        # Generate output path if not provided
        if not output_path:
            file_name, file_ext = os.path.splitext(input_path)
            output_path = f"{file_name}_{palette}{file_ext}"
            logger.info(f"Output path not provided, generated: {output_path}")

        # Read the image using OpenCV
        img = cv2.imread(input_path)
        if img is None:
            logger.error(f"Failed to read image: {input_path}")
            raise ValueError(f"Failed to read image: {input_path}")

        # Apply the selected color palette
        if palette == "grayscale":
            logger.info("Applying grayscale palette")
            output_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        elif palette == "sepia":
            logger.info("Applying sepia palette")
            # Sepia transformation matrix
            sepia_kernel = np.array([[0.272, 0.534, 0.131], [0.349, 0.686, 0.168], [0.393, 0.769, 0.189]])
            # Apply the transformation
            sepia_img = cv2.transform(img, sepia_kernel)
            # Clip values to be in the 0-255 range
            output_img = np.clip(sepia_img, 0, 255).astype(np.uint8)

        # Create directory for output if it doesn't exist
        output_dir = os.path.dirname(output_path)
        if output_dir and not os.path.exists(output_dir):
            os.makedirs(output_dir)

        # Save the image
        cv2.imwrite(output_path, output_img)
        logger.info(f"Transformed image saved successfully to: {output_path}")

        return output_path

================================================
FILE: src/imagesorcery_mcp/tools/config.py
================================================
"""
Configuration tool for ImageSorcery MCP.

This tool allows viewing and updating configuration values through the MCP interface.
"""

from typing import Annotated, Any, Dict, Optional, Union

from fastmcp import FastMCP
from pydantic import Field

# Import the central logger and config manager
from imagesorcery_mcp.config import (
    generate_config_documentation,
    get_available_config_keys,
    get_config_manager,
)
from imagesorcery_mcp.logging_config import logger


def _generate_config_tool_docstring() -> str:
    """Generate the dynamic docstring for the config tool."""
    base_doc = """View or update ImageSorcery MCP configuration.

        This tool allows you to:
        - View current configuration values
        - Update configuration values for the current session
        - Persist configuration changes to the config file
        - Reset runtime overrides

        """

    config_doc = generate_config_documentation()

    examples_doc = """

        Examples:
        - Get all config: config(action="get")
        - Get detection confidence: config(action="get", key="detection.confidence_threshold")
        - Set blur strength: config(action="set", key="blur.strength", value=21)
        - Set and persist: config(action="set", key="detection.confidence_threshold", value=0.8, persist=True)
        - Reset overrides: config(action="reset")

        Returns:
            Dictionary containing the requested configuration data or update result"""

    return base_doc + config_doc + examples_doc


def register_tool(mcp: FastMCP):
    @mcp.tool()
    def config(
        action: Annotated[
            str,
            Field(
                description="Action to perform: 'get' to view config, 'set' to update config, 'reset' to reset runtime overrides",
                pattern="^(get|set|reset)$"
            ),
        ] = "get",
        key: Annotated[
            Optional[str],
            Field(
                description=(
                    "Configuration key to get/set. Use dot notation for nested values "
                    "(e.g., 'detection.confidence_threshold', 'blur.strength'). "
                    "Leave empty to get/set entire config."
                )
            ),
        ] = None,
        value: Annotated[
            Optional[Union[str, int, float, bool]],
            Field(
                description="Value to set (only used with action='set')"
            ),
        ] = None,
        persist: Annotated[
            bool,
            Field(
                description="Whether to persist changes to config file (only used with action='set')"
            ),
        ] = False,
    ) -> Dict[str, Any]:
        """Configuration tool - docstring will be set dynamically."""
        logger.info(f"Config tool called with action='{action}', key='{key}', value='{value}', persist={persist}")
        
        config_manager = get_config_manager()
        
        try:
            if action == "get":
                if key is None:
                    # Return entire configuration
                    config_dict = config_manager.get_config_dict()
                    runtime_overrides = config_manager.get_runtime_overrides()
                    
                    result = {
                        "action": "get",
                        "config": config_dict,
                        "runtime_overrides": runtime_overrides,
                        "config_file": str(config_manager.config_file),
                        "message": "Current configuration retrieved successfully"
                    }
                    logger.info("Retrieved entire configuration")
                    return result
                else:
                    # Return specific configuration value
                    config_dict = config_manager.get_config_dict()
                    
                    # Navigate to the requested key
                    if '.' in key:
                        parts = key.split('.')
                        current = config_dict
                        for part in parts:
                            if part not in current:
                                raise KeyError(f"Configuration key '{key}' not found")
                            current = current[part]
                        value_result = current
                    else:
                        if key not in config_dict:
                            raise KeyError(f"Configuration key '{key}' not found")
                        value_result = config_dict[key]
                    
                    result = {
                        "action": "get",
                        "key": key,
                        "value": value_result,
                        "message": f"Configuration value for '{key}' retrieved successfully"
                    }
                    logger.info(f"Retrieved configuration value for key '{key}': {value_result}")
                    return result
            
            elif action == "set":
                if key is None:
                    raise ValueError("Key is required for 'set' action")
                if value is None:
                    raise ValueError("Value is required for 'set' action")
                
                # Prepare update dictionary
                updates = {key: value}
                
                # Update configuration
                updated_config = config_manager.update_config(updates, persist=persist)
                
                # Get the updated value for confirmation
                if '.' in key:
                    parts = key.split('.')
                    current = updated_config
                    for part in parts:
                        current = current[part]
                    new_value = current
                else:
                    new_value = updated_config[key]
                
                result = {
                    "action": "set",
                    "key": key,
                    "old_value": value,  # This is the input value
                    "new_value": new_value,  # This is the validated/processed value
                    "persisted": persist,
                    "message": f"Configuration '{key}' updated successfully" + (" and persisted to file" if persist else " for current session")
                }
                
                logger.info(f"Updated configuration '{key}' to '{new_value}'" + (" (persisted)" if persist else " (runtime only)"))
                return result
            
            elif action == "reset":
                # Reset runtime overrides
                config_manager.reset_runtime_overrides()
                
                result = {
                    "action": "reset",
                    "message": "Runtime configuration overrides reset successfully",
                    "config": config_manager.get_config_dict()
                }
                
                logger.info("Reset runtime configuration overrides")
                return result
            
            else:
                raise ValueError(f"Invalid action '{action}'. Must be 'get', 'set', or 'reset'")
        
        except KeyError as e:
            logger.error(f"Configuration key error: {e}")
            return {
                "action": action,
                "error": str(e),
                "available_keys": get_available_config_keys()
            }
        
        except ValueError as e:
            logger.error(f"Configuration value error: {e}")
            return {
                "action": action,
                "error": str(e),
                "message": "Please check the provided key and value"
            }
        
        except Exception as e:
            logger.error(f"Configuration tool error: {e}", exc_info=True)
            return {
                "action": action,
                "error": f"Configuration operation failed: {str(e)}",
                "message": "An unexpected error occurred while processing the configuration request"
            }

    # Set the dynamic docstring
    config.__doc__ = _generate_config_tool_docstring()


================================================
FILE: src/imagesorcery_mcp/tools/crop.py
================================================
import os
from typing import Annotated

import cv2
from fastmcp import FastMCP
from pydantic import Field

# Import the central logger
from imagesorcery_mcp.logging_config import logger


def register_tool(mcp: FastMCP):
    @mcp.tool()
    def crop(
        input_path: Annotated[str, Field(description="Full path to the input image (must be a full path)")],
        x1: Annotated[
            int,
            Field(description="X-coordinate of the top-left corner"),
        ],
        y1: Annotated[
            int,
            Field(description="Y-coordinate of the top-left corner"),
        ],
        x2: Annotated[
            int,
            Field(description="X-coordinate of the bottom-right corner"),
        ],
        y2: Annotated[
            int,
            Field(description="Y-coordinate of the bottom-right corner"),
        ],
        output_path: Annotated[
            str,
            Field(
                description=(
                    "Full path to save the output image (must be a full path). "
                    "If not provided, will use input filename "
                    "with '_cropped' suffix."
                )
            ),
        ] = None,
    ) -> str:
        """
        Crop an image using OpenCV's NumPy slicing approach
        with OpenMCP's bounding box annotations.

        Returns:
            Path to the cropped image
        """
        logger.info(f"Crop tool requested for image: {input_path} with region [{x1}, {y1}, {x2}, {y2}]")

        # Check if input file exists
        if not os.path.exists(input_path):
            logger.error(f"Input file not found: {input_path}")
            raise FileNotFoundError(f"Input file not found: {input_path}. Please provide a full path to the file.")

        # Generate output path if not provided
        if not output_path:
            file_name, file_ext = os.path.splitext(input_path)
            output_path = f"{file_name}_cropped{file_ext}"
            logger.info(f"Output path not provided, generated: {output_path}")

        # Read the image using OpenCV
        logger.info(f"Reading image: {input_path}")
        img = cv2.imread(input_path)
        if img is None:
            logger.error(f"Failed to read image: {input_path}")
            raise ValueError(f"Failed to read image: {input_path}")
        logger.info(f"Image read successfully. Shape: {img.shape}")

        # Crop the image using NumPy slicing
        logger.info(f"Cropping image with region [{x1}, {y1}, {x2}, {y2}]")
        cropped_img = img[y1:y2, x1:x2]
        logger.info(f"Image cropped successfully. New shape: {cropped_img.shape}")

        # Create directory for output if it doesn't exist
        output_dir = os.path.dirname(output_path)
      
Download .txt
gitextract_jd7zbvmp/

├── .gitignore
├── CONFIG.md
├── GEMINI.md
├── LICENSE
├── LLM-INSTALL.md
├── README.md
├── glama.json
├── pyproject.toml
├── pytest.ini
├── setup.sh
├── src/
│   └── imagesorcery_mcp/
│       ├── README.md
│       ├── __init__.py
│       ├── __main__.py
│       ├── config.py
│       ├── logging_config.py
│       ├── middlewares/
│       │   ├── path_access.py
│       │   ├── telemetry.py
│       │   └── validation.py
│       ├── prompts/
│       │   ├── README.md
│       │   ├── __init__.py
│       │   └── remove_background.py
│       ├── resources/
│       │   ├── README.md
│       │   ├── __init__.py
│       │   └── models.py
│       ├── scripts/
│       │   ├── README.md
│       │   ├── __init__.py
│       │   ├── clear_telemetry_keys.py
│       │   ├── create_model_descriptions.py
│       │   ├── download_clip.py
│       │   ├── download_models.py
│       │   ├── populate_telemetry_keys.py
│       │   └── post_install.py
│       ├── server.py
│       ├── telemetry_amplitude.py
│       ├── telemetry_keys.py
│       ├── telemetry_posthog.py
│       └── tools/
│           ├── README.md
│           ├── __init__.py
│           ├── blur.py
│           ├── change_color.py
│           ├── config.py
│           ├── crop.py
│           ├── detect.py
│           ├── draw_arrows.py
│           ├── draw_circle.py
│           ├── draw_lines.py
│           ├── draw_rectangle.py
│           ├── draw_text.py
│           ├── fill.py
│           ├── find.py
│           ├── metainfo.py
│           ├── ocr.py
│           ├── overlay.py
│           ├── resize.py
│           └── rotate.py
└── tests/
    ├── conftest.py
    ├── prompts/
    │   └── test_remove_background.py
    ├── resources/
    │   └── test_models.py
    ├── test_config.py
    ├── test_logging.py
    ├── test_path_access.py
    ├── test_server.py
    ├── test_telemetry.py
    └── tools/
        ├── test_blur.py
        ├── test_change_color.py
        ├── test_config_tool.py
        ├── test_crop.py
        ├── test_detect.py
        ├── test_draw_arrows.py
        ├── test_draw_circle.py
        ├── test_draw_lines.py
        ├── test_draw_rectangle.py
        ├── test_draw_text.py
        ├── test_fill.py
        ├── test_find.py
        ├── test_metainfo.py
        ├── test_ocr.py
        ├── test_overlay.py
        ├── test_resize.py
        └── test_rotate.py
Download .txt
SYMBOL INDEX (399 symbols across 58 files)

FILE: src/imagesorcery_mcp/config.py
  class DetectionConfig (line 17) | class DetectionConfig(BaseModel):
  class FindConfig (line 23) | class FindConfig(BaseModel):
  class BlurConfig (line 29) | class BlurConfig(BaseModel):
    method strength_must_be_odd (line 35) | def strength_must_be_odd(cls, v):
  class TextConfig (line 41) | class TextConfig(BaseModel):
  class DrawingConfig (line 46) | class DrawingConfig(BaseModel):
    method color_values_valid (line 53) | def color_values_valid(cls, v):
  class OCRConfig (line 60) | class OCRConfig(BaseModel):
  class ResizeConfig (line 65) | class ResizeConfig(BaseModel):
  class TelemetryConfig (line 70) | class TelemetryConfig(BaseModel):
  class ImageSorceryConfig (line 75) | class ImageSorceryConfig(BaseModel):
  class ConfigManager (line 87) | class ConfigManager:
    method __init__ (line 90) | def __init__(self):
    method _ensure_config_file_exists (line 98) | def _ensure_config_file_exists(self):
    method _load_config (line 108) | def _load_config(self):
    method _apply_runtime_overrides (line 128) | def _apply_runtime_overrides(self, config_data: Dict[str, Any]):
    method _save_config_to_file (line 146) | def _save_config_to_file(self, config_data: Dict[str, Any]):
    method config (line 157) | def config(self) -> ImageSorceryConfig:
    method get_config_dict (line 163) | def get_config_dict(self) -> Dict[str, Any]:
    method update_config (line 170) | def update_config(self, updates: Dict[str, Any], persist: bool = False...
    method reset_runtime_overrides (line 225) | def reset_runtime_overrides(self):
    method get_runtime_overrides (line 231) | def get_runtime_overrides(self) -> Dict[str, Any]:
  function get_config_manager (line 241) | def get_config_manager() -> ConfigManager:
  function get_config (line 254) | def get_config() -> ImageSorceryConfig:
  function get_config_schema_info (line 262) | def get_config_schema_info() -> Dict[str, Any]:
  function get_available_config_keys (line 325) | def get_available_config_keys() -> List[str]:
  function generate_config_documentation (line 331) | def generate_config_documentation() -> str:

FILE: src/imagesorcery_mcp/logging_config.py
  function setup_logging (line 8) | def setup_logging():

FILE: src/imagesorcery_mcp/middlewares/path_access.py
  class PathAccessMiddleware (line 13) | class PathAccessMiddleware(Middleware):
    method __init__ (line 16) | def __init__(self, logger: logging.Logger | None = None):
    method on_call_tool (line 19) | async def on_call_tool(
  function get_allowed_directories (line 43) | def get_allowed_directories() -> list[Path]:
  function split_paths (line 56) | def split_paths(raw_paths: str) -> list[str]:
  function iter_path_arguments (line 61) | def iter_path_arguments(value: Any, prefix: str = "") -> Iterator[tuple[...
  function is_path_argument (line 75) | def is_path_argument(name: str) -> bool:
  function resolve_path (line 79) | def resolve_path(path: str) -> Path:
  function is_path_allowed (line 83) | def is_path_allowed(path: Path, allowed_dirs: list[Path]) -> bool:

FILE: src/imagesorcery_mcp/middlewares/telemetry.py
  class TelemetryMiddleware (line 14) | class TelemetryMiddleware(Middleware):
    method __init__ (line 17) | def __init__(self, logger: logging.Logger | None = None):
    method _get_user_id (line 26) | def _get_user_id(self) -> str:
    method _get_version (line 42) | def _get_version(self) -> str:
    method _handle_action (line 60) | async def _handle_action(self, action_type: str, identifier: str, cont...
    method on_call_tool (line 92) | async def on_call_tool(self, context: MiddlewareContext, call_next: Ca...
    method on_read_resource (line 96) | async def on_read_resource(self, context: MiddlewareContext, call_next...
    method on_get_prompt (line 100) | async def on_get_prompt(self, context: MiddlewareContext, call_next: C...

FILE: src/imagesorcery_mcp/middlewares/validation.py
  class ImprovedValidationMiddleware (line 10) | class ImprovedValidationMiddleware(Middleware):
    method __init__ (line 13) | def __init__(self, logger: logging.Logger | None = None):
    method on_message (line 16) | async def on_message(self, context: MiddlewareContext, call_next: Call...

FILE: src/imagesorcery_mcp/prompts/remove_background.py
  function register_prompt (line 10) | def register_prompt(mcp: FastMCP):

FILE: src/imagesorcery_mcp/resources/models.py
  function get_model_description (line 10) | def get_model_description(model_name: str) -> str:
  function register_resource (line 50) | def register_resource(mcp: FastMCP):

FILE: src/imagesorcery_mcp/scripts/clear_telemetry_keys.py
  function write_empty_telemetry_keys (line 15) | def write_empty_telemetry_keys() -> bool:
  function main (line 34) | def main():

FILE: src/imagesorcery_mcp/scripts/create_model_descriptions.py
  function create_model_descriptions (line 15) | def create_model_descriptions():
  function main (line 159) | def main():

FILE: src/imagesorcery_mcp/scripts/download_clip.py
  function get_models_dir (line 17) | def get_models_dir():
  function download_file (line 25) | def download_file(url, output_path):
  function download_clip_model (line 53) | def download_clip_model():
  function main (line 77) | def main():

FILE: src/imagesorcery_mcp/scripts/download_models.py
  function get_models_dir (line 21) | def get_models_dir():
  function download_from_url (line 29) | def download_from_url(url, output_path):
  function download_from_huggingface (line 57) | def download_from_huggingface(model_name):
  function update_model_description (line 133) | def update_model_description(model_key, description):
  function download_ultralytics_model (line 170) | def download_ultralytics_model(model_name):
  function download_model (line 255) | def download_model(model_name, source=None):
  function main (line 270) | def main():

FILE: src/imagesorcery_mcp/scripts/populate_telemetry_keys.py
  function get_telemetry_keys (line 23) | def get_telemetry_keys() -> Dict[str, str]:
  function write_telemetry_keys_file (line 56) | def write_telemetry_keys_file(keys: Dict[str, str]) -> bool:
  function main (line 82) | def main():

FILE: src/imagesorcery_mcp/scripts/post_install.py
  function install_clip (line 29) | def install_clip():
  function create_config_file (line 70) | def create_config_file():
  function create_user_id_file (line 85) | def create_user_id_file():
  function run_post_install (line 105) | def run_post_install():
  function main (line 190) | def main():

FILE: src/imagesorcery_mcp/server.py
  function parse_arguments (line 105) | def parse_arguments():
  function main (line 140) | def main():

FILE: src/imagesorcery_mcp/telemetry_amplitude.py
  class AmplitudeHandler (line 10) | class AmplitudeHandler:
    method __init__ (line 13) | def __init__(self, logger: logging.Logger | None = None):
    method _get_api_key (line 28) | def _get_api_key(self) -> str:
    method track_event (line 37) | def track_event(self, event_data: Dict[str, Any]):

FILE: src/imagesorcery_mcp/telemetry_posthog.py
  class PostHogHandler (line 12) | class PostHogHandler:
    method __init__ (line 15) | def __init__(self, logger: logging.Logger | None = None):
    method _get_api_key (line 31) | def _get_api_key(self) -> str:
    method track_event (line 40) | def track_event(self, event_data: Dict[str, Any]):

FILE: src/imagesorcery_mcp/tools/blur.py
  function register_tool (line 14) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/change_color.py
  function register_tool (line 13) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/config.py
  function _generate_config_tool_docstring (line 21) | def _generate_config_tool_docstring() -> str:
  function register_tool (line 50) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/crop.py
  function register_tool (line 12) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/detect.py
  function get_model_path (line 15) | def get_model_path(model_name):
  function register_tool (line 26) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/draw_arrows.py
  function register_tool (line 12) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/draw_circle.py
  class CircleItem (line 12) | class CircleItem(BaseModel):
  function register_tool (line 22) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/draw_lines.py
  function register_tool (line 12) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/draw_rectangle.py
  function register_tool (line 12) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/draw_text.py
  function register_tool (line 13) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/fill.py
  function register_tool (line 13) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/find.py
  function get_model_path (line 15) | def get_model_path(model_name):
  function check_clip_installed (line 26) | def check_clip_installed():
  function register_tool (line 46) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/metainfo.py
  function register_tool (line 13) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/ocr.py
  function register_tool (line 12) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/overlay.py
  function register_tool (line 13) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/resize.py
  function register_tool (line 13) | def register_tool(mcp: FastMCP):

FILE: src/imagesorcery_mcp/tools/rotate.py
  function register_tool (line 13) | def register_tool(mcp: FastMCP):

FILE: tests/conftest.py
  function pytest_configure (line 8) | def pytest_configure(config):
  function pytest_unconfigure (line 14) | def pytest_unconfigure(config):

FILE: tests/prompts/test_remove_background.py
  function mcp_server (line 8) | def mcp_server():
  class TestRemoveBackgroundPromptDefinition (line 13) | class TestRemoveBackgroundPromptDefinition:
    method test_remove_background_in_prompts_list (line 17) | async def test_remove_background_in_prompts_list(self, mcp_server: Fas...
    method test_remove_background_description (line 31) | async def test_remove_background_description(self, mcp_server: FastMCP):
    method test_remove_background_parameters (line 48) | async def test_remove_background_parameters(self, mcp_server: FastMCP):
  class TestRemoveBackgroundPromptExecution (line 92) | class TestRemoveBackgroundPromptExecution:
    method test_remove_background_prompt_execution (line 96) | async def test_remove_background_prompt_execution(self, mcp_server: Fa...
    method test_remove_background_default_parameters (line 123) | async def test_remove_background_default_parameters(self, mcp_server: ...
    method test_remove_background_custom_target (line 149) | async def test_remove_background_custom_target(self, mcp_server: FastM...

FILE: tests/resources/test_models.py
  function mcp_server (line 12) | def mcp_server():
  function test_models_dir (line 18) | def test_models_dir(tmp_path):
  class TestModelsResourceDefinition (line 63) | class TestModelsResourceDefinition:
    method test_models_in_resources_list (line 67) | async def test_models_in_resources_list(self, mcp_server: FastMCP):
    method test_models_resource_metadata (line 86) | async def test_models_resource_metadata(self, mcp_server: FastMCP):
  class TestModelsResourceExecution (line 105) | class TestModelsResourceExecution:
    method test_models_resource_execution (line 109) | async def test_models_resource_execution(self, mcp_server: FastMCP, te...
    method test_models_empty_directory (line 142) | async def test_models_empty_directory(self, mcp_server: FastMCP, tmp_p...
    method test_models_no_directory (line 184) | async def test_models_no_directory(self, mcp_server: FastMCP, tmp_path):
    method test_models_with_subdirectories (line 218) | async def test_models_with_subdirectories(self, mcp_server: FastMCP, t...
    method test_models_ignores_non_model_files (line 293) | async def test_models_ignores_non_model_files(self, mcp_server: FastMC...

FILE: tests/test_config.py
  class TestImageSorceryConfig (line 20) | class TestImageSorceryConfig:
    method test_default_values (line 23) | def test_default_values(self):
    method test_validation_confidence_threshold (line 54) | def test_validation_confidence_threshold(self):
    method test_validation_blur_strength (line 67) | def test_validation_blur_strength(self):
    method test_validation_drawing_color (line 77) | def test_validation_drawing_color(self):
    method test_validation_interpolation (line 94) | def test_validation_interpolation(self):
    method test_validation_telemetry_enabled (line 105) | def test_validation_telemetry_enabled(self):
  class TestConfigManager (line 119) | class TestConfigManager:
    method setup_method (line 122) | def setup_method(self):
    method teardown_method (line 128) | def teardown_method(self):
    method test_config_file_creation (line 134) | def test_config_file_creation(self):
    method test_config_loading_from_file (line 148) | def test_config_loading_from_file(self):
    method test_runtime_updates (line 165) | def test_runtime_updates(self):
    method test_persistent_updates (line 187) | def test_persistent_updates(self):
    method test_persistent_telemetry_update (line 206) | def test_persistent_telemetry_update(self):
    method test_validation_in_updates (line 227) | def test_validation_in_updates(self):
    method test_reset_runtime_overrides (line 239) | def test_reset_runtime_overrides(self):
    method test_get_runtime_overrides (line 262) | def test_get_runtime_overrides(self):
  class TestGlobalConfigFunctions (line 278) | class TestGlobalConfigFunctions:
    method setup_method (line 281) | def setup_method(self):
    method teardown_method (line 291) | def teardown_method(self):
    method test_get_config_manager (line 301) | def test_get_config_manager(self):
    method test_get_config (line 309) | def test_get_config(self):

FILE: tests/test_logging.py
  function temp_log_file (line 15) | def temp_log_file():
  function test_log_structure_and_components (line 22) | def test_log_structure_and_components(temp_log_file):
  function test_different_modules_log_correctly (line 81) | def test_different_modules_log_correctly(temp_log_file):
  function test_different_log_levels (line 107) | def test_different_log_levels(temp_log_file):

FILE: tests/test_path_access.py
  function mcp_server (line 16) | def mcp_server():
  function test_image (line 21) | def test_image(tmp_path):
  function test_available_paths_empty_disables_restrictions (line 29) | def test_available_paths_empty_disables_restrictions(monkeypatch):
  function test_available_paths_supports_pathsep_and_comma (line 39) | def test_available_paths_supports_pathsep_and_comma():
  function test_path_inside_allowed_directory_is_accepted (line 46) | async def test_path_inside_allowed_directory_is_accepted(
  function test_relative_traversal_outside_allowed_directory_is_rejected (line 58) | async def test_relative_traversal_outside_allowed_directory_is_rejected(
  function test_symlink_inside_allowed_directory_is_accepted_without_resolving_target (line 82) | async def test_symlink_inside_allowed_directory_is_accepted_without_reso...
  function test_output_path_outside_allowed_directory_is_rejected (line 109) | async def test_output_path_outside_allowed_directory_is_rejected(
  function test_nested_mask_path_outside_allowed_directory_is_rejected (line 130) | async def test_nested_mask_path_outside_allowed_directory_is_rejected(

FILE: tests/test_server.py
  function mcp_server (line 8) | def mcp_server():
  function test_list_tools (line 14) | async def test_list_tools(mcp_server: FastMCP):
  function test_nonexisting_tool (line 25) | async def test_nonexisting_tool(mcp_server: FastMCP):

FILE: tests/test_telemetry.py
  function mock_call_next_func (line 18) | async def mock_call_next_func(context):
  class MockAmplitudeHandler (line 23) | class MockAmplitudeHandler:
    method __init__ (line 24) | def __init__(self):
    method track_event (line 27) | def track_event(self, event_data):
  class MockPostHogHandler (line 30) | class MockPostHogHandler:
    method __init__ (line 31) | def __init__(self):
    method track_event (line 34) | def track_event(self, event_data):
  class TestTelemetryMiddleware (line 38) | class TestTelemetryMiddleware:
    method setup_method (line 41) | def setup_method(self):
    method teardown_method (line 68) | def teardown_method(self):
    method test_middleware_initialization (line 79) | def test_middleware_initialization(self):
    method test_telemetry_tracking_enabled_and_disabled (line 90) | async def test_telemetry_tracking_enabled_and_disabled(self):

FILE: tests/tools/test_blur.py
  function mcp_server (line 12) | def mcp_server():
  function test_image_path (line 18) | def test_image_path(tmp_path):
  function test_image_for_invert_blur (line 40) | def test_image_for_invert_blur(tmp_path):
  class TestBlurToolDefinition (line 69) | class TestBlurToolDefinition:
    method test_blur_in_tools_list (line 73) | async def test_blur_in_tools_list(self, mcp_server: FastMCP):
    method test_blur_description (line 87) | async def test_blur_description(self, mcp_server: FastMCP):
    method test_blur_parameters (line 100) | async def test_blur_parameters(self, mcp_server: FastMCP):
  class TestBlurToolExecution (line 148) | class TestBlurToolExecution:
    method test_blur_tool_execution (line 152) | async def test_blur_tool_execution(
    method test_blur_invert_rectangle (line 184) | async def test_blur_invert_rectangle(self, mcp_server: FastMCP, test_i...
    method test_blur_invert_polygon (line 219) | async def test_blur_invert_polygon(self, mcp_server: FastMCP, test_ima...
    method test_blur_invert_multiple_areas (line 254) | async def test_blur_invert_multiple_areas(self, mcp_server: FastMCP, t...
    method test_blur_polygon_area (line 297) | async def test_blur_polygon_area(self, mcp_server: FastMCP, test_image...
    method test_blur_mixed_areas (line 346) | async def test_blur_mixed_areas(self, mcp_server: FastMCP, test_image_...
    method test_blur_default_output_path (line 389) | async def test_blur_default_output_path(self, mcp_server: FastMCP, tes...
    method test_blur_multiple_areas (line 415) | async def test_blur_multiple_areas(self, mcp_server: FastMCP, test_ima...

FILE: tests/tools/test_change_color.py
  function mcp_server (line 12) | def mcp_server():
  function test_image_path (line 18) | def test_image_path(tmp_path):
  class TestChangeColorToolDefinition (line 31) | class TestChangeColorToolDefinition:
    method test_change_color_in_tools_list (line 35) | async def test_change_color_in_tools_list(self, mcp_server: FastMCP):
    method test_change_color_description (line 44) | async def test_change_color_description(self, mcp_server: FastMCP):
    method test_change_color_parameters (line 53) | async def test_change_color_parameters(self, mcp_server: FastMCP):
  class TestChangeColorToolExecution (line 77) | class TestChangeColorToolExecution:
    method test_change_color_grayscale (line 81) | async def test_change_color_grayscale(self, mcp_server: FastMCP, test_...
    method test_change_color_sepia (line 109) | async def test_change_color_sepia(self, mcp_server: FastMCP, test_imag...
    method test_change_color_default_output_path (line 133) | async def test_change_color_default_output_path(self, mcp_server: Fast...
    method test_change_color_invalid_palette (line 142) | async def test_change_color_invalid_palette(self, mcp_server: FastMCP,...

FILE: tests/tools/test_config_tool.py
  class TestConfigToolE2E (line 17) | class TestConfigToolE2E:
    method setup_method (line 20) | def setup_method(self):
    method teardown_method (line 26) | def teardown_method(self):
    method test_config_tool_registration (line 37) | async def test_config_tool_registration(self):
    method test_config_get_all (line 55) | async def test_config_get_all(self):
    method test_config_get_specific_key (line 75) | async def test_config_get_specific_key(self):
    method test_config_set_runtime (line 93) | async def test_config_set_runtime(self):
    method test_config_set_persistent (line 124) | async def test_config_set_persistent(self):
    method test_config_set_invalid_value (line 151) | async def test_config_set_invalid_value(self):
    method test_config_reset (line 168) | async def test_config_reset(self):
    method test_config_get_nonexistent_key (line 206) | async def test_config_get_nonexistent_key(self):
    method test_config_invalid_action (line 222) | async def test_config_invalid_action(self):
    method test_config_set_missing_parameters (line 233) | async def test_config_set_missing_parameters(self):

FILE: tests/tools/test_crop.py
  function mcp_server (line 12) | def mcp_server():
  function test_image_path (line 18) | def test_image_path(tmp_path):
  class TestCropToolDefinition (line 35) | class TestCropToolDefinition:
    method test_crop_in_tools_list (line 39) | async def test_crop_in_tools_list(self, mcp_server: FastMCP):
    method test_crop_description (line 53) | async def test_crop_description(self, mcp_server: FastMCP):
    method test_crop_parameters (line 66) | async def test_crop_parameters(self, mcp_server: FastMCP):
  class TestCropToolExecution (line 115) | class TestCropToolExecution:
    method test_crop_tool_execution (line 119) | async def test_crop_tool_execution(
    method test_crop_default_output_path (line 151) | async def test_crop_default_output_path(self, mcp_server: FastMCP, tes...

FILE: tests/tools/test_detect.py
  function mcp_server (line 14) | def mcp_server():
  function test_image_path (line 20) | def test_image_path(tmp_path):
  function test_image_negative_path (line 32) | def test_image_negative_path(tmp_path):
  function test_segmentation_image_path (line 43) | def test_segmentation_image_path(tmp_path):
  class TestDetectToolDefinition (line 53) | class TestDetectToolDefinition:
    method test_detect_in_tools_list (line 57) | async def test_detect_in_tools_list(self, mcp_server: FastMCP):
    method test_detect_description (line 71) | async def test_detect_description(self, mcp_server: FastMCP):
    method test_detect_parameters (line 84) | async def test_detect_parameters(self, mcp_server: FastMCP):
  class TestDetectToolExecution (line 153) | class TestDetectToolExecution:
    method test_detect_tool_execution (line 161) | async def test_detect_tool_execution(self, mcp_server: FastMCP, test_i...
    method test_detect_with_mask_geometry (line 222) | async def test_detect_with_mask_geometry(self, mcp_server: FastMCP, te...
    method test_detect_with_polygon_geometry (line 253) | async def test_detect_with_polygon_geometry(self, mcp_server: FastMCP,...
    method test_detect_no_geometry_by_default (line 287) | async def test_detect_no_geometry_by_default(self, mcp_server: FastMCP...
    method test_detect_geometry_with_non_seg_model_raises_error (line 313) | async def test_detect_geometry_with_non_seg_model_raises_error(
    method test_detect_negative_scenario (line 346) | async def test_detect_negative_scenario(
  class TestDetectGeometryValidation (line 404) | class TestDetectGeometryValidation:
    method test_mask_correctness (line 412) | async def test_mask_correctness(self, mcp_server: FastMCP, test_image_...
    method test_polygon_correctness (line 490) | async def test_polygon_correctness(self, mcp_server: FastMCP, test_ima...
    method test_mask_to_polygon_consistency (line 570) | async def test_mask_to_polygon_consistency(self, mcp_server: FastMCP, ...
    method test_detect_mask_validation_on_simple_image (line 629) | async def test_detect_mask_validation_on_simple_image(

FILE: tests/tools/test_draw_arrows.py
  function mcp_server (line 12) | def mcp_server():
  function test_image_path (line 18) | def test_image_path(tmp_path):
  class TestDrawArrowsToolDefinition (line 27) | class TestDrawArrowsToolDefinition:
    method test_draw_arrows_in_tools_list (line 31) | async def test_draw_arrows_in_tools_list(self, mcp_server: FastMCP):
    method test_draw_arrows_description (line 41) | async def test_draw_arrows_description(self, mcp_server: FastMCP):
    method test_draw_arrows_parameters (line 51) | async def test_draw_arrows_parameters(self, mcp_server: FastMCP):
  class TestDrawArrowsToolExecution (line 87) | class TestDrawArrowsToolExecution:
    method test_draw_arrows_tool_execution (line 91) | async def test_draw_arrows_tool_execution(
    method test_draw_arrows_default_parameters (line 123) | async def test_draw_arrows_default_parameters(
    method test_draw_arrows_default_output_path (line 144) | async def test_draw_arrows_default_output_path(self, mcp_server: FastM...

FILE: tests/tools/test_draw_circle.py
  function mcp_server (line 12) | def mcp_server():
  function test_image_path (line 18) | def test_image_path(tmp_path):
  class TestDrawCircleToolDefinition (line 27) | class TestDrawCircleToolDefinition:
    method test_draw_circles_in_tools_list (line 31) | async def test_draw_circles_in_tools_list(self, mcp_server: FastMCP):
    method test_draw_circles_description (line 41) | async def test_draw_circles_description(self, mcp_server: FastMCP):
    method test_draw_circles_parameters (line 51) | async def test_draw_circles_parameters(self, mcp_server: FastMCP):
  class TestDrawCircleToolExecution (line 111) | class TestDrawCircleToolExecution:
    method test_draw_circles_tool_execution (line 115) | async def test_draw_circles_tool_execution(
    method test_draw_filled_circle (line 139) | async def test_draw_filled_circle(
    method test_draw_circles_default_output_path (line 159) | async def test_draw_circles_default_output_path(self, mcp_server: Fast...

FILE: tests/tools/test_draw_lines.py
  function mcp_server (line 12) | def mcp_server():
  function test_image_path (line 18) | def test_image_path(tmp_path):
  class TestDrawLinesToolDefinition (line 27) | class TestDrawLinesToolDefinition:
    method test_draw_lines_in_tools_list (line 31) | async def test_draw_lines_in_tools_list(self, mcp_server: FastMCP):
    method test_draw_lines_description (line 41) | async def test_draw_lines_description(self, mcp_server: FastMCP):
    method test_draw_lines_parameters (line 51) | async def test_draw_lines_parameters(self, mcp_server: FastMCP):
  class TestDrawLinesToolExecution (line 87) | class TestDrawLinesToolExecution:
    method test_draw_lines_tool_execution (line 91) | async def test_draw_lines_tool_execution(
    method test_draw_lines_default_parameters (line 123) | async def test_draw_lines_default_parameters(
    method test_draw_lines_default_output_path (line 144) | async def test_draw_lines_default_output_path(self, mcp_server: FastMC...

FILE: tests/tools/test_draw_rectangle.py
  function mcp_server (line 12) | def mcp_server():
  function test_image_path (line 18) | def test_image_path(tmp_path):
  class TestDrawRectanglesToolDefinition (line 27) | class TestDrawRectanglesToolDefinition:
    method test_draw_rectangles_in_tools_list (line 31) | async def test_draw_rectangles_in_tools_list(self, mcp_server: FastMCP):
    method test_draw_rectangles_description (line 45) | async def test_draw_rectangles_description(self, mcp_server: FastMCP):
    method test_draw_rectangles_parameters (line 58) | async def test_draw_rectangles_parameters(self, mcp_server: FastMCP):
  class TestDrawRectanglesToolExecution (line 106) | class TestDrawRectanglesToolExecution:
    method test_draw_rectangles_tool_execution (line 110) | async def test_draw_rectangles_tool_execution(
    method test_draw_filled_rectangle (line 161) | async def test_draw_filled_rectangle(
    method test_draw_rectangles_default_output_path (line 200) | async def test_draw_rectangles_default_output_path(self, mcp_server: F...

FILE: tests/tools/test_draw_text.py
  function get_ocr_reader (line 18) | def get_ocr_reader():
  function mcp_server (line 27) | def mcp_server():
  function test_image_path (line 33) | def test_image_path(tmp_path):
  class TestDrawTextsToolDefinition (line 42) | class TestDrawTextsToolDefinition:
    method test_draw_texts_in_tools_list (line 46) | async def test_draw_texts_in_tools_list(self, mcp_server: FastMCP):
    method test_draw_texts_description (line 60) | async def test_draw_texts_description(self, mcp_server: FastMCP):
    method test_draw_texts_parameters (line 73) | async def test_draw_texts_parameters(self, mcp_server: FastMCP):
  class TestDrawTextsToolExecution (line 121) | class TestDrawTextsToolExecution:
    method test_draw_texts_tool_execution (line 125) | async def test_draw_texts_tool_execution(
    method test_draw_texts_default_output_path (line 188) | async def test_draw_texts_default_output_path(self, mcp_server: FastMC...
    method test_draw_texts_minimal_parameters (line 229) | async def test_draw_texts_minimal_parameters(self, mcp_server: FastMCP...

FILE: tests/tools/test_fill.py
  function mcp_server (line 12) | def mcp_server():
  function test_image_path (line 18) | def test_image_path(tmp_path):
  function test_jpeg_image_path (line 32) | def test_jpeg_image_path(tmp_path):
  class TestFillToolDefinition (line 49) | class TestFillToolDefinition:
    method test_fill_in_tools_list (line 53) | async def test_fill_in_tools_list(self, mcp_server: FastMCP):
    method test_fill_description (line 62) | async def test_fill_description(self, mcp_server: FastMCP):
    method test_fill_parameters (line 71) | async def test_fill_parameters(self, mcp_server: FastMCP):
  class TestFillToolExecution (line 90) | class TestFillToolExecution:
    method test_fill_tool_execution (line 94) | async def test_fill_tool_execution(self, mcp_server: FastMCP, test_ima...
    method test_fill_polygon_area (line 114) | async def test_fill_polygon_area(self, mcp_server: FastMCP, test_image...
    method test_fill_default_output_path (line 129) | async def test_fill_default_output_path(self, mcp_server: FastMCP, tes...
    method test_fill_multiple_areas (line 138) | async def test_fill_multiple_areas(self, mcp_server: FastMCP, test_ima...
    method test_fill_transparent_rectangle (line 150) | async def test_fill_transparent_rectangle(self, mcp_server: FastMCP, t...
    method test_fill_transparent_polygon (line 186) | async def test_fill_transparent_polygon(self, mcp_server: FastMCP, tes...
    method test_fill_invert_rectangle (line 223) | async def test_fill_invert_rectangle(self, mcp_server: FastMCP, test_i...
    method test_fill_invert_polygon (line 258) | async def test_fill_invert_polygon(self, mcp_server: FastMCP, test_ima...
    method test_fill_invert_transparent (line 291) | async def test_fill_invert_transparent(self, mcp_server: FastMCP, test...
    method test_fill_invert_multiple_areas (line 331) | async def test_fill_invert_multiple_areas(self, mcp_server: FastMCP, t...
    method test_fill_invert_complex_polygon (line 369) | async def test_fill_invert_complex_polygon(self, mcp_server: FastMCP, ...
    method test_fill_invert_single_area_transparent (line 408) | async def test_fill_invert_single_area_transparent(self, mcp_server: F...
    method test_fill_with_mask_path (line 446) | async def test_fill_with_mask_path(self, mcp_server: FastMCP, test_ima...
  class TestFillToolWithJPEG (line 477) | class TestFillToolWithJPEG:
    method test_fill_jpeg_to_transparent_rectangle (line 481) | async def test_fill_jpeg_to_transparent_rectangle(self, mcp_server: Fa...
    method test_fill_jpeg_invert_transparent (line 511) | async def test_fill_jpeg_invert_transparent(self, mcp_server: FastMCP,...
    method test_fill_jpeg_invert_with_color (line 554) | async def test_fill_jpeg_invert_with_color(self, mcp_server: FastMCP, ...
    method test_fill_jpeg_multiple_transparent_areas (line 585) | async def test_fill_jpeg_multiple_transparent_areas(self, mcp_server: ...

FILE: tests/tools/test_find.py
  function mcp_server (line 14) | def mcp_server():
  function test_image_path (line 20) | def test_image_path(tmp_path):
  function test_segmentation_image_path (line 32) | def test_segmentation_image_path(tmp_path):
  class TestFindToolDefinition (line 42) | class TestFindToolDefinition:
    method test_find_in_tools_list (line 46) | async def test_find_in_tools_list(self, mcp_server: FastMCP):
    method test_find_description (line 60) | async def test_find_description(self, mcp_server: FastMCP):
    method test_find_parameters (line 73) | async def test_find_parameters(self, mcp_server: FastMCP):
  class TestFindToolExecution (line 150) | class TestFindToolExecution:
    method test_find_tool_execution (line 158) | async def test_find_tool_execution(self, mcp_server: FastMCP, test_ima...
    method test_find_single_result (line 209) | async def test_find_single_result(self, mcp_server: FastMCP, test_imag...
    method test_find_nonexistent_object (line 251) | async def test_find_nonexistent_object(self, mcp_server: FastMCP, test...
    method test_find_with_mask_geometry (line 285) | async def test_find_with_mask_geometry(self, mcp_server: FastMCP, test...
    method test_find_with_polygon_geometry (line 317) | async def test_find_with_polygon_geometry(self, mcp_server: FastMCP, t...
    method test_find_no_geometry_by_default (line 351) | async def test_find_no_geometry_by_default(self, mcp_server: FastMCP, ...
    method test_mask_correctness (line 378) | async def test_mask_correctness(self, mcp_server: FastMCP, test_image_...
    method test_polygon_correctness (line 443) | async def test_polygon_correctness(self, mcp_server: FastMCP, test_ima...
    method test_mask_to_polygon_consistency (line 523) | async def test_mask_to_polygon_consistency(self, mcp_server: FastMCP, ...
    method test_find_mask_validation_on_simple_image (line 575) | async def test_find_mask_validation_on_simple_image(

FILE: tests/tools/test_metainfo.py
  function mcp_server (line 10) | def mcp_server():
  function test_image_path (line 16) | def test_image_path(tmp_path):
  class TestMetainfoToolDefinition (line 30) | class TestMetainfoToolDefinition:
    method test_metainfo_in_tools_list (line 34) | async def test_metainfo_in_tools_list(self, mcp_server: FastMCP):
    method test_metainfo_description (line 48) | async def test_metainfo_description(self, mcp_server: FastMCP):
    method test_metainfo_parameters (line 65) | async def test_metainfo_parameters(self, mcp_server: FastMCP):
  class TestMetainfoToolExecution (line 96) | class TestMetainfoToolExecution:
    method test_metainfo_tool_execution (line 100) | async def test_metainfo_tool_execution(self, mcp_server: FastMCP, test...
    method test_metainfo_nonexistent_file (line 132) | async def test_metainfo_nonexistent_file(self, mcp_server: FastMCP, tm...

FILE: tests/tools/test_ocr.py
  function mcp_server (line 14) | def mcp_server():
  function test_image_path (line 20) | def test_image_path(tmp_path):
  class TestOcrToolDefinition (line 38) | class TestOcrToolDefinition:
    method test_ocr_in_tools_list (line 42) | async def test_ocr_in_tools_list(self, mcp_server: FastMCP):
    method test_ocr_description (line 56) | async def test_ocr_description(self, mcp_server: FastMCP):
    method test_ocr_parameters (line 69) | async def test_ocr_parameters(self, mcp_server: FastMCP):
  class TestOcrToolExecution (line 110) | class TestOcrToolExecution:
    method test_ocr_tool_execution (line 114) | async def test_ocr_tool_execution(self, mcp_server: FastMCP, test_imag...

FILE: tests/tools/test_overlay.py
  function mcp_server (line 12) | def mcp_server():
  function base_image_path (line 18) | def base_image_path(tmp_path):
  function overlay_image_path_rgb (line 28) | def overlay_image_path_rgb(tmp_path):
  function overlay_image_path_rgba (line 38) | def overlay_image_path_rgba(tmp_path):
  class TestOverlayToolDefinition (line 48) | class TestOverlayToolDefinition:
    method test_overlay_in_tools_list (line 52) | async def test_overlay_in_tools_list(self, mcp_server: FastMCP):
    method test_overlay_parameters (line 61) | async def test_overlay_parameters(self, mcp_server: FastMCP):
  class TestOverlayToolExecution (line 83) | class TestOverlayToolExecution:
    method test_overlay_rgb (line 87) | async def test_overlay_rgb(self, mcp_server: FastMCP, base_image_path,...
    method test_overlay_rgba (line 112) | async def test_overlay_rgba(self, mcp_server: FastMCP, base_image_path...
    method test_overlay_partial_offscreen (line 137) | async def test_overlay_partial_offscreen(self, mcp_server: FastMCP, ba...
    method test_overlay_default_output_path (line 160) | async def test_overlay_default_output_path(self, mcp_server: FastMCP, ...

FILE: tests/tools/test_resize.py
  function mcp_server (line 12) | def mcp_server():
  function test_image_path (line 18) | def test_image_path(tmp_path):
  class TestResizeToolDefinition (line 58) | class TestResizeToolDefinition:
    method test_resize_in_tools_list (line 62) | async def test_resize_in_tools_list(self, mcp_server: FastMCP):
    method test_resize_description (line 76) | async def test_resize_description(self, mcp_server: FastMCP):
    method test_resize_parameters (line 89) | async def test_resize_parameters(self, mcp_server: FastMCP):
  class TestResizeToolExecution (line 176) | class TestResizeToolExecution:
    method test_resize_with_dimensions_smaller (line 180) | async def test_resize_with_dimensions_smaller(
    method test_resize_with_dimensions_larger (line 208) | async def test_resize_with_dimensions_larger(
    method test_resize_with_width_only_smaller (line 236) | async def test_resize_with_width_only_smaller(
    method test_resize_with_width_only_larger (line 267) | async def test_resize_with_width_only_larger(
    method test_resize_with_height_only_smaller (line 298) | async def test_resize_with_height_only_smaller(
    method test_resize_with_height_only_larger (line 329) | async def test_resize_with_height_only_larger(
    method test_resize_with_scale_factor_smaller (line 360) | async def test_resize_with_scale_factor_smaller(
    method test_resize_with_scale_factor_larger (line 388) | async def test_resize_with_scale_factor_larger(
    method test_resize_default_output_path (line 416) | async def test_resize_default_output_path(
    method test_resize_with_interpolation (line 437) | async def test_resize_with_interpolation(

FILE: tests/tools/test_rotate.py
  function mcp_server (line 12) | def mcp_server():
  function test_image_path (line 18) | def test_image_path(tmp_path):
  class TestRotateToolDefinition (line 32) | class TestRotateToolDefinition:
    method test_rotate_in_tools_list (line 36) | async def test_rotate_in_tools_list(self, mcp_server: FastMCP):
    method test_rotate_description (line 50) | async def test_rotate_description(self, mcp_server: FastMCP):
    method test_rotate_parameters (line 63) | async def test_rotate_parameters(self, mcp_server: FastMCP):
  class TestRotateToolExecution (line 105) | class TestRotateToolExecution:
    method test_rotate_tool_execution (line 109) | async def test_rotate_tool_execution(
    method test_rotate_clockwise (line 194) | async def test_rotate_clockwise(
    method test_rotate_default_output_path (line 240) | async def test_rotate_default_output_path(
Condensed preview — 80 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (590K chars).
[
  {
    "path": ".gitignore",
    "chars": 311,
    "preview": "venv\n.venv\n.env\n__pycache__\n.pytest_cache\n.coverage\n.ruff_cache\n.vscode\nhtmlcov\n**/logs/\n/dist/\n\n# Ultralytics directori"
  },
  {
    "path": "CONFIG.md",
    "chars": 2681,
    "preview": "# ImageSorcery MCP Configuration System\n\n## What Can Be Configured\n\nThe configuration system covers the following parame"
  },
  {
    "path": "GEMINI.md",
    "chars": 233,
    "preview": "# Gemini Workspace Instructions\n\nOn each new session, please re-read the `README.md` file to ensure you have the most up"
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) [year] [fullname]\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "LLM-INSTALL.md",
    "chars": 696,
    "preview": "1.  Load MCP documentation if you have the tool for it. It will give you path to *MCP-configuration file* and other need"
  },
  {
    "path": "README.md",
    "chars": 26417,
    "preview": "# 🪄 ImageSorcery MCP\n**ComputerVision-based 🪄 sorcery of local image recognition and editing tools for AI assistants**\n\n"
  },
  {
    "path": "glama.json",
    "chars": 97,
    "preview": "{\n  \"$schema\": \"https://glama.ai/mcp/schemas/server.json\",\n  \"maintainers\": [\n    \"titulus\"\n  ]\n}"
  },
  {
    "path": "pyproject.toml",
    "chars": 2790,
    "preview": "[project]\nname = \"imagesorcery-mcp\"\nversion = \"0.12.0\"\ndescription = \"A Model Context Protocol server providing image ma"
  },
  {
    "path": "pytest.ini",
    "chars": 143,
    "preview": "[pytest]\ntestpaths = tests\npython_files = test_*.py\npython_functions = test_*\nasyncio_mode = auto\nasyncio_default_fixtur"
  },
  {
    "path": "setup.sh",
    "chars": 679,
    "preview": "#!/bin/bash\nset -e\n\necho \"Setting up imagesorcery-mcp...\"\n\n# Create virtual environment if it doesn't exist\nif [ ! -d \"v"
  },
  {
    "path": "src/imagesorcery_mcp/README.md",
    "chars": 2474,
    "preview": "# ImageSorcery MCP Core Architecture\n\nThis directory contains the core components of the ImageSorcery MCP server, includ"
  },
  {
    "path": "src/imagesorcery_mcp/__init__.py",
    "chars": 133,
    "preview": "\"\"\"ImageSorcery MCP - Powerful Image Processing Tools for AI Assistants\"\"\"\n\nfrom .server import main, mcp\n\n__all__ = [\"m"
  },
  {
    "path": "src/imagesorcery_mcp/__main__.py",
    "chars": 176,
    "preview": "from imagesorcery_mcp.server import main\n\nfrom .logging_config import logger\n\nlogger.info(\"🪄 ImageSorcery MCP server __m"
  },
  {
    "path": "src/imagesorcery_mcp/config.py",
    "chars": 11941,
    "preview": "\"\"\"\nConfiguration management for ImageSorcery MCP.\n\nThis module provides a centralized configuration system that loads s"
  },
  {
    "path": "src/imagesorcery_mcp/logging_config.py",
    "chars": 1510,
    "preview": "import logging\nimport os\nfrom logging.handlers import RotatingFileHandler\n\nLOG_FILE = os.path.join(os.path.dirname(os.pa"
  },
  {
    "path": "src/imagesorcery_mcp/middlewares/path_access.py",
    "chars": 2978,
    "preview": "import logging\nimport os\nfrom pathlib import Path\nfrom typing import Any, Iterator\n\nfrom fastmcp.server.middleware impor"
  },
  {
    "path": "src/imagesorcery_mcp/middlewares/telemetry.py",
    "chars": 4376,
    "preview": "import logging\nimport sys\nfrom importlib.metadata import version\nfrom pathlib import Path\nfrom typing import Any\n\nfrom f"
  },
  {
    "path": "src/imagesorcery_mcp/middlewares/validation.py",
    "chars": 2798,
    "preview": "import logging\nimport re\nfrom typing import Any\n\nfrom fastmcp.server.middleware import CallNext, Middleware, MiddlewareC"
  },
  {
    "path": "src/imagesorcery_mcp/prompts/README.md",
    "chars": 2175,
    "preview": "# Prompts\n\nThis directory contains reusable prompt templates for the ImageSorcery MCP server.\n\n## Overview\n\nPrompts prov"
  },
  {
    "path": "src/imagesorcery_mcp/prompts/__init__.py",
    "chars": 142,
    "preview": "# Import the central logger\nfrom imagesorcery_mcp.logging_config import logger\n\nlogger.info(\"🪄 ImageSorcery MCP prompts "
  },
  {
    "path": "src/imagesorcery_mcp/prompts/remove_background.py",
    "chars": 4309,
    "preview": "from typing import Annotated\n\nfrom fastmcp import FastMCP\nfrom pydantic import Field\n\n# Import the central logger\nfrom i"
  },
  {
    "path": "src/imagesorcery_mcp/resources/README.md",
    "chars": 2071,
    "preview": "# 🪄 ImageSorcery MCP Server Resources Documentation\n\nThis document provides detailed information about each resource ava"
  },
  {
    "path": "src/imagesorcery_mcp/resources/__init__.py",
    "chars": 143,
    "preview": "# Import the central logger\nfrom imagesorcery_mcp.logging_config import logger\n\nlogger.info(\"🪄 ImageSorcery MCP resource"
  },
  {
    "path": "src/imagesorcery_mcp/resources/models.py",
    "chars": 4199,
    "preview": "import json\nfrom pathlib import Path\n\nfrom fastmcp import FastMCP\n\n# Import the central logger\nfrom imagesorcery_mcp.log"
  },
  {
    "path": "src/imagesorcery_mcp/scripts/README.md",
    "chars": 8749,
    "preview": "# 🪄 ImageSorcery MCP Server Scripts Documentation\n\nThis document provides detailed information about each script availab"
  },
  {
    "path": "src/imagesorcery_mcp/scripts/__init__.py",
    "chars": 327,
    "preview": "# Import functions to make them available when importing the package\n# Import the central logger\nfrom imagesorcery_mcp.l"
  },
  {
    "path": "src/imagesorcery_mcp/scripts/clear_telemetry_keys.py",
    "chars": 1596,
    "preview": "#!/usr/bin/env python3\n\"\"\"Build script to clear API keys in src/imagesorcery_mcp/telemetry_keys.py while preserving .use"
  },
  {
    "path": "src/imagesorcery_mcp/scripts/create_model_descriptions.py",
    "chars": 18667,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nScript to create model descriptions JSON file.\nThis script should be run during project setup"
  },
  {
    "path": "src/imagesorcery_mcp/scripts/download_clip.py",
    "chars": 3019,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nScript to download CLIP models required for YOLOe text prompts.\n\"\"\"\n\nimport os\nimport sys\nfro"
  },
  {
    "path": "src/imagesorcery_mcp/scripts/download_models.py",
    "chars": 12120,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nScript to download YOLO compatible models for offline use.\nThis script should be run during p"
  },
  {
    "path": "src/imagesorcery_mcp/scripts/populate_telemetry_keys.py",
    "chars": 3489,
    "preview": "#!/usr/bin/env python3\n\"\"\"Build script to populate src/imagesorcery_mcp/telemetry_keys.py with API keys from environment"
  },
  {
    "path": "src/imagesorcery_mcp/scripts/post_install.py",
    "chars": 9059,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nScript to run post-installation tasks for imagesorcery-mcp.\nThis script creates the models di"
  },
  {
    "path": "src/imagesorcery_mcp/server.py",
    "chars": 5861,
    "preview": "import argparse\nimport os\nimport sys\nfrom pathlib import Path\n\nfrom fastmcp import FastMCP\nfrom fastmcp.server.middlewar"
  },
  {
    "path": "src/imagesorcery_mcp/telemetry_amplitude.py",
    "chars": 2856,
    "preview": "import logging\nimport os\nfrom typing import Any, Dict\n\nfrom amplitude import Amplitude, BaseEvent\n\nfrom imagesorcery_mcp"
  },
  {
    "path": "src/imagesorcery_mcp/telemetry_keys.py",
    "chars": 320,
    "preview": "# Auto-generated telemetry keys module.\n# This file is intended to be updated by build scripts (populate_telemetry_keys."
  },
  {
    "path": "src/imagesorcery_mcp/telemetry_posthog.py",
    "chars": 2799,
    "preview": "import logging\nimport os\nfrom typing import Any, Dict\n\nfrom posthog import Posthog\n\nfrom imagesorcery_mcp.telemetry_keys"
  },
  {
    "path": "src/imagesorcery_mcp/tools/README.md",
    "chars": 30805,
    "preview": "# 🪄 ImageSorcery MCP Server Tools Documentation\n\nThis document provides detailed information about each tool available i"
  },
  {
    "path": "src/imagesorcery_mcp/tools/__init__.py",
    "chars": 140,
    "preview": "# Import the central logger\nfrom imagesorcery_mcp.logging_config import logger\n\nlogger.info(\"🪄 ImageSorcery MCP tools pa"
  },
  {
    "path": "src/imagesorcery_mcp/tools/blur.py",
    "chars": 5708,
    "preview": "import os\nfrom typing import Annotated, Any, Dict, List, Optional\n\nimport cv2\nimport numpy as np\nfrom fastmcp import Fas"
  },
  {
    "path": "src/imagesorcery_mcp/tools/change_color.py",
    "chars": 3189,
    "preview": "import os\nfrom typing import Annotated, Literal, Optional\n\nimport cv2\nimport numpy as np\nfrom fastmcp import FastMCP\nfro"
  },
  {
    "path": "src/imagesorcery_mcp/tools/config.py",
    "chars": 8114,
    "preview": "\"\"\"\nConfiguration tool for ImageSorcery MCP.\n\nThis tool allows viewing and updating configuration values through the MCP"
  },
  {
    "path": "src/imagesorcery_mcp/tools/crop.py",
    "chars": 3235,
    "preview": "import os\nfrom typing import Annotated\n\nimport cv2\nfrom fastmcp import FastMCP\nfrom pydantic import Field\n\n# Import the "
  },
  {
    "path": "src/imagesorcery_mcp/tools/detect.py",
    "chars": 9727,
    "preview": "import os\nfrom pathlib import Path\nfrom typing import Annotated, Any, Dict, List, Literal, Optional, Union\n\nimport cv2\ni"
  },
  {
    "path": "src/imagesorcery_mcp/tools/draw_arrows.py",
    "chars": 3933,
    "preview": "import os\nfrom typing import Annotated, Any, Dict, List, Optional\n\nimport cv2\nfrom fastmcp import FastMCP\nfrom pydantic "
  },
  {
    "path": "src/imagesorcery_mcp/tools/draw_circle.py",
    "chars": 4309,
    "preview": "import os\nfrom typing import Annotated, List, Optional\n\nimport cv2\nfrom fastmcp import FastMCP\nfrom pydantic import Base"
  },
  {
    "path": "src/imagesorcery_mcp/tools/draw_lines.py",
    "chars": 3654,
    "preview": "import os\nfrom typing import Annotated, Any, Dict, List, Optional\n\nimport cv2\nfrom fastmcp import FastMCP\nfrom pydantic "
  },
  {
    "path": "src/imagesorcery_mcp/tools/draw_rectangle.py",
    "chars": 4375,
    "preview": "import os\nfrom typing import Annotated, Any, Dict, List, Optional\n\nimport cv2\nfrom fastmcp import FastMCP\nfrom pydantic "
  },
  {
    "path": "src/imagesorcery_mcp/tools/draw_text.py",
    "chars": 5453,
    "preview": "import os\nfrom typing import Annotated, Any, Dict, List, Optional\n\nimport cv2\nfrom fastmcp import FastMCP\nfrom pydantic "
  },
  {
    "path": "src/imagesorcery_mcp/tools/fill.py",
    "chars": 13201,
    "preview": "import os\nfrom typing import Annotated, Any, Dict, List, Optional\n\nimport cv2\nimport numpy as np\nfrom fastmcp import Fas"
  },
  {
    "path": "src/imagesorcery_mcp/tools/find.py",
    "chars": 16249,
    "preview": "import os\nfrom pathlib import Path\nfrom typing import Annotated, Any, Dict, List, Literal, Optional, Union\n\nimport cv2\ni"
  },
  {
    "path": "src/imagesorcery_mcp/tools/metainfo.py",
    "chars": 3107,
    "preview": "import datetime\nimport os\nfrom typing import Annotated, Any\n\nfrom fastmcp import FastMCP\nfrom PIL import Image\nfrom pyda"
  },
  {
    "path": "src/imagesorcery_mcp/tools/ocr.py",
    "chars": 7008,
    "preview": "import os\nfrom typing import Annotated, Dict, List, Optional, Union\n\nfrom fastmcp import FastMCP\nfrom pydantic import Fi"
  },
  {
    "path": "src/imagesorcery_mcp/tools/overlay.py",
    "chars": 5148,
    "preview": "import os\nfrom typing import Annotated, Optional\n\nimport cv2\nimport numpy as np\nfrom fastmcp import FastMCP\nfrom pydanti"
  },
  {
    "path": "src/imagesorcery_mcp/tools/resize.py",
    "chars": 6998,
    "preview": "import os\nfrom typing import Annotated, Optional\n\nimport cv2\nfrom fastmcp import FastMCP\nfrom pydantic import Field\n\n# I"
  },
  {
    "path": "src/imagesorcery_mcp/tools/rotate.py",
    "chars": 3259,
    "preview": "import os\nfrom typing import Annotated\n\nimport cv2\nimport imutils\nfrom fastmcp import FastMCP\nfrom pydantic import Field"
  },
  {
    "path": "tests/conftest.py",
    "chars": 529,
    "preview": "\"\"\"\nPytest configuration file for setting up test environment.\n\"\"\"\n\nimport os\n\n\ndef pytest_configure(config):\n    \"\"\"Con"
  },
  {
    "path": "tests/prompts/test_remove_background.py",
    "chars": 7992,
    "preview": "import pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server import mcp as image_sorcery_mcp_server\n\n"
  },
  {
    "path": "tests/resources/test_models.py",
    "chars": 15067,
    "preview": "import json\nimport os\nfrom pathlib import Path\n\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp"
  },
  {
    "path": "tests/test_config.py",
    "chars": 10364,
    "preview": "\"\"\"\nTests for the configuration management system.\n\"\"\"\n\nimport os\nimport tempfile\nfrom pathlib import Path\n\nimport pytes"
  },
  {
    "path": "tests/test_logging.py",
    "chars": 12321,
    "preview": "import inspect\nimport logging\nimport os\nimport re\nimport tempfile\nimport time\nfrom datetime import datetime\n\nimport pyte"
  },
  {
    "path": "tests/test_path_access.py",
    "chars": 4693,
    "preview": "import os\n\nimport pytest\nfrom fastmcp import Client, FastMCP\nfrom PIL import Image\n\nfrom imagesorcery_mcp.middlewares.pa"
  },
  {
    "path": "tests/test_server.py",
    "chars": 1094,
    "preview": "import pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server import mcp as image_sorcery_mcp_server\n\n"
  },
  {
    "path": "tests/test_telemetry.py",
    "chars": 6050,
    "preview": "\"\"\"\nTests for the telemetry system.\n\"\"\"\n\nimport logging\nimport os\nimport tempfile\nimport uuid\nfrom pathlib import Path\nf"
  },
  {
    "path": "tests/tools/test_blur.py",
    "chars": 18426,
    "preview": "import os\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server"
  },
  {
    "path": "tests/tools/test_change_color.py",
    "chars": 7382,
    "preview": "import os\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server"
  },
  {
    "path": "tests/tools/test_config_tool.py",
    "chars": 9289,
    "preview": "\"\"\"\nEnd-to-end tests for the config tool through MCP client interface.\n\"\"\"\n\nimport os\nimport tempfile\nfrom pathlib impor"
  },
  {
    "path": "tests/tools/test_crop.py",
    "chars": 6240,
    "preview": "import os\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server"
  },
  {
    "path": "tests/tools/test_detect.py",
    "chars": 31191,
    "preview": "import os\nimport shutil\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\nfrom PIL import"
  },
  {
    "path": "tests/tools/test_draw_arrows.py",
    "chars": 7146,
    "preview": "import os\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server"
  },
  {
    "path": "tests/tools/test_draw_circle.py",
    "chars": 8095,
    "preview": "import os\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server"
  },
  {
    "path": "tests/tools/test_draw_lines.py",
    "chars": 7035,
    "preview": "import os\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server"
  },
  {
    "path": "tests/tools/test_draw_rectangle.py",
    "chars": 9158,
    "preview": "import os\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server"
  },
  {
    "path": "tests/tools/test_draw_text.py",
    "chars": 10643,
    "preview": "import os\n\nimport cv2\nimport easyocr\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesor"
  },
  {
    "path": "tests/tools/test_fill.py",
    "chars": 28130,
    "preview": "import os\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server"
  },
  {
    "path": "tests/tools/test_find.py",
    "chars": 29749,
    "preview": "import os\nimport shutil\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\nfrom PIL import"
  },
  {
    "path": "tests/tools/test_metainfo.py",
    "chars": 5395,
    "preview": "\nimport pytest\nfrom fastmcp import Client, FastMCP\nfrom PIL import Image\n\nfrom imagesorcery_mcp.server import mcp as ima"
  },
  {
    "path": "tests/tools/test_ocr.py",
    "chars": 7173,
    "preview": "\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server import mc"
  },
  {
    "path": "tests/tools/test_overlay.py",
    "chars": 6892,
    "preview": "import os\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server"
  },
  {
    "path": "tests/tools/test_resize.py",
    "chars": 18623,
    "preview": "import os\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server"
  },
  {
    "path": "tests/tools/test_rotate.py",
    "chars": 10320,
    "preview": "import os\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom fastmcp import Client, FastMCP\n\nfrom imagesorcery_mcp.server"
  }
]

About this extraction

This page contains the full source code of the sunriseapps/imagesorcery-mcp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 80 files (546.9 KB), approximately 126.1k tokens, and a symbol index with 399 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!