Initial commit: DreamTail MCP client

🎨 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 🦊
This commit is contained in:
Alex Kazaiev
2025-12-16 20:51:02 -06:00
commit e0c07d678e
5 changed files with 843 additions and 0 deletions

394
README.md Executable file
View File

@@ -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**

View File

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

377
dreamtail_mcp.py Executable file
View File

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

3
requirements.txt Executable file
View File

@@ -0,0 +1,3 @@
fastmcp
httpx
Pillow

51
test_tool.py Executable file
View File

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