This is page 1 of 6. Use http://codebase.md/mikechambers/adb-mcp?lines=true&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: -------------------------------------------------------------------------------- ``` 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/.debug: -------------------------------------------------------------------------------- ``` 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <ExtensionList> 3 | <Extension Id="com.mikechambers.ae.mcp"> 4 | <HostList> 5 | <Host Name="AEFT" Port="8088"/> 6 | </Host> 7 | </Extension> 8 | </ExtensionList> ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/.debug: -------------------------------------------------------------------------------- ``` 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <ExtensionList> 3 | <Extension Id="com.mikechambers.ae.mcp"> 4 | <HostList> 5 | <Host Name="ILST" Port="8089"/> 6 | </Host> 7 | </Extension> 8 | </ExtensionList> ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | *.dxt 2 | *.ccx 3 | 4 | dist/ 5 | 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | ``` -------------------------------------------------------------------------------- /adb-proxy-socket/.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | ``` -------------------------------------------------------------------------------- /mcp/.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | ``` -------------------------------------------------------------------------------- /adb-proxy-socket/README.md: -------------------------------------------------------------------------------- ```markdown 1 | ### Package 2 | 3 | In order to package executables: 4 | 5 | ``` 6 | npm install -g pkg 7 | pkg . 8 | ``` ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # adb-mcp 2 | 3 | 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. 4 | 5 | The project is not endorsed by nor supported by Adobe. 6 | 7 | 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. 8 | 9 | Example use cases include: 10 | 11 | - 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). 12 | - 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. 13 | - Asking Claude to generate custom Photoshop tutorials for you, by creating an example file, then step by step instructions on how to recreate. 14 | - As a Photoshop utility tool (have Claude rename all of your layers into a consistent format) 15 | - Have Claude create new Premiere projects pre-populations with clips, transitions, effects and Audio 16 | 17 | [View Video Examples](https://www.youtube.com/playlist?list=PLrZcuHfRluqt5JQiKzMWefUb0Xumb7MkI) 18 | 19 | The Premiere agent is a bit more limited in functionality compared to the Photoshop agent, due to current limitations of the Premiere plugin API. 20 | 21 | ## How it works 22 | 23 | The proof of concept works by providing: 24 | 25 | - A MCP Server that provides an interface to functionality within Adobe Photoshop to the AI / LLM 26 | - A Node based command proxy server that sits between the MCP server and Adobe app plugins 27 | - An Adobe app (Photoshop and Premiere) plugin that listens for commands, and drives the programs 28 | 29 | **AI** <-> **MCP Server** <-> **Command Proxy Server** <-> **Photoshop / Premiere UXP Plugin** <-> **Photoshop / Premiere** 30 | 31 | 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). 32 | 33 | ## Requirements 34 | 35 | In order to run this, the following is required: 36 | 37 | - AI LLM with support for MCP Protocol (tested with Claude desktop on Mac & Windows, and OpenAI Agent SDK) 38 | - Python 3, which is used to run the MCP server provided with this project 39 | - NodeJS, used to provide a proxy between the MCP server and Photoshop 40 | - Adobe UXP Developer tool (available via Creative Cloud) used to install and debug the Photoshop / Premiere plugin used to connect to the proxy 41 | - Adobe Photoshop (26.0 or greater) with the MCP Plugin installed or Adobe Premiere Beta (25.3 Build 46 or greater) 42 | 43 | 44 | ## Installation 45 | 46 | This guide assumes you're using Claude Desktop. Other MCP-compatible AI applications should work similarly. 47 | 48 | 49 | ### Download Source Code 50 | Clone or download the source code from the [main project page](https://github.com/mikechambers/adb-mcp). 51 | 52 | ### Install Claude Desktop 53 | 1. Download and install [Claude Desktop](https://claude.ai/download) 54 | 2. Launch Claude Desktop to verify it works 55 | 56 | Note, you can use any client / code that supports MCP, just follow its instructions for how to configure. 57 | 58 | ### Install MCP for Development 59 | Navigate to the project directory and run: 60 | 61 | #### Photoshop 62 | ```bash 63 | uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with numpy ps-mcp.py 64 | ``` 65 | 66 | #### Premiere Pro 67 | ```bash 68 | uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with pillow pr-mcp.py 69 | ``` 70 | 71 | #### InDesign 72 | ```bash 73 | uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with pillow id-mcp.py 74 | ``` 75 | 76 | #### AfterEffects 77 | ```bash 78 | uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with pillow ae-mcp.py 79 | ``` 80 | 81 | #### Illustrator 82 | ```bash 83 | uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with pillow ai-mcp.py 84 | ``` 85 | 86 | Restart Claude Desktop after installation. 87 | 88 | ### Set Up Proxy Server 89 | 90 | #### Using Prebuilt Executables (Recommended) 91 | 92 | 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`). 93 | 2. Unzip the executable. 94 | 3. Double click or run from the console / terminal 95 | 96 | #### Running from Source 97 | 98 | 1. Navigate to the adb-proxy-socket directory 99 | 2. Run `node proxy.js` 100 | 101 | You should see a message like: 102 | `Photoshop MCP Command proxy server running on ws://localhost:3001` 103 | 104 | **Keep this running** — the proxy server must stay active for Claude to communicate with Adobe plugins. 105 | 106 | ### Install Plugins 107 | 108 | #### Photoshop, Premiere Pro, InDesign (UXP) 109 | 110 | 1. Launch **UXP Developer Tools** from Creative Cloud 111 | 2. Enable developer mode when prompted 112 | 3. Select **File > Add Plugin** 113 | 4. Navigate to the appropriate directory and select **manifest.json**: 114 | - **Photoshop**: `uxp/ps/manifest.json` 115 | - **Premiere Pro**: `uxp/pr/manifest.json` 116 | - **InDesign**: `uxp/id/manifest.json` 117 | 5. Click **Load** 118 | 6. In your Adobe application, open the plugin panel and click **Connect** 119 | 120 | ##### Enable Developer Mode in Photoshop 121 | 122 | **For Photoshop:** 123 | 1. Launch Photoshop (2025/26.0 or greater) 124 | 2. Go to **Settings > Plugins** and check **"Enable Developer Mode"** 125 | 3. Restart Photoshop 126 | 127 | #### AfterEffects, Illustrator (CEP) 128 | 129 | ##### Mac 130 | 1. Make sure the following directory exists (if it doesn't then create the directories) 131 | `/Users/USERNAME/Library/Application Support/Adobe/CEP/extensions` 132 | 133 | 2. Navigate to the extensions directory and create a symlink that points to the AfterEffect / Illustrator plugin in the CEP directory. 134 | ```bash 135 | cd /Users/USERNAME/Library/Application Support/Adobe/CEP/extensions 136 | ln -s /Users/USERNAME/src/adb-mcp/cep/com.mikechambers.ae com.mikechambers.ae 137 | ``` 138 | or 139 | ```bash 140 | cd /Users/USERNAME/Library/Application Support/Adobe/CEP/extensions 141 | ln -s /Users/USERNAME/src/adb-mcp/cep/com.mikechambers.ai com.mikechambers.ai 142 | ``` 143 | 144 | ##### Windows 145 | 1. Make sure the following directory exists (if it doesn't then create the directories) 146 | `C:\Users\USERNAME\AppData\Roaming\Adobe\CEP\extensions` 147 | 148 | 2. Open Command Prompt as Administrator (or enable Developer Mode in Windows Settings) 149 | 150 | 3. Create a junction or symbolic link that points to the AfterEffect / Illustrator plugin in the CEP directory: 151 | ```cmd 152 | mklink /D "C:\Users\USERNAME\AppData\Roaming\Adobe\CEP\extensions\com.mikechambers.ae" "C:\Users\USERNAME\src\adb-mcp\cep\com.mikechambers.ae" 153 | ``` 154 | or 155 | ```cmd 156 | mklink /D "C:\Users\USERNAME\AppData\Roaming\Adobe\CEP\extensions\com.mikechambers.ai" "C:\Users\USERNAME\src\adb-mcp\cep\com.mikechambers.ai" 157 | ``` 158 | 159 | Note if you don't want to symlink, you can copy com.mikechambers.ae / com.mikechambers.ao into the CEP directory. 160 | 161 | ### Using Claude with Adobe Apps 162 | 163 | Launch the following: 164 | 165 | 1. Claude Desktop 166 | 2. adb-proxy-socket node server 167 | 3. Launch Photoshop, Premiere, InDesign, AfterEffects, Illustrator 168 | 169 | _TIP: Create a project for Photoshop / Premiere Pro in Claude and pre-load any app specific instructions in its Project knowledge._ 170 | 171 | #### Photoshop 172 | 1. Launch UXP Developer Tool and click the Load button for _Photoshop MCP Agent_ 173 | 2. In Photoshop, if the MCP Agent panel is not open, open _Plugins > Photoshop MCP Agent > Photoshop MCP Agent_ 174 | 3. Click connect in the agent panel in Photoshop 175 | 176 | 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_. 177 | 178 | 179 | 180 | #### Premiere 181 | 1. Launch UXP Developer Tool and click the Load button for _Premiere MCP Agent_ 182 | 2. In Premiere, if the MCP Agent panel is not open, open _Window > UXP Plugins > Premiere MCP Agent > Premiere MCP Agent_ 183 | 3. Click connect in the agent panel in Photoshop 184 | 185 | #### InDesign 186 | 1. Launch UXP Developer Tool and click the Load button for InDesitn MCP Agent_ 187 | 2. In InDesign, if the MCP Agent panel is not open, open _Plugins > InDesign MCP Agent > InDesign MCP Agent_ 188 | 3. Click connect in the agent panel in Photoshop 189 | 190 | #### AfterEffects 191 | 1. _Window > Extensions > Illustrator MCP Agent_ 192 | 193 | #### Illustrator 194 | 195 | 1. Open a file (the plugin won't launch unless a file is open) 196 | 2. _Window > Extensions > Illustrator MCP Agent_ 197 | 198 | 199 | Note, you must reload the plugin via the UXP Developer app every time you restart Photoshop, Premiere and InDesign. 200 | 201 | ### Setting up session 202 | 203 | 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. 204 | 205 | <img src="images/claud-attach-mcp.png" width="300"> 206 | 207 | This will help reduce errors when the AI is using the app. 208 | 209 | 210 | ### Prompting 211 | 212 | At anytime, you can ask the following: 213 | 214 | ``` 215 | Can you list what apis / functions are available for working with Photoshop / Premiere? 216 | ``` 217 | 218 | and it will list out all of the functionality available. 219 | 220 | When prompting, you do not need to reference the APIs, just use natural language to give instructions. 221 | 222 | For example: 223 | 224 | ``` 225 | Create a new Photoshop file with a blue background, that is 1080 width by 720 height at 300 dpi 226 | ``` 227 | 228 | ``` 229 | Create a new Photoshop file for an instagram post 230 | ``` 231 | 232 | ``` 233 | Create a double exposure image in Photoshop of a woman and a forest 234 | ``` 235 | 236 | ``` 237 | Generate an image of a forest, and then add a clipping mask to only show the center in a circle 238 | ``` 239 | ``` 240 | Make something cool with photoshop 241 | ``` 242 | 243 | ``` 244 | Add cross fade transitions between all of the clips on the timeline in Premiere 245 | ``` 246 | 247 | 248 | ### Tips 249 | 250 | #### General 251 | * 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. 252 | * When prompting, ask the AI to think about and check its work. 253 | * The more you guide it (i.e. "consider using clipping masks") the better the results 254 | * The more advanced the model, or the more resources given to the model the better and more creative the AI is. 255 | * 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. 256 | * 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. 257 | 258 | 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. 259 | 260 | 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). 261 | 262 | The Photoshop plugin has more functionality that Premiere. 263 | 264 | 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). 265 | 266 | #### Photoshop 267 | 268 | * You can ask the AI to look at the content of the Photoshop file and it should be able to then see the output. 269 | * 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. 270 | * 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). 271 | * You can ask the AI for suggestions. It comes up with really useful ideas / feedback sometimes. 272 | 273 | #### Premiere 274 | 275 | * Currently the plugin assumes you are just working with a single sequence. 276 | * Pair the Premiere Pro MCP with the [media-utils-mcp](https://github.com/mikechambers/media-utils-mcp) to expand functionality. 277 | 278 | 279 | ### Troubleshooting 280 | 281 | #### MCP won't run in Claude 282 | 283 | 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). 284 | 285 | #### All fonts not available 286 | 287 | 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. 288 | 289 | You can tell the AI to use a specific font, using its postscript name. 290 | 291 | #### Plugin won't install or connect 292 | 293 | * Make sure the app is running before you try to load the plugin. 294 | * In the UXP developer tool click the debug button next to load, and see if there are any errors. 295 | * Make sure the node / proxy server is running. If you plugin connects you should see output similar to: 296 | 297 | ``` 298 | adb-mcp Command proxy server running on ws://localhost:3001 299 | User connected: Ud6L4CjMWGAeofYAAAAB 300 | Client Ud6L4CjMWGAeofYAAAAB registered for application: photoshop 301 | ``` 302 | 303 | * 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". 304 | 305 | #### Errors within AI client 306 | 307 | * 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. 308 | * 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. 309 | * 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). 310 | 311 | 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). 312 | 313 | ## Development 314 | 315 | Adding new functionality is relatively easy, and requires: 316 | 317 | 1. Adding the API and parameters in the *mcp/ps-mcp.py* / *mcp/pr-mcp.py* file (which is used by the AI) 318 | 2. Implementing the API in the *uxp/ps/commands/index.js* / *uxp/pr/commands/index.js* file. 319 | 320 | This [thread](https://github.com/mikechambers/adb-mcp/issues/10#issuecomment-3191698528) has some info on how to add functionality. 321 | 322 | ## Questions, Feature Requests, Feedback 323 | 324 | If you have any questions, feature requests, need help, or just want to chat, join the [discord](https://discord.gg/fgxw9t37D7). 325 | 326 | You can also log bugs and feature requests on the [issues page](https://github.com/mikechambers/adb-mcp/issues). 327 | 328 | ## License 329 | 330 | Project released under a [MIT License](LICENSE.md). 331 | 332 | [](LICENSE.md) 333 | 334 | 335 | ``` -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- ```markdown 1 | MIT License 2 | 3 | Copyright (c) 2025 Mike Chambers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | ``` -------------------------------------------------------------------------------- /mcp/requirements.txt: -------------------------------------------------------------------------------- ``` 1 | fonttools 2 | python-socketio 3 | mcp 4 | requests 5 | websocket-client ``` -------------------------------------------------------------------------------- /uxp/id/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "uxp-template-ps-starter", 3 | "version": "1.0.0", 4 | "description": "Adobe InDesign MCP Agent Plugin.", 5 | "author": "Mike Chambers ([email protected])", 6 | "license": "MIT" 7 | } 8 | ``` -------------------------------------------------------------------------------- /uxp/pr/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "uxp-template-ps-starter", 3 | "version": "1.0.0", 4 | "description": "Adobe Photoshop MCP Agent Plugin.", 5 | "author": "Mike Chambers ([email protected])", 6 | "license": "MIT" 7 | } 8 | ``` -------------------------------------------------------------------------------- /uxp/ps/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "uxp-template-ps-starter", 3 | "version": "1.0.0", 4 | "description": "Adobe Photoshop MCP Agent Plugin.", 5 | "author": "Mike Chambers ([email protected])", 6 | "license": "MIT" 7 | } 8 | ``` -------------------------------------------------------------------------------- /uxp/id/style.css: -------------------------------------------------------------------------------- ```css 1 | body { 2 | color: white; 3 | padding: 16px; 4 | font-family: Arial, sans-serif; 5 | } 6 | 7 | li:before { 8 | content: '• '; 9 | width: 3em; 10 | } 11 | 12 | #layers { 13 | border: 1px solid #808080; 14 | border-radius: 4px; 15 | padding: 16px; 16 | } 17 | 18 | footer { 19 | position: fixed; 20 | bottom: 16px; 21 | left: 16px; 22 | } ``` -------------------------------------------------------------------------------- /uxp/pr/style.css: -------------------------------------------------------------------------------- ```css 1 | body { 2 | color: white; 3 | padding: 16px; 4 | font-family: Arial, sans-serif; 5 | } 6 | 7 | li:before { 8 | content: '• '; 9 | width: 3em; 10 | } 11 | 12 | #layers { 13 | border: 1px solid #808080; 14 | border-radius: 4px; 15 | padding: 16px; 16 | } 17 | 18 | footer { 19 | position: fixed; 20 | bottom: 16px; 21 | left: 16px; 22 | } ``` -------------------------------------------------------------------------------- /uxp/ps/style.css: -------------------------------------------------------------------------------- ```css 1 | body { 2 | color: white; 3 | padding: 16px; 4 | font-family: Arial, sans-serif; 5 | } 6 | 7 | li:before { 8 | content: '• '; 9 | width: 3em; 10 | } 11 | 12 | #layers { 13 | border: 1px solid #808080; 14 | border-radius: 4px; 15 | padding: 16px; 16 | } 17 | 18 | footer { 19 | position: fixed; 20 | bottom: 16px; 21 | left: 16px; 22 | } ``` -------------------------------------------------------------------------------- /uxp/id/index.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html> 3 | 4 | <head> 5 | <script src="main.js"></script> 6 | <link rel="stylesheet" href="style.css"> 7 | </head> 8 | 9 | <body> 10 | 11 | 12 | 13 | <div> 14 | <sp-button id="btnStart">Connect</sp-button> 15 | </div> 16 | <p> </p> 17 | <div> 18 | <sp-checkbox id="chkConnectOnLaunch">Connect on Launch</sp-checkbox> 19 | </div> 20 | <footer> 21 | <div> 22 | <div>Created by Mike Chambers</div> 23 | <div>https://github.com/mikechambers/adb-mcp</div> 24 | </div> 25 | </footer> 26 | </body> 27 | 28 | </html> ``` -------------------------------------------------------------------------------- /uxp/pr/index.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html> 3 | 4 | <head> 5 | <script src="main.js"></script> 6 | <link rel="stylesheet" href="style.css"> 7 | </head> 8 | 9 | <body> 10 | 11 | 12 | 13 | <div> 14 | <sp-button id="btnStart">Connect</sp-button> 15 | </div> 16 | <p> </p> 17 | <div> 18 | <sp-checkbox id="chkConnectOnLaunch">Connect on Launch</sp-checkbox> 19 | </div> 20 | <footer> 21 | <div> 22 | <div>Created by Mike Chambers</div> 23 | <div>https://github.com/mikechambers/adb-mcp</div> 24 | </div> 25 | </footer> 26 | </body> 27 | 28 | </html> ``` -------------------------------------------------------------------------------- /uxp/ps/index.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html> 3 | 4 | <head> 5 | <script src="main.js"></script> 6 | <link rel="stylesheet" href="style.css"> 7 | </head> 8 | 9 | <body> 10 | 11 | 12 | 13 | <div> 14 | <sp-button id="btnStart">Connect</sp-button> 15 | </div> 16 | <p> </p> 17 | <div> 18 | <sp-checkbox id="chkConnectOnLaunch">Connect on Launch</sp-checkbox> 19 | </div> 20 | <footer> 21 | <div> 22 | <div>Created by Mike Chambers</div> 23 | <div>https://github.com/mikechambers/adb-mcp</div> 24 | </div> 25 | </footer> 26 | </body> 27 | 28 | </html> ``` -------------------------------------------------------------------------------- /adb-proxy-socket/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "adb-proxy-socket", 3 | "version": "0.85.1", 4 | "description": "Proxy socket.io node server for Adobe MCP plugins", 5 | "main": "proxy.js", 6 | "bin": "proxy.js", 7 | "scripts": { 8 | "build": "pkg ." 9 | }, 10 | "author": "Mike Chambers", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.21.2", 14 | "socket.io": "^4.8.1", 15 | "socket.io-client": "^4.8.1" 16 | }, 17 | "pkg": { 18 | "targets": [ 19 | "node18-macos-x64", 20 | "node18-macos-arm64", 21 | "node18-win-x64" 22 | ], 23 | "outputPath": "dist" 24 | } 25 | } 26 | ``` -------------------------------------------------------------------------------- /mcp/core.py: -------------------------------------------------------------------------------- ```python 1 | import logger 2 | 3 | application = None 4 | socket_client = None 5 | 6 | def init(app, socket): 7 | global application, socket_client 8 | application = app 9 | socket_client = socket 10 | 11 | 12 | def createCommand(action:str, options:dict) -> str: 13 | command = { 14 | "application":application, 15 | "action":action, 16 | "options":options 17 | } 18 | 19 | return command 20 | 21 | def sendCommand(command:dict): 22 | 23 | response = socket_client.send_message_blocking(command) 24 | 25 | logger.log(f"Final response: {response['status']}") 26 | return response ``` -------------------------------------------------------------------------------- /uxp/pr/commands/consts.js: -------------------------------------------------------------------------------- ```javascript 1 | const TRACK_TYPE = { 2 | "VIDEO":"VIDEO", 3 | "AUDIO":"AUDIO" 4 | } 5 | 6 | TICKS_PER_SECOND = 254016000000; 7 | 8 | const BLEND_MODES = { 9 | COLOR: 0, 10 | COLORBURN: 1, 11 | COLORDODGE: 2, 12 | DARKEN: 3, 13 | DARKERCOLOR: 4, 14 | DIFFERENCE: 5, 15 | DISSOLVE: 6, 16 | EXCLUSION: 7, 17 | HARDLIGHT: 8, 18 | HARDMIX: 9, 19 | HUE: 10, 20 | LIGHTEN: 11, 21 | LIGHTERCOLOR: 12, 22 | LINEARBURN: 13, 23 | LINEARDODGE: 14, 24 | LINEARLIGHT: 15, 25 | LUMINOSITY: 16, 26 | MULTIPLY: 17, 27 | NORMAL: 18, 28 | OVERLAY: 19, 29 | PINLIGHT: 20, 30 | SATURATION: 21, 31 | SCREEN: 22, 32 | SOFTLIGHT: 23, 33 | VIVIDLIGHT: 24, 34 | SUBTRACT: 25, 35 | DIVIDE: 26 36 | }; 37 | 38 | module.exports = { 39 | BLEND_MODES, 40 | TRACK_TYPE, 41 | TICKS_PER_SECOND 42 | }; ``` -------------------------------------------------------------------------------- /mcp/pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "psmcp" 7 | version = "0.85.1" 8 | description = "Adobe Photoshop automation using MCP" 9 | requires-python = ">=3.10" 10 | license = "MIT" 11 | authors = [ 12 | {name = "Mike Chambers", email = "[email protected]"} 13 | ] 14 | dependencies = [ 15 | "fonttools", 16 | "python-socketio", 17 | "mcp[cli]", 18 | "requests", 19 | "websocket-client>=1.8.0", 20 | "pillow>=11.2.1", 21 | "numpy>=2.2.6", 22 | ] 23 | 24 | [project.optional-dependencies] 25 | dev = [ 26 | "pytest>=7.0.0", 27 | "black", 28 | "isort", 29 | "mypy", 30 | ] 31 | 32 | [tool.setuptools] 33 | py-modules = ["fonts", "logger", "psmcp", "socket_client"] 34 | 35 | [tool.black] 36 | line-length = 88 37 | target-version = ['py38'] 38 | include = '\.pyi?$' 39 | 40 | [tool.isort] 41 | profile = "black" 42 | line_length = 88 43 | 44 | [tool.mypy] 45 | python_version = "3.10" 46 | warn_return_any = true 47 | warn_unused_configs = true 48 | disallow_untyped_defs = true 49 | disallow_incomplete_defs = true 50 | 51 | [tool.pytest.ini_options] 52 | testpaths = ["tests"] 53 | python_files = "test_*.py" 54 | ``` -------------------------------------------------------------------------------- /mcp/logger.py: -------------------------------------------------------------------------------- ```python 1 | # MIT License 2 | # 3 | # Copyright (c) 2025 Mike Chambers 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import sys 24 | 25 | def log(message, filter_tag="LOGGER"): 26 | 27 | print(f"{filter_tag} : {message}", file=sys.stderr) 28 | 29 | ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/index.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta charset="utf-8"> 5 | <title>Illustrator MCP Agent</title> 6 | <link rel="stylesheet" href="style.css"> 7 | </head> 8 | <body> 9 | <div class="container"> 10 | 11 | <div class="status-group"> 12 | <div class="status-dot" id="statusDot"></div> 13 | <span id="statusText">Disconnected</span> 14 | </div> 15 | 16 | <button id="btnConnect">Connect</button> 17 | 18 | <div class="divider"></div> 19 | 20 | <div class="checkbox-group"> 21 | <input type="checkbox" id="chkConnectOnLaunch"> 22 | <label for="chkConnectOnLaunch">Connect automatically on launch</label> 23 | </div> 24 | 25 | <div class="divider"></div> 26 | 27 | <div class="log-section"> 28 | <label>Message Log</label> 29 | <textarea id="messageLog" readonly></textarea> 30 | </div> 31 | </div> 32 | 33 | <!-- Load CEP's CSInterface library --> 34 | <script type="text/javascript" src="./lib/CSInterface.js"></script> 35 | 36 | <!-- Load Socket.IO client from CDN --> 37 | <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> 38 | 39 | <!-- Load commands --> 40 | <script src="commands.js"></script> 41 | 42 | <!-- Main script --> 43 | <script src="main.js"></script> 44 | </body> 45 | </html> ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/index.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta charset="utf-8"> 5 | <title>AfterEffects MCP Agent</title> 6 | <link rel="stylesheet" href="style.css"> 7 | </head> 8 | <body> 9 | <div class="container"> 10 | 11 | <div class="status-group"> 12 | <div class="status-dot" id="statusDot"></div> 13 | <span id="statusText">Disconnected</span> 14 | </div> 15 | 16 | <button id="btnConnect">Connect</button> 17 | 18 | <div class="divider"></div> 19 | 20 | <div class="checkbox-group"> 21 | <input type="checkbox" id="chkConnectOnLaunch"> 22 | <label for="chkConnectOnLaunch">Connect automatically on launch</label> 23 | </div> 24 | 25 | <div class="divider"></div> 26 | 27 | <div class="log-section"> 28 | <label>Message Log</label> 29 | <textarea id="messageLog" readonly></textarea> 30 | </div> 31 | </div> 32 | 33 | <!-- Load CEP's CSInterface library --> 34 | <script type="text/javascript" src="./lib/CSInterface.js"></script> 35 | 36 | <!-- Load Socket.IO client from CDN --> 37 | <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> 38 | 39 | <!-- Load commands --> 40 | <script src="commands.js"></script> 41 | 42 | <!-- Main script --> 43 | <script src="main.js"></script> 44 | </body> 45 | </html> ``` -------------------------------------------------------------------------------- /dxt/ps/manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "dxt_version": "0.1", 3 | "name": "adb-mcp-photoshop", 4 | "display_name": "Adobe Photoshop MCP", 5 | "version": "0.85.3", 6 | "description": "Proof of concept project to create AI Agent for Adobe Photshop by providing an interface to LLMs via the MCP protocol.", 7 | "long_description": "Proof of concept project to create AI Agent for Adobe Photoshop by providing an interface to LLMs via the MCP protocol.", 8 | "author": { 9 | "name": "Mike Chambers", 10 | "email": "[email protected]", 11 | "url": "https://www.mikechambers.com" 12 | }, 13 | "homepage": "https://github.com/mikechambers/adb-mcp", 14 | "documentation": "https://github.com/mikechambers/adb-mcp", 15 | "support": "https://github.com/mikechambers/adb-mcp/issues", 16 | "server": { 17 | "type": "python", 18 | "entry_point": "main.py", 19 | "mcp_config": { 20 | "command": "uv", 21 | "args": [ 22 | "run", 23 | "--with", 24 | "fonttools", 25 | "--with", 26 | "mcp", 27 | "--with", 28 | "mcp[cli]", 29 | "--with", 30 | "python-socketio", 31 | "--with", 32 | "requests", 33 | "--with", 34 | "numpy", 35 | "--with", 36 | "websocket-client", 37 | "mcp", 38 | "run", 39 | "${__dirname}/ps-mcp.py" 40 | ] 41 | } 42 | }, 43 | 44 | 45 | "keywords": [ 46 | "adobe", 47 | "premierepro", 48 | "video" 49 | ], 50 | "license": "MIT", 51 | "repository": { 52 | "type": "git", 53 | "url": "https://github.com/mikechambers/adb-mcp" 54 | } 55 | } 56 | ``` -------------------------------------------------------------------------------- /dxt/pr/manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "dxt_version": "0.1", 3 | "name": "adb-mcp-premiere", 4 | "display_name": "Adobe Premiere Pro MCP", 5 | "version": "0.85.3", 6 | "description": "Proof of concept project to create AI Agent for Adobe Premiere Pro by providing an interface to LLMs via the MCP protocol.", 7 | "long_description": "Proof of concept project to create AI Agent for Adobe Premiere Pro by providing an interface to LLMs via the MCP protocol.", 8 | "author": { 9 | "name": "Mike Chambers", 10 | "email": "[email protected]", 11 | "url": "https://www.mikechambers.com" 12 | }, 13 | "homepage": "https://github.com/mikechambers/adb-mcp", 14 | "documentation": "https://github.com/mikechambers/adb-mcp", 15 | "support": "https://github.com/mikechambers/adb-mcp/issues", 16 | "server": { 17 | "type": "python", 18 | "entry_point": "main.py", 19 | "mcp_config": { 20 | "command": "uv", 21 | "args": [ 22 | "run", 23 | "--with", 24 | "fonttools", 25 | "--with", 26 | "mcp", 27 | "--with", 28 | "mcp[cli]", 29 | "--with", 30 | "python-socketio", 31 | "--with", 32 | "requests", 33 | "--with", 34 | "websocket-client", 35 | "--with", 36 | "pillow", 37 | "mcp", 38 | "run", 39 | "${__dirname}/pr-mcp.py" 40 | ] 41 | } 42 | }, 43 | 44 | "keywords": [ 45 | "adobe", 46 | "photoshop", 47 | "images" 48 | ], 49 | "license": "MIT", 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/mikechambers/adb-mcp" 53 | } 54 | } 55 | ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/jsx/json-polyfill.jsx: -------------------------------------------------------------------------------- ```javascript 1 | // JSON polyfill for ExtendScript 2 | // Minimal implementation for serializing simple objects and arrays 3 | 4 | if (typeof JSON === 'undefined') { 5 | JSON = {}; 6 | } 7 | 8 | if (typeof JSON.stringify === 'undefined') { 9 | JSON.stringify = function(obj) { 10 | var type = typeof obj; 11 | 12 | // Handle primitives 13 | if (obj === null) return 'null'; 14 | if (obj === undefined) return 'undefined'; 15 | if (type === 'number') { 16 | if (isNaN(obj)) return 'null'; // JSON spec: NaN becomes null 17 | if (!isFinite(obj)) return 'null'; // JSON spec: Infinity becomes null 18 | return String(obj); 19 | } 20 | if (type === 'boolean') return String(obj); 21 | if (type === 'string') { 22 | // Escape special characters 23 | var escaped = obj.replace(/\\/g, '\\\\') 24 | .replace(/"/g, '\\"') 25 | .replace(/\n/g, '\\n') 26 | .replace(/\r/g, '\\r') 27 | .replace(/\t/g, '\\t'); 28 | return '"' + escaped + '"'; 29 | } 30 | 31 | // Handle arrays 32 | if (obj instanceof Array) { 33 | var items = []; 34 | for (var i = 0; i < obj.length; i++) { 35 | items.push(JSON.stringify(obj[i])); 36 | } 37 | return '[' + items.join(',') + ']'; 38 | } 39 | 40 | // Handle objects 41 | if (type === 'object') { 42 | var pairs = []; 43 | for (var key in obj) { 44 | if (obj.hasOwnProperty(key)) { 45 | pairs.push(JSON.stringify(key) + ':' + JSON.stringify(obj[key])); 46 | } 47 | } 48 | return '{' + pairs.join(',') + '}'; 49 | } 50 | 51 | // Fallback 52 | return '{}'; 53 | }; 54 | } ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/jsx/json-polyfill.jsx: -------------------------------------------------------------------------------- ```javascript 1 | // JSON polyfill for ExtendScript 2 | // Minimal implementation for serializing simple objects and arrays 3 | 4 | if (typeof JSON === 'undefined') { 5 | JSON = {}; 6 | } 7 | 8 | if (typeof JSON.stringify === 'undefined') { 9 | JSON.stringify = function(obj) { 10 | var type = typeof obj; 11 | 12 | // Handle primitives 13 | if (obj === null) return 'null'; 14 | if (obj === undefined) return 'undefined'; 15 | if (type === 'number') { 16 | if (isNaN(obj)) return 'null'; // JSON spec: NaN becomes null 17 | if (!isFinite(obj)) return 'null'; // JSON spec: Infinity becomes null 18 | return String(obj); 19 | } 20 | if (type === 'boolean') return String(obj); 21 | if (type === 'string') { 22 | // Escape special characters 23 | var escaped = obj.replace(/\\/g, '\\\\') 24 | .replace(/"/g, '\\"') 25 | .replace(/\n/g, '\\n') 26 | .replace(/\r/g, '\\r') 27 | .replace(/\t/g, '\\t'); 28 | return '"' + escaped + '"'; 29 | } 30 | 31 | // Handle arrays 32 | if (obj instanceof Array) { 33 | var items = []; 34 | for (var i = 0; i < obj.length; i++) { 35 | items.push(JSON.stringify(obj[i])); 36 | } 37 | return '[' + items.join(',') + ']'; 38 | } 39 | 40 | // Handle objects 41 | if (type === 'object') { 42 | var pairs = []; 43 | for (var key in obj) { 44 | if (obj.hasOwnProperty(key)) { 45 | pairs.push(JSON.stringify(key) + ':' + JSON.stringify(obj[key])); 46 | } 47 | } 48 | return '{' + pairs.join(',') + '}'; 49 | } 50 | 51 | // Fallback 52 | return '{}'; 53 | }; 54 | } ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/CSXS/manifest.xml: -------------------------------------------------------------------------------- ``` 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <ExtensionManifest Version="7.0" ExtensionBundleId="com.mikechambers.ae.mcp" ExtensionBundleVersion="1.0.0" 3 | ExtensionBundleName="AfterEffects MCP Agent" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 4 | 5 | <Author>Mike Chambers</Author> 6 | <Contact>[email protected]</Contact> 7 | <Legal>MIT License</Legal> 8 | <Abstract>AfterEffects MCP Agent</Abstract> 9 | 10 | <ExtensionList> 11 | <Extension Id="com.mikechambers.ae.mcp" Version="1.0.0"/> 12 | </ExtensionList> 13 | 14 | <ExecutionEnvironment> 15 | <HostList> 16 | <Host Name="AEFT" Version="[25.0,99.9]"/> 17 | </HostList> 18 | <LocaleList> 19 | <Locale Code="All"/> 20 | </LocaleList> 21 | <RequiredRuntimeList> 22 | <RequiredRuntime Name="CSXS" Version="12.0"/> 23 | </RequiredRuntimeList> 24 | </ExecutionEnvironment> 25 | 26 | <DispatchInfoList> 27 | <Extension Id="com.mikechambers.ae.mcp"> 28 | <DispatchInfo> 29 | <Resources> 30 | <MainPath>./index.html</MainPath> 31 | <CEFCommandLine> 32 | <Parameter>--enable-nodejs</Parameter> 33 | <Parameter>--mixed-context</Parameter> 34 | </CEFCommandLine> 35 | </Resources> 36 | <Lifecycle> 37 | <AutoVisible>true</AutoVisible> 38 | </Lifecycle> 39 | <UI> 40 | <Type>Panel</Type> 41 | <Menu>AfterEffects MCP Agent</Menu> 42 | <Geometry> 43 | <Size> 44 | <Height>400</Height> 45 | <Width>350</Width> 46 | </Size> 47 | <MinSize> 48 | <Height>300</Height> 49 | <Width>300</Width> 50 | </MinSize> 51 | <MaxSize> 52 | <Height>600</Height> 53 | <Width>500</Width> 54 | </MaxSize> 55 | </Geometry> 56 | </UI> 57 | </DispatchInfo> 58 | </Extension> 59 | </DispatchInfoList> 60 | </ExtensionManifest> ``` -------------------------------------------------------------------------------- /uxp/ps/manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "id": "Photoshop MCP Agent", 3 | "name": "Photoshop MCP Agent", 4 | "version": "0.85.3", 5 | "main": "index.html", 6 | "host": [ 7 | { 8 | "app": "PS", 9 | "minVersion": "26.0.0" 10 | } 11 | ], 12 | "manifestVersion": 5, 13 | "entrypoints": [ 14 | { 15 | "type": "panel", 16 | "id": "vanilla", 17 | "minimumSize": { 18 | "width": 300, 19 | "height": 200 20 | }, 21 | "maximumSize": { 22 | "width": 300, 23 | "height": 200 24 | }, 25 | "preferredDockedSize": { 26 | "width": 300, 27 | "height": 200 28 | }, 29 | "preferredFloatingSize": { 30 | "width": 300, 31 | "height": 200 32 | }, 33 | "icons": [ 34 | { 35 | "width": 32, 36 | "height": 32, 37 | "path": "icons/icon_D.png", 38 | "scale": [ 39 | 1, 40 | 2 41 | ], 42 | "theme": [ 43 | "dark", 44 | "darkest" 45 | ], 46 | "species": [ 47 | "generic" 48 | ] 49 | }, 50 | { 51 | "width": 32, 52 | "height": 32, 53 | "path": "icons/icon_N.png", 54 | "scale": [ 55 | 1, 56 | 2 57 | ], 58 | "theme": [ 59 | "lightest", 60 | "light" 61 | ], 62 | "species": [ 63 | "generic" 64 | ] 65 | } 66 | ], 67 | "label": { 68 | "default": "Photoshop MCP Agent" 69 | } 70 | } 71 | ], 72 | "requiredPermissions": { 73 | "network": { 74 | "domains": "all" 75 | }, 76 | "localFileSystem": "fullAccess" 77 | }, 78 | "icons": [ 79 | { 80 | "width": 23, 81 | "height": 23, 82 | "path": "icons/dark.png", 83 | "scale": [ 84 | 1, 85 | 2 86 | ], 87 | "theme": [ 88 | "darkest", 89 | "dark", 90 | "medium" 91 | ] 92 | }, 93 | { 94 | "width": 23, 95 | "height": 23, 96 | "path": "icons/light.png", 97 | "scale": [ 98 | 1, 99 | 2 100 | ], 101 | "theme": [ 102 | "lightest", 103 | "light" 104 | ] 105 | } 106 | ] 107 | } ``` -------------------------------------------------------------------------------- /uxp/pr/manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "id": "Premiere MCP Agent", 3 | "name": "Premiere MCP Agent", 4 | "version": "0.85.3", 5 | "main": "index.html", 6 | "host": [ 7 | { 8 | "app": "premierepro", 9 | "minVersion": "25.3.0" 10 | } 11 | ], 12 | "manifestVersion": 5, 13 | "entrypoints": [ 14 | { 15 | "type": "panel", 16 | "id": "vanilla", 17 | "minimumSize": { 18 | "width": 300, 19 | "height": 200 20 | }, 21 | "maximumSize": { 22 | "width": 300, 23 | "height": 200 24 | }, 25 | "preferredDockedSize": { 26 | "width": 300, 27 | "height": 200 28 | }, 29 | "preferredFloatingSize": { 30 | "width": 300, 31 | "height": 200 32 | }, 33 | "icons": [ 34 | { 35 | "width": 32, 36 | "height": 32, 37 | "path": "icons/icon_D.png", 38 | "scale": [ 39 | 1, 40 | 2 41 | ], 42 | "theme": [ 43 | "dark", 44 | "darkest" 45 | ], 46 | "species": [ 47 | "generic" 48 | ] 49 | }, 50 | { 51 | "width": 32, 52 | "height": 32, 53 | "path": "icons/icon_N.png", 54 | "scale": [ 55 | 1, 56 | 2 57 | ], 58 | "theme": [ 59 | "lightest", 60 | "light" 61 | ], 62 | "species": [ 63 | "generic" 64 | ] 65 | } 66 | ], 67 | "label": { 68 | "default": "Premiere MCP Agent" 69 | } 70 | } 71 | ], 72 | "requiredPermissions": { 73 | "network": { 74 | "domains": "all" 75 | }, 76 | "localFileSystem": "fullAccess" 77 | }, 78 | "icons": [ 79 | { 80 | "width": 23, 81 | "height": 23, 82 | "path": "icons/dark.png", 83 | "scale": [ 84 | 1, 85 | 2 86 | ], 87 | "theme": [ 88 | "darkest", 89 | "dark", 90 | "medium" 91 | ] 92 | }, 93 | { 94 | "width": 23, 95 | "height": 23, 96 | "path": "icons/light.png", 97 | "scale": [ 98 | 1, 99 | 2 100 | ], 101 | "theme": [ 102 | "lightest", 103 | "light" 104 | ] 105 | } 106 | ] 107 | } ``` -------------------------------------------------------------------------------- /uxp/id/manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "id": "InDesign MCP Agent", 3 | "name": "InDesign MCP Agent", 4 | "version": "0.85.0", 5 | "main": "index.html", 6 | "host": [ 7 | { 8 | "app": "ID", 9 | "minVersion": "20.2.0" 10 | } 11 | ], 12 | "manifestVersion": 5, 13 | "entrypoints": [ 14 | { 15 | "type": "panel", 16 | "id": "vanilla", 17 | "minimumSize": { 18 | "width": 300, 19 | "height": 200 20 | }, 21 | "maximumSize": { 22 | "width": 300, 23 | "height": 200 24 | }, 25 | "preferredDockedSize": { 26 | "width": 300, 27 | "height": 200 28 | }, 29 | "preferredFloatingSize": { 30 | "width": 300, 31 | "height": 200 32 | }, 33 | "icons": [ 34 | { 35 | "width": 32, 36 | "height": 32, 37 | "path": "icons/icon_D.png", 38 | "scale": [ 39 | 1, 40 | 2 41 | ], 42 | "theme": [ 43 | "dark", 44 | "darkest" 45 | ], 46 | "species": [ 47 | "generic" 48 | ] 49 | }, 50 | { 51 | "width": 32, 52 | "height": 32, 53 | "path": "icons/icon_N.png", 54 | "scale": [ 55 | 1, 56 | 2 57 | ], 58 | "theme": [ 59 | "lightest", 60 | "light" 61 | ], 62 | "species": [ 63 | "generic" 64 | ] 65 | } 66 | ], 67 | "label": { 68 | "default": "InDesign MCP Agent" 69 | } 70 | } 71 | ], 72 | "requiredPermissions": { 73 | "network": { 74 | "domains": [ 75 | "all", 76 | "http://localhost:3001" 77 | ] 78 | }, 79 | "localFileSystem": "fullAccess" 80 | }, 81 | "icons": [ 82 | { 83 | "width": 23, 84 | "height": 23, 85 | "path": "icons/dark.png", 86 | "scale": [ 87 | 1, 88 | 2 89 | ], 90 | "theme": [ 91 | "darkest", 92 | "dark", 93 | "medium" 94 | ] 95 | }, 96 | { 97 | "width": 23, 98 | "height": 23, 99 | "path": "icons/light.png", 100 | "scale": [ 101 | 1, 102 | 2 103 | ], 104 | "theme": [ 105 | "lightest", 106 | "light" 107 | ] 108 | } 109 | ] 110 | } ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/CSXS/manifest.xml: -------------------------------------------------------------------------------- ``` 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <ExtensionManifest Version="7.0" ExtensionBundleId="com.mikechambers.ai.mcp" ExtensionBundleVersion="1.0.0" 3 | ExtensionBundleName="Illustrator MCP Agent" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 4 | 5 | <Author>Mike Chambers</Author> 6 | <Contact>[email protected]</Contact> 7 | <Legal>MIT License</Legal> 8 | <Abstract>Illustrator MCP Agent</Abstract> 9 | 10 | <ExtensionList> 11 | <Extension Id="com.mikechambers.ai.mcp" Version="1.0.0"/> 12 | </ExtensionList> 13 | 14 | <ExecutionEnvironment> 15 | <HostList> 16 | <Host Name="ILST" Version="[28.0,99.9]"/> 17 | </HostList> 18 | <LocaleList> 19 | <Locale Code="All"/> 20 | </LocaleList> 21 | <RequiredRuntimeList> 22 | <RequiredRuntime Name="CSXS" Version="12.0"/> 23 | </RequiredRuntimeList> 24 | </ExecutionEnvironment> 25 | 26 | <DispatchInfoList> 27 | <Extension Id="com.mikechambers.ai.mcp"> 28 | <DispatchInfo> 29 | <Resources> 30 | <MainPath>./index.html</MainPath> 31 | <CEFCommandLine> 32 | <Parameter>--enable-nodejs</Parameter> 33 | <Parameter>--mixed-context</Parameter> 34 | <Parameter>--remote-debugging-port=8088</Parameter> 35 | <Parameter>--allow-file-access-from-files</Parameter> 36 | </CEFCommandLine> 37 | </Resources> 38 | <Lifecycle> 39 | <AutoVisible>true</AutoVisible> 40 | </Lifecycle> 41 | <UI> 42 | <Type>Panel</Type> 43 | <Menu>Illustrator MCP Agent</Menu> 44 | <Geometry> 45 | <Size> 46 | <Height>400</Height> 47 | <Width>350</Width> 48 | </Size> 49 | <MinSize> 50 | <Height>300</Height> 51 | <Width>300</Width> 52 | </MinSize> 53 | <MaxSize> 54 | <Height>600</Height> 55 | <Width>500</Width> 56 | </MaxSize> 57 | </Geometry> 58 | </UI> 59 | </DispatchInfo> 60 | </Extension> 61 | </DispatchInfoList> 62 | </ExtensionManifest> ``` -------------------------------------------------------------------------------- /uxp/ps/commands/filters.js: -------------------------------------------------------------------------------- ```javascript 1 | /* MIT License 2 | * 3 | * Copyright (c) 2025 Mike Chambers 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | const { app } = require("photoshop"); // For app references 25 | 26 | const { 27 | findLayer, 28 | execute 29 | } = require("./utils"); // For the utility functions used in your code 30 | 31 | const applyMotionBlur = async (command) => { 32 | 33 | let options = command.options; 34 | let layerId = options.layerId; 35 | 36 | let layer = findLayer(layerId); 37 | 38 | if (!layer) { 39 | throw new Error( 40 | `applyMotionBlur : Could not find layerId : ${layerId}` 41 | ); 42 | } 43 | 44 | await execute(async () => { 45 | await layer.applyMotionBlur(options.angle, options.distance); 46 | }); 47 | }; 48 | 49 | const applyGaussianBlur = async (command) => { 50 | 51 | let options = command.options; 52 | let layerId = options.layerId; 53 | 54 | let layer = findLayer(layerId); 55 | 56 | if (!layer) { 57 | throw new Error( 58 | `applyGaussianBlur : Could not find layerId : ${layerId}` 59 | ); 60 | } 61 | 62 | await execute(async () => { 63 | await layer.applyGaussianBlur(options.radius); 64 | }); 65 | }; 66 | 67 | const commandHandlers = { 68 | applyMotionBlur, 69 | applyGaussianBlur, 70 | }; 71 | 72 | module.exports = { 73 | commandHandlers 74 | }; ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/style.css: -------------------------------------------------------------------------------- ```css 1 | /* style.css */ 2 | body { 3 | margin: 0; 4 | padding: 16px; 5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 6 | background-color: #2a2a2a; 7 | color: #e0e0e0; 8 | font-size: 13px; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | flex-direction: column; 14 | gap: 12px; 15 | } 16 | 17 | .form-group { 18 | display: flex; 19 | flex-direction: column; 20 | gap: 6px; 21 | } 22 | 23 | label { 24 | font-size: 12px; 25 | font-weight: 500; 26 | color: #b0b0b0; 27 | } 28 | 29 | input[type="text"] { 30 | padding: 8px; 31 | border: 1px solid #444; 32 | border-radius: 4px; 33 | background-color: #1a1a1a; 34 | color: #e0e0e0; 35 | font-size: 13px; 36 | } 37 | 38 | input[type="text"]:focus { 39 | outline: none; 40 | border-color: #0d6efd; 41 | } 42 | 43 | .status-group { 44 | display: flex; 45 | align-items: center; 46 | gap: 8px; 47 | padding: 8px; 48 | background-color: #1a1a1a; 49 | border-radius: 4px; 50 | } 51 | 52 | .status-dot { 53 | width: 10px; 54 | height: 10px; 55 | border-radius: 50%; 56 | background-color: #dc3545; 57 | transition: background-color 0.3s; 58 | } 59 | 60 | .status-dot.connected { 61 | background-color: #28a745; 62 | } 63 | 64 | #statusText { 65 | font-size: 13px; 66 | font-weight: 500; 67 | } 68 | 69 | button { 70 | padding: 10px 16px; 71 | border: none; 72 | border-radius: 4px; 73 | background-color: #0d6efd; 74 | color: white; 75 | font-size: 13px; 76 | font-weight: 500; 77 | cursor: pointer; 78 | transition: background-color 0.2s; 79 | } 80 | 81 | button:hover { 82 | background-color: #0b5ed7; 83 | } 84 | 85 | button:active { 86 | background-color: #0a58ca; 87 | } 88 | 89 | .checkbox-group { 90 | display: flex; 91 | align-items: center; 92 | gap: 8px; 93 | padding: 8px 0; 94 | } 95 | 96 | input[type="checkbox"] { 97 | width: 16px; 98 | height: 16px; 99 | cursor: pointer; 100 | } 101 | 102 | .checkbox-group label { 103 | font-size: 13px; 104 | color: #e0e0e0; 105 | cursor: pointer; 106 | font-weight: normal; 107 | } 108 | 109 | .divider { 110 | height: 1px; 111 | background-color: #444; 112 | margin: 8px 0; 113 | } 114 | 115 | .log-section { 116 | display: flex; 117 | flex-direction: column; 118 | gap: 6px; 119 | } 120 | 121 | #messageLog { 122 | width: 100%; 123 | height: 120px; 124 | padding: 8px; 125 | border: 1px solid #444; 126 | border-radius: 4px; 127 | background-color: #1a1a1a; 128 | color: #e0e0e0; 129 | font-family: 'Courier New', monospace; 130 | font-size: 11px; 131 | resize: vertical; 132 | line-height: 1.4; 133 | } 134 | 135 | #messageLog:focus { 136 | outline: none; 137 | border-color: #0d6efd; 138 | } ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/style.css: -------------------------------------------------------------------------------- ```css 1 | /* style.css */ 2 | body { 3 | margin: 0; 4 | padding: 16px; 5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 6 | background-color: #2a2a2a; 7 | color: #e0e0e0; 8 | font-size: 13px; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | flex-direction: column; 14 | gap: 12px; 15 | } 16 | 17 | .form-group { 18 | display: flex; 19 | flex-direction: column; 20 | gap: 6px; 21 | } 22 | 23 | label { 24 | font-size: 12px; 25 | font-weight: 500; 26 | color: #b0b0b0; 27 | } 28 | 29 | input[type="text"] { 30 | padding: 8px; 31 | border: 1px solid #444; 32 | border-radius: 4px; 33 | background-color: #1a1a1a; 34 | color: #e0e0e0; 35 | font-size: 13px; 36 | } 37 | 38 | input[type="text"]:focus { 39 | outline: none; 40 | border-color: #0d6efd; 41 | } 42 | 43 | .status-group { 44 | display: flex; 45 | align-items: center; 46 | gap: 8px; 47 | padding: 8px; 48 | background-color: #1a1a1a; 49 | border-radius: 4px; 50 | } 51 | 52 | .status-dot { 53 | width: 10px; 54 | height: 10px; 55 | border-radius: 50%; 56 | background-color: #dc3545; 57 | transition: background-color 0.3s; 58 | } 59 | 60 | .status-dot.connected { 61 | background-color: #28a745; 62 | } 63 | 64 | #statusText { 65 | font-size: 13px; 66 | font-weight: 500; 67 | } 68 | 69 | button { 70 | padding: 10px 16px; 71 | border: none; 72 | border-radius: 4px; 73 | background-color: #0d6efd; 74 | color: white; 75 | font-size: 13px; 76 | font-weight: 500; 77 | cursor: pointer; 78 | transition: background-color 0.2s; 79 | } 80 | 81 | button:hover { 82 | background-color: #0b5ed7; 83 | } 84 | 85 | button:active { 86 | background-color: #0a58ca; 87 | } 88 | 89 | .checkbox-group { 90 | display: flex; 91 | align-items: center; 92 | gap: 8px; 93 | padding: 8px 0; 94 | } 95 | 96 | input[type="checkbox"] { 97 | width: 16px; 98 | height: 16px; 99 | cursor: pointer; 100 | } 101 | 102 | .checkbox-group label { 103 | font-size: 13px; 104 | color: #e0e0e0; 105 | cursor: pointer; 106 | font-weight: normal; 107 | } 108 | 109 | .divider { 110 | height: 1px; 111 | background-color: #444; 112 | margin: 8px 0; 113 | } 114 | 115 | .log-section { 116 | display: flex; 117 | flex-direction: column; 118 | gap: 6px; 119 | } 120 | 121 | #messageLog { 122 | width: 100%; 123 | height: 120px; 124 | padding: 8px; 125 | border: 1px solid #444; 126 | border-radius: 4px; 127 | background-color: #1a1a1a; 128 | color: #e0e0e0; 129 | font-family: 'Courier New', monospace; 130 | font-size: 11px; 131 | resize: vertical; 132 | line-height: 1.4; 133 | } 134 | 135 | #messageLog:focus { 136 | outline: none; 137 | border-color: #0d6efd; 138 | } ``` -------------------------------------------------------------------------------- /uxp/ps/commands/index.js: -------------------------------------------------------------------------------- ```javascript 1 | /* MIT License 2 | * 3 | * Copyright (c) 2025 Mike Chambers 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | const { app } = require("photoshop"); 25 | const fs = require("uxp").storage.localFileSystem; 26 | 27 | const adjustmentLayers = require("./adjustment_layers"); 28 | const core = require("./core"); 29 | const layerStyles = require("./layer_styles") 30 | const filters = require("./filters") 31 | const selection = require("./selection") 32 | const layers = require("./layers") 33 | 34 | const parseAndRouteCommands = async (commands) => { 35 | if (!commands.length) { 36 | return; 37 | } 38 | 39 | for (let c of commands) { 40 | await parseAndRouteCommand(c); 41 | } 42 | }; 43 | 44 | const parseAndRouteCommand = async (command) => { 45 | let action = command.action; 46 | 47 | let f = commandHandlers[action]; 48 | 49 | if (typeof f !== "function") { 50 | throw new Error(`Unknown Command: ${action}`); 51 | } 52 | 53 | console.log(f.name) 54 | return f(command); 55 | }; 56 | 57 | const checkRequiresActiveDocument = (command) => { 58 | if (!requiresActiveDocument(command)) { 59 | return; 60 | } 61 | 62 | if (!app.activeDocument) { 63 | throw new Error( 64 | `${command.action} : Requires an open Photoshop document` 65 | ); 66 | } 67 | }; 68 | 69 | const requiresActiveDocument = (command) => { 70 | return !["createDocument", "openFile"].includes(command.action); 71 | }; 72 | 73 | const commandHandlers = { 74 | ...selection.commandHandlers, 75 | ...filters.commandHandlers, 76 | ...core.commandHandlers, 77 | ...adjustmentLayers.commandHandlers, 78 | ...layerStyles.commandHandlers, 79 | ...layers.commandHandlers 80 | }; 81 | 82 | module.exports = { 83 | requiresActiveDocument, 84 | checkRequiresActiveDocument, 85 | parseAndRouteCommands, 86 | parseAndRouteCommand, 87 | }; 88 | ``` -------------------------------------------------------------------------------- /uxp/pr/commands/index.js: -------------------------------------------------------------------------------- ```javascript 1 | /* MIT License 2 | * 3 | * Copyright (c) 2025 Mike Chambers 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | const app = require("premierepro"); 25 | const core = require("./core"); 26 | 27 | const getProjectInfo = async () => { 28 | let project = await app.Project.getActiveProject() 29 | 30 | const name = project.name; 31 | const path = project.path; 32 | const id = project.guid.toString(); 33 | 34 | const items = await getProjectContentInfo() 35 | 36 | return { 37 | name, 38 | path, 39 | id, 40 | items 41 | } 42 | 43 | } 44 | /* 45 | const getProjectContentInfo2 = async () => { 46 | let project = await app.Project.getActiveProject() 47 | 48 | let root = await project.getRootItem() 49 | let items = await root.getItems() 50 | 51 | let out = [] 52 | for(const item of items) { 53 | console.log(item) 54 | 55 | const b = app.FolderItem.cast(item) 56 | 57 | const isBin = b != undefined 58 | 59 | //todo: it would be good to get more data / info here 60 | out.push({name:item.name}) 61 | } 62 | 63 | return out 64 | } 65 | */ 66 | 67 | const getProjectContentInfo = async () => { 68 | let project = await app.Project.getActiveProject() 69 | let root = await project.getRootItem() 70 | 71 | const processItems = async (parentItem) => { 72 | let items = await parentItem.getItems() 73 | let out = [] 74 | 75 | for(const item of items) { 76 | console.log(item) 77 | 78 | const folderItem = app.FolderItem.cast(item) 79 | const isBin = folderItem != undefined 80 | 81 | let itemData = { 82 | name: item.name, 83 | type: isBin ? 'bin' : 'projectItem' 84 | } 85 | 86 | // If it's a bin/folder, recursively get its contents 87 | if (isBin) { 88 | itemData.items = await processItems(folderItem) 89 | } 90 | 91 | out.push(itemData) 92 | } 93 | 94 | return out 95 | } 96 | 97 | return await processItems(root) 98 | } 99 | 100 | const parseAndRouteCommand = async (command) => { 101 | let action = command.action; 102 | 103 | let f = commandHandlers[action]; 104 | 105 | if (typeof f !== "function") { 106 | throw new Error(`Unknown Command: ${action}`); 107 | } 108 | 109 | console.log(f.name) 110 | return f(command); 111 | }; 112 | 113 | 114 | 115 | const checkRequiresActiveProject = async (command) => { 116 | if (!requiresActiveProject(command)) { 117 | return; 118 | } 119 | 120 | let project = await app.Project.getActiveProject() 121 | if (!project) { 122 | throw new Error( 123 | `${command.action} : Requires an open Premiere Project` 124 | ); 125 | } 126 | }; 127 | 128 | const requiresActiveProject = (command) => { 129 | return !["createProject", "openProject"].includes(command.action); 130 | }; 131 | 132 | const commandHandlers = { 133 | ...core.commandHandlers 134 | }; 135 | 136 | module.exports = { 137 | getProjectInfo, 138 | checkRequiresActiveProject, 139 | parseAndRouteCommand 140 | }; 141 | ``` -------------------------------------------------------------------------------- /mcp/ae-mcp.py: -------------------------------------------------------------------------------- ```python 1 | # MIT License 2 | # 3 | # Copyright (c) 2025 Mike Chambers 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from mcp.server.fastmcp import FastMCP 24 | from core import init, sendCommand, createCommand 25 | import socket_client 26 | import sys 27 | 28 | # Create an MCP server 29 | mcp_name = "Adobe After Effects MCP Server" 30 | mcp = FastMCP(mcp_name, log_level="ERROR") 31 | print(f"{mcp_name} running on stdio", file=sys.stderr) 32 | 33 | APPLICATION = "aftereffects" 34 | PROXY_URL = 'http://localhost:3001' 35 | PROXY_TIMEOUT = 20 36 | 37 | socket_client.configure( 38 | app=APPLICATION, 39 | url=PROXY_URL, 40 | timeout=PROXY_TIMEOUT 41 | ) 42 | 43 | init(APPLICATION, socket_client) 44 | 45 | @mcp.tool() 46 | def execute_extend_script(script_string: str): 47 | """ 48 | Executes arbitrary ExtendScript code in AfterEffects and returns the result. 49 | 50 | The script should use 'return' to send data back. The result will be automatically 51 | JSON stringified. If the script throws an error, it will be caught and returned 52 | as an error object. 53 | 54 | Args: 55 | script_string (str): The ExtendScript code to execute. Must use 'return' to 56 | send results back. 57 | 58 | Returns: 59 | any: The result returned from the ExtendScript, or an error object containing: 60 | - error (str): Error message 61 | - line (str): Line number where error occurred 62 | 63 | Example: 64 | script = ''' 65 | var doc = app.activeDocument; 66 | return { 67 | name: doc.name, 68 | path: doc.fullName.fsName, 69 | layers: doc.layers.length 70 | }; 71 | ''' 72 | result = execute_extend_script(script) 73 | """ 74 | command = createCommand("executeExtendScript", { 75 | "scriptString": script_string 76 | }) 77 | return sendCommand(command) 78 | 79 | @mcp.resource("config://get_instructions") 80 | def get_instructions() -> str: 81 | """Read this first! Returns information and instructions on how to use AfterEffects and this API""" 82 | 83 | return f""" 84 | You are an Adobe AfterEffects expert who is practical, clear, and great at teaching. 85 | 86 | Rules to follow: 87 | 88 | 1. Think deeply about how to solve the task. 89 | 2. Always check your work before responding. 90 | 3. Read the API call info to understand required arguments and return shapes. 91 | 4. Before manipulating anything, ensure a document is open and active. 92 | """ 93 | 94 | 95 | 96 | # AfterEffectsd Blend Modes (for future use) 97 | BLEND_MODES = [ 98 | "ADD", 99 | "ALPHA_ADD", 100 | "CLASSIC_COLOR_BURN", 101 | "CLASSIC_COLOR_DODGE", 102 | "CLASSIC_DIFFERENCE", 103 | "COLOR", 104 | "COLOR_BURN", 105 | "COLOR_DODGE", 106 | "DANCING_DISSOLVE", 107 | "DARKEN", 108 | "DARKER_COLOR", 109 | "DIFFERENCE", 110 | "DISSOLVE", 111 | "EXCLUSION", 112 | "HARD_LIGHT", 113 | "HARD_MIX", 114 | "HUE", 115 | "LIGHTEN", 116 | "LIGHTER_COLOR", 117 | "LINEAR_BURN", 118 | "LINEAR_DODGE", 119 | "LINEAR_LIGHT", 120 | "LUMINESCENT_PREMUL", 121 | "LUMINOSITY", 122 | "MULTIPLY", 123 | "NORMAL", 124 | "OVERLAY", 125 | "PIN_LIGHT", 126 | "SATURATION", 127 | "SCREEN", 128 | "SILHOUETE_ALPHA", 129 | "SILHOUETTE_LUMA", 130 | "SOFT_LIGHT", 131 | "STENCIL_ALPHA", 132 | "STENCIL_LUMA", 133 | "SUBTRACT", 134 | "VIVID_LIGHT" 135 | ] ``` -------------------------------------------------------------------------------- /uxp/id/commands/index.js: -------------------------------------------------------------------------------- ```javascript 1 | /* MIT License 2 | * 3 | * Copyright (c) 2025 Mike Chambers 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | //const fs = require("uxp").storage.localFileSystem; 25 | //const openfs = require('fs') 26 | const {app, DocumentIntentOptions} = require("indesign"); 27 | 28 | 29 | const createDocument = async (command) => { 30 | console.log("createDocument") 31 | 32 | const options = command.options 33 | 34 | let documents = app.documents 35 | let margins = options.margins 36 | 37 | let unit = getUnitForIntent(DocumentIntentOptions.WEB_INTENT) 38 | 39 | app.marginPreferences.bottom = `${margins.bottom}${unit}` 40 | app.marginPreferences.top = `${margins.top}${unit}` 41 | app.marginPreferences.left = `${margins.left}${unit}` 42 | app.marginPreferences.right = `${margins.right}${unit}` 43 | 44 | app.marginPreferences.columnCount = options.columns.count 45 | app.marginPreferences.columnGutter = `${options.columns.gutter}${unit}` 46 | 47 | 48 | let documentPreferences = { 49 | pageWidth: `${options.pageWidth}${unit}`, 50 | pageHeight: `${options.pageHeight}${unit}`, 51 | pagesPerDocument: options.pagesPerDocument, 52 | facingPages: options.facingPages, 53 | intent: DocumentIntentOptions.WEB_INTENT 54 | } 55 | 56 | const showingWindow = true 57 | //Boolean showingWindow, DocumentPreset documentPreset, Object withProperties 58 | documents.add({showingWindow, documentPreferences}) 59 | } 60 | 61 | 62 | const getUnitForIntent = (intent) => { 63 | 64 | if(intent && intent.toString() === DocumentIntentOptions.WEB_INTENT.toString()) { 65 | return "px" 66 | } 67 | 68 | throw new Error(`getUnitForIntent : unknown intent [${intent}]`) 69 | } 70 | 71 | const parseAndRouteCommand = async (command) => { 72 | let action = command.action; 73 | 74 | let f = commandHandlers[action]; 75 | 76 | if (typeof f !== "function") { 77 | throw new Error(`Unknown Command: ${action}`); 78 | } 79 | 80 | console.log(f.name) 81 | return f(command); 82 | }; 83 | 84 | 85 | const commandHandlers = { 86 | createDocument 87 | }; 88 | 89 | 90 | const getActiveDocumentSettings = (command) => { 91 | const document = app.activeDocument 92 | 93 | 94 | const d = document.documentPreferences 95 | const documentPreferences = { 96 | pageWidth:d.pageWidth, 97 | pageHeight:d.pageHeight, 98 | pagesPerDocument:d.pagesPerDocument, 99 | facingPages:d.facingPages, 100 | measurementUnit:getUnitForIntent(d.intent) 101 | } 102 | 103 | const marginPreferences = { 104 | top:document.marginPreferences.top, 105 | bottom:document.marginPreferences.bottom, 106 | left:document.marginPreferences.left, 107 | right:document.marginPreferences.right, 108 | columnCount : document.marginPreferences.columnCount, 109 | columnGutter : document.marginPreferences.columnGutter 110 | } 111 | return {documentPreferences, marginPreferences} 112 | } 113 | 114 | const checkRequiresActiveDocument = async (command) => { 115 | if (!requiresActiveProject(command)) { 116 | return; 117 | } 118 | 119 | let document = app.activeDocument 120 | if (!document) { 121 | throw new Error( 122 | `${command.action} : Requires an open InDesign document` 123 | ); 124 | } 125 | }; 126 | 127 | const requiresActiveDocument = (command) => { 128 | return !["createDocument"].includes(command.action); 129 | }; 130 | 131 | 132 | module.exports = { 133 | getActiveDocumentSettings, 134 | checkRequiresActiveDocument, 135 | parseAndRouteCommand 136 | }; 137 | ``` -------------------------------------------------------------------------------- /mcp/id-mcp.py: -------------------------------------------------------------------------------- ```python 1 | # MIT License 2 | # 3 | # Copyright (c) 2025 Mike Chambers 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from mcp.server.fastmcp import FastMCP 24 | from core import init, sendCommand, createCommand 25 | import socket_client 26 | import sys 27 | 28 | #logger.log(f"Python path: {sys.executable}") 29 | #logger.log(f"PYTHONPATH: {os.environ.get('PYTHONPATH')}") 30 | #logger.log(f"Current working directory: {os.getcwd()}") 31 | #logger.log(f"Sys.path: {sys.path}") 32 | 33 | 34 | # Create an MCP server 35 | mcp_name = "Adobe InDesign MCP Server" 36 | mcp = FastMCP(mcp_name, log_level="ERROR") 37 | print(f"{mcp_name} running on stdio", file=sys.stderr) 38 | 39 | APPLICATION = "indesign" 40 | PROXY_URL = 'http://localhost:3001' 41 | PROXY_TIMEOUT = 20 42 | 43 | socket_client.configure( 44 | app=APPLICATION, 45 | url=PROXY_URL, 46 | timeout=PROXY_TIMEOUT 47 | ) 48 | 49 | init(APPLICATION, socket_client) 50 | 51 | @mcp.tool() 52 | def create_document( 53 | width: int, 54 | height: int, 55 | pages: int = 0, 56 | pages_facing: bool = False, 57 | columns: dict = {"count": 1, "gutter": 12}, 58 | margins: dict = {"top": 36, "bottom": 36, "left": 36, "right": 36} 59 | ): 60 | """ 61 | Creates a new InDesign document with specified dimensions and layout settings. 62 | 63 | Args: 64 | width (int): Document width in points (1 point = 1/72 inch) 65 | height (int): Document height in points 66 | pages (int, optional): Number of pages in the document. Defaults to 0. 67 | pages_facing (bool, optional): Whether to create facing pages (spread layout). 68 | Defaults to False. 69 | columns (dict, optional): Column layout configuration with keys: 70 | - count (int): Number of columns per page 71 | - gutter (int): Space between columns in points 72 | Defaults to {"count": 1, "gutter": 12}. 73 | margins (dict, optional): Page margin settings in points with keys: 74 | - top (int): Top margin 75 | - bottom (int): Bottom margin 76 | - left (int): Left margin 77 | - right (int): Right margin 78 | Defaults to {"top": 36, "bottom": 36, "left": 36, "right": 36}. 79 | 80 | Returns: 81 | dict: Result of the command execution from the InDesign UXP plugin 82 | """ 83 | command = createCommand("createDocument", { 84 | "intent": "WEB_INTENT", 85 | "pageWidth": width, 86 | "pageHeight": height, 87 | "margins": margins, 88 | "columns": columns, 89 | "pagesPerDocument": pages, 90 | "pagesFacing": pages_facing 91 | }) 92 | 93 | return sendCommand(command) 94 | 95 | @mcp.resource("config://get_instructions") 96 | def get_instructions() -> str: 97 | """Read this first! Returns information and instructions on how to use Photoshop and this API""" 98 | 99 | return f""" 100 | You are an InDesign and design expert who is creative and loves to help other people learn to use InDesign and create. 101 | 102 | Rules to follow: 103 | 104 | 1. Think deeply about how to solve the task 105 | 2. Always check your work 106 | 3. Read the info for the API calls to make sure you understand the requirements and arguments 107 | """ 108 | 109 | 110 | """ 111 | BLEND_MODES = [ 112 | "COLOR", 113 | "COLORBURN", 114 | "COLORDODGE", 115 | "DARKEN", 116 | "DARKERCOLOR", 117 | "DIFFERENCE", 118 | "DISSOLVE", 119 | "EXCLUSION", 120 | "HARDLIGHT", 121 | "HARDMIX", 122 | "HUE", 123 | "LIGHTEN", 124 | "LIGHTERCOLOR", 125 | "LINEARBURN", 126 | "LINEARDODGE", 127 | "LINEARLIGHT", 128 | "LUMINOSITY", 129 | "MULTIPLY", 130 | "NORMAL", 131 | "OVERLAY", 132 | "PINLIGHT", 133 | "SATURATION", 134 | "SCREEN", 135 | "SOFTLIGHT", 136 | "VIVIDLIGHT", 137 | "SUBTRACT", 138 | "DIVIDE" 139 | ] 140 | """ ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/main.js: -------------------------------------------------------------------------------- ```javascript 1 | /* Socket.IO Plugin for After Effects (CEP) 2 | * Main JavaScript file 3 | */ 4 | 5 | const csInterface = new CSInterface(); 6 | const APPLICATION = "aftereffects"; 7 | const PROXY_URL = "http://localhost:3001"; 8 | 9 | 10 | let socket = null; 11 | 12 | // Log function 13 | function log(message) { 14 | const logArea = document.getElementById('messageLog'); 15 | const timestamp = new Date().toLocaleTimeString(); 16 | logArea.value += `[${timestamp}] ${message}\n`; 17 | logArea.scrollTop = logArea.scrollHeight; 18 | } 19 | 20 | // Update UI status 21 | function updateStatus(connected) { 22 | const statusDot = document.getElementById('statusDot'); 23 | const statusText = document.getElementById('statusText'); 24 | const btnConnect = document.getElementById('btnConnect'); 25 | 26 | if (connected) { 27 | statusDot.classList.add('connected'); 28 | statusText.textContent = 'Connected'; 29 | btnConnect.textContent = 'Disconnect'; 30 | } else { 31 | statusDot.classList.remove('connected'); 32 | statusText.textContent = 'Disconnected'; 33 | btnConnect.textContent = 'Connect'; 34 | } 35 | } 36 | 37 | // Handle incoming command packets 38 | async function onCommandPacket(packet) { 39 | log(`Received command: ${packet.command.action}`); 40 | 41 | let out = { 42 | senderId: packet.senderId, 43 | }; 44 | 45 | try { 46 | // Execute the command in After Effects (from commands.js) 47 | //const response = await executeCommand(packet.command); 48 | const response = await parseAndRouteCommand(packet.command); 49 | 50 | out.response = response; 51 | out.status = "SUCCESS"; 52 | 53 | // Get project info 54 | out.projectInfo = await getProjectInfo(); 55 | 56 | } catch (e) { 57 | out.status = "FAILURE"; 58 | out.message = `Error calling ${packet.command.action}: ${e.message}`; 59 | log(`Error: ${e.message}`); 60 | } 61 | 62 | return out; 63 | } 64 | 65 | // Connect to Socket.IO server 66 | function connectToServer() { 67 | 68 | log(`Connecting to ${PROXY_URL}...`); 69 | 70 | socket = io(PROXY_URL, { 71 | transports: ["websocket", "polling"], 72 | }); 73 | 74 | socket.on("connect", () => { 75 | updateStatus(true); 76 | log(`Connected with ID: ${socket.id}`); 77 | socket.emit("register", { application: APPLICATION }); 78 | }); 79 | 80 | socket.on("command_packet", async (packet) => { 81 | log(`Received command packet`); 82 | const response = await onCommandPacket(packet); 83 | sendResponsePacket(response); 84 | }); 85 | 86 | socket.on("registration_response", (data) => { 87 | log(`Registration confirmed: ${data.message || 'OK'}`); 88 | }); 89 | 90 | socket.on("connect_error", (error) => { 91 | updateStatus(false); 92 | log(`Connection error: ${error.message}`); 93 | }); 94 | 95 | socket.on("disconnect", (reason) => { 96 | updateStatus(false); 97 | log(`Disconnected: ${reason}`); 98 | }); 99 | } 100 | 101 | // Disconnect from server 102 | function disconnectFromServer() { 103 | if (socket && socket.connected) { 104 | socket.disconnect(); 105 | log('Disconnected from server'); 106 | } 107 | } 108 | 109 | // Send response packet 110 | function sendResponsePacket(packet) { 111 | if (socket && socket.connected) { 112 | socket.emit("command_packet_response", { packet }); 113 | log('Response sent'); 114 | log(packet) 115 | return true; 116 | } 117 | return false; 118 | } 119 | 120 | // LocalStorage helpers 121 | const CONNECT_ON_LAUNCH = "connectOnLaunch"; 122 | 123 | function saveSettings() { 124 | localStorage.setItem(CONNECT_ON_LAUNCH, 125 | document.getElementById('chkConnectOnLaunch').checked); 126 | } 127 | 128 | function loadSettings() { 129 | const connectOnLaunch = localStorage.getItem(CONNECT_ON_LAUNCH) === 'true'; 130 | 131 | document.getElementById('chkConnectOnLaunch').checked = connectOnLaunch; 132 | 133 | return connectOnLaunch; 134 | } 135 | 136 | // Event Listeners 137 | document.getElementById('btnConnect').addEventListener('click', () => { 138 | if (socket && socket.connected) { 139 | disconnectFromServer(); 140 | } else { 141 | connectToServer(); 142 | } 143 | saveSettings(); 144 | }); 145 | 146 | document.getElementById('chkConnectOnLaunch').addEventListener('change', saveSettings); 147 | 148 | 149 | function initializeExtension() { 150 | const csInterface = new CSInterface(); 151 | const extensionPath = csInterface.getSystemPath(SystemPath.EXTENSION); 152 | const polyfillPath = extensionPath + '/jsx/json-polyfill.jsx'; 153 | 154 | csInterface.evalScript(`$.evalFile("${polyfillPath}")`, function(result) { 155 | console.log('JSON polyfill loaded'); 156 | }); 157 | } 158 | 159 | // Initialize on load 160 | window.addEventListener('load', () => { 161 | initializeExtension() 162 | const connectOnLaunch = loadSettings(); 163 | log('Plugin loaded'); 164 | 165 | if (connectOnLaunch) { 166 | connectToServer(); 167 | } 168 | }); 169 | ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ae/commands.js: -------------------------------------------------------------------------------- ```javascript 1 | /* commands.js 2 | * After Effects command handlers 3 | */ 4 | 5 | // Execute After Effects command via ExtendScript 6 | function executeAECommand(script) { 7 | return new Promise((resolve, reject) => { 8 | const csInterface = new CSInterface(); 9 | csInterface.evalScript(script, (result) => { 10 | if (result === 'EvalScript error.') { 11 | reject(new Error('ExtendScript execution failed')); 12 | } else { 13 | try { 14 | resolve(JSON.parse(result)); 15 | } catch (e) { 16 | resolve(result); 17 | } 18 | } 19 | }); 20 | }); 21 | } 22 | 23 | // Get project information 24 | async function getProjectInfo() { 25 | const script = ` 26 | (function() { 27 | var info = { 28 | numItems: app.project.numItems, 29 | activeItemIndex: app.project.activeItem ? app.project.activeItem.id : null, 30 | projectName: app.project.file ? app.project.file.name : "Untitled" 31 | }; 32 | return JSON.stringify(info); 33 | })(); 34 | `; 35 | return await executeAECommand(script); 36 | } 37 | 38 | // Get all compositions 39 | async function getCompositions() { 40 | const script = ` 41 | (function() { 42 | var comps = []; 43 | for (var i = 1; i <= app.project.numItems; i++) { 44 | var item = app.project.item(i); 45 | if (item instanceof CompItem) { 46 | comps.push({ 47 | id: item.id, 48 | name: item.name, 49 | width: item.width, 50 | height: item.height, 51 | duration: item.duration, 52 | frameRate: item.frameRate 53 | }); 54 | } 55 | } 56 | return JSON.stringify(comps); 57 | })(); 58 | `; 59 | return await executeAECommand(script); 60 | } 61 | 62 | async function executeExtendScript(command) { 63 | console.log(command) 64 | const options = command.options 65 | const scriptString = options.scriptString; 66 | 67 | const script = ` 68 | (function() { 69 | try { 70 | var result = (function() { 71 | ${scriptString} 72 | })(); 73 | 74 | // If result is undefined, return null 75 | if (result === undefined) { 76 | return 'null'; 77 | } 78 | 79 | // Return stringified result 80 | return JSON.stringify(result); 81 | } catch(e) { 82 | return JSON.stringify({ 83 | error: e.toString(), 84 | line: e.line || 'unknown' 85 | }); 86 | } 87 | })(); 88 | `; 89 | 90 | const result = await executeAECommand(script); 91 | 92 | return createPacket(result); 93 | } 94 | 95 | 96 | async function getLayers() { 97 | const script = ` 98 | var comp = app.project.activeItem; 99 | if (!comp || !(comp instanceof CompItem)) { 100 | JSON.stringify({error: "No active composition"}); 101 | } else { 102 | var layers = []; 103 | for (var i = 1; i <= comp.numLayers; i++) { 104 | var layer = comp.layer(i); 105 | layers.push({ 106 | index: layer.index, 107 | name: layer.name, 108 | enabled: layer.enabled, 109 | selected: layer.selected, 110 | startTime: layer.startTime, 111 | inPoint: layer.inPoint, 112 | outPoint: layer.outPoint 113 | }); 114 | } 115 | JSON.stringify(layers); 116 | } 117 | `; 118 | 119 | const result = await executeAECommand(script); 120 | 121 | 122 | return createPacket(result); 123 | /*return { 124 | content: [{ 125 | type: "text", 126 | text: JSON.stringify(result, null, 2) 127 | }] 128 | };*/ 129 | } 130 | 131 | const createPacket = (result) => { 132 | return { 133 | content: [{ 134 | type: "text", 135 | text: JSON.stringify(result, null, 2) 136 | }] 137 | }; 138 | } 139 | 140 | const parseAndRouteCommand = async (command) => { 141 | let action = command.action; 142 | 143 | let f = commandHandlers[action]; 144 | 145 | if (typeof f !== "function") { 146 | throw new Error(`Unknown Command: ${action}`); 147 | } 148 | 149 | console.log(f.name) 150 | return await f(command); 151 | }; 152 | 153 | 154 | // Execute commands 155 | /* 156 | async function executeCommand(command) { 157 | switch(command.action) { 158 | 159 | case "getLayers": 160 | return await getLayers(); 161 | 162 | case "executeExtendScript": 163 | return await executeExtendScript(command); 164 | 165 | default: 166 | throw new Error(`Unknown command: ${command.action}`); 167 | } 168 | }*/ 169 | 170 | const commandHandlers = { 171 | getLayers, 172 | executeExtendScript 173 | }; ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/main.js: -------------------------------------------------------------------------------- ```javascript 1 | /* Socket.IO Plugin for After Effects (CEP) 2 | * Main JavaScript file 3 | */ 4 | 5 | const csInterface = new CSInterface(); 6 | const APPLICATION = "illustrator"; 7 | const PROXY_URL = "http://localhost:3001"; 8 | 9 | 10 | let socket = null; 11 | 12 | // Log function 13 | function log(message) { 14 | const logArea = document.getElementById('messageLog'); 15 | const timestamp = new Date().toLocaleTimeString(); 16 | logArea.value += `[${timestamp}] ${message}\n`; 17 | logArea.scrollTop = logArea.scrollHeight; 18 | } 19 | 20 | // Update UI status 21 | function updateStatus(connected) { 22 | const statusDot = document.getElementById('statusDot'); 23 | const statusText = document.getElementById('statusText'); 24 | const btnConnect = document.getElementById('btnConnect'); 25 | 26 | if (connected) { 27 | statusDot.classList.add('connected'); 28 | statusText.textContent = 'Connected'; 29 | btnConnect.textContent = 'Disconnect'; 30 | } else { 31 | statusDot.classList.remove('connected'); 32 | statusText.textContent = 'Disconnected'; 33 | btnConnect.textContent = 'Connect'; 34 | } 35 | } 36 | 37 | // Handle incoming command packets 38 | async function onCommandPacket(packet) { 39 | log(`Received command: ${packet.command.action}`); 40 | 41 | let out = { 42 | senderId: packet.senderId, 43 | }; 44 | 45 | try { 46 | // Execute the command in After Effects (from commands.js) 47 | //const response = await executeCommand(packet.command); 48 | const response = await parseAndRouteCommand(packet.command); 49 | 50 | out.response = response; 51 | out.status = "SUCCESS"; 52 | 53 | // Get project info 54 | //out.projectInfo = await getProjectInfo(); 55 | out.document = await getActiveDocumentInfo(); 56 | 57 | } catch (e) { 58 | out.status = "FAILURE"; 59 | out.message = `Error calling ${packet.command.action}: ${e.message}`; 60 | log(`Error: ${e.message}`); 61 | } 62 | 63 | return out; 64 | } 65 | 66 | // Connect to Socket.IO server 67 | function connectToServer() { 68 | 69 | log(`Connecting to ${PROXY_URL}...`); 70 | 71 | socket = io(PROXY_URL, { 72 | transports: ["websocket", "polling"], 73 | }); 74 | 75 | socket.on("connect", () => { 76 | updateStatus(true); 77 | log(`Connected with ID: ${socket.id}`); 78 | socket.emit("register", { application: APPLICATION }); 79 | }); 80 | 81 | socket.on("command_packet", async (packet) => { 82 | log(`Received command packet`); 83 | const response = await onCommandPacket(packet); 84 | sendResponsePacket(response); 85 | }); 86 | 87 | socket.on("registration_response", (data) => { 88 | log(`Registration confirmed: ${data.message || 'OK'}`); 89 | }); 90 | 91 | socket.on("connect_error", (error) => { 92 | updateStatus(false); 93 | log(`Connection error: ${error.message}`); 94 | }); 95 | 96 | socket.on("disconnect", (reason) => { 97 | updateStatus(false); 98 | log(`Disconnected: ${reason}`); 99 | }); 100 | } 101 | 102 | // Disconnect from server 103 | function disconnectFromServer() { 104 | if (socket && socket.connected) { 105 | socket.disconnect(); 106 | log('Disconnected from server'); 107 | } 108 | } 109 | 110 | // Send response packet 111 | function sendResponsePacket(packet) { 112 | if (socket && socket.connected) { 113 | socket.emit("command_packet_response", { packet }); 114 | log('Response sent'); 115 | log(packet) 116 | return true; 117 | } 118 | return false; 119 | } 120 | 121 | // LocalStorage helpers 122 | const CONNECT_ON_LAUNCH = "connectOnLaunch"; 123 | 124 | function saveSettings() { 125 | localStorage.setItem(CONNECT_ON_LAUNCH, 126 | document.getElementById('chkConnectOnLaunch').checked); 127 | } 128 | 129 | function loadSettings() { 130 | const connectOnLaunch = localStorage.getItem(CONNECT_ON_LAUNCH) === 'true'; 131 | 132 | document.getElementById('chkConnectOnLaunch').checked = connectOnLaunch; 133 | 134 | return connectOnLaunch; 135 | } 136 | 137 | // Event Listeners 138 | document.getElementById('btnConnect').addEventListener('click', () => { 139 | if (socket && socket.connected) { 140 | disconnectFromServer(); 141 | } else { 142 | connectToServer(); 143 | } 144 | saveSettings(); 145 | }); 146 | 147 | document.getElementById('chkConnectOnLaunch').addEventListener('change', saveSettings); 148 | 149 | 150 | function initializeExtension() { 151 | const csInterface = new CSInterface(); 152 | const extensionPath = csInterface.getSystemPath(SystemPath.EXTENSION); 153 | const polyfillPath = extensionPath + '/jsx/json-polyfill.jsx'; 154 | const utilsPath = extensionPath + '/jsx/utils.jsx'; 155 | 156 | csInterface.evalScript(`$.evalFile("${polyfillPath}")`, function(result) { 157 | console.log('JSON polyfill loaded'); 158 | }); 159 | 160 | csInterface.evalScript(`$.evalFile("${utilsPath}")`, function(result) { 161 | console.log('utilsPath loaded'); 162 | }); 163 | } 164 | 165 | // Initialize on load 166 | window.addEventListener('load', () => { 167 | initializeExtension() 168 | const connectOnLaunch = loadSettings(); 169 | log('Plugin loaded'); 170 | 171 | if (connectOnLaunch) { 172 | connectToServer(); 173 | } 174 | }); 175 | ``` -------------------------------------------------------------------------------- /adb-proxy-socket/proxy.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /* MIT License 4 | * 5 | * Copyright (c) 2025 Mike Chambers 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in all 15 | * copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE. 24 | */ 25 | 26 | const express = require("express"); 27 | const http = require("http"); 28 | const { Server } = require("socket.io"); 29 | const app = express(); 30 | const server = http.createServer(app); 31 | const io = new Server(server, { 32 | transports: ["websocket", "polling"], 33 | maxHttpBufferSize: 50 * 1024 * 1024, 34 | }); 35 | 36 | const PORT = 3001; 37 | // Track clients by application 38 | const applicationClients = {}; 39 | 40 | io.on("connection", (socket) => { 41 | console.log(`User connected: ${socket.id}`); 42 | 43 | socket.on("register", ({ application }) => { 44 | console.log( 45 | `Client ${socket.id} registered for application: ${application}` 46 | ); 47 | 48 | // Store the application preference with this socket 49 | socket.data.application = application; 50 | 51 | // Register this client for this application 52 | if (!applicationClients[application]) { 53 | applicationClients[application] = new Set(); 54 | } 55 | applicationClients[application].add(socket.id); 56 | 57 | // Optionally confirm registration 58 | socket.emit("registration_response", { 59 | type: "registration", 60 | status: "success", 61 | message: `Registered for ${application}`, 62 | }); 63 | }); 64 | 65 | socket.on("command_packet_response", ({ packet }) => { 66 | const senderId = packet.senderId; 67 | 68 | if (senderId) { 69 | io.to(senderId).emit("packet_response", packet); 70 | console.log(`Sent confirmation to client ${senderId}`); 71 | } else { 72 | console.log(`No sender ID provided in packet`); 73 | } 74 | }); 75 | 76 | socket.on("command_packet", ({ application, command }) => { 77 | console.log( 78 | `Command from ${socket.id} for application ${application}:`, 79 | command 80 | ); 81 | 82 | // Register this client for this application if not already registered 83 | //if (!applicationClients[application]) { 84 | // applicationClients[application] = new Set(); 85 | //} 86 | //applicationClients[application].add(socket.id); 87 | 88 | // Process the command 89 | 90 | let packet = { 91 | senderId: socket.id, 92 | application: application, 93 | command: command, 94 | }; 95 | 96 | sendToApplication(packet); 97 | 98 | // Send response back to this client 99 | //socket.emit('json_response', { from: 'server', command }); 100 | }); 101 | 102 | socket.on("disconnect", () => { 103 | console.log(`User disconnected: ${socket.id}`); 104 | 105 | // Remove this client from all application registrations 106 | for (const app in applicationClients) { 107 | applicationClients[app].delete(socket.id); 108 | // Clean up empty sets 109 | if (applicationClients[app].size === 0) { 110 | delete applicationClients[app]; 111 | } 112 | } 113 | }); 114 | }); 115 | 116 | // Add a function to send messages to clients by application 117 | function sendToApplication(packet) { 118 | let application = packet.application; 119 | if (applicationClients[application]) { 120 | console.log( 121 | `Sending to ${applicationClients[application].size} clients for ${application}` 122 | ); 123 | 124 | let senderId = packet.senderId; 125 | // Loop through all client IDs for this application 126 | applicationClients[application].forEach((clientId) => { 127 | io.to(clientId).emit("command_packet", packet); 128 | }); 129 | return true; 130 | } 131 | console.log(`No clients registered for application: ${application}`); 132 | return false; 133 | } 134 | 135 | // Example: Use this function elsewhere in your code 136 | // sendToApplication('photoshop', { message: 'Update available' }); 137 | 138 | server.listen(PORT, () => { 139 | console.log( 140 | `adb-mcp Command proxy server running on ws://localhost:${PORT}` 141 | ); 142 | }); 143 | ``` -------------------------------------------------------------------------------- /uxp/pr/main.js: -------------------------------------------------------------------------------- ```javascript 1 | /* MIT License 2 | * 3 | * Copyright (c) 2025 Mike Chambers 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | const { entrypoints } = require("uxp"); 25 | const { io } = require("./socket.io.js"); 26 | 27 | const { getSequences } = require("./commands/utils.js"); 28 | 29 | const { 30 | getProjectInfo, 31 | parseAndRouteCommand, 32 | checkRequiresActiveProject, 33 | } = require("./commands/index.js"); 34 | 35 | const APPLICATION = "premiere"; 36 | const PROXY_URL = "http://localhost:3001"; 37 | 38 | let socket = null; 39 | 40 | const onCommandPacket = async (packet) => { 41 | let command = packet.command; 42 | 43 | let out = { 44 | senderId: packet.senderId, 45 | }; 46 | 47 | try { 48 | //this will throw if an active document is required and not open 49 | await checkRequiresActiveProject(command); 50 | 51 | let response = await parseAndRouteCommand(command); 52 | 53 | out.response = response; 54 | out.status = "SUCCESS"; 55 | out.sequences = await getSequences(); 56 | out.project = await getProjectInfo(); 57 | 58 | } catch (e) { 59 | 60 | console.log(e) 61 | 62 | out.status = "FAILURE"; 63 | out.message = `Error calling ${command.action} : ${e}`; 64 | } 65 | 66 | return out; 67 | }; 68 | 69 | function connectToServer() { 70 | // Create new Socket.IO connection 71 | socket = io(PROXY_URL, { 72 | transports: ["websocket"], 73 | }); 74 | 75 | socket.on("connect", () => { 76 | updateButton(); 77 | console.log("Connected to server with ID:", socket.id); 78 | socket.emit("register", { application: APPLICATION }); 79 | }); 80 | 81 | socket.on("command_packet", async (packet) => { 82 | console.log("Received command packet:", packet); 83 | 84 | let response = await onCommandPacket(packet); 85 | sendResponsePacket(response); 86 | }); 87 | 88 | socket.on("registration_response", (data) => { 89 | console.log("Received response:", data); 90 | //TODO: connect button here 91 | }); 92 | 93 | socket.on("connect_error", (error) => { 94 | updateButton(); 95 | console.error("Connection error:", error); 96 | }); 97 | 98 | socket.on("disconnect", (reason) => { 99 | updateButton(); 100 | console.log("Disconnected from server. Reason:", reason); 101 | 102 | //TODO:connect button here 103 | }); 104 | 105 | return socket; 106 | } 107 | 108 | function disconnectFromServer() { 109 | if (socket && socket.connected) { 110 | socket.disconnect(); 111 | console.log("Disconnected from server"); 112 | } 113 | } 114 | 115 | function sendResponsePacket(packet) { 116 | if (socket && socket.connected) { 117 | socket.emit("command_packet_response", { 118 | packet: packet, 119 | }); 120 | return true; 121 | } 122 | return false; 123 | } 124 | 125 | function sendCommand(command) { 126 | if (socket && socket.connected) { 127 | socket.emit("app_command", { 128 | application: APPLICATION, 129 | command: command, 130 | }); 131 | return true; 132 | } 133 | return false; 134 | } 135 | 136 | entrypoints.setup({ 137 | panels: { 138 | vanilla: { 139 | show(node) {}, 140 | }, 141 | }, 142 | }); 143 | 144 | let updateButton = () => { 145 | let b = document.getElementById("btnStart"); 146 | 147 | b.textContent = socket && socket.connected ? "Disconnect" : "Connect"; 148 | }; 149 | 150 | //Toggle button to make it start stop 151 | document.getElementById("btnStart").addEventListener("click", () => { 152 | if (socket && socket.connected) { 153 | disconnectFromServer(); 154 | } else { 155 | connectToServer(); 156 | } 157 | }); 158 | 159 | const CONNECT_ON_LAUNCH = "connectOnLaunch"; 160 | // Save checkbox state in localStorage 161 | document 162 | .getElementById("chkConnectOnLaunch") 163 | .addEventListener("change", function (event) { 164 | window.localStorage.setItem( 165 | CONNECT_ON_LAUNCH, 166 | JSON.stringify(event.target.checked) 167 | ); 168 | }); 169 | 170 | // Retrieve checkbox state 171 | const getConnectOnLaunch = () => { 172 | return JSON.parse(window.localStorage.getItem(CONNECT_ON_LAUNCH)) || false; 173 | }; 174 | 175 | // Set checkbox state on page load 176 | document.addEventListener("DOMContentLoaded", () => { 177 | document.getElementById("chkConnectOnLaunch").checked = 178 | getConnectOnLaunch(); 179 | }); 180 | 181 | window.addEventListener("load", (event) => { 182 | if (getConnectOnLaunch()) { 183 | connectToServer(); 184 | } 185 | }); 186 | ``` -------------------------------------------------------------------------------- /uxp/id/main.js: -------------------------------------------------------------------------------- ```javascript 1 | /* MIT License 2 | * 3 | * Copyright (c) 2025 Mike Chambers 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | const { entrypoints, UI } = require("uxp"); 25 | const { io } = require("./socket.io.js"); 26 | const app = require("indesign"); 27 | 28 | const { 29 | parseAndRouteCommand, 30 | checkRequiresActiveDocument, 31 | getActiveDocumentSettings, 32 | } = require("./commands/index.js"); 33 | 34 | const APPLICATION = "indesign"; 35 | const PROXY_URL = "http://localhost:3001"; 36 | 37 | let socket = null; 38 | 39 | const onCommandPacket = async (packet) => { 40 | let command = packet.command; 41 | 42 | let out = { 43 | senderId: packet.senderId, 44 | }; 45 | 46 | try { 47 | //this will throw if an active document is required and not open 48 | checkRequiresActiveDocument(command); 49 | 50 | let response = await parseAndRouteCommand(command); 51 | 52 | out.response = response; 53 | out.status = "SUCCESS"; 54 | out.activeDocument = await getActiveDocumentSettings(); 55 | //out.projectItems = await getProjectContentInfo(); 56 | } catch (e) { 57 | out.status = "FAILURE"; 58 | out.message = `Error calling ${command.action} : ${e}`; 59 | } 60 | 61 | return out; 62 | }; 63 | 64 | function connectToServer() { 65 | // Create new Socket.IO connection 66 | const isWindows = require("os").platform() === "win32"; 67 | 68 | const socketOptions = isWindows 69 | ? { 70 | transports: ["polling"], 71 | upgrade: false, 72 | rememberUpgrade: false, 73 | } 74 | : { 75 | transports: ["websocket"], 76 | }; 77 | console.log(isWindows); 78 | console.log(socketOptions); 79 | socket = io(PROXY_URL, socketOptions); 80 | 81 | socket.on("connect", () => { 82 | updateButton(); 83 | console.log("Connected to server with ID:", socket.id); 84 | socket.emit("register", { application: APPLICATION }); 85 | }); 86 | 87 | socket.on("command_packet", async (packet) => { 88 | console.log("Received command packet:", packet); 89 | 90 | let response = await onCommandPacket(packet); 91 | sendResponsePacket(response); 92 | }); 93 | 94 | socket.on("registration_response", (data) => { 95 | console.log("Received response:", data); 96 | //TODO: connect button here 97 | }); 98 | 99 | socket.on("connect_error", (error) => { 100 | updateButton(); 101 | console.error("Connection error:", error); 102 | }); 103 | 104 | socket.on("disconnect", (reason) => { 105 | updateButton(); 106 | console.log("Disconnected from server. Reason:", reason); 107 | 108 | //TODO:connect button here 109 | }); 110 | 111 | return socket; 112 | } 113 | 114 | function disconnectFromServer() { 115 | if (socket && socket.connected) { 116 | socket.disconnect(); 117 | console.log("Disconnected from server"); 118 | } 119 | } 120 | 121 | function sendResponsePacket(packet) { 122 | if (socket && socket.connected) { 123 | socket.emit("command_packet_response", { 124 | packet: packet, 125 | }); 126 | return true; 127 | } 128 | return false; 129 | } 130 | 131 | function sendCommand(command) { 132 | if (socket && socket.connected) { 133 | socket.emit("app_command", { 134 | application: APPLICATION, 135 | command: command, 136 | }); 137 | return true; 138 | } 139 | return false; 140 | } 141 | 142 | entrypoints.setup({ 143 | panels: { 144 | vanilla: { 145 | show(node) {}, 146 | }, 147 | }, 148 | }); 149 | 150 | let updateButton = () => { 151 | let b = document.getElementById("btnStart"); 152 | 153 | b.textContent = socket && socket.connected ? "Disconnect" : "Connect"; 154 | }; 155 | 156 | //Toggle button to make it start stop 157 | document.getElementById("btnStart").addEventListener("click", () => { 158 | if (socket && socket.connected) { 159 | disconnectFromServer(); 160 | } else { 161 | connectToServer(); 162 | } 163 | }); 164 | 165 | const CONNECT_ON_LAUNCH = "connectOnLaunch"; 166 | // Save checkbox state in localStorage 167 | document 168 | .getElementById("chkConnectOnLaunch") 169 | .addEventListener("change", function (event) { 170 | window.localStorage.setItem( 171 | CONNECT_ON_LAUNCH, 172 | JSON.stringify(event.target.checked) 173 | ); 174 | }); 175 | 176 | // Retrieve checkbox state 177 | const getConnectOnLaunch = () => { 178 | return JSON.parse(window.localStorage.getItem(CONNECT_ON_LAUNCH)) || false; 179 | }; 180 | 181 | // Set checkbox state on page load 182 | document.addEventListener("DOMContentLoaded", () => { 183 | document.getElementById("chkConnectOnLaunch").checked = 184 | getConnectOnLaunch(); 185 | }); 186 | 187 | window.addEventListener("load", (event) => { 188 | if (getConnectOnLaunch()) { 189 | connectToServer(); 190 | } 191 | }); 192 | ``` -------------------------------------------------------------------------------- /mcp/fonts.py: -------------------------------------------------------------------------------- ```python 1 | # MIT License 2 | # 3 | # Copyright (c) 2025 Mike Chambers 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import os 24 | import sys 25 | import glob 26 | from fontTools.ttLib import TTFont 27 | 28 | def list_all_fonts_postscript(): 29 | """ 30 | Returns a list of PostScript names for all fonts installed on the system. 31 | Works on both Windows and macOS. 32 | 33 | Returns: 34 | list: A list of PostScript font names as strings 35 | """ 36 | postscript_names = [] 37 | 38 | # Get font directories based on platform 39 | font_dirs = [] 40 | 41 | if sys.platform == 'win32': # Windows 42 | # Windows font directory 43 | if 'WINDIR' in os.environ: 44 | font_dirs.append(os.path.join(os.environ['WINDIR'], 'Fonts')) 45 | 46 | elif sys.platform == 'darwin': # macOS 47 | # macOS system font directories 48 | font_dirs.extend([ 49 | '/System/Library/Fonts', 50 | '/Library/Fonts', 51 | os.path.expanduser('~/Library/Fonts') 52 | ]) 53 | 54 | else: 55 | print(f"Unsupported platform: {sys.platform}") 56 | return [] 57 | 58 | # Get all font files from all directories 59 | font_extensions = ['*.ttf', '*.ttc', '*.otf'] 60 | font_files = [] 61 | 62 | for font_dir in font_dirs: 63 | if os.path.exists(font_dir): 64 | for ext in font_extensions: 65 | font_files.extend(glob.glob(os.path.join(font_dir, ext))) 66 | # Also check subdirectories on macOS 67 | if sys.platform == 'darwin': 68 | font_files.extend(glob.glob(os.path.join(font_dir, '**', ext), recursive=True)) 69 | 70 | # Process each font file 71 | for font_path in font_files: 72 | try: 73 | # TrueType Collections (.ttc files) can contain multiple fonts 74 | if font_path.lower().endswith('.ttc'): 75 | try: 76 | ttc = TTFont(font_path, fontNumber=0) 77 | num_fonts = ttc.reader.numFonts 78 | ttc.close() 79 | 80 | # Extract PostScript name from each font in the collection 81 | for i in range(num_fonts): 82 | try: 83 | font = TTFont(font_path, fontNumber=i) 84 | ps_name = _extract_postscript_name(font) 85 | if ps_name and not ps_name.startswith('.'): 86 | postscript_names.append(ps_name) 87 | font.close() 88 | except Exception as e: 89 | print(f"Error processing font {i} in collection {font_path}: {e}") 90 | except Exception as e: 91 | print(f"Error determining number of fonts in collection {font_path}: {e}") 92 | else: 93 | # Regular TTF/OTF file 94 | try: 95 | font = TTFont(font_path) 96 | ps_name = _extract_postscript_name(font) 97 | if ps_name: 98 | postscript_names.append(ps_name) 99 | font.close() 100 | except Exception as e: 101 | print(f"Error processing font {font_path}: {e}") 102 | except Exception as e: 103 | print(f"Error with font file {font_path}: {e}") 104 | 105 | return list(set(postscript_names)) 106 | 107 | def _extract_postscript_name(font): 108 | """ 109 | Extract the PostScript name from a TTFont object. 110 | 111 | Args: 112 | font: A TTFont object 113 | 114 | Returns: 115 | str: The PostScript name or None if not found 116 | """ 117 | # Method 1: Try to get it from the name table (most reliable) 118 | if 'name' in font: 119 | name_table = font['name'] 120 | 121 | # PostScript name is stored with nameID 6 122 | for record in name_table.names: 123 | if record.nameID == 6: 124 | # Try to decode the name 125 | try: 126 | return ( 127 | record.string.decode('utf-16-be').encode('utf-8').decode('utf-8') 128 | if record.isUnicode() else record.string.decode('latin-1') 129 | ) 130 | except Exception: 131 | pass 132 | 133 | # Method 2: For CFF OpenType fonts 134 | if 'CFF ' in font: 135 | try: 136 | cff = font['CFF '] 137 | if cff.cff.fontNames: 138 | return cff.cff.fontNames[0] 139 | except Exception: 140 | pass 141 | 142 | return None 143 | 144 | if __name__ == "__main__": 145 | font_names = list_all_fonts_postscript() 146 | print(f"Number of fonts found: {len(font_names)}") ``` -------------------------------------------------------------------------------- /mcp/socket_client.py: -------------------------------------------------------------------------------- ```python 1 | # MIT License 2 | # 3 | # Copyright (c) 2025 Mike Chambers 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import socketio 24 | import time 25 | import threading 26 | import json 27 | from queue import Queue 28 | import logger 29 | 30 | # Global configuration variables 31 | proxy_url = None 32 | proxy_timeout = None 33 | application = None 34 | 35 | def send_message_blocking(command, timeout=None): 36 | """ 37 | Blocking function that connects to a Socket.IO server, sends a message, 38 | waits for a response, then disconnects. 39 | 40 | Args: 41 | command: The command to send 42 | timeout (int): Maximum time to wait for response in seconds 43 | 44 | Returns: 45 | dict: The response received from the server, or None if no response 46 | """ 47 | # Use global variables 48 | global application, proxy_url, proxy_timeout 49 | 50 | # Check if configuration is set 51 | if not application or not proxy_url or not proxy_timeout: 52 | logger.log("Socket client not configured. Call configure() first.") 53 | return None 54 | 55 | # Use provided timeout or default 56 | wait_timeout = timeout if timeout is not None else proxy_timeout 57 | 58 | # Create a standard (non-async) SocketIO client with WebSocket transport only 59 | sio = socketio.Client(logger=False) 60 | 61 | # Use a queue to get the response from the event handler 62 | response_queue = Queue() 63 | 64 | connection_failed = [False] 65 | 66 | @sio.event 67 | def connect(): 68 | logger.log(f"Connected to server with session ID: {sio.sid}") 69 | 70 | # Send the command 71 | logger.log(f"Sending message to {application}: {command}") 72 | sio.emit('command_packet', { 73 | 'type': "command", 74 | 'application': application, 75 | 'command': command 76 | }) 77 | 78 | @sio.event 79 | def packet_response(data): 80 | logger.log(f"Received response: {data}") 81 | response_queue.put(data) 82 | # Disconnect after receiving the response 83 | sio.disconnect() 84 | 85 | @sio.event 86 | def disconnect(): 87 | logger.log("Disconnected from server") 88 | # If we disconnect without response, put None in the queue 89 | if response_queue.empty(): 90 | response_queue.put(None) 91 | 92 | @sio.event 93 | def connect_error(error): 94 | logger.log(f"Connection error: {error}") 95 | connection_failed[0] = True 96 | response_queue.put(None) 97 | 98 | # Connect in a separate thread to avoid blocking the main thread during connection 99 | def connect_and_wait(): 100 | try: 101 | sio.connect(proxy_url, transports=['websocket']) 102 | # Keep the client running until disconnect is called 103 | sio.wait() 104 | except Exception as e: 105 | logger.log(f"Error: {e}") 106 | connection_failed[0] = True 107 | if response_queue.empty(): 108 | response_queue.put(None) 109 | if sio.connected: 110 | sio.disconnect() 111 | 112 | # Start the client in a separate thread 113 | client_thread = threading.Thread(target=connect_and_wait) 114 | client_thread.daemon = True 115 | client_thread.start() 116 | 117 | try: 118 | # Wait for a response or timeout 119 | logger.log("waiting for response...") 120 | response = response_queue.get(timeout=wait_timeout) 121 | 122 | if connection_failed[0]: 123 | 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}.") 124 | 125 | if response: 126 | logger.log("response received...") 127 | try: 128 | logger.log(json.dumps(response)) 129 | except: 130 | logger.log(f"Response (not JSON-serializable): {response}") 131 | 132 | if response["status"] == "FAILURE": 133 | raise AppError(f"Error returned from {application}: {response['message']}") 134 | 135 | return response 136 | except AppError: 137 | raise 138 | except Exception as e: 139 | logger.log(f"Error waiting for response: {e}") 140 | if sio.connected: 141 | sio.disconnect() 142 | 143 | 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}") 144 | finally: 145 | # Make sure client is disconnected 146 | if sio.connected: 147 | sio.disconnect() 148 | # Wait for the thread to finish (should be quick after disconnect) 149 | client_thread.join(timeout=1) 150 | 151 | class AppError(Exception): 152 | pass 153 | 154 | def configure(app=None, url=None, timeout=None): 155 | 156 | global application, proxy_url, proxy_timeout 157 | 158 | if app: 159 | application = app 160 | if url: 161 | proxy_url = url 162 | if timeout: 163 | proxy_timeout = timeout 164 | 165 | logger.log(f"Socket client configured: app={application}, url={proxy_url}, timeout={proxy_timeout}") ``` -------------------------------------------------------------------------------- /cep/com.mikechambers.ai/jsx/utils.jsx: -------------------------------------------------------------------------------- ```javascript 1 | // jsx/illustrator-helpers.jsx 2 | 3 | // Helper function to extract XMP attribute values 4 | $.global.extractXMPAttribute = function(xmpStr, tagName, attrName) { 5 | var pattern = new RegExp(tagName + '[^>]*' + attrName + '="([^"]+)"', 'i'); 6 | var match = xmpStr.match(pattern); 7 | return match ? match[1] : null; 8 | }; 9 | 10 | // Helper function to extract XMP tag values 11 | $.global.extractXMPValue = function(xmpStr, tagName) { 12 | var pattern = new RegExp('<' + tagName + '>([^<]+)<\\/' + tagName + '>', 'i'); 13 | var match = xmpStr.match(pattern); 14 | return match ? match[1] : null; 15 | }; 16 | 17 | // Helper function to get document ID from XMP 18 | $.global.getDocumentID = function(doc) { 19 | try { 20 | var xmpString = doc.XMPString; 21 | if (!xmpString) return null; 22 | 23 | return $.global.extractXMPAttribute(xmpString, 'xmpMM:DocumentID', 'rdf:resource') || 24 | $.global.extractXMPValue(xmpString, 'xmpMM:DocumentID'); 25 | } catch(e) { 26 | return null; 27 | } 28 | }; 29 | 30 | // jsx/illustrator-helpers.jsx 31 | 32 | // ... existing helper functions ... 33 | 34 | // Helper function to create document info object 35 | $.global.createDocumentInfo = function(doc, activeDoc) { 36 | return { 37 | id: $.global.getDocumentID(doc), 38 | name: doc.name, 39 | width: doc.width, 40 | height: doc.height, 41 | colorSpace: doc.documentColorSpace.toString(), 42 | numLayers: doc.layers.length, 43 | numArtboards: doc.artboards.length, 44 | saved: doc.saved, 45 | isActive: doc === activeDoc 46 | }; 47 | }; 48 | 49 | 50 | // Helper function to get detailed layer information 51 | $.global.getLayerInfo = function(layer, includeSubLayers) { 52 | if (includeSubLayers === undefined) includeSubLayers = true; 53 | 54 | try { 55 | var layerInfo = { 56 | id: layer.absoluteZOrderPosition, 57 | name: layer.name, 58 | visible: layer.visible, 59 | locked: layer.locked, 60 | opacity: layer.opacity, 61 | printable: layer.printable, 62 | preview: layer.preview, 63 | sliced: layer.sliced, 64 | isIsolated: layer.isIsolated, 65 | hasSelectedArtwork: layer.hasSelectedArtwork, 66 | itemCount: layer.pageItems.length, 67 | zOrderPosition: layer.zOrderPosition, 68 | absoluteZOrderPosition: layer.absoluteZOrderPosition, 69 | dimPlacedImages: layer.dimPlacedImages, 70 | typename: layer.typename 71 | }; 72 | 73 | // Get blending mode 74 | try { 75 | layerInfo.blendingMode = layer.blendingMode.toString(); 76 | } catch(e) { 77 | layerInfo.blendingMode = "Normal"; 78 | } 79 | 80 | // Get color info if available 81 | try { 82 | layerInfo.color = { 83 | red: layer.color.red, 84 | green: layer.color.green, 85 | blue: layer.color.blue 86 | }; 87 | } catch(e) { 88 | layerInfo.color = null; 89 | } 90 | 91 | // Get artwork knockout state 92 | try { 93 | layerInfo.artworkKnockout = layer.artworkKnockout.toString(); 94 | } catch(e) { 95 | layerInfo.artworkKnockout = "Inherited"; 96 | } 97 | 98 | // Count different types of items on the layer 99 | try { 100 | layerInfo.itemCounts = { 101 | total: layer.pageItems.length, 102 | pathItems: layer.pathItems.length, 103 | textFrames: layer.textFrames.length, 104 | groupItems: layer.groupItems.length, 105 | compoundPathItems: layer.compoundPathItems.length, 106 | placedItems: layer.placedItems.length, 107 | rasterItems: layer.rasterItems.length, 108 | meshItems: layer.meshItems.length, 109 | symbolItems: layer.symbolItems.length 110 | }; 111 | } catch(e) { 112 | layerInfo.itemCounts = { total: 0 }; 113 | } 114 | 115 | // Handle sublayers 116 | layerInfo.subLayerCount = layer.layers.length; 117 | layerInfo.hasSubLayers = layer.layers.length > 0; 118 | 119 | if (includeSubLayers && layer.layers.length > 0) { 120 | layerInfo.subLayers = []; 121 | for (var j = 0; j < layer.layers.length; j++) { 122 | var subLayer = layer.layers[j]; 123 | // Recursively get sublayer info (but don't go deeper to avoid infinite recursion) 124 | var subLayerInfo = $.global.getLayerInfo(subLayer, false); 125 | layerInfo.subLayers.push(subLayerInfo); 126 | } 127 | } 128 | 129 | return layerInfo; 130 | } catch(e) { 131 | return { 132 | error: "Error processing layer: " + e.toString(), 133 | layerName: layer.name || "Unknown" 134 | }; 135 | } 136 | }; 137 | 138 | // Helper function to get all layers information for a document 139 | $.global.getAllLayersInfo = function(doc) { 140 | try { 141 | var layersInfo = []; 142 | 143 | for (var i = 0; i < doc.layers.length; i++) { 144 | var layer = doc.layers[i]; 145 | var layerInfo = $.global.getLayerInfo(layer, true); 146 | layersInfo.push(layerInfo); 147 | } 148 | 149 | return { 150 | totalLayers: doc.layers.length, 151 | layers: layersInfo 152 | }; 153 | } catch(e) { 154 | return { 155 | error: e.toString(), 156 | totalLayers: 0, 157 | layers: [] 158 | }; 159 | } 160 | }; 161 | 162 | $.global.createDocumentInfo = function(doc, activeDoc) { 163 | var docInfo = { 164 | id: $.global.getDocumentID(doc), 165 | name: doc.name, 166 | width: doc.width, 167 | height: doc.height, 168 | colorSpace: doc.documentColorSpace.toString(), 169 | numLayers: doc.layers.length, 170 | numArtboards: doc.artboards.length, 171 | saved: doc.saved, 172 | isActive: doc === activeDoc 173 | }; 174 | 175 | // Add layers information 176 | var layersResult = $.global.getAllLayersInfo(doc); 177 | docInfo.layers = layersResult.layers; 178 | docInfo.totalLayers = layersResult.totalLayers; 179 | 180 | return docInfo; 181 | }; ``` -------------------------------------------------------------------------------- /uxp/ps/main.js: -------------------------------------------------------------------------------- ```javascript 1 | /* MIT License 2 | * 3 | * Copyright (c) 2025 Mike Chambers 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | const { entrypoints, UI } = require("uxp"); 25 | const { 26 | checkRequiresActiveDocument, 27 | parseAndRouteCommand, 28 | } = require("./commands/index.js"); 29 | 30 | const { hasActiveSelection, generateDocumentInfo } = require("./commands/utils.js"); 31 | 32 | const { getLayers } = require("./commands/layers.js").commandHandlers; 33 | 34 | const { io } = require("./socket.io.js"); 35 | //const { act } = require("react"); 36 | const app = require("photoshop").app; 37 | 38 | const APPLICATION = "photoshop"; 39 | const PROXY_URL = "http://localhost:3001"; 40 | 41 | let socket = null; 42 | 43 | const onCommandPacket = async (packet) => { 44 | let command = packet.command; 45 | 46 | let out = { 47 | senderId: packet.senderId, 48 | }; 49 | 50 | try { 51 | //this will throw if an active document is required and not open 52 | checkRequiresActiveDocument(command); 53 | 54 | let response = await parseAndRouteCommand(command); 55 | 56 | out.response = response; 57 | out.status = "SUCCESS"; 58 | 59 | let activeDocument = app.activeDocument 60 | let doc = generateDocumentInfo(activeDocument, activeDocument) 61 | out.document = doc; 62 | 63 | out.layers = await getLayers(); 64 | 65 | out.hasActiveSelection = hasActiveSelection(); 66 | } catch (e) { 67 | out.status = "FAILURE"; 68 | out.message = `Error calling ${command.action} : ${e}`; 69 | } 70 | 71 | return out; 72 | }; 73 | 74 | function connectToServer() { 75 | // Create new Socket.IO connection 76 | socket = io(PROXY_URL, { 77 | transports: ["websocket"], 78 | }); 79 | 80 | socket.on("connect", () => { 81 | updateButton(); 82 | console.log("Connected to server with ID:", socket.id); 83 | socket.emit("register", { application: APPLICATION }); 84 | }); 85 | 86 | socket.on("command_packet", async (packet) => { 87 | console.log("Received command packet:", packet); 88 | 89 | let response = await onCommandPacket(packet); 90 | sendResponsePacket(response); 91 | }); 92 | 93 | socket.on("registration_response", (data) => { 94 | console.log("Received response:", data); 95 | //TODO: connect button here 96 | }); 97 | 98 | socket.on("connect_error", (error) => { 99 | updateButton(); 100 | console.error("Connection error:", error); 101 | }); 102 | 103 | socket.on("disconnect", (reason) => { 104 | updateButton(); 105 | console.log("Disconnected from server. Reason:", reason); 106 | 107 | //TODO:connect button here 108 | }); 109 | 110 | return socket; 111 | } 112 | 113 | function disconnectFromServer() { 114 | if (socket && socket.connected) { 115 | socket.disconnect(); 116 | console.log("Disconnected from server"); 117 | } 118 | } 119 | 120 | function sendResponsePacket(packet) { 121 | if (socket && socket.connected) { 122 | socket.emit("command_packet_response", { 123 | packet: packet, 124 | }); 125 | return true; 126 | } 127 | return false; 128 | } 129 | 130 | function sendCommand(command) { 131 | if (socket && socket.connected) { 132 | socket.emit("app_command", { 133 | application: APPLICATION, 134 | command: command, 135 | }); 136 | return true; 137 | } 138 | return false; 139 | } 140 | 141 | let onInterval = async () => { 142 | let commands = await fetchCommands(); 143 | 144 | await parseAndRouteCommands(commands); 145 | }; 146 | 147 | let fetchCommands = async () => { 148 | try { 149 | let url = `http://127.0.0.1:3030/commands/get/${APPLICATION}/`; 150 | 151 | const fetchOptions = { 152 | method: "GET", 153 | headers: { 154 | Accept: "application/json", 155 | }, 156 | }; 157 | 158 | // Make the fetch request 159 | const response = await fetch(url, fetchOptions); 160 | 161 | // Check if the request was successful 162 | if (!response.ok) { 163 | console.log("a"); 164 | throw new Error(`HTTP error! Status: ${response.status}`); 165 | } 166 | 167 | let r = await response.json(); 168 | 169 | if (r.status != "SUCCESS") { 170 | throw new Error(`API Request error! Status: ${response.message}`); 171 | } 172 | 173 | return r.commands; 174 | } catch (error) { 175 | console.error("Error fetching data:", error); 176 | throw error; // Re-throw to allow caller to handle the error 177 | } 178 | }; 179 | 180 | entrypoints.setup({ 181 | panels: { 182 | vanilla: { 183 | show(node) {}, 184 | }, 185 | }, 186 | }); 187 | 188 | let updateButton = () => { 189 | let b = document.getElementById("btnStart"); 190 | 191 | b.textContent = socket && socket.connected ? "Disconnect" : "Connect"; 192 | }; 193 | 194 | //Toggle button to make it start stop 195 | document.getElementById("btnStart").addEventListener("click", () => { 196 | if (socket && socket.connected) { 197 | disconnectFromServer(); 198 | } else { 199 | connectToServer(); 200 | } 201 | }); 202 | 203 | const CONNECT_ON_LAUNCH = "connectOnLaunch"; 204 | // Save checkbox state in localStorage 205 | document 206 | .getElementById("chkConnectOnLaunch") 207 | .addEventListener("change", function (event) { 208 | window.localStorage.setItem( 209 | CONNECT_ON_LAUNCH, 210 | JSON.stringify(event.target.checked) 211 | ); 212 | }); 213 | 214 | // Retrieve checkbox state 215 | const getConnectOnLaunch = () => { 216 | return JSON.parse(window.localStorage.getItem(CONNECT_ON_LAUNCH)) || false; 217 | }; 218 | 219 | // Set checkbox state on page load 220 | document.addEventListener("DOMContentLoaded", () => { 221 | document.getElementById("chkConnectOnLaunch").checked = 222 | getConnectOnLaunch(); 223 | }); 224 | 225 | window.addEventListener("load", (event) => { 226 | if (getConnectOnLaunch()) { 227 | connectToServer(); 228 | } 229 | }); 230 | ``` -------------------------------------------------------------------------------- /mcp/ps-batch-play.py: -------------------------------------------------------------------------------- ```python 1 | # MIT License 2 | # 3 | # Copyright (c) 2025 Mike Chambers 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from mcp.server.fastmcp import FastMCP, Image 24 | from core import init, sendCommand, createCommand 25 | from fonts import list_all_fonts_postscript 26 | import numpy as np 27 | import base64 28 | import socket_client 29 | import sys 30 | import os 31 | 32 | FONT_LIMIT = 1000 #max number of font names to return to AI 33 | 34 | mcp_name = "Adobe Photoshop Batch Play MCP Server" 35 | mcp = FastMCP(mcp_name, log_level="ERROR") 36 | print(f"{mcp_name} running on stdio", file=sys.stderr) 37 | 38 | APPLICATION = "photoshop" 39 | PROXY_URL = 'http://localhost:3001' 40 | PROXY_TIMEOUT = 20 41 | 42 | socket_client.configure( 43 | app=APPLICATION, 44 | url=PROXY_URL, 45 | timeout=PROXY_TIMEOUT 46 | ) 47 | 48 | init(APPLICATION, socket_client) 49 | 50 | @mcp.tool() 51 | def call_batch_play_command(commands: list): 52 | """ 53 | Executes arbitrary Photoshop batchPlay commands via MCP. 54 | 55 | Args: 56 | commands (str): A raw JSON string representing a list of batchPlay descriptors. 57 | This should be the exact JSON string you would pass to `batchPlay()` in a UXP plugin. 58 | 59 | Returns: 60 | Any: The result returned from Photoshop after executing the batchPlay command(s). 61 | 62 | Example: 63 | >>> commands = ''' 64 | ... [ 65 | ... { 66 | ... "_obj": "exportDocumentAs", 67 | ... "exportAs": { 68 | ... "_obj": "exportAsPNG", 69 | ... "interlaced": false, 70 | ... "transparency": true, 71 | ... "metadata": 1 72 | ... }, 73 | ... "documentID": 1234, 74 | ... "saveFile": { 75 | ... "_path": "/Users/yourname/Downloads/export.png", 76 | ... "_kind": "local" 77 | ... }, 78 | ... "overwrite": true 79 | ... } 80 | ... ] 81 | ... ''' 82 | >>> result = call_batch_play_command(commands) 83 | >>> print(result) 84 | # Output from Photoshop will be returned as-is (usually a list of response descriptors) 85 | """ 86 | 87 | if not commands: 88 | raise ValueError("commands cannot be empty.") 89 | 90 | command = createCommand( 91 | "executeBatchPlayCommand", 92 | { 93 | "commands": commands 94 | } 95 | ) 96 | 97 | return sendCommand(command) 98 | 99 | 100 | @mcp.resource("config://get_instructions") 101 | def get_instructions() -> str: 102 | """Read this first! Returns information and instructions on how to use Photoshop and this API""" 103 | 104 | return f""" 105 | 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. 106 | 107 | Unless otherwise specified, all commands act on the currently active document in Photoshop 108 | 109 | Rules to follow: 110 | 111 | 1. Think deeply about how to solve the task 112 | 2. Always check your work 113 | 3. You can view the current visible photoshop file by calling get_document_image 114 | 4. Pay attention to font size (dont make it too big) 115 | 5. Always use alignment (align_content()) to position your text. 116 | 6. Read the info for the API calls to make sure you understand the requirements and arguments 117 | 7. When you make a selection, clear it once you no longer need it 118 | 119 | Here are some general tips for when working with Photoshop. 120 | 121 | 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. 122 | 123 | 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. 124 | 125 | Suggestions for sizes: 126 | Paragraph text : 8 to 12 pts 127 | Headings : 14 - 20 pts 128 | Single Word Large : 20 to 25pt 129 | 130 | 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. 131 | 132 | As a general rule, you should not flatten files unless asked to do so, or its necessary to apply an effect or look. 133 | 134 | 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. 135 | 136 | Colors are defined via a dict with red, green and blue properties with values between 0 and 255 137 | {{"red":255, "green":0, "blue":0}} 138 | 139 | Bounds is defined as a dict with top, left, bottom and right properties 140 | {{"top": 0, "left": 0, "bottom": 250, "right": 300}} 141 | 142 | Valid options for API calls: 143 | 144 | alignment_modes: {", ".join(alignment_modes)} 145 | 146 | justification_modes: {", ".join(justification_modes)} 147 | 148 | blend_modes: {", ".join(blend_modes)} 149 | 150 | anchor_positions: {", ".join(anchor_positions)} 151 | 152 | interpolation_methods: {", ".join(interpolation_methods)} 153 | 154 | fonts: {", ".join(font_names[:FONT_LIMIT])} 155 | """ 156 | 157 | font_names = list_all_fonts_postscript() 158 | 159 | interpolation_methods = [ 160 | "AUTOMATIC", 161 | "BICUBIC", 162 | "BICUBICSHARPER", 163 | "BICUBICSMOOTHER", 164 | "BILINEAR", 165 | "NEARESTNEIGHBOR" 166 | ] 167 | 168 | anchor_positions = [ 169 | "BOTTOMCENTER", 170 | "BOTTOMLEFT", 171 | "BOTTOMRIGHT", 172 | "MIDDLECENTER", 173 | "MIDDLELEFT", 174 | "MIDDLERIGHT", 175 | "TOPCENTER", 176 | "TOPLEFT", 177 | "TOPRIGHT" 178 | ] 179 | 180 | justification_modes = [ 181 | "CENTER", 182 | "CENTERJUSTIFIED", 183 | "FULLYJUSTIFIED", 184 | "LEFT", 185 | "LEFTJUSTIFIED", 186 | "RIGHT", 187 | "RIGHTJUSTIFIED" 188 | ] 189 | 190 | alignment_modes = [ 191 | "LEFT", 192 | "CENTER_HORIZONTAL", 193 | "RIGHT", 194 | "TOP", 195 | "CENTER_VERTICAL", 196 | "BOTTOM" 197 | ] 198 | 199 | blend_modes = [ 200 | "COLOR", 201 | "COLORBURN", 202 | "COLORDODGE", 203 | "DARKEN", 204 | "DARKERCOLOR", 205 | "DIFFERENCE", 206 | "DISSOLVE", 207 | "DIVIDE", 208 | "EXCLUSION", 209 | "HARDLIGHT", 210 | "HARDMIX", 211 | "HUE", 212 | "LIGHTEN", 213 | "LIGHTERCOLOR", 214 | "LINEARBURN", 215 | "LINEARDODGE", 216 | "LINEARLIGHT", 217 | "LUMINOSITY", 218 | "MULTIPLY", 219 | "NORMAL", 220 | "OVERLAY", 221 | "PASSTHROUGH", 222 | "PINLIGHT", 223 | "SATURATION", 224 | "SCREEN", 225 | "SOFTLIGHT", 226 | "SUBTRACT", 227 | "VIVIDLIGHT" 228 | ] 229 | ``` -------------------------------------------------------------------------------- /uxp/ps/commands/adjustment_layers.js: -------------------------------------------------------------------------------- ```javascript 1 | /* MIT License 2 | * 3 | * Copyright (c) 2025 Mike Chambers 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | const { action } = require("photoshop"); 25 | 26 | const { 27 | selectLayer, 28 | findLayer, 29 | execute 30 | } = require("./utils") 31 | 32 | const addAdjustmentLayerBlackAndWhite = async (command) => { 33 | 34 | let options = command.options; 35 | let layerId = options.layerId; 36 | 37 | let layer = findLayer(layerId); 38 | 39 | if (!layer) { 40 | throw new Error( 41 | `addAdjustmentLayerBlackAndWhite : Could not find layerId : ${layerId}` 42 | ); 43 | } 44 | 45 | let colors = options.colors; 46 | let tintColor = options.tintColor 47 | 48 | await execute(async () => { 49 | selectLayer(layer, true); 50 | 51 | let commands = [ 52 | // Make adjustment layer 53 | { 54 | _obj: "make", 55 | _target: [ 56 | { 57 | _ref: "adjustmentLayer", 58 | }, 59 | ], 60 | using: { 61 | _obj: "adjustmentLayer", 62 | type: { 63 | _obj: "blackAndWhite", 64 | blue: colors.blue, 65 | cyan: colors.cyan, 66 | grain: colors.green, 67 | magenta: colors.magenta, 68 | presetKind: { 69 | _enum: "presetKindType", 70 | _value: "presetKindDefault", 71 | }, 72 | red: colors.red, 73 | tintColor: { 74 | _obj: "RGBColor", 75 | blue: tintColor.blue, 76 | grain: tintColor.green, 77 | red: tintColor.red, 78 | }, 79 | useTint: options.tint, 80 | yellow: colors.yellow, 81 | }, 82 | }, 83 | }, 84 | ]; 85 | 86 | await action.batchPlay(commands, {}); 87 | }); 88 | }; 89 | 90 | const addBrightnessContrastAdjustmentLayer = async (command) => { 91 | 92 | let options = command.options; 93 | let layerId = options.layerId; 94 | 95 | let layer = findLayer(layerId); 96 | 97 | if (!layer) { 98 | throw new Error( 99 | `addBrightnessContrastAdjustmentLayer : Could not find layerId : ${layerId}` 100 | ); 101 | } 102 | 103 | await execute(async () => { 104 | selectLayer(layer, true); 105 | 106 | let commands = [ 107 | // Make adjustment layer 108 | { 109 | _obj: "make", 110 | _target: [ 111 | { 112 | _ref: "adjustmentLayer", 113 | }, 114 | ], 115 | using: { 116 | _obj: "adjustmentLayer", 117 | type: { 118 | _obj: "brightnessEvent", 119 | useLegacy: false, 120 | }, 121 | }, 122 | }, 123 | // Set current adjustment layer 124 | { 125 | _obj: "set", 126 | _target: [ 127 | { 128 | _enum: "ordinal", 129 | _ref: "adjustmentLayer", 130 | _value: "targetEnum", 131 | }, 132 | ], 133 | to: { 134 | _obj: "brightnessEvent", 135 | brightness: options.brightness, 136 | center: options.contrast, 137 | useLegacy: false, 138 | }, 139 | }, 140 | ]; 141 | 142 | await action.batchPlay(commands, {}); 143 | }); 144 | }; 145 | 146 | const addAdjustmentLayerVibrance = async (command) => { 147 | 148 | let options = command.options; 149 | let layerId = options.layerId; 150 | 151 | let layer = findLayer(layerId); 152 | 153 | if (!layer) { 154 | throw new Error( 155 | `addAdjustmentLayerVibrance : Could not find layerId : ${layerId}` 156 | ); 157 | } 158 | 159 | let colors = options.colors; 160 | 161 | await execute(async () => { 162 | selectLayer(layer, true); 163 | 164 | let commands = [ 165 | // Make adjustment layer 166 | { 167 | _obj: "make", 168 | _target: [ 169 | { 170 | _ref: "adjustmentLayer", 171 | }, 172 | ], 173 | using: { 174 | _obj: "adjustmentLayer", 175 | type: { 176 | _class: "vibrance", 177 | }, 178 | }, 179 | }, 180 | // Set current adjustment layer 181 | { 182 | _obj: "set", 183 | _target: [ 184 | { 185 | _enum: "ordinal", 186 | _ref: "adjustmentLayer", 187 | _value: "targetEnum", 188 | }, 189 | ], 190 | to: { 191 | _obj: "vibrance", 192 | saturation: options.saturation, 193 | vibrance: options.vibrance, 194 | }, 195 | }, 196 | ]; 197 | 198 | await action.batchPlay(commands, {}); 199 | }); 200 | }; 201 | 202 | const addColorBalanceAdjustmentLayer = async (command) => { 203 | 204 | let options = command.options; 205 | 206 | let layerId = options.layerId; 207 | let layer = findLayer(layerId); 208 | 209 | if (!layer) { 210 | throw new Error( 211 | `addColorBalanceAdjustmentLayer : Could not find layer named : [${layerId}]` 212 | ); 213 | } 214 | 215 | await execute(async () => { 216 | let commands = [ 217 | // Make adjustment layer 218 | { 219 | _obj: "make", 220 | _target: [ 221 | { 222 | _ref: "adjustmentLayer", 223 | }, 224 | ], 225 | using: { 226 | _obj: "adjustmentLayer", 227 | type: { 228 | _obj: "colorBalance", 229 | highlightLevels: [0, 0, 0], 230 | midtoneLevels: [0, 0, 0], 231 | preserveLuminosity: true, 232 | shadowLevels: [0, 0, 0], 233 | }, 234 | }, 235 | }, 236 | // Set current adjustment layer 237 | { 238 | _obj: "set", 239 | _target: [ 240 | { 241 | _enum: "ordinal", 242 | _ref: "adjustmentLayer", 243 | _value: "targetEnum", 244 | }, 245 | ], 246 | to: { 247 | _obj: "colorBalance", 248 | highlightLevels: options.highlights, 249 | midtoneLevels: options.midtones, 250 | shadowLevels: options.shadows, 251 | }, 252 | }, 253 | ]; 254 | await action.batchPlay(commands, {}); 255 | }); 256 | }; 257 | 258 | const commandHandlers = { 259 | addAdjustmentLayerBlackAndWhite, 260 | addBrightnessContrastAdjustmentLayer, 261 | addAdjustmentLayerVibrance, 262 | addColorBalanceAdjustmentLayer 263 | } 264 | 265 | module.exports = { 266 | commandHandlers 267 | }; ```