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
48 changes: 17 additions & 31 deletions mcp/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "psmcp"
version = "0.85.1"
name = "ps-mcp"
version = "0.85.5"
description = "Adobe Photoshop automation using MCP"
requires-python = ">=3.10"
license = "MIT"
Expand All @@ -17,35 +13,25 @@ dependencies = [
"mcp[cli]",
"requests",
"websocket-client>=1.8.0",
"pillow>=11.2.1",
]

[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"black",
"isort",
"mypy",
]
[project.scripts]
ps-mcp = "ps_mcp:main"

[tool.setuptools]
py-modules = ["fonts", "logger", "psmcp", "socket_client"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'
[tool.uv]
dev-dependencies = [
"freezegun>=1.5.1",
"pyright>=1.1.389",
"pytest>=8.3.3",
"ruff>=0.8.1",
]

[tool.isort]
profile = "black"
line_length = 88
[tool.setuptools]
py-modules = ["fonts", "logger", "socket_client"]

[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
7 changes: 7 additions & 0 deletions mcp/src/ps_mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from ps_mcp.psmcp import mcp

def main():
mcp.run()

if __name__ == "__main__":
main()
3 changes: 3 additions & 0 deletions mcp/src/ps_mcp/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ps_mcp import main

main()
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
73 changes: 57 additions & 16 deletions mcp/ps-mcp.py → mcp/src/ps_mcp/psmcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,28 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from mcp.server.fastmcp import FastMCP
import requests
import json
import time
import socket_client
import logger
from mcp.server.fastmcp import FastMCP, Image
from . import socket_client
from . import logger
import sys
import os
from io import BytesIO
try:
from PIL import Image as PILImage
except ImportError:
raise ImportError("Please install the `pillow` library to run this example.")


logger.log(f"Python path: {sys.executable}")
logger.log(f"PYTHONPATH: {os.environ.get('PYTHONPATH')}")
logger.log(f"Current working directory: {os.getcwd()}")
logger.log(f"Sys.path: {sys.path}")
#logger.log(f"Python path: {sys.executable}")
#logger.log(f"PYTHONPATH: {os.environ.get('PYTHONPATH')}")
#logger.log(f"Current working directory: {os.getcwd()}")
#logger.log(f"Sys.path: {sys.path}")


# Create an MCP server
mcp = FastMCP("Adobe Photoshop", log_level="ERROR")
mcp_name = "Adobe Photoshop MCP Server"
mcp = FastMCP(mcp_name, log_level="ERROR")
print(f"{mcp_name} running on stdio", file=sys.stderr)

APPLICATION = "photoshop"
PROXY_URL = 'http://localhost:3001'
Expand All @@ -49,6 +53,43 @@
timeout=PROXY_TIMEOUT
)

@mcp.resource(
uri="image://{document_id}/{layer_id}/{size}",
description="Returns the png image for the layer. Arguments are document_id, layer_id, and size. If size is provided, the image will be resized to the specified size.",
mime_type="image/png"
)
def get_layer_rendition(document_id: str, layer_id: str, size: int) -> bytes:
"""Returns the png image for the layer with the specified id.
"""

command = createCommand("getLayerImageData", {"layerID": layer_id, "documentID": document_id, "size": size})
response = sendCommand(command)
res = response["response"]

# Check if imageBounds is present and valid
if "imageSize" not in res:
return bytes()

width = res["imageSize"]["width"]
height = res["imageSize"]["height"]

image = PILImage.frombytes("RGBA",
(width, height),
res["imageData"])

# ratio = width / height
# if size != 0 and size < width and size < height:
# image = image.resize((int(size), int(size / ratio)))
# elif size != 0 and size < width:
# image = image.resize((int(size), int(size * ratio)))
# elif size != 0 and size < height:
# image = image.resize((int(size * ratio), int(size)))

buffer = BytesIO()
image.save(buffer, format="PNG")
# logger.log(f"get_layer_rendition22: {layer_id} {width} {height} {len(res['imageData'])}")
return buffer.getvalue()

@mcp.tool()
def create_gradient_layer_style(
layer_name: str,
Expand Down Expand Up @@ -208,7 +249,7 @@ def get_layers() -> list:

Returns:
list: A nested list of dictionaries containing layer information and hierarchy.
Each dict has at minimum a 'name' key with the layer name.
Each dict has at minimum a 'name' and 'id' key with the layer name and id.
If a layer has sublayers, they will be contained in a 'layers' key which contains another list of layer dicts.
Example: [{'name': 'Group 1', 'layers': [{'name': 'Layer 1'}, {'name': 'Layer 2'}]}, {'name': 'Background'}]
"""
Expand All @@ -217,7 +258,6 @@ def get_layers() -> list:

return sendCommand(command)


@mcp.tool()
def place_image(
layer_name: str,
Expand Down Expand Up @@ -1115,11 +1155,12 @@ def add_brightness_contrast_adjustment_layer(
brightness:int = 0,
contrast:int = 0):
"""Adds an adjustment layer to the layer with the specified name to adjust brightness and contrast
@ComposerAIUI Level

Args:
layer_name (str): The name of the layer to apply the brightness and contrast adjustment layer
brightness (int): The brightness value (-150 to 150)
contrasts (int): The contrast value (-50 to 100)
brightness (int): The brightness value (-150 to 150) @ComposerAIUI Slider
contrasts (int): The contrast value (-50 to 100) @ComposerAIUI Slider
"""

command = createCommand("addBrightnessContrastAdjustmentLayer", {
Expand Down Expand Up @@ -1347,7 +1388,7 @@ def createCommand(action:str, options:dict) -> str:

return command

from fonts import list_all_fonts_postscript
from .fonts import list_all_fonts_postscript
font_names = list_all_fonts_postscript()

interpolation_methods = [
Expand Down
13 changes: 8 additions & 5 deletions mcp/socket_client.py → mcp/src/ps_mcp/socket_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@
# SOFTWARE.

import socketio
import time
import threading
import json
from queue import Queue
import logger
from . import logger

# Global configuration variables
proxy_url = None
Expand Down Expand Up @@ -77,7 +76,11 @@ def connect():

@sio.event
def packet_response(data):
logger.log(f"Received response: {data}")
# Limit data output to 200 characters
data_str = str(data)
if len(data_str) > 500:
data_str = data_str[:497] + "..."
logger.log(f"Received response: {data_str}")
response_queue.put(data)
# Disconnect after receiving the response
sio.disconnect()
Expand Down Expand Up @@ -123,11 +126,11 @@ def connect_and_wait():
raise RuntimeError(f"Error: Could not connect to {application} command proxy server. Make sure that the proxy server is running listening on the correct url {proxy_url}.")

if response:
logger.log("response received...")
logger.log(f"response received... {type(response)}")
try:
logger.log(json.dumps(response))
except:
logger.log(f"Response (not JSON-serializable): {response}")
logger.log(f"Response (not JSON-serializable): {len(response)}")

if response["status"] == "FAILURE":
raise AppError(f"Error returned from {application}: {response['message']}")
Expand Down
Loading