- MCP server for Apple iWork (Numbers, Pages, Keynote) automation via JXA/osascript
- TypeScript, ESM, uses
@modelcontextprotocol/sdkv1.26.0 - 117 tools total: 50 Numbers, 26 Pages, 41 Keynote (includes 8 Creator Studio AI tools)
- npm:
iwork-mcp| GitHub:reichenbach/iwork_mcp(PRIVATE) - Requirements: macOS 13+, iWork 14.0+, Node.js 18+
- Supports both standard iWork and iWork 15.1+ "Creator Studio" app bundles (tested on 15.1.1)
- MCP SDK imports:
@modelcontextprotocol/sdk/server/mcp.jsand.../server/stdio.js server.tool(name, description, zodSchema, annotations, callback)— uses zod raw shapes (not z.object())- Tool callbacks return
{ content: [{ type: "text", text: "..." }], isError?: boolean } - JXA scripts pass params via
JSON.stringifyin argv[0], return viaJSON.stringifyin stdout execFile("/usr/bin/osascript", ["-l", "JavaScript", "-e", script, jsonArgs])- App name resolution in
src/jxa.ts: prefers Creator Studio if installed, falls back to standard names. All tool scripts use standard names —rewriteAppNames()rewrites them transparently before execution. - Numbers cell
formatproperty: set via JXAcell.format = "percent"(not "percentage"). Valid: automatic, number, currency, percent, fraction, scientific, text, checkbox, star rating.
- Colors: Numbers uses 0-65535 int range, NOT 0-1 floats. Multiply hex by 257.
- Font names: Use PostScript names (
HelveticaNeue-Bold) not display (Helvetica Neue Bold). Display names cause -10000 error. - Merging: Can't merge across header/non-header boundaries. Set headerRowCount/headerColumnCount to 0 first.
- Charts: JXA can't bind data to charts.
numbers_add_chartuses AppleScript bridge via$.NSAppleScriptto create data-bound charts from selection. - Pages 14.5 paragraphs:
doc.paragraphscompletely broken — no push/read/format/index. Onlydoc.bodyText(plain string) works. - Pages JXA paragraph access: Use
doc.bodyText.paragraphs[i], NOTdoc.paragraphs[i]. Properties arefont/size/color(notfontName/fontSize/bold/italic). Color writes use 0-65535 ints, reads return 0-1 floats. Nobold/italicproperties — use PostScript font names instead. - Pages bodyText formatting: Setting
doc.bodyText = "..."destroys ALL formatting. For structural changes (add/insert/delete), save formats first, set bodyText, then restore. For replace, use per-paragraphdoc.bodyText.paragraphs[i] = "new text"which preserves other paragraphs' formatting. - Pages paragraph index shift: When paragraph text contains
\n, Pages creates multiple actual paragraphs from one input.pages_create_document_with_contentmust track real paragraph index (count\nper input) or formatting bleeds across paragraphs. - Numbers default table: New sheets auto-create "Table 1".
numbers_add_sheetnow deletes it by default. - Keynote masterSlides JXA bridge broken:
doc.masterSlides(),.byName(),.name()all fail with -1700 "Can't convert types". UseNSAppleScriptObjC bridge from within JXA to run AppleScript for master slide operations. - Keynote shapeType: No JXA API to set shape type programmatically. Parameter removed from
keynote_add_shape. - Numbers add_row at beginning: JXA
table.rows.push()always appends. To insert at beginning, must shift existing cell data down first, then write new data at row 0. - JXA export format strings: Use
"PDF"not"Numbers PDF"/"Pages PDF"/"Keynote PDF". The app-prefixed strings cause -1700 "Can't convert types". - JXA
table.ranges["B2:C3"]: Completely broken — always throws "Invalid index" or "Can't get object".read_rangemanually parses cell refs and iterates cells. - Numbers minimum table size:
app.Table({columnCount: 1})throws "Invalid column count (-10000)". Minimum is 2 columns AND 2 rows. - Keynote can't delete all slides: Can't
app.deletethe only slide — Keynote requires at least 1. Compound tool reuses the auto-generated first slide. - Keynote
doc.save({ in: })unreliable across processes:byName+save({ in: Path(...) })fails with -1728 from a different osascript subprocess. Workaround: useapp.documents[0]or combine create+save in one JXA call. - Creator Studio
doc.save({ in: })hangs: UsecreatorStudioSaveAs()insrc/jxa.ts— closes with auto-save, copies file, reopens from new path. - Creator Studio
app.export()fails with error 6: UsecreatorStudioExportPDF()insrc/jxa.ts— usesqlmanageQuick Look to generate PDF. - Creator Studio document name resolution: Auto-save renames documents by appending file extensions (e.g. "Untitled 1" -> "Untitled 1.numbers").
injectDocumentNameResolution()insrc/jxa.tshandles this transparently by trying the extended name on lookup failure. - Keynote shape position: Setting
shape.position = [x, y](array format) silently fails — reads back as (0,0). Must use object format:shape.position = {x: x, y: y}. Width/height setting works fine with direct assignment. - JXA
indexOf()on scriptable objects:doc.sheets().indexOf(sheet)returns -1 because JXA object references can't be compared with===. Iterate by name instead. - Pages tables inaccessible via JXA:
doc.tables()throws -2763 "Don't know how to create TMAScriptTableInfoProxy" — but the AppleScript dictionary exposes them fully. The four pages table tools (list/read/write_cells/resize) use the NSAppleScript bridge, same as Keynote master-slide ops. Cell writes with strings starting "=" set formulas;set row countgrows/shrinks tables. CAVEAT: table CREATION (make new table) is broken on Creator Studio 15.x in BOTH JXA and AppleScript (-2763); on 15.xpages_add_tablefalls back to the UI-scripting workaround (see the "Pages 15.x table creation broken" bullet). - Keynote text colors: Use 0-1 float range (like Pages), NOT 0-65535 int range (like Numbers).
paragraph.color = [r/255, g/255, b/255]. - Numbers auto-parses formatted strings: Writing
"$1,234.56"to a cell auto-converts to numeric 1234.56. To preserve strings, setcell.format = "text"before writing. - Pages 15.x table creation broken:
doc.tables.push()AND AppleScriptmake new tableboth fail with -2763 "TMAScriptTableInfoProxy" (reading/resizing existing tables via AppleScript works).pages_add_tablefalls back topagesInsertTableViaUI()insrc/jxa.tson Creator Studio: System Events menu-clicks Insert > Table > Basic, then resizes via AppleScriptset row count/set column count. The insertion point is established by clicking into the body with a top-down position scan (a click on a table/image selects it and disables the menu). Synthetic keystrokes are deliberately avoided — secure keyboard entry from ANY app (e.g. a browser password field) silently blocks them, but not mouse/menu clicks. Menu clicks must be two-step (open parent menu, then click child) — direct 3-level submenu clicks silently no-op.
src/index.ts— entry point + install routingsrc/install.ts— auto-config for Claude Desktopsrc/jxa.ts— runJXA(), OsascriptError, Creator Studio workaroundssrc/tools/{numbers,pages,keynote}.ts— tool definitionssrc/instructions.ts— MCP instructions string (design guidance for AI clients)scripts/create-screenshots.ts— showcase document builder (budget, resume, pitch)scripts/create-examples.ts— example file generator (sample.numbers, sample.pages, sample.key).github/workflows/ci.yml— CI: build + unit tests on push/PR, Node 18 & 22 matrixtest/helpers/{server,app-check}.ts— test helpers (InMemoryTransport, app detection)test/{registration,jxa}.test.ts— fast unit tests (no apps needed)test/{numbers,pages,keynote}.test.ts— integration tests (require apps)
npm test— unit tests (registration + jxa), ~300ms, no apps needed, runs in CInpm run test:integration— CRUD tests for Numbers/Pages/Keynote, ~8s, needs appsnpm run test:all— both tiers combined- Uses
node:test+tsx, in-memory MCP transport (no subprocess) - 102 tests total: 34 unit + 68 integration
npm run build->tsc && chmod +x dist/index.jsnpm publishrequires 2FA (OTP). User publishes manually.- Always bump version before publish (npm rejects duplicates)
- Keep version in sync: bump -> build -> test -> commit -> push -> publish -> GitHub release
- After each npm publish, create the matching GitHub release server-side:
gh release create vX.Y.Z --generate-notes. GitHub Releases do NOT update automatically on publish — they lag behind npm unless this step runs (v0.8.6 sat as "Latest" long after 0.8.7 shipped). - NEVER
git push --tags— stale local tags may pin old, rewritten history. Always create tags server-side viagh release create. - Commits must use the noreply email (
[email protected]); it's set as repo-local git config. Never commit with a personal address. - Screenshots must be JPEG not PNG — large PNGs cause 503 errors on GitHub README rendering
- All 6 Pages text tools (pages_add_text, pages_get_paragraphs, pages_format_text, pages_insert_text_at, pages_delete_text, pages_replace_text) work via
doc.bodyText.paragraphsworkarounds. The originaldoc.paragraphsAPI remains broken on Pages 14.5. - Pages tables are not accessible via JXA (-2763) but ARE via AppleScript: pages_list_tables, pages_read_table, pages_write_table_cells, and pages_resize_table use the NSAppleScript bridge. On Creator Studio 15.x, table creation is broken in both JXA and AppleScript — pages_add_table falls back to a UI-scripting workaround there (see Critical JXA Bugs) which requires Accessibility permission for the host app.
- Keynote shape fill/border colors are not exposed by JXA. Only opacity, rotation, and text formatting are settable via
keynote_format_shape. - Pages paragraph styles (Title, Heading 1, Body, etc.) are NOT exposed by Apple's scripting dictionary. Only font, size, and color are accessible. No alignment, indent, or line spacing either.