@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.
Files changed (52) hide show
  1. package/README.md +76 -24
  2. package/SKILL.md +8 -10
  3. package/lib/app.d.ts +14 -0
  4. package/lib/app.js +138 -0
  5. package/lib/app.js.map +1 -0
  6. package/lib/bridge.d.ts +6 -6
  7. package/lib/bridge.js +40 -46
  8. package/lib/bridge.js.map +1 -1
  9. package/lib/config.d.ts +6 -4
  10. package/lib/config.js +19 -25
  11. package/lib/config.js.map +1 -1
  12. package/lib/engine.d.ts +44 -0
  13. package/lib/engine.js +127 -0
  14. package/lib/engine.js.map +1 -0
  15. package/lib/errors.d.ts +0 -3
  16. package/lib/errors.js +0 -3
  17. package/lib/errors.js.map +1 -1
  18. package/lib/index.d.ts +10 -6
  19. package/lib/index.js +17 -104
  20. package/lib/index.js.map +1 -1
  21. package/lib/npx.d.ts +2 -0
  22. package/lib/npx.js +14 -0
  23. package/lib/npx.js.map +1 -0
  24. package/lib/tools/index.d.ts +20 -11
  25. package/lib/tools/index.js +6 -2
  26. package/lib/tools/index.js.map +1 -1
  27. package/lib/tools/kit.d.ts +26 -24
  28. package/lib/tools/kit.js +68 -44
  29. package/lib/tools/kit.js.map +1 -1
  30. package/lib/version.d.ts +2 -0
  31. package/lib/version.js +4 -0
  32. package/lib/version.js.map +1 -0
  33. package/package.json +5 -5
  34. package/lib/cli.d.ts +0 -105
  35. package/lib/cli.js +0 -644
  36. package/lib/cli.js.map +0 -1
  37. package/lib/safety.d.ts +0 -27
  38. package/lib/safety.js +0 -70
  39. package/lib/safety.js.map +0 -1
  40. package/lib/server.d.ts +0 -10
  41. package/lib/server.js +0 -38
  42. package/lib/server.js.map +0 -1
  43. package/src/apple_photos_mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  44. package/src/apple_photos_mcp/__pycache__/__main__.cpython-313.pyc +0 -0
  45. package/src/apple_photos_mcp/__pycache__/config.cpython-313.pyc +0 -0
  46. package/src/apple_photos_mcp/__pycache__/doctor.cpython-313.pyc +0 -0
  47. package/src/apple_photos_mcp/__pycache__/library.cpython-313.pyc +0 -0
  48. package/src/apple_photos_mcp/__pycache__/previews.cpython-313.pyc +0 -0
  49. package/src/apple_photos_mcp/__pycache__/safety.cpython-313.pyc +0 -0
  50. package/src/apple_photos_mcp/__pycache__/search.cpython-313.pyc +0 -0
  51. package/src/apple_photos_mcp/__pycache__/server.cpython-313.pyc +0 -0
  52. 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
- An MCP server is charged on every turn; a CLI costs nothing until it is called.
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
- The `tools/list` payload for these 13 tools is about **2,800 tokens**, plus the
109
- server instructions, on every turn of every conversation whether you use it or
110
- not. The CLI is free until you run something, which is why both exist.
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-) | All 13 |
124
- | 7 | [Working safely](#7-working-safely-) | What asks, what does not |
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-) | Every setting |
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 `confirm: true` |
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.** It needs `confirm: true`. 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.
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 sends its full tool list to the model on every turn, and the CLI costs nothing until it runs.
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. This Apple Photos MCP server is one piece of that system.
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
- | [mcp](https://github.com/modelcontextprotocol/python-sdk) | MIT | The MCP protocol implementation |
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 [NM Media](https://navid.media). Made with ❤️ by [Navid Moazzez](https://navid.me?utm_source=github&utm_medium=referral&utm_campaign=apple-photos-mcp-cli&utm_content=readme).
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 when the photo lives only in iCloud, because it reads the
101
- thumbnail Apple already stored on this Mac rather than downloading the original.
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 requires `confirm: true`. Call it without
129
- confirm to see what it would do, then again with confirm once the user has
130
- actually asked for those specific items to go.
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 using the official
17
- * client, so the Python needs no changes and there is one implementation of
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 client?;
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 the protocol default of 60s
48
- * cancels work the engine is still doing.
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 using the official
17
- * client, so the Python needs no changes and there is one implementation of
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 { Client } from "@modelcontextprotocol/sdk/client/index.js";
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
- client;
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.client)
34
- return this.client;
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 client = new Client({ name: "apple-photos-cli", version: "1.0.0" }, { capabilities: {} });
39
- const transport = new StdioClientTransport({
38
+ const engine = new StdioEngine({
40
39
  command: this.config.pythonCommand,
41
40
  args: this.config.pythonArgs,
42
- env: { ...process.env, ...this.config.pythonEnv },
43
- stderr: "pipe",
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
- // The default is 60s, and a cold `uv` run builds pyobjc, which takes
64
- // longer than that on a fresh cache. Failing there reported "install
65
- // uv" at the one moment uv was working correctly.
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.client = client;
73
- return client;
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 the protocol default of 60s
87
- * cancels work the engine is still doing.
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 client = await this.connect();
84
+ const engine = await this.connect();
91
85
  let result;
92
86
  try {
93
- result = (await client.callTool({ name: tool, arguments: args }, undefined, { timeout: timeoutMs ?? this.config.requestTimeoutMs }));
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.content ?? [];
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.isError === true };
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 client = await this.connect();
108
- const { tools } = await client.listTools();
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 client = this.client;
114
- this.client = undefined;
115
- await client?.close().catch(() => undefined);
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