#
tokens: 49689/50000 62/71 files (page 1/5)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 5. Use http://codebase.md/mikechambers/adb-mcp?page={x} to view the full context.

# Directory Structure

```
├── .gitattributes
├── .gitignore
├── adb-proxy-socket
│   ├── .gitignore
│   ├── package-lock.json
│   ├── package.json
│   ├── proxy.js
│   └── README.md
├── cep
│   ├── com.mikechambers.ae
│   │   ├── .debug
│   │   ├── commands.js
│   │   ├── CSXS
│   │   │   └── manifest.xml
│   │   ├── index.html
│   │   ├── jsx
│   │   │   └── json-polyfill.jsx
│   │   ├── lib
│   │   │   └── CSInterface.js
│   │   ├── main.js
│   │   └── style.css
│   └── com.mikechambers.ai
│       ├── .debug
│       ├── commands.js
│       ├── CSXS
│       │   └── manifest.xml
│       ├── index.html
│       ├── jsx
│       │   ├── json-polyfill.jsx
│       │   └── utils.jsx
│       ├── lib
│       │   └── CSInterface.js
│       ├── main.js
│       └── style.css
├── dxt
│   ├── build
│   ├── pr
│   │   └── manifest.json
│   └── ps
│       └── manifest.json
├── images
│   └── claud-attach-mcp.png
├── LICENSE.md
├── mcp
│   ├── .gitignore
│   ├── ae-mcp.py
│   ├── ai-mcp.py
│   ├── core.py
│   ├── fonts.py
│   ├── id-mcp.py
│   ├── logger.py
│   ├── pr-mcp.py
│   ├── ps-batch-play.py
│   ├── ps-mcp.py
│   ├── pyproject.toml
│   ├── requirements.txt
│   ├── socket_client.py
│   └── uv.lock
├── package-lock.json
├── README.md
└── uxp
    ├── id
    │   ├── commands
    │   │   └── index.js
    │   ├── icons
    │   │   ├── [email protected]
    │   │   ├── [email protected]
    │   │   ├── [email protected]
    │   │   └── [email protected]
    │   ├── index.html
    │   ├── LICENSE
    │   ├── main.js
    │   ├── manifest.json
    │   ├── package.json
    │   ├── socket.io.js
    │   └── style.css
    ├── pr
    │   ├── commands
    │   │   ├── consts.js
    │   │   ├── core.js
    │   │   ├── index.js
    │   │   └── utils.js
    │   ├── icons
    │   │   ├── [email protected]
    │   │   ├── [email protected]
    │   │   ├── [email protected]
    │   │   └── [email protected]
    │   ├── index.html
    │   ├── LICENSE
    │   ├── main.js
    │   ├── manifest.json
    │   ├── package.json
    │   ├── socket.io.js
    │   └── style.css
    └── ps
        ├── commands
        │   ├── adjustment_layers.js
        │   ├── core.js
        │   ├── filters.js
        │   ├── index.js
        │   ├── layer_styles.js
        │   ├── layers.js
        │   ├── selection.js
        │   └── utils.js
        ├── icons
        │   ├── [email protected]
        │   ├── [email protected]
        │   ├── [email protected]
        │   └── [email protected]
        ├── index.html
        ├── LICENSE
        ├── main.js
        ├── manifest.json
        ├── package.json
        ├── socket.io.js
        └── style.css
```

# Files

--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------

```
# Auto detect text files and perform LF normalization
* text=auto

```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ae/.debug:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<ExtensionList>
    <Extension Id="com.mikechambers.ae.mcp">
        <HostList>
            <Host Name="AEFT" Port="8088"/>
        </Host>
    </Extension>
</ExtensionList>
```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ai/.debug:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<ExtensionList>
    <Extension Id="com.mikechambers.ae.mcp">
        <HostList>
            <Host Name="ILST" Port="8089"/>
        </Host>
    </Extension>
</ExtensionList>
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
*.dxt
*.ccx

dist/

.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon


# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

```

--------------------------------------------------------------------------------
/adb-proxy-socket/.gitignore:
--------------------------------------------------------------------------------

```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# vitepress build output
**/.vitepress/dist

# vitepress cache directory
**/.vitepress/cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

```

--------------------------------------------------------------------------------
/mcp/.gitignore:
--------------------------------------------------------------------------------

```
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

```

--------------------------------------------------------------------------------
/adb-proxy-socket/README.md:
--------------------------------------------------------------------------------

```markdown
### Package

In order to package executables:

```
npm install -g pkg
pkg .
```
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# adb-mcp

adb-mcp is a proof of concept project to enabled AI control of Adobe tools (Adobe Photoshop and Adobe Premiere) by providing an interface to LLMs via the MCP protocol.

The project is not endorsed by nor supported by Adobe.

It has been tested with Claude desktop (Mac and Windows) from Anthropic, as well as the OpenAI Agent SDK, and allows AI clients to control Adobe Photoshop and Adobe Premiere. Theoretically, it should work with any AI App / LLM that supports the MCP protocol, and is built in a way to support multiple Adobe applications.

Example use cases include:

-   Giving Claude step by step instruction on what to do in Photoshop, providing a conversational based interface (particularly useful if you are new to Photoshop).
-   Giving Claude a task (create an instagram post that looks like a Polariod image, create a double exposure) and letting it create it from start to finish to use as a template.
-   Asking Claude to generate custom Photoshop tutorials for you, by creating an example file, then step by step instructions on how to recreate.
-   As a Photoshop utility tool (have Claude rename all of your layers into a consistent format)
-   Have Claude create new Premiere projects pre-populations with clips, transitions, effects and Audio

[View Video Examples](https://www.youtube.com/playlist?list=PLrZcuHfRluqt5JQiKzMWefUb0Xumb7MkI)

The Premiere agent is a bit more limited in functionality compared to the Photoshop agent, due to current limitations of the Premiere plugin API.

## How it works

The proof of concept works by providing:

-   A MCP Server that provides an interface to functionality within Adobe Photoshop to the AI / LLM
-   A Node based command proxy server that sits between the MCP server and Adobe app plugins
-   An Adobe app (Photoshop and Premiere) plugin that listens for commands, and drives the programs

**AI** <-> **MCP Server** <-> **Command Proxy Server** <-> **Photoshop / Premiere UXP Plugin** <-> **Photoshop / Premiere**

The proxy server is required because the public facing API for UXP Based JavaScript plugin does not allow it to listen on a socket connection (as a server) for the MCP Server to connect to (it can only connect to a socket as a client).

## Requirements

In order to run this, the following is required:

-   AI LLM with support for MCP Protocol (tested with Claude desktop on Mac & Windows, and OpenAI Agent SDK)
-   Python 3, which is used to run the MCP server provided with this project
-   NodeJS, used to provide a proxy between the MCP server and Photoshop
-   Adobe UXP Developer tool (available via Creative Cloud) used to install and debug the Photoshop / Premiere plugin used to connect to the proxy
-   Adobe Photoshop (26.0 or greater) with the MCP Plugin installed or Adobe Premiere Beta (25.3 Build 46 or greater)


## Installation

This guide assumes you're using Claude Desktop. Other MCP-compatible AI applications should work similarly.


### Download Source Code
Clone or download the source code from the [main project page](https://github.com/mikechambers/adb-mcp).

### Install Claude Desktop
1. Download and install [Claude Desktop](https://claude.ai/download)
2. Launch Claude Desktop to verify it works

Note, you can use any client / code that supports MCP, just follow its instructions for how to configure.

### Install MCP for Development
Navigate to the project directory and run:

#### Photoshop
```bash
uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with numpy ps-mcp.py
```

#### Premiere Pro
```bash
uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with pillow pr-mcp.py
```

#### InDesign
```bash
uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with pillow id-mcp.py
```

#### AfterEffects
```bash
uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with pillow ae-mcp.py
```

#### Illustrator
```bash
uv run mcp install --with fonttools --with python-socketio --with mcp --with requests --with websocket-client --with pillow ai-mcp.py
```

Restart Claude Desktop after installation.

### Set Up Proxy Server

#### Using Prebuilt Executables (Recommended)

1. Download the appropriate executable for your platform from the latest [release](https://github.com/mikechambers/adb-mcp/releases) (files named like `adb-proxy-socket-macos-x64.zip` (Intel), `adb-proxy-socket-macos-arm64.zip` (Silicon), or `adb-proxy-socket-win-x64.exe.zip`).
2. Unzip the executable.
3. Double click or run from the console / terminal

#### Running from Source

1. Navigate to the adb-proxy-socket directory
2. Run `node proxy.js`

You should see a message like:  
   `Photoshop MCP Command proxy server running on ws://localhost:3001`

**Keep this running** — the proxy server must stay active for Claude to communicate with Adobe plugins.

### Install Plugins

#### Photoshop, Premiere Pro, InDesign (UXP)

1. Launch **UXP Developer Tools** from Creative Cloud
2. Enable developer mode when prompted
3. Select **File > Add Plugin**
4. Navigate to the appropriate directory and select **manifest.json**:
   - **Photoshop**: `uxp/ps/manifest.json`
   - **Premiere Pro**: `uxp/pr/manifest.json`
   - **InDesign**: `uxp/id/manifest.json`
5. Click **Load**
6. In your Adobe application, open the plugin panel and click **Connect**

##### Enable Developer Mode in Photoshop

**For Photoshop:**
1. Launch Photoshop (2025/26.0 or greater)
2. Go to **Settings > Plugins** and check **"Enable Developer Mode"**
3. Restart Photoshop

#### AfterEffects, Illustrator (CEP)

