Set Up and Deploy Image Generation MCP in Claude Desktop

Relia Software

Learn to create an image generation MCP (powered by Google’s Gemini model) which generates images based on user query and is used directly in Claude Desktop.

How to Setup & Deploy Image Generation MCP in Claude Desktop?

Right now, tools like Claude and other AI models are very good at understanding what users want through conversation. However, they don’t work smoothly with image generation tools. 

Claude is unable to directly generate images.
Claude is unable to directly generate images.

If a user wants to create an image with AI, they often have to switch to a different tool just for that. This switch can cause the chat to lose track of the conversation, and the user has to explain everything again in a new prompt. Even then, it can be hard to get the image just right without trying several times.

Our goal is to fix this problem by building one simple tool that keeps the conversation going, whether the user is asking for text or images, without losing context or making them repeat themselves.

In this tutorial, the expected output is an Model Context Protocol (MCP) which can generate the images based on the user query and be used directly in Claude, Cursor, or other AI tools which integrate MCP. The core image generation feature is powered by Google’s Gemini model, which offers strong vision capabilities even on its free plan.

Prerequisites

In order to set up and use MCP, you must already have installed the following apps in your machine.

Set Up a Development Environment

  • Open your favorite IDE. In my case, I open Cursor. Then, create a new project by clicking on the button “Open project”.
create a new project by clicking on the button “Open project"
  • Create a new directory and continue to click on the “Open” button.
Create a new directory on Cursor
  • Now create the file .env to store your API key which is crucial for the task of generating images in some next steps. 

In terms of the GOOGLE_API_KEY, you can sign up an account and get your own API key in this URL https://aistudio.google.com/apikey Gemini offers a free plan, making it easy to experiment with image generation without hitting usage limits too quickly.

get your own API key
# .env
GOOGLE_API_KEY="your_google_genai_api_key" 
MAX_IMAGE_GENERATIONS=5 # default: generate at max 5 image per call

  • Create a file environment.yml to set the necessary dependencies for this project.
set the necessary dependencies
# environment.yml
name: image-generator-mcp
channels:
  - conda-forge
  - defaults
dependencies:
  - python>=3.10
  - pip
  - pip:
    - fastmcp>=0.2.0
    - google-generativeai>=0.8.0
    - pillow>=10.0.0
    - requests>=2.31.0
    - uvicorn>=0.24.0
    - mcp>=1.9.0,<2.0.0
    - pytest>=7.4.0
    - black>=23.0.0
    - flake8>=6.0.0

  • Create a Conda environment and install the dependencies

Navigate to the top menu of Terminal, go to select Terminal and choose New Terminal.

select Terminal and choose New Terminal

Run the following code in the terminal to create a new Conda environment for this project.

conda env create -f environment.yml

create a new Conda environment

Wait a second to see that it has successfully set up an environment. In my case, a new environment named image-generator-mcp has been installed.

a new environment named image-generator-mcp has been installed

Following this instruction, run the code below to activate the environment.

conda activate image-generator-mcp

Create Response Format for the MCP Tool

Create a file utils.py which contains the functions formatting our response to the MCP-friendly format.

from typing import Dict, Any

def format_error_response(error: Exception) -> Dict[str, Any]:
    """Format error response for MCP tools."""
    return {
        "error": True,
        "message": str(error),
        "type": type(error).__name__
    }

def format_success_response(data: Any, message: str = "Success") -> Dict[str, Any]:
    """Format success response for MCP tools."""
    return {
        "error": False,
        "message": message,
        "data": data
    }

Create a file utils.py

Create A Primary Utility File for LLM Functions

Now let’s create another file named llm_utils.py

For better managing the code, my style is to separate all code into different files with their respective features. This approach quickly saves my life when debugging or handling issues during deployment or production. It's much easier to review specific blocks of code.

After that, import some necessary packages for our target function of generating images with Gemini.

"""
LLM utilities for Gemini AI integration.
"""

