Skip to main content
Glama

PsMCP - MCP Server for Photoshop

psMCP.py30.2 kB
import win32com.client import os from mcp.server.fastmcp import FastMCP import os from dotenv import load_dotenv load_dotenv() mcp = FastMCP("Photoshop-MCP-Advanced") # Global Photoshop application and document references psApp = None doc = None # Directory variables PSD_DIRECTORY = os.getenv("PSD_DIRECTORY") EXPORT_DIRECTORY = os.getenv("EXPORT_DIRECTORY") ASSETS_DIR = os.getenv("ASSETS_DIR") @mcp.tool() def list_available_psds() -> list: """ Lists all PSD files in the designated PSD_DIRECTORY. Returns: list: Filenames of all PSD files found. """ if not os.path.exists(PSD_DIRECTORY): return ["PSD directory not found."] psd_files = [f for f in os.listdir(PSD_DIRECTORY) if f.lower().endswith(".psd")] return psd_files if psd_files else ["No PSD files found."] @mcp.tool() def open_photoshop(open: bool) -> str: """ Open Photoshop application if not already opened. args: open (bool): True to open Photoshop Returns: str: Status message indicating whether Photoshop was opened or not. """ global psApp if open: psApp = win32com.client.Dispatch("Photoshop.Application") return "Opened" return "Not Opened" @mcp.tool() def open_psd_file(filename: str) -> str: """ Open a PSD file from the PSD_DIRECTORY in Photoshop. Args: filename (str): Name of the PSD file (with or without .psd extension) Returns: str: Success or error message """ global doc if not psApp: return "Photoshop not initialized" if not filename.lower().endswith('.psd'): filename += '.psd' psd_path = os.path.join(PSD_DIRECTORY, filename) if not os.path.isfile(psd_path): return f"File not found: {psd_path}" doc = psApp.Open(psd_path) return f"Opened PSD: {psd_path}" @mcp.tool() def edit_text_layer(layer_name: str, new_text: str) -> str: """ Edit the contents of a text layer. args: layer_name (str): Name of the text layer to edit new_text (str): New text content for the layer Returns: str: Success or error message """ if not doc: return "No document loaded" layer = doc.ArtLayers[layer_name] layer.TextItem.contents = new_text return f"Text in '{layer_name}' updated." @mcp.tool() def set_text_layer_size(layer_name: str, size: float) -> str: """ Change the font size of a text layer. args: layer_name (str): Name of the text layer to edit size (float): New font size for the layer Returns: str: Success or error message """ if not doc: return "No document loaded" layer = doc.ArtLayers[layer_name] layer.TextItem.size = size return f"Font size of '{layer_name}' set to {size}." @mcp.tool() def set_layer_visibility(layer_name: str, visible: bool) -> str: """ Show or hide a layer by name. agrs: layer_name (str): Name of the layer to show/hide visible (bool): True to show the layer, False to hide it Returns: str: Success or error message """ doc = get_active_document() if not doc: return "No document loaded" layer = doc.ArtLayers[layer_name] layer.visible = visible return f"Layer '{layer_name}' visibility set to {visible}." @mcp.tool() def export_as_png(filename: str) -> str: """ Export the active document as a PNG to the EXPORT_DIRECTORY. Args: filename (str): Name of the PNG file (without extension) Returns: str: Success or error message """ if not doc: return "No document loaded" full_path = os.path.join(EXPORT_DIRECTORY, f"{filename}.png") options = win32com.client.Dispatch('Photoshop.ExportOptionsSaveForWeb') options.Format = 13 # PNG options.PNG8 = False doc.Export(ExportIn=full_path, ExportAs=2, Options=options) return f"Exported PNG to {full_path}" @mcp.tool() def export_as_jpg(filename: str, quality: int = 100) -> str: """ Export the active document as a JPG to the EXPORT_DIRECTORY. Args: filename (str): Name of the JPG file (without extension) quality (int): JPEG quality (0–100) Returns: str: Success or error message """ if not doc: return "No document loaded" full_path = os.path.join(EXPORT_DIRECTORY, f"{filename}.jpg") options = win32com.client.Dispatch('Photoshop.ExportOptionsSaveForWeb') options.Format = 6 # JPEG options.Quality = quality doc.Export(ExportIn=full_path, ExportAs=2, Options=options) return f"Exported JPG to {full_path} with quality {quality}" def get_active_document(): try: return psApp.Application.ActiveDocument except Exception: return None @mcp.tool() def list_layers() -> list: """ List all layer names in the active Photoshop document. Returns: list: List of layer names. """ doc = get_active_document() if not doc: return ["No active document found."] return ["- "+layer.Name for layer in doc.ArtLayers] @mcp.tool() def rename_layer(old_name: str, new_name: str) -> str: """ Rename a Photoshop layer. Args: old_name (str): Current name of the layer. new_name (str): New name for the layer. Returns: str: Confirmation message. """ doc = get_active_document() if not doc: return ["No active document found."] layer = doc.ArtLayers[old_name] layer.Name = new_name return f"Layer '{old_name}' renamed to '{new_name}'" @mcp.tool() def delete_layer(layer_name: str) -> str: """ Delete a Photoshop layer by name. Args: layer_name (str): Name of the layer to delete. Returns: str: Confirmation message. """ doc = get_active_document() if not doc: return ["No active document found."] layer = doc.ArtLayers[layer_name] layer.Delete() return f"Layer '{layer_name}' deleted." @mcp.tool() def duplicate_layer(layer_name: str, new_name: str = None) -> str: """ Duplicate a Photoshop layer. Args: layer_name (str): Name of the layer to duplicate. new_name (str, optional): New name for the duplicated layer. Returns: str: Confirmation message. """ doc = get_active_document() if not doc: return ["No active document found."] original = doc.ArtLayers[layer_name] dup = original.Duplicate() if new_name: dup.Name = new_name return f"Layer '{layer_name}' duplicated as '{dup.Name}'." @mcp.tool() def change_layer_opacity(layer_name: str, opacity: float) -> str: """ Change the opacity of a layer. Args: layer_name (str): Name of the layer. opacity (float): Opacity value (0 to 100). Returns: str: Confirmation message. """ doc = get_active_document() if not doc: return ["No active document found."] layer = doc.ArtLayers[layer_name] layer.Opacity = opacity return f"Layer '{layer_name}' opacity set to {opacity}%." @mcp.tool() def set_text_position(layer_name: str, x: float, y: float) -> str: """ Set the position of a text layer. args: layer_name (str): Name of the text layer x (float): X coordinate for the text position y (float): Y coordinate for the text position Returns: str: Success or error message """ doc = get_active_document() if not doc: return ["No active document found."] layer = doc.ArtLayers[layer_name] if layer.Kind == 2: layer.TextItem.Position = [x, y] return f"Position set to ({x}, {y})" return "Layer is not a text layer" @mcp.tool() def apply_gaussian_blur_to_layer(layer_name: str, radius: float = 5.0) -> str: """ Apply Gaussian blur to the specified layer. Args: layer_name (str): Name of the layer to apply the blur to. radius (float): Radius of the Gaussian blur. Returns: str: Confirmation message. """ doc = get_active_document() if not doc: return ["No active document found."] try: layer = doc.ArtLayers[layer_name] layer.ApplyGaussianBlur(radius) return f"Applied Gaussian blur to '{layer_name}' with radius {radius}" except Exception as e: return f"Error: {e}" @mcp.tool() def adjust_layer_brightness_contrast(layer_name: str, brightness: int, contrast: int) -> str: """ Adjust brightness and contrast of the specified layer. Brightness and contrast should be between -100 to 100. Args: layer_name (str): Name of the layer to adjust. brightness (int): Brightness adjustment value. contrast (int): Contrast adjustment value. Returns: str: Confirmation message. """ doc = get_active_document() if not doc: return ["No active document found."] try: layer = doc.ArtLayers[layer_name] layer.AdjustBrightnessContrast(brightness, contrast) return f"Adjusted brightness/contrast of '{layer_name}' by ({brightness}, {contrast})" except Exception as e: return f"Error: {e}" @mcp.tool() def adjust_layer_hue_saturation(layer_name: str, hue: int = 0, saturation: int = 0, lightness: int = 0) -> str: """ Adjust hue, saturation, and lightness of the specified layer. All values should range from -100 to 100. Args: layer_name (str): Name of the layer to adjust. hue (int): Hue adjustment value. saturation (int): Saturation adjustment value. lightness (int): Lightness adjustment value. Returns: str: Confirmation message. """ doc = get_active_document() if not doc: return ["No active document found."] try: layer = doc.ArtLayers[layer_name] layer.AdjustHueSaturation(hue, saturation, lightness) return f"Adjusted Hue/Saturation/Lightness of '{layer_name}' by ({hue}, {saturation}, {lightness})" except Exception as e: return f"Error: {e}" @mcp.tool() def get_active_layer_name() -> str: """ Returns the name of the currently active layer in the document. """ doc = get_active_document() if not doc: return ["No active document found."] try: return doc.ActiveLayer.Name except Exception: return "No active layer or document." @mcp.tool() def quit_photoshop() -> str: """ Quit Photoshop application. """ try: psApp.Quit() return "Photoshop quit successfully." except Exception as e: return f"Error quitting Photoshop: {e}" @mcp.tool() def create_new_psd(name: str = "Untitled", width: int = 1920, height: int = 1080, resolution: int = 72, mode: str = "RGB", background_color: str = "white") -> str: """ Create a new Photoshop document. Args: name (str): Name of the document. width (int): Width in pixels. height (int): Height in pixels. resolution (int): Resolution in pixels/inch. mode (str): Color mode - options are 'RGB', 'CMYK', 'Grayscale'. background_color (str): Background color - 'white', 'black', or 'transparent'. Returns: str: Confirmation message with document name. """ global doc if not psApp: return "Photoshop not initialized" # Map color mode strings to Photoshop constants mode_map = { "RGB": 2, # psRGB "CMYK": 4, # psCMYK "Grayscale": 1 # psGrayscale } bg_map = { "white": 1, # psWhite "black": 2, # psBlack "transparent": 3 # psTransparent } try: doc = psApp.Documents.Add( width, height, resolution, name, mode_map.get(mode.upper(), 2), bg_map.get(background_color.lower(), 1) ) return f"New PSD '{name}' created ({width}x{height}, {resolution}ppi, {mode})" except Exception as e: return f"Error creating PSD: {e}" @mcp.tool() def save_current_psd(filename: str) -> str: """ Save the active Photoshop document to the PSD directory. Args: filename (str): Name to save the PSD as (with or without '.psd'). Returns: str: Confirmation message. """ global doc, PSD_DIRECTORY if not doc: return "No active document to save." if not filename.lower().endswith(".psd"): filename += ".psd" save_path = os.path.join(PSD_DIRECTORY, filename) try: psd_options = win32com.client.Dispatch("Photoshop.PhotoshopSaveOptions") doc.SaveAs(save_path, psd_options, True) # asCopy=True to avoid overwrite prompt return f"Document saved as {save_path}" except Exception as e: return f"Failed to save document: {e}" FONT_SPECIFIER_NAME_ID = 1 FONT_SPECIFIER_FAMILY_ID = 16 def shortName(font): """Extract Windows Display Name from the font file""" name = "" family = "" for record in font['name'].names: try: name_str = record.string.decode('utf-16-be') if b'\x00' in record.string else record.string.decode('utf-8') if record.nameID == FONT_SPECIFIER_NAME_ID and not name: name = name_str elif record.nameID == FONT_SPECIFIER_FAMILY_ID and not family: family = name_str except Exception: continue if name and family: break return name, family def getPostScriptNameFromDisplayName(winName): """Find PostScript name from display name using Photoshop font list""" for font in psApp.fonts: if font.name == winName: return font.postScriptName return None @mcp.tool() def create_text_layer(layer_name: str, content: str, font: str = "Arial", size: int = 36) -> str: """ Create a new text layer with specified content, font, and size using PostScript font name. Args: layer_name (str): Name of the new text layer. content (str): Text content for the layer. font (str): Font name to use (default is "Arial"). size (int): Font size (default is 36). Returns: str: Confirmation message. """ doc = get_active_document() if not doc: return "No active document found." try: ps_name = getPostScriptNameFromDisplayName(font) if not ps_name: return f"Font '{font}' not found in Photoshop. Please check the font name." text_layer = doc.ArtLayers.Add() text_layer.Name = layer_name text_layer.Kind = 2 # Text layer text_item = text_layer.TextItem text_item.Contents = content text_item.Font = ps_name text_item.Size = size return f"Text layer '{layer_name}' with content '{content}' created using font '{font}'." except Exception as e: return f"Error: {e}" @mcp.tool() def toggle_text_style_by_name(layer_name: str, style: str = "bold") -> str: """ Toggle text style (bold, unbold, italic) for a specified text layer. Args: layer_name (str): The name of the text layer. style (str): "bold", "unbold", or "italic". Returns: str: Success or error message. """ doc = get_active_document() if not doc: return "No active document found." try: target_layer = None for layer in doc.Layers: if layer.Name == layer_name: target_layer = layer break if not target_layer: return f"Layer '{layer_name}' not found." if target_layer.Kind != 2: return f"Layer '{layer_name}' is not a text layer." text_item = target_layer.TextItem if style.lower() == "bold": text_item.FauxBold = True return f"Bold enabled on '{layer_name}'." elif style.lower() == "unbold": text_item.FauxBold = False return f"Bold disabled on '{layer_name}'." elif style.lower() == "italic": text_item.FauxItalic = not text_item.FauxItalic return f"Italic toggled to {text_item.FauxItalic} on '{layer_name}'." else: return f"Invalid style option: {style}" except Exception as e: return f"Error updating text style: {e}" @mcp.tool() def list_available_fonts() -> list[str]: """ List available font names that can be used in Photoshop (Windows only). """ try: fso = win32com.client.Dispatch("Scripting.FileSystemObject") shell = win32com.client.Dispatch("Shell.Application") fonts_folder = shell.Namespace(0x14) # Fonts directory fonts = set() for item in fonts_folder.Items(): font_name = item.Name if font_name: fonts.add(font_name.split(' (')[0]) # Clean names like "Arial (TrueType)" return sorted(list(fonts)) except Exception as e: return [f"Error fetching fonts: {e}"] @mcp.tool() def test_font_compatibility(font_name: str, text: str = "Test", size: int = 36) -> str: """ Test if a given font name is compatible with Photoshop scripting. Creates a temporary text layer and checks if the font is applied. Args: font_name (str): Name of the font to test. text (str): Sample text to apply (default is "Test"). size (int): Font size (default is 36). Returns: str: Confirmation message indicating compatibility. """ doc = get_active_document() if not doc: return "No active document found." try: # Create a temporary text layer temp_layer = doc.ArtLayers.Add() temp_layer.Kind = 2 # Text layer temp_layer.Name = "TempFontTest" text_item = temp_layer.TextItem text_item.Contents = text text_item.Font = font_name text_item.Size = size # Confirm the applied font applied_font = text_item.Font # Clean up temp_layer.Delete() if applied_font == font_name: return f"✅ Font '{font_name}' is compatible." else: return f"⚠️ Font '{font_name}' is not applied as expected. Photoshop used '{applied_font}' instead." except Exception as e: return f"❌ Error: {e}" @mcp.tool() def add_image_layer(image_path: str, layer_name: str = "Imported Image") -> str: """ Add an external image as a new layer to the active document and move it to the top. Args: image_path (str): Path to the image file to import. layer_name (str): Name for the new layer (default is "Imported Image"). Returns: str: Confirmation message. """ doc = get_active_document() if not doc: return "No active document found." if not os.path.exists(image_path): return f"Image file not found: {image_path}" try: # Capture existing layer names existing_layer_names = [layer.Name for layer in doc.ArtLayers] # Open external image imported_doc = psApp.Open(image_path) # Duplicate the layer into the active document (position doesn't matter yet) imported_doc.ArtLayers[0].Duplicate(doc) # Close the imported document imported_doc.Close(2) # 2 = Don't Save # Find the new layer new_layer = None for layer in doc.ArtLayers: if layer.Name not in existing_layer_names: new_layer = layer break if not new_layer: return "Layer duplicated, but new layer not found." # Rename the new layer new_layer.Name = layer_name # Move it to the top (before the first layer) first_layer = doc.ArtLayers.Item(0) if new_layer != first_layer: new_layer.Move(first_layer, 2) # 2 = before return f"Image '{image_path}' added as layer '{layer_name}' and moved to top." except Exception as e: return f"Failed to add image layer: {e}" @mcp.tool() def list_image_assets() -> list: """ List full paths of all image files in the global assets directory. Returns: list: Full file paths of image files (.png, .jpg, .jpeg, etc.). """ if not os.path.isdir(ASSETS_DIR): return [f"Assets directory does not exist: {ASSETS_DIR}"] supported_exts = (".png", ".jpg", ".jpeg", ".bmp", ".gif", ".tiff") image_paths = [] for filename in os.listdir(ASSETS_DIR): if filename.lower().endswith(supported_exts): full_path = os.path.join(ASSETS_DIR, filename) image_paths.append(full_path) if not image_paths: return ["No image files found in the assets directory."] return image_paths @mcp.tool() def set_layer_position(layer_name: str, x: float, y: float) -> str: """ Change the X and Y position of a specified layer on the canvas. Args: layer_name (str): The name of the layer to reposition. x (float): New X position. y (float): New Y position. Returns: str: Success or error message. """ doc = get_active_document() if not doc: return "No active document found." try: target_layer = None for layer in doc.ArtLayers: if layer.Name == layer_name: target_layer = layer break if not target_layer: return f"Layer '{layer_name}' not found." # Translate layer using current position bounds = target_layer.Bounds # [left, top, right, bottom] current_x = bounds[0].Value current_y = bounds[1].Value dx = x - current_x dy = y - current_y target_layer.Translate(dx, dy) return f"Moved layer '{layer_name}' to position ({x}, {y})." except Exception as e: return f"Failed to move layer '{layer_name}': {e}" @mcp.tool() def resize_layer(layer_name: str, scale_x: float, scale_y: float) -> str: """ Resize a specific layer by percentage. Args: layer_name (str): Name of the layer to resize. scale_x (float): Horizontal scale percentage (e.g., 150 for 150%). scale_y (float): Vertical scale percentage (e.g., 150 for 150%). Returns: str: Success or error message. """ doc = get_active_document() if not doc: return ["No active document found."] try: target_layer = None for layer in doc.ArtLayers: if layer.Name == layer_name: target_layer = layer break if not target_layer: return f"Layer '{layer_name}' not found." # Activate target layer doc.ActiveLayer = target_layer # Resize target_layer.Resize(scale_x, scale_y) return f"Resized layer '{layer_name}' to {scale_x}% width and {scale_y}% height." except Exception as e: return f"Error resizing layer: {e}" @mcp.tool() def move_layer(layer_name: str, offset_x: float, offset_y: float) -> str: """ Move a specific layer by an (x, y) offset. Args: layer_name (str): Name of the layer to move. offset_x (float): Offset in pixels (horizontal). offset_y (float): Offset in pixels (vertical). Returns: str: Success or error message. """ doc = get_active_document() if not doc: return ["No active document found."] try: layer = next(l for l in doc.ArtLayers if l.Name == layer_name) doc.ActiveLayer = layer layer.Translate(offset_x, offset_y) return f"Moved layer '{layer_name}' by ({offset_x}, {offset_y})" except StopIteration: return f"Layer '{layer_name}' not found." except Exception as e: return f"Error moving layer: {e}" @mcp.tool() def rotate_layer(layer_name: str, angle: float) -> str: """ Rotate a specific layer by a given angle (in degrees). Args: layer_name (str): Name of the layer to rotate. angle (float): Angle in degrees. Positive = clockwise. Returns: str: Success or error message. """ doc = get_active_document() if not doc: return ["No active document found."] try: layer = next(l for l in doc.ArtLayers if l.Name == layer_name) doc.ActiveLayer = layer layer.Rotate(angle) return f"Rotated layer '{layer_name}' by {angle} degrees" except StopIteration: return f"Layer '{layer_name}' not found." except Exception as e: return f"Error rotating layer: {e}" @mcp.tool() def change_blend_mode(layer_name: str, blend_mode: str) -> str: """ Change the blend mode of a layer. Args: layer_name (str): Name of the layer to modify. blend_mode (str): New blend mode (e.g., 'normal', 'multiply', 'screen', etc.) Returns: str: Success or error message. """ doc = get_active_document() if not doc: return ["No active document found."] blend_modes = { "normal": 2, "dissolve": 3, "darken": 4, "multiply": 5, "color burn": 6, "linear burn": 7, "lighten": 8, "screen": 9, "color dodge": 10, "linear dodge": 11, "overlay": 12, "soft light": 13, "hard light": 14, "vivid light": 15, "linear light": 16, "pin light": 17, "hard mix": 18, "difference": 19, "exclusion": 20, "hue": 21, "saturation": 22, "color": 23, "luminosity": 24, # Add more if supported } try: layer = next(l for l in doc.ArtLayers if l.Name == layer_name) doc.ActiveLayer = layer mode_key = blend_mode.lower() if mode_key not in blend_modes: return f"Unsupported blend mode '{blend_mode}'." layer.BlendMode = blend_modes[mode_key] return f"Blend mode for '{layer_name}' set to '{blend_mode}'" except StopIteration: return f"Layer '{layer_name}' not found." except Exception as e: return f"Error changing blend mode: {e}" import os import win32com.client @mcp.tool() def export_layers_as_png() -> str: """ Export each visible layer in the active document as a separate PNG to the EXPORT_DIR. """ doc = get_active_document() if not doc: return ["No active document found."] if not psApp or not doc: return "Photoshop is not running or no document is open" if not EXPORT_DIRECTORY: return "EXPORT_DIR not set" try: original_visibility = {} # Iterate over ArtLayers (not including LayerSets for now) for i, layer in enumerate(doc.ArtLayers): # Save current visibility state original_visibility[layer.Name] = layer.Visible # Hide all layers for l in doc.ArtLayers: l.Visible = False # Show only this layer layer.Visible = True doc.ActiveLayer = layer # Set up export options options = win32com.client.Dispatch('Photoshop.ExportOptionsSaveForWeb') options.Format = 13 # PNG options.PNG8 = False # Export path safe_name = "".join(c if c.isalnum() else "_" for c in layer.Name) export_path = os.path.join(EXPORT_DIRECTORY, f"{safe_name}.png") doc.Export(ExportIn=export_path, ExportAs=2, Options=options) # Restore visibility for l in doc.ArtLayers: if l.Name in original_visibility: l.Visible = original_visibility[l.Name] return f"Exported {len(doc.ArtLayers)} layers to {EXPORT_DIRECTORY}" except Exception as e: return f"Error exporting layers: {e}" @mcp.tool() def change_canvas_size(width: int, height: int, anchor: int = 9) -> str: """ Change the canvas size of the active document. Args: width (int): New width in pixels height (int): New height in pixels anchor (int): Anchor position (1=top left, 9=center, etc.). Default is 9 (center) Returns: str: Success or error message """ doc = get_active_document() if not doc: return ["No active document found."] try: doc.ResizeCanvas(Width=width, Height=height, Anchor=anchor) return f"Canvas resized to {width}x{height}." except Exception as e: return f"Error resizing canvas: {e}" @mcp.tool() def apply_posterize(levels: int = 4) -> str: """ Apply Posterize effect to the active layer. Args: levels (int): Number of tonal levels (2-255). Default is 4. """ doc = get_active_document() if not doc: return "No active document found." try: desc = win32com.client.Dispatch("Photoshop.ActionDescriptor") desc.PutInteger(psApp.StringIDToTypeID("levels"), levels) psApp.ExecuteAction(psApp.StringIDToTypeID("posterize"), desc) return f"Posterize applied with {levels} levels." except Exception as e: return f"Failed to apply posterize: {e}" @mcp.tool() def apply_threshold(level: int = 128) -> str: """ Apply Threshold effect to the active layer. Args: level (int): Threshold level (0–255). Default is 128. """ doc = get_active_document() if not doc: return "No active document found." try: desc = win32com.client.Dispatch("Photoshop.ActionDescriptor") desc.PutInteger(psApp.StringIDToTypeID("level"), level) psApp.ExecuteAction(psApp.StringIDToTypeID("thresholdClassEvent"), desc) return f"Threshold applied at level {level}." except Exception as e: return f"Failed to apply threshold: {e}" @mcp.tool() def addition_tool(arg1: int, arg2: int) -> int: return arg1 + arg2 if __name__ == "__main__": mcp.run(transport="stdio")

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/Chandrahas455/PsMCP-MCP-Server-for-Photoshop'

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