##### Mac
1. Make sure the following directory exists (if it doesn't then create the directories)
   `/Users/USERNAME/Library/Application Support/Adobe/CEP/extensions`

2. Navigate to the extensions directory and create a symlink that points to the AfterEffect / Illustrator plugin in the CEP directory.
```bash
cd /Users/USERNAME/Library/Application Support/Adobe/CEP/extensions
ln -s /Users/USERNAME/src/adb-mcp/cep/com.mikechambers.ae com.mikechambers.ae
```
or
```bash
cd /Users/USERNAME/Library/Application Support/Adobe/CEP/extensions
ln -s /Users/USERNAME/src/adb-mcp/cep/com.mikechambers.ai com.mikechambers.ai
```

##### Windows
1. Make sure the following directory exists (if it doesn't then create the directories)
   `C:\Users\USERNAME\AppData\Roaming\Adobe\CEP\extensions`

2. Open Command Prompt as Administrator (or enable Developer Mode in Windows Settings)

3. Create a junction or symbolic link that points to the AfterEffect / Illustrator plugin in the CEP directory:
```cmd
mklink /D "C:\Users\USERNAME\AppData\Roaming\Adobe\CEP\extensions\com.mikechambers.ae" "C:\Users\USERNAME\src\adb-mcp\cep\com.mikechambers.ae"
```
or
```cmd
mklink /D "C:\Users\USERNAME\AppData\Roaming\Adobe\CEP\extensions\com.mikechambers.ai" "C:\Users\USERNAME\src\adb-mcp\cep\com.mikechambers.ai"
```

Note if you don't want to symlink, you can copy com.mikechambers.ae / com.mikechambers.ao into the CEP directory.

### Using Claude with Adobe Apps

Launch the following:

1. Claude Desktop
2. adb-proxy-socket node server
3. Launch Photoshop, Premiere, InDesign, AfterEffects, Illustrator

_TIP: Create a project for Photoshop / Premiere Pro in Claude and pre-load any app specific instructions in its Project knowledge._

#### Photoshop
1. Launch UXP Developer Tool and click the Load button for _Photoshop MCP Agent_
2. In Photoshop, if the MCP Agent panel is not open, open _Plugins > Photoshop MCP Agent > Photoshop MCP Agent_
3. Click connect in the agent panel in Photoshop

Now you can switch over the Claude desktop. Before you start a session, you should load the instructions resource which will provide guidance and info the Claude by clicking the socket icon (Attach from MCP) and then _Choose an Integration_ > _Adobe Photoshop_ > _config://get_instructions_.



#### Premiere
1. Launch UXP Developer Tool and click the Load button for _Premiere MCP Agent_
2. In Premiere, if the MCP Agent panel is not open, open _Window > UXP Plugins > Premiere MCP Agent > Premiere MCP Agent_
3. Click connect in the agent panel in Photoshop

#### InDesign
1. Launch UXP Developer Tool and click the Load button for InDesitn MCP Agent_
2. In InDesign, if the MCP Agent panel is not open, open _Plugins > InDesign MCP Agent > InDesign MCP Agent_
3. Click connect in the agent panel in Photoshop

#### AfterEffects
1. _Window > Extensions > Illustrator MCP Agent_

#### Illustrator

1. Open a file (the plugin won't launch unless a file is open)
2. _Window > Extensions > Illustrator MCP Agent_


Note, you must reload the plugin via the UXP Developer app every time you restart Photoshop, Premiere and InDesign.

### Setting up session

In the chat input field, click the "+" button. From there click "Add from Adobe Photoshop / Premiere" then select *config://get_instructions*. This will load the instructions into the prompt. Submit that to Claude and once it processes it, you are ready to go.

<img src="images/claud-attach-mcp.png" width="300">

This will help reduce errors when the AI is using the app.


### Prompting

At anytime, you can ask the following:

```
Can you list what apis / functions are available for working with Photoshop / Premiere?
```

and it will list out all of the functionality available.

When prompting, you do not need to reference the APIs, just use natural language to give instructions.

For example:

```
Create a new Photoshop file with a blue background, that is 1080 width by 720 height at 300 dpi
```

```
Create a new Photoshop file for an instagram post
```

```
Create a double exposure image in Photoshop of a woman and a forest
```

```
Generate an image of a forest, and then add a clipping mask to only show the center in a circle
```
```
Make something cool with photoshop
```

```
Add cross fade transitions between all of the clips on the timeline in Premiere
```


### Tips

#### General
* When asking AI to view the content in Photoshop / Premiere Pro, you can see the image returned in the Tool Call item in the chat. It will appear once the entire response has been added to the chat.
* When prompting, ask the AI to think about and check its work.
* The more you guide it (i.e. "consider using clipping masks") the better the results
* The more advanced the model, or the more resources given to the model the better and more creative the AI is.
* As a general rule, don't make changes in the Adobe Apps while the AI is doing work. If you do make changes, make sure to tell the AI about it.
* The AI will learn from its mistakes, but will lose its memory once you start a new chat. You can guide it to do things in a different way, and then ask it to start over and it should follow the new approach.

The AI currently has access to a subset of Photoshop / Premiere / InDesign / Illustrator / AfterEffects functionality. In general, the approach has been to provide lower level tools to give the AI the basics to do more complex stuff.

Note, for AfterEffects and Illustrator, there is a low level Extend Script API that will let the LLM run any arbitrary extend script (which allows it to do just about anything).

The Photoshop plugin has more functionality that Premiere.

By default, the AI cannot access files directly, although if you install the [Claude File System MCP server](https://www.claudemcp.com/servers/filesystem) it can access, and load files into Photoshop / Premiere (open files and embed images).

#### Photoshop

* You can ask the AI to look at the content of the Photoshop file and it should be able to then see the output.
* The AI currently has issue sizing and positioning text correctly, so giving it guidelines on font sizes to use will help, as well as telling it to align the text relative to the canvas.
* The AI has access to all of the Postscript fonts on the system. If you want to specify a font, you must use its Postscript name (you may be able to ask the AI for it).
* You can ask the AI for suggestions. It comes up with really useful ideas / feedback sometimes.

#### Premiere

* Currently the plugin assumes you are just working with a single sequence.
* Pair the Premiere Pro MCP with the [media-utils-mcp](https://github.com/mikechambers/media-utils-mcp) to expand functionality.


### Troubleshooting

#### MCP won't run in Claude

If you get an error when running Claude that the MCP is not working, you may need to edit your Claude config file and put an absolute path for the UV command. More info [here](https://github.com/mikechambers/adb-mcp/issues/5#issuecomment-2829817624).

#### All fonts not available

The MCP server will return a list of available fonts, but depending on the number of fonts installed on your system, may omit some to work around the amount of data that can be send to the AI. By default it will list the first 1000 fonts sorted in alphabetical order.

You can tell the AI to use a specific font, using its postscript name.

#### Plugin won't install or connect

*   Make sure the app is running before you try to load the plugin.
*   In the UXP developer tool click the debug button next to load, and see if there are any errors.
*   Make sure the node / proxy server is running. If you plugin connects you should see output similar to:

```
adb-mcp Command proxy server running on ws://localhost:3001
User connected: Ud6L4CjMWGAeofYAAAAB
Client Ud6L4CjMWGAeofYAAAAB registered for application: photoshop
```

*   When you press the connect button, if it still says "Connect" it means there was either an error, or it can't connect to the proxy server. You can view the error in the UXP Developer App, by opening the Developer Workspace and click "Debug".

#### Errors within AI client

* If something fails on the AI side, it will usually tell you the issue. If you click the command / code box, you can see the error.
* The first thing to check if there is an issue is to make sure the plugin in Photoshop / Premiere is connected, and that the node proxy server is running.
* If response times get really slow, check if the AI servers are under load, and that you do not have too much text in the current conversation (restarting a new chat can sometimes help speed up, but you will lose the context).

If you continue to have issues post an [issue](https://github.com/mikechambers/adb-mcp/issuesrd.gg/fgxw9t37D7). Include as much information as you can (OS, App, App version, and debug info or errors).

## Development

Adding new functionality is relatively easy, and requires:

1. Adding the API and parameters in the *mcp/ps-mcp.py* / *mcp/pr-mcp.py* file (which is used by the AI)
2. Implementing the API in the *uxp/ps/commands/index.js* / *uxp/pr/commands/index.js* file.

This [thread](https://github.com/mikechambers/adb-mcp/issues/10#issuecomment-3191698528) has some info on how to add functionality.

## Questions, Feature Requests, Feedback

If you have any questions, feature requests, need help, or just want to chat, join the [discord](https://discord.gg/fgxw9t37D7).

You can also log bugs and feature requests on the [issues page](https://github.com/mikechambers/adb-mcp/issues).

## License

Project released under a [MIT License](LICENSE.md).

[![License: MIT](https://img.shields.io/badge/License-MIT-orange.svg)](LICENSE.md)



```

--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------

```markdown
MIT License

Copyright (c) 2025 Mike Chambers

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

```

--------------------------------------------------------------------------------
/mcp/requirements.txt:
--------------------------------------------------------------------------------

```
fonttools
python-socketio
mcp
requests
websocket-client
```

--------------------------------------------------------------------------------
/uxp/id/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "uxp-template-ps-starter",
  "version": "1.0.0",
  "description": "Adobe InDesign MCP Agent Plugin.",
  "author": "Mike Chambers ([email protected])",
  "license": "MIT"
}

```

--------------------------------------------------------------------------------
/uxp/pr/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "uxp-template-ps-starter",
  "version": "1.0.0",
  "description": "Adobe Photoshop MCP Agent Plugin.",
  "author": "Mike Chambers ([email protected])",
  "license": "MIT"
}

```

--------------------------------------------------------------------------------
/uxp/ps/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "uxp-template-ps-starter",
  "version": "1.0.0",
  "description": "Adobe Photoshop MCP Agent Plugin.",
  "author": "Mike Chambers ([email protected])",
  "license": "MIT"
}

```

--------------------------------------------------------------------------------
/uxp/id/style.css:
--------------------------------------------------------------------------------

```css
body {
  color: white;
  padding: 16px;
  font-family: Arial, sans-serif;
}

li:before {
  content: '• ';
  width: 3em;
}

#layers {
  border: 1px solid #808080;
  border-radius: 4px;
  padding: 16px;
}

footer {
  position: fixed;
  bottom: 16px;
  left: 16px;
}
```

--------------------------------------------------------------------------------
/uxp/pr/style.css:
--------------------------------------------------------------------------------

```css
body {
  color: white;
  padding: 16px;
  font-family: Arial, sans-serif;
}

li:before {
  content: '• ';
  width: 3em;
}

#layers {
  border: 1px solid #808080;
  border-radius: 4px;
  padding: 16px;
}

footer {
  position: fixed;
  bottom: 16px;
  left: 16px;
}
```

--------------------------------------------------------------------------------
/uxp/ps/style.css:
--------------------------------------------------------------------------------

```css
body {
  color: white;
  padding: 16px;
  font-family: Arial, sans-serif;
}

li:before {
  content: '• ';
  width: 3em;
}

#layers {
  border: 1px solid #808080;
  border-radius: 4px;
  padding: 16px;
}

footer {
  position: fixed;
  bottom: 16px;
  left: 16px;
}
```

--------------------------------------------------------------------------------
/uxp/id/index.html:
--------------------------------------------------------------------------------

```html
<!DOCTYPE html>
<html>

<head>
  <script src="main.js"></script>
  <link rel="stylesheet" href="style.css">
</head>

<body>



  <div>
    <sp-button id="btnStart">Connect</sp-button>
  </div>
  <p>&nbsp;</p>
  <div>
    <sp-checkbox id="chkConnectOnLaunch">Connect on Launch</sp-checkbox>
  </div>
  <footer>
    <div>
      <div>Created by Mike Chambers</div>
      <div>https://github.com/mikechambers/adb-mcp</div>
    </div>
  </footer>
</body>

</html>
```

--------------------------------------------------------------------------------
/uxp/pr/index.html:
--------------------------------------------------------------------------------

```html
<!DOCTYPE html>
<html>

<head>
  <script src="main.js"></script>
  <link rel="stylesheet" href="style.css">
</head>

<body>



  <div>
    <sp-button id="btnStart">Connect</sp-button>
  </div>
  <p>&nbsp;</p>
  <div>
    <sp-checkbox id="chkConnectOnLaunch">Connect on Launch</sp-checkbox>
  </div>
  <footer>
    <div>
      <div>Created by Mike Chambers</div>
      <div>https://github.com/mikechambers/adb-mcp</div>
    </div>
  </footer>
</body>

</html>
```

--------------------------------------------------------------------------------
/uxp/ps/index.html:
--------------------------------------------------------------------------------

```html
<!DOCTYPE html>
<html>

<head>
  <script src="main.js"></script>
  <link rel="stylesheet" href="style.css">
</head>

<body>



  <div>
    <sp-button id="btnStart">Connect</sp-button>
  </div>
  <p>&nbsp;</p>
  <div>
    <sp-checkbox id="chkConnectOnLaunch">Connect on Launch</sp-checkbox>
  </div>
  <footer>
    <div>
      <div>Created by Mike Chambers</div>
      <div>https://github.com/mikechambers/adb-mcp</div>
    </div>
  </footer>
</body>

</html>
```

--------------------------------------------------------------------------------
/adb-proxy-socket/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "adb-proxy-socket",
  "version": "0.85.1",
  "description": "Proxy socket.io node server for Adobe MCP plugins",
  "main": "proxy.js",
  "bin": "proxy.js",
  "scripts": {
    "build": "pkg ."
  },
  "author": "Mike Chambers",
  "license": "ISC",
  "dependencies": {
    "express": "^4.21.2",
    "socket.io": "^4.8.1",
    "socket.io-client": "^4.8.1"
  },
  "pkg": {
    "targets": [
      "node18-macos-x64",
      "node18-macos-arm64",
      "node18-win-x64"
    ],
    "outputPath": "dist"
  }
}

```

--------------------------------------------------------------------------------
/mcp/core.py:
--------------------------------------------------------------------------------

```python
import logger

application = None
socket_client = None

def init(app, socket):
    global application, socket_client
    application = app
    socket_client = socket


def createCommand(action:str, options:dict) -> str:
    command = {
        "application":application,
        "action":action,
        "options":options
    }

    return command

def sendCommand(command:dict):

    response = socket_client.send_message_blocking(command)
    
    logger.log(f"Final response: {response['status']}")
    return response
```

--------------------------------------------------------------------------------
/uxp/pr/commands/consts.js:
--------------------------------------------------------------------------------

```javascript
const TRACK_TYPE = {
  "VIDEO":"VIDEO",
  "AUDIO":"AUDIO"
}

TICKS_PER_SECOND = 254016000000;

const BLEND_MODES = {
    COLOR: 0,
    COLORBURN: 1,
    COLORDODGE: 2,
    DARKEN: 3,
    DARKERCOLOR: 4,
    DIFFERENCE: 5,
    DISSOLVE: 6,
    EXCLUSION: 7,
    HARDLIGHT: 8,
    HARDMIX: 9,
    HUE: 10,
    LIGHTEN: 11,
    LIGHTERCOLOR: 12,
    LINEARBURN: 13,
    LINEARDODGE: 14,
    LINEARLIGHT: 15,
    LUMINOSITY: 16,
    MULTIPLY: 17,
    NORMAL: 18,
    OVERLAY: 19,
    PINLIGHT: 20,
    SATURATION: 21,
    SCREEN: 22,
    SOFTLIGHT: 23,
    VIVIDLIGHT: 24,
    SUBTRACT: 25,
    DIVIDE: 26
  };

  module.exports = {
    BLEND_MODES,
    TRACK_TYPE,
    TICKS_PER_SECOND
};
```

--------------------------------------------------------------------------------
/mcp/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "psmcp"
version = "0.85.1"
description = "Adobe Photoshop automation using MCP"
requires-python = ">=3.10"
license = "MIT"
authors = [
    {name = "Mike Chambers", email = "[email protected]"}
]
dependencies = [
    "fonttools",
    "python-socketio",
    "mcp[cli]",
    "requests",
    "websocket-client>=1.8.0",
    "pillow>=11.2.1",
    "numpy>=2.2.6",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "black",
    "isort",
    "mypy",
]

[tool.setuptools]
py-modules = ["fonts", "logger", "psmcp", "socket_client"]

[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'

[tool.isort]
profile = "black"
line_length = 88

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

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"

```

--------------------------------------------------------------------------------
/mcp/logger.py:
--------------------------------------------------------------------------------

```python
# MIT License
#
# Copyright (c) 2025 Mike Chambers
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import sys

def log(message, filter_tag="LOGGER"):

    print(f"{filter_tag} : {message}", file=sys.stderr)


```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ai/index.html:
--------------------------------------------------------------------------------

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Illustrator MCP Agent</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">

        <div class="status-group">
            <div class="status-dot" id="statusDot"></div>
            <span id="statusText">Disconnected</span>
        </div>

        <button id="btnConnect">Connect</button>

        <div class="divider"></div>

        <div class="checkbox-group">
            <input type="checkbox" id="chkConnectOnLaunch">
            <label for="chkConnectOnLaunch">Connect automatically on launch</label>
        </div>

        <div class="divider"></div>

        <div class="log-section">
            <label>Message Log</label>
            <textarea id="messageLog" readonly></textarea>
        </div>
    </div>

    <!-- Load CEP's CSInterface library -->
    <script type="text/javascript" src="./lib/CSInterface.js"></script>
    
    <!-- Load Socket.IO client from CDN -->
    <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
    
    <!-- Load commands -->
    <script src="commands.js"></script>
    
    <!-- Main script -->
    <script src="main.js"></script>
</body>
</html>
```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ae/index.html:
--------------------------------------------------------------------------------

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>AfterEffects MCP Agent</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">

        <div class="status-group">
            <div class="status-dot" id="statusDot"></div>
            <span id="statusText">Disconnected</span>
        </div>

        <button id="btnConnect">Connect</button>

        <div class="divider"></div>

        <div class="checkbox-group">
            <input type="checkbox" id="chkConnectOnLaunch">
            <label for="chkConnectOnLaunch">Connect automatically on launch</label>
        </div>

        <div class="divider"></div>

        <div class="log-section">
            <label>Message Log</label>
            <textarea id="messageLog" readonly></textarea>
        </div>
    </div>

    <!-- Load CEP's CSInterface library -->
    <script type="text/javascript" src="./lib/CSInterface.js"></script>
    
    <!-- Load Socket.IO client from CDN -->
    <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
    
    <!-- Load commands -->
    <script src="commands.js"></script>
    
    <!-- Main script -->
    <script src="main.js"></script>
</body>
</html>
```

--------------------------------------------------------------------------------
/dxt/ps/manifest.json:
--------------------------------------------------------------------------------

```json
{
  "dxt_version": "0.1",
  "name": "adb-mcp-photoshop",
  "display_name": "Adobe Photoshop MCP",
  "version": "0.85.3",
  "description": "Proof of concept project to create AI Agent for Adobe Photshop by providing an interface to LLMs via the MCP protocol.",
  "long_description": "Proof of concept project to create AI Agent for Adobe Photoshop by providing an interface to LLMs via the MCP protocol.",
  "author": {
    "name": "Mike Chambers",
    "email": "[email protected]",
    "url": "https://www.mikechambers.com"
  },
  "homepage": "https://github.com/mikechambers/adb-mcp",
  "documentation": "https://github.com/mikechambers/adb-mcp",
  "support": "https://github.com/mikechambers/adb-mcp/issues",
  "server": {
    "type": "python",
    "entry_point": "main.py",
    "mcp_config": {
      "command": "uv",
      "args": [
        "run",
        "--with",
        "fonttools",
        "--with",
        "mcp",
        "--with",
        "mcp[cli]",
        "--with",
        "python-socketio",
        "--with",
        "requests",
        "--with",
        "numpy",
        "--with",
        "websocket-client",
        "mcp",
        "run",
        "${__dirname}/ps-mcp.py"
      ]     
    }
  },


  "keywords": [
    "adobe",
    "premierepro",
    "video"
  ],
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/mikechambers/adb-mcp"
  }
}

```

--------------------------------------------------------------------------------
/dxt/pr/manifest.json:
--------------------------------------------------------------------------------

```json
{
  "dxt_version": "0.1",
  "name": "adb-mcp-premiere",
  "display_name": "Adobe Premiere Pro MCP",
  "version": "0.85.3",
  "description": "Proof of concept project to create AI Agent for Adobe Premiere Pro by providing an interface to LLMs via the MCP protocol.",
  "long_description": "Proof of concept project to create AI Agent for Adobe Premiere Pro by providing an interface to LLMs via the MCP protocol.",
  "author": {
    "name": "Mike Chambers",
    "email": "[email protected]",
    "url": "https://www.mikechambers.com"
  },
  "homepage": "https://github.com/mikechambers/adb-mcp",
  "documentation": "https://github.com/mikechambers/adb-mcp",
  "support": "https://github.com/mikechambers/adb-mcp/issues",
  "server": {
    "type": "python",
    "entry_point": "main.py",
    "mcp_config": {
      "command": "uv",
      "args": [
        "run",
        "--with",
        "fonttools",
        "--with",
        "mcp",
        "--with",
        "mcp[cli]",
        "--with",
        "python-socketio",
        "--with",
        "requests",
        "--with",
        "websocket-client",
        "--with",
        "pillow",
        "mcp",
        "run",
        "${__dirname}/pr-mcp.py"
      ]      
    }
  },

  "keywords": [
    "adobe",
    "photoshop",
    "images"
  ],
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/mikechambers/adb-mcp"
  }
}

```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ae/jsx/json-polyfill.jsx:
--------------------------------------------------------------------------------

```javascript
// JSON polyfill for ExtendScript
// Minimal implementation for serializing simple objects and arrays

if (typeof JSON === 'undefined') {
    JSON = {};
}

if (typeof JSON.stringify === 'undefined') {
    JSON.stringify = function(obj) {
        var type = typeof obj;
        
        // Handle primitives
        if (obj === null) return 'null';
        if (obj === undefined) return 'undefined';
        if (type === 'number') {
            if (isNaN(obj)) return 'null';  // JSON spec: NaN becomes null
            if (!isFinite(obj)) return 'null';  // JSON spec: Infinity becomes null
            return String(obj);
        }
        if (type === 'boolean') return String(obj);
        if (type === 'string') {
            // Escape special characters
            var escaped = obj.replace(/\\/g, '\\\\')
                             .replace(/"/g, '\\"')
                             .replace(/\n/g, '\\n')
                             .replace(/\r/g, '\\r')
                             .replace(/\t/g, '\\t');
            return '"' + escaped + '"';
        }
        
        // Handle arrays
        if (obj instanceof Array) {
            var items = [];
            for (var i = 0; i < obj.length; i++) {
                items.push(JSON.stringify(obj[i]));
            }
            return '[' + items.join(',') + ']';
        }
        
        // Handle objects
        if (type === 'object') {
            var pairs = [];
            for (var key in obj) {
                if (obj.hasOwnProperty(key)) {
                    pairs.push(JSON.stringify(key) + ':' + JSON.stringify(obj[key]));
                }
            }
            return '{' + pairs.join(',') + '}';
        }
        
        // Fallback
        return '{}';
    };
}
```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ai/jsx/json-polyfill.jsx:
--------------------------------------------------------------------------------

```javascript
// JSON polyfill for ExtendScript
// Minimal implementation for serializing simple objects and arrays

if (typeof JSON === 'undefined') {
    JSON = {};
}

if (typeof JSON.stringify === 'undefined') {
    JSON.stringify = function(obj) {
        var type = typeof obj;
        
        // Handle primitives
        if (obj === null) return 'null';
        if (obj === undefined) return 'undefined';
        if (type === 'number') {
            if (isNaN(obj)) return 'null';  // JSON spec: NaN becomes null
            if (!isFinite(obj)) return 'null';  // JSON spec: Infinity becomes null
            return String(obj);
        }
        if (type === 'boolean') return String(obj);
        if (type === 'string') {
            // Escape special characters
            var escaped = obj.replace(/\\/g, '\\\\')
                             .replace(/"/g, '\\"')
                             .replace(/\n/g, '\\n')
                             .replace(/\r/g, '\\r')
                             .replace(/\t/g, '\\t');
            return '"' + escaped + '"';
        }
        
        // Handle arrays
        if (obj instanceof Array) {
            var items = [];
            for (var i = 0; i < obj.length; i++) {
                items.push(JSON.stringify(obj[i]));
            }
            return '[' + items.join(',') + ']';
        }
        
        // Handle objects
        if (type === 'object') {
            var pairs = [];
            for (var key in obj) {
                if (obj.hasOwnProperty(key)) {
                    pairs.push(JSON.stringify(key) + ':' + JSON.stringify(obj[key]));
                }
            }
            return '{' + pairs.join(',') + '}';
        }
        
        // Fallback
        return '{}';
    };
}
```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ae/CSXS/manifest.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<ExtensionManifest Version="7.0" ExtensionBundleId="com.mikechambers.ae.mcp" ExtensionBundleVersion="1.0.0"
  ExtensionBundleName="AfterEffects MCP Agent" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  
  <Author>Mike Chambers</Author>
  <Contact>[email protected]</Contact>
  <Legal>MIT License</Legal>
  <Abstract>AfterEffects MCP Agent</Abstract>
  
  <ExtensionList>
    <Extension Id="com.mikechambers.ae.mcp" Version="1.0.0"/>
  </ExtensionList>
  
  <ExecutionEnvironment>
    <HostList>
      <Host Name="AEFT" Version="[25.0,99.9]"/>
    </HostList>
    <LocaleList>
      <Locale Code="All"/>
    </LocaleList>
    <RequiredRuntimeList>
      <RequiredRuntime Name="CSXS" Version="12.0"/>
    </RequiredRuntimeList>
  </ExecutionEnvironment>
  
  <DispatchInfoList>
    <Extension Id="com.mikechambers.ae.mcp">
      <DispatchInfo>
        <Resources>
          <MainPath>./index.html</MainPath>
          <CEFCommandLine>
            <Parameter>--enable-nodejs</Parameter>
            <Parameter>--mixed-context</Parameter>
          </CEFCommandLine>
        </Resources>
        <Lifecycle>
          <AutoVisible>true</AutoVisible>
        </Lifecycle>
        <UI>
          <Type>Panel</Type>
          <Menu>AfterEffects MCP Agent</Menu>
          <Geometry>
            <Size>
              <Height>400</Height>
              <Width>350</Width>
            </Size>
            <MinSize>
              <Height>300</Height>
              <Width>300</Width>
            </MinSize>
            <MaxSize>
              <Height>600</Height>
              <Width>500</Width>
            </MaxSize>
          </Geometry>
        </UI>
      </DispatchInfo>
    </Extension>
  </DispatchInfoList>
</ExtensionManifest>
```

--------------------------------------------------------------------------------
/uxp/ps/manifest.json:
--------------------------------------------------------------------------------

```json
{
  "id": "Photoshop MCP Agent",
  "name": "Photoshop MCP Agent",
  "version": "0.85.3",
  "main": "index.html",
  "host": [
    {
      "app": "PS",
      "minVersion": "26.0.0"
    }
  ],
  "manifestVersion": 5,
  "entrypoints": [
    {
      "type": "panel",
      "id": "vanilla",
      "minimumSize": {
        "width": 300,
        "height": 200
      },
      "maximumSize": {
        "width": 300,
        "height": 200
      },
      "preferredDockedSize": {
        "width": 300,
        "height": 200
      },
      "preferredFloatingSize": {
        "width": 300,
        "height": 200
      },
      "icons": [
        {
          "width": 32,
          "height": 32,
          "path": "icons/icon_D.png",
          "scale": [
            1,
            2
          ],
          "theme": [
            "dark",
            "darkest"
          ],
          "species": [
            "generic"
          ]
        },
        {
          "width": 32,
          "height": 32,
          "path": "icons/icon_N.png",
          "scale": [
            1,
            2
          ],
          "theme": [
            "lightest",
            "light"
          ],
          "species": [
            "generic"
          ]
        }
      ],
      "label": {
        "default": "Photoshop MCP Agent"
      }
    }
  ],
  "requiredPermissions": {
    "network": {
      "domains": "all"
    },
    "localFileSystem": "fullAccess"
  },
  "icons": [
    {
      "width": 23,
      "height": 23,
      "path": "icons/dark.png",
      "scale": [
        1,
        2
      ],
      "theme": [
        "darkest",
        "dark",
        "medium"
      ]
    },
    {
      "width": 23,
      "height": 23,
      "path": "icons/light.png",
      "scale": [
        1,
        2
      ],
      "theme": [
        "lightest",
        "light"
      ]
    }
  ]
}
```

--------------------------------------------------------------------------------
/uxp/pr/manifest.json:
--------------------------------------------------------------------------------

```json
{
  "id": "Premiere MCP Agent",
  "name": "Premiere MCP Agent",
  "version": "0.85.3",
  "main": "index.html",
  "host": [
    {
      "app": "premierepro",
      "minVersion": "25.3.0"
    }
  ],
  "manifestVersion": 5,
  "entrypoints": [
    {
      "type": "panel",
      "id": "vanilla",
      "minimumSize": {
        "width": 300,
        "height": 200
      },
      "maximumSize": {
        "width": 300,
        "height": 200
      },
      "preferredDockedSize": {
        "width": 300,
        "height": 200
      },
      "preferredFloatingSize": {
        "width": 300,
        "height": 200
      },
      "icons": [
        {
          "width": 32,
          "height": 32,
          "path": "icons/icon_D.png",
          "scale": [
            1,
            2
          ],
          "theme": [
            "dark",
            "darkest"
          ],
          "species": [
            "generic"
          ]
        },
        {
          "width": 32,
          "height": 32,
          "path": "icons/icon_N.png",
          "scale": [
            1,
            2
          ],
          "theme": [
            "lightest",
            "light"
          ],
          "species": [
            "generic"
          ]
        }
      ],
      "label": {
        "default": "Premiere MCP Agent"
      }
    }
  ],
  "requiredPermissions": {
    "network": {
      "domains": "all"
    },
    "localFileSystem": "fullAccess"
  },
  "icons": [
    {
      "width": 23,
      "height": 23,
      "path": "icons/dark.png",
      "scale": [
        1,
        2
      ],
      "theme": [
        "darkest",
        "dark",
        "medium"
      ]
    },
    {
      "width": 23,
      "height": 23,
      "path": "icons/light.png",
      "scale": [
        1,
        2
      ],
      "theme": [
        "lightest",
        "light"
      ]
    }
  ]
}
```

--------------------------------------------------------------------------------
/uxp/id/manifest.json:
--------------------------------------------------------------------------------

```json
{
  "id": "InDesign MCP Agent",
  "name": "InDesign MCP Agent",
  "version": "0.85.0",
  "main": "index.html",
  "host": [
    {
      "app": "ID",
      "minVersion": "20.2.0"
    }
  ],
  "manifestVersion": 5,
  "entrypoints": [
    {
      "type": "panel",
      "id": "vanilla",
      "minimumSize": {
        "width": 300,
        "height": 200
      },
      "maximumSize": {
        "width": 300,
        "height": 200
      },
      "preferredDockedSize": {
        "width": 300,
        "height": 200
      },
      "preferredFloatingSize": {
        "width": 300,
        "height": 200
      },
      "icons": [
        {
          "width": 32,
          "height": 32,
          "path": "icons/icon_D.png",
          "scale": [
            1,
            2
          ],
          "theme": [
            "dark",
            "darkest"
          ],
          "species": [
            "generic"
          ]
        },
        {
          "width": 32,
          "height": 32,
          "path": "icons/icon_N.png",
          "scale": [
            1,
            2
          ],
          "theme": [
            "lightest",
            "light"
          ],
          "species": [
            "generic"
          ]
        }
      ],
      "label": {
        "default": "InDesign MCP Agent"
      }
    }
  ],
  "requiredPermissions": {
    "network": {
      "domains": [
        "all",
        "http://localhost:3001"
      ]
    },
    "localFileSystem": "fullAccess"
  },
  "icons": [
    {
      "width": 23,
      "height": 23,
      "path": "icons/dark.png",
      "scale": [
        1,
        2
      ],
      "theme": [
        "darkest",
        "dark",
        "medium"
      ]
    },
    {
      "width": 23,
      "height": 23,
      "path": "icons/light.png",
      "scale": [
        1,
        2
      ],
      "theme": [
        "lightest",
        "light"
      ]
    }
  ]
}
```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ai/CSXS/manifest.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<ExtensionManifest Version="7.0" ExtensionBundleId="com.mikechambers.ai.mcp" ExtensionBundleVersion="1.0.0"
  ExtensionBundleName="Illustrator MCP Agent" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  
  <Author>Mike Chambers</Author>
  <Contact>[email protected]</Contact>
  <Legal>MIT License</Legal>
  <Abstract>Illustrator MCP Agent</Abstract>
  
  <ExtensionList>
    <Extension Id="com.mikechambers.ai.mcp" Version="1.0.0"/>
  </ExtensionList>
  
  <ExecutionEnvironment>
    <HostList>
      <Host Name="ILST" Version="[28.0,99.9]"/>
    </HostList>
    <LocaleList>
      <Locale Code="All"/>
    </LocaleList>
    <RequiredRuntimeList>
      <RequiredRuntime Name="CSXS" Version="12.0"/>
    </RequiredRuntimeList>
  </ExecutionEnvironment>
  
  <DispatchInfoList>
    <Extension Id="com.mikechambers.ai.mcp">
      <DispatchInfo>
        <Resources>
          <MainPath>./index.html</MainPath>
          <CEFCommandLine>
            <Parameter>--enable-nodejs</Parameter>
            <Parameter>--mixed-context</Parameter>
            <Parameter>--remote-debugging-port=8088</Parameter>
            <Parameter>--allow-file-access-from-files</Parameter>
          </CEFCommandLine>
        </Resources>
        <Lifecycle>
          <AutoVisible>true</AutoVisible>
        </Lifecycle>
        <UI>
          <Type>Panel</Type>
          <Menu>Illustrator MCP Agent</Menu>
          <Geometry>
            <Size>
              <Height>400</Height>
              <Width>350</Width>
            </Size>
            <MinSize>
              <Height>300</Height>
              <Width>300</Width>
            </MinSize>
            <MaxSize>
              <Height>600</Height>
              <Width>500</Width>
            </MaxSize>
          </Geometry>
        </UI>
      </DispatchInfo>
    </Extension>
  </DispatchInfoList>
</ExtensionManifest>
```

--------------------------------------------------------------------------------
/uxp/ps/commands/filters.js:
--------------------------------------------------------------------------------

```javascript
/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const { app } = require("photoshop");  // For app references

const {
    findLayer,
    execute
} = require("./utils");  // For the utility functions used in your code

const applyMotionBlur = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `applyMotionBlur : Could not find layerId : ${layerId}`
        );
    }

    await execute(async () => {
        await layer.applyMotionBlur(options.angle, options.distance);
    });
};

const applyGaussianBlur = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `applyGaussianBlur : Could not find layerId : ${layerId}`
        );
    }

    await execute(async () => {
        await layer.applyGaussianBlur(options.radius);
    });
};

const commandHandlers = {
    applyMotionBlur,
    applyGaussianBlur,
};

module.exports = {
    commandHandlers
};
```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ae/style.css:
--------------------------------------------------------------------------------

```css
/* style.css */
body {
    margin: 0;
    padding: 16px;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background-color: #2a2a2a;
    color: #e0e0e0;
    font-size: 13px;
}

.container {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.form-group {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

label {
    font-size: 12px;
    font-weight: 500;
    color: #b0b0b0;
}

input[type="text"] {
    padding: 8px;
    border: 1px solid #444;
    border-radius: 4px;
    background-color: #1a1a1a;
    color: #e0e0e0;
    font-size: 13px;
}

input[type="text"]:focus {
    outline: none;
    border-color: #0d6efd;
}

.status-group {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px;
    background-color: #1a1a1a;
    border-radius: 4px;
}

.status-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background-color: #dc3545;
    transition: background-color 0.3s;
}

.status-dot.connected {
    background-color: #28a745;
}

#statusText {
    font-size: 13px;
    font-weight: 500;
}

button {
    padding: 10px 16px;
    border: none;
    border-radius: 4px;
    background-color: #0d6efd;
    color: white;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: background-color 0.2s;
}

button:hover {
    background-color: #0b5ed7;
}

button:active {
    background-color: #0a58ca;
}

.checkbox-group {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 0;
}

input[type="checkbox"] {
    width: 16px;
    height: 16px;
    cursor: pointer;
}

.checkbox-group label {
    font-size: 13px;
    color: #e0e0e0;
    cursor: pointer;
    font-weight: normal;
}

.divider {
    height: 1px;
    background-color: #444;
    margin: 8px 0;
}

.log-section {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

#messageLog {
    width: 100%;
    height: 120px;
    padding: 8px;
    border: 1px solid #444;
    border-radius: 4px;
    background-color: #1a1a1a;
    color: #e0e0e0;
    font-family: 'Courier New', monospace;
    font-size: 11px;
    resize: vertical;
    line-height: 1.4;
}

#messageLog:focus {
    outline: none;
    border-color: #0d6efd;
}
```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ai/style.css:
--------------------------------------------------------------------------------

```css
/* style.css */
body {
    margin: 0;
    padding: 16px;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background-color: #2a2a2a;
    color: #e0e0e0;
    font-size: 13px;
}

.container {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.form-group {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

label {
    font-size: 12px;
    font-weight: 500;
    color: #b0b0b0;
}

input[type="text"] {
    padding: 8px;
    border: 1px solid #444;
    border-radius: 4px;
    background-color: #1a1a1a;
    color: #e0e0e0;
    font-size: 13px;
}

input[type="text"]:focus {
    outline: none;
    border-color: #0d6efd;
}

.status-group {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px;
    background-color: #1a1a1a;
    border-radius: 4px;
}

.status-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background-color: #dc3545;
    transition: background-color 0.3s;
}

.status-dot.connected {
    background-color: #28a745;
}

#statusText {
    font-size: 13px;
    font-weight: 500;
}

button {
    padding: 10px 16px;
    border: none;
    border-radius: 4px;
    background-color: #0d6efd;
    color: white;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: background-color 0.2s;
}

button:hover {
    background-color: #0b5ed7;
}

button:active {
    background-color: #0a58ca;
}

.checkbox-group {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 0;
}

input[type="checkbox"] {
    width: 16px;
    height: 16px;
    cursor: pointer;
}

.checkbox-group label {
    font-size: 13px;
    color: #e0e0e0;
    cursor: pointer;
    font-weight: normal;
}

.divider {
    height: 1px;
    background-color: #444;
    margin: 8px 0;
}

.log-section {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

#messageLog {
    width: 100%;
    height: 120px;
    padding: 8px;
    border: 1px solid #444;
    border-radius: 4px;
    background-color: #1a1a1a;
    color: #e0e0e0;
    font-family: 'Courier New', monospace;
    font-size: 11px;
    resize: vertical;
    line-height: 1.4;
}

#messageLog:focus {
    outline: none;
    border-color: #0d6efd;
}
```

--------------------------------------------------------------------------------
/uxp/ps/commands/index.js:
--------------------------------------------------------------------------------

```javascript
/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const { app } = require("photoshop");
const fs = require("uxp").storage.localFileSystem;

const adjustmentLayers = require("./adjustment_layers");
const core = require("./core");
const layerStyles = require("./layer_styles")
const filters = require("./filters")
const selection = require("./selection")
const layers = require("./layers")

const parseAndRouteCommands = async (commands) => {
    if (!commands.length) {
        return;
    }

    for (let c of commands) {
        await parseAndRouteCommand(c);
    }
};

const parseAndRouteCommand = async (command) => {
    let action = command.action;

    let f = commandHandlers[action];

    if (typeof f !== "function") {
        throw new Error(`Unknown Command: ${action}`);
    }

    console.log(f.name)
    return f(command);
};

const checkRequiresActiveDocument = (command) => {
    if (!requiresActiveDocument(command)) {
        return;
    }

    if (!app.activeDocument) {
        throw new Error(
            `${command.action} : Requires an open Photoshop document`
        );
    }
};

const requiresActiveDocument = (command) => {
    return !["createDocument", "openFile"].includes(command.action);
};

const commandHandlers = {
    ...selection.commandHandlers,
    ...filters.commandHandlers,
    ...core.commandHandlers,
    ...adjustmentLayers.commandHandlers,
    ...layerStyles.commandHandlers,
    ...layers.commandHandlers
};

module.exports = {
    requiresActiveDocument,
    checkRequiresActiveDocument,
    parseAndRouteCommands,
    parseAndRouteCommand,
};

```

--------------------------------------------------------------------------------
/uxp/pr/commands/index.js:
--------------------------------------------------------------------------------

```javascript
/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const app = require("premierepro");
const core = require("./core");

const getProjectInfo = async () => {
    let project = await app.Project.getActiveProject()

    const name = project.name;
    const path = project.path;
    const id = project.guid.toString();

    const items = await getProjectContentInfo()

    return {
        name,
        path,
        id,
        items
    }

}
/*
const getProjectContentInfo2 = async () => {
    let project = await app.Project.getActiveProject()

    let root = await project.getRootItem()
    let items = await root.getItems()
    
    let out = []
    for(const item of items) {
        console.log(item)

        const b = app.FolderItem.cast(item)
        
        const isBin = b != undefined

        //todo: it would be good to get more data / info here
        out.push({name:item.name})
    }

    return out
}
    */

const getProjectContentInfo = async () => {
    let project = await app.Project.getActiveProject()
    let root = await project.getRootItem()
    
    const processItems = async (parentItem) => {
        let items = await parentItem.getItems()
        let out = []
        
        for(const item of items) {
            console.log(item)
            
            const folderItem = app.FolderItem.cast(item)
            const isBin = folderItem != undefined
            
            let itemData = {
                name: item.name,
                type: isBin ? 'bin' : 'projectItem'
            }
            
            // If it's a bin/folder, recursively get its contents
            if (isBin) {
                itemData.items = await processItems(folderItem)
            }
            
            out.push(itemData)
        }
        
        return out
    }
    
    return await processItems(root)
}

const parseAndRouteCommand = async (command) => {
    let action = command.action;

    let f = commandHandlers[action];

    if (typeof f !== "function") {
        throw new Error(`Unknown Command: ${action}`);
    }

    console.log(f.name)
    return f(command);
};



const checkRequiresActiveProject = async (command) => {
    if (!requiresActiveProject(command)) {
        return;
    }

    let project = await app.Project.getActiveProject()
    if (!project) {
        throw new Error(
            `${command.action} : Requires an open Premiere Project`
        );
    }
};

const requiresActiveProject = (command) => {
    return !["createProject", "openProject"].includes(command.action);
};

const commandHandlers = {
    ...core.commandHandlers
};

module.exports = {
    getProjectInfo,
    checkRequiresActiveProject,
    parseAndRouteCommand
};

```

--------------------------------------------------------------------------------
/mcp/ae-mcp.py:
--------------------------------------------------------------------------------

```python
# MIT License
#
# Copyright (c) 2025 Mike Chambers
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from mcp.server.fastmcp import FastMCP
from core import init, sendCommand, createCommand
import socket_client
import sys

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

APPLICATION = "aftereffects"
PROXY_URL = 'http://localhost:3001'
PROXY_TIMEOUT = 20

socket_client.configure(
    app=APPLICATION, 
    url=PROXY_URL,
    timeout=PROXY_TIMEOUT
)

init(APPLICATION, socket_client)

@mcp.tool()
def execute_extend_script(script_string: str):
    """
    Executes arbitrary ExtendScript code in AfterEffects and returns the result.

    The script should use 'return' to send data back. The result will be automatically
    JSON stringified. If the script throws an error, it will be caught and returned
    as an error object.

    Args:
        script_string (str): The ExtendScript code to execute. Must use 'return' to 
                           send results back.

    Returns:
        any: The result returned from the ExtendScript, or an error object containing:
            - error (str): Error message
            - line (str): Line number where error occurred

    Example:
        script = '''
            var doc = app.activeDocument;
            return {
                name: doc.name,
                path: doc.fullName.fsName,
                layers: doc.layers.length
            };
        '''
        result = execute_extend_script(script)
    """
    command = createCommand("executeExtendScript", {
        "scriptString": script_string
    })
    return sendCommand(command)

@mcp.resource("config://get_instructions")
def get_instructions() -> str:
    """Read this first! Returns information and instructions on how to use AfterEffects and this API"""

    return f"""
    You are an Adobe AfterEffects expert who is practical, clear, and great at teaching.

    Rules to follow:

    1. Think deeply about how to solve the task.
    2. Always check your work before responding.
    3. Read the API call info to understand required arguments and return shapes.
    4. Before manipulating anything, ensure a document is open and active.
    """



# AfterEffectsd Blend Modes (for future use)
BLEND_MODES = [
    "ADD",
    "ALPHA_ADD",
    "CLASSIC_COLOR_BURN",
    "CLASSIC_COLOR_DODGE",
    "CLASSIC_DIFFERENCE",
    "COLOR",
    "COLOR_BURN",
    "COLOR_DODGE",
    "DANCING_DISSOLVE",
    "DARKEN",
    "DARKER_COLOR",
    "DIFFERENCE",
    "DISSOLVE",
    "EXCLUSION",
    "HARD_LIGHT",
    "HARD_MIX",
    "HUE",
    "LIGHTEN",
    "LIGHTER_COLOR",
    "LINEAR_BURN",
    "LINEAR_DODGE",
    "LINEAR_LIGHT",
    "LUMINESCENT_PREMUL",
    "LUMINOSITY",
    "MULTIPLY",
    "NORMAL",
    "OVERLAY",
    "PIN_LIGHT",
    "SATURATION",
    "SCREEN",
    "SILHOUETE_ALPHA",
    "SILHOUETTE_LUMA",
    "SOFT_LIGHT",
    "STENCIL_ALPHA",
    "STENCIL_LUMA",
    "SUBTRACT",
    "VIVID_LIGHT"
]
```

--------------------------------------------------------------------------------
/uxp/id/commands/index.js:
--------------------------------------------------------------------------------

```javascript
/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

//const fs = require("uxp").storage.localFileSystem;
//const openfs = require('fs')
const {app, DocumentIntentOptions} = require("indesign");


const createDocument = async (command) => {
    console.log("createDocument")

    const options = command.options

    let documents = app.documents
    let margins = options.margins

    let unit = getUnitForIntent(DocumentIntentOptions.WEB_INTENT)

    app.marginPreferences.bottom = `${margins.bottom}${unit}`
    app.marginPreferences.top = `${margins.top}${unit}`
    app.marginPreferences.left = `${margins.left}${unit}`
    app.marginPreferences.right = `${margins.right}${unit}`

    app.marginPreferences.columnCount = options.columns.count
    app.marginPreferences.columnGutter = `${options.columns.gutter}${unit}`
    

    let documentPreferences = {
        pageWidth: `${options.pageWidth}${unit}`,
        pageHeight: `${options.pageHeight}${unit}`,
        pagesPerDocument: options.pagesPerDocument,
        facingPages: options.facingPages,
        intent: DocumentIntentOptions.WEB_INTENT
    }

    const showingWindow = true
    //Boolean showingWindow, DocumentPreset documentPreset, Object withProperties 
    documents.add({showingWindow, documentPreferences})
}


const getUnitForIntent = (intent) => {

    if(intent && intent.toString() === DocumentIntentOptions.WEB_INTENT.toString()) {
        return "px"
    }

    throw new Error(`getUnitForIntent : unknown intent [${intent}]`)
}

const parseAndRouteCommand = async (command) => {
    let action = command.action;

    let f = commandHandlers[action];

    if (typeof f !== "function") {
        throw new Error(`Unknown Command: ${action}`);
    }
    
    console.log(f.name)
    return f(command);
};


const commandHandlers = {
    createDocument
};


const getActiveDocumentSettings = (command) => {
    const document = app.activeDocument


    const d = document.documentPreferences
    const documentPreferences = {
        pageWidth:d.pageWidth,
        pageHeight:d.pageHeight,
        pagesPerDocument:d.pagesPerDocument,
        facingPages:d.facingPages,
        measurementUnit:getUnitForIntent(d.intent)
    }

    const marginPreferences = {
        top:document.marginPreferences.top,
        bottom:document.marginPreferences.bottom,
        left:document.marginPreferences.left,
        right:document.marginPreferences.right,
        columnCount : document.marginPreferences.columnCount,
        columnGutter : document.marginPreferences.columnGutter
    }
    return {documentPreferences, marginPreferences}
}

const checkRequiresActiveDocument = async (command) => {
    if (!requiresActiveProject(command)) {
        return;
    }

    let document = app.activeDocument
    if (!document) {
        throw new Error(
            `${command.action} : Requires an open InDesign document`
        );
    }
};

const requiresActiveDocument = (command) => {
    return !["createDocument"].includes(command.action);
};


module.exports = {
    getActiveDocumentSettings,
    checkRequiresActiveDocument,
    parseAndRouteCommand
};

```

--------------------------------------------------------------------------------
/mcp/id-mcp.py:
--------------------------------------------------------------------------------

```python
# MIT License
#
# Copyright (c) 2025 Mike Chambers
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from mcp.server.fastmcp import FastMCP
from core import init, sendCommand, createCommand
import socket_client
import sys

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


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

APPLICATION = "indesign"
PROXY_URL = 'http://localhost:3001'
PROXY_TIMEOUT = 20

socket_client.configure(
    app=APPLICATION, 
    url=PROXY_URL,
    timeout=PROXY_TIMEOUT
)

init(APPLICATION, socket_client)

@mcp.tool()
def create_document(
   width: int, 
   height: int, 
   pages: int = 0,
   pages_facing: bool = False,
   columns: dict = {"count": 1, "gutter": 12},
   margins: dict = {"top": 36, "bottom": 36, "left": 36, "right": 36}
):
   """
   Creates a new InDesign document with specified dimensions and layout settings.
   
   Args:
       width (int): Document width in points (1 point = 1/72 inch)
       height (int): Document height in points
       pages (int, optional): Number of pages in the document. Defaults to 0.
       pages_facing (bool, optional): Whether to create facing pages (spread layout). 
           Defaults to False.
       columns (dict, optional): Column layout configuration with keys:
           - count (int): Number of columns per page
           - gutter (int): Space between columns in points
           Defaults to {"count": 1, "gutter": 12}.
       margins (dict, optional): Page margin settings in points with keys:
           - top (int): Top margin
           - bottom (int): Bottom margin  
           - left (int): Left margin
           - right (int): Right margin
           Defaults to {"top": 36, "bottom": 36, "left": 36, "right": 36}.
   
   Returns:
       dict: Result of the command execution from the InDesign UXP plugin
   """
   command = createCommand("createDocument", {
       "intent": "WEB_INTENT",
       "pageWidth": width,
       "pageHeight": height,
       "margins": margins,
       "columns": columns,
       "pagesPerDocument": pages,
       "pagesFacing": pages_facing
   })
   
   return sendCommand(command)

@mcp.resource("config://get_instructions")
def get_instructions() -> str:
    """Read this first! Returns information and instructions on how to use Photoshop and this API"""

    return f"""
    You are an InDesign and design expert who is creative and loves to help other people learn to use InDesign and create.

    Rules to follow:

    1. Think deeply about how to solve the task
    2. Always check your work
    3. Read the info for the API calls to make sure you understand the requirements and arguments
    """


"""
BLEND_MODES = [
    "COLOR",
    "COLORBURN",
    "COLORDODGE",
    "DARKEN",
    "DARKERCOLOR",
    "DIFFERENCE",
    "DISSOLVE",
    "EXCLUSION",
    "HARDLIGHT",
    "HARDMIX",
    "HUE",
    "LIGHTEN",
    "LIGHTERCOLOR",
    "LINEARBURN",
    "LINEARDODGE",
    "LINEARLIGHT",
    "LUMINOSITY",
    "MULTIPLY",
    "NORMAL",
    "OVERLAY",
    "PINLIGHT",
    "SATURATION",
    "SCREEN",
    "SOFTLIGHT",
    "VIVIDLIGHT",
    "SUBTRACT",
    "DIVIDE"
]
"""
```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ae/main.js:
--------------------------------------------------------------------------------

```javascript
/* Socket.IO Plugin for After Effects (CEP)
 * Main JavaScript file
 */

const csInterface = new CSInterface();
const APPLICATION = "aftereffects";
const PROXY_URL = "http://localhost:3001";


let socket = null;

// Log function
function log(message) {
    const logArea = document.getElementById('messageLog');
    const timestamp = new Date().toLocaleTimeString();
    logArea.value += `[${timestamp}] ${message}\n`;
    logArea.scrollTop = logArea.scrollHeight;
}

// Update UI status
function updateStatus(connected) {
    const statusDot = document.getElementById('statusDot');
    const statusText = document.getElementById('statusText');
    const btnConnect = document.getElementById('btnConnect');

    if (connected) {
        statusDot.classList.add('connected');
        statusText.textContent = 'Connected';
        btnConnect.textContent = 'Disconnect';
    } else {
        statusDot.classList.remove('connected');
        statusText.textContent = 'Disconnected';
        btnConnect.textContent = 'Connect';
    }
}

// Handle incoming command packets
async function onCommandPacket(packet) {
    log(`Received command: ${packet.command.action}`);

    let out = {
        senderId: packet.senderId,
    };

    try {
        // Execute the command in After Effects (from commands.js)
        //const response = await executeCommand(packet.command);
        const response = await parseAndRouteCommand(packet.command);
        
        out.response = response;
        out.status = "SUCCESS";
        
        // Get project info
        out.projectInfo = await getProjectInfo();
        
    } catch (e) {
        out.status = "FAILURE";
        out.message = `Error calling ${packet.command.action}: ${e.message}`;
        log(`Error: ${e.message}`);
    }

    return out;
}

// Connect to Socket.IO server
function connectToServer() {
    
    log(`Connecting to ${PROXY_URL}...`);
    
    socket = io(PROXY_URL, {
        transports: ["websocket", "polling"],
    });

    socket.on("connect", () => {
        updateStatus(true);
        log(`Connected with ID: ${socket.id}`);
        socket.emit("register", { application: APPLICATION });
    });

    socket.on("command_packet", async (packet) => {
        log(`Received command packet`);
        const response = await onCommandPacket(packet);
        sendResponsePacket(response);
    });

    socket.on("registration_response", (data) => {
        log(`Registration confirmed: ${data.message || 'OK'}`);
    });

    socket.on("connect_error", (error) => {
        updateStatus(false);
        log(`Connection error: ${error.message}`);
    });

    socket.on("disconnect", (reason) => {
        updateStatus(false);
        log(`Disconnected: ${reason}`);
    });
}

// Disconnect from server
function disconnectFromServer() {
    if (socket && socket.connected) {
        socket.disconnect();
        log('Disconnected from server');
    }
}

// Send response packet
function sendResponsePacket(packet) {
    if (socket && socket.connected) {
        socket.emit("command_packet_response", { packet });
        log('Response sent');
        log(packet)
        return true;
    }
    return false;
}

// LocalStorage helpers
const CONNECT_ON_LAUNCH = "connectOnLaunch";

function saveSettings() {
    localStorage.setItem(CONNECT_ON_LAUNCH, 
        document.getElementById('chkConnectOnLaunch').checked);
}

function loadSettings() {
    const connectOnLaunch = localStorage.getItem(CONNECT_ON_LAUNCH) === 'true';
    
    document.getElementById('chkConnectOnLaunch').checked = connectOnLaunch;
    
    return connectOnLaunch;
}

// Event Listeners
document.getElementById('btnConnect').addEventListener('click', () => {
    if (socket && socket.connected) {
        disconnectFromServer();
    } else {
        connectToServer();
    }
    saveSettings();
});

document.getElementById('chkConnectOnLaunch').addEventListener('change', saveSettings);


function initializeExtension() {
    const csInterface = new CSInterface();
    const extensionPath = csInterface.getSystemPath(SystemPath.EXTENSION);
    const polyfillPath = extensionPath + '/jsx/json-polyfill.jsx';
    
    csInterface.evalScript(`$.evalFile("${polyfillPath}")`, function(result) {
        console.log('JSON polyfill loaded');
    });
}

// Initialize on load
window.addEventListener('load', () => {
    initializeExtension()
    const connectOnLaunch = loadSettings();
    log('Plugin loaded');
    
    if (connectOnLaunch) {
        connectToServer();
    }
});

```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ae/commands.js:
--------------------------------------------------------------------------------

```javascript
/* commands.js
 * After Effects command handlers
 */

// Execute After Effects command via ExtendScript
function executeAECommand(script) {
    return new Promise((resolve, reject) => {
        const csInterface = new CSInterface();
        csInterface.evalScript(script, (result) => {
            if (result === 'EvalScript error.') {
                reject(new Error('ExtendScript execution failed'));
            } else {
                try {
                    resolve(JSON.parse(result));
                } catch (e) {
                    resolve(result);
                }
            }
        });
    });
}

// Get project information
async function getProjectInfo() {
    const script = `
        (function() {
            var info = {
                numItems: app.project.numItems,
                activeItemIndex: app.project.activeItem ? app.project.activeItem.id : null,
                projectName: app.project.file ? app.project.file.name : "Untitled"
            };
            return JSON.stringify(info);
        })();
    `;
    return await executeAECommand(script);
}

// Get all compositions
async function getCompositions() {
    const script = `
        (function() {
            var comps = [];
            for (var i = 1; i <= app.project.numItems; i++) {
                var item = app.project.item(i);
                if (item instanceof CompItem) {
                    comps.push({
                        id: item.id,
                        name: item.name,
                        width: item.width,
                        height: item.height,
                        duration: item.duration,
                        frameRate: item.frameRate
                    });
                }
            }
            return JSON.stringify(comps);
        })();
    `;
    return await executeAECommand(script);
}

async function executeExtendScript(command) {
    console.log(command)
    const options = command.options
    const scriptString = options.scriptString;

    const script = `
        (function() {
            try {
                var result = (function() {
                    ${scriptString}
                })();
                
                // If result is undefined, return null
                if (result === undefined) {
                    return 'null';
                }
                
                // Return stringified result
                return JSON.stringify(result);
            } catch(e) {
                return JSON.stringify({
                    error: e.toString(),
                    line: e.line || 'unknown'
                });
            }
        })();
    `;
    
    const result = await executeAECommand(script);
    
    return createPacket(result);
}


async function getLayers() {
    const script = `
        var comp = app.project.activeItem;
        if (!comp || !(comp instanceof CompItem)) {
            JSON.stringify({error: "No active composition"});
        } else {
            var layers = [];
            for (var i = 1; i <= comp.numLayers; i++) {
                var layer = comp.layer(i);
                layers.push({
                    index: layer.index,
                    name: layer.name,
                    enabled: layer.enabled,
                    selected: layer.selected,
                    startTime: layer.startTime,
                    inPoint: layer.inPoint,
                    outPoint: layer.outPoint
                });
            }
            JSON.stringify(layers);
        }
    `;
    
    const result = await executeAECommand(script);
    

    return createPacket(result);
    /*return {
        content: [{
            type: "text",
            text: JSON.stringify(result, null, 2)
        }]
    };*/
}

const createPacket = (result) => {
    return {
        content: [{
            type: "text",
            text: JSON.stringify(result, null, 2)
        }]
    };
}

const parseAndRouteCommand = async (command) => {
    let action = command.action;

    let f = commandHandlers[action];

    if (typeof f !== "function") {
        throw new Error(`Unknown Command: ${action}`);
    }

    console.log(f.name)
    return await f(command);
};


// Execute commands
/*
async function executeCommand(command) {
    switch(command.action) {

        case "getLayers":
            return await getLayers();
        
        case "executeExtendScript":
            return await executeExtendScript(command);
        
        default:
            throw new Error(`Unknown command: ${command.action}`);
    }
}*/

const commandHandlers = {
    getLayers,
    executeExtendScript
};
```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ai/main.js:
--------------------------------------------------------------------------------

```javascript
/* Socket.IO Plugin for After Effects (CEP)
 * Main JavaScript file
 */

const csInterface = new CSInterface();
const APPLICATION = "illustrator";
const PROXY_URL = "http://localhost:3001";


let socket = null;

// Log function
function log(message) {
    const logArea = document.getElementById('messageLog');
    const timestamp = new Date().toLocaleTimeString();
    logArea.value += `[${timestamp}] ${message}\n`;
    logArea.scrollTop = logArea.scrollHeight;
}

// Update UI status
function updateStatus(connected) {
    const statusDot = document.getElementById('statusDot');
    const statusText = document.getElementById('statusText');
    const btnConnect = document.getElementById('btnConnect');

    if (connected) {
        statusDot.classList.add('connected');
        statusText.textContent = 'Connected';
        btnConnect.textContent = 'Disconnect';
    } else {
        statusDot.classList.remove('connected');
        statusText.textContent = 'Disconnected';
        btnConnect.textContent = 'Connect';
    }
}

// Handle incoming command packets
async function onCommandPacket(packet) {
    log(`Received command: ${packet.command.action}`);

    let out = {
        senderId: packet.senderId,
    };

    try {
        // Execute the command in After Effects (from commands.js)
        //const response = await executeCommand(packet.command);
        const response = await parseAndRouteCommand(packet.command);
        
        out.response = response;
        out.status = "SUCCESS";
        
        // Get project info
        //out.projectInfo = await getProjectInfo();
        out.document = await getActiveDocumentInfo();
        
    } catch (e) {
        out.status = "FAILURE";
        out.message = `Error calling ${packet.command.action}: ${e.message}`;
        log(`Error: ${e.message}`);
    }

    return out;
}

// Connect to Socket.IO server
function connectToServer() {
    
    log(`Connecting to ${PROXY_URL}...`);
    
    socket = io(PROXY_URL, {
        transports: ["websocket", "polling"],
    });

    socket.on("connect", () => {
        updateStatus(true);
        log(`Connected with ID: ${socket.id}`);
        socket.emit("register", { application: APPLICATION });
    });

    socket.on("command_packet", async (packet) => {
        log(`Received command packet`);
        const response = await onCommandPacket(packet);
        sendResponsePacket(response);
    });

    socket.on("registration_response", (data) => {
        log(`Registration confirmed: ${data.message || 'OK'}`);
    });

    socket.on("connect_error", (error) => {
        updateStatus(false);
        log(`Connection error: ${error.message}`);
    });

    socket.on("disconnect", (reason) => {
        updateStatus(false);
        log(`Disconnected: ${reason}`);
    });
}

// Disconnect from server
function disconnectFromServer() {
    if (socket && socket.connected) {
        socket.disconnect();
        log('Disconnected from server');
    }
}

// Send response packet
function sendResponsePacket(packet) {
    if (socket && socket.connected) {
        socket.emit("command_packet_response", { packet });
        log('Response sent');
        log(packet)
        return true;
    }
    return false;
}

// LocalStorage helpers
const CONNECT_ON_LAUNCH = "connectOnLaunch";

function saveSettings() {
    localStorage.setItem(CONNECT_ON_LAUNCH, 
        document.getElementById('chkConnectOnLaunch').checked);
}

function loadSettings() {
    const connectOnLaunch = localStorage.getItem(CONNECT_ON_LAUNCH) === 'true';
    
    document.getElementById('chkConnectOnLaunch').checked = connectOnLaunch;
    
    return connectOnLaunch;
}

// Event Listeners
document.getElementById('btnConnect').addEventListener('click', () => {
    if (socket && socket.connected) {
        disconnectFromServer();
    } else {
        connectToServer();
    }
    saveSettings();
});

document.getElementById('chkConnectOnLaunch').addEventListener('change', saveSettings);


function initializeExtension() {
    const csInterface = new CSInterface();
    const extensionPath = csInterface.getSystemPath(SystemPath.EXTENSION);
    const polyfillPath = extensionPath + '/jsx/json-polyfill.jsx';
    const utilsPath = extensionPath + '/jsx/utils.jsx';
    
    csInterface.evalScript(`$.evalFile("${polyfillPath}")`, function(result) {
        console.log('JSON polyfill loaded');
    });

    csInterface.evalScript(`$.evalFile("${utilsPath}")`, function(result) {
        console.log('utilsPath loaded');
    });
}

// Initialize on load
window.addEventListener('load', () => {
    initializeExtension()
    const connectOnLaunch = loadSettings();
    log('Plugin loaded');
    
    if (connectOnLaunch) {
        connectToServer();
    }
});

```

--------------------------------------------------------------------------------
/adb-proxy-socket/proxy.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
    transports: ["websocket", "polling"],
    maxHttpBufferSize: 50 * 1024 * 1024,
});

const PORT = 3001;
// Track clients by application
const applicationClients = {};

io.on("connection", (socket) => {
    console.log(`User connected: ${socket.id}`);

    socket.on("register", ({ application }) => {
        console.log(
            `Client ${socket.id} registered for application: ${application}`
        );

        // Store the application preference with this socket
        socket.data.application = application;

        // Register this client for this application
        if (!applicationClients[application]) {
            applicationClients[application] = new Set();
        }
        applicationClients[application].add(socket.id);

        // Optionally confirm registration
        socket.emit("registration_response", {
            type: "registration",
            status: "success",
            message: `Registered for ${application}`,
        });
    });

    socket.on("command_packet_response", ({ packet }) => {
        const senderId = packet.senderId;

        if (senderId) {
            io.to(senderId).emit("packet_response", packet);
            console.log(`Sent confirmation to client ${senderId}`);
        } else {
            console.log(`No sender ID provided in packet`);
        }
    });

    socket.on("command_packet", ({ application, command }) => {
        console.log(
            `Command from ${socket.id} for application ${application}:`,
            command
        );

        // Register this client for this application if not already registered
        //if (!applicationClients[application]) {
        //  applicationClients[application] = new Set();
        //}
        //applicationClients[application].add(socket.id);

        // Process the command

        let packet = {
            senderId: socket.id,
            application: application,
            command: command,
        };

        sendToApplication(packet);

        // Send response back to this client
        //socket.emit('json_response', { from: 'server', command });
    });

    socket.on("disconnect", () => {
        console.log(`User disconnected: ${socket.id}`);

        // Remove this client from all application registrations
        for (const app in applicationClients) {
            applicationClients[app].delete(socket.id);
            // Clean up empty sets
            if (applicationClients[app].size === 0) {
                delete applicationClients[app];
            }
        }
    });
});

// Add a function to send messages to clients by application
function sendToApplication(packet) {
    let application = packet.application;
    if (applicationClients[application]) {
        console.log(
            `Sending to ${applicationClients[application].size} clients for ${application}`
        );

        let senderId = packet.senderId;
        // Loop through all client IDs for this application
        applicationClients[application].forEach((clientId) => {
            io.to(clientId).emit("command_packet", packet);
        });
        return true;
    }
    console.log(`No clients registered for application: ${application}`);
    return false;
}

// Example: Use this function elsewhere in your code
// sendToApplication('photoshop', { message: 'Update available' });

server.listen(PORT, () => {
    console.log(
        `adb-mcp Command proxy server running on ws://localhost:${PORT}`
    );
});

```

--------------------------------------------------------------------------------
/uxp/pr/main.js:
--------------------------------------------------------------------------------

```javascript
/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const { entrypoints } = require("uxp");
const { io } = require("./socket.io.js");

const { getSequences } = require("./commands/utils.js");

const {
    getProjectInfo,
    parseAndRouteCommand,
    checkRequiresActiveProject,
} = require("./commands/index.js");

const APPLICATION = "premiere";
const PROXY_URL = "http://localhost:3001";

let socket = null;

const onCommandPacket = async (packet) => {
    let command = packet.command;

    let out = {
        senderId: packet.senderId,
    };

    try {
        //this will throw if an active document is required and not open
        await checkRequiresActiveProject(command);

        let response = await parseAndRouteCommand(command);

        out.response = response;
        out.status = "SUCCESS";
        out.sequences = await getSequences();
        out.project = await getProjectInfo();
        
    } catch (e) {

        console.log(e)

        out.status = "FAILURE";
        out.message = `Error calling ${command.action} : ${e}`;
    }

    return out;
};

function connectToServer() {
    // Create new Socket.IO connection
    socket = io(PROXY_URL, {
        transports: ["websocket"],
    });

    socket.on("connect", () => {
        updateButton();
        console.log("Connected to server with ID:", socket.id);
        socket.emit("register", { application: APPLICATION });
    });

    socket.on("command_packet", async (packet) => {
        console.log("Received command packet:", packet);

        let response = await onCommandPacket(packet);
        sendResponsePacket(response);
    });

    socket.on("registration_response", (data) => {
        console.log("Received response:", data);
        //TODO: connect button here
    });

    socket.on("connect_error", (error) => {
        updateButton();
        console.error("Connection error:", error);
    });

    socket.on("disconnect", (reason) => {
        updateButton();
        console.log("Disconnected from server. Reason:", reason);

        //TODO:connect button here
    });

    return socket;
}

function disconnectFromServer() {
    if (socket && socket.connected) {
        socket.disconnect();
        console.log("Disconnected from server");
    }
}

function sendResponsePacket(packet) {
    if (socket && socket.connected) {
        socket.emit("command_packet_response", {
            packet: packet,
        });
        return true;
    }
    return false;
}

function sendCommand(command) {
    if (socket && socket.connected) {
        socket.emit("app_command", {
            application: APPLICATION,
            command: command,
        });
        return true;
    }
    return false;
}

entrypoints.setup({
    panels: {
        vanilla: {
            show(node) {},
        },
    },
});

let updateButton = () => {
    let b = document.getElementById("btnStart");

    b.textContent = socket && socket.connected ? "Disconnect" : "Connect";
};

//Toggle button to make it start stop
document.getElementById("btnStart").addEventListener("click", () => {
    if (socket && socket.connected) {
        disconnectFromServer();
    } else {
        connectToServer();
    }
});

const CONNECT_ON_LAUNCH = "connectOnLaunch";
// Save checkbox state in localStorage
document
    .getElementById("chkConnectOnLaunch")
    .addEventListener("change", function (event) {
        window.localStorage.setItem(
            CONNECT_ON_LAUNCH,
            JSON.stringify(event.target.checked)
        );
    });

// Retrieve checkbox state
const getConnectOnLaunch = () => {
    return JSON.parse(window.localStorage.getItem(CONNECT_ON_LAUNCH)) || false;
};

// Set checkbox state on page load
document.addEventListener("DOMContentLoaded", () => {
    document.getElementById("chkConnectOnLaunch").checked =
        getConnectOnLaunch();
});

window.addEventListener("load", (event) => {
    if (getConnectOnLaunch()) {
        connectToServer();
    }
});

```

--------------------------------------------------------------------------------
/uxp/id/main.js:
--------------------------------------------------------------------------------

```javascript
/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const { entrypoints, UI } = require("uxp");
const { io } = require("./socket.io.js");
const app = require("indesign");

const {
    parseAndRouteCommand,
    checkRequiresActiveDocument,
    getActiveDocumentSettings,
} = require("./commands/index.js");

const APPLICATION = "indesign";
const PROXY_URL = "http://localhost:3001";

let socket = null;

const onCommandPacket = async (packet) => {
    let command = packet.command;

    let out = {
        senderId: packet.senderId,
    };

    try {
        //this will throw if an active document is required and not open
        checkRequiresActiveDocument(command);

        let response = await parseAndRouteCommand(command);

        out.response = response;
        out.status = "SUCCESS";
        out.activeDocument = await getActiveDocumentSettings();
        //out.projectItems = await getProjectContentInfo();
    } catch (e) {
        out.status = "FAILURE";
        out.message = `Error calling ${command.action} : ${e}`;
    }

    return out;
};

function connectToServer() {
    // Create new Socket.IO connection
    const isWindows = require("os").platform() === "win32";

    const socketOptions = isWindows
        ? {
              transports: ["polling"],
              upgrade: false,
              rememberUpgrade: false,
          }
        : {
              transports: ["websocket"],
          };
    console.log(isWindows);
    console.log(socketOptions);
    socket = io(PROXY_URL, socketOptions);

    socket.on("connect", () => {
        updateButton();
        console.log("Connected to server with ID:", socket.id);
        socket.emit("register", { application: APPLICATION });
    });

    socket.on("command_packet", async (packet) => {
        console.log("Received command packet:", packet);

        let response = await onCommandPacket(packet);
        sendResponsePacket(response);
    });

    socket.on("registration_response", (data) => {
        console.log("Received response:", data);
        //TODO: connect button here
    });

    socket.on("connect_error", (error) => {
        updateButton();
        console.error("Connection error:", error);
    });

    socket.on("disconnect", (reason) => {
        updateButton();
        console.log("Disconnected from server. Reason:", reason);

        //TODO:connect button here
    });

    return socket;
}

function disconnectFromServer() {
    if (socket && socket.connected) {
        socket.disconnect();
        console.log("Disconnected from server");
    }
}

function sendResponsePacket(packet) {
    if (socket && socket.connected) {
        socket.emit("command_packet_response", {
            packet: packet,
        });
        return true;
    }
    return false;
}

function sendCommand(command) {
    if (socket && socket.connected) {
        socket.emit("app_command", {
            application: APPLICATION,
            command: command,
        });
        return true;
    }
    return false;
}

entrypoints.setup({
    panels: {
        vanilla: {
            show(node) {},
        },
    },
});

let updateButton = () => {
    let b = document.getElementById("btnStart");

    b.textContent = socket && socket.connected ? "Disconnect" : "Connect";
};

//Toggle button to make it start stop
document.getElementById("btnStart").addEventListener("click", () => {
    if (socket && socket.connected) {
        disconnectFromServer();
    } else {
        connectToServer();
    }
});

const CONNECT_ON_LAUNCH = "connectOnLaunch";
// Save checkbox state in localStorage
document
    .getElementById("chkConnectOnLaunch")
    .addEventListener("change", function (event) {
        window.localStorage.setItem(
            CONNECT_ON_LAUNCH,
            JSON.stringify(event.target.checked)
        );
    });

// Retrieve checkbox state
const getConnectOnLaunch = () => {
    return JSON.parse(window.localStorage.getItem(CONNECT_ON_LAUNCH)) || false;
};

// Set checkbox state on page load
document.addEventListener("DOMContentLoaded", () => {
    document.getElementById("chkConnectOnLaunch").checked =
        getConnectOnLaunch();
});

window.addEventListener("load", (event) => {
    if (getConnectOnLaunch()) {
        connectToServer();
    }
});

```

--------------------------------------------------------------------------------
/mcp/fonts.py:
--------------------------------------------------------------------------------

```python
# MIT License
#
# Copyright (c) 2025 Mike Chambers
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import os
import sys
import glob
from fontTools.ttLib import TTFont

def list_all_fonts_postscript():
    """
    Returns a list of PostScript names for all fonts installed on the system.
    Works on both Windows and macOS.
    
    Returns:
        list: A list of PostScript font names as strings
    """
    postscript_names = []
    
    # Get font directories based on platform
    font_dirs = []
    
    if sys.platform == 'win32':  # Windows
        # Windows font directory
        if 'WINDIR' in os.environ:
            font_dirs.append(os.path.join(os.environ['WINDIR'], 'Fonts'))
    
    elif sys.platform == 'darwin':  # macOS
        # macOS system font directories
        font_dirs.extend([
            '/System/Library/Fonts',
            '/Library/Fonts',
            os.path.expanduser('~/Library/Fonts')
        ])
    
    else:
        print(f"Unsupported platform: {sys.platform}")
        return []
    
    # Get all font files from all directories
    font_extensions = ['*.ttf', '*.ttc', '*.otf']
    font_files = []
    
    for font_dir in font_dirs:
        if os.path.exists(font_dir):
            for ext in font_extensions:
                font_files.extend(glob.glob(os.path.join(font_dir, ext)))
                # Also check subdirectories on macOS
                if sys.platform == 'darwin':
                    font_files.extend(glob.glob(os.path.join(font_dir, '**', ext), recursive=True))
    
    # Process each font file
    for font_path in font_files:
        try:
            # TrueType Collections (.ttc files) can contain multiple fonts
            if font_path.lower().endswith('.ttc'):
                try:
                    ttc = TTFont(font_path, fontNumber=0)
                    num_fonts = ttc.reader.numFonts
                    ttc.close()
                    
                    # Extract PostScript name from each font in the collection
                    for i in range(num_fonts):
                        try:
                            font = TTFont(font_path, fontNumber=i)
                            ps_name = _extract_postscript_name(font)
                            if ps_name and not ps_name.startswith('.'):
                                postscript_names.append(ps_name)
                            font.close()
                        except Exception as e:
                            print(f"Error processing font {i} in collection {font_path}: {e}")
                except Exception as e:
                    print(f"Error determining number of fonts in collection {font_path}: {e}")
            else:
                # Regular TTF/OTF file
                try:
                    font = TTFont(font_path)
                    ps_name = _extract_postscript_name(font)
                    if ps_name:
                        postscript_names.append(ps_name)
                    font.close()
                except Exception as e:
                    print(f"Error processing font {font_path}: {e}")
        except Exception as e:
            print(f"Error with font file {font_path}: {e}")
 
    return list(set(postscript_names))

def _extract_postscript_name(font):
    """
    Extract the PostScript name from a TTFont object.
    
    Args:
        font: A TTFont object
        
    Returns:
        str: The PostScript name or None if not found
    """
    # Method 1: Try to get it from the name table (most reliable)
    if 'name' in font:
        name_table = font['name']
        
        # PostScript name is stored with nameID 6
        for record in name_table.names:
            if record.nameID == 6:
                # Try to decode the name
                try:
                    return (
                        record.string.decode('utf-16-be').encode('utf-8').decode('utf-8')
                        if record.isUnicode() else record.string.decode('latin-1')
                    )
                except Exception:
                    pass
    
    # Method 2: For CFF OpenType fonts
    if 'CFF ' in font:
        try:
            cff = font['CFF ']
            if cff.cff.fontNames:
                return cff.cff.fontNames[0]
        except Exception:
            pass
    
    return None

if __name__ == "__main__":
    font_names = list_all_fonts_postscript()
    print(f"Number of fonts found: {len(font_names)}")
```

--------------------------------------------------------------------------------
/mcp/socket_client.py:
--------------------------------------------------------------------------------

```python
# MIT License
#
# Copyright (c) 2025 Mike Chambers
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

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

# Global configuration variables
proxy_url = None
proxy_timeout = None
application = None

def send_message_blocking(command, timeout=None):
    """
    Blocking function that connects to a Socket.IO server, sends a message,
    waits for a response, then disconnects.
    
    Args:
        command: The command to send
        timeout (int): Maximum time to wait for response in seconds
        
    Returns:
        dict: The response received from the server, or None if no response
    """
    # Use global variables
    global application, proxy_url, proxy_timeout
    
    # Check if configuration is set
    if not application or not proxy_url or not proxy_timeout:
        logger.log("Socket client not configured. Call configure() first.")
        return None
    
    # Use provided timeout or default
    wait_timeout = timeout if timeout is not None else proxy_timeout
    
    # Create a standard (non-async) SocketIO client with WebSocket transport only
    sio = socketio.Client(logger=False)
    
    # Use a queue to get the response from the event handler
    response_queue = Queue()
    
    connection_failed = [False]         

    @sio.event
    def connect():
        logger.log(f"Connected to server with session ID: {sio.sid}")
        
        # Send the command
        logger.log(f"Sending message to {application}: {command}")
        sio.emit('command_packet', {
            'type': "command",
            'application': application,
            'command': command
        })
    
    @sio.event
    def packet_response(data):
        logger.log(f"Received response: {data}")
        response_queue.put(data)
        # Disconnect after receiving the response
        sio.disconnect()
    
    @sio.event
    def disconnect():
        logger.log("Disconnected from server")
        # If we disconnect without response, put None in the queue
        if response_queue.empty():
            response_queue.put(None)
    
    @sio.event
    def connect_error(error):
        logger.log(f"Connection error: {error}")
        connection_failed[0] = True
        response_queue.put(None)
    
    # Connect in a separate thread to avoid blocking the main thread during connection
    def connect_and_wait():
        try:
            sio.connect(proxy_url, transports=['websocket'])
            # Keep the client running until disconnect is called
            sio.wait()
        except Exception as e:
            logger.log(f"Error: {e}")
            connection_failed[0] = True
            if response_queue.empty():
                response_queue.put(None)
            if sio.connected:
                sio.disconnect()
    
    # Start the client in a separate thread
    client_thread = threading.Thread(target=connect_and_wait)
    client_thread.daemon = True
    client_thread.start()
    
    try:
        # Wait for a response or timeout
        logger.log("waiting for response...")
        response = response_queue.get(timeout=wait_timeout)

        if connection_failed[0]:
            raise RuntimeError(f"Error: Could not connect to {application} command proxy server. Make sure that the proxy server is running listening on the correct url {proxy_url}.")

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

            if response["status"] == "FAILURE":
                raise AppError(f"Error returned from {application}: {response['message']}")
            
        return response
    except AppError:
        raise
    except Exception as e:
        logger.log(f"Error waiting for response: {e}")
        if sio.connected:
            sio.disconnect()
  
        raise RuntimeError(f"Error: Could not connect to {application}. Connection Timed Out. Make sure that {application} is running and that the MCP Plugin is connected. Original error: {e}")
    finally:
        # Make sure client is disconnected
        if sio.connected:
            sio.disconnect()
        # Wait for the thread to finish (should be quick after disconnect)
        client_thread.join(timeout=1)

class AppError(Exception):
    pass

def configure(app=None, url=None, timeout=None):
    
    global application, proxy_url, proxy_timeout
    
    if app:
        application = app
    if url:
        proxy_url = url
    if timeout:
        proxy_timeout = timeout
    
    logger.log(f"Socket client configured: app={application}, url={proxy_url}, timeout={proxy_timeout}")
```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ai/jsx/utils.jsx:
--------------------------------------------------------------------------------

```javascript
// jsx/illustrator-helpers.jsx

// Helper function to extract XMP attribute values
$.global.extractXMPAttribute = function(xmpStr, tagName, attrName) {
    var pattern = new RegExp(tagName + '[^>]*' + attrName + '="([^"]+)"', 'i');
    var match = xmpStr.match(pattern);
    return match ? match[1] : null;
};

// Helper function to extract XMP tag values
$.global.extractXMPValue = function(xmpStr, tagName) {
    var pattern = new RegExp('<' + tagName + '>([^<]+)<\\/' + tagName + '>', 'i');
    var match = xmpStr.match(pattern);
    return match ? match[1] : null;
};

// Helper function to get document ID from XMP
$.global.getDocumentID = function(doc) {
    try {
        var xmpString = doc.XMPString;
        if (!xmpString) return null;
        
        return $.global.extractXMPAttribute(xmpString, 'xmpMM:DocumentID', 'rdf:resource') || 
               $.global.extractXMPValue(xmpString, 'xmpMM:DocumentID');
    } catch(e) {
        return null;
    }
};

// jsx/illustrator-helpers.jsx

// ... existing helper functions ...

// Helper function to create document info object
$.global.createDocumentInfo = function(doc, activeDoc) {
    return {
        id: $.global.getDocumentID(doc),
        name: doc.name,
        width: doc.width,
        height: doc.height,
        colorSpace: doc.documentColorSpace.toString(),
        numLayers: doc.layers.length,
        numArtboards: doc.artboards.length,
        saved: doc.saved,
        isActive: doc === activeDoc
    };
};


// Helper function to get detailed layer information
$.global.getLayerInfo = function(layer, includeSubLayers) {
    if (includeSubLayers === undefined) includeSubLayers = true;
    
    try {
        var layerInfo = {
            id: layer.absoluteZOrderPosition,
            name: layer.name,
            visible: layer.visible,
            locked: layer.locked,
            opacity: layer.opacity,
            printable: layer.printable,
            preview: layer.preview,
            sliced: layer.sliced,
            isIsolated: layer.isIsolated,
            hasSelectedArtwork: layer.hasSelectedArtwork,
            itemCount: layer.pageItems.length,
            zOrderPosition: layer.zOrderPosition,
            absoluteZOrderPosition: layer.absoluteZOrderPosition,
            dimPlacedImages: layer.dimPlacedImages,
            typename: layer.typename
        };
        
        // Get blending mode
        try {
            layerInfo.blendingMode = layer.blendingMode.toString();
        } catch(e) {
            layerInfo.blendingMode = "Normal";
        }
        
        // Get color info if available
        try {
            layerInfo.color = {
                red: layer.color.red,
                green: layer.color.green,
                blue: layer.color.blue
            };
        } catch(e) {
            layerInfo.color = null;
        }
        
        // Get artwork knockout state
        try {
            layerInfo.artworkKnockout = layer.artworkKnockout.toString();
        } catch(e) {
            layerInfo.artworkKnockout = "Inherited";
        }
        
        // Count different types of items on the layer
        try {
            layerInfo.itemCounts = {
                total: layer.pageItems.length,
                pathItems: layer.pathItems.length,
                textFrames: layer.textFrames.length,
                groupItems: layer.groupItems.length,
                compoundPathItems: layer.compoundPathItems.length,
                placedItems: layer.placedItems.length,
                rasterItems: layer.rasterItems.length,
                meshItems: layer.meshItems.length,
                symbolItems: layer.symbolItems.length
            };
        } catch(e) {
            layerInfo.itemCounts = { total: 0 };
        }
        
        // Handle sublayers
        layerInfo.subLayerCount = layer.layers.length;
        layerInfo.hasSubLayers = layer.layers.length > 0;
        
        if (includeSubLayers && layer.layers.length > 0) {
            layerInfo.subLayers = [];
            for (var j = 0; j < layer.layers.length; j++) {
                var subLayer = layer.layers[j];
                // Recursively get sublayer info (but don't go deeper to avoid infinite recursion)
                var subLayerInfo = $.global.getLayerInfo(subLayer, false);
                layerInfo.subLayers.push(subLayerInfo);
            }
        }
        
        return layerInfo;
    } catch(e) {
        return {
            error: "Error processing layer: " + e.toString(),
            layerName: layer.name || "Unknown"
        };
    }
};

// Helper function to get all layers information for a document
$.global.getAllLayersInfo = function(doc) {
    try {
        var layersInfo = [];
        
        for (var i = 0; i < doc.layers.length; i++) {
            var layer = doc.layers[i];
            var layerInfo = $.global.getLayerInfo(layer, true);
            layersInfo.push(layerInfo);
        }
        
        return {
            totalLayers: doc.layers.length,
            layers: layersInfo
        };
    } catch(e) {
        return {
            error: e.toString(),
            totalLayers: 0,
            layers: []
        };
    }
};

$.global.createDocumentInfo = function(doc, activeDoc) {
    var docInfo = {
        id: $.global.getDocumentID(doc),
        name: doc.name,
        width: doc.width,
        height: doc.height,
        colorSpace: doc.documentColorSpace.toString(),
        numLayers: doc.layers.length,
        numArtboards: doc.artboards.length,
        saved: doc.saved,
        isActive: doc === activeDoc
    };
    
    // Add layers information
    var layersResult = $.global.getAllLayersInfo(doc);
    docInfo.layers = layersResult.layers;
    docInfo.totalLayers = layersResult.totalLayers;
    
    return docInfo;
};
```

--------------------------------------------------------------------------------
/uxp/ps/main.js:
--------------------------------------------------------------------------------

```javascript
/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const { entrypoints, UI } = require("uxp");
const {
    checkRequiresActiveDocument,
    parseAndRouteCommand,
} = require("./commands/index.js");

const { hasActiveSelection, generateDocumentInfo } = require("./commands/utils.js");

const { getLayers } = require("./commands/layers.js").commandHandlers;

const { io } = require("./socket.io.js");
//const { act } = require("react");
const app = require("photoshop").app;

const APPLICATION = "photoshop";
const PROXY_URL = "http://localhost:3001";

let socket = null;

const onCommandPacket = async (packet) => {
    let command = packet.command;

    let out = {
        senderId: packet.senderId,
    };

    try {
        //this will throw if an active document is required and not open
        checkRequiresActiveDocument(command);

        let response = await parseAndRouteCommand(command);

        out.response = response;
        out.status = "SUCCESS";

        let activeDocument = app.activeDocument
        let doc = generateDocumentInfo(activeDocument, activeDocument)
        out.document = doc;

        out.layers = await getLayers();

        out.hasActiveSelection = hasActiveSelection();
    } catch (e) {
        out.status = "FAILURE";
        out.message = `Error calling ${command.action} : ${e}`;
    }

    return out;
};

function connectToServer() {
    // Create new Socket.IO connection
    socket = io(PROXY_URL, {
        transports: ["websocket"],
    });

    socket.on("connect", () => {
        updateButton();
        console.log("Connected to server with ID:", socket.id);
        socket.emit("register", { application: APPLICATION });
    });

    socket.on("command_packet", async (packet) => {
        console.log("Received command packet:", packet);

        let response = await onCommandPacket(packet);
        sendResponsePacket(response);
    });

    socket.on("registration_response", (data) => {
        console.log("Received response:", data);
        //TODO: connect button here
    });

    socket.on("connect_error", (error) => {
        updateButton();
        console.error("Connection error:", error);
    });

    socket.on("disconnect", (reason) => {
        updateButton();
        console.log("Disconnected from server. Reason:", reason);

        //TODO:connect button here
    });

    return socket;
}

function disconnectFromServer() {
    if (socket && socket.connected) {
        socket.disconnect();
        console.log("Disconnected from server");
    }
}

function sendResponsePacket(packet) {
    if (socket && socket.connected) {
        socket.emit("command_packet_response", {
            packet: packet,
        });
        return true;
    }
    return false;
}

function sendCommand(command) {
    if (socket && socket.connected) {
        socket.emit("app_command", {
            application: APPLICATION,
            command: command,
        });
        return true;
    }
    return false;
}

let onInterval = async () => {
    let commands = await fetchCommands();

    await parseAndRouteCommands(commands);
};

let fetchCommands = async () => {
    try {
        let url = `http://127.0.0.1:3030/commands/get/${APPLICATION}/`;

        const fetchOptions = {
            method: "GET",
            headers: {
                Accept: "application/json",
            },
        };

        // Make the fetch request
        const response = await fetch(url, fetchOptions);

        // Check if the request was successful
        if (!response.ok) {
            console.log("a");
            throw new Error(`HTTP error! Status: ${response.status}`);
        }

        let r = await response.json();

        if (r.status != "SUCCESS") {
            throw new Error(`API Request error! Status: ${response.message}`);
        }

        return r.commands;
    } catch (error) {
        console.error("Error fetching data:", error);
        throw error; // Re-throw to allow caller to handle the error
    }
};

entrypoints.setup({
    panels: {
        vanilla: {
            show(node) {},
        },
    },
});

let updateButton = () => {
    let b = document.getElementById("btnStart");

    b.textContent = socket && socket.connected ? "Disconnect" : "Connect";
};

//Toggle button to make it start stop
document.getElementById("btnStart").addEventListener("click", () => {
    if (socket && socket.connected) {
        disconnectFromServer();
    } else {
        connectToServer();
    }
});

const CONNECT_ON_LAUNCH = "connectOnLaunch";
// Save checkbox state in localStorage
document
    .getElementById("chkConnectOnLaunch")
    .addEventListener("change", function (event) {
        window.localStorage.setItem(
            CONNECT_ON_LAUNCH,
            JSON.stringify(event.target.checked)
        );
    });

// Retrieve checkbox state
const getConnectOnLaunch = () => {
    return JSON.parse(window.localStorage.getItem(CONNECT_ON_LAUNCH)) || false;
};

// Set checkbox state on page load
document.addEventListener("DOMContentLoaded", () => {
    document.getElementById("chkConnectOnLaunch").checked =
        getConnectOnLaunch();
});

window.addEventListener("load", (event) => {
    if (getConnectOnLaunch()) {
        connectToServer();
    }
});

```

--------------------------------------------------------------------------------
/mcp/ps-batch-play.py:
--------------------------------------------------------------------------------

```python
# MIT License
#
# Copyright (c) 2025 Mike Chambers
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from mcp.server.fastmcp import FastMCP, Image
from core import init, sendCommand, createCommand
from fonts import list_all_fonts_postscript
import numpy as np
import base64
import socket_client
import sys
import os

FONT_LIMIT = 1000 #max number of font names to return to AI

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

APPLICATION = "photoshop"
PROXY_URL = 'http://localhost:3001'
PROXY_TIMEOUT = 20

socket_client.configure(
    app=APPLICATION, 
    url=PROXY_URL,
    timeout=PROXY_TIMEOUT
)

init(APPLICATION, socket_client)

@mcp.tool()
def call_batch_play_command(commands: list):
    """
    Executes arbitrary Photoshop batchPlay commands via MCP.

    Args:
        commands (str): A raw JSON string representing a list of batchPlay descriptors.
            This should be the exact JSON string you would pass to `batchPlay()` in a UXP plugin.

    Returns:
        Any: The result returned from Photoshop after executing the batchPlay command(s).

    Example:
        >>> commands = '''
        ... [
        ...     {
        ...         "_obj": "exportDocumentAs",
        ...         "exportAs": {
        ...             "_obj": "exportAsPNG",
        ...             "interlaced": false,
        ...             "transparency": true,
        ...             "metadata": 1
        ...         },
        ...         "documentID": 1234,
        ...         "saveFile": {
        ...             "_path": "/Users/yourname/Downloads/export.png",
        ...             "_kind": "local"
        ...         },
        ...         "overwrite": true
        ...     }
        ... ]
        ... '''
        >>> result = call_batch_play_command(commands)
        >>> print(result)
        # Output from Photoshop will be returned as-is (usually a list of response descriptors)
    """

    if not commands:
        raise ValueError("commands cannot be empty.")

    command = createCommand(
        "executeBatchPlayCommand",
        {
            "commands": commands
        }
    )

    return sendCommand(command)


@mcp.resource("config://get_instructions")
def get_instructions() -> str:
    """Read this first! Returns information and instructions on how to use Photoshop and this API"""

    return f"""
    You are a photoshop expert who is creative and loves to help other people learn to use Photoshop and create. You are well versed in composition, design and color theory, and try to follow that theory when making decisions.

    Unless otherwise specified, all commands act on the currently active document in Photoshop

    Rules to follow:

    1. Think deeply about how to solve the task
    2. Always check your work
    3. You can view the current visible photoshop file by calling get_document_image
    4. Pay attention to font size (dont make it too big)
    5. Always use alignment (align_content()) to position your text.
    6. Read the info for the API calls to make sure you understand the requirements and arguments
    7. When you make a selection, clear it once you no longer need it

    Here are some general tips for when working with Photoshop.

    In general, layers are created from bottom up, so keep that in mind as you figure out the order or operations. If you want you have lower layers show through higher ones you must either change the opacity of the higher layers and / or blend modes.

    When using fonts there are a couple of things to keep in mind. First, the font origin is the bottom left of the font, not the top right.

    Suggestions for sizes:
    Paragraph text : 8 to 12 pts
    Headings : 14 - 20 pts
    Single Word Large : 20 to 25pt

    Pay attention to what layer names are needed for. Sometimes the specify the name of a newly created layer and sometimes they specify the name of the layer that the action should be performed on.

    As a general rule, you should not flatten files unless asked to do so, or its necessary to apply an effect or look.

    When generating an image, you do not need to first create a pixel layer. A layer will automatically be created when you generate the image.

    Colors are defined via a dict with red, green and blue properties with values between 0 and 255
    {{"red":255, "green":0, "blue":0}}

    Bounds is defined as a dict with top, left, bottom and right properties
    {{"top": 0, "left": 0, "bottom": 250, "right": 300}}

    Valid options for API calls:

    alignment_modes: {", ".join(alignment_modes)}

    justification_modes: {", ".join(justification_modes)}

    blend_modes: {", ".join(blend_modes)}

    anchor_positions: {", ".join(anchor_positions)}

    interpolation_methods: {", ".join(interpolation_methods)}

    fonts: {", ".join(font_names[:FONT_LIMIT])}
    """

font_names = list_all_fonts_postscript()

interpolation_methods = [
   "AUTOMATIC",
   "BICUBIC",
   "BICUBICSHARPER",
   "BICUBICSMOOTHER",
   "BILINEAR",
   "NEARESTNEIGHBOR"
]

anchor_positions = [
   "BOTTOMCENTER",
   "BOTTOMLEFT", 
   "BOTTOMRIGHT", 
   "MIDDLECENTER", 
   "MIDDLELEFT", 
   "MIDDLERIGHT", 
   "TOPCENTER", 
   "TOPLEFT", 
   "TOPRIGHT"
]

justification_modes = [
    "CENTER",
    "CENTERJUSTIFIED",
    "FULLYJUSTIFIED",
    "LEFT",
    "LEFTJUSTIFIED",
    "RIGHT",
    "RIGHTJUSTIFIED"
]

alignment_modes = [
    "LEFT",
    "CENTER_HORIZONTAL",
    "RIGHT",
    "TOP",
    "CENTER_VERTICAL",
    "BOTTOM"
]

blend_modes = [
    "COLOR",
    "COLORBURN",
    "COLORDODGE",
    "DARKEN",
    "DARKERCOLOR",
    "DIFFERENCE",
    "DISSOLVE",
    "DIVIDE",
    "EXCLUSION",
    "HARDLIGHT",
    "HARDMIX",
    "HUE",
    "LIGHTEN",
    "LIGHTERCOLOR",
    "LINEARBURN",
    "LINEARDODGE",
    "LINEARLIGHT",
    "LUMINOSITY",
    "MULTIPLY",
    "NORMAL",
    "OVERLAY",
    "PASSTHROUGH",
    "PINLIGHT",
    "SATURATION",
    "SCREEN",
    "SOFTLIGHT",
    "SUBTRACT",
    "VIVIDLIGHT"
]

```

--------------------------------------------------------------------------------
/uxp/ps/commands/adjustment_layers.js:
--------------------------------------------------------------------------------

```javascript
/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const { action } = require("photoshop");

const {
    selectLayer,
    findLayer,
    execute
} = require("./utils")

const addAdjustmentLayerBlackAndWhite = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `addAdjustmentLayerBlackAndWhite : Could not find layerId : ${layerId}`
        );
    }

    let colors = options.colors;
    let tintColor = options.tintColor

    await execute(async () => {
        selectLayer(layer, true);

        let commands = [
            // Make adjustment layer
            {
                _obj: "make",
                _target: [
                    {
                        _ref: "adjustmentLayer",
                    },
                ],
                using: {
                    _obj: "adjustmentLayer",
                    type: {
                        _obj: "blackAndWhite",
                        blue: colors.blue,
                        cyan: colors.cyan,
                        grain: colors.green,
                        magenta: colors.magenta,
                        presetKind: {
                            _enum: "presetKindType",
                            _value: "presetKindDefault",
                        },
                        red: colors.red,
                        tintColor: {
                            _obj: "RGBColor",
                            blue: tintColor.blue,
                            grain: tintColor.green,
                            red: tintColor.red,
                        },
                        useTint: options.tint,
                        yellow: colors.yellow,
                    },
                },
            },
        ];

        await action.batchPlay(commands, {});
    });
};

const addBrightnessContrastAdjustmentLayer = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `addBrightnessContrastAdjustmentLayer : Could not find layerId : ${layerId}`
        );
    }

    await execute(async () => {
        selectLayer(layer, true);

        let commands = [
            // Make adjustment layer
            {
                _obj: "make",
                _target: [
                    {
                        _ref: "adjustmentLayer",
                    },
                ],
                using: {
                    _obj: "adjustmentLayer",
                    type: {
                        _obj: "brightnessEvent",
                        useLegacy: false,
                    },
                },
            },
            // Set current adjustment layer
            {
                _obj: "set",
                _target: [
                    {
                        _enum: "ordinal",
                        _ref: "adjustmentLayer",
                        _value: "targetEnum",
                    },
                ],
                to: {
                    _obj: "brightnessEvent",
                    brightness: options.brightness,
                    center: options.contrast,
                    useLegacy: false,
                },
            },
        ];

        await action.batchPlay(commands, {});
    });
};

const addAdjustmentLayerVibrance = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `addAdjustmentLayerVibrance : Could not find layerId : ${layerId}`
        );
    }

    let colors = options.colors;

    await execute(async () => {
        selectLayer(layer, true);

        let commands = [
            // Make adjustment layer
            {
                _obj: "make",
                _target: [
                    {
                        _ref: "adjustmentLayer",
                    },
                ],
                using: {
                    _obj: "adjustmentLayer",
                    type: {
                        _class: "vibrance",
                    },
                },
            },
            // Set current adjustment layer
            {
                _obj: "set",
                _target: [
                    {
                        _enum: "ordinal",
                        _ref: "adjustmentLayer",
                        _value: "targetEnum",
                    },
                ],
                to: {
                    _obj: "vibrance",
                    saturation: options.saturation,
                    vibrance: options.vibrance,
                },
            },
        ];

        await action.batchPlay(commands, {});
    });
};

