#
tokens: 48320/50000 55/71 files (page 1/6)
lines: on (toggle) GitHub
raw markdown copy reset
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: MIT](https://img.shields.io/badge/License-MIT-orange.svg)](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>&nbsp;</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>&nbsp;</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>&nbsp;</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 | };
```
Page 1/6FirstPrevNextLast