import base64
import binascii
from typing import Dict, Any, List
from google import genai
from google.genai import types
from PIL import Image
from io import BytesIO
import hashlib
from datetime import datetime
from pathlib import Path

necessary packages for our target function of generating images

Create the main class of Gemini Generator. In this block, we have set up a class which can be initialized by using an input constructor of an API key. Then, using this API key, the class also initializes the GenAI client with genai.Client().

class GeminiImageGenerator:
    """Handles image generation using Google's Gemini AI."""
    
    def __init__(self, api_key: str):
        """Initialize the Gemini client."""        
        self.api_key = api_key
        self.client = genai.Client(api_key=self.api_key)

Create the main class of Gemini Generator.

Create an internal function inside this class which helps us generate images using the input prompt. 

As illustrated in the following code, a new function generate_image_with_imagen has been created with a required input parameter prompt which is what the Agent will ask the MCP to generate (e.g. ‘create a picture of a beautiful singing girl’). But I have combined a little more specific guidance for the given prompt which always helps the model generate correctly and consistent the same style for all images. 

You can find it free to modify the variable enhanced_prompt to fit your need and style.

For example, this is the Artistic & Creative Style which is usually preferred for marketing materials or social media content.

enhanced_prompt = f"""Create an artistic and imaginative interpretation of: {prompt}

Style it with creative flair, bold colors, and artistic composition. Think of it as a piece of art with dramatic lighting, interesting textures, and expressive details that evoke emotion and creativity."""

This is another more professional example prompt. This style is preferred for business presentations or professional portfolio, etc.

enhanced_prompt = f"""Generate a clean, professional image depicting: {prompt}

Ensure it has a business-appropriate aesthetic with clear details, neutral lighting, and a polished finish. Focus on accuracy, clarity, and a corporate-friendly appearance suitable for presentations or documentation."""

In my case, I only set up the most general prompt style which tells MCP to generate the most well-composed images.

enhanced_prompt = f"""Create a detailed, high-quality image based on this description: {prompt}
Please make the image visually appealing, well-composed, and professionally rendered."""

This prompt will be used with GenAI by using self.client.models.generate_content(). Here, we are going to use the most popular multi-modal model of Gemini: gemini-2.0-flash-preview-image-generation. This both creates text and images.

After making an API call, the generated text and image are obtained from extracting response.candidates[0].content.parts

Remember to put the code inside a try/except code block which prevents the application from collapsing by a minor bug.

class GeminiImageGenerator:
    """Handles image generation using Google's Gemini AI."""
    
    def __init__(self, api_key: str):
        # remaining code ...
    
    def generate_image_with_imagen(self, prompt: str, **kwargs) -> Dict[str, Any]:
        """
        Generate an image using Gemini's Imagen model.
        """
        try:
            # Enhanced prompt for better image generation
            enhanced_prompt = f"""Create a detailed, high-quality image based on this description: {prompt}
            
            Please make the image visually appealing, well-composed, and professionally rendered."""
            
            response = self.client.models.generate_content(
                model="gemini-2.0-flash-preview-image-generation",
                contents=enhanced_prompt,
                config=types.GenerateContentConfig(
                    response_modalities=['TEXT', 'IMAGE']
                )
            )
            
            # Extract image from response
            image_data = None
            generated_text = ""


            for part in response.candidates[0].content.parts:
                if part.text is not None:
                    generated_text = part.text
                elif part.inline_data is not None:
                    image_data = part.inline_data.data
       except Exception as e:
            return format_error_response(e)

put the code inside a try/except code block

Now you successfully make the call to Gemini service and get the generated text and image. However, this is only a step in the whole workflow.

To show the image, we can store it to a temporary file and load it to the user window.

First, check if the client really generated a valid image.

