Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 60 additions & 2 deletions photoshop_mcp_server/ps_adapter/action_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,66 @@ def get_active_document_info(cls) -> dict[str, Any]:
except Exception as e:
print(f"Error getting document path: {e}")

# Get layers info would require more complex Action Manager code
# This is a simplified implementation
# Get layers info using PhotoshopApp document object
try:
doc = ps_app.get_active_document()
if doc:
# Get art layers
art_layers = []
for i, layer in enumerate(doc.artLayers):
try:
art_layers.append({
"index": i,
"name": layer.name,
"visible": layer.visible,
"kind": str(layer.kind),
"opacity": layer.opacity,
"blending_mode": str(layer.blendMode),
})
except Exception as e:
print(f"Error getting art layer {i}: {e}")
result["layers"] = art_layers

# Get layer sets (groups)
layer_sets = []
for i, ls in enumerate(doc.layerSets):
try:
set_layers = []
for j, layer in enumerate(ls.artLayers):
try:
set_layers.append({
"index": j,
"name": layer.name,
"visible": layer.visible,
"kind": str(layer.kind),
})
except Exception:
pass
layer_sets.append({
"index": i,
"name": ls.name,
"visible": ls.visible,
"layers": set_layers,
})
except Exception as e:
print(f"Error getting layer set {i}: {e}")
result["layer_sets"] = layer_sets

# Get channels
channels = []
for i, ch in enumerate(doc.channels):
try:
channels.append({
"index": i,
"name": ch.name,
"kind": str(ch.kind),
"visible": ch.visible,
})
except Exception as e:
print(f"Error getting channel {i}: {e}")
result["channels"] = channels
except Exception as e:
print(f"Error getting layers/layer_sets/channels: {e}")

return result

Expand Down
176 changes: 77 additions & 99 deletions photoshop_mcp_server/ps_adapter/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,111 +225,89 @@ def execute_javascript(self, script):
Returns:
str: The result of the JavaScript execution.

Note:
Photoshop uses ExtendScript (ECMAScript 3) which lacks a native
JSON object. A JSON.stringify polyfill is automatically injected.

comtypes requires all 3 arguments for doJavaScript to dispatch
correctly. Passing only the script string triggers COM error
-2147352567 on most Photoshop versions.

