@thenavidm/apple-photos-mcp-cli 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -24
- package/SKILL.md +8 -10
- package/lib/app.d.ts +14 -0
- package/lib/app.js +138 -0
- package/lib/app.js.map +1 -0
- package/lib/bridge.d.ts +6 -6
- package/lib/bridge.js +40 -46
- package/lib/bridge.js.map +1 -1
- package/lib/config.d.ts +6 -4
- package/lib/config.js +19 -25
- package/lib/config.js.map +1 -1
- package/lib/engine.d.ts +44 -0
- package/lib/engine.js +127 -0
- package/lib/engine.js.map +1 -0
- package/lib/errors.d.ts +0 -3
- package/lib/errors.js +0 -3
- package/lib/errors.js.map +1 -1
- package/lib/index.d.ts +10 -6
- package/lib/index.js +17 -104
- package/lib/index.js.map +1 -1
- package/lib/npx.d.ts +2 -0
- package/lib/npx.js +14 -0
- package/lib/npx.js.map +1 -0
- package/lib/tools/index.d.ts +20 -11
- package/lib/tools/index.js +6 -2
- package/lib/tools/index.js.map +1 -1
- package/lib/tools/kit.d.ts +26 -24
- package/lib/tools/kit.js +68 -44
- package/lib/tools/kit.js.map +1 -1
- package/lib/version.d.ts +2 -0
- package/lib/version.js +4 -0
- package/lib/version.js.map +1 -0
- package/package.json +5 -5
- package/lib/cli.d.ts +0 -105
- package/lib/cli.js +0 -644
- package/lib/cli.js.map +0 -1
- package/lib/safety.d.ts +0 -27
- package/lib/safety.js +0 -70
- package/lib/safety.js.map +0 -1
- package/lib/server.d.ts +0 -10
- package/lib/server.js +0 -38
- package/lib/server.js.map +0 -1
- package/src/apple_photos_mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/__main__.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/config.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/doctor.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/library.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/previews.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/safety.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/search.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/server.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/writes.cpython-313.pyc +0 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Apple Photos MCP server and CLI for Claude Code, Codex and AI agents. 13 tools t
|
|
|
12
12
|
|
|
13
13
|
Give any AI agent real access to your own Apple Photos library, so it can find, see, organise and export your photos instead of guessing.
|
|
14
14
|
|
|
15
|
-
Built and maintained by [Navid Moazzez](https://navid.me?utm_source=github&utm_medium=referral&utm_campaign=apple-photos-mcp-cli&utm_content=readme).
|
|
15
|
+
Built and maintained by [Navid Moazzez](https://navid.me?utm_source=github&utm_medium=referral&utm_campaign=apple-photos-mcp-cli&utm_content=readme). Built on [Slipway](https://github.com/thenavidm/slipway), which turns one definition of each tool into the MCP server and the CLI.
|
|
16
16
|
|
|
17
17
|
<img src="https://cdn.navid.media/repos/apple-photos-mcp.gif?v=1" alt="Claude Code using the Apple Photos MCP server" width="520">
|
|
18
18
|
|
|
@@ -31,6 +31,7 @@ apple-photos-cli library-stats --json # real totals, one call
|
|
|
31
31
|
apple-photos-cli search-photos "sunset" --limit 5 --screenshots exclude
|
|
32
32
|
apple-photos-cli photo-info --refs IMG_2073.MOV
|
|
33
33
|
apple-photos-cli export-originals --refs <uuid> --directory ./out
|
|
34
|
+
apple-photos-cli which save my photos to disk # find the command for a task
|
|
34
35
|
apple-photos-cli <command> --help # what any command takes
|
|
35
36
|
```
|
|
36
37
|
|
|
@@ -76,10 +77,10 @@ the tool name with dashes.
|
|
|
76
77
|
| Title and description | `apple-photos-cli set-photo-title` / `set-photo-description` | `set_photo_title` / `set_photo_description` |
|
|
77
78
|
| Keywords and albums | `apple-photos-cli add-keywords` / `add-to-album` | `add_keywords` / `add_to_album` |
|
|
78
79
|
| Move into an archive album | `apple-photos-cli archive-photos` | `archive_photos` |
|
|
79
|
-
| Check the setup | `apple-photos-cli doctor` | `doctor` |
|
|
80
|
+
| Check the setup | `apple-photos-cli doctor`, or `check-setup` for the engine's report alone | `doctor` |
|
|
80
81
|
| Find the right command | `apple-photos-cli which "..."` | not a tool |
|
|
81
82
|
|
|
82
|
-
All 13 with their arguments are in [section 6](#6-tools
|
|
83
|
+
All 13 with their arguments are in [section 6](#6-tools-%EF%B8%8F).
|
|
83
84
|
|
|
84
85
|
## Output and exit codes
|
|
85
86
|
|
|
@@ -91,10 +92,12 @@ Results on stdout, errors on stderr as JSON, so one parse handles both.
|
|
|
91
92
|
| `--json` | JSON, always |
|
|
92
93
|
| `--compact` | the same JSON on one line |
|
|
93
94
|
| `--select a,b.c` | keep only these fields |
|
|
95
|
+
| `--agent` | one-line JSON, no prompts and no color, for a script or an agent |
|
|
94
96
|
|
|
95
97
|
| Code | Means |
|
|
96
98
|
|---|---|
|
|
97
99
|
| `0` | it worked |
|
|
100
|
+
| `1` | something unexpected went wrong |
|
|
98
101
|
| `2` | it was typed wrong, or a write was refused: a missing flag, a bad value, no `--confirm`, or read only |
|
|
99
102
|
| `3` | a photo or album wasn't found |
|
|
100
103
|
| `4` | macOS refused access: Full Disk Access, or Automation for Photos |
|
|
@@ -103,11 +106,49 @@ Results on stdout, errors on stderr as JSON, so one parse handles both.
|
|
|
103
106
|
|
|
104
107
|
## Which surface, and what each costs
|
|
105
108
|
|
|
106
|
-
|
|
109
|
+
Both surfaces are the same program with the same 13 tools. The
|
|
110
|
+
difference is when the model pays for them. Measured in Claude Code:
|
|
107
111
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
| | MCP server | CLI |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| Every message, with every tool loaded | 3,400 tokens | nothing |
|
|
115
|
+
| Every message, Claude Code's default | 650 tokens | nothing |
|
|
116
|
+
| When Apple Photos comes up | nothing more, or the tools it picks | 2,500 tokens for `SKILL.md`, once |
|
|
117
|
+
| 20 messages with Apple Photos in 1, every tool loaded | 68,000 tokens | 2,500 tokens |
|
|
118
|
+
|
|
119
|
+
Claude Code's [tool search](https://code.claude.com/docs/en/mcp#scale-with-mcp-tool-search)
|
|
120
|
+
is on by default: it sends only the tool names and the server instructions,
|
|
121
|
+
and loads a tool's full definition when the model reaches for it. An app that
|
|
122
|
+
loads every tool up front pays the first line on every message, whether
|
|
123
|
+
Apple Photos comes up or not. With the skill added, Claude Code also lists its
|
|
124
|
+
one-line description, about 180 tokens.
|
|
125
|
+
|
|
126
|
+
To spend less, turn the server off when you are not using it, which in Claude
|
|
127
|
+
Code is the `/mcp` panel. `APPLE_PHOTOS_READ_ONLY=1` takes the 6 write tools off the list, leaving 7.
|
|
128
|
+
Or install the CLI and add the server on the days it earns its place.
|
|
129
|
+
|
|
130
|
+
Measured on 2026-10-05 against 1.1.1, with Claude Code 2.1.286 on Claude Opus
|
|
131
|
+
5.5 (one short prompt with and without the server connected, once with
|
|
132
|
+
`ENABLE_TOOL_SEARCH=false` and once with the default, the difference read from
|
|
133
|
+
the API's own usage figures; `SKILL.md` the same way) and Codex 0.159.3 on
|
|
134
|
+
gpt-6.1-sol, with an empty home folder and an engine that could not start, so
|
|
135
|
+
nothing read the library:
|
|
136
|
+
|
|
137
|
+
| Cost | 1.1.1 | 2.0.0 |
|
|
138
|
+
| --- | --- | --- |
|
|
139
|
+
| Claude Code, every tool loaded, every message | 3,843 | 3,402 |
|
|
140
|
+
| Claude Code's default, tool search, every message | 656 | 654 |
|
|
141
|
+
| `SKILL.md`, read once | 2,510 | 2,490 |
|
|
142
|
+
| Codex over the CLI, one task, median of five | 191,767 | 53,232 |
|
|
143
|
+
| Codex over MCP, the same task, median of five | 39,915 | 39,959 |
|
|
144
|
+
|
|
145
|
+
The task was "find the command that exports original photo files to a folder
|
|
146
|
+
on this Mac, and the flags it requires". In 1.1.1, `<command> --help` printed
|
|
147
|
+
the general help, so every run tried seven to ten commands before it found the
|
|
148
|
+
flags; every 2.0.0 run read the general help and the command's own: two. Over
|
|
149
|
+
MCP the server's printout was shorter in every 2.0.0 run, and the medians
|
|
150
|
+
differ by the model's own replies. Other apps and models count tokens a little
|
|
151
|
+
differently, and tool-list characters divided by four are not API usage.
|
|
111
152
|
|
|
112
153
|
## Contents
|
|
113
154
|
|
|
@@ -120,11 +161,11 @@ not. The CLI is free until you run something, which is why both exist.
|
|
|
120
161
|
| 3 | [Setup](#3-setup-) | One permission, once |
|
|
121
162
|
| 4 | [Connect your client](#4-connect-your-client-) | Every client, copy and paste |
|
|
122
163
|
| 5 | [Check it worked](#5-check-it-worked-) | `doctor` |
|
|
123
|
-
| 6 | [Tools](#6-tools
|
|
124
|
-
| 7 | [Working safely](#7-working-safely
|
|
164
|
+
| 6 | [Tools](#6-tools-%EF%B8%8F) | All 13 |
|
|
165
|
+
| 7 | [Working safely](#7-working-safely-%EF%B8%8F) | What asks, what does not |
|
|
125
166
|
| 8 | [What Apple Photos actually does](#8-what-apple-photos-actually-does-) | The things that surprise people |
|
|
126
167
|
| 9 | [Your data](#9-your-data-) | What is stored, and where |
|
|
127
|
-
| 10 | [Configuration](#10-configuration
|
|
168
|
+
| 10 | [Configuration](#10-configuration-%EF%B8%8F) | Every setting |
|
|
128
169
|
| 11 | [Troubleshooting](#11-troubleshooting-) | When something breaks |
|
|
129
170
|
| 12 | [FAQ](#12-faq-) | Start here if you are new |
|
|
130
171
|
|
|
@@ -324,13 +365,11 @@ Or just ask your agent: **"run doctor on apple photos"**.
|
|
|
324
365
|
|
|
325
366
|
Healthy output includes a line like:
|
|
326
367
|
|
|
327
|
-
```json
|
|
328
|
-
{
|
|
329
|
-
"check": "index",
|
|
330
|
-
"ok": true,
|
|
331
|
-
"detail": "37129 assets indexed (35983 with ML labels, 10807 with readable text)"
|
|
332
|
-
}
|
|
333
368
|
```
|
|
369
|
+
✓ index 37129 assets indexed (35983 with ML labels, 10807 with readable text)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`--json` prints the same checks as JSON, and `apple-photos-cli check-setup` prints the engine's own report.
|
|
334
373
|
|
|
335
374
|
The two things that actually fail:
|
|
336
375
|
|
|
@@ -355,7 +394,7 @@ The two things that actually fail:
|
|
|
355
394
|
| Tool | What it does |
|
|
356
395
|
|---|---|
|
|
357
396
|
| `library_stats` | Totals, albums, named people, how much is iCloud only |
|
|
358
|
-
| `doctor` | Diagnose setup |
|
|
397
|
+
| `doctor` | Diagnose setup. On the CLI it is `check-setup`, because `doctor` there runs these checks with the CLI's own |
|
|
359
398
|
|
|
360
399
|
**Organizing**
|
|
361
400
|
|
|
@@ -366,7 +405,7 @@ The two things that actually fail:
|
|
|
366
405
|
| `add_keywords` | Add keywords, keeping existing ones |
|
|
367
406
|
| `set_photo_title` | Set one item's title |
|
|
368
407
|
| `set_photo_description` | Set one item's caption |
|
|
369
|
-
| `archive_photos` | Move to an archive album. Needs
|
|
408
|
+
| `archive_photos` | Move to an archive album. Needs confirming |
|
|
370
409
|
|
|
371
410
|
**Getting files out**
|
|
372
411
|
|
|
@@ -380,11 +419,11 @@ Organizing works out of the box, because that is the point of the tool. Three th
|
|
|
380
419
|
|
|
381
420
|
**Nothing can delete a photo.** macOS does not expose scripted deletion to any application, and this server does not try to route around that. `archive_photos` moves items into an album so they leave your main view, and you empty that album yourself.
|
|
382
421
|
|
|
383
|
-
**`archive_photos` is the only tool that asks.**
|
|
422
|
+
**`archive_photos` is the only tool that asks.** Over MCP a person approves it: Claude Code (2.1.246 and later) shows its own prompt, and an app that can show forms asks with an approval form whose one box starts unticked. Where an app can do neither, the model's `confirm: true` still counts, and `APPLE_PHOTOS_CONFIRM=model` makes it enough everywhere; in a terminal it is `--confirm`, which `--agent` never adds. `APPLE_PHOTOS_ALLOW_DESTRUCTIVE=0` refuses it, confirmed or not. Everything else it can do is one click to undo in Photos, and requiring confirmation on all of them would just teach a model to pass `confirm` reflexively, including on the one that matters.
|
|
384
423
|
|
|
385
424
|
**`APPLE_PHOTOS_READ_ONLY=1` removes every write tool.** They vanish from the tool list rather than erroring when called, because a model cannot call a tool it cannot see. This is the right setting for an agent working unattended.
|
|
386
425
|
|
|
387
|
-
`APPLE_PHOTOS_AUDIT_LOG=~/.apple-photos-mcp/writes.jsonl` records one JSON line per attempted write, allowed and blocked alike.
|
|
426
|
+
`APPLE_PHOTOS_AUDIT_LOG=~/.apple-photos-mcp/writes.jsonl` records one JSON line per attempted write, allowed and blocked alike, with who approved it, then whether it was done or failed.
|
|
388
427
|
|
|
389
428
|
Every tool carries MCP annotations, so a client can decide what to auto-approve: reads are marked read-only, organizing is marked non-destructive, and `archive_photos` is marked destructive. Nothing is marked open-world, because nothing here leaves your machine.
|
|
390
429
|
|
|
@@ -428,7 +467,13 @@ Every setting is an environment variable, set in your client's `env` block.
|
|
|
428
467
|
| Variable | Default | What it does |
|
|
429
468
|
|---|---|---|
|
|
430
469
|
| `APPLE_PHOTOS_READ_ONLY` | off | Removes every write tool from the tool list |
|
|
470
|
+
| `APPLE_PHOTOS_ALLOW_DESTRUCTIVE` | on | `0` refuses `archive_photos`, confirmed or not |
|
|
471
|
+
| `APPLE_PHOTOS_CONFIRM` | `human` | `model` lets `confirm: true` alone approve an archive over MCP, for an agent with no person to ask |
|
|
431
472
|
| `APPLE_PHOTOS_AUDIT_LOG` | off | Path for one JSON line per attempted write |
|
|
473
|
+
| `APPLE_PHOTOS_PYTHON` | `uv` | A Python that has osxphotos and photoscript; uv fetches them on demand when unset |
|
|
474
|
+
| `APPLE_PHOTOS_PYTHONPATH` | the copy in this package | Where the engine's package lives |
|
|
475
|
+
| `APPLE_PHOTOS_STARTUP_TIMEOUT_MS` | `300000` | How long the engine may take to start, since a cold uv run builds pyobjc |
|
|
476
|
+
| `APPLE_PHOTOS_REQUEST_TIMEOUT_MS` | `300000` | How long one call may take |
|
|
432
477
|
| `APPLE_PHOTOS_LIBRARY` | last opened | Path to a specific `.photoslibrary` |
|
|
433
478
|
| `APPLE_PHOTOS_EXPORT_DIR` | `~/Downloads/Photos Exports` | Default export folder |
|
|
434
479
|
| `APPLE_PHOTOS_PREVIEW_DIR` | `~/.apple-photos-mcp/previews` | Where previews are cached |
|
|
@@ -436,6 +481,11 @@ Every setting is an environment variable, set in your client's `env` block.
|
|
|
436
481
|
| `APPLE_PHOTOS_PREVIEW_MAX` | `8` | Most items rendered per `look_at_photos` call |
|
|
437
482
|
| `APPLE_PHOTOS_WRITE_BATCH_MAX` | `100` | Most items a single write may touch |
|
|
438
483
|
| `APPLE_PHOTOS_ARCHIVE_ALBUM` | `Archived by Claude` | Album `archive_photos` moves items into |
|
|
484
|
+
| `APPLE_PHOTOS_SURFACE` | `full` | `search` lists three tools that find, describe and run the rest |
|
|
485
|
+
| `APPLE_PHOTOS_TOOL_TIMEOUT_MS` | none | Give up on any tool after this long |
|
|
486
|
+
| `APPLE_PHOTOS_HTTP_PORT`, `APPLE_PHOTOS_HTTP_HOST`, `APPLE_PHOTOS_HTTP_TOKEN` | 8787, 127.0.0.1, none | For `--http`. Any host but 127.0.0.1 needs the bearer token |
|
|
487
|
+
| `APPLE_PHOTOS_HTTP_ALLOWED_ORIGINS` | none | Comma-separated browser origins allowed to call `--http`; a page from any other site is refused |
|
|
488
|
+
| `APPLE_PHOTOS_DEBUG` | `0` | `1` prints debug lines on stderr |
|
|
439
489
|
|
|
440
490
|
## 11. Troubleshooting 🔧
|
|
441
491
|
|
|
@@ -471,7 +521,7 @@ A standard way to give an AI assistant real access to a tool, so it can act rath
|
|
|
471
521
|
<details>
|
|
472
522
|
<summary><b>Should I use the MCP server or the CLI?</b></summary>
|
|
473
523
|
|
|
474
|
-
Use the MCP server in an app with no terminal, like Claude Desktop's chat. Use the CLI anywhere commands run: an agent like Claude Code, Codex or OpenCode, a script or a cron job. The MCP server
|
|
524
|
+
Use the MCP server in an app with no terminal, like Claude Desktop's chat. Use the CLI anywhere commands run: an agent like Claude Code, Codex or OpenCode, a script or a cron job. The MCP server's tools take up context on every message, and the CLI costs nothing until it runs.
|
|
475
525
|
|
|
476
526
|
</details>
|
|
477
527
|
|
|
@@ -568,7 +618,7 @@ Run into a problem or have a question? [Open an issue](https://github.com/thenav
|
|
|
568
618
|
|
|
569
619
|
## About the author
|
|
570
620
|
|
|
571
|
-
Navid Moazzez is a leading AI business strategist, and the host of the AI Creator Summit, watched by 100,000+ creators. He helps creators and founders master AI and build their own AI Operating System (AI OS) to automate their business and life.
|
|
621
|
+
Navid Moazzez is a leading AI business strategist, and the host of the AI Creator Summit, watched by 100,000+ creators. He helps creators and founders master AI and build their own AI Operating System (AI OS) to automate their business and life. He creates useful free tools, MCP servers and CLIs that creators and founders can use in their own workflows.
|
|
572
622
|
|
|
573
623
|
**Links**
|
|
574
624
|
|
|
@@ -586,7 +636,9 @@ If this is useful, star the repo and come say hi on [X](https://x.com/thenavidm)
|
|
|
586
636
|
|
|
587
637
|
| Package | License | Why |
|
|
588
638
|
|---|---|---|
|
|
589
|
-
| [
|
|
639
|
+
| [Slipway](https://github.com/thenavidm/slipway) | Apache-2.0 | The MCP server and the CLI from one definition of each tool |
|
|
640
|
+
| [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) | Apache-2.0 | The MCP protocol and its transports, through Slipway |
|
|
641
|
+
| [mcp](https://github.com/modelcontextprotocol/python-sdk) | MIT | The MCP protocol for the Python engine |
|
|
590
642
|
| [osxphotos](https://github.com/RhetTbull/osxphotos) | MIT | Reads the Photos library database and Apple's on-device ML metadata |
|
|
591
643
|
| [photoscript](https://github.com/RhetTbull/photoscript) | MIT | Drives Photos.app for writes |
|
|
592
644
|
|
|
@@ -598,4 +650,4 @@ Not affiliated with, endorsed by, or connected to Apple Inc. Apple, macOS, Photo
|
|
|
598
650
|
|
|
599
651
|
---
|
|
600
652
|
|
|
601
|
-
© 2026 [
|
|
653
|
+
© 2026 [Navid Media](https://navid.media?utm_source=github&utm_medium=referral&utm_campaign=apple-photos-mcp-cli&utm_content=readme). Made with ❤️ by [Navid Moazzez](https://navid.me?utm_source=github&utm_medium=referral&utm_campaign=apple-photos-mcp-cli&utm_content=readme).
|
package/SKILL.md
CHANGED
|
@@ -26,6 +26,7 @@ piping and one-off questions, and costs no context until it is called.
|
|
|
26
26
|
apple-photos-cli library-stats --json
|
|
27
27
|
apple-photos-cli search-photos "sunset" --limit 5 --screenshots exclude
|
|
28
28
|
apple-photos-cli photo-info --refs IMG_2073.MOV
|
|
29
|
+
apple-photos-cli which <words>
|
|
29
30
|
apple-photos-cli <command> --help
|
|
30
31
|
```
|
|
31
32
|
|
|
@@ -61,8 +62,6 @@ into the library. That is the index:
|
|
|
61
62
|
| Faces | Only people the user has actually named in Photos |
|
|
62
63
|
| Albums, keywords, titles | Only what the user typed themselves |
|
|
63
64
|
|
|
64
|
-
Two consequences worth internalizing.
|
|
65
|
-
|
|
66
65
|
**The visual vocabulary is closed.** Apple knows "Sunset" but not "golden hour",
|
|
67
66
|
"Crowd" but not "keynote", "Dog" but not "goofy". If a result comes back with
|
|
68
67
|
`unmatched_terms`, that word does not exist in this library and rephrasing the
|
|
@@ -97,9 +96,8 @@ Useful habits:
|
|
|
97
96
|
look_at_photos(refs=["<uuid>", "<uuid>", "<uuid>"])
|
|
98
97
|
```
|
|
99
98
|
|
|
100
|
-
This works even
|
|
101
|
-
|
|
102
|
-
It is fast, so use it freely.
|
|
99
|
+
This works even for iCloud-only photos: it reads the thumbnail already on this
|
|
100
|
+
Mac, so it is fast. Use it freely.
|
|
103
101
|
|
|
104
102
|
Up to eight items render per call. Ask for a bigger `size` only when fine detail
|
|
105
103
|
matters, such as reading text off a receipt.
|
|
@@ -125,9 +123,9 @@ Each of those is one click to undo in Photos, so none of them ask first.
|
|
|
125
123
|
archive_photos(refs=[...], confirm=True)
|
|
126
124
|
```
|
|
127
125
|
|
|
128
|
-
`archive_photos` is the only tool that
|
|
129
|
-
confirm
|
|
130
|
-
|
|
126
|
+
`archive_photos` is the only tool that needs confirming: the user approves it
|
|
127
|
+
over MCP, or `confirm: true` where the app cannot ask, and `--confirm` on the
|
|
128
|
+
CLI. Call it only once the user has asked for those specific items to go.
|
|
131
129
|
|
|
132
130
|
**Nothing deletes.** macOS does not let any app delete photos by script. This
|
|
133
131
|
moves items into an album for the user to empty by hand. Always say so rather
|
|
@@ -149,7 +147,7 @@ wants to see a photo, look at it instead.
|
|
|
149
147
|
library_stats() # totals, albums, named people, how much is in iCloud
|
|
150
148
|
list_vocabulary() # the visual words this library knows
|
|
151
149
|
photo_info(refs=[...]) # everything about specific items, including OCR text
|
|
152
|
-
doctor() # run this first whenever anything misbehaves
|
|
150
|
+
doctor() # run this first whenever anything misbehaves; apple-photos-cli doctor
|
|
153
151
|
```
|
|
154
152
|
|
|
155
153
|
## Exit codes
|
|
@@ -159,6 +157,7 @@ A script branches on the number, not the message:
|
|
|
159
157
|
| Code | Means |
|
|
160
158
|
|---|---|
|
|
161
159
|
| 0 | it worked |
|
|
160
|
+
| 1 | something unexpected went wrong |
|
|
162
161
|
| 2 | typed wrong, or a write was refused: a missing flag, a bad value, no `--confirm`, or read only |
|
|
163
162
|
| 3 | a photo or album was not found |
|
|
164
163
|
| 4 | macOS refused access: Full Disk Access, or Automation for Photos |
|
|
@@ -170,7 +169,6 @@ A script branches on the number, not the message:
|
|
|
170
169
|
| Symptom | Cause | Fix |
|
|
171
170
|
|---|---|---|
|
|
172
171
|
| Permission denied on the library | No Full Disk Access | System Settings, Privacy & Security, Full Disk Access. Add the app running the server, then fully quit and reopen it. |
|
|
173
|
-
| Write tools are missing entirely | `APPLE_PHOTOS_READ_ONLY=1` is set | Unset it and restart the client |
|
|
174
172
|
| A write fails but reads work | Photos automation not allowed | Approve the one-time popup, or System Settings, Privacy & Security, Automation |
|
|
175
173
|
| No preview for one photo | iCloud-only with no cached thumbnail | Open it once in Photos, or export it |
|
|
176
174
|
| Library not found | Photos has never run, or a non-default library | Set `APPLE_PHOTOS_LIBRARY` to the .photoslibrary path |
|
package/lib/app.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Apple Photos app: everything Slipway needs to ship the MCP server and the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Mac only. The tools are declared here in TypeScript and run by the Python
|
|
5
|
+
* engine, which reads the library through osxphotos and photoscript; the
|
|
6
|
+
* bridge starts it on the first call. This file only describes; `index.ts` runs.
|
|
7
|
+
*/
|
|
8
|
+
import type { ToolContext } from "./tools/kit.js";
|
|
9
|
+
export type AppOptions = {
|
|
10
|
+
/** Replace how calls reach the engine, for tests that stand a fake one in. */
|
|
11
|
+
context?: (env: NodeJS.ProcessEnv) => ToolContext;
|
|
12
|
+
};
|
|
13
|
+
export declare function createApp(options?: AppOptions): import("@thenavidm/slipway").App<ToolContext>;
|
|
14
|
+
export declare const app: import("@thenavidm/slipway").App<ToolContext>;
|
package/lib/app.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Apple Photos app: everything Slipway needs to ship the MCP server and the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Mac only. The tools are declared here in TypeScript and run by the Python
|
|
5
|
+
* engine, which reads the library through osxphotos and photoscript; the
|
|
6
|
+
* bridge starts it on the first call. This file only describes; `index.ts` runs.
|
|
7
|
+
*/
|
|
8
|
+
import { slipway } from "@thenavidm/slipway";
|
|
9
|
+
import { PythonBridge } from "./bridge.js";
|
|
10
|
+
import { loadConfig } from "./config.js";
|
|
11
|
+
import { PhotosError } from "./errors.js";
|
|
12
|
+
import { TOOLS } from "./tools/index.js";
|
|
13
|
+
import { VERSION } from "./version.js";
|
|
14
|
+
const INSTRUCTIONS = `Tools for the user's own Apple Photos library, read directly on this Mac. Nothing is uploaded anywhere.
|
|
15
|
+
|
|
16
|
+
How to actually find something:
|
|
17
|
+
|
|
18
|
+
1. search_photos first. It queries Apple's own on-device index across the whole library, so the match count is library-wide; \`limit\` caps what comes back, not what is searched.
|
|
19
|
+
2. Then look_at_photos on the top few. Search returns candidates, not answers, and filenames tell you nothing. Look before you describe, recommend or choose.
|
|
20
|
+
3. Only then reply, naming the file and the date so the user can find it.
|
|
21
|
+
|
|
22
|
+
Three things that prevent confident wrong answers:
|
|
23
|
+
|
|
24
|
+
- For anything about scale or proportion, call library_stats. It gives real totals in one call. Counting by running searches and reading results double-counts overlapping terms and cannot see the items that carry no place or label, which in a typical library is thousands.
|
|
25
|
+
- Apple's visual vocabulary is closed, about 1,500 words. If a result carries \`unmatched_terms\`, Apple has never heard of that word and rephrasing the same idea will not help. Read \`did_you_mean\`, or call list_vocabulary.
|
|
26
|
+
- Screenshots are often a fifth of a library and skew every count. Use \`screenshots: "exclude"\` when the question is about photographs.
|
|
27
|
+
|
|
28
|
+
Nothing here can delete a photo; macOS does not permit it. archive_photos moves items into an album for the user to empty by hand, and it is the one tool that asks for confirmation.`;
|
|
29
|
+
/**
|
|
30
|
+
* The engine's own doctor: macOS, Python, Full Disk Access, the library, its
|
|
31
|
+
* index and the write switches, each as one check. When the engine cannot even
|
|
32
|
+
* start, that is the one check, with how to fix it.
|
|
33
|
+
*/
|
|
34
|
+
async function doctor({ bridge }) {
|
|
35
|
+
try {
|
|
36
|
+
const result = await bridge.call("doctor", {});
|
|
37
|
+
const report = JSON.parse(result.text);
|
|
38
|
+
return (report.checks ?? []).map((check) => ({ name: check.check, ok: check.ok, detail: check.detail, ...(check.fix ? { fix: check.fix } : {}) }));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// The message carries the advice, which goes in fix; the cause is its first sentence and the engine's own words.
|
|
42
|
+
const cause = error.message.split(/(?<=`)\. /)[0] ?? error.message;
|
|
43
|
+
const detail = error instanceof PhotosError && error.detail ? `${cause}: ${error.detail.split("\n")[0]}` : cause;
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
name: "Photos engine",
|
|
47
|
+
ok: false,
|
|
48
|
+
detail,
|
|
49
|
+
fix: "Install uv (https://docs.astral.sh/uv/) or set APPLE_PHOTOS_PYTHON to a Python that has osxphotos and photoscript.",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** 1.1's spellings for the refs and the folder, which `which` and help still accept. */
|
|
55
|
+
const FLAG_ALIASES = {
|
|
56
|
+
uuid: "refs",
|
|
57
|
+
uuids: "refs",
|
|
58
|
+
ref: "refs",
|
|
59
|
+
file: "refs",
|
|
60
|
+
files: "refs",
|
|
61
|
+
out: "directory",
|
|
62
|
+
dir: "directory",
|
|
63
|
+
n: "limit",
|
|
64
|
+
};
|
|
65
|
+
/** The words people use for what the tools call something else, as 1.1's `which` read them. */
|
|
66
|
+
const SYNONYMS = {
|
|
67
|
+
save: ["export", "originals"],
|
|
68
|
+
saving: ["export", "originals"],
|
|
69
|
+
download: ["export", "originals"],
|
|
70
|
+
copy: ["export", "originals"],
|
|
71
|
+
disk: ["export", "originals"],
|
|
72
|
+
file: ["export", "originals"],
|
|
73
|
+
files: ["export", "originals"],
|
|
74
|
+
find: ["search"],
|
|
75
|
+
show: ["search", "look"],
|
|
76
|
+
see: ["look"],
|
|
77
|
+
view: ["look"],
|
|
78
|
+
preview: ["look"],
|
|
79
|
+
image: ["photos"],
|
|
80
|
+
images: ["photos"],
|
|
81
|
+
picture: ["photos"],
|
|
82
|
+
pictures: ["photos"],
|
|
83
|
+
pic: ["photos"],
|
|
84
|
+
pics: ["photos"],
|
|
85
|
+
video: ["photos"],
|
|
86
|
+
videos: ["photos"],
|
|
87
|
+
tag: ["keywords"],
|
|
88
|
+
tags: ["keywords"],
|
|
89
|
+
label: ["keywords"],
|
|
90
|
+
labels: ["keywords"],
|
|
91
|
+
caption: ["description"],
|
|
92
|
+
name: ["title"],
|
|
93
|
+
heart: ["favourite", "favorite"],
|
|
94
|
+
star: ["favourite", "favorite"],
|
|
95
|
+
like: ["favourite", "favorite"],
|
|
96
|
+
delete: ["archive"],
|
|
97
|
+
};
|
|
98
|
+
export function createApp(options = {}) {
|
|
99
|
+
return slipway({
|
|
100
|
+
name: "apple-photos",
|
|
101
|
+
title: "Apple Photos",
|
|
102
|
+
version: VERSION,
|
|
103
|
+
package: "@thenavidm/apple-photos-mcp-cli",
|
|
104
|
+
envPrefix: "APPLE_PHOTOS",
|
|
105
|
+
description: "Search, look at and organize the Apple Photos library on this Mac, through Apple's own on-device index. Nothing is uploaded.",
|
|
106
|
+
instructions: INSTRUCTIONS,
|
|
107
|
+
context: options.context ??
|
|
108
|
+
((env) => {
|
|
109
|
+
const config = loadConfig(env);
|
|
110
|
+
return { config, bridge: new PythonBridge(config) };
|
|
111
|
+
}),
|
|
112
|
+
// Nothing to sign in to: macOS decides who may read the library, and doctor says whether it lets this.
|
|
113
|
+
configured: () => true,
|
|
114
|
+
tools: TOOLS,
|
|
115
|
+
doctor,
|
|
116
|
+
// The checks are the engine's, on this Mac, so they run on every doctor rather than only with --network.
|
|
117
|
+
doctorNetwork: true,
|
|
118
|
+
flagAliases: FLAG_ALIASES,
|
|
119
|
+
synonyms: SYNONYMS,
|
|
120
|
+
login: "There is no sign-in: the library is on this Mac, and macOS decides who may read it. The first run asks for permission; if it is refused, give the app that launches this server Full Disk Access, then run apple-photos-cli doctor.",
|
|
121
|
+
settings: [
|
|
122
|
+
{ env: "APPLE_PHOTOS_PYTHON", description: "A Python that has osxphotos and photoscript; uv fetches them on demand when unset." },
|
|
123
|
+
{ env: "APPLE_PHOTOS_LIBRARY", description: "Another Photos library; the system library when unset." },
|
|
124
|
+
{ env: "APPLE_PHOTOS_EXPORT_DIR", description: "Where export_originals writes; ~/Downloads/Photos Exports when unset." },
|
|
125
|
+
{ env: "APPLE_PHOTOS_ARCHIVE_ALBUM", description: "The album archive_photos moves items into." },
|
|
126
|
+
{ env: "APPLE_PHOTOS_PYTHONPATH", description: "Where the engine's package lives; the copy shipped with this package when unset.", tuning: true },
|
|
127
|
+
{ env: "APPLE_PHOTOS_STARTUP_TIMEOUT_MS", description: "How long the engine may take to start; 300000, since a cold uv run builds pyobjc.", tuning: true },
|
|
128
|
+
{ env: "APPLE_PHOTOS_REQUEST_TIMEOUT_MS", description: "How long one call may take; 300000.", tuning: true },
|
|
129
|
+
{ env: "APPLE_PHOTOS_PREVIEW_DIR", description: "Where look_at_photos renders previews.", tuning: true },
|
|
130
|
+
{ env: "APPLE_PHOTOS_PREVIEW_PX", description: "The default preview size in pixels.", tuning: true },
|
|
131
|
+
{ env: "APPLE_PHOTOS_PREVIEW_MAX", description: "How many previews one call renders.", tuning: true },
|
|
132
|
+
{ env: "APPLE_PHOTOS_WRITE_BATCH_MAX", description: "How many items one write may touch.", tuning: true },
|
|
133
|
+
],
|
|
134
|
+
links: { repository: "https://github.com/thenavidm/apple-photos-mcp-cli" },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
export const app = createApp();
|
|
138
|
+
//# sourceMappingURL=app.js.map
|
package/lib/app.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src-ts/app.ts"],"names":[],"mappings":"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"}
|
package/lib/bridge.d.ts
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* from the same tool array as the MCP server, and a static `ALL_TOOLS` that the
|
|
14
14
|
* HQ connector can import so the hosted surface cannot drift from this one.
|
|
15
15
|
*
|
|
16
|
-
* Calls are proxied to the Python MCP server over stdio
|
|
17
|
-
* client, so the Python needs no changes and there is one
|
|
18
|
-
* every tool.
|
|
16
|
+
* Calls are proxied to the Python MCP server over stdio, through the small
|
|
17
|
+
* client in engine.ts, so the Python needs no changes and there is one
|
|
18
|
+
* implementation of every tool.
|
|
19
19
|
*/
|
|
20
20
|
import type { Config } from "./config.js";
|
|
21
21
|
/** A content part as the MCP protocol carries it: text, image, or anything later. */
|
|
@@ -34,7 +34,7 @@ export type BridgeResult = {
|
|
|
34
34
|
};
|
|
35
35
|
export declare class PythonBridge {
|
|
36
36
|
private readonly config;
|
|
37
|
-
private
|
|
37
|
+
private engine?;
|
|
38
38
|
private starting?;
|
|
39
39
|
/** The tail of the engine's stderr, so a failure can say what actually went wrong. */
|
|
40
40
|
private stderrTail;
|
|
@@ -44,8 +44,8 @@ export declare class PythonBridge {
|
|
|
44
44
|
* Call one Python tool and return every content part.
|
|
45
45
|
*
|
|
46
46
|
* `timeoutMs` matters: exporting originals pulls them out of iCloud first and
|
|
47
|
-
* rendering previews is not quick either, so
|
|
48
|
-
*
|
|
47
|
+
* rendering previews is not quick either, so a short default would cancel
|
|
48
|
+
* work the engine is still doing.
|
|
49
49
|
*/
|
|
50
50
|
call(tool: string, args: Record<string, unknown>, timeoutMs?: number): Promise<BridgeResult>;
|
|
51
51
|
/** What the Python server says it offers, for checking the two lists agree. */
|
package/lib/bridge.js
CHANGED
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
* from the same tool array as the MCP server, and a static `ALL_TOOLS` that the
|
|
14
14
|
* HQ connector can import so the hosted surface cannot drift from this one.
|
|
15
15
|
*
|
|
16
|
-
* Calls are proxied to the Python MCP server over stdio
|
|
17
|
-
* client, so the Python needs no changes and there is one
|
|
18
|
-
* every tool.
|
|
16
|
+
* Calls are proxied to the Python MCP server over stdio, through the small
|
|
17
|
+
* client in engine.ts, so the Python needs no changes and there is one
|
|
18
|
+
* implementation of every tool.
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
21
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
20
|
+
import { StdioEngine } from "./engine.js";
|
|
22
21
|
import { BridgeError } from "./errors.js";
|
|
22
|
+
import { VERSION } from "./version.js";
|
|
23
23
|
export class PythonBridge {
|
|
24
24
|
config;
|
|
25
|
-
|
|
25
|
+
engine;
|
|
26
26
|
starting;
|
|
27
27
|
/** The tail of the engine's stderr, so a failure can say what actually went wrong. */
|
|
28
28
|
stderrTail = "";
|
|
@@ -30,47 +30,41 @@ export class PythonBridge {
|
|
|
30
30
|
this.config = config;
|
|
31
31
|
}
|
|
32
32
|
async connect() {
|
|
33
|
-
if (this.
|
|
34
|
-
return this.
|
|
33
|
+
if (this.engine)
|
|
34
|
+
return this.engine;
|
|
35
35
|
if (this.starting)
|
|
36
36
|
return this.starting;
|
|
37
37
|
this.starting = (async () => {
|
|
38
|
-
const
|
|
39
|
-
const transport = new StdioClientTransport({
|
|
38
|
+
const engine = new StdioEngine({
|
|
40
39
|
command: this.config.pythonCommand,
|
|
41
40
|
args: this.config.pythonArgs,
|
|
42
|
-
env:
|
|
43
|
-
|
|
41
|
+
env: this.config.pythonEnv,
|
|
42
|
+
clientInfo: { name: "apple-photos-cli", version: VERSION },
|
|
43
|
+
// Drained as it arrives: osxphotos is chatty on a large library, and an
|
|
44
|
+
// unread pipe fills and blocks the engine. Keeping the tail also means a
|
|
45
|
+
// failure can name the actual cause, a missing module or a Full Disk
|
|
46
|
+
// Access denial, instead of a boilerplate "install uv".
|
|
47
|
+
onStderr: (chunk) => {
|
|
48
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-4000);
|
|
49
|
+
},
|
|
50
|
+
// A dead engine must not stay cached, or one crash breaks every later
|
|
51
|
+
// call for the life of the process.
|
|
52
|
+
onClose: () => {
|
|
53
|
+
if (this.engine === engine)
|
|
54
|
+
this.engine = undefined;
|
|
55
|
+
},
|
|
44
56
|
});
|
|
45
|
-
// Drain stderr immediately. The SDK pipes it into a PassThrough with a
|
|
46
|
-
// 16KB buffer; with no reader that fills, the OS pipe fills behind it,
|
|
47
|
-
// and the child blocks writing to stderr. osxphotos is chatty on a large
|
|
48
|
-
// library, so this is a real hang, not a theoretical one. Keeping the
|
|
49
|
-
// tail also means a failure can name the actual cause — a missing module,
|
|
50
|
-
// a Full Disk Access denial — instead of a boilerplate "install uv".
|
|
51
|
-
transport.stderr?.on("data", (chunk) => {
|
|
52
|
-
this.stderrTail = (this.stderrTail + chunk.toString()).slice(-4000);
|
|
53
|
-
});
|
|
54
|
-
// A dead client must not stay cached, or one engine crash bricks every
|
|
55
|
-
// later call for the life of the process with "Connection closed".
|
|
56
|
-
const forget = () => {
|
|
57
|
-
if (this.client === client)
|
|
58
|
-
this.client = undefined;
|
|
59
|
-
};
|
|
60
|
-
client.onclose = forget;
|
|
61
|
-
client.onerror = forget;
|
|
62
57
|
try {
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
await client.connect(transport, { timeout: this.config.startupTimeoutMs });
|
|
58
|
+
// A cold `uv` run builds pyobjc, which takes longer than a minute on a
|
|
59
|
+
// fresh cache, so the wait is the configured startup timeout.
|
|
60
|
+
await engine.start(this.config.startupTimeoutMs);
|
|
67
61
|
}
|
|
68
62
|
catch (error) {
|
|
69
63
|
throw new BridgeError(`Could not start the Photos engine with \`${this.config.pythonCommand} ${this.config.pythonArgs.join(" ")}\`. ` +
|
|
70
64
|
`Install uv (https://docs.astral.sh/uv/) or set APPLE_PHOTOS_PYTHON to a Python that has osxphotos and photoscript.`, [error.message, this.stderrTail.trim()].filter(Boolean).join("\n"));
|
|
71
65
|
}
|
|
72
|
-
this.
|
|
73
|
-
return
|
|
66
|
+
this.engine = engine;
|
|
67
|
+
return engine;
|
|
74
68
|
})();
|
|
75
69
|
try {
|
|
76
70
|
return await this.starting;
|
|
@@ -83,36 +77,36 @@ export class PythonBridge {
|
|
|
83
77
|
* Call one Python tool and return every content part.
|
|
84
78
|
*
|
|
85
79
|
* `timeoutMs` matters: exporting originals pulls them out of iCloud first and
|
|
86
|
-
* rendering previews is not quick either, so
|
|
87
|
-
*
|
|
80
|
+
* rendering previews is not quick either, so a short default would cancel
|
|
81
|
+
* work the engine is still doing.
|
|
88
82
|
*/
|
|
89
83
|
async call(tool, args, timeoutMs) {
|
|
90
|
-
const
|
|
84
|
+
const engine = await this.connect();
|
|
91
85
|
let result;
|
|
92
86
|
try {
|
|
93
|
-
result = (await
|
|
87
|
+
result = (await engine.request("tools/call", { name: tool, arguments: args }, timeoutMs ?? this.config.requestTimeoutMs));
|
|
94
88
|
}
|
|
95
89
|
catch (error) {
|
|
96
90
|
throw new BridgeError(`The Photos engine failed while running ${tool}.`, [error.message, this.stderrTail.trim()].filter(Boolean).join("\n"));
|
|
97
91
|
}
|
|
98
|
-
const content = result
|
|
92
|
+
const content = result?.content ?? [];
|
|
99
93
|
const text = content
|
|
100
94
|
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
101
95
|
.map((part) => part.text)
|
|
102
96
|
.join("\n");
|
|
103
|
-
return { content, text, isError: result
|
|
97
|
+
return { content, text, isError: result?.isError === true };
|
|
104
98
|
}
|
|
105
99
|
/** What the Python server says it offers, for checking the two lists agree. */
|
|
106
100
|
async listTools() {
|
|
107
|
-
const
|
|
108
|
-
const { tools } = await
|
|
101
|
+
const engine = await this.connect();
|
|
102
|
+
const { tools } = (await engine.request("tools/list", {}, this.config.requestTimeoutMs));
|
|
109
103
|
return tools.map((tool) => tool.name);
|
|
110
104
|
}
|
|
111
105
|
async close() {
|
|
112
106
|
this.starting = undefined;
|
|
113
|
-
const
|
|
114
|
-
this.
|
|
115
|
-
await
|
|
107
|
+
const engine = this.engine;
|
|
108
|
+
this.engine = undefined;
|
|
109
|
+
await engine?.close().catch(() => undefined);
|
|
116
110
|
}
|
|
117
111
|
}
|
|
118
112
|
//# sourceMappingURL=bridge.js.map
|