if not image_data:
    # If no image data, return the text response as a successful result
    return format_success_response({
        "prompt": prompt,
        "enhanced_prompt": enhanced_prompt,
        "generated_text": generated_text,
        "note": "Gemini responded with text content only - no image data available",
        "response_type": "text_only"
    }, "Content generation completed - text response received")

Second, to save the image to a temporary file, we need to get a unique temporary name. In order to do this, we take this approach.

  • Encode the prompt to bytes.
  • Use a hash function to turn the prompt bytes into a unique hash string.
  • Turn the hash string into a unique hexadecimal string. 
  • Due to the length, we take only the first 12 characters of the generated string and add to the file name besides the prefix and datetime.
# Generate unique filename based on prompt hash
prompt_hash = hashlib.md5(prompt.encode()).hexdigest()[:12]
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
img_prefix = f"image_"
image_filename = f"{img_prefix}{timestamp}_{prompt_hash}.png"

Now having a unique image name, we should continue to check for the image type and load them by using PIL.

To ensure the valid image data bytes type, we use the package base64 to load the image string if the client returns a string.

# Ensure image_data is valid base64 encoded data
if isinstance(image_data, str):
    image_bytes = base64.b64decode(image_data)
elif isinstance(image_data, bytes):
    image_bytes = image_data
else:
    raise ValueError(f"Invalid image data type: {type(image_data)}")

Now load the image from generated bytes of the client and return the exception if any error occurs.

# Validate the image data can be opened by PIL
image_buffer = BytesIO(image_bytes)
try:
    image = Image.open(image_buffer)
    # Verify the image is valid by checking format
    image.verify()
    # Re-open for actual use (verify() closes the file)
    image_buffer.seek(0)
    image = Image.open(image_buffer)
except Exception as pil_error:
    # If PIL can't identify the image, it might not be image data
    # Return a success response without trying to display the image
                            return format_success_response({
            "prompt": prompt,
            "enhanced_prompt": enhanced_prompt,
            "generated_text": generated_text,
            "note": "Image data received but could not be processed as a valid image",
            "data_type": type(image_data).__name__,
            "data_size": len(image_bytes) if image_bytes else 0,
            "error_details": str(pil_error),
            "raw_data_sample": str(image_bytes[:100]) if image_bytes else "No data"
        }, "Content generation completed - text response available")

Save the loaded image to the temporary path.

# Save image temporarily
temp_path = Path(f"/tmp/{image_filename}")
image.save(temp_path, "PNG")

Show the image on the user window.

# Load the generated image and display in a preview window
loaded_image = Image.open(temp_path)
loaded_image.show()  # Opens the image in the default image viewer

Now you have successfully completed the task of generating and displaying the image. Then the response must be nicely formatted and returned to the Agent for it to understand that the API was successful.

return format_success_response({
    "prompt": prompt,
    "enhanced_prompt": enhanced_prompt,
    "generated_text": generated_text,
    "image_data_base64": base64.b64encode(image_bytes).decode('utf-8'),
    "image_path": str(temp_path),
    "image_dimensions": f"{image.width}x{image.height}",
    "image_format": image.format,
    "image_size_bytes": len(image_bytes),
    "loaded_image": "Image opened in preview window"
}, "Image generation request processed and displayed")

Now I will help you continue to create another file in this project.

Create the Main MCP Server Application

Create a new file named app.py and import some necessary packages.

"""
Main MCP server application with Gemini image generation capabilities.
"""


# import asyncio
import os
import logging
from typing import Any, Dict, List, Optional
from fastmcp import FastMCP
from pydantic import BaseModel
# Load environment variables
from dotenv import load_dotenv
load_dotenv()


from utils import (
    format_error_response,
    format_success_response,
)
from llm_utils import GeminiImageGenerator

Create a new file named app.py

Set up the configuration of your Logging, the image generator counter, and load the Google API key from the environment variables.

# Configure logging
logging.basicConfig(level=logging.INFO) 
logger = logging.getLogger(__name__)
# Load environment variables
GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
# Configuration constants
MAX_IMAGE_GENERATIONS = int(os.getenv('MAX_IMAGE_GENERATIONS', 3))
# Global counter for image generation limit
_image_generation_count = 0

