gimp-mcp-plugin.py•47.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)