Skip to main content
Glama
matthijs1000

indesign-mcp

by matthijs1000

InDesign MCP Server

A custom MCP server that lets Claude Code read, search, and manipulate Adobe InDesign documents. macOS only.

Claude connects to InDesign via osascript → ExtendScript. No plugins, no UXP — just a single Python file.

What it can do

  • Read documents — pages overview, stories (threaded text flows), paragraph styles, character styles

  • Search & replace — find text across the entire document, bulk replace

  • Inspect structure — text frames per page with bounds/overflow info, tables with contents

  • Run arbitrary scripts — raw ExtendScript escape hatch for anything the built-in tools don't cover

Related MCP server: Claude-LMStudio-Bridge

Prerequisites

  • macOS (uses osascript)

  • Adobe InDesign 2025 or 2026 (tested with 2026 v21.4.1)

  • uv — Python package runner (brew install uv or curl -LsSf https://astral.sh/uv/install.sh | sh)

  • Claude Codeclaude.ai/code (CLI, desktop app, or IDE extension)

You do NOT need to install Python separately — uv handles it.

Setup

  1. Clone this repo somewhere on your Mac:

    git clone <repo-url> ~/indesign-mcp
    cd ~/indesign-mcp
  2. Open InDesign and open the document you want to work with.

  3. Start Claude Code in the project directory:

    claude

    The MCP server starts automatically (configured in .mcp.json).

  4. Try it out — ask Claude to read your document:

    "What's in this InDesign document?" "Find all instances of 'Chapter' in the document" "Show me the paragraph styles"

That's it. No manual server launch needed.

If your InDesign version is different

Edit server.py line 13 and change the app name:

APP_NAME = "Adobe InDesign 2026"  # Change to match your version

Adjusting the .mcp.json path

The .mcp.json file contains an absolute path to server.py. After cloning, update it to match your location:

{
  "mcpServers": {
    "indesign": {
      "command": "uv",
      "args": ["run", "--python", "3.13", "--with", "mcp[cli]", "mcp", "run", "/YOUR/PATH/HERE/server.py"]
    }
  }
}

Available tools

Tool

Description

get_document_info

Document overview: name, pages, layers, story count

get_pages_overview

All pages with dimensions, item counts, master pages

get_stories_overview

All text stories with content previews

read_story

Full text of a story by index

get_story_paragraphs

Paragraphs with applied styles (paginated)

get_page_text_frames

Text frames on a page with position, size, overflow status

find_text

Search for text across the document (max 100 results)

replace_text

Find and replace text document-wide

get_paragraph_styles

List all paragraph styles

get_character_styles

List all character styles

get_tables

Find all tables in the document

get_table_contents

Read a table as a 2D array

run_script

Execute arbitrary ExtendScript — the escape hatch

Hard-won lessons (read before extending)

These are things we learned the painful way. Save yourself the debugging.

ExtendScript has no JSON

InDesign's ExtendScript engine (even in 2026) does not have JSON.stringify or JSON.parse. The server includes a polyfill that gets prepended to every script automatically. If you write standalone .jsx files or use run_script, the polyfill is already included.

Units are document units, not points

All geometry values (column widths, cell insets, bounds, positions) use the document's ruler units — usually inches. Setting table.columns[0].width = 48 means 48 inches, not 48 points. Check with doc.viewPreferences.horizontalMeasurementUnits. Font sizes and leading remain in points (they're typographic units).

findChangeTextOptions, not findTextPreferences

wholeWord and caseSensitive go on app.findChangeTextOptions, NOT on app.findTextPreferences. The API splits what to find (findTextPreferences) from how to find (findChangeTextOptions).

Tables: use bodyRowCount for everything

Create tables with insertionPoint.tables.add({columnCount: N, bodyRowCount: M, headerRowCount: 0}). Use bodyRowCount for all rows including visual headers — ExtendScript's headerRows accessor causes errors. Don't prefix frame contents with \r before inserting a table.

Table styling pattern

// Dark header row
cell.fillColor = doc.swatches.itemByName("Black");
cell.texts[0].fillColor = doc.swatches.itemByName("Paper");

// Alternating row shading
cell.fillColor = doc.swatches.itemByName("Black");
cell.fillTint = 10;

// Cell insets (in inches!)
cell.topInset = 0.03;

Text wrap with hand-made contour paths

For wrapping text around an image's actual shape (not its bounding box):

  1. Create a polygon that traces the figure silhouette (~60+ anchor points)

  2. Place the image INTO the polygon (polygon.place(File))

  3. Fit proportionally (polygon.fit(FitOptions.PROPORTIONALLY))

  4. Set contour wrap (polygon.textWrapPreferences.textWrapMode = TextWrapModes.CONTOUR)

The polygon does double duty: clips the image AND serves as the text wrap contour. Don't use a separate rectangle for the image — its white background will cover text behind it.

Detect Edges doesn't work on white-background images. You need transparent PNGs (alpha channel) or hand-made paths.

Auto-sizing text frames

Set textFramePreferences.autoSizingType = AutoSizingTypeEnum.HEIGHT_ONLY on content frames, then read geometricBounds[2] to find actual bottom — use that to position subsequent elements.

Adding new tools

  1. Add a function decorated with @mcp.tool() in server.py

  2. Build the ExtendScript as a Python f-string — use {{ / }} for JS braces

  3. End the script with a JSON.stringify(...) expression (last expression's value is returned)

  4. Use _escape_jsx() to sanitize any user-provided strings before embedding

  5. Call run_jsx(script) to execute

Architecture

Claude Code  →  MCP tool call  →  server.py builds ExtendScript string
                                       ↓
                                  run_jsx() prepends JSON polyfill
                                       ↓
                                  writes temp .jsx file
                                       ↓
                                  osascript tells InDesign to execute it
                                       ↓
                                  InDesign returns result via stdout

Single file, no build step, no dependencies beyond mcp[cli].

License

Do what you want with it.

F
license - not found
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/matthijs1000/indesign-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server