@thenavidm/apple-photos-mcp-cli 1.1.1 → 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 +56 -25
- 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/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 +3 -4
- 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 |
|
|
@@ -108,10 +111,10 @@ difference is when the model pays for them. Measured in Claude Code:
|
|
|
108
111
|
|
|
109
112
|
| | MCP server | CLI |
|
|
110
113
|
|---|---|---|
|
|
111
|
-
| Every message, with every tool loaded | 3,
|
|
112
|
-
| Every message, Claude Code's default |
|
|
114
|
+
| Every message, with every tool loaded | 3,400 tokens | nothing |
|
|
115
|
+
| Every message, Claude Code's default | 650 tokens | nothing |
|
|
113
116
|
| When Apple Photos comes up | nothing more, or the tools it picks | 2,500 tokens for `SKILL.md`, once |
|
|
114
|
-
| 20 messages with Apple Photos in 1, every tool loaded |
|
|
117
|
+
| 20 messages with Apple Photos in 1, every tool loaded | 68,000 tokens | 2,500 tokens |
|
|
115
118
|
|
|
116
119
|
Claude Code's [tool search](https://code.claude.com/docs/en/mcp#scale-with-mcp-tool-search)
|
|
117
120
|
is on by default: it sends only the tool names and the server instructions,
|
|
@@ -124,11 +127,28 @@ To spend less, turn the server off when you are not using it, which in Claude
|
|
|
124
127
|
Code is the `/mcp` panel. `APPLE_PHOTOS_READ_ONLY=1` takes the 6 write tools off the list, leaving 7.
|
|
125
128
|
Or install the CLI and add the server on the days it earns its place.
|
|
126
129
|
|
|
127
|
-
Measured on 2026-
|
|
128
|
-
short prompt with and without the server connected, once with
|
|
129
|
-
`ENABLE_TOOL_SEARCH=false` and once with the default, the difference read
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
132
152
|
|
|
133
153
|
## Contents
|
|
134
154
|
|
|
@@ -141,11 +161,11 @@ apps and models count tokens a little differently.
|
|
|
141
161
|
| 3 | [Setup](#3-setup-) | One permission, once |
|
|
142
162
|
| 4 | [Connect your client](#4-connect-your-client-) | Every client, copy and paste |
|
|
143
163
|
| 5 | [Check it worked](#5-check-it-worked-) | `doctor` |
|
|
144
|
-
| 6 | [Tools](#6-tools
|
|
145
|
-
| 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 |
|
|
146
166
|
| 8 | [What Apple Photos actually does](#8-what-apple-photos-actually-does-) | The things that surprise people |
|
|
147
167
|
| 9 | [Your data](#9-your-data-) | What is stored, and where |
|
|
148
|
-
| 10 | [Configuration](#10-configuration
|
|
168
|
+
| 10 | [Configuration](#10-configuration-%EF%B8%8F) | Every setting |
|
|
149
169
|
| 11 | [Troubleshooting](#11-troubleshooting-) | When something breaks |
|
|
150
170
|
| 12 | [FAQ](#12-faq-) | Start here if you are new |
|
|
151
171
|
|
|
@@ -345,13 +365,11 @@ Or just ask your agent: **"run doctor on apple photos"**.
|
|
|
345
365
|
|
|
346
366
|
Healthy output includes a line like:
|
|
347
367
|
|
|
348
|
-
```json
|
|
349
|
-
{
|
|
350
|
-
"check": "index",
|
|
351
|
-
"ok": true,
|
|
352
|
-
"detail": "37129 assets indexed (35983 with ML labels, 10807 with readable text)"
|
|
353
|
-
}
|
|
354
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.
|
|
355
373
|
|
|
356
374
|
The two things that actually fail:
|
|
357
375
|
|
|
@@ -376,7 +394,7 @@ The two things that actually fail:
|
|
|
376
394
|
| Tool | What it does |
|
|
377
395
|
|---|---|
|
|
378
396
|
| `library_stats` | Totals, albums, named people, how much is iCloud only |
|
|
379
|
-
| `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 |
|
|
380
398
|
|
|
381
399
|
**Organizing**
|
|
382
400
|
|
|
@@ -387,7 +405,7 @@ The two things that actually fail:
|
|
|
387
405
|
| `add_keywords` | Add keywords, keeping existing ones |
|
|
388
406
|
| `set_photo_title` | Set one item's title |
|
|
389
407
|
| `set_photo_description` | Set one item's caption |
|
|
390
|
-
| `archive_photos` | Move to an archive album. Needs
|
|
408
|
+
| `archive_photos` | Move to an archive album. Needs confirming |
|
|
391
409
|
|
|
392
410
|
**Getting files out**
|
|
393
411
|
|
|
@@ -401,11 +419,11 @@ Organizing works out of the box, because that is the point of the tool. Three th
|
|
|
401
419
|
|
|
402
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.
|
|
403
421
|
|
|
404
|
-
**`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.
|
|
405
423
|
|
|
406
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.
|
|
407
425
|
|
|
408
|
-
`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.
|
|
409
427
|
|
|
410
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.
|
|
411
429
|
|
|
@@ -449,7 +467,13 @@ Every setting is an environment variable, set in your client's `env` block.
|
|
|
449
467
|
| Variable | Default | What it does |
|
|
450
468
|
|---|---|---|
|
|
451
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 |
|
|
452
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 |
|
|
453
477
|
| `APPLE_PHOTOS_LIBRARY` | last opened | Path to a specific `.photoslibrary` |
|
|
454
478
|
| `APPLE_PHOTOS_EXPORT_DIR` | `~/Downloads/Photos Exports` | Default export folder |
|
|
455
479
|
| `APPLE_PHOTOS_PREVIEW_DIR` | `~/.apple-photos-mcp/previews` | Where previews are cached |
|
|
@@ -457,6 +481,11 @@ Every setting is an environment variable, set in your client's `env` block.
|
|
|
457
481
|
| `APPLE_PHOTOS_PREVIEW_MAX` | `8` | Most items rendered per `look_at_photos` call |
|
|
458
482
|
| `APPLE_PHOTOS_WRITE_BATCH_MAX` | `100` | Most items a single write may touch |
|
|
459
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 |
|
|
460
489
|
|
|
461
490
|
## 11. Troubleshooting 🔧
|
|
462
491
|
|
|
@@ -607,7 +636,9 @@ If this is useful, star the repo and come say hi on [X](https://x.com/thenavidm)
|
|
|
607
636
|
|
|
608
637
|
| Package | License | Why |
|
|
609
638
|
|---|---|---|
|
|
610
|
-
| [
|
|
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 |
|
|
611
642
|
| [osxphotos](https://github.com/RhetTbull/osxphotos) | MIT | Reads the Photos library database and Apple's on-device ML metadata |
|
|
612
643
|
| [photoscript](https://github.com/RhetTbull/photoscript) | MIT | Drives Photos.app for writes |
|
|
613
644
|
|
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
|
package/lib/bridge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src-ts/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src-ts/bridge.ts"],"names":[],"mappings":"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"}
|
package/lib/config.d.ts
CHANGED
|
@@ -13,11 +13,13 @@ export type Config = {
|
|
|
13
13
|
/** Per-call deadline. Exports pull originals out of iCloud, so it is generous. */
|
|
14
14
|
requestTimeoutMs: number;
|
|
15
15
|
pythonEnv: Record<string, string>;
|
|
16
|
-
readOnly: boolean;
|
|
17
|
-
allowDestructive: boolean;
|
|
18
|
-
auditPath?: string;
|
|
19
16
|
exportDir?: string;
|
|
20
17
|
};
|
|
21
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The settings, read from `env`. Read-only mode, the switch for archiving and
|
|
20
|
+
* the audit log are Slipway's now; the engine reads the same variables itself,
|
|
21
|
+
* from the environment it inherits, and keeps its own guard.
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
|
|
22
24
|
/** Every variable the code reads, so --help, the README and the tests agree. */
|
|
23
25
|
export declare const ENV_VARS: readonly ["APPLE_PHOTOS_PYTHON", "APPLE_PHOTOS_PYTHONPATH", "APPLE_PHOTOS_STARTUP_TIMEOUT_MS", "APPLE_PHOTOS_REQUEST_TIMEOUT_MS", "APPLE_PHOTOS_READ_ONLY", "APPLE_PHOTOS_ALLOW_DESTRUCTIVE", "APPLE_PHOTOS_AUDIT_LOG", "APPLE_PHOTOS_EXPORT_DIR", "APPLE_PHOTOS_LIBRARY", "APPLE_PHOTOS_PREVIEW_DIR", "APPLE_PHOTOS_PREVIEW_PX", "APPLE_PHOTOS_PREVIEW_MAX", "APPLE_PHOTOS_WRITE_BATCH_MAX", "APPLE_PHOTOS_ARCHIVE_ALBUM"];
|