const addColorBalanceAdjustmentLayer = async (command) => {

    let options = command.options;

    let layerId = options.layerId;
    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `addColorBalanceAdjustmentLayer : Could not find layer named : [${layerId}]`
        );
    }

    await execute(async () => {
        let commands = [
            // Make adjustment layer
            {
                _obj: "make",
                _target: [
                    {
                        _ref: "adjustmentLayer",
                    },
                ],
                using: {
                    _obj: "adjustmentLayer",
                    type: {
                        _obj: "colorBalance",
                        highlightLevels: [0, 0, 0],
                        midtoneLevels: [0, 0, 0],
                        preserveLuminosity: true,
                        shadowLevels: [0, 0, 0],
                    },
                },
            },
            // Set current adjustment layer
            {
                _obj: "set",
                _target: [
                    {
                        _enum: "ordinal",
                        _ref: "adjustmentLayer",
                        _value: "targetEnum",
                    },
                ],
                to: {
                    _obj: "colorBalance",
                    highlightLevels: options.highlights,
                    midtoneLevels: options.midtones,
                    shadowLevels: options.shadows,
                },
            },
        ];
        await action.batchPlay(commands, {});
    });
};

const commandHandlers = {
    addAdjustmentLayerBlackAndWhite,
    addBrightnessContrastAdjustmentLayer,
    addAdjustmentLayerVibrance,
    addColorBalanceAdjustmentLayer
}

