Skip to main content
Glama
satomic

Azure AI Image Editor MCP Server

by satomic
mcp_server.py18 kB
#!/usr/bin/env python3 """ Azure Image Editor MCP Server - STDIO Version A MCP server that supports Azure AI Foundry image generation and editing capabilities. """ import asyncio import base64 import os import sys import logging from datetime import datetime from typing import Any from pathlib import Path try: import anyio from mcp.server import Server, NotificationOptions from mcp.server.models import InitializationOptions import mcp.server.stdio import mcp.types as types from dotenv import load_dotenv except ImportError as e: print(f"Error: Required dependency missing: {e}", file=sys.stderr) print("Please install required packages: pip install mcp python-dotenv", file=sys.stderr) sys.exit(1) # Add src directory to Python path sys.path.insert(0, str(Path(__file__).parent)) try: from azure_image_client import AzureImageGenerator except ImportError as e: print(f"Error: Cannot import azure_image_client: {e}", file=sys.stderr) print("Please ensure azure_image_client.py is in the same directory", file=sys.stderr) sys.exit(1) # Load environment variables load_dotenv() class _MemoryStreamFactoryWrapper: """Allow anyio.create_memory_object_stream[...] syntax at runtime.""" __slots__ = ("_factory",) def __init__(self, factory): self._factory = factory def __call__(self, *args, **kwargs): return self._factory(*args, **kwargs) def __getitem__(self, _item): return self def __getattr__(self, name): return getattr(self._factory, name) def _patch_anyio_memory_stream_factory() -> bool: factory = anyio.create_memory_object_stream if isinstance(factory, _MemoryStreamFactoryWrapper): return False if hasattr(factory, "__getitem__"): return False anyio.create_memory_object_stream = _MemoryStreamFactoryWrapper(factory) return True # Set up logging def setup_logging(): """Setup logging configuration""" log_dir = Path("logs") log_dir.mkdir(exist_ok=True) log_file = log_dir / f"mcp_server_{datetime.now().strftime('%Y%m%d')}.log" logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(log_file, encoding='utf-8'), logging.StreamHandler(sys.stderr) ] ) return logging.getLogger(__name__) logger = setup_logging() if _patch_anyio_memory_stream_factory(): logger.info("Applied compatibility patch for anyio.create_memory_object_stream") # Create the MCP server server = Server("azure-image-editor") def get_azure_config(): """Get Azure configuration from environment variables""" try: required_vars = { "AZURE_BASE_URL": "Base URL for Azure AI service", "AZURE_API_KEY": "API key for Azure AI service", "AZURE_DEPLOYMENT_NAME": "Deployment model name" } missing_vars = [] config = {} for var_name, description in required_vars.items(): value = os.getenv(var_name) if not value or not value.strip(): missing_vars.append(f"{var_name} ({description})") else: key_map = { "AZURE_BASE_URL": "base_url", "AZURE_API_KEY": "api_key", "AZURE_DEPLOYMENT_NAME": "deployment_name" } config[key_map[var_name]] = value.strip() # Handle optional variables model_value = os.getenv("AZURE_MODEL") config["model"] = model_value.strip() if model_value and model_value.strip() else "flux.1-kontext-pro" api_version_value = os.getenv("AZURE_API_VERSION") config["api_version"] = api_version_value.strip() if api_version_value and api_version_value.strip() else "2025-04-01-preview" if missing_vars: error_msg = f"Missing required environment variables: {', '.join(missing_vars)}" logger.error(error_msg) raise ValueError(error_msg) # Basic validation if not config["base_url"].startswith(("http://", "https://")): raise ValueError("AZURE_BASE_URL must be a valid HTTP/HTTPS URL") if len(config["api_key"]) < 10: raise ValueError("AZURE_API_KEY is too short, please check if the API key is correct") if not config["deployment_name"]: raise ValueError("AZURE_DEPLOYMENT_NAME cannot be empty") logger.info("Azure configuration loaded successfully") return config except ValueError: raise except Exception as e: error_msg = f"Error loading Azure configuration: {str(e)}" logger.error(error_msg) raise Exception(error_msg) def is_english_text(text: str) -> bool: """Check if text is primarily in English""" if not text.strip(): return True # Check if it contains CJK characters for char in text: if '\u4e00' <= char <= '\u9fff' or \ '\u3040' <= char <= '\u309f' or \ '\u30a0' <= char <= '\u30ff' or \ '\uac00' <= char <= '\ud7af': return False return True def validate_image_size(size: str) -> bool: """Validate image size format""" supported_sizes = ["1024x1024", "1792x1024", "1024x1792"] return size in supported_sizes @server.list_tools() async def handle_list_tools() -> list[types.Tool]: """List available tools""" default_size = os.getenv("DEFAULT_IMAGE_SIZE", "1024x1024") return [ types.Tool( name="generate_image", description="Generate images from text prompts using Azure AI Foundry (English prompts only)", inputSchema={ "type": "object", "properties": { "prompt": { "type": "string", "description": "English description for image generation", }, "size": { "type": "string", "description": f"Image size, supports 1024x1024, 1792x1024, 1024x1792, default: {default_size}", "default": default_size, "enum": ["1024x1024", "1792x1024", "1024x1792"] }, "output_path": { "type": "string", "description": "absolute output file path" } }, "required": ["prompt", "size", "output_path"] }, ), types.Tool( name="edit_image", description="Edit existing images using Azure AI Foundry (English prompts only)", inputSchema={ "type": "object", "properties": { "image_path": { "type": "string", "description": "Path to the image file to edit" }, "prompt": { "type": "string", "description": "English description of how to edit the image" }, "size": { "type": "string", "description": "Optional size for edited image, if not specified uses original image dimensions", "enum": ["1024x1024", "1792x1024", "1024x1792"] }, "output_path": { "type": "string", "description": "absolute output file path" } }, "required": ["image_path", "prompt", "size", "output_path"] }, ) ] @server.call_tool() async def handle_call_tool( name: str, arguments: dict[str, Any] | None ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: """Handle tool calls""" try: if name == "generate_image": return await handle_generate_image(arguments or {}) elif name == "edit_image": return await handle_edit_image(arguments or {}) else: return [types.TextContent( type="text", text=f"Error: Unknown tool '{name}'" )] except Exception as e: error_msg = f"Unexpected error in tool '{name}': {str(e)}" logger.error(error_msg) return [types.TextContent(type="text", text=error_msg)] async def handle_generate_image(arguments: dict[str, Any]) -> list[types.TextContent | types.ImageContent]: """Handle image generation request""" try: prompt = arguments.get("prompt", "") size = arguments.get("size", os.getenv("DEFAULT_IMAGE_SIZE", "1024x1024")) output_path = arguments.get("output_path") logger.info(f"Image generation request: prompt='{prompt}', size={size}, output_path={output_path}") # Get Azure configuration try: azure_config = get_azure_config() except (ValueError, Exception) as e: error_msg = f"Azure configuration error: {str(e)}" logger.error(f"Failed to get Azure configuration: {error_msg}") return [types.TextContent(type="text", text=error_msg)] # Validate prompt is in English if not is_english_text(prompt): error_msg = "Prompt must be in English. Please use English to describe the image you want to generate." logger.warning(f"Non-English prompt rejected: '{prompt}'") return [types.TextContent(type="text", text=error_msg)] # Validate image size if not validate_image_size(size): error_msg = f"Unsupported image size: {size}. Supported sizes: 1024x1024, 1792x1024, 1024x1792" logger.warning(f"Invalid image size: {size}") return [types.TextContent(type="text", text=error_msg)] async with AzureImageGenerator( base_url=azure_config["base_url"], api_key=azure_config["api_key"], deployment_name=azure_config["deployment_name"], model=azure_config["model"], api_version=azure_config["api_version"] ) as generator: result = await generator.generate_image( prompt=prompt, size=size, output_path=output_path ) if output_path: logger.info(f"Image saved to file: {result}") return [types.TextContent(type="text", text=f"Image successfully generated and saved to: {result}")] else: image_b64 = base64.b64encode(result).decode('utf-8') logger.info(f"Image generation successful, returning base64 data (size: {len(result)} bytes)") return [ types.TextContent(type="text", text=f"Image generation successful, prompt: '{prompt}', size: {size}"), types.ImageContent(type="image", data=image_b64, mimeType="image/png") ] except Exception as e: error_msg = f"Error generating image: {str(e)}" logger.error(f"Image generation failed: {error_msg}") return [types.TextContent(type="text", text=error_msg)] async def handle_edit_image(arguments: dict[str, Any]) -> list[types.TextContent | types.ImageContent]: """Handle image editing request""" try: image_path = arguments.get("image_path") prompt = arguments.get("prompt", "") + " (and all other elements exactly the same)" size = arguments.get("size") # Optional size override output_path = arguments.get("output_path") # Validate input parameters if not image_path: error_msg = "image_path parameter is required" logger.error(error_msg) return [types.TextContent(type="text", text=error_msg)] logger.info(f"Image editing request: image_path='{image_path}', prompt='{prompt}', output_path={output_path}") # Get Azure configuration try: azure_config = get_azure_config() except (ValueError, Exception) as e: error_msg = f"Azure configuration error: {str(e)}" logger.error(f"Failed to get Azure configuration: {error_msg}") return [types.TextContent(type="text", text=error_msg)] # Validate prompt is in English if not is_english_text(prompt): error_msg = "Prompt must be in English. Please use English to describe how you want to edit the image." logger.warning(f"Non-English prompt rejected: '{prompt}'") return [types.TextContent(type="text", text=error_msg)] # Check if input file exists if not os.path.exists(image_path): error_msg = f"Error: Image file not found {image_path}" logger.error(f"Input file does not exist: {image_path}") return [types.TextContent(type="text", text=error_msg)] async with AzureImageGenerator( base_url=azure_config["base_url"], api_key=azure_config["api_key"], deployment_name=azure_config["deployment_name"], model=azure_config["model"], api_version=azure_config["api_version"] ) as generator: result = await generator.edit_image( image_path=image_path, prompt=prompt, size=size, output_path=output_path ) if output_path: logger.info(f"Edited image saved to file: {result}") return [types.TextContent(type="text", text=f"Image successfully edited and saved to: {result}")] else: image_b64 = base64.b64encode(result).decode('utf-8') logger.info(f"Image editing successful, returning base64 data (size: {len(result)} bytes)") return [ types.TextContent(type="text", text=f"Image editing successful, edit prompt: '{prompt}', source file: {image_path}"), types.ImageContent(type="image", data=image_b64, mimeType="image/png") ] except Exception as e: error_msg = f"Error editing image: {str(e)}" logger.error(f"Image editing failed: {error_msg}") return [types.TextContent(type="text", text=error_msg)] async def main(): """Main server function""" # Verify Azure configuration before starting server try: logger.info("🔍 Verifying Azure configuration...") azure_config = get_azure_config() logger.info("✅ Azure configuration verification successful") logger.info(f" - Service URL: {azure_config['base_url']}") logger.info(f" - Deployment name: {azure_config['deployment_name']}") logger.info(f" - Model: {azure_config['model']}") logger.info(f" - API key: {'*' * (len(azure_config['api_key']) - 4) + azure_config['api_key'][-4:]}") except (ValueError, Exception) as e: logger.error(f"❌ Azure configuration verification failed: {str(e)}") logger.error("Please set the following required environment variables:") logger.error(" - AZURE_BASE_URL: Base URL for Azure AI service") logger.error(" - AZURE_API_KEY: API key for Azure AI service") logger.error(" - AZURE_DEPLOYMENT_NAME: Deployment model name") logger.error("Optional environment variables:") logger.error(" - AZURE_MODEL: Model name (defaults to flux.1-kontext-pro)") logger.error(" - AZURE_API_VERSION: API version (defaults to 2025-04-01-preview)") print("\n💡 Tip: Set these environment variables in a .env file", file=sys.stderr) print("Example:", file=sys.stderr) print(" export AZURE_BASE_URL='https://your-service.services.ai.azure.com'", file=sys.stderr) print(" export AZURE_API_KEY='your-api-key'", file=sys.stderr) print(" export AZURE_DEPLOYMENT_NAME='your-deployment-name'", file=sys.stderr) print(" export AZURE_MODEL='flux.1-kontext-pro'", file=sys.stderr) print(" export AZURE_API_VERSION='2025-04-01-preview'", file=sys.stderr) sys.exit(1) logger.info("🚀 Starting Azure Image Editor MCP STDIO Server") logger.info(f"📊 Default image size: {os.getenv('DEFAULT_IMAGE_SIZE', '1024x1024')}") # Server capabilities try: logger.info("Getting server capabilities...") server_capabilities = server.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ) logger.info("Server capabilities retrieved successfully") except Exception: logger.exception("Failed to compute server capabilities") raise try: logger.info("Creating STDIO server...") async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): logger.info("Running MCP server...") await server.run( read_stream, write_stream, InitializationOptions( server_name="azure-image-editor", server_version="1.0.0", capabilities=server_capabilities, ), raise_exceptions=True, ) except Exception: logger.exception("Error in server setup") raise if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: logger.info("Server stopped by user") except Exception as e: logger.error(f"Server error: {e}") sys.exit(1)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/satomic/Azure-AI-Image-Editor-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server