indesign-nutria-mcp
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="media/hero.svg">
<img src="media/hero.svg" width="100%" alt="indesign-nutria-mcp">
</picture>
</p>
<p align="center">
<b>Say "Create an A4 document with 5 pages, add a red circle on page 3" β and it happens.</b><br>
<i>The most comprehensive MCP server for Adobe InDesign. 197 tools. 34 handlers. Full DOM coverage.</i>
</p>
<p align="center">
<a href="media/demo.mp4">
<img src="media/social-preview.png" width="600" alt="Demo video" style="border-radius: 12px; border: 1px solid #312e81;">
</a>
<br>
<sub>β‘ Click for a quick demo Β· <a href="https://github.com/nutriandrea/adobe-indesign-mcp/releases/tag/v1.1.0">v1.1.0 release</a></sub>
</p>
<p align="center">
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-7c3aed?style=flat" alt="license"></a>
<a href="https://www.npmjs.com/package/indesign-nutria-mcp"><img src="https://img.shields.io/npm/v/indesign-nutria-mcp?style=flat" alt="npm"></a>
<a href="#"><img src="https://img.shields.io/badge/tools-197-7c3aed?style=flat" alt="tools"></a>
<a href="#"><img src="https://img.shields.io/badge/tests-1131-22c55e?style=flat" alt="tests"></a>
<a href="https://github.com/nutriandrea/adobe-indesign-mcp/actions/workflows/ci.yml/badge.svg"><img src="https://github.com/nutriandrea/adobe-indesign-mcp/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18-339933?style=flat&logo=node.js" alt="node"></a>
<a href="https://www.adobe.com/products/indesign.html"><img src="https://img.shields.io/badge/InDesign-2022%2B-007396?style=flat&logo=adobe" alt="indesign"></a>
</p>
> **Published to npm** as
> [`indesign-nutria-mcp`](https://www.npmjs.com/package/indesign-nutria-mcp).
> Your agent starts it with `npx -y indesign-nutria-mcp` β see
> [Quick Start](#-quick-start-30-seconds). InDesign still runs on your machine:
> the server reaches it over a local bridge, and nothing leaves the box.
---
## π Why This Exists
**InDesign automation has been broken for 20 years.** ExtendScript is ancient. CEP panels are over-engineered. UXP scripting requires a CS degree. You just want AI to do the layout.
This MCP server fixes that. Clone it, point your agent at it, and InDesign is
yours to drive like a puppeteer.
### π MCP vs The Old Ways
| | **MCP (this)** | **ExtendScript** | **CEP Panels** | **UXP Scripts** | **Manual** |
|---|---|---|---|---|---|
| **Learn in** | 30 seconds | 3 weeks | 2 months | 1 month | 0 (slow) |
| **AI-native** | β
Any MCP agent | β | β | β | β |
| **Tool count** | **197** | Unlimited (you write them) | 5-20 typical | 5-20 typical | β (manual) |
| **Error messages** | Human-readable | Opaque crashes | Varies | Better | N/A |
| **Setup time** | 5 minutes | 10 minutes | 2 hours | 30 minutes | Instant |
| **Undo support** | β
Built-in | β | Varies | β | β
|
| **Search text** | `text_search("pattern")` | Write 50 lines | Maybe | Maybe | Ctrl+F |
| **Debug mode** | β
Stack traces | β | Varies | Limited | N/A |
| **Remote control** | β
Anywhere STDIO works | β InDesign-only | β | β | N/A |
| **Maintenance** | Zero (we handle 197 tools) | You write+test everything | Fragile | Fragile | N/A |
---
## β¨ What It Can Do
### π Text & Typography (27 tools)
Create frames, set content, apply paragraph/character styles, control formatting, search with GREP, find/replace, apply fonts, set columns, insets, auto-size, vertical justification, drop caps, keep options, hyphenation, tabs, paragraph rules, text wrap, baseline, linking/unlinking.
### π¨ Shapes & Objects (20 tools)
Rectangles, ellipses, polygons, lines, groups, anchored objects β create, modify, delete, list, arrange.
### πΌοΈ Images (5 tools)
Place from disk, query metadata, fit/proportional, adjust brightness/contrast, relink.
### π Colors & Swatches (6 tools)
CMYK/RGB/LAB/spot swatches, ink list, gradients, apply to fill/stroke.
### π Pages & Documents (15 tools)
Create, open, save, close, getInfo, listOpen β plus page add/delete/duplicate/move/getInfo/listAll/applyMaster, sections.
### π Tables (20 tools)
Create, set cells, add/delete rows/columns, merge/split, alignment, fills, strokes, insets, header/footer, row/column sizing, table styles, cell styles.
### 𧬠Styles (10 tools)
Paragraph, character, object styles β create, list, duplicate, delete, apply.
### ποΈ Masters & Books (10 tools)
Master spreads β create, duplicate, apply, delete, list, getPages. Books β list, open, getDocuments, synchronize.
### π€ Export (9 tools)
PDF, EPUB, HTML, JPG, PNG, package β plus preflight, font/swatch/table lists, and `script_run()` for custom ExtendScript.
### π Find & Replace (5 tools)
GREP find/replace, format-aware find/replace.
### π Transform (5 tools)
Align, distribute, rotate, scale, flip.
### π Interactive (5 tools)
Hyperlinks, buttons, cross-reference anchors.
### π References (10 tools)
Footnotes, endnotes, cross-references, index entries/topics/generation.
### π Lists (6 tools)
Define, apply to paragraph/selection, remove, restart numbering.
### π§© XML & Data Merge (11 tools)
XML tags, import/export, data merge with CSV/TSV/XML sources.
### π§ Other
Layers, effects (drop shadow, feather, transparency), TOC, sections, undo/redo, undo groups.
---
## π§ Included AI Skills
Ten skills ship with the repo, auto-loaded by trigger keywords when you use any AI agent:
| # | Skill | What it does for you |
|---|---|---|
| 1 | **Aesthetic Preference** | Asks 8 questions before any creative work β font, palette, style, margins. Builds a persistent JSON profile. |
| 2 | **Layout Readability** | Validates overlays, contrast, orphans/widows, hierarchy, spacing, overflow before delivery. |
| 3 | **Export & Verify** | Mandatory modify β export JPG β analyze pixels β fix β repeat cycle. |
| 4 | **Import Word** | Imports `.docx`, maps Word styles to InDesign paragraph styles. |
| 5 | **Batch Operations** | Applies the same modification across N pages. |
| 6 | **Image Optimize** | Place, resize, DPI check, relink. Profiles for print (300dpi CMYK) vs web (72dpi RGB). |
| 7 | **Table Format** | Creates and styles tables β columns, rows, borders, fills, merge cells. |
| 8 | **Template Manager** | Save/load reusable page templates. |
| 9 | **Export Batch** | Export same document to multiple formats at once with different profiles. |
| 10 | **Style Extractor** | Scans `.indd` files, extracts full style profile (fonts, colors, styles, masters, margins) as JSON, replicates on new layout. |
---
## ποΈ Architecture
```
βββββββββββββββββββββββ STDIO ββββββββββββββββββββββββ WebSocket ββββββββββββββββββββ
β Any AI Agent β ββββββββββββββββββΊ β indesign-nutria-mcp β βββββββββββββββββΊ β Adobe InDesign β
β (Claude, OpenCode, β β (Node.js MCP Server) β port 8120 β + UXP Plugin β
β Cursor, etc.) β β 34 handlers β β + ExtendScript β
βββββββββββββββββββββββ β 197 tools β ββββββββββββββββββββ
β 10 AI skills β
ββββββββββββββββββββββββ
```
### Two ways to reach InDesign
The MCP protocol always speaks stdio. Only the last hop differs, and you pick
one per platform:
| | macOS | Windows |
|---|---|---|
| **With plugin** | UXP panel in `plugin/` | UXP panel in `plugin/` |
| **Without plugin** | `node bridge-proxy.mjs` (JXA/osascript) | `COM_BRIDGE_ENABLED=true` (in-process COM) |
Both are supported. The plugin is the richer path (it also emits change
events); the proxy exists for setups where you cannot or do not want to load a
UXP panel. See [WINDOWS.md](WINDOWS.md) for the Windows bridge and
`start-bridge.sh` for the macOS one.
> **Full breakdown:** the four macOS processes, the request lifecycle, the error
> contract and the trust boundaries are in
> [docs/architecture.md](docs/architecture.md).
### Full Handler Catalog
| Handler | Tools | Key Features |
|---------|-------|-------------|
| **AnchoredObject** | 5 | create, getSettings, release, setPosition, setProperties |
| **Book** | 4 | list, open, getDocuments, synchronize |
| **Changes** | 1 | getStatus β what changed since T?, without touching InDesign |
| **Color** | 6 | swatch CRUD, inks, gradients, apply |
| **DataMerge** | 5 | selectDataSource, listFields, mergeRecords, export, removeDataSource |
| **Document** | 9 | create/open/save/close, getInfo, listOpen, **getState**, **getPageStories, getStoryPages** |
| **Effect** | 4 | drop shadow, feather, transparency, gradient feather |
| **Export** | **10** | multi-format export, batch folder, preflight, fonts/swatches/tables/masters/XML, **executeScript, script_run** |
| **Font** | 5 | list, find, change, missing check, glyph insert |
| **Grep** | 4 | find, replace, findFormat, replaceFormat |
| **Image** | 5 | place, info, adjust, fit, relink |
| **Index** | 4 | addEntry, createTopic, generate, listTopics |
| **Interactive** | 5 | hyperlinks, buttons, anchors |
| **Layer** | 5 | create, delete, list, reorder, setProperties |
| **List** | 6 | define, apply, remove, restart numbering |
| **Master** | 6 | create, duplicate, apply, delete, list, getPages |
| **Note** | 4 | footnotes, endnotes |
| **Object** | 6 | shapes, groups, image links |
| **Page** | 7 | add, delete, duplicate, move, getInfo, listAll, applyMaster |
| **Preview** | 1 | document β render a page as an image the agent can see |
| **Recipe** | 2 | flow a placed text story across pages, export with a named PDF preset β each in one undo group |
| **Resources** | 6 | list/update/embed/unembed links, getLinkInfo |
| **Section** | 4 | create, list, setNumbering, delete |
| **Shape** | 6 | rectangle/ellipse/line/polygon create, delete, modify |
| **Style** | 7 | paragraph/character/object styles CRUD |
| **Table** | 16 | create, cells, rows/columns, merge/split, alignment, fills, strokes, header/footer |
| **TableStyle** | 4 | tableStyle, cellStyle CRUD |
| **Text** | 7 | frames, content, stories, findReplace, **BOM filtering, timeout** |
| **TextAdvanced** | **20** | columns, wrap, links, drop caps, rules, tabs, **formatting read/write, search** |
| **Toc** | 4 | createStyle, generate, update, listStyles |
| **Transform** | 5 | align, distribute, rotate, scale, flip |
| **Undo** | **5** | undo, redo, history, **beginGroup, endGroup** |
| **Xml** | 6 | tags CRUD, tag/untag items, import/export |
| **Xref** | 3 | create, list, updateFormat |
| | **197** | 34 handlers, no duplicates |
`tests/contract/toolInventory.test.ts` pins this number, so the table cannot
drift from the code without failing CI.
Verified end-to-end against InDesign 2026 (21.3) on the plugin-free macOS path:
MCP stdio β WebSocket bridge β JXA proxy β InDesign, creating a document,
adding a text frame, reading it back, and closing.
> **Closing documents from an agent?** `document_close` defaults to
> `saveOptions: "no"` β the only value that cannot raise a modal dialog. `"ask"`
> opens a save prompt, and `"yes"` opens a **Save As** dialog for a document that
> was never saved; both block the script until the bridge times out. Save first
> with `document_save { filePath }`, then close.
### MCP Resources
| URI | What it gives you |
|---|---|
| `mcp://session/status` | Active document session state |
| `mcp://bridge/status` | WebSocket bridge health + queue depth |
| `mcp://tools/inventory` | Full 197-tool catalog for agent self-discovery |
| `mcp://document/active` | Currently active document info |
---
## π― Use Cases
### π’ Agency Production
> "Create a 200-page catalog from this CSV. Product name in 14pt Arial Bold, price in 12pt Arial Regular, description in 10pt. Add a red 'SALE' badge on page 1, 50, 100, 150. Export as PDF."
### π Book Publishing
> "Import this Word document. Map Heading 1 to 'Chapter Title' style, Normal to 'Body Text'. Add running headers with page numbers. Generate a TOC. Export to PDF with crop marks."
### π¨ Creative Automation
> "Take this InDesign template and generate 50 variations with different colors and text from this spreadsheet. Export each as a separate PDF."
### π Print Production
> "Check all 200 files for missing fonts and broken links. Fix them. Export as PDF/X-1a."
### π Continuous Publishing
> "Every morning at 8am, update the daily newspaper template with fresh content from our CMS, preflight, and upload to the web server."
---
## β‘ Quick Start (30 seconds)
Two halves: the **server** your agent runs β one line from npm, no clone and no
build β and the **bridge** that reaches InDesign on this machine.
### 1οΈβ£ Connect InDesign
The server listens on `127.0.0.1:8120`; something still has to drive InDesign.
Pick by platform:
**macOS β with the UXP panel** (also emits change events)
1. Open **UXP Developer Tool**
2. Load the `plugin/` directory β it ships in this repository, so clone it once:
`git clone https://github.com/nutriandrea/adobe-indesign-mcp` (no build step)
3. Open the **MCP Bridge** panel β it connects on its own
**macOS β without the panel**
```bash
git clone https://github.com/nutriandrea/adobe-indesign-mcp
cd adobe-indesign-mcp
npm install # gives the proxy its `ws` dependency; nothing to build
./start-bridge.sh # launches InDesign, then the JXA proxy
```
**Windows β no bridge process.** The COM bridge drives InDesign in-process, so
Windows needs neither the clone nor a second process; it is enabled by the env
var in the config you use in step 2 (see
[`examples/claude-desktop.windows.json`](examples/claude-desktop.windows.json)).
See [WINDOWS.md](WINDOWS.md) for the details and caveats.
### 2οΈβ£ Connect your AI agent
Both hosts start the same published package β nothing to clone, nothing to
build.
**Claude Desktop:**
```json
{
"mcpServers": {
"indesign": {
"command": "npx",
"args": ["-y", "indesign-nutria-mcp"]
}
}
}
```
**OpenCode** (project `opencode.json`, or the same block in
`~/.config/opencode/opencode.json`):
```json
{
"mcp": {
"servers": {
"indesign": {
"type": "local",
"command": ["npx", "-y", "indesign-nutria-mcp"]
}
}
}
}
```
On OpenCode the servers live under `mcp.servers`, not `mcpServers`, and the
command is an array β that is [the documented V2 shape](https://opencode.ai/v2/docs/mcp-servers).
Ready-made files for Claude Desktop, Cursor and Claude Code β including the
Windows COM variant β are in [`examples/`](examples/README.md). Each one is
copy-paste.
### 3οΈβ£ Start creating
```
π€ "Create a landscape A3 document with 3 pages."
π€ "Add a blue rectangle covering the top half of page 1."
π€ "In the rectangle, add text 'Hello World' in white, Arial Bold 72pt."
π€ "Export page 1 as JPG."
```
### π Notes for MCP client developers
Building a custom client (script, harness, test runner) against this server? Two lessons from real-world integration:
| Pitfall | Fix |
|---------|-----|
| **Large responses arrive split across stdout chunks.** `preview_document` at 150ppi returns ~100+ KB of base64 β more than one pipe chunk. Parsing each `data` event with `JSON.parse(chunk)` silently drops the pieces and the call appears to hang forever. | Accumulate stdin into a buffer and only `JSON.parse` complete newline-delimited lines: `buf += chunk; while ((i = buf.indexOf('\n')) >= 0) { parse(buf.slice(0, i)); buf = buf.slice(i + 1); }` |
| **Image tools return `image` content, not `text`.** `preview_document` responds with `{ type: 'image', mimeType: 'image/png', data: '<base64>' }` β reading `.text` yields `undefined`. | Read the base64 payload from `content[0].data` (check `content[0].type`). |
Every message is a single-line JSON-RPC object terminated by `\n`; there is no framing beyond the newline.
---
## π What's New in v1.6.0
### Fixed
| β οΈ | Read this before you script |
|---|---|
| **The layout skill named tools that do not exist** | `skills/indesign/indesign-mcp-layout/SKILL.md` advertised an `mcp_indesign_*` prefix the registry never used, and pointed connectivity checks at `health_check` and `list_ai_skills` β neither is registered by any handler. A skill is instruction, so naming a tool that does not resolve sends a model into a second failure on top of the one it was recovering from. |
| Change | What it means for you |
|--------|----------------------|
| **The skill names real tools now** | It points at the bare tool names the registry actually exposes, and documents the read-only resources (`mcp://bridge/status` and friends), which never touch InDesign and cost no round-trip. |
| **Skills are guarded like the instructions are** | `tests/unit/skillGuidance.test.ts` walks every `SKILL.md` under `skills/` and `marketplace/` and fails on a tool name the registry does not know β and on a skill whose frontmatter `name` disagrees with its folder. Regression-proven against the bug above. |
| **Marketplace counts are checked too** | `verify:tool-counts` now reads the markdown under `marketplace/`, so the number quoted to a buyer is checked alongside the README, `llms.txt` and the package description. |
### Added
| Change | What it means for you |
|--------|----------------------|
| **Client configs** | `examples/` holds copy-paste configs for Claude Desktop (macOS, and the Windows COM variant), Cursor and Claude Code, plus `examples/README.md`. |
| **Registry and bundle metadata** | `server.json` β generated from `package.json` and gated by `verify:server-json` β and `mcpb/manifest.json`, so the MCP Registry and an MCP Bundle can be published from the same release. |
| **Release automation** | `release.yml` takes a tag through `verify` and `verify:pack`, publishes to npm with provenance, builds the `.mcpb` and attaches it to the GitHub Release with the CHANGELOG notes. `publish-mcp-registry.yml` chains off the release, so the registry never advertises a version npm does not have. `validate-release-tag.sh` refuses a tag that disagrees with `package.json` or has no CHANGELOG section. |
| **A packaged companion skill** | `marketplace/agensi/` prepares the skill for a third-party marketplace, with `npm run build:agensi` asserting the archive shape the reviewer requires β one top-level folder, `SKILL.md` at its root, text files only. Like `mcpb/`, it does not ship to npm. |
<details>
<summary>v1.5.0</summary>
### For AI agents
| β οΈ | Read this before you script |
|---|---|
| **Pass `document_id` β don't trust the front tab** | Every tool takes an optional `document_id`: a document name (as `document_listOpen` reports it) or a 0-based index. With more than one document open the front tab is **not** reliably the intended target, and a call that lands on the wrong document edits the wrong file. Call `document_getState`, then pass `document_id` on everything that follows. |
| Change | What it means for you |
|--------|----------------------|
| **`document_id` on every tool** | 197 tools, one extra optional parameter. Omit it and a tool acts on the front document β exactly the previous behaviour, so nothing you already send changes. |
| **A target that is not open fails loudly** | `document_id 'Gone.indd' is not open. Open documents: A.indd, B.indd`, classified `document_not_found` and suggesting `document_listOpen`. Previously the script silently edited whichever document happened to be frontmost. |
| **One shared resolver** | `app.activeDocument` was written inline in **197 places across 32 handlers**; `__resolveDocument()` is now injected with every script and every document-scoped handler resolves through it, so the target holds for the whole call β even if the front tab changes mid-script. |
| **Safe by construction** | `document_id` reaches InDesign as a JSON string literal and is matched by name, so it is never executable. It is bound per call in `AsyncLocalStorage`, so a call that awaits before dispatching cannot be hijacked by a concurrent one. |
### Changed
| Change | What it means for you |
|--------|----------------------|
| **The "pin the document" advice is now executable** | The connect-time contract used to tell the host to *ask which document was meant*. It now names `document_id` and how to pass it, and the shipped `indesign-mcp-layout` skill points at it too. |
</details>
<details>
<summary>v1.4.3</summary>
### For AI agents
| β οΈ | Read this before you script |
|---|---|
| **Failures now tell you how to recover** | A failing call returns a JSON envelope `{ ok: false, code, message, suggested_next_tool?, suggested_args? }` instead of prose. Branch on `code`, and call `suggested_next_tool` when it is present rather than guessing. |
| **State before action** | `document_getState` returns the front document's geometry, its index, every open document, the active layer and the selection count in one round-trip. Call it before mutating instead of assembling that state from four separate calls. |
| **A recipe is one call, not a chain** | `recipe_flowTextStory` places a text file and flows it across pages; `recipe_exportPreset` applies a named PDF preset and exports. Measured live, the flow cost **1 call / 215 bytes** against **6 calls / 786 bytes** for the nearest atomic chain β which cannot place the text at all. |
| Change | What it means for you |
|--------|----------------------|
| **Structured error envelopes** | Sixteen failure classes, each with a `suggested_next_tool` checked against the live registry (a bad frame index suggests `document_getInfo`; a timeout suggests `document_listOpen`). ExtendScript errors carry `line`, `file` and `stack`. |
| **A workflow contract on connect** | The server advertises roughly 680 tokens of `instructions` β bridge bootstrap, state-before-action, the `document_close` save trap, the error-envelope shape and undo grouping. |
| **`document_getState`** | One round-trip replaces the `document_getInfo` + `document_listOpen` + layer + selection sequence. |
| **`preview_document` takes `documentIndex`** | Render any open document, not only the front one. Exports are capped at 4 MB and re-rendered once at a fitting resolution before failing with `output_too_large`. |
| **Two new recipes** | `recipe_flowTextStory` and `recipe_exportPreset`, bringing the catalog to 197. |
| **Opt-in success envelope** | `RESPONSE_ENVELOPE_SUCCESS` (or `responseEnvelope.success`) wraps successful results as `{ ok: true, ... }`, so a single key covers success and failure. **Off by default**, because it changes the response shape. |
| **A smaller, correct package** | A `files` allowlist replaces the `.npmignore` denylist: **1.6 MB unpacked**, down from 4.4 MB β and the `bridge-proxy.mjs` that the macOS launcher execs is now actually shipped. |
### Changed
| Change | What it means for you |
|--------|----------------------|
| **Error responses are JSON text** | The failure message is preserved verbatim inside the envelope; `isError: true` is unchanged. |
| **`document_close` defaults `saveOptions` to `"no"`** | `"ask"` raised a modal save dialog that blocked until the bridge timed out and could leave InDesign wedged; `"no"` cannot prompt. Call `document_save` first when you want the work kept. |
### Fixed
| Fix | What it means |
|-----|---------------|
| **Every call after `undo_beginGroup` failed** | `wrapExtendScript` set `app.scriptPreferences.undoMode`, a property InDesign does not have. Undo groups now run through `app.doScript(..., UndoModes.ENTIRE_SCRIPT)`. |
| **`undo`, `redo` and `undo_history` threw on every call** | They read `document.undoable` / `document.redoable`, which are not part of the DOM. |
| **The server reported itself as v1.0.0** | Four releases after v1.0.0 it advertised the old literal on `initialize`. `SERVER_VERSION` is now the single source of truth, and two gates keep it honest. |
</details>
<details>
<summary>v1.4.2</summary>
### For AI agents
| β οΈ | Read this before you script |
|---|---|
| **Prefer the typed tools** | `export_executeScript` and `script_run` have **no cancellation**. A hung script leaves the bridge busy and every later call queues behind it until the host restarts. Loops, font enumeration and multi-mutation probes belong in the typed tools, which validate arguments and time out. See [skills/indesign/indesign-mcp-layout](skills/indesign/indesign-mcp-layout/SKILL.md). |
| Change | What it means for you |
|--------|----------------------|
| **The bridge protocol is now canonical** | Responses are `{ type: 'result', id, result }` or `{ type: 'error', id, error }`. Legacy `success`/`response` types are still accepted on input, but an unknown type with a valid `id` is now an explicit error instead of a silently resolved promise. |
| **Plugin-free macOS setup** | `start-bridge.sh` launches InDesign and the JXA proxy β no UXP panel to load. |
| **The JXA proxy no longer blocks or corrupts** | Scripts reach `osascript` through a temp file instead of shell-quoted `-e` strings, and execution is asynchronous with a FIFO queue. A script containing a quote, backslash or newline can no longer be mangled, and the MCP server keeps answering while InDesign works. |
| **Configurable InDesign version** | `INDESIGN_APP` replaces the hardcoded "Adobe InDesign 2024" process name. |
| **The panel connects itself** | The MCP Bridge panel connects on open, so there is no Connect click to miss. |
| **InDesign 2026 anchoring** | Creating an anchored object no longer depends on `move()`, which 2026 rejects for insertion-point targets. |
| **The shipped config starts everywhere** | It declared the wrong MCP transport and would have aborted startup on macOS and Linux had it enabled the Windows COM bridge. Both are now covered by a test. |
</details>
<details>
<summary>v1.4.0</summary>
| Change | What it means for you |
|--------|----------------------|
| **Agent vision: `preview_document`** | Render any page as an image the agent can actually see β layout mistakes get caught in the loop, not after export. |
| **Batch PDF: `export_batchFolder`** | Export every `.indd` in a folder to PDF in one call, with per-file results. |
| **Windows COM bridge** | Run with zero plugins via `COM_BRIDGE_ENABLED=true`. See [WINDOWS.md](WINDOWS.md). |
| **Hardened file paths** | Every file-touching tool validates paths up front β traversal attempts (`..`) and system directories are rejected before InDesign ever sees them (now 11 guarded call sites). |
</details>
<details>
<summary>v1.3.0</summary>
| Change | What it means for you |
|--------|----------------------|
| **InDesign 2026 ready** | Color and anchored-object tools work on 2026's renamed enums β no more silent failures after upgrading. |
| **Real env var config** | Every setting in `.env.example` is now honored: `BRIDGE_PORT`, `HTTP_BRIDGE_ENABLED`, `LOG_LEVEL`, β¦ Invalid values never block startup. |
| **No more 30s hangs** | If the plugin isn't connected, tool calls fail instantly with "Bridge is not connected" instead of timing out. |
| **One escaping path** | All handlers share a single battle-tested ExtendScript string escaper. |
| **Hardened path validation** | File-path checks now use true containment (no prefix-collision escapes). |
| **Honest status** | `getStatus().connected` reflects actual plugin connectivity, and CI enforces coverage gates. |
</details>
<details>
<summary>What's New in v1.1.0</summary>
| Feature | Tools | What it solves |
|---------|-------|----------------|
| **Text formatting read** | `text_getFormatting` | Get font/size/style per text range. No more custom scripts. |
| **Character style apply** | `text_applyCharStyle` | Apply to a range in one call. |
| **Font apply** | `text_applyFont` | Apply font family/style/size to a range. |
| **Text search** | `text_search`, `text_searchFormatting` | GREP + format-aware search with paragraph-relative positions. |
| **StoryβPage map** | `document_getPageStories`, `document_getStoryPages` | Which stories are on which pages. |
| **Undo groups** | `undo_beginGroup`, `undo_endGroup` | Group operations into one undo step. |
| **Debug mode** | `script_run(code, debug=true)` | Full ExtendScript stack traces. |
| **BOM filtering** | Text reads | `\ufeff` and `\u0004` filtered by default. |
| **Timeout/maxResults** | `text_getStories`, etc. | Custom limits for large documents. |
| **MCP resources** | `mcp://tools/inventory` | Agent auto-discovers all tools. |
</details>
---
## βοΈ Configuration
All settings have defaults β zero config required. To customize, copy `.env.example` to `.env` or pass a JSON file; environment variables win over the file:
```bash
BRIDGE_PORT=8120 # WebSocket port the UXP plugin connects to
HTTP_BRIDGE_ENABLED=false # optional REST fallback bridge
HTTP_BRIDGE_PORT=3000
SERVER_TRANSPORT=stdio # stdio | websocket
LOG_LEVEL=info # debug | info | warn | error
COM_BRIDGE_ENABLED=false # Windows only: drive InDesign over COM, no plugin
RESPONSE_ENVELOPE_SUCCESS=false # wrap successful results as { ok: true, ... }
```
The plugin-free macOS proxy takes its own variables:
```bash
INDESIGN_APP="Adobe InDesign 2026" # matches your installed version
BRIDGE_WS_URL=ws://127.0.0.1:8120
BRIDGE_TIMEOUT_MS=120000 # a long export is not a hang
BRIDGE_RECONNECT_DELAY_MS=3000
```
Precedence: **environment variables > JSON config file > defaults**. Invalid values are ignored, never fatal.
---
## πͺ Windows
Two ways to run on Windows: the standard UXP plugin flow (identical to macOS) or the new opt-in **COM bridge** that needs no plugin at all. See [WINDOWS.md](WINDOWS.md).
---
## π¦ Project Structure
```
βββ src/
β βββ server/ # MCP server (STDIO transport)
β βββ bridge/ # WebSocket bridge, ScriptExecutor, protocol, COM executor
β βββ handlers/ # 34 handler modules (197 tools)
β βββ schemas/ # Zod parameter schemas
β βββ core/ # Session tracking
β βββ types/ # TypeScript definitions
β βββ utils/ # Config, logger, security, JSON polyfill
βββ plugin/ # UXP panel (index.html, index.js, manifest.json)
βββ examples/ # Copy-paste client configs (Claude Desktop, Cursor, Claude Code)
βββ mcpb/ # MCP Bundle manifest (npm run build:mcpb packs the archive)
βββ server.json # MCP Registry manifest (generated from package.json)
βββ llms.txt # llms.txt index for agents and directories
βββ bridge-proxy.mjs # Plugin-free macOS proxy (JXA/osascript)
βββ jxa-driver.js # Runs one ExtendScript inside InDesign
βββ start-bridge.sh # Launches InDesign + the proxy
βββ skills/ # Agent-facing skills shipped with the server
βββ marketplace/ # Store listings that package a skill for third-party channels
βββ tests/ # 1131 tests (vitest)
βββ .opencode/skills/ # 10 AI agent skills
βββ docs/ # Documentation and fork audits
βββ media/ # Social preview, hero images
βββ dist/ # Compiled output
βββ opencode.json # MCP configuration
```
---
## π Requirements
| Requirement | Version |
|-------------|---------|
| **Adobe InDesign** | 2022+ (2024/2025/2026 recommended) |
| **Node.js** | 18+ |
| **OS** | macOS or Windows (Linux can run the MCP server, but no InDesign automation path exists there) |
---
## π§ͺ Development
```bash
npm test # Run 1131 tests
npm run test:watch # Watch mode
npm run build # TypeScript compile
npm run lint # ESLint
```
### Live smoke test (macOS)
```bash
npm run smoke:macos
```
`npm test` covers the handlers, the protocol and the startup sequence, but it
cannot see the seams between the four separate processes on the plugin-free
macOS path. That gap is not hypothetical: 965 tests once passed against a build
whose bridge never listened on its port, so every tool call failed in the field
and nothing went red.
`smoke:macos` drives the real chain β MCP stdio β WebSocket bridge β JXA proxy β
InDesign β against a running InDesign, creating a document, writing to it,
reading it back, and closing it. It creates one unsaved document and cleans up
after itself. It needs a desktop app, so it is not part of `npm test` and does
not run on CI; it exits 0 with a SKIP notice if InDesign is not open.
Run it by hand before releasing a change to the bridge, the protocol, the proxy
or the driver.
```bash
npm run smoke:recipes # the live gate for the two recipes
```
`smoke:recipes` does what the unit tests cannot: it flows a long text file with
`recipe_flowTextStory`, checks the document grew by exactly what the tool
reported, calls `undo` **once** to confirm the whole flow is a single undo step,
then exercises both preset paths of `recipe_exportPreset`. Same rules as above β
needs a desktop app, exits 0 with a SKIP notice when InDesign is closed.
```bash
npm run smoke:target # the live gate for document_id
```
`smoke:target` opens **two** documents and proves the thing `document_id` is for:
a call that names the non-front document edits *that* document and leaves the
front one alone. It then checks index targeting, that omitting `document_id`
still acts on the front document (the additive guarantee), that an unopened
target arrives as a `document_not_found` envelope suggesting `document_listOpen`,
and that a hostile `document_id` is inert rather than executed. It closes only
the documents it created, never saves, and leaves every pre-existing document
untouched. Same rules as above β needs a desktop app, exits 0 with a SKIP notice
when InDesign is closed.
---
## πΊοΈ Roadmap
- [ ] **InDesign Server** β headless server support for CI/CD pipelines
- [ ] **Live preview** β stream InDesign canvas to agent
- [ ] **Template marketplace** β share and discover InDesign templates
- [ ] **Natural language β layout** β describe a page, get a page
- [ ] **Batch PDF processing** β apply changes across hundreds of files
- [ ] **VSCode extension** β control InDesign from your editor
---
## π Deep Dives
- [Architecture](docs/architecture.md) β the four-process macOS path, request lifecycle, error contract
- [Available tools](docs/available-tools.md) β every tool, generated from the registry
- [Troubleshooting](docs/troubleshooting.md) β modal hangs, port conflicts, missing fonts, blocked scripts
- [Development](docs/development.md) β build, test, and the live smoke test
- [Origin Story](docs/origin.md) β how this project came to life
- [Comparison docs](docs/comparison.md) β MCP vs CEP vs ExtendScript in depth
- [Launch thread](docs/launch-thread.md) β social launch content
---
## π€ Contributing
PRs welcome! The handler pattern is designed to be simple:
1. Create `src/handlers/YourHandler.ts`
2. Define tools with Zod schemas
3. Register in `IndesignMcpServer.ts`
4. Add tests
Check [CONTRIBUTING.md](CONTRIBUTING.md) for details.
---
## π Acknowledgements
This server is a merge of what four people found in the field. Each of these
contributions fixed something that was genuinely broken, not a cosmetic
preference.
- **[@graydini](https://github.com/graydini)** β built the Windows COM bridge
that this project runs on today: a persistent `cscript` process holding one
live COM connection, `NEVER_INTERACT` so export dialogs can never block, and
the path-containment hardening in `validateFilePath`.
See [PR #1](https://github.com/nutriandrea/adobe-indesign-mcp/pull/1) and the
[PR #21](https://github.com/nutriandrea/adobe-indesign-mcp/pull/21) merge.
Also reported the InDesign 2026 API incompatibilities that shaped official
Windows support: renamed and read-only `ColorModel` and `AnchorPoint` enums,
and `move()` no longer accepting `InsertionPoint` targets. Earlier fixes from
the same line of work landed on `main` as safe `basedOn`/`pointSize` reads in
style listing, rich style-creation parameters, `\n` β paragraph-break
normalization in text content, and an `eval`-free JSON polyfill that survives
the ExtendScript sanitizer.
Their branch also carried COM-specific operational knowledge β that a hung
script wedges the Windows bridge with no way out, that `fontFamily`,
`italic` and `weight` throw under COM, and that InDesign must be visibly
running β now documented in
[WINDOWS.md](WINDOWS.md) and the layout skill. A file-by-file audit of what
was still unmerged is in
[docs/forks/graydini-windows-com-bridge-audit.md](docs/forks/graydini-windows-com-bridge-audit.md).
- **[@advaitakelkar](https://github.com/advaitakelkar)** β diagnosed why the
macOS proxy returned junk, in
[PR #22](https://github.com/nutriandrea/adobe-indesign-mcp/pull/22). Three
real bugs in one report: `osascript` sends script *results* on **stderr**, so
every successful call was read as a failure; the script was passed through
shell `-e` quoting, which mangles any quote, backslash or newline; and the
app name was hardcoded to "Adobe InDesign 2024", so every other version
failed. The same work also moved the connection handshake out of
`app.doScript`, where it was being parsed as ExtendScript. Adopted and
extended β see [What's New in v1.4.2](CHANGELOG.md#142---2026-09-27).
- **[@ijeetu](https://github.com/ijeetu)** β two fixes, both easy to miss and
both real. The MCP Bridge panel now connects on open instead of waiting for
a Connect click, so the panel no longer looks dead when the server is
already running. Earlier, they traced a parse failure in the JSON polyfill to
`sanitizeCode()` rewriting `eval(` into a comment and orphaning the
parentheses β the shipped `wrapExtendScript` already reflects that fix.
---
## π License
MIT Β© Andrea Cacioppo
---
<p align="center">
<b>Made with β€οΈ for designers who code and AI agents who design.</b><br>
<a href="https://github.com/nutriandrea/adobe-indesign-mcp">GitHub</a> Β·
<a href="https://github.com/nutriandrea/adobe-indesign-mcp/issues">Issues</a> Β·
<a href="https://github.com/nutriandrea/adobe-indesign-mcp/blob/main/CONTRIBUTING.md">Contributing</a>
</p>
TDQS
Scored across 197 tools
Many tools duplicate one another (export_executeScript/script_run, page_applyMaster/master_apply, table_list/export_getTables, font_list/export_getFonts), making selection ambiguous. Although some descriptions clarify intent, the sheer overlap forces agents to guess.
Names mix noun_verb and verb_noun patterns (page_add vs export_document) and use inconsistent camelCase (document_getStoryPages) alongside snake_case. Prefixes vary by domain (table_ vs tableStyle_, font_missing_check), preventing a predictable convention.
197 tools is far beyond the 50+ threshold for extreme mismatch, even for a feature-rich domain like InDesign automation. Many tools exist only to duplicate or wrap others, bloating the surface unnecessarily.
The surface covers most InDesign operationsβdocuments, pages, text, tables, images, styles, export, data merge, scriptsβwith few obvious gaps. Missing a group_create and book_create/book_close prevents a perfect score, but core workflows are workable.