module.exports = {
    commandHandlers
};
```

--------------------------------------------------------------------------------
/mcp/ai-mcp.py:
--------------------------------------------------------------------------------

```python
# MIT License
#
# Copyright (c) 2025 Mike Chambers
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from mcp.server.fastmcp import FastMCP
from core import init, sendCommand, createCommand
import socket_client
import sys

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

APPLICATION = "illustrator"
PROXY_URL = 'http://localhost:3001'
PROXY_TIMEOUT = 20

socket_client.configure(
    app=APPLICATION, 
    url=PROXY_URL,
    timeout=PROXY_TIMEOUT
)

init(APPLICATION, socket_client)

@mcp.tool()
def get_documents():
    """
    Returns information about all currently open documents in Illustrator.

    """
    command = createCommand("getDocuments", {})
    return sendCommand(command)

@mcp.tool()
def get_active_document_info():
    """
    Returns information about the current active document.

    """
    command = createCommand("getActiveDocumentInfo", {})
    return sendCommand(command)

@mcp.tool()
def open_file(
    path: str
):
    """
    Opens an Illustrator (.ai) file in Adobe Illustrator.
    
    Args:
        path (str): The absolute file path to the Illustrator file to open.
            Example: "/Users/username/Documents/my_artwork.ai"
    
    Returns:
        dict: Result containing:
            - success (bool): Whether the file was opened successfully
            - error (str): Error message if opening failed
    
    """
    
    command_params = {
        "path": path
    }
    
    command = createCommand("openFile", command_params)
    return sendCommand(command)

