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 | };
```