There’s a powerful tool that makes building MCP (Model Context Protocol) apps much faster and easier: FastMCP. 

Instead of writing a lot of complex code, FastMCP lets you use simple decorators like @mcp.tool and @mcp.resource to create tools and connect them to AI models. It handles the tricky parts for you - like managing data formats, connections, and security, so you can focus on what your app should do, not how it works behind the scenes.

FastMCP is perfect for quickly building smart, AI-powered MCPs. Let’s start by importing FastMCP and setting it up:

# Initialize MCP server
mcp = FastMCP("Gemini Image Generator")

# Global generator instance
image_generator: Optional[GeminiImageGenerator] = None

To make sure that the inputs sent from the Agent to the MCP are always correct and well-structured, we can use a Pydantic model. Pydantic helps validate the data automatically and ensures that it matches the expected format. For this MCP, we can define a model like this:

# Pydantic models for tool inputs
class GenerateImageInput(BaseModel):
    prompt: str
    style: Optional[str] = None
    quality: Optional[str] = "standard"

Now we’re going to set up an MCP tool using FastMCP. You can do this by simply adding the @mcp.tool() decorator above a function. FastMCP will automatically recognize that this function should be exposed as an MCP tool.

In this example, we’ll create a function called generate_image, which takes an input of type GenerateImageInput (a Pydantic model we defined earlier). This function will process the input and return a dictionary with the result.

Here’s how it looks:

@mcp.tool()
def generate_image(input: GenerateImageInput) -> Dict[str, Any]:
    # function logic here

In this function, define the first blocks of code to check if the number of generated images has reached the allowed limit (MAX_IMAGE_GENERATIONS). If the limit is exceeded, it returns a formatted error response, preventing further image generation until the server is restarted. This helps manage resource usage and avoids overloading the image generation system.

@mcp.tool()
def generate_image(input: GenerateImageInput) -> Dict[str, Any]:
    """
    Generate an image using Gemini AI based on a text prompt.
    
    Args:
        prompt: The text description of the image to generate
        style: Optional style modifier (e.g., "photorealistic", "artistic", "cartoon")
        quality: Image quality setting ("standard", "high")
    
    Returns:
        Dictionary containing image generation result or error information
    """
    global image_generator, _image_generation_count
    
    # Check image generation limit
    if _image_generation_count >= MAX_IMAGE_GENERATIONS:
        return format_error_response(ValueError(
            f"Image generation limit reached. Maximum {MAX_IMAGE_GENERATIONS} images allowed per session. "
            f"Restart the server to reset the counter."
        ))

  • After checking the image generation limit, the function verifies whether a valid GOOGLE_API_KEY is configured. This key is required to use the Gemini AI image generation service. If the key is missing, the function immediately returns a formatted error response, preventing further execution.
  • If the API key is present, it proceeds to initialize the GeminiImageGenerator instance using the provided key. This setup ensures that the image generator is only created when needed and that the service won’t run without proper authentication.
@mcp.tool()
def generate_image(input: GenerateImageInput) -> Dict[str, Any]:
    # ... 
   if not GOOGLE_API_KEY:
        return format_error_response(ValueError("Google API key not configured"))
    image_generator = GeminiImageGenerator(GOOGLE_API_KEY)

  • After initializing the Image Generator Engine, let’s get the prompt from Agent input. Moreover, if the Agent tells the specific image style, we could combine the style into a unified prompt.
  • After initializing the Image Generator Engine, let’s get the prompt from Agent input. Moreover, if the Agent tells the specific image style, we could combine the style into a unified prompt.