@mcp.tool()
def export_png(
    path: str,
    transparency: bool = True,
    anti_aliasing: bool = True,
    artboard_clipping: bool = True,
    horizontal_scale: int = 100,
    vertical_scale: int = 100,
    export_type: str = "PNG24",
    matte: bool = None,
    matte_color: dict = {"red": 255, "green": 255, "blue": 255}
):
    """
    Exports the active Illustrator document as a PNG file.
    
    Args:
        path (str): The absolute file path where the PNG will be saved.
            Example: "/Users/username/Documents/my_export.png"
        transparency (bool, optional): Enable/disable transparency. Defaults to True.
        anti_aliasing (bool, optional): Enable/disable anti-aliasing for smooth edges. Defaults to True.
        artboard_clipping (bool, optional): Clip export to artboard bounds. Defaults to True.
        horizontal_scale (int, optional): Horizontal scale percentage (1-1000). Defaults to 100.
        vertical_scale (int, optional): Vertical scale percentage (1-1000). Defaults to 100.
        export_type (str, optional): PNG format type. "PNG24" (24-bit) or "PNG8" (8-bit). Defaults to "PNG24".
        matte (bool, optional): Enable matte background color for transparency preview. 
            If None, uses Illustrator's default behavior.
        matte_color (dict, optional): RGB color for matte background. Defaults to {"red": 255, "green": 255, "blue": 255}.
            Dict with keys "red", "green", "blue" with values 0-255.
    
    Returns:
        dict: Export result containing:
            - success (bool): Whether the export succeeded
            - filePath (str): The actual file path where the PNG was saved
            - fileExists (bool): Whether the exported file exists
            - options (dict): The export options that were used
            - documentName (str): Name of the exported document
            - error (str): Error message if export failed
    
    Example:
        # Basic PNG export
        result = export_png("/Users/username/Desktop/my_artwork.png")
        
        # High-resolution export with transparency
        result = export_png(
            path="/Users/username/Desktop/high_res.png",
            horizontal_scale=300,
            vertical_scale=300,
            transparency=True
        )
        
        # PNG8 export with red matte background
        result = export_png(
            path="/Users/username/Desktop/small_file.png",
            export_type="PNG8",
            matte=True,
            matte_color={"red": 255, "green": 0, "blue": 0}
        )
        
        # Blue matte background
        result = export_png(
            path="/Users/username/Desktop/blue_bg.png",
            matte=True,
            matte_color={"red": 0, "green": 100, "blue": 255}
        )
    """


    # Only include matte and matteColor if needed
    command_params = {
        "path": path,
        "transparency": transparency,
        "antiAliasing": anti_aliasing,
        "artBoardClipping": artboard_clipping,
        "horizontalScale": horizontal_scale,
        "verticalScale": vertical_scale,
        "exportType": export_type
    }

    # Only include matte if explicitly set
    if matte is not None:
        command_params["matte"] = matte
        
    # Include matte color if matte is enabled or custom colors provided
    if matte or matte_color != {"red": 255, "green": 255, "blue": 255}:
        command_params["matteColor"] = matte_color

    command = createCommand("exportPNG", command_params)
    return sendCommand(command)



