Skip to main content
Glama

gimp-mcp

gimp-mcp-plugin.py47.2 kB
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ GIMP MCP Plugin - Model Context Protocol integration for GIMP Provides bitmap extraction and metadata access functionality """ import gi gi.require_version('Gimp', '3.0') from gi.repository import Gimp from gi.repository import GLib import io import sys import json import socket import traceback import threading import base64 import tempfile import os import platform # Constants for configuration and thresholds LARGE_SCALING_THRESHOLD = 4.0 # Warn if scaling ratio exceeds this value MAX_REGION_SIZE = 8192 # Maximum region dimension in pixels DEFAULT_TIMEOUT_SECONDS = 30 # Default timeout for operations def N_(message): return message def _(message): return GLib.dgettext(None, message) def exec_and_get_results(command, context): buffer = io.StringIO() original_stdout = sys.stdout sys.stdout = buffer exec(command, context) sys.stdout = original_stdout output = buffer.getvalue() return output class MCPPlugin(Gimp.PlugIn): def __init__(self, host='localhost', port=9877): super().__init__() self.host = host self.port = port self.running = False self.socket = None self.server_thread = None self.context = {} exec("from gi.repository import Gimp", self.context) self.auto_disconnect_client = True def do_query_procedures(self): """Register the plugin procedure.""" return ["plug-in-mcp-server"] def do_create_procedure(self, name): """Define the procedure properties.""" procedure = Gimp.ImageProcedure.new(self, name, Gimp.PDBProcType.PLUGIN, self.run, None) procedure.set_menu_label(_("Start MCP Server")) procedure.set_documentation(_("Starts an MCP server to control GIMP externally"), _("Starts an MCP server to control GIMP externally"), name) procedure.set_attribution("Your Name", "Your Name", "2023") procedure.add_menu_path('<Image>/Tools/') return procedure def run(self, procedure, run_mode, image, drawables, config, run_data): """Run the plugin and start the server.""" if self.running: print("MCP Server is already running") return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error()) self.running = True try: print("Creating socket...") self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind((self.host, self.port)) self.socket.listen(1) print(f"GimpMCP server started on {self.host}:{self.port}") while self.running: client, address = self.socket.accept() print(f"Connected to client: {address}") # Handle client in a separate thread client_thread = threading.Thread( target=self._handle_client, args=(client,) ) client_thread.daemon = True client_thread.start() return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error()) except Exception as e: print(f"Error starting server: {str(e)}") self.running = False if self.socket: self.socket.close() self.socket = None if self.server_thread: self.server_thread.join(timeout=1.0) self.server_thread = None return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error()) def _handle_client(self, client): """Handle connected client""" # print("Client handler started") buffer = b'' # Receive data in chunks to handle larger payloads while True: data = client.recv(4096) # print(f"Received data: {data}") if not data: break buffer += data # Check if we have a complete message # For simplicity, assume messages end with newline or are complete JSON try: if isinstance(buffer, (bytes, bytearray)): request = buffer.decode('utf-8') else: request = str(buffer) # Try to parse as JSON to see if complete if request.strip(): json.loads(request) # This will raise if incomplete break except (json.JSONDecodeError, UnicodeDecodeError): # Continue receiving if JSON is incomplete continue if not buffer: print("Client disconnected") return if isinstance(buffer, (bytes, bytearray)): request = buffer.decode('utf-8') else: request = str(buffer) # print(f"Parsed request: {request}") response = self.execute_command(request) print(f"response type: {type(response)}") if isinstance(response, dict): response_str = json.dumps(response) else: response_str = str(response) # Send response in chunks for large data response_bytes = response_str.encode('utf-8') bytes_sent = 0 while bytes_sent < len(response_bytes): chunk = response_bytes[bytes_sent:bytes_sent + 8192] client.sendall(chunk) bytes_sent += len(chunk) if self.auto_disconnect_client: client.close() return def execute_command(self, request): """Execute commands in GIMP's main thread.""" try: # print("command", request) if request == "disable_auto_disconnect": self.auto_disconnect_client = False return { "status": "success", "results": "OK" } j = json.loads(request) if "type" in j and j["type"] == "get_image_bitmap": params = j.get("params", {}) return self._get_current_image_bitmap(params) elif "type" in j and j["type"] == "get_image_metadata": return self._get_current_image_metadata() elif "type" in j and j["type"] == "get_gimp_info": return self._get_gimp_info() elif "cmds" in j: a = ['python-fu-exec', j["cmds"]] else: p = j["params"] a = p['args'] if a[0] == 'python-fu-eval': if len(a) > 0: print(f"evaluating exprs: {a[1]}") vals = [str(eval(e)) for e in a[1]] results = { "status": "success", "results": vals } else: results = { "status": "success", "results": "[NULL]" } print(f"expression result: {results}") return results else: outputs = ["OK"] if len(a) > 0: print(f"Executing commands: {a[1]}") outputs = [exec_and_get_results(c, self.context) for c in a[1]] else: print(f"no command to execute") result = { "status": "success", "results": outputs } print(f"Command result: {result}") return result except Exception as e: error_msg = f"Error executing command: {str(e)}\n{traceback.format_exc()}" print(error_msg) return { "status": "error", "error": str(e), "traceback": traceback.format_exc() } def _get_current_image_bitmap(self, params=None): """Get the current image as a base64-encoded bitmap with optional scaling and region selection.""" try: if params is None: params = {} print(f"Getting current image bitmap with params: {params}") # Extract parameters max_width = params.get("max_width") max_height = params.get("max_height") # Extract region parameters if provided region = params.get("region", {}) # Validate region parameters if provided if region: # Validate region parameter types for key, expected_type in [("origin_x", int), ("origin_y", int), ("width", int), ("height", int), ("max_width", int), ("max_height", int)]: if key in region and region[key] is not None: if not isinstance(region[key], expected_type): return { "status": "error", "error": f"Region parameter '{key}' must be of type {expected_type.__name__}, got {type(region[key]).__name__}" } if region[key] < 0: return { "status": "error", "error": f"Region parameter '{key}' must be non-negative, got {region[key]}" } origin_x = region.get("origin_x") origin_y = region.get("origin_y") region_width = region.get("width") region_height = region.get("height") scaled_to_width = region.get("max_width") # Region scaling uses max_width/max_height scaled_to_height = region.get("max_height") # Get the current images images = Gimp.get_images() if not images: return { "status": "error", "error": "No images are currently open in GIMP" } # Use the first image (most recently active) original_image = images[0] # Get original image dimensions orig_img_width = original_image.get_width() orig_img_height = original_image.get_height() # Determine working image and region working_image = None should_delete_working = False # Case 1: Region selection if any(param is not None for param in [origin_x, origin_y, region_width, region_height]): print("Processing region extraction...") # Validate region parameters if origin_x is None or origin_y is None or region_width is None or region_height is None: return { "status": "error", "error": "For region selection, all parameters are required: origin_x, origin_y, width, height" } # Validate region bounds if (origin_x < 0 or origin_y < 0 or origin_x + region_width > orig_img_width or origin_y + region_height > orig_img_height): return { "status": "error", "error": f"Region bounds invalid. Image size: {orig_img_width}x{orig_img_height}, " f"requested region: ({origin_x},{origin_y}) {region_width}x{region_height}" } # Create new image with the region working_image = Gimp.Image.new(region_width, region_height, original_image.get_base_type()) should_delete_working = True # Copy the region from original image # First, select the region in the original image original_image.select_rectangle(Gimp.ChannelOps.REPLACE, origin_x, origin_y, region_width, region_height) # Get the active layer from original image orig_layers = original_image.get_layers() if not orig_layers: return { "status": "error", "error": "No layers found in original image" } # Create a new layer in working image # In GIMP 3.0+, use the image's base type instead of layer.get_image_type() try: # Try to get layer type - fallback to image base type if hasattr(orig_layers[0], 'get_type'): layer_type = orig_layers[0].get_type() else: # Use image base type as fallback layer_type = original_image.get_base_type() except AttributeError: # Final fallback - use RGB layer_type = Gimp.ImageBaseType.RGB new_layer = Gimp.Layer.new(working_image, 'Region', region_width, region_height, layer_type, 100, Gimp.LayerMode.NORMAL) working_image.insert_layer(new_layer, None, 0) # Copy and paste the selection Gimp.edit_copy([orig_layers[0]]) floating_sel = Gimp.edit_paste(new_layer, True)[0] Gimp.floating_sel_anchor(floating_sel) # Clear selection try: # Try different methods to clear selection based on GIMP version if hasattr(original_image, 'select_none'): original_image.select_none() else: # Use Gimp.Selection.none() for GIMP 3.0+ Gimp.Selection.none(original_image) except (AttributeError, RuntimeError) as e: print(f"Warning: Could not clear selection: {e}") else: # Case 2: Full image print("Processing full image...") working_image = original_image should_delete_working = False # Now handle scaling if needed final_image = working_image should_delete_final = should_delete_working # Calculate target dimensions current_width = working_image.get_width() current_height = working_image.get_height() target_width = current_width target_height = current_height # Determine scaling target if scaled_to_width is not None and scaled_to_height is not None: # Region scaling - use scaled_to dimensions max_w, max_h = scaled_to_width, scaled_to_height elif max_width is not None and max_height is not None: # Full image scaling - use max dimensions max_w, max_h = max_width, max_height else: max_w = max_h = None # Apply center inside scaling if target dimensions provided if max_w is not None and max_h is not None: # Calculate center inside scaling aspect_ratio = current_width / current_height max_aspect_ratio = max_w / max_h if aspect_ratio > max_aspect_ratio: # Width is the limiting factor target_width = max_w target_height = int(max_w / aspect_ratio) else: # Height is the limiting factor target_height = max_h target_width = int(max_h * aspect_ratio) print(f"Scaling from {current_width}x{current_height} to {target_width}x{target_height}") # Scale the image if dimensions changed if target_width != current_width or target_height != current_height: # Create scaled image final_image = working_image.duplicate() should_delete_final = True # Scale the image with timeout consideration for large operations scaling_ratio = (target_width * target_height) / (current_width * current_height) if scaling_ratio > LARGE_SCALING_THRESHOLD: # Scaling up significantly print(f"Warning: Large scaling operation detected (ratio: {scaling_ratio:.2f}). This may take time.") try: final_image.scale(target_width, target_height) except (RuntimeError, AttributeError) as scale_error: # Clean up and return error for scaling failures if should_delete_final: try: final_image.delete() except (AttributeError, RuntimeError): pass raise RuntimeError(f"Failed to scale image from {current_width}x{current_height} to {target_width}x{target_height}: {scale_error}") # Create a temporary file for export temp_fd, temp_path = tempfile.mkstemp(suffix='.png') os.close(temp_fd) # Close the file descriptor as GIMP will handle the file try: # Export the final image as PNG # Get all layers - we'll export the flattened image layers = final_image.get_layers() if not layers: return { "status": "error", "error": "No layers found in the processed image" } # For PNG export, we can use all layers or the active layer try: drawable = final_image.get_active_layer() except (AttributeError, RuntimeError): # If get_active_layer doesn't exist or fails, use the first layer drawable = layers[0] if not drawable: drawable = layers[0] # Export the image to PNG try: # In GIMP 3.0, use the simplified export approach from gi.repository import Gio file_obj = Gio.File.new_for_path(temp_path) # Use file-png-export with the correct parameters for GIMP 3.0 export_proc = Gimp.get_pdb().lookup_procedure('file-png-export') if not export_proc: return { "status": "error", "error": "PNG export procedure not found" } export_config = export_proc.create_config() export_config.set_property('image', final_image) export_config.set_property('file', file_obj) # Try different property names that might exist try: export_config.set_property('drawable', drawable) except: try: export_config.set_property('drawables', [drawable]) except: # Some export procedures might not need drawable specification pass result = export_proc.run(export_config) print(f"Export result: {result}") except Exception as export_error: print(f"Export error: {export_error}") # Fallback: try using the PDB directly with correct arguments try: from gi.repository import Gio file_obj = Gio.File.new_for_path(temp_path) # Try alternative approach using Gimp.file_save with correct number of arguments Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, final_image, file_obj) print("Fallback export successful") except Exception as fallback_error: print(f"Fallback export error: {fallback_error}") # Try another fallback using gimp-file-save PDB procedure try: pdb = Gimp.get_pdb() save_proc = pdb.lookup_procedure('gimp-file-save') if save_proc: save_config = save_proc.create_config() save_config.set_property('image', final_image) save_config.set_property('file', file_obj) save_result = save_proc.run(save_config) print(f"PDB save result: {save_result}") else: return { "status": "error", "error": f"All export methods failed: {export_error}, fallback: {fallback_error}" } except Exception as pdb_error: return { "status": "error", "error": f"All export methods failed: {export_error}, fallback: {fallback_error}, PDB: {pdb_error}" } # Read the exported file and encode as base64 with open(temp_path, 'rb') as f: image_data = f.read() encoded_image = base64.b64encode(image_data).decode('utf-8') # Get final image metadata final_width = final_image.get_width() final_height = final_image.get_height() return { "status": "success", "results": { "image_data": encoded_image, "format": "png", "width": final_width, "height": final_height, "original_width": orig_img_width, "original_height": orig_img_height, "encoding": "base64", "processing_applied": { "region_extracted": any(param is not None for param in [origin_x, origin_y, region_width, region_height]), "scaled": target_width != current_width or target_height != current_height, "region_coords": {"x": origin_x, "y": origin_y, "w": region_width, "h": region_height} if origin_x is not None else None } } } finally: # Clean up temporary images if should_delete_final and final_image != working_image: try: final_image.delete() except (AttributeError, RuntimeError) as e: print(f"Warning: Failed to delete final temporary image: {e}") if should_delete_working and working_image != original_image: try: working_image.delete() except (AttributeError, RuntimeError) as e: print(f"Warning: Failed to delete working temporary image: {e}") # Clean up the temporary file if os.path.exists(temp_path): os.unlink(temp_path) except (RuntimeError, AttributeError, OSError, ValueError) as e: return { "status": "error", "error": f"Processing error: {str(e)}", "traceback": traceback.format_exc() } def _get_current_image_metadata(self): """Get comprehensive metadata about the current image without bitmap data.""" try: print("Getting current image metadata...") # Get the current images images = Gimp.get_images() if not images: return { "status": "error", "error": "No images are currently open in GIMP" } # Use the first image (most recently active) image = images[0] # Basic image properties width = image.get_width() height = image.get_height() # Get image type and base type base_type = image.get_base_type() base_type_str = self._base_type_to_string(base_type) # Get precision and color profile info precision = image.get_precision() precision_str = self._precision_to_string(precision) # Get layers information layers = image.get_layers() layers_info = [] for i, layer in enumerate(layers): try: layer_info = { "name": layer.get_name(), "visible": layer.get_visible(), "opacity": layer.get_opacity(), "width": layer.get_width(), "height": layer.get_height(), "has_alpha": layer.has_alpha(), "is_group": hasattr(layer, 'get_children') and callable(getattr(layer, 'get_children')), "layer_type": self._get_layer_type_string(layer) } # Try to get layer mode if available try: layer_info["blend_mode"] = str(layer.get_mode()) except: layer_info["blend_mode"] = "unknown" layers_info.append(layer_info) except Exception as layer_error: print(f"Error getting layer {i} info: {layer_error}") layers_info.append({ "name": f"Layer {i}", "error": str(layer_error) }) # Get channels information channels = image.get_channels() channels_info = [] for i, channel in enumerate(channels): try: channel_info = { "name": channel.get_name(), "visible": channel.get_visible(), "opacity": channel.get_opacity(), "color": str(channel.get_color()) if hasattr(channel, 'get_color') else "unknown" } channels_info.append(channel_info) except Exception as channel_error: print(f"Error getting channel {i} info: {channel_error}") channels_info.append({ "name": f"Channel {i}", "error": str(channel_error) }) # Get paths/vectors information paths = [] try: image_paths = image.get_paths() for i, path in enumerate(image_paths): try: path_info = { "name": path.get_name(), "visible": path.get_visible(), "num_strokes": len(path.get_strokes()) if hasattr(path, 'get_strokes') else 0 } paths.append(path_info) except Exception as path_error: print(f"Error getting path {i} info: {path_error}") paths.append({ "name": f"Path {i}", "error": str(path_error) }) except Exception as paths_error: print(f"Error getting paths: {paths_error}") # Get file information if available file_info = {} try: image_file = image.get_file() if image_file: file_info = { "path": image_file.get_path() if hasattr(image_file, 'get_path') else None, "uri": image_file.get_uri() if hasattr(image_file, 'get_uri') else None, "basename": image_file.get_basename() if hasattr(image_file, 'get_basename') else None } except Exception as file_error: print(f"Error getting file info: {file_error}") file_info = {"error": str(file_error)} # Get resolution information resolution_x = resolution_y = None try: resolution_x, resolution_y = image.get_resolution() except Exception as res_error: print(f"Error getting resolution: {res_error}") # Check if image has unsaved changes is_dirty = False try: is_dirty = image.is_dirty() except Exception as dirty_error: print(f"Error getting dirty status: {dirty_error}") metadata = { "basic": { "width": width, "height": height, "base_type": base_type_str, "precision": precision_str, "resolution_x": resolution_x, "resolution_y": resolution_y, "is_dirty": is_dirty }, "structure": { "num_layers": len(layers), "num_channels": len(channels), "num_paths": len(paths), "layers": layers_info, "channels": channels_info, "paths": paths }, "file": file_info } return { "status": "success", "results": metadata } except Exception as e: error_msg = f"Error getting image metadata: {str(e)}\n{traceback.format_exc()}" print(error_msg) return { "status": "error", "error": str(e), "traceback": traceback.format_exc() } def _base_type_to_string(self, base_type): """Convert GIMP base type enum to string.""" try: base_type_map = { Gimp.ImageBaseType.RGB: "RGB", Gimp.ImageBaseType.GRAY: "Grayscale", Gimp.ImageBaseType.INDEXED: "Indexed" } return base_type_map.get(base_type, f"Unknown ({base_type})") except: return str(base_type) def _precision_to_string(self, precision): """Convert GIMP precision enum to readable string.""" try: precision_map = { 100: "u8", # Gimp.Precision.U8_LINEAR 150: "u8-gamma", # Gimp.Precision.U8_GAMMA 200: "u16", # Gimp.Precision.U16_LINEAR 250: "u16-gamma", # Gimp.Precision.U16_GAMMA 300: "u32", # Gimp.Precision.U32_LINEAR 350: "u32-gamma", # Gimp.Precision.U32_GAMMA 500: "half", # Gimp.Precision.HALF_LINEAR 550: "half-gamma", # Gimp.Precision.HALF_GAMMA 600: "float", # Gimp.Precision.FLOAT_LINEAR 650: "float-gamma", # Gimp.Precision.FLOAT_GAMMA 700: "double", # Gimp.Precision.DOUBLE_LINEAR 750: "double-gamma" # Gimp.Precision.DOUBLE_GAMMA } return precision_map.get(int(precision), f"precision-{precision}") except: return str(precision) def _get_layer_type_string(self, layer): """Get layer type string with compatibility for different GIMP versions.""" try: # Try different methods to get layer type if hasattr(layer, 'get_type'): return str(layer.get_type()) elif hasattr(layer, 'get_image_type'): return str(layer.get_image_type()) elif hasattr(layer, 'type'): return str(layer.type) else: # Fallback - determine from layer properties if layer.has_alpha(): return "RGBA" else: return "RGB" except Exception as e: print(f"Warning: Could not determine layer type: {e}") return "unknown" def _get_gimp_info(self): """Get comprehensive information about GIMP installation and environment.""" try: print("Getting GIMP environment information...") gimp_info = {} # Basic GIMP version and build information try: version_info = {} # Try different methods to get version info try: # Try the version() method if it exists if hasattr(Gimp, 'version'): version_info["version_method"] = str(Gimp.version()) except Exception as v_error: version_info["version_method_error"] = str(v_error) # Try to get version from constants if they exist for attr in ['MAJOR_VERSION', 'MINOR_VERSION', 'MICRO_VERSION']: try: if hasattr(Gimp, attr): version_info[attr.lower()] = getattr(Gimp, attr) except Exception as attr_error: version_info[f"{attr.lower()}_error"] = str(attr_error) # Get available version-related attributes version_attrs = [attr for attr in dir(Gimp) if 'version' in attr.lower()] if version_attrs: version_info["available_version_attributes"] = version_attrs # Try to get version string from any available source version_string = "Unknown" try: # Check if there's a version string constant if hasattr(Gimp, 'VERSION'): version_string = str(Gimp.VERSION) elif hasattr(Gimp, 'version_string'): version_string = str(Gimp.version_string()) elif hasattr(Gimp, 'get_version'): version_string = str(Gimp.get_version()) except: pass version_info["detected_version"] = version_string version_info["gimp_module_type"] = str(type(Gimp)) gimp_info["version"] = version_info except Exception as version_error: print(f"Error getting version info: {version_error}") gimp_info["version"] = {"error": str(version_error)} # Installation and directory information try: directories = {} # Safely try each directory method directory_methods = [ ('user_directory', 'directory'), ('system_data_directory', 'data_directory'), ('locale_directory', 'locale_directory'), ('plugin_directory', 'plug_in_directory'), ('sysconf_directory', 'sysconf_directory') ] for dir_name, method_name in directory_methods: try: if hasattr(Gimp, method_name): method = getattr(Gimp, method_name) if callable(method): directories[dir_name] = str(method()) else: directories[dir_name] = str(method) else: directories[f"{dir_name}_not_available"] = True except Exception as method_error: directories[f"{dir_name}_error"] = str(method_error) # List available directory-related methods dir_attrs = [attr for attr in dir(Gimp) if 'dir' in attr.lower()] directories["available_directory_methods"] = dir_attrs gimp_info["directories"] = directories except Exception as dir_error: print(f"Error getting directory info: {dir_error}") gimp_info["directories"] = {"error": str(dir_error)} # Current session information try: images = Gimp.get_images() gimp_info["session"] = { "num_open_images": len(images), "has_open_images": len(images) > 0, "open_image_files": [] } # Get file information for open images for i, image in enumerate(images): try: image_file = image.get_file() file_info = { "index": i, "width": image.get_width(), "height": image.get_height(), "base_type": self._base_type_to_string(image.get_base_type()), "is_dirty": image.is_dirty() if hasattr(image, 'is_dirty') else None } if image_file: file_info.update({ "path": image_file.get_path() if hasattr(image_file, 'get_path') else None, "basename": image_file.get_basename() if hasattr(image_file, 'get_basename') else None }) else: file_info["path"] = "Untitled" gimp_info["session"]["open_image_files"].append(file_info) except Exception as image_error: print(f"Error getting image {i} info: {image_error}") gimp_info["session"]["open_image_files"].append({ "index": i, "error": str(image_error) }) except Exception as session_error: print(f"Error getting session info: {session_error}") gimp_info["session"] = {"error": str(session_error)} # PDB (Procedure Database) information try: pdb = Gimp.get_pdb() pdb_info = { "available": pdb is not None, "type": str(type(pdb)) if pdb else None } # Try to get some example procedures if pdb: sample_procedures = [] try: # Test some common procedures test_procs = ['file-png-export', 'gimp-file-save', 'gimp-image-new', 'python-fu-console'] for proc_name in test_procs: try: proc = pdb.lookup_procedure(proc_name) sample_procedures.append({ "name": proc_name, "available": proc is not None, "type": str(type(proc)) if proc else None }) except: sample_procedures.append({ "name": proc_name, "available": False, "error": "lookup_failed" }) except Exception as proc_error: print(f"Error testing procedures: {proc_error}") pdb_info["sample_procedures"] = sample_procedures gimp_info["pdb"] = pdb_info except Exception as pdb_error: print(f"Error getting PDB info: {pdb_error}") gimp_info["pdb"] = {"error": str(pdb_error)} # Context and environment information try: context_info = {} # Try to get current context information try: fg_color = Gimp.context_get_foreground() context_info["foreground_color"] = str(fg_color) if fg_color else None except: context_info["foreground_color"] = "unavailable" try: bg_color = Gimp.context_get_background() context_info["background_color"] = str(bg_color) if bg_color else None except: context_info["background_color"] = "unavailable" try: brush_size = Gimp.context_get_brush_size() context_info["brush_size"] = brush_size if brush_size else None except: context_info["brush_size"] = "unavailable" gimp_info["context"] = context_info except Exception as context_error: print(f"Error getting context info: {context_error}") gimp_info["context"] = {"error": str(context_error)} # Capabilities and features try: capabilities = { "has_python_console": True, # We're running Python "mcp_server_running": True, # We're responding to MCP requests "supports_image_export": True, # We have the bitmap export function "supports_metadata_export": True, # We have the metadata function "supports_gimp_info": True, # We have the gimp info function "api_version": "3.0+", "python_version": sys.version, "available_modules": [], "gimp_module_attributes": len(dir(Gimp)), "gimp_methods": [attr for attr in dir(Gimp) if callable(getattr(Gimp, attr, None))][:20] # First 20 methods } # Test for available Python modules test_modules = ['gi.repository.Gimp', 'gi.repository.Gegl', 'gi.repository.Gio', 'json', 'base64', 'tempfile'] for module_name in test_modules: try: if module_name == 'gi.repository.Gimp': # Already imported capabilities["available_modules"].append({"name": module_name, "available": True}) elif module_name == 'gi.repository.Gegl': from gi.repository import Gegl capabilities["available_modules"].append({"name": module_name, "available": True}) elif module_name == 'gi.repository.Gio': from gi.repository import Gio capabilities["available_modules"].append({"name": module_name, "available": True}) else: __import__(module_name) capabilities["available_modules"].append({"name": module_name, "available": True}) except ImportError: capabilities["available_modules"].append({"name": module_name, "available": False}) except Exception as mod_error: capabilities["available_modules"].append({"name": module_name, "available": False, "error": str(mod_error)}) gimp_info["capabilities"] = capabilities except Exception as cap_error: print(f"Error getting capabilities: {cap_error}") gimp_info["capabilities"] = {"error": str(cap_error)} # System and platform information try: system_info = { "platform": platform.platform(), "system": platform.system(), "machine": platform.machine(), "python_version": platform.python_version(), "environment_vars": { "HOME": os.environ.get("HOME"), "USER": os.environ.get("USER"), "GIMP_PLUG_IN_DIR": os.environ.get("GIMP_PLUG_IN_DIR"), "GIMP_DATA_DIR": os.environ.get("GIMP_DATA_DIR") } } gimp_info["system"] = system_info except Exception as sys_error: print(f"Error getting system info: {sys_error}") gimp_info["system"] = {"error": str(sys_error)} return { "status": "success", "results": gimp_info } except Exception as e: error_msg = f"Error getting GIMP info: {str(e)}\n{traceback.format_exc()}" return { "status": "error", "error": error_msg, "traceback": traceback.format_exc() } Gimp.main(MCPPlugin.__gtype__, sys.argv)

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/maorcc/gimp-mcp'

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