@mcp.tool()
def generate_image(input: GenerateImageInput) -> Dict[str, Any]:
    ...
    # Get the prompt
    enhanced_prompt = input.prompt
    # Enhance prompt with style if provided
    if input.style:
        enhanced_prompt = f"{input.prompt}, in {input.style} style"

  • Now we will step into the most important core function. This will be the call to the function generate_image_with_imagen which is responsible for generating for us a successful image based on our request.
  • The returned result is assigned to a variable ‘result’.
@mcp.tool()
def generate_image(input: GenerateImageInput) -> Dict[str, Any]:
    ...
    # Generate image
    result = image_generator.generate_image_with_imagen(
        enhanced_prompt,
        quality=input.quality
    )

  • After that, to check if this processing was successful, we get the boolean value from the key ‘error’ from the return result dictionary. If the result returned the pair error: False, this would mean the core generation function had successfully generated the images.
  • Update the generation counter to prevent the Agent from abusing this MCP by consistently generating unlimited images for the session.
@mcp.tool()
def generate_image(input: GenerateImageInput) -> Dict[str, Any]:
    ...
    # Increment counter only if generation was successful
    if result.get('error', True) == False:
        _image_generation_count += 1
        # Add generation count info to the result
        if 'result' in result and isinstance(result['result'], dict):
            result['result']['generation_count'] = _image_generation_count
            result['result']['remaining_generations'] = MAX_IMAGE_GENERATIONS - _image_generation_count
        logger.info(f"Image generated successfully. Count: {_image_generation_count}/{MAX_IMAGE_GENERATIONS}")
    return result

  • You have successfully set up your core MCP server file. There is one more suggestion: you should put the code inside a try/except block for better error handling and preventing the server from collapsing by any bug.
  • Finally, to make this file truly become a core MCP server, we have to run this MCP server as a local server host. I would go for the default transport method of the MCP functionality. You can find out more about it here: https://modelcontextprotocol.io/docs/concepts/transports#python-client 
if __name__ == "__main__":
    mcp.run(
        transport="stdio"
    )

The following is the full version of the code.

@mcp.tool()
def generate_image(input: GenerateImageInput) -> Dict[str, Any]:
    """
    Generate an image using Gemini AI based on a text prompt.
    
    Args:
        prompt: The text description of the image to generate
        style: Optional style modifier (e.g., "photorealistic", "artistic", "cartoon")
        quality: Image quality setting ("standard", "high")
    
    Returns:
        Dictionary containing image generation result or error information
    """
    try:
        global image_generator, _image_generation_count
        
        # Check image generation limit
        if _image_generation_count >= MAX_IMAGE_GENERATIONS:
            return format_error_response(ValueError(
                f"Image generation limit reached. Maximum {MAX_IMAGE_GENERATIONS} images allowed per session. "
                f"Restart the server to reset the counter."
            ))
        
        if not GOOGLE_API_KEY:
            return format_error_response(ValueError("Google API key not configured"))
        image_generator = GeminiImageGenerator(GOOGLE_API_KEY)
        
        # Get the prompt
        enhanced_prompt = input.prompt
        # Enhance prompt with style if provided
        if input.style:
            enhanced_prompt = f"{input.prompt}, in {input.style} style"
        
        # Generate image
        result = image_generator.generate_image_with_imagen(
            enhanced_prompt,
            quality=input.quality
        )

        # Increment counter only if generation was successful
        if result.get('error', True) == False:
            _image_generation_count += 1
            # Add generation count info to the result
            if 'result' in result and isinstance(result['result'], dict):
                result['result']['generation_count'] = _image_generation_count
                result['result']['remaining_generations'] = MAX_IMAGE_GENERATIONS - _image_generation_count
            
            logger.info(f"Image generated successfully. Count: {_image_generation_count}/{MAX_IMAGE_GENERATIONS}")
        return result
    
    except Exception as e:
        logger.error(f"Error generating image: {str(e)}")
        return format_error_response(e)
if __name__ == "__main__":
    mcp.run(
        transport="stdio"
    )
  • Now we move to the next step which is integration with Claude.