@mcp.tool()
def execute_extend_script(script_string: str):
    """
    Executes arbitrary ExtendScript code in Illustrator and returns the result.
    
    The script should use 'return' to send data back. The result will be automatically
    JSON stringified. If the script throws an error, it will be caught and returned
    as an error object.
    
    Args:
        script_string (str): The ExtendScript code to execute. Must use 'return' to 
                           send results back.
    
    Returns:
        any: The result returned from the ExtendScript, or an error object containing:
            - error (str): Error message
            - line (str): Line number where error occurred
    
    Example:
        script = '''
            var comp = app.project.activeItem;
            return {
                name: comp.name,
                layers: comp.numLayers
            };
        '''
        result = execute_extend_script(script)
    """
    command = createCommand("executeExtendScript", {
        "scriptString": script_string
    })
    return sendCommand(command)

@mcp.resource("config://get_instructions")
def get_instructions() -> str:
    """Read this first! Returns information and instructions on how to use Illustrator and this API"""

    return f"""
    You are an Illustrator export who is creative and loves to help other people learn to use Illustrator.

    Rules to follow:

    1. Think deeply about how to solve the task
    2. Always check your work before responding
    3. Read the info for the API calls to make sure you understand the requirements and arguments

    """


# Illustrator Blend Modes (for future use)
BLEND_MODES = [
    "ADD",
    "ALPHA_ADD",
    "CLASSIC_COLOR_BURN",
    "CLASSIC_COLOR_DODGE",
    "CLASSIC_DIFFERENCE",
    "COLOR",
    "COLOR_BURN",
    "COLOR_DODGE",
    "DANCING_DISSOLVE",
    "DARKEN",
    "DARKER_COLOR",
    "DIFFERENCE",
    "DISSOLVE",
    "EXCLUSION",
    "HARD_LIGHT",
    "HARD_MIX",
    "HUE",
    "LIGHTEN",
    "LIGHTER_COLOR",
    "LINEAR_BURN",
    "LINEAR_DODGE",
    "LINEAR_LIGHT",
    "LUMINESCENT_PREMUL",
    "LUMINOSITY",
    "MULTIPLY",
    "NORMAL",
    "OVERLAY",
    "PIN_LIGHT",
    "SATURATION",
    "SCREEN",
    "SILHOUETE_ALPHA",
    "SILHOUETTE_LUMA",
    "SOFT_LIGHT",
    "STENCIL_ALPHA",
    "STENCIL_LUMA",
    "SUBTRACT",
    "VIVID_LIGHT"
]
```

--------------------------------------------------------------------------------
/uxp/ps/commands/utils.js:
--------------------------------------------------------------------------------

```javascript
/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const { app, constants, core } = require("photoshop");
const fs = require("uxp").storage.localFileSystem;
const openfs = require('fs')


const convertFontSize = (fontSize) => {
    return (app.activeDocument.resolution / 72) * fontSize
}

const convertFromPhotoshopFontSize = (photoshopFontSize) => {
    return photoshopFontSize / (app.activeDocument.resolution / 72);
}

const createFile = async (filePath) => {
    let url = `file:${filePath}`
    const fd = await openfs.open(url, "a+");
    await openfs.close(fd)

    return url
}

const parseColor = (color) => {
    try {
        const c = new app.SolidColor();
        c.rgb.red = color.red;
        c.rgb.green = color.green;
        c.rgb.blue = color.blue;

        return c;
    } catch (e) {
        throw new Error(`Invalid color values: ${JSON.stringify(color)}`);
    }
};

const getAlignmentMode = (mode) => {
    switch (mode) {
        case "LEFT":
            return "ADSLefts";
        case "CENTER_HORIZONTAL":
            return "ADSCentersH";
        case "RIGHT":
            return "ADSRights";
        case "TOP":
            return "ADSTops";
        case "CENTER_VERTICAL":
            return "ADSCentersV";
        case "BOTTOM":
            return "ADSBottoms";
        default:
            throw new Error(
                `getAlignmentMode : Unknown alignment mode : ${mode}`
            );
    }
};

const getJustificationMode = (value) => {
    return getConstantValue(constants.Justification, value, "Justification");
};

const getBlendMode = (value) => {
    return getConstantValue(constants.BlendMode, value, "BlendMode");
};

const getInterpolationMethod = (value) => {
    return getConstantValue(
        constants.InterpolationMethod,
        value,
        "InterpolationMethod"
    );
};

const getAnchorPosition = (value) => {
    return getConstantValue(constants.AnchorPosition, value, "AnchorPosition");
};

const getNewDocumentMode = (value) => {
    return getConstantValue(
        constants.NewDocumentMode,
        value,
        "NewDocumentMode"
    );
};

const getConstantValue = (c, v, n) => {
    let out = c[v.toUpperCase()];

    if (!out) {
        throw new Error(`getConstantValue : Unknown constant value :${c} ${v}`);
    }

    return out;
};

const selectLayer = (layer, exclusive = false) => {
    if (exclusive) {
        clearLayerSelections();
    }

    layer.selected = true;
};

const clearLayerSelections = (layers) => {
    if (!layers) {
        layers = app.activeDocument.layers;
    }

    for (const layer of layers) {
        layer.selected = false;

        if (layer.layers && layer.layers.length > 0) {
            clearLayerSelections(layer.layers);
        }
    }
};

const setVisibleAllLayers = (visible, layers) => {
    if (!layers) {
        layers = app.activeDocument.layers;
    }

    for (const layer of layers) {
        layer.visible = visible

        if (layer.layers && layer.layers.length > 0) {
            setVisibleAllLayers(visible, layer.layers)
        }
    }
};


const findLayer = (id, layers) => {
    if (!layers) {
        layers = app.activeDocument.layers;
    }

    for (const layer of layers) {
        if (layer.id === id) {
            return layer;
        }

        if (layer.layers && layer.layers.length > 0) {
            const found = findLayer(id, layer.layers);
            if (found) {
                return found; // Stop as soon as we’ve found the target layer
            }
        }
    }

    return null;
};


const findLayerByName = (name, layers) => {
    if (!layers) {
        layers = app.activeDocument.layers;
    }

    return app.activeDocument.layers.getByName(name);
};

const _saveDocumentAs = async (filePath, fileType) => {

    let url = await createFile(filePath)

    let saveFile = await fs.getEntryWithUrl(url);

    return await execute(async () => {

        fileType = fileType.toUpperCase()
        if (fileType == "JPG") {
            await app.activeDocument.saveAs.jpg(saveFile, {
                quality:9
            }, true)
        } else if (fileType == "PNG") {
            await app.activeDocument.saveAs.png(saveFile, {
            }, true)
        } else {
            await app.activeDocument.saveAs.psd(saveFile, {
                alphaChannels:true,
                annotations:true,
                embedColorProfile:true,
                layers:true,
                maximizeCompatibility:true,
                spotColor:true,
            }, true)
        }

        return {savedFilePath:saveFile.nativePath}
    });
};

const execute = async (callback, commandName = "Executing command...") => {
    try {
        return await core.executeAsModal(callback, {
            commandName: commandName,
        });
    } catch (e) {
        throw new Error(`Error executing command [modal] : ${e}`);
    }
};

const tokenify = async (url) => {
    let out = await fs.createSessionToken(
        await fs.getEntryWithUrl("file:" + url)
    );
    return out;
};

const getElementPlacement = (placement) => {
    return constants.ElementPlacement[placement.toUpperCase()];
};

const hasActiveSelection = () => {
    return app.activeDocument.selection.bounds != null;
};

const getMostRecentlyModifiedFile = async (directoryPath)  => {
    try {
      // Get directory contents
      const dirEntries = await openfs.readdir(directoryPath);
      
      const fileDetails = [];
      
      // Process each file
      let i = 0
      for (const entry of dirEntries) {
        console.log(i++)
        const filePath = window.path.join(directoryPath, entry);
        
        // Get file stats using lstat
        try {
          const stats = await openfs.lstat(filePath);

          // Skip if it's a directory
          if (stats.isDirectory()) {
            continue;
          }
          
          fileDetails.push({
            name: entry,
            path: filePath,
            modifiedTime: stats.mtime,  // Date object
            modifiedTimestamp: stats.mtimeMs  // Use mtimeMs directly instead of getTime()
          });
        } catch (err) {
          console.log(`Error getting stats for ${filePath}:`, err);
          // Continue to next file if there's an error with this one
          continue;
        }
      }
      
      if (fileDetails.length === 0) {
        return null;
      }
      
      // Sort by modification timestamp (newest first)
      fileDetails.sort((a, b) => b.modifiedTimestamp - a.modifiedTimestamp);
      
      // Return the most recently modified file
      return fileDetails[0];
    } catch (err) {
      console.error('Error getting most recently modified file:', err);
      return null;
    }
  }

  const fileExists = async (filePath) => {
    try {
      await openfs.lstat(`file:${filePath}`);
      return true;
    } catch (error) {
        return false;
    }
  }

  const generateDocumentInfo = (document, activeDocument) => {
    return {
            name:document.name,
            id:document.id,
            isActive: document === activeDocument,
            path:document.path,
            saved:document.saved,
            title:document.title
        };
}

const listOpenDocuments = () => {
    const docs = app.documents;
    const activeDocument = app.activeDocument

    let out = []

    for (let doc of docs) {
        let d = generateDocumentInfo(doc, activeDocument)
        out.push(d)
    }

    return out
}

