psforge 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- psforge/__init__.py +5 -0
- psforge/app.py +5 -0
- psforge/decorators.py +91 -0
- psforge/ps_adapter/__init__.py +12 -0
- psforge/ps_adapter/action_manager.py +5 -0
- psforge/ps_adapter/application.py +149 -0
- psforge/ps_adapter/context.py +219 -0
- psforge/ps_adapter/process_guard.py +109 -0
- psforge/ps_adapter/utils.py +80 -0
- psforge/registry.py +121 -0
- psforge/resources/__init__.py +3 -0
- psforge/resources/registry.py +3 -0
- psforge/server.py +79 -0
- psforge/tools/__init__.py +3 -0
- psforge/tools/action_tools.py +149 -0
- psforge/tools/adjustment_tools.py +316 -0
- psforge/tools/batch_tools.py +124 -0
- psforge/tools/document_tools.py +341 -0
- psforge/tools/filter_tools.py +252 -0
- psforge/tools/history_tools.py +241 -0
- psforge/tools/image_tools.py +201 -0
- psforge/tools/layer_ordering_tools.py +306 -0
- psforge/tools/layer_properties_tools.py +364 -0
- psforge/tools/layer_tools.py +316 -0
- psforge/tools/layer_transform_tools.py +331 -0
- psforge/tools/mask_tools.py +286 -0
- psforge/tools/registry.py +6 -0
- psforge/tools/selection_tools.py +248 -0
- psforge/tools/session_tools.py +244 -0
- psforge/tools/text_tools.py +390 -0
- psforge-0.2.0.dist-info/METADATA +594 -0
- psforge-0.2.0.dist-info/RECORD +35 -0
- psforge-0.2.0.dist-info/WHEEL +4 -0
- psforge-0.2.0.dist-info/entry_points.txt +3 -0
- psforge-0.2.0.dist-info/licenses/LICENSE +21 -0
psforge/registry.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Tool and resource registration system for MCP server."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import pkgutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from mcp.server.fastmcp import FastMCP
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_tool(mcp: FastMCP, func: Callable, name: str | None = None) -> str:
|
|
13
|
+
"""Register a single tool function with the MCP server.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
mcp: MCP server instance.
|
|
17
|
+
func: Tool function to register.
|
|
18
|
+
name: Optional tool name override (defaults to function name).
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The registered tool name.
|
|
22
|
+
"""
|
|
23
|
+
tool_name = name or func.__name__
|
|
24
|
+
description = func.__doc__ or f"Tool: {tool_name}"
|
|
25
|
+
|
|
26
|
+
mcp.tool(name=tool_name, description=description.strip())(func)
|
|
27
|
+
|
|
28
|
+
logger.debug(f"Registered tool: {tool_name}")
|
|
29
|
+
return tool_name
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def discover_and_register_tools(mcp: FastMCP) -> list[str]:
|
|
33
|
+
"""Automatically discover and register all tools from the tools package.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
mcp: MCP server instance.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List of registered tool names.
|
|
40
|
+
"""
|
|
41
|
+
registered_tools = []
|
|
42
|
+
|
|
43
|
+
# Import tools package
|
|
44
|
+
try:
|
|
45
|
+
import psforge.tools as tools_pkg
|
|
46
|
+
except ImportError as e:
|
|
47
|
+
logger.error(f"Failed to import tools package: {e}")
|
|
48
|
+
return registered_tools
|
|
49
|
+
|
|
50
|
+
# Get tools package path
|
|
51
|
+
tools_path = Path(tools_pkg.__file__).parent
|
|
52
|
+
|
|
53
|
+
# Iterate through all Python modules in tools/
|
|
54
|
+
for module_info in pkgutil.iter_modules([str(tools_path)]):
|
|
55
|
+
if module_info.name.startswith("_"):
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
module_name = f"psforge.tools.{module_info.name}"
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
module = importlib.import_module(module_name)
|
|
62
|
+
|
|
63
|
+
# Look for register() function
|
|
64
|
+
if hasattr(module, "register"):
|
|
65
|
+
logger.info(f"Registering tools from: {module_name}")
|
|
66
|
+
tool_names = module.register(mcp)
|
|
67
|
+
registered_tools.extend(tool_names)
|
|
68
|
+
else:
|
|
69
|
+
logger.warning(f"Module {module_name} has no register() function")
|
|
70
|
+
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"Failed to register tools from {module_name}: {e}")
|
|
73
|
+
|
|
74
|
+
logger.info(f"Total tools registered: {len(registered_tools)}")
|
|
75
|
+
return registered_tools
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def discover_and_register_resources(mcp: FastMCP) -> list[str]:
|
|
79
|
+
"""Automatically discover and register all resources from the resources package.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
mcp: MCP server instance.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
List of registered resource URIs.
|
|
86
|
+
"""
|
|
87
|
+
registered_resources = []
|
|
88
|
+
|
|
89
|
+
# Import resources package
|
|
90
|
+
try:
|
|
91
|
+
import psforge.resources as resources_pkg
|
|
92
|
+
except ImportError as e:
|
|
93
|
+
logger.error(f"Failed to import resources package: {e}")
|
|
94
|
+
return registered_resources
|
|
95
|
+
|
|
96
|
+
# Get resources package path
|
|
97
|
+
resources_path = Path(resources_pkg.__file__).parent
|
|
98
|
+
|
|
99
|
+
# Iterate through all Python modules in resources/
|
|
100
|
+
for module_info in pkgutil.iter_modules([str(resources_path)]):
|
|
101
|
+
if module_info.name.startswith("_"):
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
module_name = f"psforge.resources.{module_info.name}"
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
module = importlib.import_module(module_name)
|
|
108
|
+
|
|
109
|
+
# Look for register() function
|
|
110
|
+
if hasattr(module, "register"):
|
|
111
|
+
logger.info(f"Registering resources from: {module_name}")
|
|
112
|
+
resource_uris = module.register(mcp)
|
|
113
|
+
registered_resources.extend(resource_uris)
|
|
114
|
+
else:
|
|
115
|
+
logger.warning(f"Module {module_name} has no register() function")
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error(f"Failed to register resources from {module_name}: {e}")
|
|
119
|
+
|
|
120
|
+
logger.info(f"Total resources registered: {len(registered_resources)}")
|
|
121
|
+
return registered_resources
|
psforge/server.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""PSForge MCP Server - Main entry point."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
|
|
8
|
+
from psforge.app import __app_name__, __version__
|
|
9
|
+
from psforge.registry import discover_and_register_resources, discover_and_register_tools
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def setup_logging():
|
|
13
|
+
"""Configure logging with loguru."""
|
|
14
|
+
logger.remove() # Remove default handler
|
|
15
|
+
|
|
16
|
+
# Add stderr handler for general logging
|
|
17
|
+
logger.add(
|
|
18
|
+
sys.stderr,
|
|
19
|
+
level="INFO",
|
|
20
|
+
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <level>{message}</level>",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Add file handler for detailed debug logs
|
|
24
|
+
logger.add(
|
|
25
|
+
"psforge_debug.log",
|
|
26
|
+
level="DEBUG",
|
|
27
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
|
28
|
+
rotation="10 MB",
|
|
29
|
+
retention="7 days",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run():
|
|
34
|
+
"""Entry point for the CLI command."""
|
|
35
|
+
setup_logging()
|
|
36
|
+
|
|
37
|
+
logger.info(f"Starting {__app_name__} v{__version__}")
|
|
38
|
+
logger.info("Photoshop MCP Server - AI-Powered Automation")
|
|
39
|
+
|
|
40
|
+
# Create MCP server using FastMCP
|
|
41
|
+
mcp = FastMCP(__app_name__)
|
|
42
|
+
|
|
43
|
+
# Discover and register all tools
|
|
44
|
+
logger.info("Discovering and registering tools...")
|
|
45
|
+
tool_names = discover_and_register_tools(mcp)
|
|
46
|
+
|
|
47
|
+
if tool_names:
|
|
48
|
+
logger.info(f"Registered {len(tool_names)} tools:")
|
|
49
|
+
for name in sorted(tool_names):
|
|
50
|
+
logger.info(f" - {name}")
|
|
51
|
+
else:
|
|
52
|
+
logger.warning("No tools were registered!")
|
|
53
|
+
|
|
54
|
+
# Discover and register all resources
|
|
55
|
+
logger.info("Discovering and registering resources...")
|
|
56
|
+
resource_uris = discover_and_register_resources(mcp)
|
|
57
|
+
|
|
58
|
+
if resource_uris:
|
|
59
|
+
logger.info(f"Registered {len(resource_uris)} resources:")
|
|
60
|
+
for uri in sorted(resource_uris):
|
|
61
|
+
logger.info(f" - {uri}")
|
|
62
|
+
else:
|
|
63
|
+
logger.info("No resources registered (this is OK)")
|
|
64
|
+
|
|
65
|
+
# Run the server
|
|
66
|
+
logger.info("PSForge MCP Server is ready")
|
|
67
|
+
logger.info("Waiting for connections...")
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
mcp.run(transport="stdio")
|
|
71
|
+
except KeyboardInterrupt:
|
|
72
|
+
logger.info("Server stopped by user")
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"Server error: {e}")
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
run()
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Action and script execution tools - play actions, run custom scripts."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from psforge.decorators import debug_tool, log_tool_call
|
|
8
|
+
from psforge.ps_adapter.application import PhotoshopApp
|
|
9
|
+
from psforge.ps_adapter.utils import js_escape_string
|
|
10
|
+
from psforge.registry import register_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(mcp) -> list[str]:
|
|
14
|
+
"""Register all action tools with MCP server.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
mcp: MCP server instance.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
List of registered tool names.
|
|
21
|
+
"""
|
|
22
|
+
registered_tools = []
|
|
23
|
+
|
|
24
|
+
@debug_tool
|
|
25
|
+
@log_tool_call
|
|
26
|
+
def play_action(action_name: str, action_set: str) -> dict[str, Any]:
|
|
27
|
+
"""Execute a Photoshop action from an action set.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
action_name: Name of the action to execute.
|
|
31
|
+
action_set: Name of the action set containing the action.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict: Operation result and context.
|
|
35
|
+
"""
|
|
36
|
+
ps_app = PhotoshopApp()
|
|
37
|
+
doc = ps_app.get_active_document()
|
|
38
|
+
|
|
39
|
+
if not doc:
|
|
40
|
+
return {
|
|
41
|
+
"success": False,
|
|
42
|
+
"error": "No active document. Some actions require an open document.",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Escape strings for JavaScript
|
|
47
|
+
action_name_escaped = js_escape_string(action_name)
|
|
48
|
+
action_set_escaped = js_escape_string(action_set)
|
|
49
|
+
|
|
50
|
+
# Execute action via JavaScript
|
|
51
|
+
action_script = f"""
|
|
52
|
+
(function() {{
|
|
53
|
+
try {{
|
|
54
|
+
app.doAction("{action_name_escaped}", "{action_set_escaped}");
|
|
55
|
+
return "Action executed successfully";
|
|
56
|
+
}} catch(e) {{
|
|
57
|
+
return "Error: " + e.toString();
|
|
58
|
+
}}
|
|
59
|
+
}})();
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
result = ps_app.execute_javascript(action_script)
|
|
63
|
+
|
|
64
|
+
if isinstance(result, str) and result.startswith("Error:"):
|
|
65
|
+
return {
|
|
66
|
+
"success": False,
|
|
67
|
+
"error": result,
|
|
68
|
+
"action_name": action_name,
|
|
69
|
+
"action_set": action_set,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"success": True,
|
|
74
|
+
"message": f"Executed action '{action_name}' from set '{action_set}'",
|
|
75
|
+
"action_name": action_name,
|
|
76
|
+
"action_set": action_set,
|
|
77
|
+
"result": result,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(f"Failed to execute action: {e}")
|
|
82
|
+
return {
|
|
83
|
+
"success": False,
|
|
84
|
+
"error": str(e),
|
|
85
|
+
"action_name": action_name,
|
|
86
|
+
"action_set": action_set,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@debug_tool
|
|
90
|
+
@log_tool_call
|
|
91
|
+
def execute_script(script: str) -> dict[str, Any]:
|
|
92
|
+
"""Execute arbitrary ExtendScript/JavaScript code in Photoshop.
|
|
93
|
+
|
|
94
|
+
This is a powerful tool that allows executing any valid Photoshop ExtendScript.
|
|
95
|
+
Use with caution - invalid scripts can cause errors or unexpected behavior.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
script: JavaScript/ExtendScript code to execute in Photoshop.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
dict: Execution result and context.
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
- Simple operation: "app.activeDocument.activeLayer.opacity = 50;"
|
|
105
|
+
- Get info: "app.activeDocument.layers.length"
|
|
106
|
+
- Complex: "(function() { var layer = app.activeDocument.activeLayer; layer.rotate(45); return 'Rotated'; })()"
|
|
107
|
+
"""
|
|
108
|
+
ps_app = PhotoshopApp()
|
|
109
|
+
|
|
110
|
+
if not script or not script.strip():
|
|
111
|
+
return {
|
|
112
|
+
"success": False,
|
|
113
|
+
"error": "Script cannot be empty",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
logger.info(f"Executing custom script (length: {len(script)} chars)")
|
|
118
|
+
logger.debug(f"Script content:\n{script}")
|
|
119
|
+
|
|
120
|
+
# Execute the script
|
|
121
|
+
result = ps_app.execute_javascript(script)
|
|
122
|
+
|
|
123
|
+
# Convert result to string if it's not already
|
|
124
|
+
result_str = str(result) if result is not None else "undefined"
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"success": True,
|
|
128
|
+
"message": "Script executed successfully",
|
|
129
|
+
"result": result_str,
|
|
130
|
+
"script_length": len(script),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"Script execution failed: {e}")
|
|
135
|
+
|
|
136
|
+
# Try to extract useful error info
|
|
137
|
+
error_msg = str(e)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
"success": False,
|
|
141
|
+
"error": error_msg,
|
|
142
|
+
"script_preview": script[:200] + ("..." if len(script) > 200 else ""),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Register all tools
|
|
146
|
+
registered_tools.append(register_tool(mcp, play_action, "play_action"))
|
|
147
|
+
registered_tools.append(register_tool(mcp, execute_script, "execute_script"))
|
|
148
|
+
|
|
149
|
+
return registered_tools
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Adjustment tools - brightness/contrast, hue/saturation, auto-levels, auto-contrast, desaturate, invert."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from psforge.decorators import debug_tool, log_tool_call
|
|
8
|
+
from psforge.ps_adapter.application import PhotoshopApp
|
|
9
|
+
from psforge.ps_adapter.utils import validate_numeric_range
|
|
10
|
+
from psforge.registry import register_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(mcp) -> list[str]:
|
|
14
|
+
"""Register all adjustment tools with MCP server.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
mcp: MCP server instance.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
List of registered tool names.
|
|
21
|
+
"""
|
|
22
|
+
registered_tools = []
|
|
23
|
+
|
|
24
|
+
@debug_tool
|
|
25
|
+
@log_tool_call
|
|
26
|
+
def adjust_brightness_contrast(brightness: int, contrast: int) -> dict[str, Any]:
|
|
27
|
+
"""Adjust brightness and contrast of the currently active layer.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
brightness: Brightness adjustment (-150 to 150, 0 = no change).
|
|
31
|
+
contrast: Contrast adjustment (-50 to 100, 0 = no change).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict: Operation result and context.
|
|
35
|
+
"""
|
|
36
|
+
validate_numeric_range(brightness, -150, 150, "brightness")
|
|
37
|
+
validate_numeric_range(contrast, -50, 100, "contrast")
|
|
38
|
+
|
|
39
|
+
ps_app = PhotoshopApp()
|
|
40
|
+
doc = ps_app.get_active_document()
|
|
41
|
+
|
|
42
|
+
if not doc:
|
|
43
|
+
return {
|
|
44
|
+
"success": False,
|
|
45
|
+
"error": "No active document",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
adjust_script = f"""
|
|
50
|
+
var layer = app.activeDocument.activeLayer;
|
|
51
|
+
var layerName = layer.name;
|
|
52
|
+
|
|
53
|
+
// Apply Brightness/Contrast
|
|
54
|
+
layer.adjustBrightnessContrast({brightness}, {contrast});
|
|
55
|
+
|
|
56
|
+
layerName;
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
layer_name = ps_app.execute_javascript(adjust_script)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
"success": True,
|
|
63
|
+
"message": f"Adjusted brightness ({brightness:+d}) and contrast ({contrast:+d}) on layer '{layer_name}'",
|
|
64
|
+
"layer_name": layer_name,
|
|
65
|
+
"brightness": brightness,
|
|
66
|
+
"contrast": contrast,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.error(f"Failed to adjust brightness/contrast: {e}")
|
|
71
|
+
return {
|
|
72
|
+
"success": False,
|
|
73
|
+
"error": str(e),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@debug_tool
|
|
77
|
+
@log_tool_call
|
|
78
|
+
def adjust_hue_saturation(hue: int, saturation: int, lightness: int) -> dict[str, Any]:
|
|
79
|
+
"""Adjust hue, saturation, and lightness of the currently active layer.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
hue: Hue shift in degrees (-180 to 180, 0 = no change).
|
|
83
|
+
saturation: Saturation adjustment (-100 to 100, 0 = no change).
|
|
84
|
+
lightness: Lightness adjustment (-100 to 100, 0 = no change).
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
dict: Operation result and context.
|
|
88
|
+
"""
|
|
89
|
+
validate_numeric_range(hue, -180, 180, "hue")
|
|
90
|
+
validate_numeric_range(saturation, -100, 100, "saturation")
|
|
91
|
+
validate_numeric_range(lightness, -100, 100, "lightness")
|
|
92
|
+
|
|
93
|
+
ps_app = PhotoshopApp()
|
|
94
|
+
doc = ps_app.get_active_document()
|
|
95
|
+
|
|
96
|
+
if not doc:
|
|
97
|
+
return {
|
|
98
|
+
"success": False,
|
|
99
|
+
"error": "No active document",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
adjust_script = f"""
|
|
104
|
+
var layer = app.activeDocument.activeLayer;
|
|
105
|
+
var layerName = layer.name;
|
|
106
|
+
|
|
107
|
+
// Apply Hue/Saturation
|
|
108
|
+
layer.adjustColorBalance([0, 0, 0], [0, 0, 0], [0, 0, 0], true);
|
|
109
|
+
layer.adjustHueSaturation({hue}, {saturation}, {lightness});
|
|
110
|
+
|
|
111
|
+
layerName;
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
layer_name = ps_app.execute_javascript(adjust_script)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"success": True,
|
|
118
|
+
"message": f"Adjusted hue/saturation/lightness on layer '{layer_name}'",
|
|
119
|
+
"layer_name": layer_name,
|
|
120
|
+
"hue": hue,
|
|
121
|
+
"saturation": saturation,
|
|
122
|
+
"lightness": lightness,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.error(f"Failed to adjust hue/saturation: {e}")
|
|
127
|
+
return {
|
|
128
|
+
"success": False,
|
|
129
|
+
"error": str(e),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@debug_tool
|
|
133
|
+
@log_tool_call
|
|
134
|
+
def auto_levels() -> dict[str, Any]:
|
|
135
|
+
"""Apply Auto Levels adjustment to the currently active layer.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
dict: Operation result and context.
|
|
139
|
+
"""
|
|
140
|
+
ps_app = PhotoshopApp()
|
|
141
|
+
doc = ps_app.get_active_document()
|
|
142
|
+
|
|
143
|
+
if not doc:
|
|
144
|
+
return {
|
|
145
|
+
"success": False,
|
|
146
|
+
"error": "No active document",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
auto_levels_script = """
|
|
151
|
+
var layer = app.activeDocument.activeLayer;
|
|
152
|
+
var layerName = layer.name;
|
|
153
|
+
|
|
154
|
+
// Apply Auto Levels
|
|
155
|
+
layer.autoLevels();
|
|
156
|
+
|
|
157
|
+
layerName;
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
layer_name = ps_app.execute_javascript(auto_levels_script)
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
"success": True,
|
|
164
|
+
"message": f"Applied Auto Levels to layer '{layer_name}'",
|
|
165
|
+
"layer_name": layer_name,
|
|
166
|
+
"adjustment": "Auto Levels",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Failed to apply Auto Levels: {e}")
|
|
171
|
+
return {
|
|
172
|
+
"success": False,
|
|
173
|
+
"error": str(e),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@debug_tool
|
|
177
|
+
@log_tool_call
|
|
178
|
+
def auto_contrast() -> dict[str, Any]:
|
|
179
|
+
"""Apply Auto Contrast adjustment to the currently active layer.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
dict: Operation result and context.
|
|
183
|
+
"""
|
|
184
|
+
ps_app = PhotoshopApp()
|
|
185
|
+
doc = ps_app.get_active_document()
|
|
186
|
+
|
|
187
|
+
if not doc:
|
|
188
|
+
return {
|
|
189
|
+
"success": False,
|
|
190
|
+
"error": "No active document",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
auto_contrast_script = """
|
|
195
|
+
var layer = app.activeDocument.activeLayer;
|
|
196
|
+
var layerName = layer.name;
|
|
197
|
+
|
|
198
|
+
// Apply Auto Contrast
|
|
199
|
+
layer.autoContrast();
|
|
200
|
+
|
|
201
|
+
layerName;
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
layer_name = ps_app.execute_javascript(auto_contrast_script)
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"success": True,
|
|
208
|
+
"message": f"Applied Auto Contrast to layer '{layer_name}'",
|
|
209
|
+
"layer_name": layer_name,
|
|
210
|
+
"adjustment": "Auto Contrast",
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.error(f"Failed to apply Auto Contrast: {e}")
|
|
215
|
+
return {
|
|
216
|
+
"success": False,
|
|
217
|
+
"error": str(e),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@debug_tool
|
|
221
|
+
@log_tool_call
|
|
222
|
+
def desaturate() -> dict[str, Any]:
|
|
223
|
+
"""Desaturate (convert to grayscale) the currently active layer.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
dict: Operation result and context.
|
|
227
|
+
"""
|
|
228
|
+
ps_app = PhotoshopApp()
|
|
229
|
+
doc = ps_app.get_active_document()
|
|
230
|
+
|
|
231
|
+
if not doc:
|
|
232
|
+
return {
|
|
233
|
+
"success": False,
|
|
234
|
+
"error": "No active document",
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
desaturate_script = """
|
|
239
|
+
var layer = app.activeDocument.activeLayer;
|
|
240
|
+
var layerName = layer.name;
|
|
241
|
+
|
|
242
|
+
// Desaturate
|
|
243
|
+
layer.desaturate();
|
|
244
|
+
|
|
245
|
+
layerName;
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
layer_name = ps_app.execute_javascript(desaturate_script)
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"success": True,
|
|
252
|
+
"message": f"Desaturated layer '{layer_name}'",
|
|
253
|
+
"layer_name": layer_name,
|
|
254
|
+
"adjustment": "Desaturate",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
logger.error(f"Failed to desaturate: {e}")
|
|
259
|
+
return {
|
|
260
|
+
"success": False,
|
|
261
|
+
"error": str(e),
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@debug_tool
|
|
265
|
+
@log_tool_call
|
|
266
|
+
def invert() -> dict[str, Any]:
|
|
267
|
+
"""Invert colors of the currently active layer.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
dict: Operation result and context.
|
|
271
|
+
"""
|
|
272
|
+
ps_app = PhotoshopApp()
|
|
273
|
+
doc = ps_app.get_active_document()
|
|
274
|
+
|
|
275
|
+
if not doc:
|
|
276
|
+
return {
|
|
277
|
+
"success": False,
|
|
278
|
+
"error": "No active document",
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
invert_script = """
|
|
283
|
+
var layer = app.activeDocument.activeLayer;
|
|
284
|
+
var layerName = layer.name;
|
|
285
|
+
|
|
286
|
+
// Invert
|
|
287
|
+
layer.invert();
|
|
288
|
+
|
|
289
|
+
layerName;
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
layer_name = ps_app.execute_javascript(invert_script)
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
"success": True,
|
|
296
|
+
"message": f"Inverted colors of layer '{layer_name}'",
|
|
297
|
+
"layer_name": layer_name,
|
|
298
|
+
"adjustment": "Invert",
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.error(f"Failed to invert: {e}")
|
|
303
|
+
return {
|
|
304
|
+
"success": False,
|
|
305
|
+
"error": str(e),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Register all tools
|
|
309
|
+
registered_tools.append(register_tool(mcp, adjust_brightness_contrast, "adjust_brightness_contrast"))
|
|
310
|
+
registered_tools.append(register_tool(mcp, adjust_hue_saturation, "adjust_hue_saturation"))
|
|
311
|
+
registered_tools.append(register_tool(mcp, auto_levels, "auto_levels"))
|
|
312
|
+
registered_tools.append(register_tool(mcp, auto_contrast, "auto_contrast"))
|
|
313
|
+
registered_tools.append(register_tool(mcp, desaturate, "desaturate"))
|
|
314
|
+
registered_tools.append(register_tool(mcp, invert, "invert"))
|
|
315
|
+
|
|
316
|
+
return registered_tools
|