Claude Setup

To continue, please check if your device has a working version of Claude Desktop. You can try opening the application and ask for some general information such as the weather.

ask for some general information about the weather.

Then you know that your Claude application is working properly.

To check or edit the MCP configuration:

  • On macOS: Go to the Claude menu in the top bar → Click Settings.
  • On Windows: Click the gear icon in the top-right corner of the Claude app window → Then select Settings from the dropdown menu.
Claude settings in MacOS

Once you're in Settings, switch to the Developer tab and click Edit Config to configure your MCP.

switch to the Developer tab

This button opens a new window where Claude is located, the file which contains all the configuration information shows up with highlights as well.

a new window where Claude is located

Now you should click to open this file. You can open this file with any of your favorite editor applications, e.g. Visual Studio Code, Cursor, Text Editor, Notepad. In my case, I will open it with Cursor.

>> Explore more:

 open this file with Cursor


After opening the file, you will see the following content in the JSON file. There is not any content yet in this file. 

content in the JSON file

Basically, you have to set up your Claude config content like the one below.

{
            "mcpServers": {
                "gemini-image-generator": {
                    "command": "python",
                    "args": ["app.py"],
                    "env": {
                        "GOOGLE_API_KEY": "Your_API_Key"
                    }
                }
            }
        }

set up your Claude config content
  • However, Claude config must be available to work dependently, so Claude does not understand which Python version or which file named app.py should be used to run the MCP server. Therefore, please clearly specify the Python package and the file path.
  • Open the terminal window, then activate your created Conda environment.
  • Run the command which python to search for the location of your current Python engine.
Run the command which python

Copy this Python path and replace the current command in your Claude configuration JSON file.  

eplace the current command in your Claude configuration JSON file

Copy the absolute path that leads to your main file app.py and replace this path in the configuration file.

path that leads to your main file app.py

Also remember to replace your Gemini API Key in the file. Save it and you will have the MCP ready for your Claude.

replace your Gemini API Key in the file

Now quit or force close your Claude Desktop application. When you reopen the Claude, the MCP will be usable.

Use Image Generation MCP in Claude

Open your Claude Desktop application, you will see in the Menu option. There is a new Menu named ‘gemini-image-generator’ which is exactly what you have done and have defined so far in the previous sections.

gemini-image-generator menu

When you click on this menu, there will be 1 tool shown up. It is named ‘generate_image’. In the previous section, we have only defined 1 MCP tool.

MCP tool

If you can see the MCP tool, you have done everything correctly.

Now try asking Claude to generate an image that matches your preferences. In my case, I would ask this question:

    

help me create a picture with style Sketchbook about a dragon flying in front of a tech company named Relia

After that, just wait for the miracle.

If Claude needs to run an external tool, or an MCP, it will ask for your permission to run. You can choose Allow once or Allow always.

permission to run the MCP tool

Then wait a little bit. When it completes the MCP call, you will see the result image shown up.

The generated image on Claude via an MCP
The generated image on Claude via an MCP.

Besides generating the image, Claude also shares and describes the generated image.

You can click on the tool call inside Claude to view the exact command that the Agent sent to the MCP tool. This lets you verify whether Claude correctly understood your request.

click on the tool call inside Claude to view the exact command

Now congratulations for your successful MCP implementation. You can try to ask multiple questions and try different styles, to see which suits you. I hope you will enjoy this application.

What’s Next?

If you enjoy this application, I think you should play around with it a little bit more to really understand the project structure, how the Agent and MCP work together.

Here are some of my suggestions: 

  • Try OpenAI’s MCP to make a comparison with FastMCP: https://platform.openai.com/docs/mcp 
  • Try to make a comparison between traditional LLM tool calling and this MCP concept.
  • Try to implement MCP servers for other purposes: retail sales, marketing campaign analysis, Deep Research, etc.

>>> Follow and Contact Relia Software for more information!

  • coding
  • development
  • automation