module.exports = {
    findLayerByName,
    generateDocumentInfo,
    listOpenDocuments,
    convertFromPhotoshopFontSize,
    convertFontSize,
    setVisibleAllLayers,
    _saveDocumentAs,
    getMostRecentlyModifiedFile,
    fileExists,
    createFile,
    parseColor,
    getAlignmentMode,
    getJustificationMode,
    getBlendMode,
    getInterpolationMethod,
    getAnchorPosition,
    getNewDocumentMode,
    getConstantValue,
    selectLayer,
    clearLayerSelections,
    findLayer,
    execute,
    tokenify,
    getElementPlacement,
    hasActiveSelection
}
```

--------------------------------------------------------------------------------
/uxp/ps/commands/selection.js:
--------------------------------------------------------------------------------

```javascript
const { app, constants, action } = require("photoshop");
const { 
    findLayer, 
    execute, 
    parseColor, 
    selectLayer 
} = require("./utils");

const {hasActiveSelection} = require("./utils")

const clearSelection = async () => {
    await app.activeDocument.selection.selectRectangle(
        { top: 0, left: 0, bottom: 0, right: 0 },
        constants.SelectionType.REPLACE,
        0,
        true
    );
};

const createMaskFromSelection = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `createMaskFromSelection : Could not find layerId : ${layerId}`
        );
    }

    await execute(async () => {
        selectLayer(layer, true);

        let commands = [
            {
                _obj: "make",
                at: {
                    _enum: "channel",
                    _ref: "channel",
                    _value: "mask",
                },
                new: {
                    _class: "channel",
                },
                using: {
                    _enum: "userMaskEnabled",
                    _value: "revealSelection",
                },
            },
        ];

        await action.batchPlay(commands, {});
    });
};

const selectSubject = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `selectSubject : Could not find layerId : ${layerId}`
        );
    }

    return await execute(async () => {
        selectLayer(layer, true);

        let commands = [
            // Select Subject
            {
                _obj: "autoCutout",
                sampleAllLayers: false,
            },
        ];

        await action.batchPlay(commands, {});
    });
};

const selectSky = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(`selectSky : Could not find layerId : ${layerId}`);
    }

    return await execute(async () => {
        selectLayer(layer, true);

        let commands = [
            // Select Sky
            {
                _obj: "selectSky",
                sampleAllLayers: false,
            },
        ];

        await action.batchPlay(commands, {});

    });
};

const cutSelectionToClipboard = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `cutSelectionToClipboard : Could not find layerId : ${layerId}`
        );
    }

    if (!hasActiveSelection()) {
        throw new Error(
            "cutSelectionToClipboard : Requires an active selection"
        );
    }

    return await execute(async () => {
        selectLayer(layer, true);

        let commands = [
            {
                _obj: "cut",
            },
        ];

        await action.batchPlay(commands, {});
    });
};

const copyMergedSelectionToClipboard = async (command) => {

    let options = command.options;

    if (!hasActiveSelection()) {
        throw new Error(
            "copySelectionToClipboard : Requires an active selection"
        );
    }

    return await execute(async () => {
        let commands = [{
            _obj: "copyMerged",
        }];

        await action.batchPlay(commands, {});
    });
};

const copySelectionToClipboard = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `copySelectionToClipboard : Could not find layerId : ${layerId}`
        );
    }

    if (!hasActiveSelection()) {
        throw new Error(
            "copySelectionToClipboard : Requires an active selection"
        );
    }

    return await execute(async () => {
        selectLayer(layer, true);

        let commands = [{
            _obj: "copyEvent",
            copyHint: "pixels",
        }];

        await action.batchPlay(commands, {});
    });
};

const pasteFromClipboard = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `pasteFromClipboard : Could not find layerId : ${layerId}`
        );
    }

    return await execute(async () => {
        selectLayer(layer, true);

        let pasteInPlace = options.pasteInPlace;

        let commands = [
            {
                _obj: "paste",
                antiAlias: {
                    _enum: "antiAliasType",
                    _value: "antiAliasNone",
                },
                as: {
                    _class: "pixel",
                },
                inPlace: pasteInPlace,
            },
        ];

        await action.batchPlay(commands, {});
    });
};

const deleteSelection = async (command) => {

    let options = command.options;
    let layerId = options.layerId;
    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `deleteSelection : Could not find layerId : ${layerId}`
        );
    }

    if (!app.activeDocument.selection.bounds) {
        throw new Error(`invertSelection : Requires an active selection`);
    }

    await execute(async () => {
        selectLayer(layer, true);
        let commands = [
            {
                _obj: "delete",
            },
        ];
        await action.batchPlay(commands, {});
    });
};

const fillSelection = async (command) => {

    let options = command.options;
    let layerId = options.layerId;
    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `fillSelection : Could not find layerId : ${layerId}`
        );
    }

    if (!app.activeDocument.selection.bounds) {
        throw new Error(`invertSelection : Requires an active selection`);
    }

    await execute(async () => {
        selectLayer(layer, true);

        let c = parseColor(options.color).rgb;
        let commands = [
            // Fill
            {
                _obj: "fill",
                color: {
                    _obj: "RGBColor",
                    blue: c.blue,
                    grain: c.green,
                    red: c.red,
                },
                mode: {
                    _enum: "blendMode",
                    _value: options.blendMode.toLowerCase(),
                },
                opacity: {
                    _unit: "percentUnit",
                    _value: options.opacity,
                },
                using: {
                    _enum: "fillContents",
                    _value: "color",
                },
            },
        ];
        await action.batchPlay(commands, {});
    });
};

const selectPolygon = async (command) => {

    let options = command.options;
    let layerId = options.layerId;
    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `selectPolygon : Could not find layerId : ${layerId}`
        );
    }

    await execute(async () => {

        selectLayer(layer, true);

        await app.activeDocument.selection.selectPolygon(
            options.points,
            constants.SelectionType.REPLACE,
            options.feather,
            options.antiAlias
        );
    });
};

let selectEllipse = async (command) => {

    let options = command.options;
    let layerId = options.layerId;
    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `selectEllipse : Could not find layerId : ${layerId}`
        );
    }

    await execute(async () => {

        selectLayer(layer, true);

        await app.activeDocument.selection.selectEllipse(
            options.bounds,
            constants.SelectionType.REPLACE,
            options.feather,
            options.antiAlias
        );
    });
};

const selectRectangle = async (command) => {
    let options = command.options;
    let layerId = options.layerId;
    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `selectRectangle : Could not find layerId : ${layerId}`
        );
    }

    await execute(async () => {
        selectLayer(layer, true);

        await app.activeDocument.selection.selectRectangle(
            options.bounds,
            constants.SelectionType.REPLACE,
            options.feather,
            options.antiAlias
        );
    });
};

const invertSelection = async (command) => {

    if (!app.activeDocument.selection.bounds) {
        throw new Error(`invertSelection : Requires an active selection`);
    }

    await execute(async () => {
        let commands = [
            {
                _obj: "inverse",
            },
        ];
        await action.batchPlay(commands, {});
    });
};

const commandHandlers = {
    clearSelection,
    createMaskFromSelection,
    selectSubject,
    selectSky,
    cutSelectionToClipboard,
    copyMergedSelectionToClipboard,
    copySelectionToClipboard,
    pasteFromClipboard,
    deleteSelection,
    fillSelection,
    selectPolygon,
    selectEllipse,
    selectRectangle,
    invertSelection
};

module.exports = {
    commandHandlers
};
```

--------------------------------------------------------------------------------
/uxp/ps/commands/layer_styles.js:
--------------------------------------------------------------------------------

```javascript
/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const { action } = require("photoshop");

const {
    selectLayer,
    findLayer,
    execute
} = require("./utils")

const addDropShadowLayerStyle = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `addDropShadowLayerStyle : Could not find layerId : ${layerId}`
        );
    }

    await execute(async () => {
        selectLayer(layer, true);

        let commands = [
            // Set Layer Styles of current layer
            {
                _obj: "set",
                _target: [
                    {
                        _property: "layerEffects",
                        _ref: "property",
                    },
                    {
                        _enum: "ordinal",
                        _ref: "layer",
                        _value: "targetEnum",
                    },
                ],
                to: {
                    _obj: "layerEffects",
                    dropShadow: {
                        _obj: "dropShadow",
                        antiAlias: false,
                        blur: {
                            _unit: "pixelsUnit",
                            _value: options.size,
                        },
                        chokeMatte: {
                            _unit: "pixelsUnit",
                            _value: options.spread,
                        },
                        color: {
                            _obj: "RGBColor",
                            blue: options.color.blue,
                            grain: options.color.green,
                            red: options.color.red,
                        },
                        distance: {
                            _unit: "pixelsUnit",
                            _value: options.distance,
                        },
                        enabled: true,
                        layerConceals: true,
                        localLightingAngle: {
                            _unit: "angleUnit",
                            _value: options.angle,
                        },
                        mode: {
                            _enum: "blendMode",
                            _value: options.blendMode.toLowerCase(),
                        },
                        noise: {
                            _unit: "percentUnit",
                            _value: 0.0,
                        },
                        opacity: {
                            _unit: "percentUnit",
                            _value: options.opacity,
                        },
                        present: true,
                        showInDialog: true,
                        transferSpec: {
                            _obj: "shapeCurveType",
                            name: "Linear",
                        },
                        useGlobalAngle: true,
                    },
                    globalLightingAngle: {
                        _unit: "angleUnit",
                        _value: options.angle,
                    },
                    scale: {
                        _unit: "percentUnit",
                        _value: 100.0,
                    },
                },
            },
        ];

        await action.batchPlay(commands, {});
    });
};

const addStrokeLayerStyle = async (command) => {
    const options = command.options

    const layerId = options.layerId

    let layer = findLayer(layerId)

    if (!layer) {
        throw new Error(
            `addStrokeLayerStyle : Could not find layerId : ${layerId}`
        );
    }

    let position = "centeredFrame"

    if (options.position == "INSIDE") {
        position = "insetFrame"
    } else if (options.position == "OUTSIDE") {
        position = "outsetFrame"
    }


    await execute(async () => {
        selectLayer(layer, true);

        let strokeColor = options.color
        let commands = [
            // Set Layer Styles of current layer
            {
                "_obj": "set",
                "_target": [
                    {
                        "_property": "layerEffects",
                        "_ref": "property"
                    },
                    {
                        "_enum": "ordinal",
                        "_ref": "layer",
                        "_value": "targetEnum"
                    }
                ],
                "to": {
                    "_obj": "layerEffects",
                    "frameFX": {
                        "_obj": "frameFX",
                        "color": {
                            "_obj": "RGBColor",
                            "blue": strokeColor.blue,
                            "grain": strokeColor.green,
                            "red": strokeColor.red
                        },
                        "enabled": true,
                        "mode": {
                            "_enum": "blendMode",
                            "_value": options.blendMode.toLowerCase()
                        },
                        "opacity": {
                            "_unit": "percentUnit",
                            "_value": options.opacity
                        },
                        "overprint": false,
                        "paintType": {
                            "_enum": "frameFill",
                            "_value": "solidColor"
                        },
                        "present": true,
                        "showInDialog": true,
                        "size": {
                            "_unit": "pixelsUnit",
                            "_value": options.size
                        },
                        "style": {
                            "_enum": "frameStyle",
                            "_value": position
                        }
                    },
                    "scale": {
                        "_unit": "percentUnit",
                        "_value": 100.0
                    }
                }
            }
        ];

        await action.batchPlay(commands, {});
    });
}

const createGradientLayerStyle = async (command) => {

    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `createGradientAdjustmentLayer : Could not find layerId : ${layerId}`
        );
    }

    await execute(async () => {
        selectLayer(layer, true);

        let angle = options.angle;
        let colorStops = options.colorStops;
        let opacityStops = options.opacityStops;

        let colors = [];
        for (let c of colorStops) {
            colors.push({
                _obj: "colorStop",
                color: {
                    _obj: "RGBColor",
                    blue: c.color.blue,
                    grain: c.color.green,
                    red: c.color.red,
                },
                location: Math.round((c.location / 100) * 4096),
                midpoint: c.midpoint,
                type: {
                    _enum: "colorStopType",
                    _value: "userStop",
                },
            });
        }

        let opacities = [];
        for (let o of opacityStops) {
            opacities.push({
                _obj: "transferSpec",
                location: Math.round((o.location / 100) * 4096),
                midpoint: o.midpoint,
                opacity: {
                    _unit: "percentUnit",
                    _value: o.opacity,
                },
            });
        }

        let commands = [
            // Make fill layer
            {
                _obj: "make",
                _target: [
                    {
                        _ref: "contentLayer",
                    },
                ],
                using: {
                    _obj: "contentLayer",
                    type: {
                        _obj: "gradientLayer",
                        angle: {
                            _unit: "angleUnit",
                            _value: angle,
                        },
                        gradient: {
                            _obj: "gradientClassEvent",
                            colors: colors,
                            gradientForm: {
                                _enum: "gradientForm",
                                _value: "customStops",
                            },
                            interfaceIconFrameDimmed: 4096.0,
                            name: "Custom",
                            transparency: opacities,
                        },
                        gradientsInterpolationMethod: {
                            _enum: "gradientInterpolationMethodType",
                            _value: "smooth",
                        },
                        type: {
                            _enum: "gradientType",
                            _value: options.type.toLowerCase(),
                        },
                    },
                },
            },
        ];

        await action.batchPlay(commands, {});
    });
};



const commandHandlers = {
    createGradientLayerStyle,
    addStrokeLayerStyle,
    addDropShadowLayerStyle
};

module.exports = {
    commandHandlers
};
```

--------------------------------------------------------------------------------
/uxp/pr/commands/utils.js:
--------------------------------------------------------------------------------

```javascript
/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const app = require("premierepro");
const { TRACK_TYPE, TICKS_PER_SECOND } = require("./consts.js");

const _getSequenceFromId = async (id) => {
    let project = await app.Project.getActiveProject();

    let guid = app.Guid.fromString(id);
    let sequence = await project.getSequence(guid);

    if (!sequence) {
        throw new Error(
            `_getSequenceFromId : Could not find sequence with id : ${id}`
        );
    }

    return sequence;
};

const _setActiveSequence = async (sequence) => {
    let project = await app.Project.getActiveProject();
    await project.setActiveSequence(sequence);

    let item = await findProjectItem(sequence.name, project);
    await app.SourceMonitor.openProjectItem(item);
};

const setParam = async (trackItem, componentName, paramName, value) => {
    const project = await app.Project.getActiveProject();

    let param = await getParam(trackItem, componentName, paramName);

    let keyframe = await param.createKeyframe(value);

    execute(() => {
        let action = param.createSetValueAction(keyframe);
        return [action];
    }, project);
};

const getParam = async (trackItem, componentName, paramName) => {
    let components = await trackItem.getComponentChain();

    const count = components.getComponentCount();
    for (let i = 0; i < count; i++) {
        const component = components.getComponentAtIndex(i);

        //search for match name
        //component name AE.ADBE Opacity
        const matchName = await component.getMatchName();

        if (matchName == componentName) {
            console.log(matchName);
            let pCount = component.getParamCount();

            for (let j = 0; j < pCount; j++) {
                const param = component.getParam(j);

                console.log(param.type);
                console.log(param);
                if (param.displayName == paramName) {
                    return param;
                }
            }
        }
    }
};

const addEffect = async (trackItem, effectName) => {
    let project = await app.Project.getActiveProject();
    const effect = await app.VideoFilterFactory.createComponent(effectName);

    let componentChain = await trackItem.getComponentChain();

    execute(() => {
        let action = componentChain.createAppendComponentAction(effect, 0); //todo, second isnt needed
        return [action];
    }, project);
};

/*
const findProjectItem2 = async (itemName, project) => {
    let root = await project.getRootItem();
    let rootItems = await root.getItems();

    let insertItem;
    for (const item of rootItems) {
        if (item.name == itemName) {
            insertItem = item;
            break;
        }
    }

    if (!insertItem) {
        throw new Error(
            `addItemToSequence : Could not find item named ${itemName}`
        );
    }

    return insertItem;
};
*/

const findProjectItem = async (itemName, project) => {
    let root = await project.getRootItem();
    
    const searchItems = async (parentItem) => {
        let items = await parentItem.getItems();
        
        // First, check items at this level
        for (const item of items) {
            if (item.name === itemName) {
                return item;
            }
        }
        
        // If not found, search recursively in bins/folders
        for (const item of items) {
            const folderItem = app.FolderItem.cast(item);
            if (folderItem) {
                // This is a bin/folder, search inside it
                const foundItem = await searchItems(folderItem);
                if (foundItem) {
                    return foundItem;
                }
            }
        }
        
        return null; // Not found at this level or in any sub-folders
    };
    
    const insertItem = await searchItems(root);
    
    if (!insertItem) {
        throw new Error(
            `addItemToSequence : Could not find item named ${itemName}`
        );
    }

    return insertItem;
};


const execute = (getActions, project) => {
    try {
        project.lockedAccess(() => {
            project.executeTransaction((compoundAction) => {
                let actions = getActions();

                for (const a of actions) {
                    compoundAction.addAction(a);
                }
            });
        });
    } catch (e) {
        throw new Error(`Error executing locked transaction : ${e}`);
    }
};

const getTracks = async (sequence, trackType) => {
    let count;

    if (trackType === TRACK_TYPE.VIDEO) {
        count = await sequence.getVideoTrackCount();
    } else if (trackType === TRACK_TYPE.AUDIO) {
        count = await sequence.getAudioTrackCount();
    }

    let tracks = [];
    for (let i = 0; i < count; i++) {
        let track;

        if (trackType === TRACK_TYPE.VIDEO) {
            track = await sequence.getVideoTrack(i);
        } else if (trackType === TRACK_TYPE.AUDIO) {
            track = await sequence.getAudioTrack(i);
        }

        let out = {
            index: i,
            tracks: [],
        };

        let clips = await track.getTrackItems(1, false);

        if (clips.length === 0) {
            continue;
        }

        let k = 0;
        for (const c of clips) {
            let startTimeTicks = (await c.getStartTime()).ticks;
            let endTimeTicks = (await c.getEndTime()).ticks;
            let durationTicks = (await c.getDuration()).ticks;
            let durationSeconds = (await c.getDuration()).seconds;
            let name = (await c.getProjectItem()).name;
            let type = await c.getType();
            let index = k++;

            out.tracks.push({
                startTimeTicks,
                endTimeTicks,
                durationTicks,
                durationSeconds,
                name,
                type,
                index,
            });
        }

        tracks.push(out);
    }
    return tracks;
};

const getSequences = async () => {
    let project = await app.Project.getActiveProject();
    let active = await project.getActiveSequence();

    let sequences = await project.getSequences();

    let out = [];
    for (const sequence of sequences) {
        let size = await sequence.getFrameSize();
        //let settings = await sequence.getSettings()

        //let projectItem = await sequence.getProjectItem()
        //let name = projectItem.name
        let name = sequence.name;
        let id = sequence.guid.toString();

        let videoTracks = await getTracks(sequence,TRACK_TYPE.VIDEO);
        let audioTracks = await getTracks(sequence, TRACK_TYPE.AUDIO);

        let isActive = active == sequence;


        let timebase = await sequence.getTimebase()
        let fps = TICKS_PER_SECOND / timebase

        let endTime = await sequence.getEndTime()
        let durationSeconds = await endTime.seconds
        let durationTicks = await endTime.ticksNumber
        let ticksPerSecond = TICKS_PER_SECOND

        out.push({
            isActive,
            name,
            id,
            frameSize: { width: size.width, height: size.height },
            videoTracks,
            audioTracks,
            timebase,
            fps,
            durationSeconds,
            durationTicks,
            ticksPerSecond
        });
    }

    return out;
};

const getTrack = async (sequence, trackIndex, clipIndex, trackType) => {
    let trackItems = await getTrackItems(sequence, trackIndex, trackType);

    let trackItem;
    let i = 0;
    for (const t of trackItems) {
        let index = i++;
        if (index === clipIndex) {
            trackItem = t;
            break;
        }
    }
    if (!trackItem) {
        throw new Error(
            `getTrack : trackItemIndex [${clipIndex}] does not exist for track type [${trackType}]`
        );
    }

    return trackItem;
};

/*
const getAudioTrack = async (sequence, trackIndex, clipIndex) => {

    let trackItems = await getAudioTrackItems(sequence, trackIndex)

    let trackItem;
    let i = 0
    for(const t of trackItems) {
        let index = i++
        if(index === clipIndex) {
            trackItem = t
            break
        }
    }
    if(!trackItem) {
        throw new Error(`getAudioTrack : trackItemIndex [${clipIndex}] does not exist`)
    }

    return trackItem
}
    */

const getTrackItems = async (sequence, trackIndex, trackType) => {
    let track;

    if (trackType === TRACK_TYPE.AUDIO) {
        track = await sequence.getAudioTrack(trackIndex);
    } else if (trackType === TRACK_TYPE.VIDEO) {
        track = await sequence.getVideoTrack(trackIndex);
    }

    if (!track) {
        throw new Error(
            `getTrackItems : getTrackItems [${trackIndex}] does not exist. Type : [${trackType}]`
        );
    }

    let trackItems = await track.getTrackItems(1, false);

    return trackItems;
};

