This is page 1 of 5. Use http://codebase.md/mikechambers/adb-mcp?page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .gitignore ├── adb-proxy-socket │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── proxy.js │ └── README.md ├── cep │ ├── com.mikechambers.ae │ │ ├── .debug │ │ ├── commands.js │ │ ├── CSXS │ │ │ └── manifest.xml │ │ ├── index.html │ │ ├── jsx │ │ │ └── json-polyfill.jsx │ │ ├── lib │ │ │ └── CSInterface.js │ │ ├── main.js │ │ └── style.css │ └── com.mikechambers.ai │ ├── .debug │ ├── commands.js │ ├── CSXS │ │ └── manifest.xml │ ├── index.html │ ├── jsx │ │ ├── json-polyfill.jsx │ │ └── utils.jsx │ ├── lib │ │ └── CSInterface.js │ ├── main.js │ └── style.css ├── dxt │ ├── build │ ├── pr │ │ └── manifest.json │ └── ps │ └── manifest.json ├── images │ └── claud-attach-mcp.png ├── LICENSE.md ├── mcp │ ├── .gitignore │ ├── ae-mcp.py │ ├── ai-mcp.py │ ├── core.py │ ├── fonts.py │ ├── id-mcp.py │ ├── logger.py │ ├── pr-mcp.py │ ├── ps-batch-play.py │ ├── ps-mcp.py │ ├── pyproject.toml │ ├── requirements.txt │ ├── socket_client.py │ └── uv.lock ├── package-lock.json ├── README.md └── uxp ├── id │ ├── commands │ │ └── index.js │ ├── icons │ │ ├── [email protected] │ │ ├── [email protected] │ │ ├── [email protected] │ │ └── [email protected] │ ├── index.html │ ├── LICENSE │ ├── main.js │ ├── manifest.json │ ├── package.json │ ├── socket.io.js │ └── style.css ├── pr │ ├── commands │ │ ├── consts.js │ │ ├── core.js │ │ ├── index.js │ │ └── utils.js │ ├── icons │ │ ├── [email protected] │ │ ├── [email protected] │ │ ├── [email protected] │ │ └── [email protected] │ ├── index.html │ ├── LICENSE │ ├── main.js │ ├── manifest.json │ ├── package.json │ ├── socket.io.js │ └── style.css └── ps ├── commands │ ├── adjustment_layers.js │ ├── core.js │ ├── filters.js │ ├── index.js │ ├── layer_styles.js │ ├── layers.js │ ├── selection.js │ └── utils.js ├── icons │ ├── [email protected] │ ├── [email protected] │ ├── [email protected] │ └── [email protected] ├── index.html ├── LICENSE ├── main.js ├── manifest.json ├── package.json ├── socket.io.js └── style.css ``` # Files -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- ``` # Auto detect text files and perform LF normalization * text=auto ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/.debug: -------------------------------------------------------------------------------- ``` <?xml version="1.0" encoding="UTF-8"?> <ExtensionList> <Extension Id="com.mikechambers.ae.mcp"> <HostList> <Host Name="AEFT" Port="8088"/> </Host> </Extension> </ExtensionList> ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/.debug: -------------------------------------------------------------------------------- ``` <?xml version="1.0" encoding="UTF-8"?> <ExtensionList> <Extension Id="com.mikechambers.ae.mcp"> <HostList> <Host Name="ILST" Port="8089"/> </Host> </Extension> </ExtensionList> ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` *.dxt *.ccx dist/ .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ``` -------------------------------------------------------------------------------- /adb-proxy-socket/.gitignore: -------------------------------------------------------------------------------- ``` # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # vitepress build output **/.vitepress/dist # vitepress cache directory **/.vitepress/cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* ``` -------------------------------------------------------------------------------- /mcp/.gitignore: -------------------------------------------------------------------------------- ``` # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ ``` -------------------------------------------------------------------------------- /adb-proxy-socket/README.md: -------------------------------------------------------------------------------- ```markdown ### Package In order to package executables: ``` npm install -g pkg pkg . ``` ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # adb-mcp adb-mcp is a proof of concept project to enabled AI control of Adobe tools (Adobe Photoshop and Adobe Premiere) by providing an interface to LLMs via the MCP protocol. The project is not endorsed by nor supported by Adobe. It has been tested with Claude desktop (Mac and Windows) from Anthropic, as well as the OpenAI Agent SDK, and allows AI clients to control Adobe Photoshop and Adobe Premiere. Theoretically, it should work with any AI App / LLM that supports the MCP protocol, and is built in a way to support multiple Adobe applications. Example use cases include: - Giving Claude step by step instruction on what to do in Photoshop, providing a conversational based interface (particularly useful if you are new to Photoshop). - Giving Claude a task (create an instagram post that looks like a Polariod image, create a double exposure) and letting it create it from start to finish to use as a template. - Asking Claude to generate custom Photoshop tutorials for you, by creating an example file, then step by step instructions on how to recreate. - As a Photoshop utility tool (have Claude rename all of your layers into a consistent format) - Have Claude create new Premiere projects pre-populations with clips, transitions, effects and Audio [View Video Examples](https://www.youtube.com/playlist?list=PLrZcuHfRluqt5JQiKzMWefUb0Xumb7MkI) The Premiere agent is a bit more limited in functionality compared to the Photoshop agent, due to current limitations of the Premiere plugin API. ## How it works The proof of concept works by providing: - A MCP Server that provides an interface to functionality within Adobe Photoshop to the AI / LLM - A Node based command proxy server that sits between the MCP server and Adobe app plugins - An Adobe app (Photoshop and Premiere) plugin that listens for commands, and drives the programs **AI** <-> **MCP Server** <-> **Command Proxy Server** <-> **Photoshop / Premiere UXP Plugin** <-> **Photoshop / Premiere** The proxy server is required because the public facing API for UXP Based JavaScript plugin does not allow it to listen on a socket connection (as a server) for the MCP Server to connect to (it can only connect to a socket as a client). ## Requirements In order to run this, the following is required: - AI LLM with support for MCP Protocol (tested with Claude desktop on Mac & Windows, and OpenAI Agent SDK) - Python 3, which is used to run the MCP server provided with this project - NodeJS, used to provide a proxy between the MCP server and Photoshop - Adobe UXP Developer tool (available via Creative Cloud) used to install and debug the Photoshop / Premiere plugin used to connect to the proxy - Adobe Photoshop (26.0 or greater) with the MCP Plugin installed or Adobe Premiere Beta (25.3 Build 46 or greater) ## Installation This guide assumes you're using Claude Desktop. Other MCP-compatible AI applications should work similarly. ### Download Source Code Clone or download the source code from the [main project page](https://github.com/mikechambers/adb-mcp). ### Install Claude Desktop 1. Download and install [Claude Desktop](https://claude.ai/download) 2. Launch Claude Desktop to verify it works Note, you can use any client / code that supports MCP, just follow its instructions for how to configure. ### Install MCP for Development Navigate to the project directory and run: #### Photoshop ```bash uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with numpy ps-mcp.py ``` #### Premiere Pro ```bash uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with pillow pr-mcp.py ``` #### InDesign ```bash uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with pillow id-mcp.py ``` #### AfterEffects ```bash uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with pillow ae-mcp.py ``` #### Illustrator ```bash uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with pillow ai-mcp.py ``` Restart Claude Desktop after installation. ### Set Up Proxy Server #### Using Prebuilt Executables (Recommended) 1. Download the appropriate executable for your platform from the latest [release](https://github.com/mikechambers/adb-mcp/releases) (files named like `adb-proxy-socket-macos-x64.zip` (Intel), `adb-proxy-socket-macos-arm64.zip` (Silicon), or `adb-proxy-socket-win-x64.exe.zip`). 2. Unzip the executable. 3. Double click or run from the console / terminal #### Running from Source 1. Navigate to the adb-proxy-socket directory 2. Run `node proxy.js` You should see a message like: `Photoshop MCP Command proxy server running on ws://localhost:3001` **Keep this running** — the proxy server must stay active for Claude to communicate with Adobe plugins. ### Install Plugins #### Photoshop, Premiere Pro, InDesign (UXP) 1. Launch **UXP Developer Tools** from Creative Cloud 2. Enable developer mode when prompted 3. Select **File > Add Plugin** 4. Navigate to the appropriate directory and select **manifest.json**: - **Photoshop**: `uxp/ps/manifest.json` - **Premiere Pro**: `uxp/pr/manifest.json` - **InDesign**: `uxp/id/manifest.json` 5. Click **Load** 6. In your Adobe application, open the plugin panel and click **Connect** ##### Enable Developer Mode in Photoshop **For Photoshop:** 1. Launch Photoshop (2025/26.0 or greater) 2. Go to **Settings > Plugins** and check **"Enable Developer Mode"** 3. Restart Photoshop #### AfterEffects, Illustrator (CEP) ##### Mac 1. Make sure the following directory exists (if it doesn't then create the directories) `/Users/USERNAME/Library/Application Support/Adobe/CEP/extensions` 2. Navigate to the extensions directory and create a symlink that points to the AfterEffect / Illustrator plugin in the CEP directory. ```bash cd /Users/USERNAME/Library/Application Support/Adobe/CEP/extensions ln -s /Users/USERNAME/src/adb-mcp/cep/com.mikechambers.ae com.mikechambers.ae ``` or ```bash cd /Users/USERNAME/Library/Application Support/Adobe/CEP/extensions ln -s /Users/USERNAME/src/adb-mcp/cep/com.mikechambers.ai com.mikechambers.ai ``` ##### Windows 1. Make sure the following directory exists (if it doesn't then create the directories) `C:\Users\USERNAME\AppData\Roaming\Adobe\CEP\extensions` 2. Open Command Prompt as Administrator (or enable Developer Mode in Windows Settings) 3. Create a junction or symbolic link that points to the AfterEffect / Illustrator plugin in the CEP directory: ```cmd mklink /D "C:\Users\USERNAME\AppData\Roaming\Adobe\CEP\extensions\com.mikechambers.ae" "C:\Users\USERNAME\src\adb-mcp\cep\com.mikechambers.ae" ``` or ```cmd mklink /D "C:\Users\USERNAME\AppData\Roaming\Adobe\CEP\extensions\com.mikechambers.ai" "C:\Users\USERNAME\src\adb-mcp\cep\com.mikechambers.ai" ``` Note if you don't want to symlink, you can copy com.mikechambers.ae / com.mikechambers.ao into the CEP directory. ### Using Claude with Adobe Apps Launch the following: 1. Claude Desktop 2. adb-proxy-socket node server 3. Launch Photoshop, Premiere, InDesign, AfterEffects, Illustrator _TIP: Create a project for Photoshop / Premiere Pro in Claude and pre-load any app specific instructions in its Project knowledge._ #### Photoshop 1. Launch UXP Developer Tool and click the Load button for _Photoshop MCP Agent_ 2. In Photoshop, if the MCP Agent panel is not open, open _Plugins > Photoshop MCP Agent > Photoshop MCP Agent_ 3. Click connect in the agent panel in Photoshop Now you can switch over the Claude desktop. Before you start a session, you should load the instructions resource which will provide guidance and info the Claude by clicking the socket icon (Attach from MCP) and then _Choose an Integration_ > _Adobe Photoshop_ > _config://get_instructions_. #### Premiere 1. Launch UXP Developer Tool and click the Load button for _Premiere MCP Agent_ 2. In Premiere, if the MCP Agent panel is not open, open _Window > UXP Plugins > Premiere MCP Agent > Premiere MCP Agent_ 3. Click connect in the agent panel in Photoshop #### InDesign 1. Launch UXP Developer Tool and click the Load button for InDesitn MCP Agent_ 2. In InDesign, if the MCP Agent panel is not open, open _Plugins > InDesign MCP Agent > InDesign MCP Agent_ 3. Click connect in the agent panel in Photoshop #### AfterEffects 1. _Window > Extensions > Illustrator MCP Agent_ #### Illustrator 1. Open a file (the plugin won't launch unless a file is open) 2. _Window > Extensions > Illustrator MCP Agent_ Note, you must reload the plugin via the UXP Developer app every time you restart Photoshop, Premiere and InDesign. ### Setting up session In the chat input field, click the "+" button. From there click "Add from Adobe Photoshop / Premiere" then select *config://get_instructions*. This will load the instructions into the prompt. Submit that to Claude and once it processes it, you are ready to go. <img src="images/claud-attach-mcp.png" width="300"> This will help reduce errors when the AI is using the app. ### Prompting At anytime, you can ask the following: ``` Can you list what apis / functions are available for working with Photoshop / Premiere? ``` and it will list out all of the functionality available. When prompting, you do not need to reference the APIs, just use natural language to give instructions. For example: ``` Create a new Photoshop file with a blue background, that is 1080 width by 720 height at 300 dpi ``` ``` Create a new Photoshop file for an instagram post ``` ``` Create a double exposure image in Photoshop of a woman and a forest ``` ``` Generate an image of a forest, and then add a clipping mask to only show the center in a circle ``` ``` Make something cool with photoshop ``` ``` Add cross fade transitions between all of the clips on the timeline in Premiere ``` ### Tips #### General * When asking AI to view the content in Photoshop / Premiere Pro, you can see the image returned in the Tool Call item in the chat. It will appear once the entire response has been added to the chat. * When prompting, ask the AI to think about and check its work. * The more you guide it (i.e. "consider using clipping masks") the better the results * The more advanced the model, or the more resources given to the model the better and more creative the AI is. * As a general rule, don't make changes in the Adobe Apps while the AI is doing work. If you do make changes, make sure to tell the AI about it. * The AI will learn from its mistakes, but will lose its memory once you start a new chat. You can guide it to do things in a different way, and then ask it to start over and it should follow the new approach. The AI currently has access to a subset of Photoshop / Premiere / InDesign / Illustrator / AfterEffects functionality. In general, the approach has been to provide lower level tools to give the AI the basics to do more complex stuff. Note, for AfterEffects and Illustrator, there is a low level Extend Script API that will let the LLM run any arbitrary extend script (which allows it to do just about anything). The Photoshop plugin has more functionality that Premiere. By default, the AI cannot access files directly, although if you install the [Claude File System MCP server](https://www.claudemcp.com/servers/filesystem) it can access, and load files into Photoshop / Premiere (open files and embed images). #### Photoshop * You can ask the AI to look at the content of the Photoshop file and it should be able to then see the output. * The AI currently has issue sizing and positioning text correctly, so giving it guidelines on font sizes to use will help, as well as telling it to align the text relative to the canvas. * The AI has access to all of the Postscript fonts on the system. If you want to specify a font, you must use its Postscript name (you may be able to ask the AI for it). * You can ask the AI for suggestions. It comes up with really useful ideas / feedback sometimes. #### Premiere * Currently the plugin assumes you are just working with a single sequence. * Pair the Premiere Pro MCP with the [media-utils-mcp](https://github.com/mikechambers/media-utils-mcp) to expand functionality. ### Troubleshooting #### MCP won't run in Claude If you get an error when running Claude that the MCP is not working, you may need to edit your Claude config file and put an absolute path for the UV command. More info [here](https://github.com/mikechambers/adb-mcp/issues/5#issuecomment-2829817624). #### All fonts not available The MCP server will return a list of available fonts, but depending on the number of fonts installed on your system, may omit some to work around the amount of data that can be send to the AI. By default it will list the first 1000 fonts sorted in alphabetical order. You can tell the AI to use a specific font, using its postscript name. #### Plugin won't install or connect * Make sure the app is running before you try to load the plugin. * In the UXP developer tool click the debug button next to load, and see if there are any errors. * Make sure the node / proxy server is running. If you plugin connects you should see output similar to: ``` adb-mcp Command proxy server running on ws://localhost:3001 User connected: Ud6L4CjMWGAeofYAAAAB Client Ud6L4CjMWGAeofYAAAAB registered for application: photoshop ``` * When you press the connect button, if it still says "Connect" it means there was either an error, or it can't connect to the proxy server. You can view the error in the UXP Developer App, by opening the Developer Workspace and click "Debug". #### Errors within AI client * If something fails on the AI side, it will usually tell you the issue. If you click the command / code box, you can see the error. * The first thing to check if there is an issue is to make sure the plugin in Photoshop / Premiere is connected, and that the node proxy server is running. * If response times get really slow, check if the AI servers are under load, and that you do not have too much text in the current conversation (restarting a new chat can sometimes help speed up, but you will lose the context). If you continue to have issues post an [issue](https://github.com/mikechambers/adb-mcp/issuesrd.gg/fgxw9t37D7). Include as much information as you can (OS, App, App version, and debug info or errors). ## Development Adding new functionality is relatively easy, and requires: 1. Adding the API and parameters in the *mcp/ps-mcp.py* / *mcp/pr-mcp.py* file (which is used by the AI) 2. Implementing the API in the *uxp/ps/commands/index.js* / *uxp/pr/commands/index.js* file. This [thread](https://github.com/mikechambers/adb-mcp/issues/10#issuecomment-3191698528) has some info on how to add functionality. ## Questions, Feature Requests, Feedback If you have any questions, feature requests, need help, or just want to chat, join the [discord](https://discord.gg/fgxw9t37D7). You can also log bugs and feature requests on the [issues page](https://github.com/mikechambers/adb-mcp/issues). ## License Project released under a [MIT License](LICENSE.md). [](LICENSE.md) ``` -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- ```markdown MIT License Copyright (c) 2025 Mike Chambers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -------------------------------------------------------------------------------- /mcp/requirements.txt: -------------------------------------------------------------------------------- ``` fonttools python-socketio mcp requests websocket-client ``` -------------------------------------------------------------------------------- /uxp/id/package.json: -------------------------------------------------------------------------------- ```json { "name": "uxp-template-ps-starter", "version": "1.0.0", "description": "Adobe InDesign MCP Agent Plugin.", "author": "Mike Chambers ([email protected])", "license": "MIT" } ``` -------------------------------------------------------------------------------- /uxp/pr/package.json: -------------------------------------------------------------------------------- ```json { "name": "uxp-template-ps-starter", "version": "1.0.0", "description": "Adobe Photoshop MCP Agent Plugin.", "author": "Mike Chambers ([email protected])", "license": "MIT" } ``` -------------------------------------------------------------------------------- /uxp/ps/package.json: -------------------------------------------------------------------------------- ```json { "name": "uxp-template-ps-starter", "version": "1.0.0", "description": "Adobe Photoshop MCP Agent Plugin.", "author": "Mike Chambers ([email protected])", "license": "MIT" } ``` -------------------------------------------------------------------------------- /uxp/id/style.css: -------------------------------------------------------------------------------- ```css body { color: white; padding: 16px; font-family: Arial, sans-serif; } li:before { content: '• '; width: 3em; } #layers { border: 1px solid #808080; border-radius: 4px; padding: 16px; } footer { position: fixed; bottom: 16px; left: 16px; } ``` -------------------------------------------------------------------------------- /uxp/pr/style.css: -------------------------------------------------------------------------------- ```css body { color: white; padding: 16px; font-family: Arial, sans-serif; } li:before { content: '• '; width: 3em; } #layers { border: 1px solid #808080; border-radius: 4px; padding: 16px; } footer { position: fixed; bottom: 16px; left: 16px; } ``` -------------------------------------------------------------------------------- /uxp/ps/style.css: -------------------------------------------------------------------------------- ```css body { color: white; padding: 16px; font-family: Arial, sans-serif; } li:before { content: '• '; width: 3em; } #layers { border: 1px solid #808080; border-radius: 4px; padding: 16px; } footer { position: fixed; bottom: 16px; left: 16px; } ``` -------------------------------------------------------------------------------- /uxp/id/index.html: -------------------------------------------------------------------------------- ```html <!DOCTYPE html> <html> <head> <script src="main.js"></script> <link rel="stylesheet" href="style.css"> </head> <body> <div> <sp-button id="btnStart">Connect</sp-button> </div> <p> </p> <div> <sp-checkbox id="chkConnectOnLaunch">Connect on Launch</sp-checkbox> </div> <footer> <div> <div>Created by Mike Chambers</div> <div>https://github.com/mikechambers/adb-mcp</div> </div> </footer> </body> </html> ``` -------------------------------------------------------------------------------- /uxp/pr/index.html: -------------------------------------------------------------------------------- ```html <!DOCTYPE html> <html> <head> <script src="main.js"></script> <link rel="stylesheet" href="style.css"> </head> <body> <div> <sp-button id="btnStart">Connect</sp-button> </div> <p> </p> <div> <sp-checkbox id="chkConnectOnLaunch">Connect on Launch</sp-checkbox> </div> <footer> <div> <div>Created by Mike Chambers</div> <div>https://github.com/mikechambers/adb-mcp</div> </div> </footer> </body> </html> ``` -------------------------------------------------------------------------------- /uxp/ps/index.html: -------------------------------------------------------------------------------- ```html <!DOCTYPE html> <html> <head> <script src="main.js"></script> <link rel="stylesheet" href="style.css"> </head> <body> <div> <sp-button id="btnStart">Connect</sp-button> </div> <p> </p> <div> <sp-checkbox id="chkConnectOnLaunch">Connect on Launch</sp-checkbox> </div> <footer> <div> <div>Created by Mike Chambers</div> <div>https://github.com/mikechambers/adb-mcp</div> </div> </footer> </body> </html> ``` -------------------------------------------------------------------------------- /adb-proxy-socket/package.json: -------------------------------------------------------------------------------- ```json { "name": "adb-proxy-socket", "version": "0.85.1", "description": "Proxy socket.io node server for Adobe MCP plugins", "main": "proxy.js", "bin": "proxy.js", "scripts": { "build": "pkg ." }, "author": "Mike Chambers", "license": "ISC", "dependencies": { "express": "^4.21.2", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1" }, "pkg": { "targets": [ "node18-macos-x64", "node18-macos-arm64", "node18-win-x64" ], "outputPath": "dist" } } ``` -------------------------------------------------------------------------------- /mcp/core.py: -------------------------------------------------------------------------------- ```python import logger application = None socket_client = None def init(app, socket): global application, socket_client application = app socket_client = socket def createCommand(action:str, options:dict) -> str: command = { "application":application, "action":action, "options":options } return command def sendCommand(command:dict): response = socket_client.send_message_blocking(command) logger.log(f"Final response: {response['status']}") return response ``` -------------------------------------------------------------------------------- /uxp/pr/commands/consts.js: -------------------------------------------------------------------------------- ```javascript const TRACK_TYPE = { "VIDEO":"VIDEO", "AUDIO":"AUDIO" } TICKS_PER_SECOND = 254016000000; const BLEND_MODES = { COLOR: 0, COLORBURN: 1, COLORDODGE: 2, DARKEN: 3, DARKERCOLOR: 4, DIFFERENCE: 5, DISSOLVE: 6, EXCLUSION: 7, HARDLIGHT: 8, HARDMIX: 9, HUE: 10, LIGHTEN: 11, LIGHTERCOLOR: 12, LINEARBURN: 13, LINEARDODGE: 14, LINEARLIGHT: 15, LUMINOSITY: 16, MULTIPLY: 17, NORMAL: 18, OVERLAY: 19, PINLIGHT: 20, SATURATION: 21, SCREEN: 22, SOFTLIGHT: 23, VIVIDLIGHT: 24, SUBTRACT: 25, DIVIDE: 26 }; module.exports = { BLEND_MODES, TRACK_TYPE, TICKS_PER_SECOND }; ``` -------------------------------------------------------------------------------- /mcp/pyproject.toml: -------------------------------------------------------------------------------- ```toml [build-system] requires = ["setuptools>=42", "wheel"] build-backend = "setuptools.build_meta" [project] name = "psmcp" version = "0.85.1" description = "Adobe Photoshop automation using MCP" requires-python = ">=3.10" license = "MIT" authors = [ {name = "Mike Chambers", email = "[email protected]"} ] dependencies = [ "fonttools", "python-socketio", "mcp[cli]", "requests", "websocket-client>=1.8.0", "pillow>=11.2.1", "numpy>=2.2.6", ] [project.optional-dependencies] dev = [ "pytest>=7.0.0", "black", "isort", "mypy", ] [tool.setuptools] py-modules = ["fonts", "logger", "psmcp", "socket_client"] [tool.black] line-length = 88 target-version = ['py38'] include = '\.pyi?$' [tool.isort] profile = "black" line_length = 88 [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" ``` -------------------------------------------------------------------------------- /mcp/logger.py: -------------------------------------------------------------------------------- ```python # MIT License # # Copyright (c) 2025 Mike Chambers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys def log(message, filter_tag="LOGGER"): print(f"{filter_tag} : {message}", file=sys.stderr) ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/index.html: -------------------------------------------------------------------------------- ```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Illustrator MCP Agent</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="container"> <div class="status-group"> <div class="status-dot" id="statusDot"></div> <span id="statusText">Disconnected</span> </div> <button id="btnConnect">Connect</button> <div class="divider"></div> <div class="checkbox-group"> <input type="checkbox" id="chkConnectOnLaunch"> <label for="chkConnectOnLaunch">Connect automatically on launch</label> </div> <div class="divider"></div> <div class="log-section"> <label>Message Log</label> <textarea id="messageLog" readonly></textarea> </div> </div> <!-- Load CEP's CSInterface library --> <script type="text/javascript" src="./lib/CSInterface.js"></script> <!-- Load Socket.IO client from CDN --> <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> <!-- Load commands --> <script src="commands.js"></script> <!-- Main script --> <script src="main.js"></script> </body> </html> ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/index.html: -------------------------------------------------------------------------------- ```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>AfterEffects MCP Agent</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="container"> <div class="status-group"> <div class="status-dot" id="statusDot"></div> <span id="statusText">Disconnected</span> </div> <button id="btnConnect">Connect</button> <div class="divider"></div> <div class="checkbox-group"> <input type="checkbox" id="chkConnectOnLaunch"> <label for="chkConnectOnLaunch">Connect automatically on launch</label> </div> <div class="divider"></div> <div class="log-section"> <label>Message Log</label> <textarea id="messageLog" readonly></textarea> </div> </div> <!-- Load CEP's CSInterface library --> <script type="text/javascript" src="./lib/CSInterface.js"></script> <!-- Load Socket.IO client from CDN --> <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> <!-- Load commands --> <script src="commands.js"></script> <!-- Main script --> <script src="main.js"></script> </body> </html> ``` -------------------------------------------------------------------------------- /dxt/ps/manifest.json: -------------------------------------------------------------------------------- ```json { "dxt_version": "0.1", "name": "adb-mcp-photoshop", "display_name": "Adobe Photoshop MCP", "version": "0.85.3", "description": "Proof of concept project to create AI Agent for Adobe Photshop by providing an interface to LLMs via the MCP protocol.", "long_description": "Proof of concept project to create AI Agent for Adobe Photoshop by providing an interface to LLMs via the MCP protocol.", "author": { "name": "Mike Chambers", "email": "[email protected]", "url": "https://www.mikechambers.com" }, "homepage": "https://github.com/mikechambers/adb-mcp", "documentation": "https://github.com/mikechambers/adb-mcp", "support": "https://github.com/mikechambers/adb-mcp/issues", "server": { "type": "python", "entry_point": "main.py", "mcp_config": { "command": "uv", "args": [ "run", "--with", "fonttools", "--with", "mcp", "--with", "mcp[cli]", "--with", "python-socketio", "--with", "requests", "--with", "numpy", "--with", "websocket-client", "mcp", "run", "${__dirname}/ps-mcp.py" ] } }, "keywords": [ "adobe", "premierepro", "video" ], "license": "MIT", "repository": { "type": "git", "url": "https://github.com/mikechambers/adb-mcp" } } ``` -------------------------------------------------------------------------------- /dxt/pr/manifest.json: -------------------------------------------------------------------------------- ```json { "dxt_version": "0.1", "name": "adb-mcp-premiere", "display_name": "Adobe Premiere Pro MCP", "version": "0.85.3", "description": "Proof of concept project to create AI Agent for Adobe Premiere Pro by providing an interface to LLMs via the MCP protocol.", "long_description": "Proof of concept project to create AI Agent for Adobe Premiere Pro by providing an interface to LLMs via the MCP protocol.", "author": { "name": "Mike Chambers", "email": "[email protected]", "url": "https://www.mikechambers.com" }, "homepage": "https://github.com/mikechambers/adb-mcp", "documentation": "https://github.com/mikechambers/adb-mcp", "support": "https://github.com/mikechambers/adb-mcp/issues", "server": { "type": "python", "entry_point": "main.py", "mcp_config": { "command": "uv", "args": [ "run", "--with", "fonttools", "--with", "mcp", "--with", "mcp[cli]", "--with", "python-socketio", "--with", "requests", "--with", "websocket-client", "--with", "pillow", "mcp", "run", "${__dirname}/pr-mcp.py" ] } }, "keywords": [ "adobe", "photoshop", "images" ], "license": "MIT", "repository": { "type": "git", "url": "https://github.com/mikechambers/adb-mcp" } } ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/jsx/json-polyfill.jsx: -------------------------------------------------------------------------------- ```javascript // JSON polyfill for ExtendScript // Minimal implementation for serializing simple objects and arrays if (typeof JSON === 'undefined') { JSON = {}; } if (typeof JSON.stringify === 'undefined') { JSON.stringify = function(obj) { var type = typeof obj; // Handle primitives if (obj === null) return 'null'; if (obj === undefined) return 'undefined'; if (type === 'number') { if (isNaN(obj)) return 'null'; // JSON spec: NaN becomes null if (!isFinite(obj)) return 'null'; // JSON spec: Infinity becomes null return String(obj); } if (type === 'boolean') return String(obj); if (type === 'string') { // Escape special characters var escaped = obj.replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t'); return '"' + escaped + '"'; } // Handle arrays if (obj instanceof Array) { var items = []; for (var i = 0; i < obj.length; i++) { items.push(JSON.stringify(obj[i])); } return '[' + items.join(',') + ']'; } // Handle objects if (type === 'object') { var pairs = []; for (var key in obj) { if (obj.hasOwnProperty(key)) { pairs.push(JSON.stringify(key) + ':' + JSON.stringify(obj[key])); } } return '{' + pairs.join(',') + '}'; } // Fallback return '{}'; }; } ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/jsx/json-polyfill.jsx: -------------------------------------------------------------------------------- ```javascript // JSON polyfill for ExtendScript // Minimal implementation for serializing simple objects and arrays if (typeof JSON === 'undefined') { JSON = {}; } if (typeof JSON.stringify === 'undefined') { JSON.stringify = function(obj) { var type = typeof obj; // Handle primitives if (obj === null) return 'null'; if (obj === undefined) return 'undefined'; if (type === 'number') { if (isNaN(obj)) return 'null'; // JSON spec: NaN becomes null if (!isFinite(obj)) return 'null'; // JSON spec: Infinity becomes null return String(obj); } if (type === 'boolean') return String(obj); if (type === 'string') { // Escape special characters var escaped = obj.replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t'); return '"' + escaped + '"'; } // Handle arrays if (obj instanceof Array) { var items = []; for (var i = 0; i < obj.length; i++) { items.push(JSON.stringify(obj[i])); } return '[' + items.join(',') + ']'; } // Handle objects if (type === 'object') { var pairs = []; for (var key in obj) { if (obj.hasOwnProperty(key)) { pairs.push(JSON.stringify(key) + ':' + JSON.stringify(obj[key])); } } return '{' + pairs.join(',') + '}'; } // Fallback return '{}'; }; } ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/CSXS/manifest.xml: -------------------------------------------------------------------------------- ``` <?xml version="1.0" encoding="UTF-8"?> <ExtensionManifest Version="7.0" ExtensionBundleId="com.mikechambers.ae.mcp" ExtensionBundleVersion="1.0.0" ExtensionBundleName="AfterEffects MCP Agent" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Author>Mike Chambers</Author> <Contact>[email protected]</Contact> <Legal>MIT License</Legal> <Abstract>AfterEffects MCP Agent</Abstract> <ExtensionList> <Extension Id="com.mikechambers.ae.mcp" Version="1.0.0"/> </ExtensionList> <ExecutionEnvironment> <HostList> <Host Name="AEFT" Version="[25.0,99.9]"/> </HostList> <LocaleList> <Locale Code="All"/> </LocaleList> <RequiredRuntimeList> <RequiredRuntime Name="CSXS" Version="12.0"/> </RequiredRuntimeList> </ExecutionEnvironment> <DispatchInfoList> <Extension Id="com.mikechambers.ae.mcp"> <DispatchInfo> <Resources> <MainPath>./index.html</MainPath> <CEFCommandLine> <Parameter>--enable-nodejs</Parameter> <Parameter>--mixed-context</Parameter> </CEFCommandLine> </Resources> <Lifecycle> <AutoVisible>true</AutoVisible> </Lifecycle> <UI> <Type>Panel</Type> <Menu>AfterEffects MCP Agent</Menu> <Geometry> <Size> <Height>400</Height> <Width>350</Width> </Size> <MinSize> <Height>300</Height> <Width>300</Width> </MinSize> <MaxSize> <Height>600</Height> <Width>500</Width> </MaxSize> </Geometry> </UI> </DispatchInfo> </Extension> </DispatchInfoList> </ExtensionManifest> ``` -------------------------------------------------------------------------------- /uxp/ps/manifest.json: -------------------------------------------------------------------------------- ```json { "id": "Photoshop MCP Agent", "name": "Photoshop MCP Agent", "version": "0.85.3", "main": "index.html", "host": [ { "app": "PS", "minVersion": "26.0.0" } ], "manifestVersion": 5, "entrypoints": [ { "type": "panel", "id": "vanilla", "minimumSize": { "width": 300, "height": 200 }, "maximumSize": { "width": 300, "height": 200 }, "preferredDockedSize": { "width": 300, "height": 200 }, "preferredFloatingSize": { "width": 300, "height": 200 }, "icons": [ { "width": 32, "height": 32, "path": "icons/icon_D.png", "scale": [ 1, 2 ], "theme": [ "dark", "darkest" ], "species": [ "generic" ] }, { "width": 32, "height": 32, "path": "icons/icon_N.png", "scale": [ 1, 2 ], "theme": [ "lightest", "light" ], "species": [ "generic" ] } ], "label": { "default": "Photoshop MCP Agent" } } ], "requiredPermissions": { "network": { "domains": "all" }, "localFileSystem": "fullAccess" }, "icons": [ { "width": 23, "height": 23, "path": "icons/dark.png", "scale": [ 1, 2 ], "theme": [ "darkest", "dark", "medium" ] }, { "width": 23, "height": 23, "path": "icons/light.png", "scale": [ 1, 2 ], "theme": [ "lightest", "light" ] } ] } ``` -------------------------------------------------------------------------------- /uxp/pr/manifest.json: -------------------------------------------------------------------------------- ```json { "id": "Premiere MCP Agent", "name": "Premiere MCP Agent", "version": "0.85.3", "main": "index.html", "host": [ { "app": "premierepro", "minVersion": "25.3.0" } ], "manifestVersion": 5, "entrypoints": [ { "type": "panel", "id": "vanilla", "minimumSize": { "width": 300, "height": 200 }, "maximumSize": { "width": 300, "height": 200 }, "preferredDockedSize": { "width": 300, "height": 200 }, "preferredFloatingSize": { "width": 300, "height": 200 }, "icons": [ { "width": 32, "height": 32, "path": "icons/icon_D.png", "scale": [ 1, 2 ], "theme": [ "dark", "darkest" ], "species": [ "generic" ] }, { "width": 32, "height": 32, "path": "icons/icon_N.png", "scale": [ 1, 2 ], "theme": [ "lightest", "light" ], "species": [ "generic" ] } ], "label": { "default": "Premiere MCP Agent" } } ], "requiredPermissions": { "network": { "domains": "all" }, "localFileSystem": "fullAccess" }, "icons": [ { "width": 23, "height": 23, "path": "icons/dark.png", "scale": [ 1, 2 ], "theme": [ "darkest", "dark", "medium" ] }, { "width": 23, "height": 23, "path": "icons/light.png", "scale": [ 1, 2 ], "theme": [ "lightest", "light" ] } ] } ``` -------------------------------------------------------------------------------- /uxp/id/manifest.json: -------------------------------------------------------------------------------- ```json { "id": "InDesign MCP Agent", "name": "InDesign MCP Agent", "version": "0.85.0", "main": "index.html", "host": [ { "app": "ID", "minVersion": "20.2.0" } ], "manifestVersion": 5, "entrypoints": [ { "type": "panel", "id": "vanilla", "minimumSize": { "width": 300, "height": 200 }, "maximumSize": { "width": 300, "height": 200 }, "preferredDockedSize": { "width": 300, "height": 200 }, "preferredFloatingSize": { "width": 300, "height": 200 }, "icons": [ { "width": 32, "height": 32, "path": "icons/icon_D.png", "scale": [ 1, 2 ], "theme": [ "dark", "darkest" ], "species": [ "generic" ] }, { "width": 32, "height": 32, "path": "icons/icon_N.png", "scale": [ 1, 2 ], "theme": [ "lightest", "light" ], "species": [ "generic" ] } ], "label": { "default": "InDesign MCP Agent" } } ], "requiredPermissions": { "network": { "domains": [ "all", "http://localhost:3001" ] }, "localFileSystem": "fullAccess" }, "icons": [ { "width": 23, "height": 23, "path": "icons/dark.png", "scale": [ 1, 2 ], "theme": [ "darkest", "dark", "medium" ] }, { "width": 23, "height": 23, "path": "icons/light.png", "scale": [ 1, 2 ], "theme": [ "lightest", "light" ] } ] } ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/CSXS/manifest.xml: -------------------------------------------------------------------------------- ``` <?xml version="1.0" encoding="UTF-8"?> <ExtensionManifest Version="7.0" ExtensionBundleId="com.mikechambers.ai.mcp" ExtensionBundleVersion="1.0.0" ExtensionBundleName="Illustrator MCP Agent" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Author>Mike Chambers</Author> <Contact>[email protected]</Contact> <Legal>MIT License</Legal> <Abstract>Illustrator MCP Agent</Abstract> <ExtensionList> <Extension Id="com.mikechambers.ai.mcp" Version="1.0.0"/> </ExtensionList> <ExecutionEnvironment> <HostList> <Host Name="ILST" Version="[28.0,99.9]"/> </HostList> <LocaleList> <Locale Code="All"/> </LocaleList> <RequiredRuntimeList> <RequiredRuntime Name="CSXS" Version="12.0"/> </RequiredRuntimeList> </ExecutionEnvironment> <DispatchInfoList> <Extension Id="com.mikechambers.ai.mcp"> <DispatchInfo> <Resources> <MainPath>./index.html</MainPath> <CEFCommandLine> <Parameter>--enable-nodejs</Parameter> <Parameter>--mixed-context</Parameter> <Parameter>--remote-debugging-port=8088</Parameter> <Parameter>--allow-file-access-from-files</Parameter> </CEFCommandLine> </Resources> <Lifecycle> <AutoVisible>true</AutoVisible> </Lifecycle> <UI> <Type>Panel</Type> <Menu>Illustrator MCP Agent</Menu> <Geometry> <Size> <Height>400</Height> <Width>350</Width> </Size> <MinSize> <Height>300</Height> <Width>300</Width> </MinSize> <MaxSize> <Height>600</Height> <Width>500</Width> </MaxSize> </Geometry> </UI> </DispatchInfo> </Extension> </DispatchInfoList> </ExtensionManifest> ``` -------------------------------------------------------------------------------- /uxp/ps/commands/filters.js: -------------------------------------------------------------------------------- ```javascript /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const { app } = require("photoshop"); // For app references const { findLayer, execute } = require("./utils"); // For the utility functions used in your code const applyMotionBlur = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `applyMotionBlur : Could not find layerId : ${layerId}` ); } await execute(async () => { await layer.applyMotionBlur(options.angle, options.distance); }); }; const applyGaussianBlur = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `applyGaussianBlur : Could not find layerId : ${layerId}` ); } await execute(async () => { await layer.applyGaussianBlur(options.radius); }); }; const commandHandlers = { applyMotionBlur, applyGaussianBlur, }; module.exports = { commandHandlers }; ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/style.css: -------------------------------------------------------------------------------- ```css /* style.css */ body { margin: 0; padding: 16px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #2a2a2a; color: #e0e0e0; font-size: 13px; } .container { display: flex; flex-direction: column; gap: 12px; } .form-group { display: flex; flex-direction: column; gap: 6px; } label { font-size: 12px; font-weight: 500; color: #b0b0b0; } input[type="text"] { padding: 8px; border: 1px solid #444; border-radius: 4px; background-color: #1a1a1a; color: #e0e0e0; font-size: 13px; } input[type="text"]:focus { outline: none; border-color: #0d6efd; } .status-group { display: flex; align-items: center; gap: 8px; padding: 8px; background-color: #1a1a1a; border-radius: 4px; } .status-dot { width: 10px; height: 10px; border-radius: 50%; background-color: #dc3545; transition: background-color 0.3s; } .status-dot.connected { background-color: #28a745; } #statusText { font-size: 13px; font-weight: 500; } button { padding: 10px 16px; border: none; border-radius: 4px; background-color: #0d6efd; color: white; font-size: 13px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; } button:hover { background-color: #0b5ed7; } button:active { background-color: #0a58ca; } .checkbox-group { display: flex; align-items: center; gap: 8px; padding: 8px 0; } input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; } .checkbox-group label { font-size: 13px; color: #e0e0e0; cursor: pointer; font-weight: normal; } .divider { height: 1px; background-color: #444; margin: 8px 0; } .log-section { display: flex; flex-direction: column; gap: 6px; } #messageLog { width: 100%; height: 120px; padding: 8px; border: 1px solid #444; border-radius: 4px; background-color: #1a1a1a; color: #e0e0e0; font-family: 'Courier New', monospace; font-size: 11px; resize: vertical; line-height: 1.4; } #messageLog:focus { outline: none; border-color: #0d6efd; } ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/style.css: -------------------------------------------------------------------------------- ```css /* style.css */ body { margin: 0; padding: 16px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #2a2a2a; color: #e0e0e0; font-size: 13px; } .container { display: flex; flex-direction: column; gap: 12px; } .form-group { display: flex; flex-direction: column; gap: 6px; } label { font-size: 12px; font-weight: 500; color: #b0b0b0; } input[type="text"] { padding: 8px; border: 1px solid #444; border-radius: 4px; background-color: #1a1a1a; color: #e0e0e0; font-size: 13px; } input[type="text"]:focus { outline: none; border-color: #0d6efd; } .status-group { display: flex; align-items: center; gap: 8px; padding: 8px; background-color: #1a1a1a; border-radius: 4px; } .status-dot { width: 10px; height: 10px; border-radius: 50%; background-color: #dc3545; transition: background-color 0.3s; } .status-dot.connected { background-color: #28a745; } #statusText { font-size: 13px; font-weight: 500; } button { padding: 10px 16px; border: none; border-radius: 4px; background-color: #0d6efd; color: white; font-size: 13px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; } button:hover { background-color: #0b5ed7; } button:active { background-color: #0a58ca; } .checkbox-group { display: flex; align-items: center; gap: 8px; padding: 8px 0; } input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; } .checkbox-group label { font-size: 13px; color: #e0e0e0; cursor: pointer; font-weight: normal; } .divider { height: 1px; background-color: #444; margin: 8px 0; } .log-section { display: flex; flex-direction: column; gap: 6px; } #messageLog { width: 100%; height: 120px; padding: 8px; border: 1px solid #444; border-radius: 4px; background-color: #1a1a1a; color: #e0e0e0; font-family: 'Courier New', monospace; font-size: 11px; resize: vertical; line-height: 1.4; } #messageLog:focus { outline: none; border-color: #0d6efd; } ``` -------------------------------------------------------------------------------- /uxp/ps/commands/index.js: -------------------------------------------------------------------------------- ```javascript /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const { app } = require("photoshop"); const fs = require("uxp").storage.localFileSystem; const adjustmentLayers = require("./adjustment_layers"); const core = require("./core"); const layerStyles = require("./layer_styles") const filters = require("./filters") const selection = require("./selection") const layers = require("./layers") const parseAndRouteCommands = async (commands) => { if (!commands.length) { return; } for (let c of commands) { await parseAndRouteCommand(c); } }; const parseAndRouteCommand = async (command) => { let action = command.action; let f = commandHandlers[action]; if (typeof f !== "function") { throw new Error(`Unknown Command: ${action}`); } console.log(f.name) return f(command); }; const checkRequiresActiveDocument = (command) => { if (!requiresActiveDocument(command)) { return; } if (!app.activeDocument) { throw new Error( `${command.action} : Requires an open Photoshop document` ); } }; const requiresActiveDocument = (command) => { return !["createDocument", "openFile"].includes(command.action); }; const commandHandlers = { ...selection.commandHandlers, ...filters.commandHandlers, ...core.commandHandlers, ...adjustmentLayers.commandHandlers, ...layerStyles.commandHandlers, ...layers.commandHandlers }; module.exports = { requiresActiveDocument, checkRequiresActiveDocument, parseAndRouteCommands, parseAndRouteCommand, }; ``` -------------------------------------------------------------------------------- /uxp/pr/commands/index.js: -------------------------------------------------------------------------------- ```javascript /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const app = require("premierepro"); const core = require("./core"); const getProjectInfo = async () => { let project = await app.Project.getActiveProject() const name = project.name; const path = project.path; const id = project.guid.toString(); const items = await getProjectContentInfo() return { name, path, id, items } } /* const getProjectContentInfo2 = async () => { let project = await app.Project.getActiveProject() let root = await project.getRootItem() let items = await root.getItems() let out = [] for(const item of items) { console.log(item) const b = app.FolderItem.cast(item) const isBin = b != undefined //todo: it would be good to get more data / info here out.push({name:item.name}) } return out } */ const getProjectContentInfo = async () => { let project = await app.Project.getActiveProject() let root = await project.getRootItem() const processItems = async (parentItem) => { let items = await parentItem.getItems() let out = [] for(const item of items) { console.log(item) const folderItem = app.FolderItem.cast(item) const isBin = folderItem != undefined let itemData = { name: item.name, type: isBin ? 'bin' : 'projectItem' } // If it's a bin/folder, recursively get its contents if (isBin) { itemData.items = await processItems(folderItem) } out.push(itemData) } return out } return await processItems(root) } const parseAndRouteCommand = async (command) => { let action = command.action; let f = commandHandlers[action]; if (typeof f !== "function") { throw new Error(`Unknown Command: ${action}`); } console.log(f.name) return f(command); }; const checkRequiresActiveProject = async (command) => { if (!requiresActiveProject(command)) { return; } let project = await app.Project.getActiveProject() if (!project) { throw new Error( `${command.action} : Requires an open Premiere Project` ); } }; const requiresActiveProject = (command) => { return !["createProject", "openProject"].includes(command.action); }; const commandHandlers = { ...core.commandHandlers }; module.exports = { getProjectInfo, checkRequiresActiveProject, parseAndRouteCommand }; ``` -------------------------------------------------------------------------------- /mcp/ae-mcp.py: -------------------------------------------------------------------------------- ```python # MIT License # # Copyright (c) 2025 Mike Chambers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from mcp.server.fastmcp import FastMCP from core import init, sendCommand, createCommand import socket_client import sys # Create an MCP server mcp_name = "Adobe After Effects MCP Server" mcp = FastMCP(mcp_name, log_level="ERROR") print(f"{mcp_name} running on stdio", file=sys.stderr) APPLICATION = "aftereffects" PROXY_URL = 'http://localhost:3001' PROXY_TIMEOUT = 20 socket_client.configure( app=APPLICATION, url=PROXY_URL, timeout=PROXY_TIMEOUT ) init(APPLICATION, socket_client) @mcp.tool() def execute_extend_script(script_string: str): """ Executes arbitrary ExtendScript code in AfterEffects and returns the result. The script should use 'return' to send data back. The result will be automatically JSON stringified. If the script throws an error, it will be caught and returned as an error object. Args: script_string (str): The ExtendScript code to execute. Must use 'return' to send results back. Returns: any: The result returned from the ExtendScript, or an error object containing: - error (str): Error message - line (str): Line number where error occurred Example: script = ''' var doc = app.activeDocument; return { name: doc.name, path: doc.fullName.fsName, layers: doc.layers.length }; ''' result = execute_extend_script(script) """ command = createCommand("executeExtendScript", { "scriptString": script_string }) return sendCommand(command) @mcp.resource("config://get_instructions") def get_instructions() -> str: """Read this first! Returns information and instructions on how to use AfterEffects and this API""" return f""" You are an Adobe AfterEffects expert who is practical, clear, and great at teaching. Rules to follow: 1. Think deeply about how to solve the task. 2. Always check your work before responding. 3. Read the API call info to understand required arguments and return shapes. 4. Before manipulating anything, ensure a document is open and active. """ # AfterEffectsd Blend Modes (for future use) BLEND_MODES = [ "ADD", "ALPHA_ADD", "CLASSIC_COLOR_BURN", "CLASSIC_COLOR_DODGE", "CLASSIC_DIFFERENCE", "COLOR", "COLOR_BURN", "COLOR_DODGE", "DANCING_DISSOLVE", "DARKEN", "DARKER_COLOR", "DIFFERENCE", "DISSOLVE", "EXCLUSION", "HARD_LIGHT", "HARD_MIX", "HUE", "LIGHTEN", "LIGHTER_COLOR", "LINEAR_BURN", "LINEAR_DODGE", "LINEAR_LIGHT", "LUMINESCENT_PREMUL", "LUMINOSITY", "MULTIPLY", "NORMAL", "OVERLAY", "PIN_LIGHT", "SATURATION", "SCREEN", "SILHOUETE_ALPHA", "SILHOUETTE_LUMA", "SOFT_LIGHT", "STENCIL_ALPHA", "STENCIL_LUMA", "SUBTRACT", "VIVID_LIGHT" ] ``` -------------------------------------------------------------------------------- /uxp/id/commands/index.js: -------------------------------------------------------------------------------- ```javascript /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ //const fs = require("uxp").storage.localFileSystem; //const openfs = require('fs') const {app, DocumentIntentOptions} = require("indesign"); const createDocument = async (command) => { console.log("createDocument") const options = command.options let documents = app.documents let margins = options.margins let unit = getUnitForIntent(DocumentIntentOptions.WEB_INTENT) app.marginPreferences.bottom = `${margins.bottom}${unit}` app.marginPreferences.top = `${margins.top}${unit}` app.marginPreferences.left = `${margins.left}${unit}` app.marginPreferences.right = `${margins.right}${unit}` app.marginPreferences.columnCount = options.columns.count app.marginPreferences.columnGutter = `${options.columns.gutter}${unit}` let documentPreferences = { pageWidth: `${options.pageWidth}${unit}`, pageHeight: `${options.pageHeight}${unit}`, pagesPerDocument: options.pagesPerDocument, facingPages: options.facingPages, intent: DocumentIntentOptions.WEB_INTENT } const showingWindow = true //Boolean showingWindow, DocumentPreset documentPreset, Object withProperties documents.add({showingWindow, documentPreferences}) } const getUnitForIntent = (intent) => { if(intent && intent.toString() === DocumentIntentOptions.WEB_INTENT.toString()) { return "px" } throw new Error(`getUnitForIntent : unknown intent [${intent}]`) } const parseAndRouteCommand = async (command) => { let action = command.action; let f = commandHandlers[action]; if (typeof f !== "function") { throw new Error(`Unknown Command: ${action}`); } console.log(f.name) return f(command); }; const commandHandlers = { createDocument }; const getActiveDocumentSettings = (command) => { const document = app.activeDocument const d = document.documentPreferences const documentPreferences = { pageWidth:d.pageWidth, pageHeight:d.pageHeight, pagesPerDocument:d.pagesPerDocument, facingPages:d.facingPages, measurementUnit:getUnitForIntent(d.intent) } const marginPreferences = { top:document.marginPreferences.top, bottom:document.marginPreferences.bottom, left:document.marginPreferences.left, right:document.marginPreferences.right, columnCount : document.marginPreferences.columnCount, columnGutter : document.marginPreferences.columnGutter } return {documentPreferences, marginPreferences} } const checkRequiresActiveDocument = async (command) => { if (!requiresActiveProject(command)) { return; } let document = app.activeDocument if (!document) { throw new Error( `${command.action} : Requires an open InDesign document` ); } }; const requiresActiveDocument = (command) => { return !["createDocument"].includes(command.action); }; module.exports = { getActiveDocumentSettings, checkRequiresActiveDocument, parseAndRouteCommand }; ``` -------------------------------------------------------------------------------- /mcp/id-mcp.py: -------------------------------------------------------------------------------- ```python # MIT License # # Copyright (c) 2025 Mike Chambers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from mcp.server.fastmcp import FastMCP from core import init, sendCommand, createCommand import socket_client import sys #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_name = "Adobe InDesign MCP Server" mcp = FastMCP(mcp_name, log_level="ERROR") print(f"{mcp_name} running on stdio", file=sys.stderr) APPLICATION = "indesign" PROXY_URL = 'http://localhost:3001' PROXY_TIMEOUT = 20 socket_client.configure( app=APPLICATION, url=PROXY_URL, timeout=PROXY_TIMEOUT ) init(APPLICATION, socket_client) @mcp.tool() def create_document( width: int, height: int, pages: int = 0, pages_facing: bool = False, columns: dict = {"count": 1, "gutter": 12}, margins: dict = {"top": 36, "bottom": 36, "left": 36, "right": 36} ): """ Creates a new InDesign document with specified dimensions and layout settings. Args: width (int): Document width in points (1 point = 1/72 inch) height (int): Document height in points pages (int, optional): Number of pages in the document. Defaults to 0. pages_facing (bool, optional): Whether to create facing pages (spread layout). Defaults to False. columns (dict, optional): Column layout configuration with keys: - count (int): Number of columns per page - gutter (int): Space between columns in points Defaults to {"count": 1, "gutter": 12}. margins (dict, optional): Page margin settings in points with keys: - top (int): Top margin - bottom (int): Bottom margin - left (int): Left margin - right (int): Right margin Defaults to {"top": 36, "bottom": 36, "left": 36, "right": 36}. Returns: dict: Result of the command execution from the InDesign UXP plugin """ command = createCommand("createDocument", { "intent": "WEB_INTENT", "pageWidth": width, "pageHeight": height, "margins": margins, "columns": columns, "pagesPerDocument": pages, "pagesFacing": pages_facing }) return sendCommand(command) @mcp.resource("config://get_instructions") def get_instructions() -> str: """Read this first! Returns information and instructions on how to use Photoshop and this API""" return f""" You are an InDesign and design expert who is creative and loves to help other people learn to use InDesign and create. Rules to follow: 1. Think deeply about how to solve the task 2. Always check your work 3. Read the info for the API calls to make sure you understand the requirements and arguments """ """ BLEND_MODES = [ "COLOR", "COLORBURN", "COLORDODGE", "DARKEN", "DARKERCOLOR", "DIFFERENCE", "DISSOLVE", "EXCLUSION", "HARDLIGHT", "HARDMIX", "HUE", "LIGHTEN", "LIGHTERCOLOR", "LINEARBURN", "LINEARDODGE", "LINEARLIGHT", "LUMINOSITY", "MULTIPLY", "NORMAL", "OVERLAY", "PINLIGHT", "SATURATION", "SCREEN", "SOFTLIGHT", "VIVIDLIGHT", "SUBTRACT", "DIVIDE" ] """ ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/main.js: -------------------------------------------------------------------------------- ```javascript /* Socket.IO Plugin for After Effects (CEP) * Main JavaScript file */ const csInterface = new CSInterface(); const APPLICATION = "aftereffects"; const PROXY_URL = "http://localhost:3001"; let socket = null; // Log function function log(message) { const logArea = document.getElementById('messageLog'); const timestamp = new Date().toLocaleTimeString(); logArea.value += `[${timestamp}] ${message}\n`; logArea.scrollTop = logArea.scrollHeight; } // Update UI status function updateStatus(connected) { const statusDot = document.getElementById('statusDot'); const statusText = document.getElementById('statusText'); const btnConnect = document.getElementById('btnConnect'); if (connected) { statusDot.classList.add('connected'); statusText.textContent = 'Connected'; btnConnect.textContent = 'Disconnect'; } else { statusDot.classList.remove('connected'); statusText.textContent = 'Disconnected'; btnConnect.textContent = 'Connect'; } } // Handle incoming command packets async function onCommandPacket(packet) { log(`Received command: ${packet.command.action}`); let out = { senderId: packet.senderId, }; try { // Execute the command in After Effects (from commands.js) //const response = await executeCommand(packet.command); const response = await parseAndRouteCommand(packet.command); out.response = response; out.status = "SUCCESS"; // Get project info out.projectInfo = await getProjectInfo(); } catch (e) { out.status = "FAILURE"; out.message = `Error calling ${packet.command.action}: ${e.message}`; log(`Error: ${e.message}`); } return out; } // Connect to Socket.IO server function connectToServer() { log(`Connecting to ${PROXY_URL}...`); socket = io(PROXY_URL, { transports: ["websocket", "polling"], }); socket.on("connect", () => { updateStatus(true); log(`Connected with ID: ${socket.id}`); socket.emit("register", { application: APPLICATION }); }); socket.on("command_packet", async (packet) => { log(`Received command packet`); const response = await onCommandPacket(packet); sendResponsePacket(response); }); socket.on("registration_response", (data) => { log(`Registration confirmed: ${data.message || 'OK'}`); }); socket.on("connect_error", (error) => { updateStatus(false); log(`Connection error: ${error.message}`); }); socket.on("disconnect", (reason) => { updateStatus(false); log(`Disconnected: ${reason}`); }); } // Disconnect from server function disconnectFromServer() { if (socket && socket.connected) { socket.disconnect(); log('Disconnected from server'); } } // Send response packet function sendResponsePacket(packet) { if (socket && socket.connected) { socket.emit("command_packet_response", { packet }); log('Response sent'); log(packet) return true; } return false; } // LocalStorage helpers const CONNECT_ON_LAUNCH = "connectOnLaunch"; function saveSettings() { localStorage.setItem(CONNECT_ON_LAUNCH, document.getElementById('chkConnectOnLaunch').checked); } function loadSettings() { const connectOnLaunch = localStorage.getItem(CONNECT_ON_LAUNCH) === 'true'; document.getElementById('chkConnectOnLaunch').checked = connectOnLaunch; return connectOnLaunch; } // Event Listeners document.getElementById('btnConnect').addEventListener('click', () => { if (socket && socket.connected) { disconnectFromServer(); } else { connectToServer(); } saveSettings(); }); document.getElementById('chkConnectOnLaunch').addEventListener('change', saveSettings); function initializeExtension() { const csInterface = new CSInterface(); const extensionPath = csInterface.getSystemPath(SystemPath.EXTENSION); const polyfillPath = extensionPath + '/jsx/json-polyfill.jsx'; csInterface.evalScript(`$.evalFile("${polyfillPath}")`, function(result) { console.log('JSON polyfill loaded'); }); } // Initialize on load window.addEventListener('load', () => { initializeExtension() const connectOnLaunch = loadSettings(); log('Plugin loaded'); if (connectOnLaunch) { connectToServer(); } }); ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/commands.js: -------------------------------------------------------------------------------- ```javascript /* commands.js * After Effects command handlers */ // Execute After Effects command via ExtendScript function executeAECommand(script) { return new Promise((resolve, reject) => { const csInterface = new CSInterface(); csInterface.evalScript(script, (result) => { if (result === 'EvalScript error.') { reject(new Error('ExtendScript execution failed')); } else { try { resolve(JSON.parse(result)); } catch (e) { resolve(result); } } }); }); } // Get project information async function getProjectInfo() { const script = ` (function() { var info = { numItems: app.project.numItems, activeItemIndex: app.project.activeItem ? app.project.activeItem.id : null, projectName: app.project.file ? app.project.file.name : "Untitled" }; return JSON.stringify(info); })(); `; return await executeAECommand(script); } // Get all compositions async function getCompositions() { const script = ` (function() { var comps = []; for (var i = 1; i <= app.project.numItems; i++) { var item = app.project.item(i); if (item instanceof CompItem) { comps.push({ id: item.id, name: item.name, width: item.width, height: item.height, duration: item.duration, frameRate: item.frameRate }); } } return JSON.stringify(comps); })(); `; return await executeAECommand(script); } async function executeExtendScript(command) { console.log(command) const options = command.options const scriptString = options.scriptString; const script = ` (function() { try { var result = (function() { ${scriptString} })(); // If result is undefined, return null if (result === undefined) { return 'null'; } // Return stringified result return JSON.stringify(result); } catch(e) { return JSON.stringify({ error: e.toString(), line: e.line || 'unknown' }); } })(); `; const result = await executeAECommand(script); return createPacket(result); } async function getLayers() { const script = ` var comp = app.project.activeItem; if (!comp || !(comp instanceof CompItem)) { JSON.stringify({error: "No active composition"}); } else { var layers = []; for (var i = 1; i <= comp.numLayers; i++) { var layer = comp.layer(i); layers.push({ index: layer.index, name: layer.name, enabled: layer.enabled, selected: layer.selected, startTime: layer.startTime, inPoint: layer.inPoint, outPoint: layer.outPoint }); } JSON.stringify(layers); } `; const result = await executeAECommand(script); return createPacket(result); /*return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };*/ } const createPacket = (result) => { return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } const parseAndRouteCommand = async (command) => { let action = command.action; let f = commandHandlers[action]; if (typeof f !== "function") { throw new Error(`Unknown Command: ${action}`); } console.log(f.name) return await f(command); }; // Execute commands /* async function executeCommand(command) { switch(command.action) { case "getLayers": return await getLayers(); case "executeExtendScript": return await executeExtendScript(command); default: throw new Error(`Unknown command: ${command.action}`); } }*/ const commandHandlers = { getLayers, executeExtendScript }; ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/main.js: -------------------------------------------------------------------------------- ```javascript /* Socket.IO Plugin for After Effects (CEP) * Main JavaScript file */ const csInterface = new CSInterface(); const APPLICATION = "illustrator"; const PROXY_URL = "http://localhost:3001"; let socket = null; // Log function function log(message) { const logArea = document.getElementById('messageLog'); const timestamp = new Date().toLocaleTimeString(); logArea.value += `[${timestamp}] ${message}\n`; logArea.scrollTop = logArea.scrollHeight; } // Update UI status function updateStatus(connected) { const statusDot = document.getElementById('statusDot'); const statusText = document.getElementById('statusText'); const btnConnect = document.getElementById('btnConnect'); if (connected) { statusDot.classList.add('connected'); statusText.textContent = 'Connected'; btnConnect.textContent = 'Disconnect'; } else { statusDot.classList.remove('connected'); statusText.textContent = 'Disconnected'; btnConnect.textContent = 'Connect'; } } // Handle incoming command packets async function onCommandPacket(packet) { log(`Received command: ${packet.command.action}`); let out = { senderId: packet.senderId, }; try { // Execute the command in After Effects (from commands.js) //const response = await executeCommand(packet.command); const response = await parseAndRouteCommand(packet.command); out.response = response; out.status = "SUCCESS"; // Get project info //out.projectInfo = await getProjectInfo(); out.document = await getActiveDocumentInfo(); } catch (e) { out.status = "FAILURE"; out.message = `Error calling ${packet.command.action}: ${e.message}`; log(`Error: ${e.message}`); } return out; } // Connect to Socket.IO server function connectToServer() { log(`Connecting to ${PROXY_URL}...`); socket = io(PROXY_URL, { transports: ["websocket", "polling"], }); socket.on("connect", () => { updateStatus(true); log(`Connected with ID: ${socket.id}`); socket.emit("register", { application: APPLICATION }); }); socket.on("command_packet", async (packet) => { log(`Received command packet`); const response = await onCommandPacket(packet); sendResponsePacket(response); }); socket.on("registration_response", (data) => { log(`Registration confirmed: ${data.message || 'OK'}`); }); socket.on("connect_error", (error) => { updateStatus(false); log(`Connection error: ${error.message}`); }); socket.on("disconnect", (reason) => { updateStatus(false); log(`Disconnected: ${reason}`); }); } // Disconnect from server function disconnectFromServer() { if (socket && socket.connected) { socket.disconnect(); log('Disconnected from server'); } } // Send response packet function sendResponsePacket(packet) { if (socket && socket.connected) { socket.emit("command_packet_response", { packet }); log('Response sent'); log(packet) return true; } return false; } // LocalStorage helpers const CONNECT_ON_LAUNCH = "connectOnLaunch"; function saveSettings() { localStorage.setItem(CONNECT_ON_LAUNCH, document.getElementById('chkConnectOnLaunch').checked); } function loadSettings() { const connectOnLaunch = localStorage.getItem(CONNECT_ON_LAUNCH) === 'true'; document.getElementById('chkConnectOnLaunch').checked = connectOnLaunch; return connectOnLaunch; } // Event Listeners document.getElementById('btnConnect').addEventListener('click', () => { if (socket && socket.connected) { disconnectFromServer(); } else { connectToServer(); } saveSettings(); }); document.getElementById('chkConnectOnLaunch').addEventListener('change', saveSettings); function initializeExtension() { const csInterface = new CSInterface(); const extensionPath = csInterface.getSystemPath(SystemPath.EXTENSION); const polyfillPath = extensionPath + '/jsx/json-polyfill.jsx'; const utilsPath = extensionPath + '/jsx/utils.jsx'; csInterface.evalScript(`$.evalFile("${polyfillPath}")`, function(result) { console.log('JSON polyfill loaded'); }); csInterface.evalScript(`$.evalFile("${utilsPath}")`, function(result) { console.log('utilsPath loaded'); }); } // Initialize on load window.addEventListener('load', () => { initializeExtension() const connectOnLaunch = loadSettings(); log('Plugin loaded'); if (connectOnLaunch) { connectToServer(); } }); ``` -------------------------------------------------------------------------------- /adb-proxy-socket/proxy.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const express = require("express"); const http = require("http"); const { Server } = require("socket.io"); const app = express(); const server = http.createServer(app); const io = new Server(server, { transports: ["websocket", "polling"], maxHttpBufferSize: 50 * 1024 * 1024, }); const PORT = 3001; // Track clients by application const applicationClients = {}; io.on("connection", (socket) => { console.log(`User connected: ${socket.id}`); socket.on("register", ({ application }) => { console.log( `Client ${socket.id} registered for application: ${application}` ); // Store the application preference with this socket socket.data.application = application; // Register this client for this application if (!applicationClients[application]) { applicationClients[application] = new Set(); } applicationClients[application].add(socket.id); // Optionally confirm registration socket.emit("registration_response", { type: "registration", status: "success", message: `Registered for ${application}`, }); }); socket.on("command_packet_response", ({ packet }) => { const senderId = packet.senderId; if (senderId) { io.to(senderId).emit("packet_response", packet); console.log(`Sent confirmation to client ${senderId}`); } else { console.log(`No sender ID provided in packet`); } }); socket.on("command_packet", ({ application, command }) => { console.log( `Command from ${socket.id} for application ${application}:`, command ); // Register this client for this application if not already registered //if (!applicationClients[application]) { // applicationClients[application] = new Set(); //} //applicationClients[application].add(socket.id); // Process the command let packet = { senderId: socket.id, application: application, command: command, }; sendToApplication(packet); // Send response back to this client //socket.emit('json_response', { from: 'server', command }); }); socket.on("disconnect", () => { console.log(`User disconnected: ${socket.id}`); // Remove this client from all application registrations for (const app in applicationClients) { applicationClients[app].delete(socket.id); // Clean up empty sets if (applicationClients[app].size === 0) { delete applicationClients[app]; } } }); }); // Add a function to send messages to clients by application function sendToApplication(packet) { let application = packet.application; if (applicationClients[application]) { console.log( `Sending to ${applicationClients[application].size} clients for ${application}` ); let senderId = packet.senderId; // Loop through all client IDs for this application applicationClients[application].forEach((clientId) => { io.to(clientId).emit("command_packet", packet); }); return true; } console.log(`No clients registered for application: ${application}`); return false; } // Example: Use this function elsewhere in your code // sendToApplication('photoshop', { message: 'Update available' }); server.listen(PORT, () => { console.log( `adb-mcp Command proxy server running on ws://localhost:${PORT}` ); }); ``` -------------------------------------------------------------------------------- /uxp/pr/main.js: -------------------------------------------------------------------------------- ```javascript /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const { entrypoints } = require("uxp"); const { io } = require("./socket.io.js"); const { getSequences } = require("./commands/utils.js"); const { getProjectInfo, parseAndRouteCommand, checkRequiresActiveProject, } = require("./commands/index.js"); const APPLICATION = "premiere"; const PROXY_URL = "http://localhost:3001"; let socket = null; const onCommandPacket = async (packet) => { let command = packet.command; let out = { senderId: packet.senderId, }; try { //this will throw if an active document is required and not open await checkRequiresActiveProject(command); let response = await parseAndRouteCommand(command); out.response = response; out.status = "SUCCESS"; out.sequences = await getSequences(); out.project = await getProjectInfo(); } catch (e) { console.log(e) out.status = "FAILURE"; out.message = `Error calling ${command.action} : ${e}`; } return out; }; function connectToServer() { // Create new Socket.IO connection socket = io(PROXY_URL, { transports: ["websocket"], }); socket.on("connect", () => { updateButton(); console.log("Connected to server with ID:", socket.id); socket.emit("register", { application: APPLICATION }); }); socket.on("command_packet", async (packet) => { console.log("Received command packet:", packet); let response = await onCommandPacket(packet); sendResponsePacket(response); }); socket.on("registration_response", (data) => { console.log("Received response:", data); //TODO: connect button here }); socket.on("connect_error", (error) => { updateButton(); console.error("Connection error:", error); }); socket.on("disconnect", (reason) => { updateButton(); console.log("Disconnected from server. Reason:", reason); //TODO:connect button here }); return socket; } function disconnectFromServer() { if (socket && socket.connected) { socket.disconnect(); console.log("Disconnected from server"); } } function sendResponsePacket(packet) { if (socket && socket.connected) { socket.emit("command_packet_response", { packet: packet, }); return true; } return false; } function sendCommand(command) { if (socket && socket.connected) { socket.emit("app_command", { application: APPLICATION, command: command, }); return true; } return false; } entrypoints.setup({ panels: { vanilla: { show(node) {}, }, }, }); let updateButton = () => { let b = document.getElementById("btnStart"); b.textContent = socket && socket.connected ? "Disconnect" : "Connect"; }; //Toggle button to make it start stop document.getElementById("btnStart").addEventListener("click", () => { if (socket && socket.connected) { disconnectFromServer(); } else { connectToServer(); } }); const CONNECT_ON_LAUNCH = "connectOnLaunch"; // Save checkbox state in localStorage document .getElementById("chkConnectOnLaunch") .addEventListener("change", function (event) { window.localStorage.setItem( CONNECT_ON_LAUNCH, JSON.stringify(event.target.checked) ); }); // Retrieve checkbox state const getConnectOnLaunch = () => { return JSON.parse(window.localStorage.getItem(CONNECT_ON_LAUNCH)) || false; }; // Set checkbox state on page load document.addEventListener("DOMContentLoaded", () => { document.getElementById("chkConnectOnLaunch").checked = getConnectOnLaunch(); }); window.addEventListener("load", (event) => { if (getConnectOnLaunch()) { connectToServer(); } }); ``` -------------------------------------------------------------------------------- /uxp/id/main.js: -------------------------------------------------------------------------------- ```javascript /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const { entrypoints, UI } = require("uxp"); const { io } = require("./socket.io.js"); const app = require("indesign"); const { parseAndRouteCommand, checkRequiresActiveDocument, getActiveDocumentSettings, } = require("./commands/index.js"); const APPLICATION = "indesign"; const PROXY_URL = "http://localhost:3001"; let socket = null; const onCommandPacket = async (packet) => { let command = packet.command; let out = { senderId: packet.senderId, }; try { //this will throw if an active document is required and not open checkRequiresActiveDocument(command); let response = await parseAndRouteCommand(command); out.response = response; out.status = "SUCCESS"; out.activeDocument = await getActiveDocumentSettings(); //out.projectItems = await getProjectContentInfo(); } catch (e) { out.status = "FAILURE"; out.message = `Error calling ${command.action} : ${e}`; } return out; }; function connectToServer() { // Create new Socket.IO connection const isWindows = require("os").platform() === "win32"; const socketOptions = isWindows ? { transports: ["polling"], upgrade: false, rememberUpgrade: false, } : { transports: ["websocket"], }; console.log(isWindows); console.log(socketOptions); socket = io(PROXY_URL, socketOptions); socket.on("connect", () => { updateButton(); console.log("Connected to server with ID:", socket.id); socket.emit("register", { application: APPLICATION }); }); socket.on("command_packet", async (packet) => { console.log("Received command packet:", packet); let response = await onCommandPacket(packet); sendResponsePacket(response); }); socket.on("registration_response", (data) => { console.log("Received response:", data); //TODO: connect button here }); socket.on("connect_error", (error) => { updateButton(); console.error("Connection error:", error); }); socket.on("disconnect", (reason) => { updateButton(); console.log("Disconnected from server. Reason:", reason); //TODO:connect button here }); return socket; } function disconnectFromServer() { if (socket && socket.connected) { socket.disconnect(); console.log("Disconnected from server"); } } function sendResponsePacket(packet) { if (socket && socket.connected) { socket.emit("command_packet_response", { packet: packet, }); return true; } return false; } function sendCommand(command) { if (socket && socket.connected) { socket.emit("app_command", { application: APPLICATION, command: command, }); return true; } return false; } entrypoints.setup({ panels: { vanilla: { show(node) {}, }, }, }); let updateButton = () => { let b = document.getElementById("btnStart"); b.textContent = socket && socket.connected ? "Disconnect" : "Connect"; }; //Toggle button to make it start stop document.getElementById("btnStart").addEventListener("click", () => { if (socket && socket.connected) { disconnectFromServer(); } else { connectToServer(); } }); const CONNECT_ON_LAUNCH = "connectOnLaunch"; // Save checkbox state in localStorage document .getElementById("chkConnectOnLaunch") .addEventListener("change", function (event) { window.localStorage.setItem( CONNECT_ON_LAUNCH, JSON.stringify(event.target.checked) ); }); // Retrieve checkbox state const getConnectOnLaunch = () => { return JSON.parse(window.localStorage.getItem(CONNECT_ON_LAUNCH)) || false; }; // Set checkbox state on page load document.addEventListener("DOMContentLoaded", () => { document.getElementById("chkConnectOnLaunch").checked = getConnectOnLaunch(); }); window.addEventListener("load", (event) => { if (getConnectOnLaunch()) { connectToServer(); } }); ``` -------------------------------------------------------------------------------- /mcp/fonts.py: -------------------------------------------------------------------------------- ```python # MIT License # # Copyright (c) 2025 Mike Chambers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import sys import glob from fontTools.ttLib import TTFont def list_all_fonts_postscript(): """ Returns a list of PostScript names for all fonts installed on the system. Works on both Windows and macOS. Returns: list: A list of PostScript font names as strings """ postscript_names = [] # Get font directories based on platform font_dirs = [] if sys.platform == 'win32': # Windows # Windows font directory if 'WINDIR' in os.environ: font_dirs.append(os.path.join(os.environ['WINDIR'], 'Fonts')) elif sys.platform == 'darwin': # macOS # macOS system font directories font_dirs.extend([ '/System/Library/Fonts', '/Library/Fonts', os.path.expanduser('~/Library/Fonts') ]) else: print(f"Unsupported platform: {sys.platform}") return [] # Get all font files from all directories font_extensions = ['*.ttf', '*.ttc', '*.otf'] font_files = [] for font_dir in font_dirs: if os.path.exists(font_dir): for ext in font_extensions: font_files.extend(glob.glob(os.path.join(font_dir, ext))) # Also check subdirectories on macOS if sys.platform == 'darwin': font_files.extend(glob.glob(os.path.join(font_dir, '**', ext), recursive=True)) # Process each font file for font_path in font_files: try: # TrueType Collections (.ttc files) can contain multiple fonts if font_path.lower().endswith('.ttc'): try: ttc = TTFont(font_path, fontNumber=0) num_fonts = ttc.reader.numFonts ttc.close() # Extract PostScript name from each font in the collection for i in range(num_fonts): try: font = TTFont(font_path, fontNumber=i) ps_name = _extract_postscript_name(font) if ps_name and not ps_name.startswith('.'): postscript_names.append(ps_name) font.close() except Exception as e: print(f"Error processing font {i} in collection {font_path}: {e}") except Exception as e: print(f"Error determining number of fonts in collection {font_path}: {e}") else: # Regular TTF/OTF file try: font = TTFont(font_path) ps_name = _extract_postscript_name(font) if ps_name: postscript_names.append(ps_name) font.close() except Exception as e: print(f"Error processing font {font_path}: {e}") except Exception as e: print(f"Error with font file {font_path}: {e}") return list(set(postscript_names)) def _extract_postscript_name(font): """ Extract the PostScript name from a TTFont object. Args: font: A TTFont object Returns: str: The PostScript name or None if not found """ # Method 1: Try to get it from the name table (most reliable) if 'name' in font: name_table = font['name'] # PostScript name is stored with nameID 6 for record in name_table.names: if record.nameID == 6: # Try to decode the name try: return ( record.string.decode('utf-16-be').encode('utf-8').decode('utf-8') if record.isUnicode() else record.string.decode('latin-1') ) except Exception: pass # Method 2: For CFF OpenType fonts if 'CFF ' in font: try: cff = font['CFF '] if cff.cff.fontNames: return cff.cff.fontNames[0] except Exception: pass return None if __name__ == "__main__": font_names = list_all_fonts_postscript() print(f"Number of fonts found: {len(font_names)}") ``` -------------------------------------------------------------------------------- /mcp/socket_client.py: -------------------------------------------------------------------------------- ```python # MIT License # # Copyright (c) 2025 Mike Chambers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import socketio import time import threading import json from queue import Queue import logger # Global configuration variables proxy_url = None proxy_timeout = None application = None def send_message_blocking(command, timeout=None): """ Blocking function that connects to a Socket.IO server, sends a message, waits for a response, then disconnects. Args: command: The command to send timeout (int): Maximum time to wait for response in seconds Returns: dict: The response received from the server, or None if no response """ # Use global variables global application, proxy_url, proxy_timeout # Check if configuration is set if not application or not proxy_url or not proxy_timeout: logger.log("Socket client not configured. Call configure() first.") return None # Use provided timeout or default wait_timeout = timeout if timeout is not None else proxy_timeout # Create a standard (non-async) SocketIO client with WebSocket transport only sio = socketio.Client(logger=False) # Use a queue to get the response from the event handler response_queue = Queue() connection_failed = [False] @sio.event def connect(): logger.log(f"Connected to server with session ID: {sio.sid}") # Send the command logger.log(f"Sending message to {application}: {command}") sio.emit('command_packet', { 'type': "command", 'application': application, 'command': command }) @sio.event def packet_response(data): logger.log(f"Received response: {data}") response_queue.put(data) # Disconnect after receiving the response sio.disconnect() @sio.event def disconnect(): logger.log("Disconnected from server") # If we disconnect without response, put None in the queue if response_queue.empty(): response_queue.put(None) @sio.event def connect_error(error): logger.log(f"Connection error: {error}") connection_failed[0] = True response_queue.put(None) # Connect in a separate thread to avoid blocking the main thread during connection def connect_and_wait(): try: sio.connect(proxy_url, transports=['websocket']) # Keep the client running until disconnect is called sio.wait() except Exception as e: logger.log(f"Error: {e}") connection_failed[0] = True if response_queue.empty(): response_queue.put(None) if sio.connected: sio.disconnect() # Start the client in a separate thread client_thread = threading.Thread(target=connect_and_wait) client_thread.daemon = True client_thread.start() try: # Wait for a response or timeout logger.log("waiting for response...") response = response_queue.get(timeout=wait_timeout) if connection_failed[0]: 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...") try: logger.log(json.dumps(response)) except: logger.log(f"Response (not JSON-serializable): {response}") if response["status"] == "FAILURE": raise AppError(f"Error returned from {application}: {response['message']}") return response except AppError: raise except Exception as e: logger.log(f"Error waiting for response: {e}") if sio.connected: sio.disconnect() raise RuntimeError(f"Error: Could not connect to {application}. Connection Timed Out. Make sure that {application} is running and that the MCP Plugin is connected. Original error: {e}") finally: # Make sure client is disconnected if sio.connected: sio.disconnect() # Wait for the thread to finish (should be quick after disconnect) client_thread.join(timeout=1) class AppError(Exception): pass def configure(app=None, url=None, timeout=None): global application, proxy_url, proxy_timeout if app: application = app if url: proxy_url = url if timeout: proxy_timeout = timeout logger.log(f"Socket client configured: app={application}, url={proxy_url}, timeout={proxy_timeout}") ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/jsx/utils.jsx: -------------------------------------------------------------------------------- ```javascript // jsx/illustrator-helpers.jsx // Helper function to extract XMP attribute values $.global.extractXMPAttribute = function(xmpStr, tagName, attrName) { var pattern = new RegExp(tagName + '[^>]*' + attrName + '="([^"]+)"', 'i'); var match = xmpStr.match(pattern); return match ? match[1] : null; }; // Helper function to extract XMP tag values $.global.extractXMPValue = function(xmpStr, tagName) { var pattern = new RegExp('<' + tagName + '>([^<]+)<\\/' + tagName + '>', 'i'); var match = xmpStr.match(pattern); return match ? match[1] : null; }; // Helper function to get document ID from XMP $.global.getDocumentID = function(doc) { try { var xmpString = doc.XMPString; if (!xmpString) return null; return $.global.extractXMPAttribute(xmpString, 'xmpMM:DocumentID', 'rdf:resource') || $.global.extractXMPValue(xmpString, 'xmpMM:DocumentID'); } catch(e) { return null; } }; // jsx/illustrator-helpers.jsx // ... existing helper functions ... // Helper function to create document info object $.global.createDocumentInfo = function(doc, activeDoc) { return { id: $.global.getDocumentID(doc), name: doc.name, width: doc.width, height: doc.height, colorSpace: doc.documentColorSpace.toString(), numLayers: doc.layers.length, numArtboards: doc.artboards.length, saved: doc.saved, isActive: doc === activeDoc }; }; // Helper function to get detailed layer information $.global.getLayerInfo = function(layer, includeSubLayers) { if (includeSubLayers === undefined) includeSubLayers = true; try { var layerInfo = { id: layer.absoluteZOrderPosition, name: layer.name, visible: layer.visible, locked: layer.locked, opacity: layer.opacity, printable: layer.printable, preview: layer.preview, sliced: layer.sliced, isIsolated: layer.isIsolated, hasSelectedArtwork: layer.hasSelectedArtwork, itemCount: layer.pageItems.length, zOrderPosition: layer.zOrderPosition, absoluteZOrderPosition: layer.absoluteZOrderPosition, dimPlacedImages: layer.dimPlacedImages, typename: layer.typename }; // Get blending mode try { layerInfo.blendingMode = layer.blendingMode.toString(); } catch(e) { layerInfo.blendingMode = "Normal"; } // Get color info if available try { layerInfo.color = { red: layer.color.red, green: layer.color.green, blue: layer.color.blue }; } catch(e) { layerInfo.color = null; } // Get artwork knockout state try { layerInfo.artworkKnockout = layer.artworkKnockout.toString(); } catch(e) { layerInfo.artworkKnockout = "Inherited"; } // Count different types of items on the layer try { layerInfo.itemCounts = { total: layer.pageItems.length, pathItems: layer.pathItems.length, textFrames: layer.textFrames.length, groupItems: layer.groupItems.length, compoundPathItems: layer.compoundPathItems.length, placedItems: layer.placedItems.length, rasterItems: layer.rasterItems.length, meshItems: layer.meshItems.length, symbolItems: layer.symbolItems.length }; } catch(e) { layerInfo.itemCounts = { total: 0 }; } // Handle sublayers layerInfo.subLayerCount = layer.layers.length; layerInfo.hasSubLayers = layer.layers.length > 0; if (includeSubLayers && layer.layers.length > 0) { layerInfo.subLayers = []; for (var j = 0; j < layer.layers.length; j++) { var subLayer = layer.layers[j]; // Recursively get sublayer info (but don't go deeper to avoid infinite recursion) var subLayerInfo = $.global.getLayerInfo(subLayer, false); layerInfo.subLayers.push(subLayerInfo); } } return layerInfo; } catch(e) { return { error: "Error processing layer: " + e.toString(), layerName: layer.name || "Unknown" }; } }; // Helper function to get all layers information for a document $.global.getAllLayersInfo = function(doc) { try { var layersInfo = []; for (var i = 0; i < doc.layers.length; i++) { var layer = doc.layers[i]; var layerInfo = $.global.getLayerInfo(layer, true); layersInfo.push(layerInfo); } return { totalLayers: doc.layers.length, layers: layersInfo }; } catch(e) { return { error: e.toString(), totalLayers: 0, layers: [] }; } }; $.global.createDocumentInfo = function(doc, activeDoc) { var docInfo = { id: $.global.getDocumentID(doc), name: doc.name, width: doc.width, height: doc.height, colorSpace: doc.documentColorSpace.toString(), numLayers: doc.layers.length, numArtboards: doc.artboards.length, saved: doc.saved, isActive: doc === activeDoc }; // Add layers information var layersResult = $.global.getAllLayersInfo(doc); docInfo.layers = layersResult.layers; docInfo.totalLayers = layersResult.totalLayers; return docInfo; }; ``` -------------------------------------------------------------------------------- /uxp/ps/main.js: -------------------------------------------------------------------------------- ```javascript /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const { entrypoints, UI } = require("uxp"); const { checkRequiresActiveDocument, parseAndRouteCommand, } = require("./commands/index.js"); const { hasActiveSelection, generateDocumentInfo } = require("./commands/utils.js"); const { getLayers } = require("./commands/layers.js").commandHandlers; const { io } = require("./socket.io.js"); //const { act } = require("react"); const app = require("photoshop").app; const APPLICATION = "photoshop"; const PROXY_URL = "http://localhost:3001"; let socket = null; const onCommandPacket = async (packet) => { let command = packet.command; let out = { senderId: packet.senderId, }; try { //this will throw if an active document is required and not open checkRequiresActiveDocument(command); let response = await parseAndRouteCommand(command); out.response = response; out.status = "SUCCESS"; let activeDocument = app.activeDocument let doc = generateDocumentInfo(activeDocument, activeDocument) out.document = doc; out.layers = await getLayers(); out.hasActiveSelection = hasActiveSelection(); } catch (e) { out.status = "FAILURE"; out.message = `Error calling ${command.action} : ${e}`; } return out; }; function connectToServer() { // Create new Socket.IO connection socket = io(PROXY_URL, { transports: ["websocket"], }); socket.on("connect", () => { updateButton(); console.log("Connected to server with ID:", socket.id); socket.emit("register", { application: APPLICATION }); }); socket.on("command_packet", async (packet) => { console.log("Received command packet:", packet); let response = await onCommandPacket(packet); sendResponsePacket(response); }); socket.on("registration_response", (data) => { console.log("Received response:", data); //TODO: connect button here }); socket.on("connect_error", (error) => { updateButton(); console.error("Connection error:", error); }); socket.on("disconnect", (reason) => { updateButton(); console.log("Disconnected from server. Reason:", reason); //TODO:connect button here }); return socket; } function disconnectFromServer() { if (socket && socket.connected) { socket.disconnect(); console.log("Disconnected from server"); } } function sendResponsePacket(packet) { if (socket && socket.connected) { socket.emit("command_packet_response", { packet: packet, }); return true; } return false; } function sendCommand(command) { if (socket && socket.connected) { socket.emit("app_command", { application: APPLICATION, command: command, }); return true; } return false; } let onInterval = async () => { let commands = await fetchCommands(); await parseAndRouteCommands(commands); }; let fetchCommands = async () => { try { let url = `http://127.0.0.1:3030/commands/get/${APPLICATION}/`; const fetchOptions = { method: "GET", headers: { Accept: "application/json", }, }; // Make the fetch request const response = await fetch(url, fetchOptions); // Check if the request was successful if (!response.ok) { console.log("a"); throw new Error(`HTTP error! Status: ${response.status}`); } let r = await response.json(); if (r.status != "SUCCESS") { throw new Error(`API Request error! Status: ${response.message}`); } return r.commands; } catch (error) { console.error("Error fetching data:", error); throw error; // Re-throw to allow caller to handle the error } }; entrypoints.setup({ panels: { vanilla: { show(node) {}, }, }, }); let updateButton = () => { let b = document.getElementById("btnStart"); b.textContent = socket && socket.connected ? "Disconnect" : "Connect"; }; //Toggle button to make it start stop document.getElementById("btnStart").addEventListener("click", () => { if (socket && socket.connected) { disconnectFromServer(); } else { connectToServer(); } }); const CONNECT_ON_LAUNCH = "connectOnLaunch"; // Save checkbox state in localStorage document .getElementById("chkConnectOnLaunch") .addEventListener("change", function (event) { window.localStorage.setItem( CONNECT_ON_LAUNCH, JSON.stringify(event.target.checked) ); }); // Retrieve checkbox state const getConnectOnLaunch = () => { return JSON.parse(window.localStorage.getItem(CONNECT_ON_LAUNCH)) || false; }; // Set checkbox state on page load document.addEventListener("DOMContentLoaded", () => { document.getElementById("chkConnectOnLaunch").checked = getConnectOnLaunch(); }); window.addEventListener("load", (event) => { if (getConnectOnLaunch()) { connectToServer(); } }); ``` -------------------------------------------------------------------------------- /mcp/ps-batch-play.py: -------------------------------------------------------------------------------- ```python # MIT License # # Copyright (c) 2025 Mike Chambers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from mcp.server.fastmcp import FastMCP, Image from core import init, sendCommand, createCommand from fonts import list_all_fonts_postscript import numpy as np import base64 import socket_client import sys import os FONT_LIMIT = 1000 #max number of font names to return to AI mcp_name = "Adobe Photoshop Batch Play 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' PROXY_TIMEOUT = 20 socket_client.configure( app=APPLICATION, url=PROXY_URL, timeout=PROXY_TIMEOUT ) init(APPLICATION, socket_client) @mcp.tool() def call_batch_play_command(commands: list): """ Executes arbitrary Photoshop batchPlay commands via MCP. Args: commands (str): A raw JSON string representing a list of batchPlay descriptors. This should be the exact JSON string you would pass to `batchPlay()` in a UXP plugin. Returns: Any: The result returned from Photoshop after executing the batchPlay command(s). Example: >>> commands = ''' ... [ ... { ... "_obj": "exportDocumentAs", ... "exportAs": { ... "_obj": "exportAsPNG", ... "interlaced": false, ... "transparency": true, ... "metadata": 1 ... }, ... "documentID": 1234, ... "saveFile": { ... "_path": "/Users/yourname/Downloads/export.png", ... "_kind": "local" ... }, ... "overwrite": true ... } ... ] ... ''' >>> result = call_batch_play_command(commands) >>> print(result) # Output from Photoshop will be returned as-is (usually a list of response descriptors) """ if not commands: raise ValueError("commands cannot be empty.") command = createCommand( "executeBatchPlayCommand", { "commands": commands } ) return sendCommand(command) @mcp.resource("config://get_instructions") def get_instructions() -> str: """Read this first! Returns information and instructions on how to use Photoshop and this API""" return f""" You are a photoshop expert who is creative and loves to help other people learn to use Photoshop and create. You are well versed in composition, design and color theory, and try to follow that theory when making decisions. Unless otherwise specified, all commands act on the currently active document in Photoshop Rules to follow: 1. Think deeply about how to solve the task 2. Always check your work 3. You can view the current visible photoshop file by calling get_document_image 4. Pay attention to font size (dont make it too big) 5. Always use alignment (align_content()) to position your text. 6. Read the info for the API calls to make sure you understand the requirements and arguments 7. When you make a selection, clear it once you no longer need it Here are some general tips for when working with Photoshop. In general, layers are created from bottom up, so keep that in mind as you figure out the order or operations. If you want you have lower layers show through higher ones you must either change the opacity of the higher layers and / or blend modes. When using fonts there are a couple of things to keep in mind. First, the font origin is the bottom left of the font, not the top right. Suggestions for sizes: Paragraph text : 8 to 12 pts Headings : 14 - 20 pts Single Word Large : 20 to 25pt Pay attention to what layer names are needed for. Sometimes the specify the name of a newly created layer and sometimes they specify the name of the layer that the action should be performed on. As a general rule, you should not flatten files unless asked to do so, or its necessary to apply an effect or look. When generating an image, you do not need to first create a pixel layer. A layer will automatically be created when you generate the image. Colors are defined via a dict with red, green and blue properties with values between 0 and 255 {{"red":255, "green":0, "blue":0}} Bounds is defined as a dict with top, left, bottom and right properties {{"top": 0, "left": 0, "bottom": 250, "right": 300}} Valid options for API calls: alignment_modes: {", ".join(alignment_modes)} justification_modes: {", ".join(justification_modes)} blend_modes: {", ".join(blend_modes)} anchor_positions: {", ".join(anchor_positions)} interpolation_methods: {", ".join(interpolation_methods)} fonts: {", ".join(font_names[:FONT_LIMIT])} """ font_names = list_all_fonts_postscript() interpolation_methods = [ "AUTOMATIC", "BICUBIC", "BICUBICSHARPER", "BICUBICSMOOTHER", "BILINEAR", "NEARESTNEIGHBOR" ] anchor_positions = [ "BOTTOMCENTER", "BOTTOMLEFT", "BOTTOMRIGHT", "MIDDLECENTER", "MIDDLELEFT", "MIDDLERIGHT", "TOPCENTER", "TOPLEFT", "TOPRIGHT" ] justification_modes = [ "CENTER", "CENTERJUSTIFIED", "FULLYJUSTIFIED", "LEFT", "LEFTJUSTIFIED", "RIGHT", "RIGHTJUSTIFIED" ] alignment_modes = [ "LEFT", "CENTER_HORIZONTAL", "RIGHT", "TOP", "CENTER_VERTICAL", "BOTTOM" ] blend_modes = [ "COLOR", "COLORBURN", "COLORDODGE", "DARKEN", "DARKERCOLOR", "DIFFERENCE", "DISSOLVE", "DIVIDE", "EXCLUSION", "HARDLIGHT", "HARDMIX", "HUE", "LIGHTEN", "LIGHTERCOLOR", "LINEARBURN", "LINEARDODGE", "LINEARLIGHT", "LUMINOSITY", "MULTIPLY", "NORMAL", "OVERLAY", "PASSTHROUGH", "PINLIGHT", "SATURATION", "SCREEN", "SOFTLIGHT", "SUBTRACT", "VIVIDLIGHT" ] ``` -------------------------------------------------------------------------------- /uxp/ps/commands/adjustment_layers.js: -------------------------------------------------------------------------------- ```javascript /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const { action } = require("photoshop"); const { selectLayer, findLayer, execute } = require("./utils") const addAdjustmentLayerBlackAndWhite = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `addAdjustmentLayerBlackAndWhite : Could not find layerId : ${layerId}` ); } let colors = options.colors; let tintColor = options.tintColor await execute(async () => { selectLayer(layer, true); let commands = [ // Make adjustment layer { _obj: "make", _target: [ { _ref: "adjustmentLayer", }, ], using: { _obj: "adjustmentLayer", type: { _obj: "blackAndWhite", blue: colors.blue, cyan: colors.cyan, grain: colors.green, magenta: colors.magenta, presetKind: { _enum: "presetKindType", _value: "presetKindDefault", }, red: colors.red, tintColor: { _obj: "RGBColor", blue: tintColor.blue, grain: tintColor.green, red: tintColor.red, }, useTint: options.tint, yellow: colors.yellow, }, }, }, ]; await action.batchPlay(commands, {}); }); }; const addBrightnessContrastAdjustmentLayer = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `addBrightnessContrastAdjustmentLayer : Could not find layerId : ${layerId}` ); } await execute(async () => { selectLayer(layer, true); let commands = [ // Make adjustment layer { _obj: "make", _target: [ { _ref: "adjustmentLayer", }, ], using: { _obj: "adjustmentLayer", type: { _obj: "brightnessEvent", useLegacy: false, }, }, }, // Set current adjustment layer { _obj: "set", _target: [ { _enum: "ordinal", _ref: "adjustmentLayer", _value: "targetEnum", }, ], to: { _obj: "brightnessEvent", brightness: options.brightness, center: options.contrast, useLegacy: false, }, }, ]; await action.batchPlay(commands, {}); }); }; const addAdjustmentLayerVibrance = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `addAdjustmentLayerVibrance : Could not find layerId : ${layerId}` ); } let colors = options.colors; await execute(async () => { selectLayer(layer, true); let commands = [ // Make adjustment layer { _obj: "make", _target: [ { _ref: "adjustmentLayer", }, ], using: { _obj: "adjustmentLayer", type: { _class: "vibrance", }, }, }, // Set current adjustment layer { _obj: "set", _target: [ { _enum: "ordinal", _ref: "adjustmentLayer", _value: "targetEnum", }, ], to: { _obj: "vibrance", saturation: options.saturation, vibrance: options.vibrance, }, }, ]; await action.batchPlay(commands, {}); }); }; const addColorBalanceAdjustmentLayer = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `addColorBalanceAdjustmentLayer : Could not find layer named : [${layerId}]` ); } await execute(async () => { let commands = [ // Make adjustment layer { _obj: "make", _target: [ { _ref: "adjustmentLayer", }, ], using: { _obj: "adjustmentLayer", type: { _obj: "colorBalance", highlightLevels: [0, 0, 0], midtoneLevels: [0, 0, 0], preserveLuminosity: true, shadowLevels: [0, 0, 0], }, }, }, // Set current adjustment layer { _obj: "set", _target: [ { _enum: "ordinal", _ref: "adjustmentLayer", _value: "targetEnum", }, ], to: { _obj: "colorBalance", highlightLevels: options.highlights, midtoneLevels: options.midtones, shadowLevels: options.shadows, }, }, ]; await action.batchPlay(commands, {}); }); }; const commandHandlers = { addAdjustmentLayerBlackAndWhite, addBrightnessContrastAdjustmentLayer, addAdjustmentLayerVibrance, addColorBalanceAdjustmentLayer } module.exports = { commandHandlers }; ``` -------------------------------------------------------------------------------- /mcp/ai-mcp.py: -------------------------------------------------------------------------------- ```python # MIT License # # Copyright (c) 2025 Mike Chambers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from mcp.server.fastmcp import FastMCP from core import init, sendCommand, createCommand import socket_client import sys # Create an MCP server mcp_name = "Adobe Illustrator MCP Server" mcp = FastMCP(mcp_name, log_level="ERROR") print(f"{mcp_name} running on stdio", file=sys.stderr) APPLICATION = "illustrator" PROXY_URL = 'http://localhost:3001' PROXY_TIMEOUT = 20 socket_client.configure( app=APPLICATION, url=PROXY_URL, timeout=PROXY_TIMEOUT ) init(APPLICATION, socket_client) @mcp.tool() def get_documents(): """ Returns information about all currently open documents in Illustrator. """ command = createCommand("getDocuments", {}) return sendCommand(command) @mcp.tool() def get_active_document_info(): """ Returns information about the current active document. """ command = createCommand("getActiveDocumentInfo", {}) return sendCommand(command) @mcp.tool() def open_file( path: str ): """ Opens an Illustrator (.ai) file in Adobe Illustrator. Args: path (str): The absolute file path to the Illustrator file to open. Example: "/Users/username/Documents/my_artwork.ai" Returns: dict: Result containing: - success (bool): Whether the file was opened successfully - error (str): Error message if opening failed """ command_params = { "path": path } command = createCommand("openFile", command_params) return sendCommand(command) @mcp.tool() def export_png( path: str, transparency: bool = True, anti_aliasing: bool = True, artboard_clipping: bool = True, horizontal_scale: int = 100, vertical_scale: int = 100, export_type: str = "PNG24", matte: bool = None, matte_color: dict = {"red": 255, "green": 255, "blue": 255} ): """ Exports the active Illustrator document as a PNG file. Args: path (str): The absolute file path where the PNG will be saved. Example: "/Users/username/Documents/my_export.png" transparency (bool, optional): Enable/disable transparency. Defaults to True. anti_aliasing (bool, optional): Enable/disable anti-aliasing for smooth edges. Defaults to True. artboard_clipping (bool, optional): Clip export to artboard bounds. Defaults to True. horizontal_scale (int, optional): Horizontal scale percentage (1-1000). Defaults to 100. vertical_scale (int, optional): Vertical scale percentage (1-1000). Defaults to 100. export_type (str, optional): PNG format type. "PNG24" (24-bit) or "PNG8" (8-bit). Defaults to "PNG24". matte (bool, optional): Enable matte background color for transparency preview. If None, uses Illustrator's default behavior. matte_color (dict, optional): RGB color for matte background. Defaults to {"red": 255, "green": 255, "blue": 255}. Dict with keys "red", "green", "blue" with values 0-255. Returns: dict: Export result containing: - success (bool): Whether the export succeeded - filePath (str): The actual file path where the PNG was saved - fileExists (bool): Whether the exported file exists - options (dict): The export options that were used - documentName (str): Name of the exported document - error (str): Error message if export failed Example: # Basic PNG export result = export_png("/Users/username/Desktop/my_artwork.png") # High-resolution export with transparency result = export_png( path="/Users/username/Desktop/high_res.png", horizontal_scale=300, vertical_scale=300, transparency=True ) # PNG8 export with red matte background result = export_png( path="/Users/username/Desktop/small_file.png", export_type="PNG8", matte=True, matte_color={"red": 255, "green": 0, "blue": 0} ) # Blue matte background result = export_png( path="/Users/username/Desktop/blue_bg.png", matte=True, matte_color={"red": 0, "green": 100, "blue": 255} ) """ # Only include matte and matteColor if needed command_params = { "path": path, "transparency": transparency, "antiAliasing": anti_aliasing, "artBoardClipping": artboard_clipping, "horizontalScale": horizontal_scale, "verticalScale": vertical_scale, "exportType": export_type } # Only include matte if explicitly set if matte is not None: command_params["matte"] = matte # Include matte color if matte is enabled or custom colors provided if matte or matte_color != {"red": 255, "green": 255, "blue": 255}: command_params["matteColor"] = matte_color command = createCommand("exportPNG", command_params) return sendCommand(command) @mcp.tool() def execute_extend_script(script_string: str): """ Executes arbitrary ExtendScript code in Illustrator and returns the result. The script should use 'return' to send data back. The result will be automatically JSON stringified. If the script throws an error, it will be caught and returned as an error object. Args: script_string (str): The ExtendScript code to execute. Must use 'return' to send results back. Returns: any: The result returned from the ExtendScript, or an error object containing: - error (str): Error message - line (str): Line number where error occurred Example: script = ''' var comp = app.project.activeItem; return { name: comp.name, layers: comp.numLayers }; ''' result = execute_extend_script(script) """ command = createCommand("executeExtendScript", { "scriptString": script_string }) return sendCommand(command) @mcp.resource("config://get_instructions") def get_instructions() -> str: """Read this first! Returns information and instructions on how to use Illustrator and this API""" return f""" You are an Illustrator export who is creative and loves to help other people learn to use Illustrator. Rules to follow: 1. Think deeply about how to solve the task 2. Always check your work before responding 3. Read the info for the API calls to make sure you understand the requirements and arguments """ # Illustrator Blend Modes (for future use) BLEND_MODES = [ "ADD", "ALPHA_ADD", "CLASSIC_COLOR_BURN", "CLASSIC_COLOR_DODGE", "CLASSIC_DIFFERENCE", "COLOR", "COLOR_BURN", "COLOR_DODGE", "DANCING_DISSOLVE", "DARKEN", "DARKER_COLOR", "DIFFERENCE", "DISSOLVE", "EXCLUSION", "HARD_LIGHT", "HARD_MIX", "HUE", "LIGHTEN", "LIGHTER_COLOR", "LINEAR_BURN", "LINEAR_DODGE", "LINEAR_LIGHT", "LUMINESCENT_PREMUL", "LUMINOSITY", "MULTIPLY", "NORMAL", "OVERLAY", "PIN_LIGHT", "SATURATION", "SCREEN", "SILHOUETE_ALPHA", "SILHOUETTE_LUMA", "SOFT_LIGHT", "STENCIL_ALPHA", "STENCIL_LUMA", "SUBTRACT", "VIVID_LIGHT" ] ``` -------------------------------------------------------------------------------- /uxp/ps/commands/utils.js: -------------------------------------------------------------------------------- ```javascript /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const { app, constants, core } = require("photoshop"); const fs = require("uxp").storage.localFileSystem; const openfs = require('fs') const convertFontSize = (fontSize) => { return (app.activeDocument.resolution / 72) * fontSize } const convertFromPhotoshopFontSize = (photoshopFontSize) => { return photoshopFontSize / (app.activeDocument.resolution / 72); } const createFile = async (filePath) => { let url = `file:${filePath}` const fd = await openfs.open(url, "a+"); await openfs.close(fd) return url } const parseColor = (color) => { try { const c = new app.SolidColor(); c.rgb.red = color.red; c.rgb.green = color.green; c.rgb.blue = color.blue; return c; } catch (e) { throw new Error(`Invalid color values: ${JSON.stringify(color)}`); } }; const getAlignmentMode = (mode) => { switch (mode) { case "LEFT": return "ADSLefts"; case "CENTER_HORIZONTAL": return "ADSCentersH"; case "RIGHT": return "ADSRights"; case "TOP": return "ADSTops"; case "CENTER_VERTICAL": return "ADSCentersV"; case "BOTTOM": return "ADSBottoms"; default: throw new Error( `getAlignmentMode : Unknown alignment mode : ${mode}` ); } }; const getJustificationMode = (value) => { return getConstantValue(constants.Justification, value, "Justification"); }; const getBlendMode = (value) => { return getConstantValue(constants.BlendMode, value, "BlendMode"); }; const getInterpolationMethod = (value) => { return getConstantValue( constants.InterpolationMethod, value, "InterpolationMethod" ); }; const getAnchorPosition = (value) => { return getConstantValue(constants.AnchorPosition, value, "AnchorPosition"); }; const getNewDocumentMode = (value) => { return getConstantValue( constants.NewDocumentMode, value, "NewDocumentMode" ); }; const getConstantValue = (c, v, n) => { let out = c[v.toUpperCase()]; if (!out) { throw new Error(`getConstantValue : Unknown constant value :${c} ${v}`); } return out; }; const selectLayer = (layer, exclusive = false) => { if (exclusive) { clearLayerSelections(); } layer.selected = true; }; const clearLayerSelections = (layers) => { if (!layers) { layers = app.activeDocument.layers; } for (const layer of layers) { layer.selected = false; if (layer.layers && layer.layers.length > 0) { clearLayerSelections(layer.layers); } } }; const setVisibleAllLayers = (visible, layers) => { if (!layers) { layers = app.activeDocument.layers; } for (const layer of layers) { layer.visible = visible if (layer.layers && layer.layers.length > 0) { setVisibleAllLayers(visible, layer.layers) } } }; const findLayer = (id, layers) => { if (!layers) { layers = app.activeDocument.layers; } for (const layer of layers) { if (layer.id === id) { return layer; } if (layer.layers && layer.layers.length > 0) { const found = findLayer(id, layer.layers); if (found) { return found; // Stop as soon as we’ve found the target layer } } } return null; }; const findLayerByName = (name, layers) => { if (!layers) { layers = app.activeDocument.layers; } return app.activeDocument.layers.getByName(name); }; const _saveDocumentAs = async (filePath, fileType) => { let url = await createFile(filePath) let saveFile = await fs.getEntryWithUrl(url); return await execute(async () => { fileType = fileType.toUpperCase() if (fileType == "JPG") { await app.activeDocument.saveAs.jpg(saveFile, { quality:9 }, true) } else if (fileType == "PNG") { await app.activeDocument.saveAs.png(saveFile, { }, true) } else { await app.activeDocument.saveAs.psd(saveFile, { alphaChannels:true, annotations:true, embedColorProfile:true, layers:true, maximizeCompatibility:true, spotColor:true, }, true) } return {savedFilePath:saveFile.nativePath} }); }; const execute = async (callback, commandName = "Executing command...") => { try { return await core.executeAsModal(callback, { commandName: commandName, }); } catch (e) { throw new Error(`Error executing command [modal] : ${e}`); } }; const tokenify = async (url) => { let out = await fs.createSessionToken( await fs.getEntryWithUrl("file:" + url) ); return out; }; const getElementPlacement = (placement) => { return constants.ElementPlacement[placement.toUpperCase()]; }; const hasActiveSelection = () => { return app.activeDocument.selection.bounds != null; }; const getMostRecentlyModifiedFile = async (directoryPath) => { try { // Get directory contents const dirEntries = await openfs.readdir(directoryPath); const fileDetails = []; // Process each file let i = 0 for (const entry of dirEntries) { console.log(i++) const filePath = window.path.join(directoryPath, entry); // Get file stats using lstat try { const stats = await openfs.lstat(filePath); // Skip if it's a directory if (stats.isDirectory()) { continue; } fileDetails.push({ name: entry, path: filePath, modifiedTime: stats.mtime, // Date object modifiedTimestamp: stats.mtimeMs // Use mtimeMs directly instead of getTime() }); } catch (err) { console.log(`Error getting stats for ${filePath}:`, err); // Continue to next file if there's an error with this one continue; } } if (fileDetails.length === 0) { return null; } // Sort by modification timestamp (newest first) fileDetails.sort((a, b) => b.modifiedTimestamp - a.modifiedTimestamp); // Return the most recently modified file return fileDetails[0]; } catch (err) { console.error('Error getting most recently modified file:', err); return null; } } const fileExists = async (filePath) => { try { await openfs.lstat(`file:${filePath}`); return true; } catch (error) { return false; } } const generateDocumentInfo = (document, activeDocument) => { return { name:document.name, id:document.id, isActive: document === activeDocument, path:document.path, saved:document.saved, title:document.title }; } const listOpenDocuments = () => { const docs = app.documents; const activeDocument = app.activeDocument let out = [] for (let doc of docs) { let d = generateDocumentInfo(doc, activeDocument) out.push(d) } return out } module.exports = { findLayerByName, generateDocumentInfo, listOpenDocuments, convertFromPhotoshopFontSize, convertFontSize, setVisibleAllLayers, _saveDocumentAs, getMostRecentlyModifiedFile, fileExists, createFile, parseColor, getAlignmentMode, getJustificationMode, getBlendMode, getInterpolationMethod, getAnchorPosition, getNewDocumentMode, getConstantValue, selectLayer, clearLayerSelections, findLayer, execute, tokenify, getElementPlacement, hasActiveSelection } ``` -------------------------------------------------------------------------------- /uxp/ps/commands/selection.js: -------------------------------------------------------------------------------- ```javascript const { app, constants, action } = require("photoshop"); const { findLayer, execute, parseColor, selectLayer } = require("./utils"); const {hasActiveSelection} = require("./utils") const clearSelection = async () => { await app.activeDocument.selection.selectRectangle( { top: 0, left: 0, bottom: 0, right: 0 }, constants.SelectionType.REPLACE, 0, true ); }; const createMaskFromSelection = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `createMaskFromSelection : Could not find layerId : ${layerId}` ); } await execute(async () => { selectLayer(layer, true); let commands = [ { _obj: "make", at: { _enum: "channel", _ref: "channel", _value: "mask", }, new: { _class: "channel", }, using: { _enum: "userMaskEnabled", _value: "revealSelection", }, }, ]; await action.batchPlay(commands, {}); }); }; const selectSubject = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `selectSubject : Could not find layerId : ${layerId}` ); } return await execute(async () => { selectLayer(layer, true); let commands = [ // Select Subject { _obj: "autoCutout", sampleAllLayers: false, }, ]; await action.batchPlay(commands, {}); }); }; const selectSky = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error(`selectSky : Could not find layerId : ${layerId}`); } return await execute(async () => { selectLayer(layer, true); let commands = [ // Select Sky { _obj: "selectSky", sampleAllLayers: false, }, ]; await action.batchPlay(commands, {}); }); }; const cutSelectionToClipboard = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `cutSelectionToClipboard : Could not find layerId : ${layerId}` ); } if (!hasActiveSelection()) { throw new Error( "cutSelectionToClipboard : Requires an active selection" ); } return await execute(async () => { selectLayer(layer, true); let commands = [ { _obj: "cut", }, ]; await action.batchPlay(commands, {}); }); }; const copyMergedSelectionToClipboard = async (command) => { let options = command.options; if (!hasActiveSelection()) { throw new Error( "copySelectionToClipboard : Requires an active selection" ); } return await execute(async () => { let commands = [{ _obj: "copyMerged", }]; await action.batchPlay(commands, {}); }); }; const copySelectionToClipboard = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `copySelectionToClipboard : Could not find layerId : ${layerId}` ); } if (!hasActiveSelection()) { throw new Error( "copySelectionToClipboard : Requires an active selection" ); } return await execute(async () => { selectLayer(layer, true); let commands = [{ _obj: "copyEvent", copyHint: "pixels", }]; await action.batchPlay(commands, {}); }); }; const pasteFromClipboard = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `pasteFromClipboard : Could not find layerId : ${layerId}` ); } return await execute(async () => { selectLayer(layer, true); let pasteInPlace = options.pasteInPlace; let commands = [ { _obj: "paste", antiAlias: { _enum: "antiAliasType", _value: "antiAliasNone", }, as: { _class: "pixel", }, inPlace: pasteInPlace, }, ]; await action.batchPlay(commands, {}); }); }; const deleteSelection = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `deleteSelection : Could not find layerId : ${layerId}` ); } if (!app.activeDocument.selection.bounds) { throw new Error(`invertSelection : Requires an active selection`); } await execute(async () => { selectLayer(layer, true); let commands = [ { _obj: "delete", }, ]; await action.batchPlay(commands, {}); }); }; const fillSelection = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `fillSelection : Could not find layerId : ${layerId}` ); } if (!app.activeDocument.selection.bounds) { throw new Error(`invertSelection : Requires an active selection`); } await execute(async () => { selectLayer(layer, true); let c = parseColor(options.color).rgb; let commands = [ // Fill { _obj: "fill", color: { _obj: "RGBColor", blue: c.blue, grain: c.green, red: c.red, }, mode: { _enum: "blendMode", _value: options.blendMode.toLowerCase(), }, opacity: { _unit: "percentUnit", _value: options.opacity, }, using: { _enum: "fillContents", _value: "color", }, }, ]; await action.batchPlay(commands, {}); }); }; const selectPolygon = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `selectPolygon : Could not find layerId : ${layerId}` ); } await execute(async () => { selectLayer(layer, true); await app.activeDocument.selection.selectPolygon( options.points, constants.SelectionType.REPLACE, options.feather, options.antiAlias ); }); }; let selectEllipse = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `selectEllipse : Could not find layerId : ${layerId}` ); } await execute(async () => { selectLayer(layer, true); await app.activeDocument.selection.selectEllipse( options.bounds, constants.SelectionType.REPLACE, options.feather, options.antiAlias ); }); }; const selectRectangle = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `selectRectangle : Could not find layerId : ${layerId}` ); } await execute(async () => { selectLayer(layer, true); await app.activeDocument.selection.selectRectangle( options.bounds, constants.SelectionType.REPLACE, options.feather, options.antiAlias ); }); }; const invertSelection = async (command) => { if (!app.activeDocument.selection.bounds) { throw new Error(`invertSelection : Requires an active selection`); } await execute(async () => { let commands = [ { _obj: "inverse", }, ]; await action.batchPlay(commands, {}); }); }; const commandHandlers = { clearSelection, createMaskFromSelection, selectSubject, selectSky, cutSelectionToClipboard, copyMergedSelectionToClipboard, copySelectionToClipboard, pasteFromClipboard, deleteSelection, fillSelection, selectPolygon, selectEllipse, selectRectangle, invertSelection }; module.exports = { commandHandlers }; ``` -------------------------------------------------------------------------------- /uxp/ps/commands/layer_styles.js: -------------------------------------------------------------------------------- ```javascript /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const { action } = require("photoshop"); const { selectLayer, findLayer, execute } = require("./utils") const addDropShadowLayerStyle = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `addDropShadowLayerStyle : Could not find layerId : ${layerId}` ); } await execute(async () => { selectLayer(layer, true); let commands = [ // Set Layer Styles of current layer { _obj: "set", _target: [ { _property: "layerEffects", _ref: "property", }, { _enum: "ordinal", _ref: "layer", _value: "targetEnum", }, ], to: { _obj: "layerEffects", dropShadow: { _obj: "dropShadow", antiAlias: false, blur: { _unit: "pixelsUnit", _value: options.size, }, chokeMatte: { _unit: "pixelsUnit", _value: options.spread, }, color: { _obj: "RGBColor", blue: options.color.blue, grain: options.color.green, red: options.color.red, }, distance: { _unit: "pixelsUnit", _value: options.distance, }, enabled: true, layerConceals: true, localLightingAngle: { _unit: "angleUnit", _value: options.angle, }, mode: { _enum: "blendMode", _value: options.blendMode.toLowerCase(), }, noise: { _unit: "percentUnit", _value: 0.0, }, opacity: { _unit: "percentUnit", _value: options.opacity, }, present: true, showInDialog: true, transferSpec: { _obj: "shapeCurveType", name: "Linear", }, useGlobalAngle: true, }, globalLightingAngle: { _unit: "angleUnit", _value: options.angle, }, scale: { _unit: "percentUnit", _value: 100.0, }, }, }, ]; await action.batchPlay(commands, {}); }); }; const addStrokeLayerStyle = async (command) => { const options = command.options const layerId = options.layerId let layer = findLayer(layerId) if (!layer) { throw new Error( `addStrokeLayerStyle : Could not find layerId : ${layerId}` ); } let position = "centeredFrame" if (options.position == "INSIDE") { position = "insetFrame" } else if (options.position == "OUTSIDE") { position = "outsetFrame" } await execute(async () => { selectLayer(layer, true); let strokeColor = options.color let commands = [ // Set Layer Styles of current layer { "_obj": "set", "_target": [ { "_property": "layerEffects", "_ref": "property" }, { "_enum": "ordinal", "_ref": "layer", "_value": "targetEnum" } ], "to": { "_obj": "layerEffects", "frameFX": { "_obj": "frameFX", "color": { "_obj": "RGBColor", "blue": strokeColor.blue, "grain": strokeColor.green, "red": strokeColor.red }, "enabled": true, "mode": { "_enum": "blendMode", "_value": options.blendMode.toLowerCase() }, "opacity": { "_unit": "percentUnit", "_value": options.opacity }, "overprint": false, "paintType": { "_enum": "frameFill", "_value": "solidColor" }, "present": true, "showInDialog": true, "size": { "_unit": "pixelsUnit", "_value": options.size }, "style": { "_enum": "frameStyle", "_value": position } }, "scale": { "_unit": "percentUnit", "_value": 100.0 } } } ]; await action.batchPlay(commands, {}); }); } const createGradientLayerStyle = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `createGradientAdjustmentLayer : Could not find layerId : ${layerId}` ); } await execute(async () => { selectLayer(layer, true); let angle = options.angle; let colorStops = options.colorStops; let opacityStops = options.opacityStops; let colors = []; for (let c of colorStops) { colors.push({ _obj: "colorStop", color: { _obj: "RGBColor", blue: c.color.blue, grain: c.color.green, red: c.color.red, }, location: Math.round((c.location / 100) * 4096), midpoint: c.midpoint, type: { _enum: "colorStopType", _value: "userStop", }, }); } let opacities = []; for (let o of opacityStops) { opacities.push({ _obj: "transferSpec", location: Math.round((o.location / 100) * 4096), midpoint: o.midpoint, opacity: { _unit: "percentUnit", _value: o.opacity, }, }); } let commands = [ // Make fill layer { _obj: "make", _target: [ { _ref: "contentLayer", }, ], using: { _obj: "contentLayer", type: { _obj: "gradientLayer", angle: { _unit: "angleUnit", _value: angle, }, gradient: { _obj: "gradientClassEvent", colors: colors, gradientForm: { _enum: "gradientForm", _value: "customStops", }, interfaceIconFrameDimmed: 4096.0, name: "Custom", transparency: opacities, }, gradientsInterpolationMethod: { _enum: "gradientInterpolationMethodType", _value: "smooth", }, type: { _enum: "gradientType", _value: options.type.toLowerCase(), }, }, }, }, ]; await action.batchPlay(commands, {}); }); }; const commandHandlers = { createGradientLayerStyle, addStrokeLayerStyle, addDropShadowLayerStyle }; module.exports = { commandHandlers }; ``` -------------------------------------------------------------------------------- /uxp/pr/commands/utils.js: -------------------------------------------------------------------------------- ```javascript /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const app = require("premierepro"); const { TRACK_TYPE, TICKS_PER_SECOND } = require("./consts.js"); const _getSequenceFromId = async (id) => { let project = await app.Project.getActiveProject(); let guid = app.Guid.fromString(id); let sequence = await project.getSequence(guid); if (!sequence) { throw new Error( `_getSequenceFromId : Could not find sequence with id : ${id}` ); } return sequence; }; const _setActiveSequence = async (sequence) => { let project = await app.Project.getActiveProject(); await project.setActiveSequence(sequence); let item = await findProjectItem(sequence.name, project); await app.SourceMonitor.openProjectItem(item); }; const setParam = async (trackItem, componentName, paramName, value) => { const project = await app.Project.getActiveProject(); let param = await getParam(trackItem, componentName, paramName); let keyframe = await param.createKeyframe(value); execute(() => { let action = param.createSetValueAction(keyframe); return [action]; }, project); }; const getParam = async (trackItem, componentName, paramName) => { let components = await trackItem.getComponentChain(); const count = components.getComponentCount(); for (let i = 0; i < count; i++) { const component = components.getComponentAtIndex(i); //search for match name //component name AE.ADBE Opacity const matchName = await component.getMatchName(); if (matchName == componentName) { console.log(matchName); let pCount = component.getParamCount(); for (let j = 0; j < pCount; j++) { const param = component.getParam(j); console.log(param.type); console.log(param); if (param.displayName == paramName) { return param; } } } } }; const addEffect = async (trackItem, effectName) => { let project = await app.Project.getActiveProject(); const effect = await app.VideoFilterFactory.createComponent(effectName); let componentChain = await trackItem.getComponentChain(); execute(() => { let action = componentChain.createAppendComponentAction(effect, 0); //todo, second isnt needed return [action]; }, project); }; /* const findProjectItem2 = async (itemName, project) => { let root = await project.getRootItem(); let rootItems = await root.getItems(); let insertItem; for (const item of rootItems) { if (item.name == itemName) { insertItem = item; break; } } if (!insertItem) { throw new Error( `addItemToSequence : Could not find item named ${itemName}` ); } return insertItem; }; */ const findProjectItem = async (itemName, project) => { let root = await project.getRootItem(); const searchItems = async (parentItem) => { let items = await parentItem.getItems(); // First, check items at this level for (const item of items) { if (item.name === itemName) { return item; } } // If not found, search recursively in bins/folders for (const item of items) { const folderItem = app.FolderItem.cast(item); if (folderItem) { // This is a bin/folder, search inside it const foundItem = await searchItems(folderItem); if (foundItem) { return foundItem; } } } return null; // Not found at this level or in any sub-folders }; const insertItem = await searchItems(root); if (!insertItem) { throw new Error( `addItemToSequence : Could not find item named ${itemName}` ); } return insertItem; }; const execute = (getActions, project) => { try { project.lockedAccess(() => { project.executeTransaction((compoundAction) => { let actions = getActions(); for (const a of actions) { compoundAction.addAction(a); } }); }); } catch (e) { throw new Error(`Error executing locked transaction : ${e}`); } }; const getTracks = async (sequence, trackType) => { let count; if (trackType === TRACK_TYPE.VIDEO) { count = await sequence.getVideoTrackCount(); } else if (trackType === TRACK_TYPE.AUDIO) { count = await sequence.getAudioTrackCount(); } let tracks = []; for (let i = 0; i < count; i++) { let track; if (trackType === TRACK_TYPE.VIDEO) { track = await sequence.getVideoTrack(i); } else if (trackType === TRACK_TYPE.AUDIO) { track = await sequence.getAudioTrack(i); } let out = { index: i, tracks: [], }; let clips = await track.getTrackItems(1, false); if (clips.length === 0) { continue; } let k = 0; for (const c of clips) { let startTimeTicks = (await c.getStartTime()).ticks; let endTimeTicks = (await c.getEndTime()).ticks; let durationTicks = (await c.getDuration()).ticks; let durationSeconds = (await c.getDuration()).seconds; let name = (await c.getProjectItem()).name; let type = await c.getType(); let index = k++; out.tracks.push({ startTimeTicks, endTimeTicks, durationTicks, durationSeconds, name, type, index, }); } tracks.push(out); } return tracks; }; const getSequences = async () => { let project = await app.Project.getActiveProject(); let active = await project.getActiveSequence(); let sequences = await project.getSequences(); let out = []; for (const sequence of sequences) { let size = await sequence.getFrameSize(); //let settings = await sequence.getSettings() //let projectItem = await sequence.getProjectItem() //let name = projectItem.name let name = sequence.name; let id = sequence.guid.toString(); let videoTracks = await getTracks(sequence,TRACK_TYPE.VIDEO); let audioTracks = await getTracks(sequence, TRACK_TYPE.AUDIO); let isActive = active == sequence; let timebase = await sequence.getTimebase() let fps = TICKS_PER_SECOND / timebase let endTime = await sequence.getEndTime() let durationSeconds = await endTime.seconds let durationTicks = await endTime.ticksNumber let ticksPerSecond = TICKS_PER_SECOND out.push({ isActive, name, id, frameSize: { width: size.width, height: size.height }, videoTracks, audioTracks, timebase, fps, durationSeconds, durationTicks, ticksPerSecond }); } return out; }; const getTrack = async (sequence, trackIndex, clipIndex, trackType) => { let trackItems = await getTrackItems(sequence, trackIndex, trackType); let trackItem; let i = 0; for (const t of trackItems) { let index = i++; if (index === clipIndex) { trackItem = t; break; } } if (!trackItem) { throw new Error( `getTrack : trackItemIndex [${clipIndex}] does not exist for track type [${trackType}]` ); } return trackItem; }; /* const getAudioTrack = async (sequence, trackIndex, clipIndex) => { let trackItems = await getAudioTrackItems(sequence, trackIndex) let trackItem; let i = 0 for(const t of trackItems) { let index = i++ if(index === clipIndex) { trackItem = t break } } if(!trackItem) { throw new Error(`getAudioTrack : trackItemIndex [${clipIndex}] does not exist`) } return trackItem } */ const getTrackItems = async (sequence, trackIndex, trackType) => { let track; if (trackType === TRACK_TYPE.AUDIO) { track = await sequence.getAudioTrack(trackIndex); } else if (trackType === TRACK_TYPE.VIDEO) { track = await sequence.getVideoTrack(trackIndex); } if (!track) { throw new Error( `getTrackItems : getTrackItems [${trackIndex}] does not exist. Type : [${trackType}]` ); } let trackItems = await track.getTrackItems(1, false); return trackItems; }; /* const getAudioTrackItems = async (sequence, trackIndex) => { let audioTrack = await sequence.getAudioTrack(trackIndex) if(!audioTrack) { throw new Error(`getAudioTrackItems : getAudioTrackItems [${trackIndex}] does not exist`) } let trackItems = await audioTrack.getTrackItems(1, false) return trackItems } const getVideoTrackItems = async (sequence, trackIndex) => { let videoTrack = await sequence.getVideoTrack(trackIndex) if(!videoTrack) { throw new Error(`getVideoTrackItems : videoTrackIndex [${trackIndex}] does not exist`) } let trackItems = await videoTrack.getTrackItems(1, false) return trackItems } */ /* const getVideoTrack = async (sequence, trackIndex, clipIndex) => { let trackItems = await getVideoTrackItems(sequence, trackIndex) let trackItem; let i = 0 for(const t of trackItems) { let index = i++ if(index === clipIndex) { trackItem = t break } } if(!trackItem) { throw new Error(`getVideoTrack : clipIndex [${clipIndex}] does not exist`) } return trackItem } */ module.exports = { getTrackItems, _getSequenceFromId, _setActiveSequence, setParam, getParam, addEffect, findProjectItem, execute, getTracks, getSequences, getTrack, }; ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/commands.js: -------------------------------------------------------------------------------- ```javascript /* commands.js * Illustrator command handlers */ const getDocuments = async (command) => { const script = ` (function() { try { var result = (function() { if (app.documents.length > 0) { var activeDoc = app.activeDocument; var docs = []; for (var i = 0; i < app.documents.length; i++) { var doc = app.documents[i]; docs.push($.global.createDocumentInfo(doc, activeDoc)); } return docs; } else { return []; } })(); if (result === undefined) { return 'null'; } return JSON.stringify(result); } catch(e) { return JSON.stringify({ error: e.toString(), line: e.line || 'unknown' }); } })(); `; let result = await executeCommand(script); return createPacket(result); } const exportPNG = async (command) => { const options = command.options || {}; // Extract all options into variables const path = options.path; const transparency = options.transparency !== undefined ? options.transparency : true; const antiAliasing = options.antiAliasing !== undefined ? options.antiAliasing : true; const artBoardClipping = options.artBoardClipping !== undefined ? options.artBoardClipping : true; const horizontalScale = options.horizontalScale || 100; const verticalScale = options.verticalScale || 100; const exportType = options.exportType || 'PNG24'; const matte = options.matte; const matteColor = options.matteColor; // Validate required path parameter if (!path) { return createPacket(JSON.stringify({ error: "Path is required for PNG export" })); } const script = ` (function() { try { var result = (function() { if (app.documents.length === 0) { return { error: "No document is currently open" }; } var doc = app.activeDocument; var exportPath = "${path}"; // Export options from variables var exportOptions = { transparency: ${transparency}, antiAliasing: ${antiAliasing}, artBoardClipping: ${artBoardClipping}, horizontalScale: ${horizontalScale}, verticalScale: ${verticalScale}, exportType: "${exportType}" }; ${matte !== undefined ? `exportOptions.matte = ${matte};` : ''} ${matteColor ? `exportOptions.matteColor = ${JSON.stringify(matteColor)};` : ''} // Use the global helper function if available, otherwise inline export if (typeof $.global.exportToPNG === 'function') { return $.global.exportToPNG(doc, exportPath, exportOptions); } else { // Inline export logic try { // Create PNG export options var pngOptions = exportOptions.exportType === 'PNG8' ? new ExportOptionsPNG8() : new ExportOptionsPNG24(); pngOptions.transparency = exportOptions.transparency; pngOptions.antiAliasing = exportOptions.antiAliasing; pngOptions.artBoardClipping = exportOptions.artBoardClipping; pngOptions.horizontalScale = exportOptions.horizontalScale; pngOptions.verticalScale = exportOptions.verticalScale; ${matte !== undefined ? `pngOptions.matte = ${matte};` : ''} ${matteColor ? ` // Set matte color pngOptions.matteColor.red = ${matteColor.red}; pngOptions.matteColor.green = ${matteColor.green}; pngOptions.matteColor.blue = ${matteColor.blue}; ` : ''} // Create file object var exportFile = new File(exportPath); // Determine export type var exportType = exportOptions.exportType === 'PNG8' ? ExportType.PNG8 : ExportType.PNG24; // Export the file doc.exportFile(exportFile, exportType, pngOptions); return { success: true, filePath: exportFile.fsName, fileExists: exportFile.exists, options: exportOptions, documentName: doc.name }; } catch(exportError) { return { success: false, error: exportError.toString(), filePath: exportPath, options: exportOptions, documentName: doc.name }; } } })(); if (result === undefined) { return 'null'; } return JSON.stringify(result); } catch(e) { return JSON.stringify({ error: e.toString(), line: e.line || 'unknown' }); } })(); `; let result = await executeCommand(script); return createPacket(result); } const openFile = async (command) => { const options = command.options || {}; // Extract path parameter const path = options.path; // Validate required path parameter if (!path) { return createPacket(JSON.stringify({ error: "Path is required to open an Illustrator file" })); } const script = ` (function() { try { var result = (function() { var filePath = "${path}"; try { // Create file object var fileToOpen = new File(filePath); // Check if file exists if (!fileToOpen.exists) { return { success: false, error: "File does not exist at the specified path", filePath: filePath }; } // Open the document var doc = app.open(fileToOpen); return { success: true, }; } catch(openError) { return { success: false, error: openError.toString(), filePath: filePath }; } })(); if (result === undefined) { return 'null'; } return JSON.stringify(result); } catch(e) { return JSON.stringify({ error: e.toString(), line: e.line || 'unknown' }); } })(); `; let result = await executeCommand(script); return createPacket(result); }; const getActiveDocumentInfo = async (command) => { const script = ` (function() { try { var result = (function() { if (app.documents.length > 0) { var doc = app.activeDocument; return $.global.createDocumentInfo(doc, doc); } else { return { error: "No document is currently open" }; } })(); if (result === undefined) { return 'null'; } return JSON.stringify(result); } catch(e) { return JSON.stringify({ error: e.toString(), line: e.line || 'unknown' }); } })(); `; let result = await executeCommand(script); return createPacket(result); } // Execute Illustrator command via ExtendScript function executeCommand(script) { return new Promise((resolve, reject) => { const csInterface = new CSInterface(); csInterface.evalScript(script, (result) => { if (result === 'EvalScript error.') { reject(new Error('ExtendScript execution failed')); } else { try { resolve(JSON.parse(result)); } catch (e) { resolve(result); } } }); }); } async function executeExtendScript(command) { const options = command.options const scriptString = options.scriptString; const script = ` (function() { try { ${scriptString} } catch(e) { return JSON.stringify({ error: e.toString(), line: e.line || 'unknown' }); } })(); `; const result = await executeCommand(script); return createPacket(result); } const createPacket = (result) => { return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } const parseAndRouteCommand = async (command) => { let action = command.action; let f = commandHandlers[action]; if (typeof f !== "function") { throw new Error(`Unknown Command: ${action}`); } console.log(f.name) return await f(command); }; // Execute commands /* async function executeCommand(command) { switch(command.action) { case "getLayers": return await getLayers(); case "executeExtendScript": return await executeExtendScript(command); default: throw new Error(`Unknown command: ${command.action}`); } }*/ const commandHandlers = { executeExtendScript, getDocuments, getActiveDocumentInfo, exportPNG, openFile }; ``` -------------------------------------------------------------------------------- /uxp/ps/commands/core.js: -------------------------------------------------------------------------------- ```javascript /* MIT License * * Copyright (c) 2025 Mike Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const { app, constants, action, imaging } = require("photoshop"); const fs = require("uxp").storage.localFileSystem; const { _saveDocumentAs, parseColor, getAlignmentMode, getNewDocumentMode, selectLayer, findLayer, findLayerByName, execute, tokenify, hasActiveSelection, listOpenDocuments } = require("./utils"); const { rasterizeLayer } = require("./layers").commandHandlers; const openFile = async (command) => { let options = command.options; await execute(async () => { let entry = null; try { entry = await fs.getEntryWithUrl("file:" + options.filePath); } catch (e) { throw new Error( "openFile: Could not create file entry. File probably does not exist." ); } await app.open(entry); }); }; const placeImage = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error(`placeImage : Could not find layerId : ${layerId}`); } await execute(async () => { selectLayer(layer, true); let layerId = layer.id; let imagePath = await tokenify(options.imagePath); let commands = [ // Place { ID: layerId, _obj: "placeEvent", freeTransformCenterState: { _enum: "quadCenterState", _value: "QCSAverage", }, null: { _kind: "local", _path: imagePath, }, offset: { _obj: "offset", horizontal: { _unit: "pixelsUnit", _value: 0.0, }, vertical: { _unit: "pixelsUnit", _value: 0.0, }, }, replaceLayer: { _obj: "placeEvent", to: { _id: layerId, _ref: "layer", }, }, }, { _obj: "set", _target: [ { _enum: "ordinal", _ref: "layer", _value: "targetEnum", }, ], to: { _obj: "layer", name: layerId, }, }, ]; await action.batchPlay(commands, {}); await rasterizeLayer(command); }); }; const getDocumentImage = async (command) => { let out = await execute(async () => { const pixelsOpt = { applyAlpha: true }; const imgObj = await imaging.getPixels(pixelsOpt); const base64Data = await imaging.encodeImageData({ imageData: imgObj.imageData, base64: true, }); const result = { base64Image: base64Data, dataUrl: `data:image/jpeg;base64,${base64Data}`, width: imgObj.imageData.width, height: imgObj.imageData.height, colorSpace: imgObj.imageData.colorSpace, components: imgObj.imageData.components, format: "jpeg", }; imgObj.imageData.dispose(); return result; }); return out; }; const getDocumentInfo = async (command) => { let doc = app.activeDocument; let path = doc.path; let out = { height: doc.height, width: doc.width, colorMode: doc.mode.toString(), pixelAspectRatio: doc.pixelAspectRatio, resolution: doc.resolution, path: path, saved: path.length > 0, hasUnsavedChanges: !doc.saved, }; return out; }; const cropDocument = async (command) => { let options = command.options; if (!hasActiveSelection()) { throw new Error("cropDocument : Requires an active selection"); } return await execute(async () => { let commands = [ // Crop { _obj: "crop", delete: true, }, ]; await action.batchPlay(commands, {}); }); }; const removeBackground = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `removeBackground : Could not find layerId : ${layerId}` ); } await execute(async () => { selectLayer(layer, true); let commands = [ // Remove Background { _obj: "removeBackground", }, ]; await action.batchPlay(commands, {}); }); }; const alignContent = async (command) => { let options = command.options; let layerId = options.layerId; let layer = findLayer(layerId); if (!layer) { throw new Error( `alignContent : Could not find layerId : ${layerId}` ); } if (!app.activeDocument.selection.bounds) { throw new Error(`alignContent : Requires an active selection`); } await execute(async () => { let m = getAlignmentMode(options.alignmentMode); selectLayer(layer, true); let commands = [ { _obj: "align", _target: [ { _enum: "ordinal", _ref: "layer", _value: "targetEnum", }, ], alignToCanvas: false, using: { _enum: "alignDistributeSelector", _value: m, }, }, ]; await action.batchPlay(commands, {}); }); }; const generateImage = async (command) => { let options = command.options; await execute(async () => { let doc = app.activeDocument; await doc.selection.selectAll(); let contentType = "none"; const c = options.contentType.toLowerCase() if (c === "photo" || c === "art") { contentType = c; } let commands = [ // Generate Image current document { _obj: "syntheticTextToImage", _target: [ { _enum: "ordinal", _ref: "document", _value: "targetEnum", }, ], documentID: doc.id, layerID: 0, prompt: options.prompt, serviceID: "clio", serviceOptionsList: { clio: { _obj: "clio", clio_advanced_options: { text_to_image_styles_options: { text_to_image_content_type: contentType, text_to_image_effects_count: 0, text_to_image_effects_list: [ "none", "none", "none", ], }, }, dualCrop: true, gentech_workflow_name: "text_to_image", gi_ADVANCED: '{"enable_mts":true}', gi_CONTENT_PRESERVE: 0, gi_CROP: false, gi_DILATE: false, gi_ENABLE_PROMPT_FILTER: true, gi_GUIDANCE: 6, gi_MODE: "ginp", gi_NUM_STEPS: -1, gi_PROMPT: options.prompt, gi_SEED: -1, gi_SIMILARITY: 0, }, }, workflow: "text_to_image", workflowType: { _enum: "genWorkflow", _value: "text_to_image", }, }, // Rasterize current layer { _obj: "rasterizeLayer", _target: [ { _enum: "ordinal", _ref: "layer", _value: "targetEnum", }, ], }, ]; let o = await action.batchPlay(commands, {}); let layerId = o[0].layerID; //let l = findLayerByName(options.prompt); let l = findLayer(layerId); l.name = options.layerName; }); }; const generativeFill = async (command) => { const options = command.options; const layerId = options.layerId; const prompt = options.prompt; const layer = findLayer(layerId); if (!layer) { throw new Error( `generativeFill : Could not find layerId : ${layerId}` ); } if(!hasActiveSelection()) { throw new Error( `generativeFill : Requires an active selection.` ); } await execute(async () => { let doc = app.activeDocument; let contentType = "none"; const c = options.contentType.toLowerCase() if (c === "photo" || c === "art") { contentType = c; } let commands = [ // Generative Fill current document { "_obj": "syntheticFill", "_target": [ { "_enum": "ordinal", "_ref": "document", "_value": "targetEnum" } ], "documentID": doc.id, "layerID": layerId, "prompt": prompt, "serviceID": "clio", "serviceOptionsList": { "clio": { "_obj": "clio", "dualCrop": true, "gi_ADVANCED": "{\"enable_mts\":true}", "gi_CONTENT_PRESERVE": 0, "gi_CROP": false, "gi_DILATE": false, "gi_ENABLE_PROMPT_FILTER": true, "gi_GUIDANCE": 6, "gi_MODE": "tinp", "gi_NUM_STEPS": -1, "gi_PROMPT": prompt, "gi_SEED": -1, "gi_SIMILARITY": 0, clio_advanced_options: { text_to_image_styles_options: { text_to_image_content_type: contentType, text_to_image_effects_count: 0, text_to_image_effects_list: [ "none", "none", "none", ], }, }, } }, "serviceVersion": "clio3", "workflowType": { "_enum": "genWorkflow", "_value": "in_painting" }, "workflow_to_active_service_identifier_map": { "gen_harmonize": "clio3", "generate_background": "clio3", "generate_similar": "clio3", "generativeUpscale": "fal_aura_sr", "in_painting": "clio3", "instruct_edit": "clio3", "out_painting": "clio3", "text_to_image": "clio3" } } ]; let o = await action.batchPlay(commands, {}); let id = o[0].layerID; //let l = findLayerByName(options.prompt); let l = findLayer(id); l.name = options.layerName; }); }; const saveDocument = async (command) => { await execute(async () => { await app.activeDocument.save(); }); }; const saveDocumentAs = async (command) => { let options = command.options; return await _saveDocumentAs(options.filePath, options.fileType); }; const setActiveDocument = async (command) => { let options = command.options; let documentId = options.documentId; let docs = listOpenDocuments(); for (let doc of docs) { if (doc.id === documentId) { await execute(async () => { app.activeDocument = doc; }); return } } } const getDocuments = async (command) => { return listOpenDocuments() } const duplicateDocument = async (command) => { let options = command.options; let name = options.name await execute(async () => { const doc = app.activeDocument; await doc.duplicate(name) }); }; const createDocument = async (command) => { let options = command.options; let colorMode = getNewDocumentMode(command.options.colorMode); let fillColor = parseColor(options.fillColor); await execute(async () => { await app.createDocument({ typename: "DocumentCreateOptions", width: options.width, height: options.height, resolution: options.resolution, mode: colorMode, fill: constants.DocumentFill.COLOR, fillColor: fillColor, profile: "sRGB IEC61966-2.1", }); let background = findLayerByName("Background"); background.allLocked = false; background.name = "Background"; }); }; const executeBatchPlayCommand = async (commands) => { let options = commands.options; let c = options.commands; let out = await execute(async () => { let o = await action.batchPlay(c, {}); return o[0] }); console.log(out) return out; } const commandHandlers = { generativeFill, executeBatchPlayCommand, setActiveDocument, getDocuments, duplicateDocument, getDocumentImage, openFile, placeImage, getDocumentInfo, cropDocument, removeBackground, alignContent, generateImage, saveDocument, saveDocumentAs, createDocument, }; module.exports = { commandHandlers, }; ```