"""
# Ensure script returns a valid JSON string
# Ensure script ends with semicolon
if not script.strip().endswith(";"):
script = script.rstrip() + ";"

# Make sure script returns a value
if "return " not in script and "JSON.stringify" not in script:
script = script + "\n'success';" # Add a default return value
# Inject JSON polyfill for ExtendScript (ES3 has no native JSON object)
json_polyfill = (
"if(typeof JSON==='undefined'){JSON={stringify:function(v){"
"if(v===null)return'null';"
"if(typeof v==='number'||typeof v==='boolean')return String(v);"
"if(typeof v==='string')return'\"'+v.replace(/\\\\/g,'\\\\\\\\').replace(/\"/g,'\\\\\"')+'\"';"
"if(v.constructor===Array){var a=[];for(var i=0;i<v.length;i++)a.push(JSON.stringify(v[i]));return'['+a.join(',')+']';}"
"if(typeof v==='object'){var a=[];for(var k in v)if(v.hasOwnProperty(k))a.push('\"'+k+'\":'+JSON.stringify(v[k]));return'{'+a.join(',')+'}';}"
"return String(v);}}}"
)
full_script = json_polyfill + "\n" + script

try:
# Try to execute with default parameters
result = self.app.doJavaScript(script)
if result:
return result
return '{"success": true}' # Return a valid JSON if no result
# comtypes requires all 3 arguments for doJavaScript to work correctly.
# Arguments: (script, arguments_array_or_None, execution_mode)
# execution_mode: 1 = psNormalMode
result = self.app.doJavaScript(full_script, None, 1)
if result is not None:
return str(result)
return '{"success": true}'
except Exception as e:
print(f"Error executing JavaScript (attempt 1): {e}")

# Check for specific COM error code -2147212704
if "-2147212704" in str(e):
print("Detected COM error -2147212704, trying alternative approach")
# This is often a dialog-related error, try with a safer script
safer_script = f"""
try {{
// Disable dialogs
var originalDialogMode = app.displayDialogs;
app.displayDialogs = DialogModes.NO;

// Execute the original script
var result = (function() {{
{script}
}})();

// Restore dialog mode
app.displayDialogs = originalDialogMode;

return result;
}} catch(e) {{
return JSON.stringify({{
"error": e.toString(),
"success": false
}});
}}
"""
error_str = str(e)
print(f"Error executing JavaScript: {e}")

# For dialog-related COM errors, retry with dialogs disabled
if "-2147212704" in error_str:
safer_script = (
"var _origDM = app.displayDialogs;"
"app.displayDialogs = DialogModes.NO;"
"var _r = (function(){" + full_script + "})();"
"app.displayDialogs = _origDM;"
"_r;"
)
try:
return self.app.doJavaScript(safer_script, None, 1)
except Exception as e_safer:
print(f"Safer script approach failed: {e_safer}")
# Continue to other fallbacks

try:
# Try with explicit parameters
# 1 = PsJavaScriptExecutionMode.psNormalMode
result = self.app.doJavaScript(script, None, 1)
if result:
return result
return '{"success": true}' # Return a valid JSON if no result
except Exception as e2:
print(f"Error executing JavaScript (attempt 2): {e2}")

# Try with a different execution mode
result = self.app.doJavaScript(safer_script, None, 1)
if result is not None:
return str(result)
return '{"success": true}'
except Exception as e2:
print(f"Retry with DialogModes.NO also failed: {e2}")

# Wrap in try-catch as last resort
if "try {" not in full_script:
wrapped = (
json_polyfill
+ "try{"
"var _origDM = app.displayDialogs;"
"app.displayDialogs = DialogModes.NO;"
"var _r = (function(){" + full_script + "})();"
"app.displayDialogs = _origDM;"
"_r;"
"}catch(e){"
"JSON.stringify({error: e.toString(), success: false});"
"}"
)
try:
# 2 = PsJavaScriptExecutionMode.psInteractiveMode
result = self.app.doJavaScript(script, None, 2)
if result:
return result
return '{"success": true}' # Return a valid JSON if no result
except Exception as e3:
print(f"Error executing JavaScript (attempt 3): {e3}")

# Last resort: wrap script in a try-catch block if not already wrapped
if "try {" not in script:
wrapped_script = f"""
try {{
// Disable dialogs
var originalDialogMode = app.displayDialogs;
app.displayDialogs = DialogModes.NO;

// Execute the original script
var result = (function() {{
{script}
}})();

// Restore dialog mode
app.displayDialogs = originalDialogMode;

return result;
}} catch(e) {{
return JSON.stringify({{
"error": e.toString(),
"success": false
}});
}}
"""
try:
result = self.app.doJavaScript(wrapped_script, None, 1)
if result:
return result
return '{"success": true}' # Return a valid JSON if no result
except Exception as e4:
print(f"Error executing JavaScript (final attempt): {e4}")
# Return a valid JSON with error information
error_msg = str(e4).replace('"', '\\"')
return '{"error": "' + error_msg + '", "success": false}'
else:
# Script already has try-catch, just return the error
error_msg = str(e2).replace('"', '\\"')
return '{"error": "' + error_msg + '", "success": false}'
result = self.app.doJavaScript(wrapped, None, 1)
if result is not None:
return str(result)
return '{"success": true}'
except Exception as e_final:
print(f"All JavaScript execution attempts failed: {e_final}")
return (
'{"error": "'
+ str(e_final).replace('"', '\\"')
+ '", "success": false}'
)

return (
'{"error": "'
+ error_str.replace('"', '\\"')
+ '", "success": false}'
)
75 changes: 75 additions & 0 deletions photoshop_mcp_server/tools/script_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Script execution MCP tools for Photoshop."""

from photoshop_mcp_server.ps_adapter.application import PhotoshopApp
from photoshop_mcp_server.registry import register_tool


def register(mcp):
"""Register script execution tools.

Args:
mcp: The MCP server instance.

"""

def execute_jsx(script: str) -> dict:
"""Execute JavaScript (JSX) code in Photoshop.

This is a universal tool that can run any Photoshop JavaScript code,
giving access to the full Photoshop API including operations not covered
by other dedicated tools.

A JSON.stringify polyfill is automatically injected since Photoshop's
ExtendScript engine is based on ECMAScript 3 which lacks native JSON.

Args:
script: JavaScript/JSX code to execute in Photoshop.
The script should return a value (string, number, or JSON string).
If no return statement is present, the last expression is returned.

Returns:
dict: Result containing 'success' flag and 'result' with the script output,
or 'error' if execution failed.

Examples:
Get layer count:
script: "app.activeDocument.artLayers.length;"

Get all layer names as JSON:
script: '''
var doc = app.activeDocument;
var names = [];
for (var i = 0; i < doc.artLayers.length; i++) {
names.push(doc.artLayers[i].name);
}
JSON.stringify(names);
'''

Create a rectangle shape:
script: '''
var doc = app.activeDocument;
var layer = doc.artLayers.add();
layer.name = "Rectangle";
var selection = doc.selection;
selection.select([[100,100],[500,100],[500,400],[100,400]]);
selection.fill(app.foregroundColor);
selection.deselect();
"done";
'''

"""
ps_app = PhotoshopApp()
try:
result = ps_app.execute_javascript(script)
return {
"success": True,
"result": result,
}
except Exception as e:
return {
"success": False,
"error": str(e),
}

tool_name = register_tool(mcp, execute_jsx, "execute_jsx")
return [tool_name]