/*
const getAudioTrackItems = async (sequence, trackIndex) => {
    let audioTrack = await sequence.getAudioTrack(trackIndex)
 
    if(!audioTrack) {
        throw new Error(`getAudioTrackItems : getAudioTrackItems [${trackIndex}] does not exist`)
    }

    let trackItems = await audioTrack.getTrackItems(1, false)

    return trackItems
}

const getVideoTrackItems = async (sequence, trackIndex) => {
    let videoTrack = await sequence.getVideoTrack(trackIndex)
 
    if(!videoTrack) {
        throw new Error(`getVideoTrackItems : videoTrackIndex [${trackIndex}] does not exist`)
    }

    let trackItems = await videoTrack.getTrackItems(1, false)

    return trackItems
}
*/
/*
const getVideoTrack = async (sequence, trackIndex, clipIndex) => {

    let trackItems = await getVideoTrackItems(sequence, trackIndex)

    let trackItem;
    let i = 0
    for(const t of trackItems) {
        let index = i++
        if(index === clipIndex) {
            trackItem = t
            break
        }
    }
    if(!trackItem) {
        throw new Error(`getVideoTrack : clipIndex [${clipIndex}] does not exist`)
    }

    return trackItem
}
    */

module.exports = {
    getTrackItems,
    _getSequenceFromId,
    _setActiveSequence,
    setParam,
    getParam,
    addEffect,
    findProjectItem,
    execute,
    getTracks,
    getSequences,
    getTrack,
};

```

--------------------------------------------------------------------------------
/cep/com.mikechambers.ai/commands.js:
--------------------------------------------------------------------------------

```javascript
/* commands.js
 * Illustrator command handlers
 */


const getDocuments = async (command) => {
    const script = `
        (function() {
            try {
                var result = (function() {
                    if (app.documents.length > 0) {
                        var activeDoc = app.activeDocument;
                        var docs = [];
                        
                        for (var i = 0; i < app.documents.length; i++) {
                            var doc = app.documents[i];
                            docs.push($.global.createDocumentInfo(doc, activeDoc));
                        }
                        
                        return docs;
                    } else {
                        return [];
                    }
                })();
                
                if (result === undefined) {
                    return 'null';
                }
                
                return JSON.stringify(result);
            } catch(e) {
                return JSON.stringify({
                    error: e.toString(),
                    line: e.line || 'unknown'
                });
            }
        })();
    `;
    
    let result = await executeCommand(script);
    return createPacket(result);
}

const exportPNG = async (command) => {
    const options = command.options || {};
    
    // Extract all options into variables
    const path = options.path;
    const transparency = options.transparency !== undefined ? options.transparency : true;
    const antiAliasing = options.antiAliasing !== undefined ? options.antiAliasing : true;
    const artBoardClipping = options.artBoardClipping !== undefined ? options.artBoardClipping : true;
    const horizontalScale = options.horizontalScale || 100;
    const verticalScale = options.verticalScale || 100;
    const exportType = options.exportType || 'PNG24';
    const matte = options.matte;
    const matteColor = options.matteColor;
    
    // Validate required path parameter
    if (!path) {
        return createPacket(JSON.stringify({
            error: "Path is required for PNG export"
        }));
    }
    
    const script = `
        (function() {
            try {
                var result = (function() {
                    if (app.documents.length === 0) {
                        return { error: "No document is currently open" };
                    }
                    
                    var doc = app.activeDocument;
                    var exportPath = "${path}";
                    
                    // Export options from variables
                    var exportOptions = {
                        transparency: ${transparency},
                        antiAliasing: ${antiAliasing},
                        artBoardClipping: ${artBoardClipping},
                        horizontalScale: ${horizontalScale},
                        verticalScale: ${verticalScale},
                        exportType: "${exportType}"
                    };
                    
                    ${matte !== undefined ? `exportOptions.matte = ${matte};` : ''}
                    ${matteColor ? `exportOptions.matteColor = ${JSON.stringify(matteColor)};` : ''}
                    
                    // Use the global helper function if available, otherwise inline export
                    if (typeof $.global.exportToPNG === 'function') {
                        return $.global.exportToPNG(doc, exportPath, exportOptions);
                    } else {
                        // Inline export logic
                        try {
                            // Create PNG export options
                            var pngOptions = exportOptions.exportType === 'PNG8' ? 
                                new ExportOptionsPNG8() : new ExportOptionsPNG24();
                                
                            pngOptions.transparency = exportOptions.transparency;
                            pngOptions.antiAliasing = exportOptions.antiAliasing;
                            pngOptions.artBoardClipping = exportOptions.artBoardClipping;
                            pngOptions.horizontalScale = exportOptions.horizontalScale;
                            pngOptions.verticalScale = exportOptions.verticalScale;
                            
                            ${matte !== undefined ? `pngOptions.matte = ${matte};` : ''}
                            
                            ${matteColor ? `
                            // Set matte color
                            pngOptions.matteColor.red = ${matteColor.red};
                            pngOptions.matteColor.green = ${matteColor.green};
                            pngOptions.matteColor.blue = ${matteColor.blue};
                            ` : ''}
                            
                            // Create file object
                            var exportFile = new File(exportPath);
                            
                            // Determine export type
                            var exportType = exportOptions.exportType === 'PNG8' ? 
                                ExportType.PNG8 : ExportType.PNG24;
                            
                            // Export the file
                            doc.exportFile(exportFile, exportType, pngOptions);
                            
                            return {
                                success: true,
                                filePath: exportFile.fsName,
                                fileExists: exportFile.exists,
                                options: exportOptions,
                                documentName: doc.name
                            };
                            
                        } catch(exportError) {
                            return {
                                success: false,
                                error: exportError.toString(),
                                filePath: exportPath,
                                options: exportOptions,
                                documentName: doc.name
                            };
                        }
                    }
                })();
                
                if (result === undefined) {
                    return 'null';
                }
                
                return JSON.stringify(result);
            } catch(e) {
                return JSON.stringify({
                    error: e.toString(),
                    line: e.line || 'unknown'
                });
            }
        })();
    `;
    
    let result = await executeCommand(script);
    return createPacket(result);
}

const openFile = async (command) => {
    const options = command.options || {};
    
    // Extract path parameter
    const path = options.path;
    
    // Validate required path parameter
    if (!path) {
        return createPacket(JSON.stringify({
            error: "Path is required to open an Illustrator file"
        }));
    }
    
    const script = `
        (function() {
            try {
                var result = (function() {
                    var filePath = "${path}";
                    
                    try {
                        // Create file object
                        var fileToOpen = new File(filePath);
                        
                        // Check if file exists
                        if (!fileToOpen.exists) {
                            return {
                                success: false,
                                error: "File does not exist at the specified path",
                                filePath: filePath
                            };
                        }
                        
                        // Open the document
                        var doc = app.open(fileToOpen);
                        
                        return {
                            success: true,
                        };
                        
                    } catch(openError) {
                        return {
                            success: false,
                            error: openError.toString(),
                            filePath: filePath
                        };
                    }
                })();
                
                if (result === undefined) {
                    return 'null';
                }
                
                return JSON.stringify(result);
            } catch(e) {
                return JSON.stringify({
                    error: e.toString(),
                    line: e.line || 'unknown'
                });
            }
        })();
    `;
    
    let result = await executeCommand(script);
    return createPacket(result);
};

const getActiveDocumentInfo = async (command) => {
    const script = `
        (function() {
            try {
                var result = (function() {
                    if (app.documents.length > 0) {
                        var doc = app.activeDocument;
                        return $.global.createDocumentInfo(doc, doc);
                    } else {
                        return { error: "No document is currently open" };
                    }
                })();
                
                if (result === undefined) {
                    return 'null';
                }
                
                return JSON.stringify(result);
            } catch(e) {
                return JSON.stringify({
                    error: e.toString(),
                    line: e.line || 'unknown'
                });
            }
        })();
    `;
    
    let result = await executeCommand(script);
    return createPacket(result);
}

// Execute Illustrator command via ExtendScript
function executeCommand(script) {
    return new Promise((resolve, reject) => {
        const csInterface = new CSInterface();
        csInterface.evalScript(script, (result) => {
            if (result === 'EvalScript error.') {
                reject(new Error('ExtendScript execution failed'));
            } else {
                try {
                    resolve(JSON.parse(result));
                } catch (e) {
                    resolve(result);
                }
            }
        });
    });
}


async function executeExtendScript(command) {
    const options = command.options
    const scriptString = options.scriptString;

    const script = `
        (function() {
            try {
                ${scriptString}
            } catch(e) {
                return JSON.stringify({
                    error: e.toString(),
                    line: e.line || 'unknown'
                });
            }
        })();
    `;
    
    const result = await executeCommand(script);
    
    return createPacket(result);
}

const createPacket = (result) => {
    return {
        content: [{
            type: "text",
            text: JSON.stringify(result, null, 2)
        }]
    };
}

const parseAndRouteCommand = async (command) => {
    let action = command.action;

    let f = commandHandlers[action];

    if (typeof f !== "function") {
        throw new Error(`Unknown Command: ${action}`);
    }

    console.log(f.name)
    return await f(command);
};


// Execute commands
/*
async function executeCommand(command) {
    switch(command.action) {

        case "getLayers":
            return await getLayers();
        
        case "executeExtendScript":
            return await executeExtendScript(command);
        
        default:
            throw new Error(`Unknown command: ${command.action}`);
    }
}*/

const commandHandlers = {
    executeExtendScript,
    getDocuments,
    getActiveDocumentInfo,
    exportPNG,
    openFile
};
```

--------------------------------------------------------------------------------
/uxp/ps/commands/core.js:
--------------------------------------------------------------------------------

```javascript
/* MIT License
 *
 * Copyright (c) 2025 Mike Chambers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const { app, constants, action, imaging } = require("photoshop");
const fs = require("uxp").storage.localFileSystem;

const {
    _saveDocumentAs,
    parseColor,
    getAlignmentMode,
    getNewDocumentMode,
    selectLayer,
    findLayer,
    findLayerByName,
    execute,
    tokenify,
    hasActiveSelection,
    listOpenDocuments
} = require("./utils");

const { rasterizeLayer } = require("./layers").commandHandlers;

const openFile = async (command) => {
    let options = command.options;

    await execute(async () => {
        let entry = null;
        try {
            entry = await fs.getEntryWithUrl("file:" + options.filePath);
        } catch (e) {
            throw new Error(
                "openFile: Could not create file entry. File probably does not exist."
            );
        }

        await app.open(entry);
    });
};

const placeImage = async (command) => {
    let options = command.options;
    let layerId = options.layerId;
    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(`placeImage : Could not find layerId : ${layerId}`);
    }

    await execute(async () => {
        selectLayer(layer, true);
        let layerId = layer.id;

        let imagePath = await tokenify(options.imagePath);

        let commands = [
            // Place
            {
                ID: layerId,
                _obj: "placeEvent",
                freeTransformCenterState: {
                    _enum: "quadCenterState",
                    _value: "QCSAverage",
                },
                null: {
                    _kind: "local",
                    _path: imagePath,
                },
                offset: {
                    _obj: "offset",
                    horizontal: {
                        _unit: "pixelsUnit",
                        _value: 0.0,
                    },
                    vertical: {
                        _unit: "pixelsUnit",
                        _value: 0.0,
                    },
                },
                replaceLayer: {
                    _obj: "placeEvent",
                    to: {
                        _id: layerId,
                        _ref: "layer",
                    },
                },
            },
            {
                _obj: "set",
                _target: [
                    {
                        _enum: "ordinal",
                        _ref: "layer",
                        _value: "targetEnum",
                    },
                ],
                to: {
                    _obj: "layer",
                    name: layerId,
                },
            },
        ];

        await action.batchPlay(commands, {});
        await rasterizeLayer(command);
    });
};

const getDocumentImage = async (command) => {
    let out = await execute(async () => {

        const pixelsOpt = {
            applyAlpha: true
        };

        const imgObj = await imaging.getPixels(pixelsOpt);

        const base64Data = await imaging.encodeImageData({
            imageData: imgObj.imageData,
            base64: true,
        });

        const result = {
            base64Image: base64Data,
            dataUrl: `data:image/jpeg;base64,${base64Data}`,
            width: imgObj.imageData.width,
            height: imgObj.imageData.height,
            colorSpace: imgObj.imageData.colorSpace,
            components: imgObj.imageData.components,
            format: "jpeg",
        };

        imgObj.imageData.dispose();
        return result;
    });

    return out;
};

const getDocumentInfo = async (command) => {
    let doc = app.activeDocument;
    let path = doc.path;

    let out = {
        height: doc.height,
        width: doc.width,
        colorMode: doc.mode.toString(),
        pixelAspectRatio: doc.pixelAspectRatio,
        resolution: doc.resolution,
        path: path,
        saved: path.length > 0,
        hasUnsavedChanges: !doc.saved,
    };

    return out;
};

const cropDocument = async (command) => {
    let options = command.options;

    if (!hasActiveSelection()) {
        throw new Error("cropDocument : Requires an active selection");
    }

    return await execute(async () => {
        let commands = [
            // Crop
            {
                _obj: "crop",
                delete: true,
            },
        ];

        await action.batchPlay(commands, {});
    });
};

const removeBackground = async (command) => {
    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `removeBackground : Could not find layerId : ${layerId}`
        );
    }

    await execute(async () => {
        selectLayer(layer, true);

        let commands = [
            // Remove Background
            {
                _obj: "removeBackground",
            },
        ];

        await action.batchPlay(commands, {});
    });
};

const alignContent = async (command) => {
    let options = command.options;
    let layerId = options.layerId;

    let layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `alignContent : Could not find layerId : ${layerId}`
        );
    }

    if (!app.activeDocument.selection.bounds) {
        throw new Error(`alignContent : Requires an active selection`);
    }

    await execute(async () => {
        let m = getAlignmentMode(options.alignmentMode);

        selectLayer(layer, true);

        let commands = [
            {
                _obj: "align",
                _target: [
                    {
                        _enum: "ordinal",
                        _ref: "layer",
                        _value: "targetEnum",
                    },
                ],
                alignToCanvas: false,
                using: {
                    _enum: "alignDistributeSelector",
                    _value: m,
                },
            },
        ];
        await action.batchPlay(commands, {});
    });
};

const generateImage = async (command) => {
    let options = command.options;

    await execute(async () => {
        let doc = app.activeDocument;

        await doc.selection.selectAll();

        let contentType = "none";
        const c = options.contentType.toLowerCase()
        if (c === "photo" || c === "art") {
            contentType = c;
        }

        let commands = [
            // Generate Image current document
            {
                _obj: "syntheticTextToImage",
                _target: [
                    {
                        _enum: "ordinal",
                        _ref: "document",
                        _value: "targetEnum",
                    },
                ],
                documentID: doc.id,
                layerID: 0,
                prompt: options.prompt,
                serviceID: "clio",
                serviceOptionsList: {
                    clio: {
                        _obj: "clio",
                        clio_advanced_options: {
                            text_to_image_styles_options: {
                                text_to_image_content_type: contentType,
                                text_to_image_effects_count: 0,
                                text_to_image_effects_list: [
                                    "none",
                                    "none",
                                    "none",
                                ],
                            },
                        },
                        dualCrop: true,
                        gentech_workflow_name: "text_to_image",
                        gi_ADVANCED: '{"enable_mts":true}',
                        gi_CONTENT_PRESERVE: 0,
                        gi_CROP: false,
                        gi_DILATE: false,
                        gi_ENABLE_PROMPT_FILTER: true,
                        gi_GUIDANCE: 6,
                        gi_MODE: "ginp",
                        gi_NUM_STEPS: -1,
                        gi_PROMPT: options.prompt,
                        gi_SEED: -1,
                        gi_SIMILARITY: 0,
                    },
                },
                workflow: "text_to_image",
                workflowType: {
                    _enum: "genWorkflow",
                    _value: "text_to_image",
                },
            },
            // Rasterize current layer
            {
                _obj: "rasterizeLayer",
                _target: [
                    {
                        _enum: "ordinal",
                        _ref: "layer",
                        _value: "targetEnum",
                    },
                ],
            },
        ];
        let o = await action.batchPlay(commands, {});
        let layerId = o[0].layerID;

        //let l = findLayerByName(options.prompt);
        let l = findLayer(layerId);
        l.name = options.layerName;
    });
};

const generativeFill = async (command) => {
    const options = command.options;
    const layerId = options.layerId;
    const prompt = options.prompt;

    const layer = findLayer(layerId);

    if (!layer) {
        throw new Error(
            `generativeFill : Could not find layerId : ${layerId}`
        );
    }

    if(!hasActiveSelection()) {
        throw new Error(
            `generativeFill : Requires an active selection.`
        ); 
    }

    await execute(async () => {
        let doc = app.activeDocument;

        let contentType = "none";
        const c = options.contentType.toLowerCase()
        if (c === "photo" || c === "art") {
            contentType = c;
        }

        let commands = [
            // Generative Fill current document
            {
                "_obj": "syntheticFill",
                "_target": [
                    {
                        "_enum": "ordinal",
                        "_ref": "document",
                        "_value": "targetEnum"
                    }
                ],
                "documentID": doc.id,
                "layerID": layerId,
                "prompt": prompt,
                "serviceID": "clio",
                "serviceOptionsList": {
                    "clio": {
                        "_obj": "clio",
                        "dualCrop": true,
                        "gi_ADVANCED": "{\"enable_mts\":true}",
                        "gi_CONTENT_PRESERVE": 0,
                        "gi_CROP": false,
                        "gi_DILATE": false,
                        "gi_ENABLE_PROMPT_FILTER": true,
                        "gi_GUIDANCE": 6,
                        "gi_MODE": "tinp",
                        "gi_NUM_STEPS": -1,
                        "gi_PROMPT": prompt,
                        "gi_SEED": -1,
                        "gi_SIMILARITY": 0,


                        clio_advanced_options: {
                            text_to_image_styles_options: {
                                text_to_image_content_type: contentType,
                                text_to_image_effects_count: 0,
                                text_to_image_effects_list: [
                                    "none",
                                    "none",
                                    "none",
                                ],
                            },
                        },

                    }
                },
                "serviceVersion": "clio3",
                "workflowType": {
                    "_enum": "genWorkflow",
                    "_value": "in_painting"
                },
                "workflow_to_active_service_identifier_map": {
                    "gen_harmonize": "clio3",
                    "generate_background": "clio3",
                    "generate_similar": "clio3",
                    "generativeUpscale": "fal_aura_sr",
                    "in_painting": "clio3",
                    "instruct_edit": "clio3",
                    "out_painting": "clio3",
                    "text_to_image": "clio3"
                }
            }
        ];


        let o = await action.batchPlay(commands, {});
        let id = o[0].layerID;

        //let l = findLayerByName(options.prompt);
        let l = findLayer(id);
        l.name = options.layerName;
    });
};

const saveDocument = async (command) => {
    await execute(async () => {
        await app.activeDocument.save();
    });
};

const saveDocumentAs = async (command) => {
    let options = command.options;

    return await _saveDocumentAs(options.filePath, options.fileType);
};

const setActiveDocument = async (command) => {

    let options = command.options;
    let documentId = options.documentId;
    let docs = listOpenDocuments();

    for (let doc of docs) {
        if (doc.id === documentId) {
            await execute(async () => {
                app.activeDocument = doc;
            });

            return
        }
    }
}

const getDocuments = async (command) => {
    return listOpenDocuments()
}

const duplicateDocument = async (command) => {
    let options = command.options;
    let name = options.name

    await execute(async () => {
        const doc = app.activeDocument;
        await doc.duplicate(name)
    });
};

const createDocument = async (command) => {
    let options = command.options;
    let colorMode = getNewDocumentMode(command.options.colorMode);
    let fillColor = parseColor(options.fillColor);

    await execute(async () => {
        await app.createDocument({
            typename: "DocumentCreateOptions",
            width: options.width,
            height: options.height,
            resolution: options.resolution,
            mode: colorMode,
            fill: constants.DocumentFill.COLOR,
            fillColor: fillColor,
            profile: "sRGB IEC61966-2.1",
        });

        let background = findLayerByName("Background");
        background.allLocked = false;
        background.name = "Background";
    });
};

const executeBatchPlayCommand = async (commands) => {
    let options = commands.options;
    let c = options.commands;



    let out = await execute(async () => {
        let o = await action.batchPlay(c, {});
        return o[0]
    });

    console.log(out)
    return out;
}

const commandHandlers = {
    generativeFill,
    executeBatchPlayCommand,
    setActiveDocument,
    getDocuments,
    duplicateDocument,
    getDocumentImage,
    openFile,
    placeImage,
    getDocumentInfo,
    cropDocument,
    removeBackground,
    alignContent,
    generateImage,
    saveDocument,
    saveDocumentAs,
    createDocument,
};

module.exports = {
    commandHandlers,
};

```
Page 1/5FirstPrevNextLast