From e0c07d678e2937828eb80697e400f3deeededb22 Mon Sep 17 00:00:00 2001 From: Alex Kazaiev Date: Tue, 16 Dec 2025 20:51:02 -0600 Subject: [PATCH] Initial commit: DreamTail MCP client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit šŸŽØ MCP integration for DreamTail (SDXL on Jetson Orin) - dreamtail_generate: Create images with prompts - dreamtail_get_info: Get last generated image path - Inline display support for Claude Desktop - Configurable JPEG quality and download directory Built with love for the hardware dragon 🦊 --- README.md | 394 +++++++++++++++++++++++++++++ claude_desktop_config.example.json | 18 ++ dreamtail_mcp.py | 377 +++++++++++++++++++++++++++ requirements.txt | 3 + test_tool.py | 51 ++++ 5 files changed, 843 insertions(+) create mode 100755 README.md create mode 100755 claude_desktop_config.example.json create mode 100755 dreamtail_mcp.py create mode 100755 requirements.txt create mode 100755 test_tool.py diff --git a/README.md b/README.md new file mode 100755 index 0000000..2f0cf4d --- /dev/null +++ b/README.md @@ -0,0 +1,394 @@ +# šŸŽØ DreamTail MCP Server + +Model Context Protocol (MCP) server for integrating [DreamTail](../dreamtail) SDXL image generation with Claude Desktop. + +## Overview + +This MCP server exposes DreamTail's Stable Diffusion XL image generation capabilities to Claude Desktop through a single, simple tool: `dreamtail_generate()`. + +**Features:** +- šŸŽØ Generate high-quality 1024x1024 images using SDXL +- šŸ–¼ļø **Inline image display** in Claude Desktop (new!) +- ā±ļø Automatic job submission and polling (no manual checking) +- šŸ’¾ Automatic download to local folder (configurable) +- šŸ—œļø Smart format conversion with adaptive compression for 1MB limit +- šŸ”„ Built-in progress tracking and timeout handling +- šŸ’¬ Natural language prompts via Claude +- ⚔ Powered by Jetson AGX Orin GPU + +## Prerequisites + +1. **DreamTail service** running on bigorin:8765 +2. **Python 3.8+** installed +3. **Claude Desktop** installed + +## Installation + +### 1. Install Dependencies + +```bash +cd /home/alex/Projects/dreamtail-mcp +pip install -r requirements.txt +``` + +Or using `uv` (recommended): + +```bash +uv pip install -r requirements.txt +``` + +### 2. Make Script Executable + +```bash +chmod +x dreamtail_mcp.py +``` + +### 3. Test the Server Locally + +```bash +# Test that the server starts without errors +python3 dreamtail_mcp.py +# Press Ctrl+C to stop +``` + +## Claude Desktop Configuration + +### Step 1: Locate Configuration File + +**macOS:** +```bash +~/Library/Application Support/Claude/claude_desktop_config.json +``` + +**Or use Claude Desktop:** +- Open Claude Desktop +- Go to: Settings → Developer → "Edit Config" + +### Step 2: Add DreamTail MCP Server + +Add this configuration to your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "dreamtail": { + "command": "python3", + "args": [ + "/home/alex/Projects/dreamtail-mcp/dreamtail_mcp.py" + ], + "env": { + "DREAMTAIL_BASE_URL": "http://192.168.50.30:8765", + "DREAMTAIL_DOWNLOAD_DIR": "/Users/yourname/Pictures/dreamtail", + "DREAMTAIL_FORMAT": "jpeg", + "DREAMTAIL_JPEG_QUALITY": "95", + "DREAMTAIL_INLINE_DISPLAY": "true", + "DREAMTAIL_INLINE_QUALITY": "85" + } + } + } +} +``` + +**Important notes:** +- Use the **absolute path** to `dreamtail_mcp.py` +- Use the **IP address** for `DREAMTAIL_BASE_URL` instead of hostname (e.g., `192.168.50.30:8765` instead of `bigorin:8765`) +- Set `DREAMTAIL_DOWNLOAD_DIR` to your preferred download location (defaults to `~/dreamtail_images`) +- `DREAMTAIL_FORMAT` options: `"jpeg"` (default), `"png"`, or `"both"` - for saved files +- `DREAMTAIL_JPEG_QUALITY`: 1-100 (default: 95) - quality for saved files +- `DREAMTAIL_INLINE_DISPLAY`: `"true"` (default) or `"false"` - enable inline image display in Claude Desktop +- `DREAMTAIL_INLINE_QUALITY`: 1-100 (default: 85) - JPEG quality for inline display (lower = smaller file for 1MB limit) +- If you have other MCP servers configured, add the "dreamtail" entry alongside them + +### Step 3: Restart Claude Desktop + +**You MUST completely quit and restart Claude Desktop** for the changes to take effect: + +**macOS:** +```bash +# Quit Claude Desktop completely +# Then reopen from Applications +``` + +### Step 4: Verify Installation + +1. Open Claude Desktop +2. Look for the šŸ”Ø (hammer) icon in the bottom-right corner of the chat input +3. Click it to see available tools +4. You should see "DreamTail Image Generator" with the `dreamtail_generate` tool + +## Usage + +Once configured, you can ask Claude to generate images naturally: + +### Example Prompts + +**Basic generation:** +``` +Generate an image of a serene mountain landscape at sunset +``` + +**With negative prompt:** +``` +Create an image of a cat wearing a wizard hat, but avoid any blurry or +low-quality elements +``` + +**Custom dimensions:** +``` +Generate a 512x512 image of a futuristic cityscape +``` + +**More inference steps for quality:** +``` +Create a highly detailed portrait of a robot, using 50 inference steps +``` + +### What Claude Will Do + +1. Parse your natural language request +2. Call `dreamtail_generate()` with appropriate parameters +3. Wait ~45-60 seconds while the image generates +4. **Display the image inline** in the chat (when enabled) +5. Automatically save full-resolution version to your local folder + +### Response Format + +**With inline display enabled (default):** + +Claude will show the generated image directly in the conversation! No need to open files or URLs - the image appears immediately for analysis and discussion. + +The image displayed inline is: +- Compressed JPEG at quality 85 (or lower if needed to fit 1MB limit) +- May be slightly resized if the original is too large +- Optimized for fast loading while maintaining visual quality + +Full-resolution files are saved to disk: +- Location: `~/dreamtail_images/{job_id}.jpg` (and `.png` if configured) +- Quality: Your configured JPEG quality (default 95) or original PNG +- Use these for high-quality archival or further editing + +**With inline display disabled:** + +Claude will provide a text message: +``` +Image generated successfully! +Saved to: /Users/yourname/Pictures/dreamtail/{job_id}.jpg +``` + +You can then open the file directly from your local filesystem. + +## Tool Reference + +### `dreamtail_generate()` + +**Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `prompt` | string | *required* | Text description of image to generate | +| `negative_prompt` | string | None | What to avoid in the image (optional) | +| `width` | integer | 1024 | Image width (must be multiple of 8) | +| `height` | integer | 1024 | Image height (must be multiple of 8) | +| `num_inference_steps` | integer | 30 | Denoising steps (20-50, higher = better quality but slower) | +| `client_id` | string | "claude" | Client identifier | + +**Returns:** + +```python +{ + "status": "completed", # or "failed", "timeout", "error" + "job_id": "uuid", + "image_url": "http://192.168.50.30:8765/result/{job_id}", + "local_paths": ["/path/to/download/dir/{job_id}.jpg"], # List of saved files + "progress": 100, + "message": "Image generated successfully! Saved to: /path/to/download/dir/{job_id}.jpg" +} +``` + +**Notes:** +- `local_paths` contains 1 file (JPEG or PNG) or 2 files (when `DREAMTAIL_FORMAT="both"`) +- JPEG files are optimized and ~10x smaller than PNG with negligible quality loss at 95% + +**Generation Time:** +- Typical: ~45-60 seconds for 1024x1024 @ 30 steps +- Fast: ~30-40 seconds for 20 steps or 512x512 +- High quality: ~60-90 seconds for 50 steps + +## Troubleshooting + +### Tool Not Appearing in Claude Desktop + +1. **Check config path**: Ensure you're editing the correct config file +2. **Verify absolute path**: Must be absolute, not relative (e.g., `/home/alex/...` not `~/...`) +3. **Restart Claude**: Completely quit and reopen Claude Desktop +4. **Check logs**: Look for errors in Claude Desktop's logs + +### "Failed to connect to DreamTail" + +1. **Check DreamTail is running**: + ```bash + curl http://bigorin:8765/health + ``` + +2. **Verify network access**: Ensure your machine can reach bigorin:8765 + +3. **Update config**: Check `DREAMTAIL_BASE_URL` in Claude config + +### "Generation exceeded timeout" + +- Normal generation takes 45-60 seconds +- If you get timeouts, DreamTail might be overloaded or having issues +- Check DreamTail logs: `ssh bigorin "sudo docker logs dreamtail"` + +### Import Errors + +```bash +# Reinstall dependencies +cd /home/alex/Projects/dreamtail-mcp +pip install --upgrade -r requirements.txt +``` + +## Advanced Configuration + +### Inline Display Options + +Control how images are displayed in Claude Desktop: + +```json +"env": { + "DREAMTAIL_INLINE_DISPLAY": "true", + "DREAMTAIL_INLINE_QUALITY": "85" +} +``` + +**Options:** +- `DREAMTAIL_INLINE_DISPLAY`: `"true"` (default) or `"false"` + - `"true"`: Images display inline in Claude Desktop (recommended) + - `"false"`: Return text message with file path only +- `DREAMTAIL_INLINE_QUALITY`: 50-95 (default: 85) + - JPEG compression quality for inline display + - Lower values = smaller files (required for 1MB limit) + - 85: Good balance of quality and size (~150-300KB) + - 70-75: More aggressive compression for large images + - Will automatically reduce if image still exceeds 1MB + +**Why inline quality is lower than saved quality:** +- Claude Desktop has a 1MB limit for tool responses +- Images must be compressed to fit this limit +- Full-resolution images are always saved to disk at your configured quality +- You get the best of both worlds: instant viewing + high-quality archives + +### Image Format Options + +Control saved file format in your Claude Desktop config: + +```json +"env": { + "DREAMTAIL_BASE_URL": "http://192.168.50.30:8765", + "DREAMTAIL_DOWNLOAD_DIR": "/Users/yourname/Pictures/dreamtail", + "DREAMTAIL_FORMAT": "jpeg", + "DREAMTAIL_JPEG_QUALITY": "95" +} +``` + +**Format Options:** +- `"jpeg"` (default): Save as JPEG at specified quality + - Quality 95: Visually lossless, ~10x smaller than PNG + - Quality 85: Very good, ~15x smaller than PNG + - Quality 75: Good for web, ~20x smaller than PNG +- `"png"`: Save as PNG (lossless, ~2-5MB per image) +- `"both"`: Save both JPEG and PNG versions + +**File Size Comparison (1024x1024):** +- PNG: ~2-5MB +- JPEG Q95: ~200-500KB (recommended) +- JPEG Q85: ~150-300KB +- JPEG Q75: ~100-200KB + +### Custom Download Directory + +**Tip:** Use an absolute path, not `~`. For example: +- āœ… `/Users/alex/Pictures/dreamtail` +- āœ… `/home/alex/dreamtail_images` +- āŒ `~/Pictures/dreamtail` (may not expand correctly in Claude Desktop) + +If not specified, images will be saved to `~/dreamtail_images` by default. + +### Custom Timeout + +Edit `dreamtail_mcp.py` and change: +```python +DEFAULT_TIMEOUT = 180 # 3 minutes instead of 2 +``` + +### Different DreamTail Instance + +In Claude Desktop config: +```json +"env": { + "DREAMTAIL_BASE_URL": "http://192.168.50.30:8765" +} +``` + +### Using uv (Python Package Manager) + +In Claude Desktop config: +```json +{ + "mcpServers": { + "dreamtail": { + "command": "uv", + "args": [ + "--directory", + "/home/alex/Projects/dreamtail-mcp", + "run", + "dreamtail_mcp.py" + ], + "env": { + "DREAMTAIL_BASE_URL": "http://bigorin:8765" + } + } + } +} +``` + +## Architecture + +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Claude Desktop │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ stdio (MCP) + ā–¼ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ dreamtail_mcp │ (this server) +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ HTTP REST + ā–¼ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ DreamTail │ (bigorin:8765) +│ SDXL Service │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +**Flow:** +1. User asks Claude to generate an image +2. Claude calls `dreamtail_generate()` via MCP +3. MCP server submits job to DreamTail API +4. MCP server polls status every 3 seconds +5. When complete, downloads image to local folder +6. Claude shows the user the local file path + +## Related Projects + +- **DreamTail**: SDXL image generation service (../dreamtail) +- **Lyra**: AI assistant ecosystem +- **Model Context Protocol**: https://modelcontextprotocol.io + +## License + +Part of the Lyra project ecosystem. + +--- + +**Built with ā¤ļø for Claude Desktop integration** diff --git a/claude_desktop_config.example.json b/claude_desktop_config.example.json new file mode 100755 index 0000000..7a2cf26 --- /dev/null +++ b/claude_desktop_config.example.json @@ -0,0 +1,18 @@ +{ + "mcpServers": { + "dreamtail": { + "command": "python3", + "args": [ + "/home/alex/Projects/dreamtail-mcp/dreamtail_mcp.py" + ], + "env": { + "DREAMTAIL_BASE_URL": "http://192.168.50.30:8765", + "DREAMTAIL_DOWNLOAD_DIR": "/Users/yourname/Pictures/dreamtail", + "DREAMTAIL_FORMAT": "jpeg", + "DREAMTAIL_JPEG_QUALITY": "95", + "DREAMTAIL_INLINE_DISPLAY": "true", + "DREAMTAIL_INLINE_QUALITY": "85" + } + } + } +} diff --git a/dreamtail_mcp.py b/dreamtail_mcp.py new file mode 100755 index 0000000..62dd399 --- /dev/null +++ b/dreamtail_mcp.py @@ -0,0 +1,377 @@ +#!/usr/bin/env python3 +""" +DreamTail MCP Server + +Model Context Protocol server for integrating DreamTail SDXL image generation +with Claude Desktop. + +Provides a single tool: dreamtail_generate() that submits a job, polls for +completion, and returns the image URL. +""" + +import asyncio +import os +import logging +from typing import Optional, List, Union, Dict, Any +from pathlib import Path +from io import BytesIO +from datetime import datetime +import httpx +from fastmcp import FastMCP +from fastmcp.utilities.types import Image as MCPImage +from PIL import Image + +# Configure logging to file for debugging +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/tmp/dreamtail_mcp.log'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# Initialize MCP server +mcp = FastMCP("DreamTail Image Generator") + +# Configuration from environment +DREAMTAIL_BASE_URL = os.getenv("DREAMTAIL_BASE_URL", "http://bigorin.local:8765") +DOWNLOAD_DIR = os.getenv("DREAMTAIL_DOWNLOAD_DIR", os.path.expanduser("~/dreamtail_images")) +DOWNLOAD_FORMAT = os.getenv("DREAMTAIL_FORMAT", "jpeg") # "jpeg", "png", or "both" +JPEG_QUALITY = int(os.getenv("DREAMTAIL_JPEG_QUALITY", "95")) # 1-100 +INLINE_DISPLAY = os.getenv("DREAMTAIL_INLINE_DISPLAY", "true").lower() == "true" # Enable inline display in Claude +INLINE_QUALITY = int(os.getenv("DREAMTAIL_INLINE_QUALITY", "85")) # Quality for inline display (70-95) +MAX_INLINE_SIZE_MB = 0.90 # Leave margin below 1MB limit (accounting for dict metadata) +DEFAULT_POLL_INTERVAL = 3 # seconds +DEFAULT_TIMEOUT = 120 # seconds (2 minutes) + +# Create download directory if it doesn't exist +Path(DOWNLOAD_DIR).mkdir(parents=True, exist_ok=True) + +# Track last generated image info +last_generation_info: Optional[Dict[str, Any]] = None +logger.info(f"Download directory: {DOWNLOAD_DIR}") +logger.info(f"Download format: {DOWNLOAD_FORMAT} (JPEG quality: {JPEG_QUALITY})") +logger.info(f"Inline display: {INLINE_DISPLAY} (quality: {INLINE_QUALITY})") + + +async def download_image(client: httpx.AsyncClient, image_url: str, job_id: str) -> List[str]: + """ + Download the generated image to local storage with format conversion. + + Args: + client: httpx AsyncClient instance + image_url: URL to download the image from + job_id: Job ID to use as filename + + Returns: + List of local file paths where images were saved + """ + logger.info(f"Downloading image from {image_url}") + + # Download the image + response = await client.get(image_url) + response.raise_for_status() + + # Open image with PIL + img = Image.open(BytesIO(response.content)) + + saved_paths = [] + + # Save based on format preference + if DOWNLOAD_FORMAT in ["jpeg", "both"]: + # Convert RGBA to RGB if needed (JPEG doesn't support transparency) + if img.mode in ("RGBA", "LA", "P"): + rgb_img = Image.new("RGB", img.size, (255, 255, 255)) + rgb_img.paste(img, mask=img.split()[-1] if img.mode == "RGBA" else None) + img_to_save = rgb_img + else: + img_to_save = img + + jpeg_path = Path(DOWNLOAD_DIR) / f"{job_id}.jpg" + img_to_save.save(jpeg_path, "JPEG", quality=JPEG_QUALITY, optimize=True) + saved_paths.append(str(jpeg_path)) + logger.info(f"JPEG saved: {jpeg_path} (quality {JPEG_QUALITY})") + + if DOWNLOAD_FORMAT in ["png", "both"]: + png_path = Path(DOWNLOAD_DIR) / f"{job_id}.png" + img.save(png_path, "PNG") + saved_paths.append(str(png_path)) + logger.info(f"PNG saved: {png_path}") + + if not saved_paths: + # Fallback to PNG if invalid format specified + png_path = Path(DOWNLOAD_DIR) / f"{job_id}.png" + img.save(png_path, "PNG") + saved_paths.append(str(png_path)) + logger.warning(f"Invalid format '{DOWNLOAD_FORMAT}', defaulting to PNG: {png_path}") + + return saved_paths + + +async def prepare_inline_image(client: httpx.AsyncClient, image_url: str) -> Optional[MCPImage]: + """ + Prepare image for inline display in Claude Desktop. + + Compresses image to stay under 1MB limit while maintaining visual quality. + + Args: + client: httpx AsyncClient instance + image_url: URL to download the image from + + Returns: + MCPImage object for inline display, or None if preparation fails + """ + try: + logger.info(f"Preparing inline image from {image_url}") + + # Download the image + response = await client.get(image_url) + response.raise_for_status() + + # Open with PIL + pil_image = Image.open(BytesIO(response.content)) + + # Convert to RGB if needed (JPEG doesn't support transparency) + if pil_image.mode in ("RGBA", "LA", "P"): + rgb_image = Image.new("RGB", pil_image.size, (255, 255, 255)) + if pil_image.mode == "RGBA": + rgb_image.paste(pil_image, mask=pil_image.split()[-1]) + else: + rgb_image.paste(pil_image) + pil_image = rgb_image + elif pil_image.mode != "RGB": + pil_image = pil_image.convert("RGB") + + # Try compression at configured quality first + buffer = BytesIO() + pil_image.save(buffer, format="JPEG", quality=INLINE_QUALITY, optimize=True) + img_bytes = buffer.getvalue() + size_mb = len(img_bytes) / (1024 * 1024) + + logger.info(f"Initial compression: {size_mb:.2f}MB at quality {INLINE_QUALITY}") + + # If still too large, try lower quality + if size_mb > MAX_INLINE_SIZE_MB: + quality = INLINE_QUALITY - 10 + while quality >= 50 and size_mb > MAX_INLINE_SIZE_MB: + buffer = BytesIO() + pil_image.save(buffer, format="JPEG", quality=quality, optimize=True) + img_bytes = buffer.getvalue() + size_mb = len(img_bytes) / (1024 * 1024) + logger.info(f"Recompressed: {size_mb:.2f}MB at quality {quality}") + quality -= 10 + + # If still too large, try resizing + if size_mb > MAX_INLINE_SIZE_MB: + scale = 0.8 + while scale >= 0.5 and size_mb > MAX_INLINE_SIZE_MB: + new_size = (int(pil_image.width * scale), int(pil_image.height * scale)) + resized = pil_image.resize(new_size, Image.Resampling.LANCZOS) + buffer = BytesIO() + resized.save(buffer, format="JPEG", quality=75, optimize=True) + img_bytes = buffer.getvalue() + size_mb = len(img_bytes) / (1024 * 1024) + logger.info(f"Resized to {new_size}: {size_mb:.2f}MB") + scale -= 0.1 + + # Final check + if size_mb > MAX_INLINE_SIZE_MB: + logger.warning(f"Image still too large ({size_mb:.2f}MB), cannot display inline") + return None + + logger.info(f"āœ“ Inline image ready: {size_mb:.2f}MB") + return MCPImage(data=img_bytes, format="jpeg") + + except Exception as e: + logger.error(f"Failed to prepare inline image: {e}") + return None + + +@mcp.tool() +async def dreamtail_generate( + prompt: str, + negative_prompt: Optional[str] = None, + width: int = 1024, + height: int = 1024, + num_inference_steps: int = 30, + client_id: str = "claude" +) -> Union[MCPImage, str]: + """ + Generate an image using SDXL on the Jetson Orin. + + This tool submits an image generation job to DreamTail, waits for it to + complete (polling every 3 seconds), and returns the generated image. + + When inline display is enabled (default), the image is shown directly in + Claude Desktop. Full-resolution images are also saved to your local folder. + + Generation typically takes 45-60 seconds for a 1024x1024 image. + + Args: + prompt: Text description of the image to generate + negative_prompt: Optional text describing what to avoid in the image + width: Image width in pixels (must be multiple of 8, default 1024) + height: Image height in pixels (must be multiple of 8, default 1024) + num_inference_steps: Number of denoising steps (20-50, default 30) + client_id: Client identifier (default "claude") + + Returns: + MCPImage object for inline display (if enabled and successful), + or text message with file paths and filename + """ + try: + logger.info("=" * 60) + logger.info(f"TOOL CALLED: dreamtail_generate") + logger.info(f"DreamTail base URL: {DREAMTAIL_BASE_URL}") + logger.info(f"Prompt: {prompt[:50]}...") + logger.info(f"Inline display: {INLINE_DISPLAY}") + logger.info("=" * 60) + except Exception as e: + logger.error(f"Error in initial logging: {e}") + + async with httpx.AsyncClient(timeout=30.0) as client: + try: + # Step 1: Submit generation job + submit_url = f"{DREAMTAIL_BASE_URL}/generate" + logger.debug(f"Submitting to: {submit_url}") + + submit_response = await client.post( + submit_url, + json={ + "prompt": prompt, + "client_id": client_id, + "negative_prompt": negative_prompt, + "params": { + "width": width, + "height": height, + "num_inference_steps": num_inference_steps + } + } + ) + submit_response.raise_for_status() + job_data = submit_response.json() + job_id = job_data["job_id"] + logger.info(f"Job submitted successfully: {job_id}") + + # Step 2: Poll for completion + elapsed = 0 + last_progress = 0 + + while elapsed < DEFAULT_TIMEOUT: + # Check job status + status_response = await client.get( + f"{DREAMTAIL_BASE_URL}/status/{job_id}" + ) + status_response.raise_for_status() + status_data = status_response.json() + + current_status = status_data["status"] + last_progress = status_data.get("progress", 0) + + # Job completed successfully + if current_status == "completed": + image_url = f"{DREAMTAIL_BASE_URL}/result/{job_id}" + + # Try inline display if enabled + if INLINE_DISPLAY: + try: + inline_image = await prepare_inline_image(client, image_url) + + # Download full-resolution to local storage + local_paths = [] + try: + local_paths = await download_image(client, image_url, job_id) + logger.info(f"Saved full-res to: {', '.join(local_paths)}") + except Exception as save_error: + logger.warning(f"Failed to save full-res locally: {save_error}") + + # Return inline image if successful + if inline_image: + logger.info("āœ“ Inline image prepared successfully") + + # Store info about this generation + global last_generation_info + filename = Path(local_paths[0]).name if local_paths else f"{job_id}.jpg" + last_generation_info = { + "job_id": job_id, + "filename": filename, + "saved_paths": local_paths, + "prompt": prompt[:100], # Truncate long prompts + "timestamp": datetime.now().isoformat() + } + + # Note: We return just the MCPImage, not wrapped in a dict + # Use get_last_dreamtail_info() to retrieve filename and paths + return inline_image + else: + logger.warning("Inline image preparation failed, falling back to file path") + + except Exception as inline_error: + logger.error(f"Inline display error: {inline_error}") + + # Fallback: Download and return message with file paths + try: + local_paths = await download_image(client, image_url, job_id) + + # Format message based on number of files + if len(local_paths) == 1: + filename = Path(local_paths[0]).name + message = f"āœ“ Image generated successfully!\n\nFilename: {filename}\nSaved to: {local_paths[0]}\nJob ID: {job_id}" + else: + filename = Path(local_paths[0]).name + paths_str = "\n".join([f" - {p}" for p in local_paths]) + message = f"āœ“ Image generated successfully!\n\nFilename: {filename}\nSaved to:\n{paths_str}\nJob ID: {job_id}" + + return message + except Exception as download_error: + logger.error(f"Failed to download image: {download_error}") + return f"Image generated successfully but download failed: {download_error}. You can download it manually from: {image_url}" + + # Job failed + if current_status == "failed": + error = status_data.get("error", "Unknown error") + return f"āŒ Image generation failed: {error}" + + # Still processing, wait and poll again + await asyncio.sleep(DEFAULT_POLL_INTERVAL) + elapsed += DEFAULT_POLL_INTERVAL + + # Timeout reached + return f"ā±ļø Generation exceeded {DEFAULT_TIMEOUT} second timeout. Job {job_id} is still processing. Check status at: {DREAMTAIL_BASE_URL}/status/{job_id}" + + except httpx.HTTPStatusError as e: + logger.exception(f"HTTPStatusError in dreamtail_generate: {e}") + return f"āŒ DreamTail API error: {e.response.status_code} - {e.response.text}" + except httpx.RequestError as e: + logger.exception(f"RequestError in dreamtail_generate: {e}") + return f"āŒ Failed to connect to DreamTail at {DREAMTAIL_BASE_URL}: {str(e)}" + except Exception as e: + logger.exception(f"Unexpected error in dreamtail_generate: {e}") + return f"āŒ Unexpected error: {str(e)}" + + +@mcp.tool() +def dreamtail_get_info() -> str: + """ + Get the file path of the last image generated by DreamTail. + + Useful for getting the file path after dreamtail_generate() returns an image, + so you can send it to Matrix with send_matrix_image_from_file(). + + Returns: + File path of the last generated image, or error message if none exists + """ + global last_generation_info + + if last_generation_info is None: + return "No DreamTail images generated yet in this session" + + # Return just the first saved path (primary file) + return last_generation_info['saved_paths'][0] + + +if __name__ == "__main__": + # Run the MCP server (uses stdio transport by default) + mcp.run() diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..cda4921 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +fastmcp +httpx +Pillow diff --git a/test_tool.py b/test_tool.py new file mode 100755 index 0000000..08eec59 --- /dev/null +++ b/test_tool.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +Quick test script to verify dreamtail_generate works +Run this to test the tool without Claude Desktop +""" + +import asyncio +import sys +sys.path.insert(0, '/home/alex/Projects/dreamtail-mcp') + +# Import the actual function by accessing it from the tool wrapper +import dreamtail_mcp + +async def test(): + print("Testing dreamtail_generate tool...") + print("=" * 60) + + try: + # Get the actual function from the tool wrapper + tool = dreamtail_mcp.mcp._tool_manager._tools['dreamtail_generate'] + actual_function = tool.fn + + result = await actual_function( + prompt="a simple test image of a red circle", + width=512, # Smaller for faster testing + height=512, + num_inference_steps=20 # Fewer steps for faster testing + ) + + print("\nāœ“ Tool executed successfully!") + print(f"Result type: {type(result)}") + print(f"Result: {result}") + + # If it's an Image object, check its properties + if hasattr(result, 'data'): + print(f"Image data size: {len(result.data)} bytes") + print(f"Image format: {result.format if hasattr(result, 'format') else 'unknown'}") + + return True + + except Exception as e: + print(f"\nāŒ Tool failed with error:") + print(f"Error type: {type(e).__name__}") + print(f"Error: {e}") + import traceback + traceback.print_exc() + return False + +if __name__ == "__main__": + success = asyncio.run(test()) + sys.exit(0 if success else 1)